Power Assert を Rust で作ってみた

素敵な表示をしてくれるassertionライブラリ、Power Assert を Rust でも実現してみました!!!

Assertion failure 時にこんな感じに分かりやすくメッセージ出力してくれます。使い方は GitHub参照ください。

$ cargo run --example test[master]
   Compiling power-assert v0.1.0 (file:///home/nksm/repos/gifnksm/power-assert-rs)
     Running `target/debug/examples/test`
power_assert!(bar.val == bar.foo.val)
              |   |   |  |   |   |
              |   3   |  |   |   2
              |       |  |   Foo { val: 2 }
              |       |  Bar { val: 3, foo: Foo { val: 2 } }
              |       false
              Bar { val: 3, foo: Foo { val: 2 } }
thread '<main>' panicked at 'assertion failed: bar.val == bar.foo.val', examples/test.rs:26
An unknown error occurred

To learn more, run the command again with --verbose.

とりあえず動くところまでいったという段階でバグはいっぱいあると思います。使ってみて気づいた点があれば Issue を立てるか、 PullReq いただけると大変うれしいです。

技術的な話

Rust のコンパイラプラグインを利用して、power_assert!() マクロに与えられた引数の AST を変換して、式中に登場する値を表示できるようにしています。 例えば、

power_assert!(bar.val == bar.foo.val);

という式は、以下のように変換されます。

let mut vals = vec!();
let cond =
    {
        let expr =
            {
                vals.push((0i32, format!("{:?}" , bar)));
                vals.push((4i32, format!("{:?}" , bar.val)));
                bar.val
            } ==
                {
                    vals.push((11i32, format!("{:?}" , bar)));
                    vals.push((15i32, format!("{:?}" , bar.foo)));
                    vals.push((19i32, format!("{:?}" , bar.foo.val)));
                    bar.foo.val
                };
        vals.push((8i32, format!("{:?}" , expr)));
        expr
    };
if !cond {
    use std::io::{self, Write};
    fn width(s: &str) -> i32 { s.len() as i32 }
    fn align<T: Write>(writer: &mut T, cur: &mut i32, col: i32, s: &str) {
        while *cur < col { let _ = write!(writer , " "); *cur += 1; }
        let _ = write!(writer , "{}" , s);
        *cur += width(s);
    }
    {
        vals.sort();
        let mut err = io::stderr();
        let _ =
            writeln!(err , "{}{}{}" , "power_assert!(" ,
                     "bar.val == bar.foo.val" , ")");
        {
            let mut cur = 0;
            for &(c, _) in &vals {
                align(&mut err, &mut cur, 14i32 + c, "|");
            }
            let _ = writeln!(err , "");
        }
        while !vals.is_empty() {
            let mut cur = 0;
            let mut i = 0;
            while i < vals.len() {
                if i == vals.len() - 1 ||
                       vals[i].0 + width(&vals[i].1) < vals[i + 1].0 {
                    align(&mut err, &mut cur, 14i32 + vals[i].0,
                          &vals[i].1);
                    let _ = vals.remove(i);
                } else {
                    align(&mut err, &mut cur, 14i32 + vals[i].0, "|");
                    i += 1;
                }
            }
            let _ = writeln!(err , "");
        }
    }
    panic!(concat ! ( "assertion failed: " , "bar.val == bar.foo.val" ))
}

いろいろと汚いことやっていますが、変換処理自体はツリーを再帰的に見ていくなどしているだけでシンプルです。syntax::fold::Folder のデフォルト実装を利用することで、AST を渡り歩くこと自体は結構簡単にできる感じです。とはいえ、AST 関連は全然ドキュメントが整備されていないので、慣れるまではなかなか思った通りのコードを書けませんでしたが。。。

ランタイムの依存関係が不要になるように、メッセージ表示のための処理が個々の assert に対して一つずつ定義されるようになっており、コード量は結構多くなっています。plugin から crate の root に何かを追加することが可能ならば解決できるのですが、やり方が分かりませんでした。

なお、AST 変換で実現している関係上、Debug が実装されていない値に対して format!("{:?}", ...) を実行してしまいコンパイルが通らなくなってしまう場合があります。これは仕組み上どうしようもないかもしれませんが、なんとか改善したいところではあります。