LoginSignup
2
4

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-09-11

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


20年ぶりくらいに Perl のコードを書きました。やっつけなので汚いです。ライフゲームのコンパイルが通ったのでヨシ、という程度の雑なものです。


移植元

自作言語の概要などについてはこちらを参照してください。

ベースになっているバージョン: tag:49 のあたり
(追記 2021-09-11: ステップ60の修正まで適用しました)

作り方はここに全部書いています(Ruby 版のものですが): vm2gol v2 製作メモ

ただ、Ruby 版よりは主に C言語版 を見ながら書き写していました。クラスを使わなかったのもあり、C言語版に近いところも多いです。

メモ

主な部分のサイズ(2021-09-18 現在):

$ wc -l {lexer,parser,codegen}.pl lib/{token,utils,val}.pm 
   64 lexer.pl
  553 parser.pl
  528 codegen.pl
   34 lib/token.pm
   27 lib/utils.pm
   38 lib/val.pm
 1244 合計

  • 20年ぶりくらいといっても、当時はちょっと入門した程度だったしもうほとんど忘れてるので ほぼゼロから再入門といった感じ
  • でも 2日(日曜+月曜)でなんとか完成した。殴り書きです。
  • 配列・ハッシュとリファレンスが絡むあたりが最初よく分からなかった
    • まず配列の要素数の取り方が分からないとか、 for my $x (@$xs) { ... }@ がなくて動かないとか、 たぶん初心者あるある
  • 数と文字列の区別がない(シェルスクリプトっぽい) のをどうするかちょっと悩んだ。 データの型を見て処理を分岐しているところがあるので。
    • ハッシュでラッパーオブジェクトみたいなものを作ってどうにかした( lib/Val.pm )。
    • 最初は C言語版のときの NodeList, NodeItem 構造体を使う書き方にすれば何も考えずに機械的に移植できそうと思ったけど、 あれはあれでリストの処理が煩雑・大げさで、 配列で済むならその方がシンプルなので、最低限文字列と数だけ判別できるようにした。
  • クラスの使い方もちょっと調べてみたけど、 もうちょっと Perl 力が上がった状態で使わないと 逆に回り道っぽくなりそうな気がして、 C言語版の「構造体+関数群のセット」方式でやってしまった

my $foo = ... をいっぱい書かないといけなくてさすがに面倒だったので途中で次のような Emacs Lisp を書いて Ctrl + Alt + M で入力できるようにしたり。yasnippet でも良かったかも。

(add-hook
 'perl-mode-hook
 '(lambda ()
    (local-set-key (kbd "C-M-m")
                   (lambda ()
                     (interactive)
                       (insert "my $ = ;")
                       (backward-char 4)))))

Dart版callJava版set を不要にした流れで、今回は call_set キーワードを不要にしてみました。同じ要領でできますね、ということが分かりました。

(追記 2021-04-09: この修正はいったん revert しましたが、一応 trial ブランチに残してあります。)

sub parse_stmt {
    my $t = peek(0);

    if (Token::is($t, "sym", "}")) {
        return -1;
    }

    if    (Token::str_eq($t, "func"    )) { return parse_func();       }
    elsif (Token::str_eq($t, "var"     )) { return parse_var();        }
    elsif (Token::str_eq($t, "set"     )) { return parse_set();        }
    elsif (Token::str_eq($t, "call"    )) { return parse_call();       }
    # elsif (Token::str_eq($t, "call_set")) { return parse_call_set();   }
    elsif (Token::str_eq($t, "return"  )) { return parse_return();     }
    elsif (Token::str_eq($t, "while"   )) { return parse_while();      }
    elsif (Token::str_eq($t, "case"    )) { return parse_case();       }
    elsif (Token::str_eq($t, "_cmt"    )) { return parse_vm_comment(); }
    else {
        if (Token::kind_eq($t, "ident")) {
            return parse_call_set();
        } else {
            p_e("parse_stmt", $t);
            die "not_yet_impl";
        }
    }
}

(追記 2021-08-30: その後の修正により、下記の部分はなくなりました)

それと、今回はコード生成処理のアレ、何度も似たようなのが出てきて鬱陶しかった部分をためしにサブルーチンに抽出して共通化してみました。うーん。どうでしょう。どうしようかな。とりあえず名前が微妙。

名前が微妙な上に上手い抽象でもない気がする……となるとこれはメソッド抽出すべきではないパターンのように思えます。いい解決法が見つかるまで本家の Ruby版には取り込めなさそう。とはいえコーディングの手間・退屈さは減らせるので、移植版では気軽に使っていこうかなと。

sub to_asm_str {
    my $fn_arg_names = shift;
    my $lvar_names = shift;
    my $val = shift;

    if (Utils::is_arr($val)) {
        return undef;
    } elsif (Val::kind_eq($val, "int")) {
        return $val->{"val"};
    } elsif (Val::kind_eq($val, "str")) {
        my $str = $val->{"val"};
        if (0 <= str_arr_index($fn_arg_names, $str)) {
            return to_fn_arg_ref($fn_arg_names, $str);
        } elsif (0 <= str_arr_index($lvar_names, $str)) {
            return to_lvar_ref($lvar_names, $str);
        } else {
            return undef;
        }
    } else {
        return undef;
    }
}

呼び出し箇所は6箇所。それなりに使いまわせているようではあります。

vgcg.pl:132:    $push_arg = to_asm_str($fn_arg_names, $lvar_names, $val);
vgcg.pl:240:    $push_arg = to_asm_str($fn_arg_names, $lvar_names, $fn_arg);
vgcg.pl:304:    $src_val = to_asm_str($fn_arg_names, $lvar_names, $expr);
vgcg.pl:316:                    my $vram_ref = to_asm_str($fn_arg_names, $lvar_names, sval($vram_arg));
vgcg.pl:342:            my $vram_ref = to_asm_str($fn_arg_names, $lvar_names, sval($vram_arg));
vgcg.pl:375:                my $vram_ref = to_asm_str([], $lvar_names, sval($vram_arg));

他の言語への移植

かんたんなコンパイラなのでいろんな言語に気軽に移植しています。

記事 リポジトリ 日付
Haskell github 2021-06-28
OCaml github 2021-06-26
Pascal github 2021-05-22
Julia github 2021-05-03
Rust github 2021-04-07
Crystal github 2021-03-27
Pric(セルフホスト) github 2021-02-21
Kotlin github 2021-01-14
Zig github 2021-01-07
LibreOffice Basic github 2020-12-14
Go github 2020-09-25
PHP github 2020-09-18
C♭ github 2020-09-13
C github 2020-09-06
Java github 2020-08-30
Dart github 2020-08-22
Python github 2020-08-19
TypeScript (Deno) github 2020-08-15
2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4