LoginSignup
18
7

More than 3 years have passed since last update.

rustc_driverとrustc_interfaceを使ってみる (2019年5月版)

Posted at

Rustコンパイラと連携するプログラムを書くには主に5つの方法があります。

  • Rustのソースコードを単に生成する。 (lalrpop など)
  • deriveマクロ (serde_derive など)
  • 手続きマクロ (runtime など)
  • コンパイラプラグイン ※nightly限定
  • コンパイラの一部を使った独立ツール ※nightly限定

ここでは最後の「コンパイラの一部を使った独立ツール」を作る方法を考えます。以前はRustのコンパイラフェーズを呼び出す部分は rustc_driver::driver にハードコードされていたため、複雑な処理をする場合はdriverのコードの大部分をコピーする必要がありました。しかし、2019年3月末にマージされた#56732により、コンパイラフェーズの呼び出し部分が rustc_interface として分離されたため、ツールの作成が比較的容易になるはずです。

プロジェクト作成

今回はRustc GuideにあるStupid Statsというサンプルを作ってみます。これはマクロ展開前のASTを処理し、 「println! が使われている回数」と「グローバル関数の引数の個数のヒストグラム」を分析します。

toolstaterustc component historyを参考に、RLSなど主要ツールが揃っている中でできるだけ最新のnightlyを選びます。今回は2019-05-22を使います。

$ cargo new --bin stupid-stats
$ cd stupid-stats
$ echo nightly-2019-05-22 > rust-toolchain
$ rustup component add rustfmt clippy rls rust-analysis rust-src

rustc_driver を使った実装

Stupid Statsのページに記載のものを実装すればOKです。ただし2019-05-26時点のrustc-guideの内容とrustcの実際の構造には以下のような違いがありました。

  • CompilerCalls ではなく Callbacks になっています。
  • 差し込めるコールバックは減っていました。ここでは after_parsing を使います。
  • コールバックはboolを返します。falseを返すと処理を止められるようなので、ここでは after_parsing で必要な処理をしてから false を返します。
  • rustc_driver::run_compiler に第3引数と第4引数があります。 None でうまくいきそうなので None を渡しておきます。
  • ast::Item_ast::ItemKind になっていました。
  • ast::Mac_ はenumではなくなっていました。

コンパイラ自身のrustdocは https://doc.rust-lang.org/nightly/nightly-rustc/rustc_driver/index.html で公開されているので、必要ならこれを参照しながらプログラムを書くとよいでしょう。 (ただし、使っているnightlyのバージョンとドキュメントの元になったnightlyバージョンが異なる可能性に注意が必要)

main.rs
// Rustコンパイラ自身を使うので必要
#![feature(rustc_private)]

extern crate rustc_driver;
extern crate rustc_interface;
extern crate syntax;

use rustc_driver::Callbacks;
use rustc_interface::interface;
use syntax::{ast, visit};

fn main() {
    let args: Vec<_> = std::env::args().collect();
    rustc_driver::run_compiler(&args, &mut StupidCallbacks, None, None).unwrap();
}

struct StupidCallbacks;

impl Callbacks for StupidCallbacks {
    fn after_parsing(&mut self, compiler: &interface::Compiler) -> bool {
        // 既にパースした結果のキャッシュを取り出す
        let cratename = compiler.crate_name().unwrap().peek().clone();
        let krate = compiler.parse().unwrap().peek();

        // ASTをトラバースする
        let mut visitor = StupidVisitor::new();
        visit::walk_crate(&mut visitor, &krate);

        // 結果を出力
        println!("In crate: {},\n", cratename);
        println!("Found {} uses of `println!`;", visitor.println_count);

        let (common, common_percent, four_percent) = visitor.compute_arg_stats();
        println!(
            "The most common number of arguments is {} ({:.0}% of all functions);",
            common, common_percent
        );
        println!(
            "{:.0}% of functions have four or more arguments.",
            four_percent
        );

        // falseを投げると以降の処理を中止できる
        false
    }
}

struct StupidVisitor {
    println_count: usize,
    arg_counts: Vec<usize>,
}

impl StupidVisitor {
    fn new() -> Self {
        Self {
            println_count: 0,
            arg_counts: Vec::new(),
        }
    }

    fn increment_args(&mut self, args: usize) {
        if self.arg_counts.len() <= args {
            self.arg_counts.resize(args + 1, 0);
        }
        self.arg_counts[args] += 1;
    }

    fn compute_arg_stats(&self) -> (usize, f64, f64) {
        let iter = || self.arg_counts.iter().copied();
        let sum = iter().sum::<usize>();
        let sum4 = iter().skip(4).sum::<usize>();
        let max = iter().max().unwrap_or(0);
        let max_idx = iter().position(|x| x == max).unwrap_or(0);
        (
            max_idx,
            max as f64 / sum as f64 * 100.0,
            sum4 as f64 / sum as f64 * 100.0,
        )
    }
}

// visit::Visitor を実装することでASTのトラバースができる
impl<'v> visit::Visitor<'v> for StupidVisitor {
    fn visit_item(&mut self, i: &'v ast::Item) {
        match i.node {
            ast::ItemKind::Fn(ref decl, _, _, _) => {
                self.increment_args(decl.inputs.len());
            }
            _ => {}
        }

        // 続けて、再帰的にスキャンしたいときは `walk_*` を呼ぶ
        visit::walk_item(self, i)
    }

    fn visit_mac(&mut self, mac: &'v ast::Mac) {
        if mac.node.path.to_string() == "println" {
            self.println_count += 1;
        }

        // 続けて、再帰的にスキャンしたいときは `walk_*` を呼ぶ
        visit::walk_mac(self, mac)
    }
}

実行してみる

driver を使って作ったツールは rustc と同様の引数をとるので、自分自身を引数として渡してみます。

$ cargo run src/main.rs
   Compiling stupid-stats v0.1.0 (/home/qnighy/workdir/stupid-stats)
    Finished dev [unoptimized + debuginfo] target(s) in 6.24s
     Running `target/debug/stupid-stats src/main.rs`
In crate: main,

Found 4 uses of `println!`;
The most common number of arguments is 0 (100% of all functions);
0% of functions have four or more arguments.

rustc_interface を使った実装

rustc_driver はあくまで rustc コマンドの仕組みがベースにあり、そこにフックを差し込む形で利用するものでした。より主体的に rustc 内部のコンパイラフェーズを利用したい場合は rustc_interface を直接呼びます。

main.rs
// Rustコンパイラ自身を使うので必要
#![feature(rustc_private)]

extern crate rustc;
extern crate rustc_interface;
extern crate syntax;

use rustc::session::config::Input;
use rustc::session::DiagnosticOutput;
use rustc_interface::interface;
use syntax::{ast, visit};

fn main() {
    // コマンドライン引数を自分で解析する
    // rustc_driverにコマンドライン引数用のヘルパーもあるので、
    // rustcコマンドに近づけるならそれを使う手もある
    let input: std::path::PathBuf = std::env::args().nth(1).unwrap().into();

    // 今回はパースするだけなのであまりオプションは必要ない。
    // ほぼデフォルトで組み立てる
    let config = interface::Config {
        opts: Default::default(),
        crate_cfg: Default::default(),
        input: Input::File(input.clone()),
        input_path: Some(input.clone()),
        output_file: None,
        output_dir: None,
        file_loader: None,
        diagnostic_output: DiagnosticOutput::Default,
        stderr: None,
        crate_name: None,
        lint_caps: Default::default(),
    };

    // コンパイラのためのリソース (スレッドプールやアロケータ) を起動する
    interface::run_compiler(config, |compiler| {
        // クレート名の取得とパースをする
        let cratename = compiler.crate_name().unwrap().peek().clone();
        let krate = compiler.parse().unwrap().peek();

        // ASTをトラバースする
        let mut visitor = StupidVisitor::new();
        visit::walk_crate(&mut visitor, &krate);

        // 結果を出力
        println!("In crate: {},\n", cratename);
        println!("Found {} uses of `println!`;", visitor.println_count);

        let (common, common_percent, four_percent) = visitor.compute_arg_stats();
        println!(
            "The most common number of arguments is {} ({:.0}% of all functions);",
            common, common_percent
        );
        println!(
            "{:.0}% of functions have four or more arguments.",
            four_percent
        );
    });
}

struct StupidVisitor {
    println_count: usize,
    arg_counts: Vec<usize>,
}

impl StupidVisitor {
    fn new() -> Self {
        Self {
            println_count: 0,
            arg_counts: Vec::new(),
        }
    }

    fn increment_args(&mut self, args: usize) {
        if self.arg_counts.len() <= args {
            self.arg_counts.resize(args + 1, 0);
        }
        self.arg_counts[args] += 1;
    }

    fn compute_arg_stats(&self) -> (usize, f64, f64) {
        let iter = || self.arg_counts.iter().copied();
        let sum = iter().sum::<usize>();
        let sum4 = iter().skip(4).sum::<usize>();
        let max = iter().max().unwrap_or(0);
        let max_idx = iter().position(|x| x == max).unwrap_or(0);
        (
            max_idx,
            max as f64 / sum as f64 * 100.0,
            sum4 as f64 / sum as f64 * 100.0,
        )
    }
}

// visit::Visitor を実装することでASTのトラバースができる
impl<'v> visit::Visitor<'v> for StupidVisitor {
    fn visit_item(&mut self, i: &'v ast::Item) {
        match i.node {
            ast::ItemKind::Fn(ref decl, _, _, _) => {
                self.increment_args(decl.inputs.len());
            }
            _ => {}
        }

        // 続けて、再帰的にスキャンしたいときは `walk_*` を呼ぶ
        visit::walk_item(self, i)
    }

    fn visit_mac(&mut self, mac: &'v ast::Mac) {
        if mac.node.path.to_string() == "println" {
            self.println_count += 1;
        }

        // 続けて、再帰的にスキャンしたいときは `walk_*` を呼ぶ
        visit::walk_mac(self, mac)
    }
}

実行結果は元のものと同じです。

まとめ

  • rustc コマンドをベースに、途中にフックを挟む形で独自コマンドを作るには、 rustc_driver::run_compiler が使えます。
  • より独自のフローでrustcの一部を利用するには、 rustc_interface::run_compiler が使えます。
18
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
7