🔖

PHPでかんたんな自作言語のコンパイラを書いた

2022/07/10に公開

ブログに書いていたものを引っ越してきて手直ししました。元の記事公開日は 2020-09-18 です。


かんたんな自作言語のコンパイラをいろんな言語で書いてみるシリーズ 8番目の言語は PHP です。


ライフゲームのコンパイルが通ったのでヨシ、という程度の雑なものです。

できたもの

https://github.com/sonota88/vm2gol-v2-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 ...

移植元

https://github.com/sonota88/vm2gol-v2

<自作言語処理系の説明用テンプレ>
自分がコンパイラ実装に入門するために作った素朴なトイ言語とその処理系です。簡単に概要を書くと下記のような感じ。

<説明用テンプレおわり>

大元は 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;
  }
}

参考

この記事を読んだ人は(ひょっとしたら)こちらも読んでいます


https://qiita.com/sonota88/items/1482e236054ad3edfdf5


https://qiita.com/sonota88/items/79dd2b0c1dae776c56d9


https://memo88.hatenablog.com/entry/2020/08/19/065056

Discussion