볞묞 바로가Ʞ

Computer Science

[CS] 나만의 읞터프늬터륌 만듀얎볎자! (2): Parser 만듀Ʞ - 두번짞

반응형

🔊 핎당 포슀팅은 ë°‘바닥부터 만드는 읞터프늬터 in go ì±…을 읜고 개읞적읞 정늬 목적 하에 작성된 Ꞁ입니닀. 볞 포슀팅에 사용된 자료는 몚두 볞읞읎 직접 재구성하여 작성하였음을 알늜니닀.
 

출처: Yes24


읎번 포슀팅에서는 직전 포슀팅까지 í•Žì„œ 만듀었던 우늬만의 파서에 표현식을 파싱할 수 있는 Ʞ능을 탑재핎볌 것읎닀. 윔드레벚로 알아볎Ʞ에 앞서 표현식 파싱읎띌는 것을 구현할 때 알아두얎알할 사전 개념 몇 가지와 고렀사항에 대핎서 짚고 넘얎가볎자.

1. 표현식 파싱을 하Ʞ 전에..

직전 포슀팅에서 구현했던 let 묞, return 묞 파싱은 let 또는 return 묞 닀음에 ì–Žë–€ 토큰듀읎 등장할지 명확했닀. 하지만 표현식 파싱은 let, return 묞 파싱처럌 쉜게 구현할만큌은 아니며 ꜀ 까닀로욎 작업읎닀. 

 

가장 뚌저 표현식을 파싱할 때 고렀핎알 할 사항은 연산자 우선순위(operator precedence)읎닀. 수학에서 사칙연산읎띌는 죌제가 있닀. 덧셈곌 뺄셈은 곱셈곌 나눗셈곌 같읎 등장하게 되멎 곱셈곌 나눗셈은 덧셈곌 뺄셈볎닀 뚌저 계산되얎알 한닀는 사싀은 대부분의 사람듀읎 알고 있을 것읎닀. 읎러한 원칙읎 프로귞래밍 얞얎에서도 당연히 적용되얎알 한닀. 뿐만 아니띌 덧셈곌 곱셈읎 같읎 등장했지만 덧셈 연산에 소ꎄ혞로 감싞젞 있윌멎 덧셈읎 뚌저 수행되얎알 한닀. 예시는 닀음곌 같닀.

 

 (5 + 10) * 2

 

표현식을 파싱할 때 고렀핎알할 또 닀륞 사항은 같은 타입의 토큰듀읎 여러 위치에 나타날 수 있닀는 점읎닀. 예륌 듀얎, 닀음곌 같은 윔드가 있닀고 핎볎자.

 

-5 - 10
5 * (add(2, 3) + 10)

 

첫번짞 쀄의 윔드륌 볎멎 뚌저 등장하는 - 띌는 Ʞ혞는 전위 연산자륌 의믞한닀. 슉 0에서 -5만큌을 빌는 연산읎닀. 하지만 뒀의 - 띌는 Ʞ혞는 쀑위 연산자로서 -5 와 10읎띌는 두 개의 숫자륌 뺄셈하는 역할을 한닀. 닀음윌로 두 번짞 쀄 윔드륌 볎자. 가장 바깥에 있는 소ꎄ혞는 귞룹 표현식윌로서, 읎 소ꎄ혞 안에 있는 표현식을 뚌저 수행하띌는 의믞읎닀. 반멎에 add(2, 3)에 있는 소ꎄ혞는 add 띌는 핚수의 혞출 연산자로서 혞출 표현식에 핎당한닀. 읎렇게 같은 타입의 토큰읎띌도 여러 위치에 나타나게 될 때 각Ʞ의 Ʞ능읎 달띌지는 것을 볌 수 있닀. 우늬의 파서는 읎런 점도 고렀하멎서 파싱을 수행핎알 한닀.

 

귞러멎 Monkey 프로귞래밍 얞얎륌 대상윌로 우늬만의 파서가 파싱할 수 있는 표현식 예시는 대표적윌로 닀음곌 같닀.

 

# 전위 연산자
-5
!true
!false

# 쀑위 연산자
5 + 5
5 - 5
5 / 5
5 * 5

# 비교 연산자
foo == bar
foo != bar
foo < bar
foo > bar

# 귞룹 표현식
5 * (5 + 5)
((5 + 5) * 5) * 5

# 혞출 표현식
add(2, 3)
add(add(2, 3), add(5, 10))
max(5, add(5, (5 * 5)))

# 식별자 표현식
foo * bar / foobar
add(foo, bar)

# 핚수 늬터럎 표현식
fn(x, y) {return x + y}(5, 5)
(fn(x) { return x }(5) + 10 ) * 10

# if 표현식
let result = if (10 > 5) { true } else { false };

 

우늬는 위와 같은 종류듀의 표현식을 파싱할 수 있도록 하Ʞ 위핎서 하양식 연산자 우선순위 파싱 방법읞 프랫 파싱을 사용핎볌 것읎닀. 프랫 파싱의 핵심적읞 구상은 "토큰을 파싱핚수와 연ꎀ시킬 때, 핎당 토큰읎 쀑위읞지 전위읞지에 따띌 서로 닀륞 파싱 핚수로 연ꎀ시킀는 것"읎닀.

2. AST 녞드에 String 메서드 추가하Ʞ

볞격적읞 프랫 파서륌 구현하Ʞ 전에 디버깅을 쉜게 할 수 있도록 구현한 AST 녾드 슉, Node 띌는 읞터페읎슀에 String() 메서드륌 충족시킬 수 있도록 구성하자. 읎 String() 메서드는 AST륌 구성하는 녞드의 낎용묌을 출력핚윌로썚 닀륞 녞드와 비교도 하고, 우늬가 의도하여 녞드륌 잘 구성했는지도 삎펎볌 수 있닀. Node 띌는 읞터페읎슀는 Ʞ졎에 구현했던 let 묞, return 묞 녞드의 읞터페읎슀 뿐만 아니띌 식별자 녞드읞 Identifier, 귞늬고 읎번에 추가로 구현할 표현식 녞드에도 추가핎죌도록 하자. 

 

// ast/ast.go

type Node interface {
	TokenLiteral() string
	String() string
}

(..생략..)

func (p *Program) String() string {
	var out bytes.Buffer

	for _, s := range p.Statements {
		out.WriteString(s.String())
	}
	return out.String()
}

func (i *Identifier) String() string       { return i.Value }

func (ls *LetStatement) String() string {
	var out bytes.Buffer

	out.WriteString(ls.TokenLiteral() + " ")
	out.WriteString(ls.Name.String())
	out.WriteString(" = ")
	if ls.Value != nil {
		out.WriteString(ls.Value.String())
	}
	out.WriteString(";")
	return out.String()
}

func (rs *ReturnStatement) String() string {
	var out bytes.Buffer

	out.WriteString(rs.TokenLiteral() + " ")
	if rs.ReturnValue != nil {
		out.WriteString(rs.ReturnValue.String())
	}
	out.WriteString(";")
	return out.String()
}

func (es *ExpressionStatement) String() string {
	if es.Expression != nil {
		return es.Expression.String()
	}
	return ""
}

3. 프랫 파서 구현하Ʞ

읎제 볞격적윌로 프랫 파서륌 구현핎볎자. 앞서 얞꞉했지만 프랫 파서의 핵심은 "토큰 타입곌 파싱 핚수륌 연ꎀ 짓는 것"읎닀. 우늬가 가장 뚌저 할 음은 전위(prefix) 파싱 핚수와 쀑위(infix) 파싱 핚수륌 귞에 맞는 토큰곌 연ꎀ 짓는 음읎닀.

 

뚌저 우늬는 전위 또는 쀑위 파싱곌 연ꎀ된 토큰읎 혞출할 대상읞 전위, 쀑위 파싱 핚수 타입을 정의핎볎자.

 

// parser/parser.go

type (
	prefixParseFn func() ast.Expression
	infixParseFn func(ast.Expression) ast.Expression
)

 

prefixParseFn 읎띌는 타입의 핚수는 읎늄에서 유추할 수 있닀시플 전위 파싱을 수행하는 핚수에 핎당한닀. 읎는 아묎것도 읞자로 받지 않되, 표현식 타입을 반환한닀. 반멎에 infixParseFn 타입의 핚수는 쀑위 파싱을 수행하며, 표현식 1개륌 읞자로 전달 받는닀. 읎 읞자로 전달 받는 표현식은 쀑위 연산자의 좌잡에 있는 표현식을 의믞한닀. 예륌 듀얎, 아래처럌 쀑위 연산자가 사용되는 소슀윔드가 있닀고 핎볎자.

 

2 * 5

 

귞러멎 infixParseFn 타입 핚수의 읞자에는 위 소슀윔드 Ʞ쀀윌로 2 띌는 표현식읎 읞자로 듀얎가는 셈읎닀.

 

읎렇게 전위, 쀑위 파싱 핚수 타입을 정의했윌니 앞윌로 전위 연산자 또는 쀑위 연산자와 ꎀ렚된 토큰을 만나멎 귞에 맞는 파싱 핚수륌 혞출할 수 있도록 핎죌자. 우늬는 읎륌 위핎 Parser 구조첎에 닀음곌 같은 map 자료구조륌 추가하자.

 

// parser/parser.go

type Parser struct {
	l         *lexer.Lexer
	errors    []string
	
	curToken  token.Token
	peekToken token.Token
	
	prefixParseFns map[token.TokenType]prefixParseFn
	infixParseFns map[token.TokenType]infixParseFn
}

 

귞늬고 읎 map 자료구조에 특정 토큰곌 특정 파싱 핚수가 저장될 수 있게 핎죌는 핚수도 같읎 정의핎볎자.

 

// parser/parser.go

func (p *Parser) registerPrefix(tokenType token.TokenType, fn prefixParseFn) {
	p.prefixParseFns[tokenType] = fn
}

func (p *Parser) registerInfix(tokenType token.TokenType, fn infixParseFn) {
	p.infixParseFns[tokenType] = fn
}

3-1. 식별자 표현식 파싱하Ʞ

가장 뚌저 구현핎볌 Ʞ능은 식별자 표현식을 파싱하는 것읎닀. 닀시 한번 식별자 표현식읎란 ì–Žë–€ 소슀윔드륌 의믞하는지 늬마읞드 핎볎자.

 

foobar;
add(foobar, barfoo);
foobar + barfoo;
if (foobar) {
   (...생략...)
}

 

식별자 표현식을 잘 파싱하는지 테슀튞하는 윔드부터 삎펎볎자.

 

// parser/parser_test.go

func TestIdentifierExpression(t *testing.T) {
	input := "foobar;"

	l := lexer.New(input)
	p := New(l)
	// run lexing and parsing
	program := p.ParseProgram()

	if len(program.Statements) != 1 {
		t.Fatalf("program.Statements does not contain 1 statements. got=%d", len(program.Statements))
	}
	// type assertion: downcast from interface to struct that satisfies `Statement` interface
    stmt, ok := program.Statements[0].(*ast.ExpressionStatement)  // `[0]`은 유음한 명령묞읎 닎겚있는지 확읞하Ʞ 위핚
	if !ok {
		t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T", stmt)
	}

	ident, ok := stmt.Expression.(*ast.Identifier)
	if !ok {
		t.Fatalf("stmt.Expression is not ast.Identifier. got=%T", ident)
	}
	if ident.Value != "foobar" {
		t.Errorf("ident.Value not %s. got=%s", "foobar", ident.Value)
	}
	if ident.TokenLiteral() != "foobar" {
		t.Errorf("ident.TokenLiteral() not %s. got=%s", "foobar", ident.TokenLiteral())
	}
}

 

테슀튞 윔드는 읎전곌 작성한 것곌 크게 닀륎지 ì•Šë‹€. 테슀튞 윔드의 핎석은 독자에게 맡Ʞ렀고 한닀. 천천히 읜얎볎멎서 귞동안 배욎 낎용을 복습하는 것도 좋닀. 현재 위 테슀튞 윔드륌 싀행하멎 당연히 싀팚한닀. 왜냐하멎 우늬는 아직 식별자 표현식을 싀질적윌로 파싱하는 로직을 작성하지 않았Ʞ 때묞읎닀. 읎제 볞격적윌로 작성핎볎자.

 

가장 뚌저 특정 토큰에 따띌 switch-case 구묞윌로 분Ʞ핎서 특정 구묞을 파싱하도록 하는 핚수에 식별자 표현식 분Ʞ륌 추가핎볎도록 하자.

 

// parser/parser.go

func (p *Parser) parseStatement() ast.Statement {
	switch p.curToken.Type {
	case token.LET:
		return p.parseLetStatement()
	case token.RETURN:
		return p.parseReturnStatement()
	default:
		return p.ParseExpressionStatement()  // 새로 추가된 핚수
	}
}

 

위 소슀윔드륌 볎멎 ParseExpressionStatment 띌는 새로욎 핚수가 추가되었닀. 읎 핚수의 역할을 또 삎펎볎자.

 

// parser/parser.go

func (p *Parser) parseExpressionStatement() *ast.ExpressionStatement {
	stmt := &ast.ExpressionStatement{Token: p.curToken}
	stmt.Expression = p.parseExpression(LOWEST)
	
	if p.peekTokenIs(token.SEMICOLON) {
		p.nextToken()
	}
	return stmt
}

 

위 핚수는 ExpressionStatment 띌는 구조첎륌 쎈Ʞ화하고, 핎당 구조첎의 Token 멀버에 퍌서가 바띌볎고 있는 현재 토큰 슉, IDENT 읎띌는 토큰을 넣얎쀀닀. 귞늬고 핎당 구조첎의 Expression 멀버에 표현식을 싀질적윌로 파싱하는 역할을 하는 parseExpression 핚수륌 혞출한닀. 읎 때, LOWEST 띌는 읞자륌 넣얎죌는데, 읎 LOWEST는 바로 닀음윌로 삎펎볌 연산자 우선순위 쀑 가장 낮은 연산순위륌 의믞한닀.

 

연산자 우선순위륌 정의핎볎자.

 

// parser/parser.go

const (
	_ int = iota
	LOWEST
	EQUALS         // ==
	LESSGREATER    // < >
	SUM            // +
	PRODUCT        // *
	PREFIX         // !
	CALL           // 혞출 연산자로서의 소ꎄ혞(ex. add())
)

 

Go 얞얎에는 iota띌는 Enumerator가 졎재한닀. 읎 iota륌 사용하멎 위의 겜우 0에서부터 시작핎서 1씩 슝가하는 양의 정수륌 부여할 수 있게 된닀. ê²°êµ­, LOWEST는 1읎고 아래로 낎렀갈수록 1씩 슝가하여 CALL은 7읎 부여된닀. 읎는 연산자 우선순위륌 의믞하며 값읎 높을수록 연산자 우선순위가 높닀. 고로, 직전 소슀윔드에서 parseExpression 핚수의 읞자에 LOWEST륌 넘겚쀀 것은 가장 낮은 연산자 우선순위륌 넘겚쀀 것읞 셈읎닀.

 

읎제 parseExpression 핚수의 로직을 삎펎볎자.

 

// parser/parser.go

func (p *Parser) parseExpression(precedence int) ast.Expression {
	prefix := p.prefixParseFns[p.curToken.Type]
	if prefix == nil {
		return nil
	}
	leftExp := prefix()  // call `prefixParseFn`
	
	return leftExp
}

 

아까 정의했던 토큰곌 전위 파싱핚수로 구성된 map 자료구조에서 현재 토큰 타입에 맞는 전위 파싱 핚수륌 가젞와서 핎당 핚수륌 혞출한닀. 읎 혞출하는 전위 파싱 핚수의 생김새륌 아직은 삎펎볎지는 않았지만 leftExp 변수에는 분명 파싱된 표현식 녞드가 졎재할 것읎닀. 

 

방ꞈ 혞출한 전위 파싱 핚수의 생김새륌 삎펎볎Ʞ 위핎서 map 자료구조에 토큰곌 귞에 맞는 전위 파싱 핚수륌 등록핎볎자.

 

// parser/parser.go

func New(l *lexer.Lexer) *Parser {
	p := &Parser{
		l:      l,
		errors: []string{},
	}
	// 현재 쎈Ʞ token 값은 빈 묞자읎Ʞ 때묞에, curToken, peekToken에 토큰을 닎윌렀멎 2번 수행
	p.nextToken()
	p.nextToken()

	// 전위 파싱 핚수와 귞에 맞는 식별자 토큰을 등록
	p.prefixParseFns = make(map[token.TokenType]prefixParseFn)
	p.registerPrefix(token.IDENT, p.parseIdentifier)
	return p
}

// ast.Expression 읎띌는 읞터페읎슀륌 ast.Identifier 구조첎가 충족하Ʞ 때묞에 읎처럌 표현 가능
func (p *Parser) parseIdentifier() ast.Expression {
	return &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal}
}

 

렉서륌 읞자로 넣얎 파서륌 반환하는 New 핚수에서 map 자료구조륌 쎈Ʞ화하고, 전위 파싱 핚수와 귞에 맞는 IDENT(식별자) 토큰을 저장한닀. 귞늬고 읎 때 전위 파싱 핚수읞 parseIdentifer 핚수는 닚지 Identifer 구조첎의 포읞터 변수륌 반환한닀. 

 

개읞적윌로 go-lang의 묞법적윌로 궁ꞈ했던 점은 Identifier 구조첎 포읞터 변수륌 반환하는데, 왜 핚수 시귞니처에는 ast.Expression 읞터페읎슀륌 명시했나 궁ꞈ했닀. 결국에는 반환하는 결곌묌을 구첎적읞 타입에 종속시킀지 않고 추상화된 방식 슉, 혞출자 입장에서 parseIdentifer 핚수 말고도 닀륞 종류의 파싱 핚수륌 혞출할 때 동음한 읞터페읎슀로 혞출읎 가능하도록 만듀얎 죌Ʞ 위핚읎였닀. 아직 go-lang에 익숙하진 않아 얎색하지만 적응핎 나가는 시간읎 필요할 듯 하닀.

 

자, 읎제 식별자 표현식을 파싱할 수 있는 Ʞ볞적읞 쀀비가 되었닀. 아까 위에서 삎펎볞 테슀튞 윔드륌 아래처럌 싀행핎볎자. 테슀튞 결곌가 통곌하멎 잘 따띌옚 것읎닀.

 

go test ./parser

3-2. 정수 늬터럎 표현식 파싱하Ʞ

닀음윌로 파서가 파싱할 표현식은 정수 늬터럎 표현식읎닀. 정수 늬터럎은 값 자첎륌 생성하Ʞ 때묞에 표현식에 핎당한닀. 예시는 닀음곌 같닀.

 

5;

 

위의 겜우는 정수 늬터럎만 졎재하지만, 아래처럌 정수 늬터럎읎 닀륞 표현식곌 섞여 졎재할 수도 있닀.

 

let x = 5; # `5`가 정수 늬터럎
add(5, 10); # `5` 와 `10`읎 정수 늬터럎
5 + 5 + 5; # `5`띌는 숫자 각각읎 정수 늬터럎

 

읎번에도 테슀튞 윔드륌 뚌저 삎펎볎자. 읎 테슀튞 윔드는 앞윌로 우늬가 구현할 정수 늬터럎 표현식을 파서가 잘 파싱하는지 점검하Ʞ 위한 윔드읎닀.

 

// parser/parser_test.go

func TestIntegerLiteralExpression(t *testing.T) {
	input := "5;"

	l := lexer.New(input)
	p := New(l)
	program := p.ParseProgram()
	checkParserErrors(t, p)
	
	if len(program.Statements) != 1 {
		t.Fatalf("program.Statements does not contain 1 statements. got=%d", program.Statements)
	}
	// type assertion: downcast from interface to struct(`ExpressionStatement`) that satisfies `Statement` interface
	stmt, ok := program.Statements[0].(*ast.ExpressionStatement)
	if !ok {
		t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T", program.Statements[0])
	}
	// type assertion: downcast from interface(`Expression`) to struct(`IntegerLiteral`) that satisfies `Expression` interface
	literal, ok := stmt.Expression.(*ast.IntegerLiteral)
	if literal.Value != 5 {
		t.Errorf("literal.Value not %d. got=%d", 5, literal.Value)
	}
	if literal.TokenLiteral() != "5" {
		t.Errorf("literal.TokenLiteral not %s. got=%s", "5", literal.TokenLiteral())
	}
}

 

식별자 표현식을 테슀튞하는 윔드와 거의 유사하되 앜간 닀륞점읎 있닀. 바로 정수 늬터럎 표현식을 나타낮는 구조첎로 type assertion 하는 부분읎닀. 아직은 IntegerLiteral 읎띌는 구조첎륌 우늬가 정의하지는 않았닀. 바로 닀음에 정의할 것읎닀. 얎욌걎 IntegerLiteral 읎띌는 구조첎로 닀욎캐슀팅을 수행한 후, 핎당 구조첎의 Value 멀버에 듀얎있는 값곌 TokenLiteral() 메소드륌 혞출핚윌로썚 반환되는 묞자엎을 비교한닀. 읎 때 죌목할 점은 읎전곌 닀륎게 읎번에는 Value 멀버에 듀얎있는 값읎 묞자가 아닌 싀제 숫자읞지륌 비교한닀는 점읎닀.

 

읎제 IntegerLiteral 구조첎륌 아래처럌 정의핎볎자.

 

// ast/ast.go

type IntegerLiteral struct {
	Token token.Token
	Value int64
}

func (il *IntegerLiteral) expressionNode()      {}
func (il *IntegerLiteral) TokenLiteral() string { return il.Token.Literal }
func (il *IntegerLiteral) String() string { return il.Token.Literal }

 

IntegerLiteral 구조첎 역시 2개의 멀버륌 갖는데, 특읎한 점은 Value 멀버의 타입읎 묞자엎읎 아닌 int64로 정의되었닀. 읎제 읎 Value 멀버에는 정수 늬터럎읎 표현하는 묞자가 갖는 싀제 값 슉, 정수값을 닎을 것읎닀. 

 

읎제 정수 늬터럎에 대핎서 싀질적읞 파싱을 수행하는 핚수륌 정의핎알 한닀. 아래 윔드륌 볎자.

 

// parser/parser.go

func (p *Parser) parseIntegerLiteral() ast.Expression {
	lit := &ast.IntegerLiteral{Token: p.curToken}

	// `base = 0`: 묞자엎 접두사에 따띌 자동윌로 진법을 판닚하여 정수로 변환
	value, err := strconv.ParseInt(p.curToken.Literal, 0, 64)
	if err != nil {
		msg := fmt.Sprintf("could not parse %q as integer", p.curToken.Literal)
		p.errors = append(p.errors, msg)
		return nil
	}
	lit.Value = value
	return lit
}

 

핵심 로직은 묞자엎은 정수형윌로 변환하Ʞ 위핎 strconv.ParseInt 핚수륌 사용했고, 변환 후, IntegerLiteral 구조첎의 Value 멀버에 변환된 정수 값을 할당했닀는 점읎닀.

 

읎제 읎렇게 작성한 정수 늬터럎을 파싱하는 핚수륌 특정 토큰곌 ê·ž 토큰에 맞는 파싱핚수가 닎겚있는 map 자료구조에 등록하Ʞ만 하멎 된닀. 아래와 같읎 말읎닀.

 

// parser/parser.go

func New(l *lexer.Lexer) *Parser {
	(... 생략 ...)
    
	p.prefixParseFns = make(map[token.TokenType]prefixParseFn)
	p.registerPrefix(token.IDENT, p.parseIdentifier)
	p.registerPrefix(token.INT, p.parseIntegerLiteral)  // 새롭게 토큰곌 파싱핚수륌 등록!
	return p
}

3-3. 전위 연산자 표현식 파싱하Ʞ

닀음윌로 삎펎볌 부분은 전위 연산자, 귞늬고 ê·ž 전위 연산자와 핚께 등장하는 표현식을 파싱할 찚례닀. 전위 연산자는 플연산자 1개륌 갖는닀. 귞늬고 읎 플연산자륌 가지고 표현식을 형성하는데, 아래와 같은 구조로 표현식읎 만듀얎진닀.(ì°žê³ ë¡œ 핎당 책에서는 - 또는 ! 띌는 전위 연산자만 파싱할 수 있도록 되얎 있닀. Ʞ볞적윌로 + 연산자와 같은 것에 대한 파싱 Ʞ능읎 제공되지는 않는닀)

 

# 구조
<전위 연산자><표현식>

# 예시
-5;
!foobar;
!myFunc(2);
-add(5, 5);

 

읎번에도 전위 연산자 표현식 파싱읎 잘 동작하는지 테슀튞하는 테슀튞 윔드부터 삎펎볎자.

 

// parser/parser_test.go

func TestParsingPrefixExpression(t *testing.T) {
	prefixTests := []struct {
		input        string
		operator     string
		integerValue int64
	}{
		{"!5;", "!", 5},
		{"-15;", "-", 15},
	}

	for _, tt := range prefixTests {
		l := lexer.New(tt.input)
		p := New(l)
		program := p.ParseProgram()
		checkParserErrors(t, p)

		if len(program.Statements) != 1 {
			t.Fatalf("program.Statements does not contain 1 statements. got=%d", len(program.Statements))
		}

		stmt, ok := program.Statements[0].(*ast.ExpressionStatement)
		if !ok {
			t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T", program.Statements[0])
		}

		exp, ok := stmt.Expression.(*ast.PrefixExpression)
		if !ok {
			t.Fatalf("stmt is not ast.PrefixExpression. got=%T", stmt.Expression)
		}
		if exp.Operator != tt.operator {
			t.Fatalf("exp.Operator is not '%s'. got=%s", tt.operator, exp.Operator)
		}
		if !testIntegerLiteral(t, exp.Right, tt.IntegerValue) {
			return
		}
	}
}

func testIntegerLiteral(t *testing.T, il ast.Expression, value int64) bool {
	integ, ok := il.(*ast.IntegerLiteral)
	if !ok {
		t.Errorf("il not *ast.IntegerLiteral. got=%T", il)
		return false
	}
	if integ.Value != value {
		t.Errorf("integ.Value not %d. got=%d", value, integ.Value)
		return false
	}
	if integ.TokenLiteral() != fmt.Sprintf("%d", value) {
		t.Errorf("integ.TokenLiteral not %d. got=%s", value, integ.TokenLiteral())
		return false
	}
	return true
}

 

위 테슀튞 윔드에서 아직 선얞읎 안된 것듀읎 있는 상태읎닀. 예륌 듀얎, PrefixExpression 띌는 구조첎는 아직 정의가 안되얎있지만, 읎 구조첎는 전위 연산자 표현식에 대한 정볎륌 ë‹Žê³  있는 구조첎읎닀. 읎제 읎 PrefixExpression 읎띌는 구조첎륌 정의핎볎자.

 

// ast/ast.go

type PrefixExpression struct {
	Token token.Token
	Operator string 
	Right Expression
}

func (pe *PrefixExpression) expressionNode()      {}
func (pe *PrefixExpression) TokenLiteral() string { return pe.Token.Literal }
func (pe *PrefixExpression) String() string {
	var out bytes.Buffer
	
	out.WriteString("(")
	out.WriteString(pe.Operator)
	out.WriteString(pe.Right.String())
	out.WriteString(")")
	
	return out.String()
}

 

PrefixExpression 구조첎는 크게 3가지 멀버륌 가진닀. 첫번짞는 닀륞 구조첎랑 동음하게 토큰 멀버륌 가진닀. 귞늬고 Operator는 연산자륌 의믞한닀. 전위 연산자로는 -, ! 가 있닀. 읎 연산자 묞자 자첎가 듀얎간닀. 귞늬고 Right 띌는 멀버가 있는데, 읎는 전위 연산자의 위치륌 Ʞ쀀윌로 였륞쪜에 있는 표현식을 의믞한하며 Expression 읎띌는 읞터페읎슀 타입윌로 선얞되었닀. PrefixExpression 구조첎도 얎욌걎 AST 녞드로 구성되얎알 하Ʞ 때묞에 닀륞 구조첎처럌 읞터페읎슀륌 충족시킀Ʞ 위핎 위에서 3가지 메소드륌 정의했닀.

 

여Ʞ서 잠시, parseExpression 메소드에 한 가지 추가적읞 로직욞 추가핎볎도록 하자. 읎 추가된 로직은 전위 연산자 핚수륌 찟지 못했을 때 디버깅하Ʞ 쉜도록 윘솔에 메섞지륌 낎뱉도록 한닀.

 

// parser/parser.go

func (p *Parser) parseExpression(precedence int) ast.Expression {
	prefix := p.prefixParseFns[p.curToken.Type]
	if prefix == nil {
		p.noPrefixParseFnError(p.curToken.Type)
		return nil
	}
	leftExp := prefix() // call `prefixParseFn`

	return leftExp
}

func (p *Parser) noPrefixParseFnError(t token.TokenType) {
	msg := fmt.Sprintf("no prefix parse function for %s found", t)
	p.errors = append(p.errors, msg)
}

 

귞러멎 읎제 전위 연산자 표현식에 대한 싀질적읞 파싱을 수행하는 핚수륌 작성핎볎자. 귞늬고 읎 파싱 핚수륌 토큰곌 ê·ž 토큰에 맞는 파싱 핚수가 저장되얎 있는 map 자료구조에 등록시쌜볎자.

 

// parser/parser.go

func (p *Parser) parsePrefixExpression() ast.Expression {
	expression := &ast.PrefixExpression{
		Token: p.curToken,
		Operator: p.curToken.Literal,
	}
	
	p.nextToken()
	
	expression.Right = p.parseExpression(PREFIX)
	return expression
}

func New(l *lexer.Lexer) *Parser {
	(... 생략 ...)

	p.prefixParseFns = make(map[token.TokenType]prefixParseFn)
	p.registerPrefix(token.IDENT, p.parseIdentifier)
	p.registerPrefix(token.INT, p.parseIntegerLiteral)
	p.registerPrefix(token.BANG, p.parsePrefixExpression)  // 전위 연산자 `!` 추가
	p.registerPrefix(token.MINUS, p.parsePrefixExpression) // 전위 연산자 `-` 추가
	return p
}

 

위 소슀윔드에서 특읎한 점은 parsePrefixExpression 핚수 낎부에서 nextToken 핚수륌 한 번 혞출한닀는 점읎닀. 왜 혞출할까? 읎유부터 말하멎 전위 연산자 였륞쪜에 있는 표현식윌로 토큰 위치륌 옮ꞰꞰ 위핚읎닀. 슉, nextToken 핚수륌 혞출하Ʞ 전까지의 curToken은 전위 연산자(! 혹은 -)음 것읎닀. 하지만 전위 연산자가 등장하멎 반드시 였륞쪜에 표현식읎 등장하Ʞ 마렚읎닀. 따띌서 읎 였륞쪜에 있는 표현식윌로 현재 위치륌 옮ꞰꞰ 위핎 nextToken 핚수륌 한 번만 혞출하는 것읎닀. 귞늬고 난 뒀에 parseExpression 핚수로 였륞쪜의 표현식을 파싱한닀. 

 

닀만, 여Ʞ서 parseExpression 핚수의 읞자로 전달되는 PREFIX 슉, 전위 연산자의 우선순위 값읎 구첎적윌로 얎떻게 동작하는지는 아직 알아볎지 못했닀. 읎는 바로 닀음 목찚읞 쀑위 연산자에서 삎펎볌 예정읎닀.

3-4. 쀑위 연산자 표현식 파싱하Ʞ

마지막윌로 쀑위 연산자 표현식을 파싱하는 Ʞ능을 추가핎볎자. Monkey 프로귞래밍 얞얎에서 쀑위 표현식의 예시 소슀윔드는 닀음곌 같닀.

 

2 + 2;
2 - 2;
2 * 2;
2 / 2;
2 > 2;
2 < 2;
2 == 2;
2 != 2;

 

위 소슀윔드의 팚턎에서 볎멎 알겠지만 쀑위 연산자 표현식은 닀음곌 같은 구조륌 갖는닀. 

 

<왌쪜 플연산자> <쀑위 연산자> <였륞쪜 플연산자>

 

역시나 읎번에도 쀑위 연산자 표현식을 잘 파싱하는지 테슀튞하는 테슀튞 윔드부터 삎펎볎자.

 

// parser/parser_test.go

func TestParsingInfixExpressions(t *testing.T) {
	infixTests := []struct {
		input string
		leftValue int64
		operator string
		rightValue int64
	}{
		{"2 + 2;", 2, "+", 2},
		{"2 - 2;", 2, "-", 2},
		{"2 * 2;", 2, "*", 2},
		{"2 / 2;", 2, "/", 2},
		{"2 > 2;", 2, ">", 2},
		{"2 < 2;", 2, "<", 2},
		{"2 == 2;", 2, "==", 2},
		{"2 != 2;", 2, "!=", 2},
	}
	
	for _, tt := range infixTests {
		l := lexer.New(tt.input)
		p := New(l)
		program := p.ParseProgram()
		checkParserErrors(t, p)
		
		if len(program.Statements) != 1 {
			t.Fatalf("program.Statements does not contain 1 statements. got=%d", len(program.Statements))
		}
		
		stmt, ok := program.Statements[0].(*ast.ExpressionStatement)
		if !ok {
			t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T", program.Statements[0])
		}
		
		exp, ok := stmt.Expression.(*ast.InfixExpression)
		if !ok {
			t.Fatalf("stmt is not ast.InfixExpression. got=%T", stmt.Expression)
		}
		
		if !testIntegerLiteral(t, exp.Left, tt.leftValue) {
			return
		}
		if exp.Operator != tt.operator {
			t.Fatalf("exp.Operator is not '%s'. got=%s", tt.operator, exp.Operator)
		}
		if !testIntegerLiteral(t, exp.Right, tt.rightValue) {
			return
		}
	}
}

 

테슀튞 윔드 자첎는 전위 표현식 테슀튞 윔드와 거의 유사하닀. 닀만, 쀑위 연산자 표현식에 사용되는 쀑위 연산자가 2개의 플연산자륌 갖는 읎항 연산자읎닀 볎니, 연산자륌 Ʞ쀀윌로 왌쪜, 였륞쪜에 있는 각 표현식을 검슝하는 if 구묞만 추가되었을 뿐읎닀. 

 

위 테슀튞 윔드에서 못볎던 구조첎가 나타나Ʞ 시작했닀. 바로 ast.InfixExpression 읎닀. 읎제 읎 구조첎륌 정의핎볎자. 읎 구조첎 역시 AST 녞드로 표현되얎알 하므로 마찬가지로 Expression 읞터페읎슀륌 충족시쌜볎자.

 

// ast/ast.go

type InfixExpression struct {
	Token token.Token
	Left Expression
	Operator string
	Right Expression
}

func (ie *InfixExpression) expressionNode()      {}
func (ie *InfixExpression) TokenLiteral() string { return ie.Token.Literal }
func (ie *InfixExpression) String() string {
	var out bytes.Buffer
	
	out.WriteString("(")
	out.WriteString(ie.Left.String())
	out.WriteString(" "+ ie.Operator +" ")
	out.WriteString(ie.Right.String())
	out.WriteString(")")
	
	return out.String()
}

 

InfixExpression 구조첎의 멀버륌 볎멎 전위 표현식을 나타낮는 구조첎읞 PrefixExpression곌 달늬 Left 멀버가 추가되었음을 볌 수 있닀.

 

읎제 우늬는 쀑위 연산자 표현식읎 등장했을 때, 파싱핎쀄 로직을 작성핎볎도록 하자. 가장 뚌저 작성할 윔드는 쀑위 연산자륌 파싱하는 파싱 핚수륌 정의하고 등록시쌜알 한닀. 닀만, 읎 때 죌의할 점은 ì–Žë–€ 연산자 토큰을 만났을 때 전위 연산자 파싱 핚수륌 가젞올 것읞지, 쀑위 연산자 파싱 핚수륌 가젞올 것읞지 잘 판닚핎알 한닀. 닚적읞 예로, - 연산자가 등장했을 때 하나의 명령묞 낎에 서로 닀륞 Ʞ능을 하는 파싱 핚수륌 가젞와알 할 때가 있닀.

 

-5 - 3;

 

가장 앞에 있는 - 는 분명 전위 연산자읎고, 읎륌 파싱할 때는 전위 연산자 파싱 핚수륌 가젞와알 한닀. 하지만 가욎데에 있는 - 는 쀑위 연산자읎고, 쀑위 연산자 파싱 핚수륌 가젞와알 한닀. 바로 읎런 겜우륌 잘 구분핎죌얎알 한닀는 것읎닀. 밑에서 소개하겠지만 읎러한 구분은 연산자 우선순위륌 활용할 것읎닀. 따띌서 특정 연산자의 우선순위륌 저장하는 map 자료구조륌 새롭게 하나 정의핎볎자.

 

// parser/parser.go

var precedences = map[token.TokenType]int {
	token.EQ: EQUALS,
	token.NOT_EQ: EQUALS,
	token.LT: LESSGREATER,
	token.GT: LESSGREATER,
	token.PLUS: SUM,
	token.MINUS: SUM,
	token.SLASH: PRODUCT,
	token.ASTERISK: PRODUCT,
}

 

위 map 자료구조의 value에 핎당하는 값은 [3-1] 목찚에서 상수로 정의한 각 연산자의 우선순위 값읎닀. 읎로썚 특정 토큰에 대한 우선순위 값을 맀겚녌 셈읎닀. 예륌 듀얎, == 와 != 토큰은 같은 우선순위읎고, > 와 < 가 같은 우선순위, + 와 -가, * 와 /가 서로 같은 우선순위읎닀.

 

귞늬고 연산자 우선순위 ꎀ렚하여 유틞성 핚수륌 2가지륌 정의할 것읎닀. 읎는 파서가 바띌볎고 있는 토큰, 귞늬고 ê·ž 닀음의 토큰에 맀핑되는 연산자 우선순위륌 반환하는 핚수읎닀.

 

// parser/parser.go

func (p *Parser) peekPrecedence() int {
	if p, ok := precedences[p.peekToken.Type]; ok {
		return p
	}
	return LOWEST
}

func (p *Parser) curPrecedence() int {
	if p, ok := precedences[p.curToken.Type]; ok {
		return p
	}
	return LOWEST
}

 

위 소슀윔드의 로직은 맀우 간닚하니 섀명은 따로 하지 ì•Šê² ë‹€. ê·ž 닀음은 파서륌 쎈Ʞ화할 때 각 쀑위 연산자 토큰에 맞는 파싱 핚수륌 등록하자.

 

// parser/parser.go

func New(l *lexer.Lexer) *Parser {
	(... 생략 ...)
    
	p.prefixParseFns = make(map[token.TokenType]prefixParseFn)
	p.registerPrefix(token.IDENT, p.parseIdentifier)
	p.registerPrefix(token.INT, p.parseIntegerLiteral)
	p.registerPrefix(token.BANG, p.parsePrefixExpression)
	p.registerPrefix(token.MINUS, p.parsePrefixExpression)
	// 쀑위 연산자듀에 대한 파싱 핚수 추가
	p.infixParseFns = make(map[token.TokenType]infixParseFn)
	p.registerInfix(token.MINUS, p.parseInfixExpression)
	p.registerInfix(token.PLUS, p.parseInfixExpression)
	p.registerInfix(token.MINUS, p.parseInfixExpression)
	p.registerInfix(token.SLASH, p.parseInfixExpression)
	p.registerInfix(token.ASTERISK, p.parseInfixExpression)
	p.registerInfix(token.EQ, p.parseInfixExpression)
	p.registerInfix(token.NOT_EQ, p.parseInfixExpression)
	p.registerInfix(token.LT, p.parseInfixExpression)
	p.registerInfix(token.GT, p.parseInfixExpression)
	return p

 

새로욎 메서드가 등장했닀. 바로 parseInfixExpression읎닀. 읎 메서드의 생김새륌 볎자.

 

// parser/parser.go

func (p *Parser) parseInfixExpression(left ast.Expression) ast.Expression {
	expression := &ast.InfixExpression{
		Token: p.curToken,
		Left: left,
		Operator: p.curToken.Literal,
	}
	
	precedence := p.curPrecedence()
	p.nextToken()
	
	expression.Right = p.parseExpression(precedence)
	return expression
}

 

읎전에 작성한 parsePrefixExpression 메서드 생김새와 거의 유사하닀. 찚읎점은 precedence 띌는 변수에닀가 현재 토큰의 연산자 우선순위륌 반환한닀. 여Ʞ에서도 nextToken 핚수륌 혞출핎서 현재 위치륌 닀음 토큰윌로 옮ꞎ닀. 여Ʞ서도 읎 동작은 왜 수행할까?

parseInfixExpression 메서드륌 혞출한닀는 것은 파서가 현재 바띌볎는 토큰읎 쀑위 연산자(ex. +, - ..) 토큰읎띌는 것읎닀. 귞러므로 nextToken 핚수륌 한 번 혞출하게 되멎 현재 위치의 토큰읎 쀑위 연산자의 였륞쪜에 있는 표현식윌로 읎동하게 된닀. 귞늬고 난 ë’€ parseExpression 핚수륌 혞출핎서 InfixExpression 구조첎의 Right 멀버에 할당핎쀀닀.

 

아직 끝난 것은 아니닀. 쀑위 연산자 표현식 파싱 Ʞ능읎 추가됚에 따띌 아까 위에서 정의한 parseExpression 메서드의 개선읎 필요하닀. 현재 parseExpression 메서드의 생김새는 닀음곌 같닀.

 

// parser/parser.go

func (p *Parser) parseExpression(precedence int) ast.Expression {
	prefix := p.prefixParseFns[p.curToken.Type]
	if prefix == nil {
		p.noPrefixParseFnError(p.curToken.Type)
		return nil
	}
	leftExp := prefix()

	return leftExp
}

 

현재는 전위 연산자 표현식만 처늬할 수 있도록 되얎 있는 상태읎며 precedence 띌는 읞자 슉, 연산자 우선순위 값을 활용하지도 않는 상태읎닀. 쀑위 연산자 표현식도 처늬할 수 있도록 개선한 parseExpression 메서드는 닀음곌 같닀.

 

// parser/parser.go

func (p *Parser) parseExpression(precedence int) ast.Expression {
	prefix := p.prefixParseFns[p.curToken.Type]
	if prefix == nil {
		p.noPrefixParseFnError(p.curToken.Type)
		return nil
	}
	leftExp := prefix()
	
	for !p.peekTokenIs(token.SEMICOLON) && precedence < p.peekPrecedence() {
		infix := p.infixParseFns[p.peekToken.Type]
		if infix == nil {
			return leftExp
		}
		p.nextToken()
		
		leftExp = infix(leftExp)
	}

	return leftExp
}

 

연산자 우선순위륌 사용핎서 for loop륌 도는데, 구첎적윌로 얎떻게 동작하는지는 닀음 포슀팅에서 프랫 파서의 동작 원늬륌 삎펎볎멎서 읎핎핎볎자. 궁ꞈ하닀멎 지ꞈ까지 작성한 소슀윔드에 대한 읞풋 예시로 "2 == 2;" 륌 넣는닀고 가정하고 윔드의 흐늄을 따띌가볎자.

반응형