Rustで自作言語のインタプリタを作った話
リポジトリ
前提
字句解析、構文解析を理解または記事を読んでいることを前提として話します。
そしてインタプリタに関してはウェブの情報を下に作成した独自のものなのでおそらく本来イメージするものではないと思われます。Rustにメモリを生やすというわけ変わらん実装になってます。
字句解析器
構文解析器
インタプリタとは
随時解釈しながら実行するプログラムのことです。直接解釈実行するタイプ、中言語を介するタイプなどがあります。
今回の作成したインタプリタの場合は構文木を直接実行しています。Rustの配列を仮想のメモリとして扱っているので仮想マシンに近い実装なのかなぁと勝手思っています。
そして構文分析器とかなり密結合な設計となっています。
インタプリタで実装したこと
- ミュータブル、イミュータブルな変数
- 関数
- 四則演算
- for
- if
ミュータブル、イミュータブルな変数
変数がミュータブルかイミュータブルかは構文解析時にノードにBool
で設定されています。
参照するために全て仮想メモリ代わりのどこからでも参照できる配列に保存されていきます。
asts::Types::Variable(var) => {
...
matchを使いノードのTypes
がVariable
だった場合中身を取り出しますvar
の中にname
というプロパティがあり、仮想メモリの配列から名前に合致するものを取得します。
関数
関数には関数自体を定義をしているノードCall
と関数を実行しているノードFunction
があります。
関数の定義
関数の定義自体は構文解析で行われており、Function
のノードは全て仮想メモリの配列に保存されています
関数の呼び出し
...
asts::Types::Call(function) => {
let result = vec_function.function_run(function, vec_variable);
match result {
Some(_) => {
return (false, result);
}
None => {
return (true, None);
}
}
...
matchを使いノードのType
がCall
だった場合関数を呼び出す処理に入ります。
vec_variable
は変数が保存されている配列です。
...
let callee = &call_ast.callee;
if callee == "print" {
let value = &call_ast.argument[0];
match value {
asts::Types::Variable(var) => {
let (var_result, _) = variable.search_variable(&var, self);
self.print_var(&var_result[0]);
asts::Types::Binary(_) => {
let result = interpreters::calculation(&value, variable, self);
self.print_var(&result);
_ => {
self.print_var(&value);
}
}
}
...
function_run
で実際の関数を実行しています。
Call
ノードの中にはcallee
という関数名を保存してあるプロパティがありそれがビルトイン関数のprint
だった場合引数のプロパティから値をノードを取り出しRustのprintln!
で実行しています。
他の関数の呼び出しだった場合はFunction
の配列から同じ名前のものを取り出し中身のノードを実行します。
for
Rustのfo分を使いループの処理を行っています。
...
asts::Types::For(fors) => {
vec_variable.vec_push();
vec_function.vec_push();
vec_function.push(fors.node.clone());
let fors = fors::For::new(&fors.init_var, &fors.node);
let result = fors.for_run(vec_variable, vec_function);
vec_variable.last_remove();
vec_function.last_remove
match result {
Some(_) => {
return (false, result);
None => {
return (true, None);
}
}
}
...
matchを使いノードのType
がFor
だった場合関数を呼び出す処理に入ります。
init_var
の中にlet i = 0; i > 5; i++;
のような文ノードが入っており、node
の中にはループ実行するノードが入っています。
...
let variant = self.ast_for[0].clone();
let judge = self.ast_for[1].clone();
let loop_for = self.ast_for[2].clone
let mut result_var = asts::Types::Error(asts::ErrorAST::new("variable error"));
let mut name = "".to_string();
...
loop {
let mut result = asts::Types::Error(asts::ErrorAST::new("judge error"));
match judge.clone() {
asts::Types::Binary(mut bin) => {
let var = self.for_variables(&name, &result_var, bin.node);
bin.node = var;
result = interpreters::calculation(
&asts::Types::Binary(bin),
vec_variable,
vec_function,
);
}
_ => {
let err = error::Error::new(&judge);
err.exit("For judge error");
}
}
...
Rustのloop文を使ってforを回しています。
init_var
のノード実行しつつ条件になればループを抜けます。
if
Rustのif文を使い条件分岐を行っています。
...
let result = calculation(&ifs.judge[0], vec_variable, vec_function);
let mut result_if = None;
vec_variable.vec_push();
vec_function.vec_push();
if !ifs.node.is_empty() {
vec_function.push(ifs.node.clone());
let ifs = ifs::If::new(&result, &ifs.node);
result_if = ifs.if_run(vec_variable, vec_function);
}
vec_variable.last_remove();
vec_function.last_remove();
calculation
関数でjudge
の中に入ってる式を評価してIf::new
に渡しています
pub fn if_run(
self,
vec_variable: &mut variable::Variable,
vec_function: &mut function::Function,
) -> Option<asts::Types> {
match self.result {
asts::Types::Boolean(boolean) => {
if boolean.boolean {
let (_, result) = interpreters::scope(&self.ifs, vec_variable, vec_function);
return result;
}
}
_ => {}
}
return None;
}
式を評価して得られたresult
をif文を使い判定を行っています。
ifs
の中に実行するノードが入っており、それをインタプリタの最初の実行処理に渡しています。
スコープ
スコープは仮想メモリ代わりの配列を2次元配列にすることにより実装しています。
[[変数, 変数]]
例えば初期状態でこのような状態になっている場合でスコープに入った場合。
[[変数, 変数], [変数]]
このように配列が一つ増え、増えた配列に変数が保存されていきます。
[[変数, 変数]]
またスコープを抜けると末尾の配列が削除されます。
まとめ
複雑なのでかなり稚拙な解説になってしまいましたが自作言語作成のきっかけになればいいなぁ。
Discussion