んだ日記

ndaDayoの技術日記です

『Go言語でつくるインタプリタ』を読んだ。字句解析編

はじめに

この本を読むのは、2回目です。

なぜもう一度読み返しているかというと、この本の続編である「Writing A Compiler In Go」に挑戦してみたものの、まるで理解が進みまなかったからです

「Writing A Compiler In Go」を読む前に、コンピュータの理論と実装のハードウェアの部分を終えており、VMやメモリ、CPUについては前よりは理解していたつもりでいたので、「コンパイラもまーなんとかいけるだろ」と思ったんですけど、、見事に打ち砕きにあいました。

で、このまま「Writing A Compiler In Go」で粘っていても、時間ばかりかかりモチベーションも下がっていくだろうなと感じました

そこで、なんで理解できんだろうかと考えた結果、前編であるがインタプリタをきちんと理解していないことに気づきました。

ということで、まずはインタプリタを理解してから、再度チャレンジしようということで2度目の写経に取り込みました。

1回目の反省点と2回目でやること

『Go言語でつくるインタプリタ』を、初めに写経したのは去年の末でした。

このときは、プログラミング言語ができていく過程が興奮ものでしたし、さらにテストドリブンで進むため、テンポが良くて気持ちも上がりながら進めました。

ただ途中、「あれ?これどういうことなんだろ?」という部分があっても、とにかくやり切ることを優先し、不明点は放置してしまったんですよね。

結果として、完走できたのは良かったんですが、今になって雰囲気でしか理解していないことに気づいてしまいました。

だから、今回はブログを書きながら一つひとつ整理して、しっかり理解していこうと思っています。

字句解析とは

まず、『Go言語でつくるインタプリタ』は字句解析器を作るところから始まります。

字句解析は、ソースコードトークンと呼ばれる意味のある単位に分割するプロセスです。

例えば

let five = 5;

というコードがあるとします。

このコードをトークン単位に字句解析を通じて分割すると

1. let 
2. five 
3. = 
4. 5
5. ;

という5つのトークンに分割されます

これらのトークン列は、後の構文解析のステップで使用され、構文解析によって抽象構文木になります。

字句解析は、一般的に「lexer」と呼ばれるもので、これは「lexical(字句の)」という単語が由来となっています。

文字通り、字句を解析するという役割を担うため、この段階では、コードが意味をなすか、動作するか、エラーを含むかどうかを判定することはありません。

字句解析 in Go

ここからは実際にソースコードで解説していきます。

以下、Monkeyというワードが出てきますが、Monkeyとは『Go言語でつくるインタプリタ』でつくっていくプログラミング言語の名前です

トークンの定義

まずは、字句解析をつくるにあたり、最初に行うのはトークンの定義です。

ここでは、Monkeyで扱うことができるトークンを定義しています。 これは一部ですが、

// token/token.go

const (
         // [...]
    IDENT = "IDENT" // 識別子
    INT   = "INT"   // 数値

    ASSIGN   = "="
    PLUS     = "+"
    MINUS    = "-"
    BANG     = "!"
        // [...]
)

token.goで定義したトークンのみがMonkeyで扱うことができます。

言語によって、使える記号が違いますが、それは定義されているトークンが違うからでしょう

字句解析のtests

お次に見ていきたいのは、字句解析のテストです。

テストケースを見ることで、挙動を確認することができます(本書のいい点がここ。

先程の例のlet five = 5;のテストコードとTokenの構造体は、以下のような感じです

// lexer/lexer_test.go

func TestNextToken(t *testing.T) {
    input := `let five = 5;`

    tests := []struct {
        expectedType    token.TokenType
        expectedLiteral string
    }{
        {token.LET, "let"},
        {token.IDENT, "five"},
        {token.ASSIGN, "="},
        {token.INT, "5"},
        {token.SEMICOLON, ";"},
         // [...]
// token/token.go
type TokenType string

type Token struct {
    Type    TokenType
    Literal string
}

テストコードを見れば、字句解析が何をするのかより具体的に理解できますよね。

すなわち、let five = 5;というソースコードがinputとして与えられたときに

① 定義されたトークンとして分割すること
② 各トークンのトークンタイプを保持すること

ができればよいわけですね。

字句解析の実際

ここまで、トークンの定義と字句解析の挙動をテストコードを通じてみてきましたので、実際に 字句解析のコードをみてみましょう

// lexer/lexer.go

type Lexer struct {
    input        string
    position     int
    readPosition int
    ch           byte
}

func New(input string) *Lexer {
    l := &Lexer{input: input}
    l.readChar()
    return l
}

func (l *Lexer) readChar() {
    if l.readPosition >= len(l.input) {
        l.ch = 0
    } else {
        l.ch = l.input[l.readPosition]
    }

    l.position = l.readPosition
    l.readPosition += 1
}

func (l *Lexer) NextToken() token.Token {
    var tok token.Token

    switch l.ch {
    case '=':
        tok = newToken(token.ASSIGN, l.ch)
    case '+':
        tok = newToken(token.PLUS, l.ch)
 // [...]

順で読み込んでいって、その都度、トークンに変換するだけです。 とてもシンプルですよね。

ただそれだけですが、おもしろいというか愛くるしいのはヘルパー関数です。

ヘルパー関数

さて、ソースコードを順に読み込んでいくにあたっては

現在読み込んでいるのは、英字なのかどうか? ”=”と”==”をどのように見分けるのか?

などを判断しながら、解析を進める必要があります。

ここを担当するのがヘルパー関数ですが、なんとも愛くるしいです

たとえばですが、英字かどうかを判定するロジックですが、以下のようなヘルパー関数が担当します

func (l *Lexer) readIdentifier() string {
    position := l.position
    for isLetter(l.ch) {
        l.readChar()
    }

    return l.input[position:l.position]
}

func isLetter(ch byte) bool {
    return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_'
}

このようなヘルパー関数を作りながら、字句解析を拡張していきます。

個人的には、次の単語を覗き見するpeek関数が一番好きでした。

Repl

で、字句解析の章を完成すると、こんな感じで評価することができます。 気持ちがいいですね。

$ go run main.go
Hey nda! This is the Monkey programming language!
Feel free to type in commands
>> let name = nda;
{Type:LET Literal:let}
{Type:IDENT Literal:name}
{Type:= Literal:=}
{Type:IDENT Literal:nda}
{Type:; Literal:;}
>>

まとめ

今回は、字句解析について、書いていきました。 やはり、こうして文字にすると整理されますね

次回もやっていきたいと思います

僕から以上。あったくして寝ろよ