LoginSignup
4
4

More than 3 years have passed since last update.

Monkey言語に機能を追加する!

Posted at

Monkey言語って何?って思った方もいるでしょう。Monkey言語というのは、一般に使われている言語ではなく、「Go言語でつくるインタプリタ」という書籍の中で作成される独自の言語です。

この書籍では、Go言語の標準ライブラリのみを用いて、一からインタプリタ型言語を作成することができます。
字句解析器の作成から始めて、最終的には型(数値、文字列、真偽値、配列 etc)、四則演算、変数定義、関数定義などいろいろな構文を実装していく。さらに、各々を独立して実装するため、実装するたびに実際に動作させることができる!
作った機能が目に見える形で動くから、とても分かりやすい。

なにより、自分で一からプログラミング言語を作成するのはとても面白く楽しい!

ループ処理がない!

しかし!Monkey言語には、プログラミング言語といえば必ずと言ってもいいほど存在するループ処理の実装が無い!
ならば、追加実装するしかないな!と思い、自分でwhile文の機能を実装してみることに

目指す形は、以下のように実行できること。

>> let i = 0;
>> while (i < 5) {
..     puts("Hello");
..     let i = i + 1;
.. }
Hello
Hello
Hello
Hello
Hello
>> 

さぁ作ろう!

while文を認識させよう

まずは、なにをしなければならないのだろうか?
そうだね。while文が書いてあるのかを認識しなければならない。今のままだと「while」は変数と勘違いされてしまう...
というわけで、字句解析するために「while」のキーワードを登録しよう。

token.go
var keywords = map[string]TokenType{
    "fn":     Function,
    "let":    Let,
    "true":   True,
    "false":  False,
    "if":     If,
    "while":  While, // ここに追加
    "else":   Else,
    "return": Return,
}

上記のkeywordsは、字句解析時に最初に確認するキーワードたちを登録している。ここに登録してあるものは変数ではなく、最初から意味を持った字句であると認識される。見てみると、let文やif文などが登録されていることがわかる。
なのでここに「while」を登録すると、字句解析器が「while」を変数ではなくwhile文の開始を意味する特別なキーワードであると認識してくれる。

while文のASTのノードを登録しよう

字句解析でwhile文を認識するだけでは、もちろん何も起こらない。なぜなら、while文の構文をまだ定義していないからね。現在のインタプリタからすると「while文の始まりは見つけたけど、どう処理していいの...?」といった状態になっている。なので、while文に書いてある内容を解析しなければならない。

というわけで、解析しよう!と言いたいところだけど、まずは、解析後の結果を格納する構造体(ASTのノード:抽象構文木のノード)を定義しよう。

定義するのはいいけど、保持しておくべき情報は、何だろうか?
例として、とてもシンプルなwhile文を見てみよう。(無限ループしてるとかは気にしてはいけません...)

while (i < 5) {
    let msg = "Hello";
    puts(msg);
}

このとき、何を保持しようか。
結論としては、必要となるのは条件部と実行部の二つだね。

  • 条件部:i < 5
  • 実行部:let msg = "Hello"; puts(msg);

この二つを保持しておかないと、実際に実行する際に、ループしていいのか?何を実行すればいいのか?がわからなくなってしまう。

というわけで、この情報を保持する構造体を定義しよう。

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

この構造体は、以下のように解析後の情報を格納する。

  • Token    :「while」を意味するトークン
  • Condition   :「i < 5」を意味する式
  • Consequence :「{}」の中のすべての式を意味する式のカタマリ(let文やputs文の部分)

このように保持しておくことで、実際の実行時に条件部と実行部の情報を引っ張り出してきて、実行することができる。(Tokenフィールドは、自分が何のトークンなのかを認識する際に用いられている)

while文の構文解析を行おう

構文解析後の情報の格納先を定義できたので、今度こそwhile文の構文解析を行おうか。構文解析器にwhile文を解析する機能を追加しよう。

解析は具体的にはどのように進めていけばよいのだろうか?何を解析すればいいのだろう?
おそらくここが最も難しい部分となると思う。けれど心配はご無用で、難しいとは言っても簡単だ。簡単にできるようにMonkey言語は設計されているからね。

よし、while文の構文解析の流れを考えよう。前提として、「while」キーワードは発見されているとしよう。「while」の後には何が続いていけばよくて、何の文字が来たら終了なのだろう?
先ほどの例を用いて考えてみよう。

while (i < 5) {
    let msg = "Hello";
    puts(msg);
}

例を眺めてみればわかると思うが、空白と改行を取り除くと以下の形になっている。

while(条件部){実行部}

この中で解析したいのは、条件部と実行部のみなので、以下の流れなら解析できそうじゃないだろうか?

  1. 「while」の次の文字は「(」ではないなら構文エラー
  2. 条件部の式を解析
  3. 条件部の次は「)」で、その次は「{」ではないなら構文エラー
  4. 実行部の式を解析

実は、ほんとにこれだけで解析が完了してしまう。言葉にしてみると簡単だね。これをコードに起こしてみようか。

というわけで、以下は構文解析器が「while」を見つけた際に呼ばれる関数でwhile文の構文を解析する。

func (p *Parser) parseWhileExpression() ast.Expression {
    // while のASTノードを生成
    expression := &ast.WhileExpression{Token: p.curToken}

    // 「while」の次が「(」で始まっていない場合、構文エラーなので処理終了
    if !p.expectPeek(token.LParen) {
        return nil
    }

    p.nextToken()
    // 条件部にある式の解析を行って格納
    expression.Condition = p.parseExpression(Lowest)

    // 条件式の次が「)」で終わっていない場合、構文エラーなので処理終了
    if !p.expectPeek(token.RParen) {
        return nil
    }
    // 条件部の次が「{」で始まっていない場合、構文エラーなので処理終了
    if !p.expectPeek(token.LBrace) {
        return nil
    }

    // 実行部の式すべてを解析し、格納
    expression.Consequence = p.parseBlockStatemnt()

    return expression
}

ちょっと条件分岐が多いが、先ほどの処理の流れにあった構文エラーチェックを逐次行っているだけだ。これだけで、while文の解析は終了だ。とても簡単に解析できてしまったね!

while文を実行させよう

最後は、今まで解析してきたwhile文を実行する関数を追加しようか。

今まで通り、処理の流れから考えよう。while文は条件部の式の結果が真の場合、実行部の式たちを実行していく。1ループごとに条件部の式を再度評価することも忘れてはいけないね。じゃないと無限ループが発生してしまう。

以上のことをまとめると、以下の流れで処理すればいいのではないだろうか。

  1. 条件式を実行し、結果を得る
  2. 条件式の結果が真である限り、以下を実行し続ける
  3. 実行部の内容を実行
  4. 条件式を再度実行し、結果を再評価

またも、とてもシンプルだね。じゃあ実装してみよう。以下が、今まで解析してきたwhile文を実際に実行する関数だ。

func evalWhileExpression(we *ast.WhileExpression, env *object.Environment) object.Object {
    // 条件式の実行
    condition := Eval(we.Condition, env)
    if isError(condition) {
        return condition
    }

    var res object.Object
    res = Null
    for isTruthry(condition) {
        // 条件式が真の間、実行部の式を実行する
        res = Eval(we.Consequence, env)
        // 条件式の結果を更新する
        condition = Eval(we.Condition, env)
    }
    return res
}

これだけで終了だ。先ほど考えた流れの通りに実行しているだけで、特筆すべきところもない、なんともシンプルな関数だね。

ループ処理を手に入れた!

以上の関数などの追加と、今回は省略しているが関数たちを呼ぶための数行を追加したら完成。晴れて、Monkey言語でループ処理が書けるようになる!

go-monkey.PNG

while文の追加した際に、行ったすべての変更箇所まで知りたい方は、以下のコミットを見てほしい。

ほんとに、たったこれだけの追加で、こんなにも簡単に構文を一つ実装することができる。
トークンや字句解析器、構文解析器、評価処理などがうまくインターフェイスを用いて、適度に抽象化されているおかげだ。互いの構文の定義が影響を及ぼすことの無いように、影響範囲がきれいに分けられている。

これだけうまく抽象化されていると一種の感動をおぼえる。同じように「すごい!」と思ってくれた人はいるかな?自分には文才が無いから全然伝わってないかもしれない...。
もしそうでも、そうでなくても、実際にMoneky言語のコードの全体像を見てみてほしい。ほんとに素晴らしく、なるべく無駄を省き簡単に理解できるように設計されていて、少し読んだら美しさがわかると思う。

以下のリンクは、自分が書籍を写経しながら作成したMonkey言語のリポジトリなので、実際に見てみてほしい。while文以外にも、ソースファイル実行機能や複数行入力機能なども追加している。これをクローンしてどんどんいじってみるのもいいと思う。

まだまだMoneky言語には、いろいろな機能を追加することができる。for文を追加するのもいいし、組み込み関数を追加するのもいいね。break文ももちろん無いから、実装できればさらに便利になるだろう。

最後に

プログラミング言語を自作したことがない方は、是非一度作ってみてほしい。今回紹介した「Go言語でつくるインタプリタ」を読んで実装するのもいいし、ソースコードを読んでみて、完全に自作することに挑戦するのもいいと思う。

最初は難しそうだなと思うかもしれないが、実際に作ってみると意外とシンプルであることがわかる。また、抽象化の大事さを実感することになると思う。

そして、プログラミング言語を自作し、自分の言語が動く楽しさを味わってみてほしい。

4
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
4
4