自由帳

@_nibral の技術ブログ

Writing An Interpreter In Go その5 (Parser完成)

その4はこちら

引き続き各種トークンの処理を追加し、Parserを完成させる。

この辺から一部のテストコードが省略されるようになってきて、実装してテストが通った後に著者のコードと比べて答え合わせをする作業が増えた。

2.8: boolean

2.8: Add boolean · nibral/monkey_interpreter@9b60d31 · GitHub

true/false の値を持つ構造体を作って、token.TRUE/FALSE が来たらASTに追加する。

このコミットではテストの方が変更が多い。まず、前置演算子と2項演算子のテストで値が整数であること前提としていたのでbooleanも使えるように型をinterface{}に変更。ついでにリテラル (整数, 識別子, boolean) と2項演算子のASTが期待した値になっているかテストする処理を作って共通で使えるようにした。

2.8: 式のグループ化

2.8: Add grouped expressions · nibral/monkey_interpreter@e80c585 · GitHub

式の一部を括弧でくくって先に計算させるために、先に括弧の中身だけでASTを作る。一瞬で終わった。

2.8: if-else

2.8: Add if-else · nibral/monkey_interpreter@d9c439f · GitHub

ついに制御構文を処理するところまできた。if文は

if <条件式> {
  <処理ブロック>
} else {
  <処理ブロック>
}

という形式なので、ASTで使う構造体には条件式 (condition)・真の時の処理ブロック (consequence)・偽の時の処理ブロック (alternative)を持たせる。処理ブロックは1文とは限らないので、BlockStatement として複数の文が扱えるようにしておく。

type IfExpression struct {
    Token       token.Token
    Condition   Expression
    Consequence *BlockStatement
    Alternative *BlockStatement
}

構造体を定義したら、あとはifの構文に従ってフィールドを設定する parseIfExpression() を書く。順番にトークンを読んでいって、もしあるべき場所にあるべき括弧がなかったらその時点で nil を返してしまうので比較的読みやすいと感じた。

2.8: 関数定義

続いて関数の定義。Monkeyでは fn という語を使う。

fn (<パラメータ1>, <パラメータ2>, ...) {
  <処理ブロック>
}

↓

type FunctionLiteral struct {
    Token      token.Token
    Parameters []*Identifier
    Body       *BlockStatement
}

if文と同様、構文に従ってトークンを読み進める。fnの後ろには小括弧があって、その中身はパラメータなのでカンマごとに識別子として追加、続く中括弧の中身はすべてBlockStatement。もしこの形から外れたらその時点で return nil

2.8: 関数呼び出し

2.8: Add call expressions · nibral/monkey_interpreter@05202be · GitHub

Monkeyでの構文は <式>(<パラメータ1>, <パラメータ2>, ...) になる。上で定義した FunctionLiteral も式として扱えるので

fn (x, y) { x + y; }(2, 3);
callsFunction(2, 3, fn(x, y) { x + y; });

という書き方も許容されるが、自分で書くなら関数の部分をいったん変数に入れたいなぁという感じ。

識別子に続く ( を登録するだけだとダメで、関数呼び出しが最優先で処理される (=ASTの葉側になる) ように優先度を設定するところがポイント。

2.8: let と return に値をセット

2.8: Set values to let statement and return statement · nibral/monkey_interpreter@dd38cef · GitHub

コミットメッセージが雑すぎたかなという気がしている。2.4と2.5で後回しにした値の取り出し処理を追加した。

let と return の構造体に値がセットされるようになったので、いままで名前しか見ていなかったところを期待した値になっているかどうかのテストに置き換える。

2.9: RPPLの実装

2.9: Implement Read-Parse-Print-Loop (RPPL) · nibral/monkey_interpreter@ea32869 · GitHub

LetStatementのString()で値を出すのを忘れてたので追加して go run。よく考えたら1.5で作ったのもRead-Lexer-Print-Loopだし、REPLへの道のりはまだ長い。

monkey_interpreter> go run main.go
Hello AQUAMARINE\athlex! This is the Monkey programming language!
Feel free to type commands
>> let x = 1 + 2 * 3;
let x = (1 + (2 * 3))
>> fn (x, y, z) { return x + y + z; }
fn(x, y, z) return ((x + y) + z)
>> 

Parserの実装はいったんここまで。

ちなみに、本ではエラーが起きた時に猿のAAを表示する機能を付けているが、イラつくだけだと思ったので省略した。

↓こんな感じ

>> let x 12 * 3
            __,__
   .--.  .-"     "-.  .--.
  / .. \/  .-. .-.  \/ .. \
 | |  '|  /   Y   \  |'  | |
 | \   \  \ 0 | 0 /  /   / |
  \ '- ,\.-"""""""-./, -' /
   ''-' /_   ^ ^   _\ '-''
       |  \._   _./  |
       \   \ '~' /   /
        '._ '-=-' _.'
           '-----'
Woops! We ran into some monkey business here!
 parser errors:
        expected next token to be =, got INT instead