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