Golang でプログラミング言語を作る__Part20
前回からの続きになります。今回は、新しいデータタイプを追加していきます。今あるタイプは、数値と論理値のみとなっています。
今回は、文字列を、今までの処理に加えていきます。
流れをおさらいします。
- まずは、Tokenタイプに文字を表すものを追加
- lexerのテストファイルに、文字列用のテストケースを作成
- 上のテストを失敗させたら、それが通るようなlexing処理、つまりソースコードのToken化を加える。値に(“)が来たら、文字列タイプのTokenになります。
- テストをpassさせる。
- AST内に、文字列を表すノードを設定。これで、内部的に、ソースの文字列が認識されます。
- parserの文字列を含んだテストケースを書いて、failさせる
- テストが通るparsing(解析)処理を加える。
- テストをpassさせる
- オブジェクトに、文字列を保持するオブジェクトを追加
- evaluatorに文字列を含んだテストケースを書いて、failさせる
- 文字列を評価(evaluation)する関数を設定
上の処理に加えて、文字列の連結機能も作っていきます。といっても、既にinfixタイプ(例: 1 + 4や 54 /2と言った式)の評価関数は作られているので、そこに文字列の場合の分岐を加えればOKです。
前提として、今回扱う文字列は、下のようなイメージになります
"<sequence of characters>"“”で囲まれたものを一つのTokenとして見ていきます。つまり、一連の文字が一つのものとして扱われます。
では、実際に作っていきます。まずはTokenタイプに文字列を加えます。
token/token.go const内に次のコードを入れます。
STRING = "STRING"次に文字列を含むテストケースを作ります。
次のテスト入力値を加えテストをfailさせます。
"foobar"
"foo bar"そしたら、次のコードをlexerに加えます。
lexer/lexer.go
func (l *Lexer) NextToken() token.Token { // [...]
switch l.ch { // [...]
case '"':
tok.Type = token.STRING tok.Literal = l.readString()// [...]
func (l *Lexer) readString() string {
position := l.position + 1
for {
l.readChar()
if l.ch == '"' || l.ch == 0 {
break
}
}
return l.input[position:l.position]
}
ここでlexerのテストがpassすることを確認したら、文字列ソースのToken化は成功です。次はこのTokenをparserに渡します。
ASTに文字列を表すノードを設定します。
ast/ast.go
type StringLiteral struct {
Token token.Token
Value string
}func (sl *StringLiteral) expressNode() {}
func (sl *StringLiteral) TokenLiteral() string { return sl.Token.Literal }
func (sl *StringLiteral) String() string { return sl.Token.Literal }
次に、テストケースを書いてきます。
func TestStringLiteralExpression(t *testing.T) {
input := `"hello world";`l := lexer.New(input)
p := New(l)
program := p.ParseProgram()
checkParserErrors(t, p)stmt := program.Statements[0].(*ast.ExpressionStatement)
literal, ok := stmt.Expression.(*ast.StringLiteral)
if !ok {
t.Fatalf("exp not *ast.StringLiteral, Got=%T", stmt.Expression)
}
if literal.Value != "hello world" {
t.Errorf("literal.Value not %q, got=%q", "hello world", literal.Value)
}
}
このテストをfailさせたら、次のコードをparser.goに書き込みます
func (p *Parser) parseStringLiteral() ast.Expression {
return &ast.StringLiteral{Token: p.curToken, Value: p.curToken.Literal}
}この処理を、parserの初期化関数、 New 内で、文字タイプのTokenと結べつけるのを行う必要があります。New内に、次の登録関数を追加します。
p.registerPrefix(token.STRING, p.parseStringLiteral)ここでテストがpassしたら、parserの処理は完了です。
次は文字列の評価に移ります。
ここも、今まで通りの処理の流れになります。
以下の記事と同じ実装の仕方になります。
evaluationの処理内容としては、ast.StringLiteralが渡された時に、ストリングタイプのオブジェクトの参照を返しています。
以上の処理を経て、文字列がREPL内で使えることが、確認できると思います。
続いて、文字列の連結機能を付けていきます。まずはテストケースを書いていきます。
evaluator/evaluator_test.go
func TestStringConcatenation(t *testing.T) {
input := `"hello" + " " + "World"`
evaluated := testEval(input)
str, ok := evaluated.(*object.String)
if !ok {
t.Fatalf("object is not string, got=%T (%+v)", evaluated, evaluated)
}
if str.Value != "hello World" {
t.Errorf("string has wrong value, got=%q", str.Value)
}
}またエラーハンドリングのテスト TestErrorHandling に次の入力値を加えます。+以外の文字列の連結には、エラーがでることのテストになります。
{
`"hello" - "world"`,
"unknown operator: STRING - STRING",
},上の2つのテストがfailすることを確認したら、先程言いました、infix用の処理にstring同士の時の分岐処理を加えます。
evaluator/evaluator.go
case left.Type() == object.STRING_OBJ && right.Type() == object.STRING_OBJ:
return evalStringInfixExpression(operator, left, right)上のケースをinfixの関数、 evalInfixExpression のswitch内に入れます。
evalStringInfixExpression は、次の処理になります。
func evalStringInfixExpression(operator string, left, right object.Object) object.Object {
if operator != "+" {
return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type())
}
leftVal := left.(*object.String).Value
rightVal := right.(*object.String).Value
return &object.String{Value: leftVal + rightVal}
}文字列の連結に+(プラス)以外が用いられた時は、エラーが出されます。
最後にテストを走らせpassすることを確認しましたら、文字列の連結機能が実装されています。
次はBuilt-in関数の実装になります。
