🔖
PHPでかんたんな自作言語のコンパイラを書いた
ブログに書いていたものを引っ越してきて手直ししました。元の記事公開日は 2020-09-18 です。
かんたんな自作言語のコンパイラをいろんな言語で書いてみるシリーズ 8番目の言語は PHP です。
ライフゲームのコンパイルが通ったのでヨシ、という程度の雑なものです。
できたもの
サイズはこんな感じ:
$ wc -l {lexer,parser,codegen}.php lib/utils.php
66 lexer.php
536 parser.php
423 codegen.php
42 lib/utils.php
1067 合計
動かし方の例
echo '
func add(a, b) {
return a + b;
}
func main() {
call add(1, 2);
}
' | php lexer.php | php parser.php | php codegen.php
# ↓アセンブリが出力される
call main
exit
label add
push bp
cp sp bp
cp [bp:2] reg_a
push reg_a
cp [bp:3] reg_a
push reg_a
pop reg_b
pop reg_a
add_ab
cp bp sp
pop bp
ret
label main
push bp
cp sp bp
cp 2 reg_a
push reg_a
cp 1 reg_a
push reg_a
_cmt call~~add
call add
add_sp 2
cp bp sp
pop bp
ret
# ... snip ...
移植元
<自作言語処理系の説明用テンプレ>
自分がコンパイラ実装に入門するために作った素朴なトイ言語とその処理系です。簡単に概要を書くと下記のような感じ。
- 小規模: コンパイラ部分は 1,000 行程度
- pure Ruby / 標準ライブラリ以外のライブラリ不要
- x86風の自作VM向けにコンパイルする
- ライフゲームのために必要な機能だけ
- 変数宣言、代入、反復、条件分岐、関数定義
- 演算子:
+
,*
,==
,!=
のみ(優先順位は(
)
で明示) - 型なし(値は符号付き整数のみ)
- 作ったときに書いた備忘記事
-
本体には含めていない後付けの機能など
- 真偽値リテラル / break / if/else / 単項マイナス / Racc などを使って書いたパーサの別実装
-
他言語への移植
- コンパイラ部分のみ
- Python, Java, TypeScript など、2021-12-19 の時点では 20言語
-
セルフホスト版
- 作り方は製作メモに全部書いています。凝ったことはしていないので Ruby 知らない人でも雰囲気くらいは分かるんじゃないかと。
<説明用テンプレおわり>
大元は Ruby 版なのですが、実質的には Perl版からの移植です。
ベースになっているバージョンは tag:50 のあたり。
(追記 2022-07-10: ステップ62 の修正まで適用しました)
メモ
- PHP を書くのは今回が初めて
- さらっと文法を見た感じ Perl に近そうだったので、Perl版をコピーして書き換えていった
- 割と機械的に置き換え
- my を消して
- sub => function
- elsif => elseif
- 正規表現はダブルクォートで囲んで preg_match にして
- ……などなど
- Perl → PHP という流れで進んだのは良かった
- 割と機械的に置き換え
- Emacs に php-mode を追加するのがめんどくさかったので perl-mode で書いてた
- クラスも普通に使えるし、 int と string の型の判別もできるし、 Perl より楽ちんという印象。正直なところあまり書くことがないです……。
今回の実験コーナー。
これまで Dart への移植のときに set を不要にし、Java への移植のときに call を不要にし、Perl への移植のときに call_set を不要にしてきました。
これらを全部適用するとどうなるか trial ブランチでやってみました。
とりあえず parse_stmt()
の分岐のとこだけ貼ってみます。
<?php
# ...
# if ($t->str_eq("set" )) { return parse_set(); }
# if ($t->str_eq("call" )) { return parse_call(); }
# elseif ($t->str_eq("call_set")) { return parse_call_set(); }
elseif ($t->str_eq("return" )) { return parse_return(); }
elseif ($t->str_eq("while" )) { return parse_while(); }
elseif ($t->str_eq("case" )) { return parse_case(); }
elseif ($t->str_eq("_cmt" )) { return parse_vm_comment(); }
else {
if (
$t->kind_eq("ident")
&& peek(1)->is("sym", "=")
) {
if (
peek(2)->kind_eq("ident")
&& peek(3)->is("sym", "(")
) {
return parse_call_set();
} else {
return parse_set();
}
} elseif (
$t->kind_eq("ident")
&& peek(1)->is("sym", "(")
) {
return parse_call();
} else {
throw not_yet_impl($t);
}
}
個別に試していたときはそれぞれの個別のことだけ考えてやればよかったのが、3つを同時に満たそうとして else に押し込めてとりあえずこうなった、というものです。ちょっと野暮ったいですが peek(n)
で先読みしてやればなんとかなるようです。
funcall を expr に含めるとかするともうちょっとすっきりする気がします。
vgコードはこんな感じ。ここまでやるとかなり普通の言語っぽい(というか JavaScript っぽい)見た目になりますね。
func main() {
var w = 5; // 盤面の幅
var h = 5; // 盤面の高さ
// 初期状態の設定
vram_set(w, 1, 0, 1);
vram_set(w, 2, 1, 1);
vram_set(w, 0, 2, 1);
vram_set(w, 1, 2, 1);
vram_set(w, 2, 2, 1);
var gen_limit = 0;
var gen = 1;
while (gen != gen_limit) {
make_next_gen(w, h);
replace_with_buf();
gen = gen + 1;
}
}
参考
この記事を読んだ人は(ひょっとしたら)こちらも読んでいます
Discussion