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

かんたんな自作言語のコンパイラをいろんな言語で書いてみるシリーズ 18番目の言語は OCaml です。 いつもの通りでライフゲームコンパイルだけ通ればヨシ、という程度の雑なものです。

できたもの

github.com

主な部分のサイズ(行数)はこんな感じ。

$ wc -l *.ml lib/{utils,types}.ml
  401 codegen.ml
  127 lexer.ml
  369 parser.ml
   52 lib/utils.ml
    6 lib/types.ml
  955 合計

Ruby 版と同じくらい。

移植元

memo88.hatenablog.com

github.com

ライフゲームのプログラムだけコンパイルできればOKという簡単なコンパイラです。Ruby 版だとコンパイラ部分だけで 1000行くらい。

ベースになっているバージョンは ステップ 58 のあたり。 (追記 2023-04-15 ステップ 63 まで反映させました)

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

動かし方の例

$ echo '
  func add(a, b) {
    return a + b;
  }

  func main() {
    call add(1, 2);
  }
' | bin/lexer | bin/parser | bin/codegen

# ↓アセンブリが出力される

  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

メモ

  • Lisp が静的型になったみたいな印象(雑な印象)
  • Haskell よりはとっつきやすい
    • LispHaskell の中間みたいな印象
    • 慣れてくると Lisp を書くときと同じような感覚で書ける
    • 純粋でない書き方ができるのと、遅延評価ではないので、メジャーな言語とのギャップが (Haskell に比べれば)小さい
    • オフサイドルールもない
    • Haskell 難しい、よく分からなかったという人は急がば廻れで OCaml を試すとよいかもしれません
  • ref は使わなくても書けた
  • コード生成で関数の引数名のリスト、ローカル変数名のリストに加えて label id も引き回す作りにしているので、ここはレコードでまとめてみた
  • 多少読めるようになってきたので次は Haskell に再チャレンジしたい
  • せっかくなので ocamlyacc を使えるようにしたい。 そして MinCaml を読みたい
    • 今回は ocamlyacc などのライブラリは使っておらず、パーサは手書き

(以下、初心者が書いてますので不正確な記述・間違いなどあるかと思います)

書いていたら意外とすぐ慣れたけど最初よく分からなかった関数適用まわりの書き方について。

まず、1引数の関数しかないというのが前提。 引数なしの関数というのもない(たぶん)。 返り値も必ず1つ(たぶん)。

参考: なぜ関数型言語の関数は必ず値を返すのか - Qiita

引数なしの場合は () を渡す(() に対して関数を適用する)

(* 関数の定義 *)
let f () = print_string "hello\n" in

(* 関数の適用 *)
f ()

これは「関数名に続けて ( ) を書く」という構文なのではなくて、「() という値(unit 型の値、ユニット値)に対して関数 f を適用している」と読む(『プログラミング in OCaml』 p152, 153)。

引数が2個以上の場合は、カリー化関数を使うか、タプルを使う。 (カリー化関数を使うといっても単に下記のように書くだけなので意識することはほとんどありませんでした)

(* カリー化 *)
let add1 a b = a + b in
add1 1 2

(* タプル *)
let add2 (a, b) = a + b in
add2 (1, 2)

使い分けについては

引数が組であることに意味がなければ,カリー化して定義することが推奨されます。 しかも,大抵の場合,カリー化関数のほうが効率良く実行できるようになっています。

(『プログラミング in OCaml』 p62)

とのことです。


ALGOL系の言語だと、関数呼び出しはこう。見慣れた感じ。

foo_func(1, 2)

Ruby では括弧を省略してこう書ける。

foo_func 1, 2

OCaml では引数の区切りのカンマは不要(スペースで区切る)。

foo_func 1 2

次のように括弧で囲んでいる場合、これは一見すると Lisp の関数適用と同じ見た目になっているけど、 中身の foo_func 1 2 の部分が式で、それを括弧で囲んでいる、と読むっぽい。 ALGOL系言語で 1 + 2 を括弧で囲んで (1 + 2) にするのと同じような感覚。

(foo_func 1 2)

↑この例だと括弧を付ける必要はないが、評価結果に対してさらに別の関数を適用する、といった場合は括弧が必要になる(ないと区切りが分からない)。

bar_func (foo_func 1 2)

ここらへんの括弧の付け方が分かってくると普通に読み書きできるようになります。


if 式の then/else で複数の式を間違った形で書くとうまく動かないので、 慣れるまではとりあえず全部括弧で囲んでおけばよい(という感じでやってました)。 慣れてきて、ここは外しても大丈夫そうだな、と判断できるようになってきたら外していく。

if ... then
  (
    ...
  )
else
  (
    ...
  )

match も同様。

match ... with
| 0 ->
  (
    ...
  )
| _ ->
  (
    ...
  )

ちなみに、私はめんどくさくて括弧で書いていましたが

文の並びを囲むときには括弧ではなくて begin/end を使うのが 「よいスタイル」 だと言われて推奨されています。

『プログラミング in OCaml』 p167

とのことです。

追記 2023-04-15

www.saiensu.co.jp

その後『プログラミングの基礎』も読みました。こういうタイトルですが OCaml の入門書でもあります。説明がとても平易で、一番最初に読む OCaml の入門書としておすすめ。

参考: [OCaml]書評「プログラミングの基礎」 - あどけない話


www.coronasha.co.jp

タイトルに OCaml と書かれていませんがこれも OCaml の本です。こちらも後から読みました(といっても図書館で借りてまだざっと目を通した程度)。扱われている内容の難易度的には今の自分にちょうどよさそうという印象で、購入しておこうかと。 ocamllex と ocamlyacc を使い、ターゲットは x64。

参考

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

memo88.hatenablog.com

memo88.hatenablog.com

memo88.hatenablog.com

qiita.com