RustのマクロだけでBASICっぽい何かを作った

  • 15
    いいね
  • 0
    コメント

書くまでの流れ

ふとTiny BASICを書きたいと思った
ちょうど達人プログラマーを読んで、ミニ言語を作れ、というTipがあったからかもしれない
Rebuild.fmではそんなのやめとけ!と言われていたような気もするけれど、ちょっとやってみるか!

どうやらパーサコンビネータ(Geal/nomMarwes/combine)を使うといい感じらしい

全然知識は無いが、いい勉強になりそう
nomのサンプルを動かしたり、Qiitaの記事(Rustのパーサコンビネータnomを使ってみよう)を真似する

nomで使う大量のマクロに面食らう
これ、もうミニ言語、もとい、DSLじゃねーか!

上記の記事を書いている人が、Rustのマクロを覚えるも書いていた
なので、マクロに体を慣らそうと考える

Rustのマクロ、薄々気づいてはいたけどめっちゃ強力だな (Lispの影響なのだろうか?)
BASIC っぽいなにかマクロでいけるんじゃないか???

できたもの

BASICっぽいものができました
以下のものが実行できます

#[allow(non_snake_case)]
#[allow(unused_assignments)]
fn main()
{
    interpret_basic!{
        PRINT "Hello, world!\n";

        LET A = 1 + 1;
        LET B = A + 2;
        PRINT "A = ", A, ", ";
        PRINT "B = ", B;
        PRINT ;

        LET A = 2;
        IF (A == 2) {
            PRINT "The condition is matched !\n";
            PRINT "A is 2\n";
        }

        FOR A = 1 TO 10 {
            PRINT A, ", ";
        }
        PRINT;

        LET_MUT N = 0;
        PRINT "N = ";
        INPUT N;

        PRINT "Your input number is ", N, "\n";

        FN FIZZ_BUZZ(N) {
            PRINT "It is ";

            IF ((N % 15) == 0) {
                PRINT "FizzBuzz\n";
            } ELSE {
                IF ((N % 3) == 0) {
                    PRINT "Fizz\n";
                } ELSE {
                    IF ((N % 5) == 0) {
                        PRINT "Buzz\n";
                    } ELSE {
                        PRINT N, "\n";
                    }
                }
            }
        }

        FIZZ_BUZZ(N);

        FN FIBONACCI(N) {
            IF ((N == 0) || (N == 1)) {
                RETURN N;
            }

            LET_MUT X = 0;
            LET_MUT Y = 1;
            LET_MUT F = 0;
            FOR _ = 2 TO (N + 1) {
                F = X + Y;
                X = Y;
                Y = F;
            }

            RETURN F;
        }

        LET F = FIBONACCI(N);
        PRINT N, "th fibonacci number is ", F, "\n";

        PRINT "FIN\n";
    };
}

BASICは変種がたくさんあるようなので、頑張ればBASICって言い張ってもいいかなーと思います
次のものを実装しました

  • 変数(整数型のみ)
  • 外部入力(整数のみ)
  • IF, IF ELSE命令による条件分岐
  • FOR命令によるループ

main関数の中に、FizzBuzzやフィボナッチ数の計算など、サンプルが雑多に書いてあります
関数はRustのクロージャによる実装のため、再帰呼出しできません。
サンプルのフィボナッチがループで実装されているのはそういう理由です。

実装

Github - mopp/CodeGarage/rust/macro_bs.rs

macro_rules! interpret_basic {
    () => {
        ()
    };

    (PRINT ; $( $rest:tt )*) => {
        println!("");
        interpret_basic!($( $rest )*);
    };

    (PRINT $( $args:expr ),+ ; $( $rest:tt )*) => {
        $( print!("{}", $args); )+
        interpret_basic!($( $rest )*);
    };

    (INPUT $var:ident ; $( $rest:tt )*) => {
        $var = {
            // https://github.com/rust-lang/rust/issues/23818
            use std::io::Write;
            std::io::stdout().flush().expect("could not flush the stdout.");

            let mut number_string = String::new();
            std::io::stdin()
                .read_line(&mut number_string)
                .expect("Failed to read line");

            number_string
                .trim()
                .parse::<usize>()
                .expect("The input allows only a number.")
        };
        interpret_basic!($( $rest )*);
    };

    (LET_MUT $var:ident = $val:expr ; $( $rest:tt )*) => {
        let mut $var: usize = $val;
        interpret_basic!($( $rest )*);
    };

    (LET $var:ident = $val:expr ; $( $rest:tt )*) => {
        let $var: usize = $val;
        interpret_basic!($( $rest )*);
    };

    (IF ( $cond:expr ) { $( $inner_if:tt )* } ELSE { $( $inner_else:tt )* } $( $rest:tt )*) => {
        if $cond {
            interpret_basic!($( $inner_if )*);
        } else {
            interpret_basic!($( $inner_else )*);
        }
        interpret_basic!($( $rest )*);
    };

    (IF ( $cond:expr ) { $( $inner:tt )* } $( $rest:tt )*) => {
        if $cond {
            interpret_basic!($( $inner )*);
        }
        interpret_basic!($( $rest )*);
    };

    (FOR $var:pat = $begin:tt TO $end:tt { $( $inner:tt )* } $( $rest:tt )*) => {
        for $var in $begin..$end {
            interpret_basic!($( $inner )*);
        }
        interpret_basic!($( $rest )*);
    };

    ($var:ident = $val:expr ; $( $rest:tt )*) => {
        $var = $val;
        interpret_basic!($( $rest )*);
    };

    (FN $name:ident ( $( $args:ident ),* ) { $( $inner:tt )* } $( $rest:tt )*) => {
        let $name = |$( $args: usize ),*| {
            interpret_basic!($( $inner )*);
        };
        interpret_basic!($( $rest )*);
    };

    (RETURN $value:expr; $( $rest:tt )*) => {
        return $value;
    };

    ($func_name:ident ($( $args:ident ),*) ; $( $rest:tt )*) => {
        $func_name($( $args ),*);
        interpret_basic!($( $rest )*);
    }
}

解説

コードを貼り付けるだけでは面白くないので、簡単な解説を書いてみます
マクロ自体については上記した、Rustのマクロを覚える公式ドキュメントを参考に書きました

命令はセミコロン区切り

まず大きな設定として、セミコロン区切り、ということにしました
そのための基本となるコードは以下のようになります

macro_rules! interpret_basic {
    () => {
        ()
    };

    (PRINT ; $( $rest:tt )*) => {
        println!("");
        interpret_basic!($( $rest )*);
    };
}

一番はじめに実装したPRINT命令を見てみます
matcherが(PRINT ; $( $rest:tt )*)となっており、
引数無しのPRINT命令ですね

セミコロンの次にあるのがsingle token tree, tt0個以上の繰り返しです
マクロをいまいちわかっていなかったので、繰り返しの解釈に少し手こずりました
この場合であれば0個以上のsingle token treeを受け取る、となります
というか、読みづらい

そして、restという名前の通り、PRINT命令が終わったあとの、全ての残りをここで受け取ります
interpret_basic!($( $rest )*);のように再帰的にマクロを展開させます
ここで、interpret_basic!の引数が$( $rest )*となっていますが、これも繰り返しです
繰り返し展開された$restが引数として渡されます

全ての命令を展開し終えたとき、$restは空になります
なので、マクロの先頭に

() => {
    ()
};

という空の展開先を書いています

RETURN命令以外は全て、このように展開していきます
毎回同じことを書いていて、嫌なので、うまいこと直したかった…

先にセミコロン区切りで、ばらしてから書く命令のマッチングをしようと考えたのですが
一意に展開できない、曖昧だ、と、コンパイラにお叱りを受けてだめでした
PRINT命令が2通りあるのも同じエラーのためです

INPUT 命令

これはそのままなので、解説しなくてもいいかと思います
stdoutをフラッシュしてやらないと直前に改行が出力されていなかった場合に挙動がおかしくなるので、フラッシュしています
マクロなど関係なしにRustの問題?みたいです

LET, LET_MUT 命令

これもそのままですね
全部mutにするとコンパイラがうるさいので、mutableかimmutableで分けました

IF, IF ELSE命令

(IF ( $cond:expr ) { $( $inner_if:tt )* } ELSE { $( $inner_else:tt )* } $( $rest:tt )*) => { 
    if $cond {                                                                               
        interpret_basic!($( $inner_if )*);                                                   
    } else {                                                                                 
        interpret_basic!($( $inner_else )*);                                                 
    }                                                                                        
    interpret_basic!($( $rest )*);                                                           
};                                                                                           

(IF ( $cond:expr ) { $( $inner:tt )* } $( $rest:tt )*) => {                                  
    if $cond {                                                                               
        interpret_basic!($( $inner )*);                                                      
    }                                                                                        
    interpret_basic!($( $rest )*);                                                           
};                                                                                           

書いているときは結構苦労したんですが、見返してみるとなんてことないですね
(){}で囲まれている部分を引っこ抜いて、Rustの普通の構文になおしています
そのあと、それぞれを更に再帰展開します
matcherは上からマッチしていくので、IF ELSEを先に書くことに注意です

FOR 命令

(FOR $var:pat = $begin:tt TO $end:tt { $( $inner:tt )* } $( $rest:tt )*) => { 
    for $var in $begin..$end {                                                
        interpret_basic!($( $inner )*);                                       
    }                                                                         
    interpret_basic!($( $rest )*);                                            
};                                                             

FOR命令もIF命令と似たようにRustのforに展開して、ブロック内を再帰展開しているだけになります

FN, RETURN 命令

(FN $name:ident ( $( $args:ident ),* ) { $( $inner:tt )* } $( $rest:tt )*) => { 
    let $name = |$( $args: usize ),*| {                                         
        interpret_basic!($( $inner )*);                                         
    };                                                                          
    interpret_basic!($( $rest )*);                                              
};                                                                              

(RETURN $value:expr; $( $rest:tt )*) => {                                       
    return $value;                                                              
}; 

関数の定義と戻り値を返す部分です
最初はRustの普通の関数に対応付けようと思ったのですが、戻り値型の部分をマクロで展開するのがめんどうだなと思い、クロージャを使いました。
(と、書いていて思ったけど、常に何かしらの整数を返すようにすればよかったのでは…)
引数部分をカンマ区切りの0個以上の繰り返しで表現しているので引数の数は好きにできます

RETURN命令はそのままreturnへの対応付けにしました。
なので、Rustのifの中でもいい感じに動いてくれています。

変数代入/関数呼出し

($var:ident = $val:expr ; $( $rest:tt )*) => { 
    $var = $val;                               
    interpret_basic!($( $rest )*);             
};                                             

($func_name:ident ($( $args:ident ),*) ; $( $rest:tt )*) => { 
    $func_name($( $args ),*);                                 
    interpret_basic!($( $rest )*);                            
}                                                             

マクロで再帰展開していく都合上書いたもので、特に面白みはないですね

感想

BASIC的なのを作ってみたいな欲が多少満たされた
また、90行くらいのマクロでこんなに見た目が変わるんだなと感じた
しかし、便利ではあるが、可読性やとっつきやすさがだいぶ違うのでtraitなどでできることはそっちでやるべきだと思います

振り返ってみると、Rustの構文に対応付けているだけなので、そんなに面白いものでもなかったかな、と感じてしまい、少しだけ残念
だが、マクロがよくわかっていなかったので、とても良い勉強になった