如何编写生成模块的Rust编译器插件?

时间:2016-08-23 07:20:44

标签: rust rust-compiler-plugin

我正在编写一个扩展

的Rust编译器插件
choose! {
    test_a
    test_b
}

#[cfg(feature = "a")]
mod test_a;
#[cfg(feature = "b")]
mod test_b;

现在几乎完成了,但模块在最终扩展的代码中不包含任何内容。我想原因是跨度不包括模块文件。

use syntax::ast;
use syntax::ptr::P;
use syntax::codemap;
use syntax::parse::token;
use syntax::tokenstream::TokenTree;
use syntax::ext::base::{ExtCtxt, MacResult, DummyResult, MacEager};
use syntax::ext::build::AstBuilder;
use syntax_pos::Span;
use rustc_plugin::Registry;
use syntax::util::small_vector::SmallVector;

// Ideally, it will expand
//
// ```rust
// choose! {
//   test_a
//   test_b
// }
// ```
// to
// ```rust
// #[cfg(feature = "a")]
// mod test_a;
// #[cfg(feature = "b")]
// mod test_b;
// ```
//
// but the modules contain nothing in the expanded code at present

fn choose(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'static> {
    let mut test_mods: SmallVector<P<ast::Item>> = SmallVector::many(vec![]);
    for arg in args {
        let mut attrs = vec![];
        let text = match arg {
            &TokenTree::Token(_, token::Ident(s)) => s.to_string(),
            _ => {
                return DummyResult::any(sp);
            }
        };
        let cfg_str = token::InternedString::new("cfg");
        let feat_str = token::InternedString::new("feature");
        attrs.push(cx.attribute(sp,
                                cx.meta_list(sp,
                                             cfg_str,
                                             vec![cx.meta_name_value(sp,
                                                                     feat_str,
                                                                     ast::LitKind::Str(token::intern_and_get_ident(text.trim_left_matches("test_")), ast::StrStyle::Cooked))])));
        test_mods.push(P(ast::Item {
            ident: cx.ident_of(text.as_str()),
            attrs: attrs,
            id: ast::DUMMY_NODE_ID,
            node: ast::ItemKind::Mod(
                // === How to include the specified module file here? ===
                ast::Mod {
                    inner: codemap::DUMMY_SP,
                    items: vec![],
                }
            ),
            vis: ast::Visibility::Inherited,
            span: sp,
        }))
    }

    MacEager::items(test_mods)
}

#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
    reg.register_macro("choose", choose);
}

Gist

1 个答案:

答案 0 :(得分:1)

2016-08-25更新:使用libsyntax::parse::new_parser_from_source_str以避免手动设置模块路径。 new_parser_from_source_str只会在CWD找到模块,这是意料之外的。

正如@Francis所指出的,模块文件的真实路径可能类似foo/mod.rs, ,我发现了一个名为new_parser_from_source_str的函数,它可以从中创建一个新的解析器一个源字符串,在libsyntax::parse中,所以我决定让编译器为我处理这个案例所以我必须手动处理这个案例。更新的代码:

fn choose(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'static> {
    let mut test_mods = SmallVector::zero();
    let cfg_str = intern("cfg");
    let ftre_str = intern("feature");
    for arg in args {
        let mut attrs = vec![];
        let mod_name = match arg {
            &TokenTree::Token(_, token::Ident(s)) => s.to_string(),
            _ => {
                return DummyResult::any(sp);
            }
        };
        attrs.push(cx.attribute(sp,
                                cx.meta_list(sp,
                                             // simply increase the reference counter
                                             cfg_str.clone(),
                                             vec![cx.meta_name_value(sp,
                                                                     ftre_str.clone(),
                                                                     ast::LitKind::Str(intern(mod_name.trim_left_matches("test_")), ast::StrStyle::Cooked))])));

        let mut mod_path = PathBuf::from(&cx.codemap().span_to_filename(sp));
        let dir = mod_path.parent().expect("no parent directory").to_owned();
        let default_path = dir.join(format!("{}.rs", mod_name.as_str()));
        let secondary_path = dir.join(format!("{}/mod.rs", mod_name.as_str()));
        match (default_path.exists(), secondary_path.exists()) {
            (false, false) => {
                cx.span_err(sp, &format!("file not found for module `{}`", mod_name.as_str()));
                return DummyResult::any(sp);
            }
            (true, true) => {
                cx.span_err(sp, &format!("file for module `{}` found at both {} and {}", mod_name.as_str(), default_path.display(), secondary_path.display()));
                return DummyResult::any(sp);
            }
            (true, false) => mod_path = default_path,
            (false, true) => mod_path = secondary_path,
        }

        test_mods.push(P(ast::Item {
            ident: cx.ident_of(mod_name.as_str()),
            attrs: attrs,
            id: ast::DUMMY_NODE_ID,
            node: ast::ItemKind::Mod(
                ast::Mod {
                    inner: sp,
                    items: expand_include(cx, sp, &mod_path),
                }
            ),
            vis: ast::Visibility::Inherited,
            span: sp,
        }))
    }

    MacEager::items(test_mods)
}

最后我找到了解决方案! \ O /

Rust处理模块文件的过程与include!类似。结果,我查看了宏include!的实现,可以找到它here,并重写它以满足我的需要:

use ::std::path::Path;
use ::std::path::PathBuf;
use syntax::parse::{self, token};
use syntax::errors::FatalError;
macro_rules! panictry {
    ($e:expr) => ({
        match $e {
            Ok(e) => e,
            Err(mut e) => {
                e.emit();
                panic!(FatalError);
            }
        }
    })
}

pub fn expand_include<'cx>(cx: &'cx mut ExtCtxt, sp: Span, file: &Path) -> Vec<P<ast::Item>> {
    let mut p = parse::new_sub_parser_from_file(cx.parse_sess(), cx.cfg(), file, true, None, sp);
    let mut ret = vec![];
    while p.token != token::Eof {
        match panictry!(p.parse_item()) {
            Some(item) => ret.push(item),
            None => {
                panic!(p.diagnostic().span_fatal(p.span,
                                                 &format!("expected item, found `{}`", p.this_token_to_string())))
            }
        }
    }
    ret
}

要从模块文件中获取项目,我们必须找出真正的模块路径:

let mut mod_path = PathBuf::from(&cx.codemap().span_to_filename(sp));
mod_path.set_file_name(mod_name.as_str());
mod_path.set_extension("rs");

然后我们可以构建我们的模块节点:

P(ast::Item {
    ident: cx.ident_of(mod_name.as_str()),
    attrs: attrs,
    id: ast::DUMMY_NODE_ID,
    node: ast::ItemKind::Mod(ast::Mod {
        inner: sp,
        items: expand_include(cx, sp, &mod_path),
    }),
    vis: ast::Visibility::Inherited,
    span: sp,
})

总之,插件应该重写如下:

#![feature(plugin_registrar, rustc_private)]

extern crate syntax;
extern crate rustc_plugin;

use syntax::ast;
use syntax::ptr::P;
use syntax::codemap::Span;
use syntax::parse::{self, token};
use syntax::tokenstream::TokenTree;
use syntax::ext::base::{ExtCtxt, MacResult, DummyResult, MacEager};
use syntax::errors::FatalError;
use syntax::ext::build::AstBuilder;
use rustc_plugin::Registry;
use syntax::util::small_vector::SmallVector;

use ::std::path::Path;
use ::std::path::PathBuf;

macro_rules! panictry {
    ($e:expr) => ({
        match $e {
            Ok(e) => e,
            Err(mut e) => {
                e.emit();
                panic!(FatalError);
            }
        }
    })
}

pub fn expand_include<'cx>(cx: &'cx mut ExtCtxt, sp: Span, file: &Path) -> Vec<P<ast::Item>> {
    let mut p = parse::new_sub_parser_from_file(cx.parse_sess(), cx.cfg(), file, true, None, sp);
    let mut ret = vec![];
    while p.token != token::Eof {
        match panictry!(p.parse_item()) {
            Some(item) => ret.push(item),
            None => {
                panic!(p.diagnostic().span_fatal(p.span,
                                                 &format!("expected item, found `{}`", p.this_token_to_string())))
            }
        }
    }
    ret
}

fn intern(s: &str) -> token::InternedString {
    token::intern_and_get_ident(s)
}

fn choose(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'static> {
    let mut test_mods = SmallVector::zero();
    let cfg_str = intern("cfg");
    let feat_str = intern("feature");
    for arg in args {
        let mut attrs = vec![];
        let mod_name = match arg {
            &TokenTree::Token(_, token::Ident(s)) => s.to_string(),
            _ => {
                return DummyResult::any(sp);
            }
        };
        attrs.push(cx.attribute(sp,
                                cx.meta_list(sp,
                                             // simply increase the reference counter
                                             cfg_str.clone(),
                                             vec![cx.meta_name_value(sp,
                                                                     feat_str.clone(),
                                                                     ast::LitKind::Str(intern(mod_name.trim_left_matches("test_")), ast::StrStyle::Cooked))])));

        let mut mod_path = PathBuf::from(&cx.codemap().span_to_filename(sp));
        mod_path.set_file_name(mod_name.as_str());
        mod_path.set_extension("rs");

        test_mods.push(P(ast::Item {
            ident: cx.ident_of(mod_name.as_str()),
            attrs: attrs,
            id: ast::DUMMY_NODE_ID,
            node: ast::ItemKind::Mod(
                ast::Mod {
                    inner: sp,
                    items: expand_include(cx, sp, &mod_path),
                }
            ),
            vis: ast::Visibility::Inherited,
            span: sp,
        }))
    }

    MacEager::items(test_mods)
}

#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
    reg.register_macro("choose", choose);
}