ド・初学者による丁寧に具体例を読んで理解したつもりになるインタープリタ


どうも、まーくんです。変な名前ですね。

大学でめちゃくちゃインタープリタ系の課題やってるんですが、さっぱりわからん。
「取ってた科目やコースが違うから」とかそういう次元でない問題の大きさを感じたので、きちんと勉強すべく遂にネットに記事を立てることになりました。まるで勤勉な学生みたい。

でもこれきちんと学べれば「計算機工学と電気工学の両方をきちんと通ってきた人」としてちょっとは誇れすみません本題行きます。

インタープリタってなんだよ

大学で出された課題はこうです。「インタープリタを作ろう」。
インタープリタとググってみましょう。
するとこのようなサイトが出ます。参考として引用させていただきます。

まあ読んでるだけじゃ分からん。そもそもそれで分かればこんな記事書いてない。

ザックリとした解釈だと、要は
「インタープリタを作れ」
「入力を翻訳して一個一個解釈しながら動くプログラムを作れ」

ということになります。というかここではそう考えることにします。

具体例を出せよ

出します。今回使うのはOCamlという言語です。
「関数型プログラミング言語」というものの一種です。
一応そこは学生なので、少しはOCamlの前提知識があるとして進めます。
(※なくても読めるくらいに簡単な表現には努めます)

必要なファイルは6つ。うち2つはMakefileおよびOCamlMakefileといって、要は「プログラムを実行可能な形に構築するよ」というもので、今の自分の手に負える奴じゃないので省略します。

残りはそれぞれ、example.ml、exampleLexer.mll、exampleParser.mly、Syntax.mlというファイルです。

exampleはプログラムの本体です。
exampleLexerは「字句解析器」といって、与えられた入力の文字列を見て「この文字列って文法的にはどういう種類?」と理解する役割。
exampleParserは「構文解析器」といって、字句解析の結果から文法上の分類ごとに処理を行っていきます。
「x = 3」を与えられた場合、x : 変数、= : 等号、3 : 整数 のように分類され、各種ごとにParser上で処理されます。

ファイル名の後ろについてる.mlが、「OCaml(や類似のML系言語)で扱うものですよ」という意味。mllやmlyはその亜種みたいですね。

じゃあ早速中身を見ましょうや。

example.mlを見る

open Syntax
open ExampleParser
open ExampleLexer

let main () =
  try 
    let lexbuf = Lexing.from_channel stdin in 
    let result = ExampleParser.main ExampleLexer.token lexbuf in
    print_expr result; print_newline ()
  with 
    | Parsing.Parse_error -> 
      print_endline "Parse Error!"
      
;;
if !Sys.interactive then 
  ()
else 
  main ()    

……???
分からん。全然分からん。ジャガーさん出ちゃった。

ジャガーさんも困惑

一番上にopen Syntax~とか、他のファイルを参照するように書いてあるっぽい。そりゃこれ単体で見ても何やってるか分からんわけだ。

main関数の中身

ザックリ動きを見ておきましょう。
let main()~はこのプログラムのmain関数だよってことを言ってます。
つまりこれが一番大事なプログラムの背骨。

try
……
with
| ……
の表記ですが、try withは「例外の捕捉」といって、tryより後ろの式や処理で例外、すなわち予想外のことをしちゃった場合に、withより後ろの処理に進むということです。

例外の方から

先に例外の方を見てみましょう。Parsing.Parse_errorは、「Parsingというモジュールで決められているParse_errorって処理をしてね」ってことです。これはOCamlに最初から入ってるモジュールで、「構文解析器が文法エラーを起こしたら発生する例外」です。つまり「オイ!!!字句解析ちゃんとできとらんやんけ!!!」とか、「オイ!!!定義されてない文字列入力されとるやんけ!!!」ってことです。

処理の本番、tryの内側

ではtryの内側です。
let lexbuf = Lexing.from_channel stdinも、さっきと同様Lexingというモジュールを使ってます。

from_channelは、標準入力であるstdin(つまりカーソルが合っている部分)の現在の読み込み位置から読み込んで、字句解析バッファと呼ばれるlexbufの値を返します。要は「入力した値をlexbufにそのまま代入してね」です。

このあとにinが付いているので、ここで定義したlexbufは更に後の処理で使うことになります。

let result = ExampleParser.main ExampleLexer.token lexbuf in
ここでは、この後見ていくParserとLexerのそれぞれのファイルでmainとtokenの処理を呼び出してます(処理を呼び出すという名前が合っているのかは不明ですが、ここではそう呼ぶことにしましょう)。
その処理のためにはさっき定義したlexbufの値が必要なので、関数の引数のように後ろにlexbufが付いています。
そしてその値をresultとして、更に後の処理でresultを使います。

print_expr result; print_newline ()
ここですね。print_exprの処理も後でSyntax上に出てきます。
そこにresultを入れてあげたあと、print_newline ()の処理です。
print_newline ()は改行するためのものだそうです。
後ろに()がありますが、これはunit型と言って、値のやり取り(代入や返り値)の必要がないときにダミーとして入れておくもの。気にしなくておk。


main関数まとめ

まとめると、
①他のファイルを参照しながら
②標準入力からlexbufに値を入れると
③モジュールが処理してくれたresultの値を
④出力して改行してくれる

これがexampleなわけですね。長いわ。

長すぎたので分割します。ParserやLexerは次回以降。
ではまた。

この記事が気に入ったらサポートをしてみませんか?