10. 字符串表达式支持

159 阅读4分钟

字符串表达式支持

在这一节中,我们要加入对于字符串表达式的支持。

我们需要做如下的更改:

  1. 在词元(Token)源码token.go中加入一个新的词元(Token)类型
  2. 在词法分析器(Lexer)源码lexer.go中加入对字符串的识别
  3. 在抽象语法树(AST)的源码ast.go中加入字符串对应的抽象语法表示。
  4. 在语法解析器(Parser)的源码parser.go中加入对字符串的语法解析。
  5. 在对象(Object)系统中的源码object.go中加入一个新的字符串对象(String Object)
  6. 在解释器(Evaluator)的源码eval.go中加入对字符串的解释。

词元(Token)更改

第一处改动

//token.go
const (
	//...
	TOKEN_STRING     //""

第二处改动

//token.go
//词元类型的字符串表示
func (tt TokenType) String() string {
	switch tt {
	//...
	case TOKEN_STRING:
		return "STRING"
	}
}

词法分析器(Lexer)的更改

我们需要在词法分析器(Lexer)的NextToken()函数中加入对字符串的识别:

//lexer.go

//获取下一个词元(Token)
func (l *Lexer) NextToken() token.Token {
	//...
	switch l.ch {
	//...
	default:
		if isDigit(l.ch) {
			tok.Literal = l.readNumber()
			tok.Type = token.TOKEN_NUMBER
			tok.Pos = pos
			return tok
		}
		//...
		} else if l.ch == 34 { //34的ASCII码对应的是双引号
			if s, err := l.readString(l.ch); err == nil {
				tok.Type = token.TOKEN_STRING
				tok.Pos = pos
				tok.Literal = s
				return tok
			} else {
				tok.Type = token.TOKEN_ILLEGAL
				tok.Pos = pos
				tok.Literal = err.Error()
				return tok
			}
		} else {
			tok = newToken(token.TOKEN_ILLEGAL, l.ch)
		}
	}
}

第16行的else if判断,我们加入了对字符串的识别。下面是实际识别字符串的代码:

//lexer.go
func (l *Lexer) readString(r rune) (string, error) {
	var ret []rune
eos:
	for {
		l.readNext()
		switch l.ch {
		case '\n':
			return "", errors.New("unexpected EOL")
		case 0:
			return "", errors.New("unexpected EOF")
		case r: //遇到了字符串结束符
			l.readNext()
			break eos //eos:end of string
		case '\\': //如果有转义字符
			l.readNext()
			switch l.ch {
			case 'b':
				ret = append(ret, '\b')
				continue
			case 'f':
				ret = append(ret, '\f')
				continue
			case 'r':
				ret = append(ret, '\r')
				continue
			case 'n':
				ret = append(ret, '\n')
				continue
			case 't':
				ret = append(ret, '\t')
				continue
			}
			ret = append(ret, l.ch)
			continue
		default:
			ret = append(ret, l.ch)
		}
	}

	return string(ret), nil
}

上面的代码看起来有点多。处理逻辑大致如下:

循环读取下一个字符,如果遇到字符为双引号(即字符串结束符)的话就退出循环。如果遇到了文件结束标志(EOF),那么就表明处理到文件结束也没有找到字符串的结束标记,就报错。其它的处理主要是处理字符串中的转义字符。

抽象语法树(AST)的更改

字符串的抽象语法表示与我们第一节介绍的数字字面量(NumberLiteral)的抽象语法表示几乎类似,下面直接上代码:

//ast.go
//'字符串'的抽象语法表示
type StringLiteral struct {
	Token token.Token
	Value string //字符串的值
}

func (s *StringLiteral) Pos() token.Position {
	return s.Token.Pos
}

//结束位置 = 开始位置 + 字符串的长度
func (s *StringLiteral) End() token.Position {
	length := utf8.RuneCountInString(s.Value)
	return token.Position{Filename: s.Token.Pos.Filename, Line: s.Token.Pos.Line, 
                          Col: s.Token.Pos.Col + length}
}

func (s *StringLiteral) expressionNode()      {}
func (s *StringLiteral) TokenLiteral() string { return s.Token.Literal }
func (s *StringLiteral) String() string       { return s.Token.Literal }

这里的代码也比较简单,也无需做太多解释。

语法解析器(Parser)的更改

我们需要做两处更改:

  1. 对新增加的TOKEN_STRING词元类型注册前缀表达式回调函数(代码中的5行)
  2. 新增解析字符串表达式的函数parseStringLiteral()(代码中的9-11行)
//parser.go
func (p *Parser) registerAction() {
	p.prefixParseFns = make(map[token.TokenType]prefixParseFn)
	//...
	p.registerPrefix(token.TOKEN_STRING, p.parseStringLiteral) //对字符串注册前缀表达式回调函数
}

//解析字符串
func (p *Parser) parseStringLiteral() ast.Expression {
	return &ast.StringLiteral{Token: p.curToken, Value: p.curToken.Literal}
}

对象系统的更改

我们需要往对象系统中增加一个字符串对象(String Object)。因为也是我们熟悉的代码且比较简单,所以也是直接上代码:

//object.go

const (
	//...
	STRING_OBJ       = "STRING"
)

//字符串对象
type String struct {
	String string //存储字符串的值
}

func (s *String) Inspect() string {
	return s.String
}

func (s *String) Type() ObjectType { return STRING_OBJ }

//一个工具(utility)函数,用来生成新的字符串对象
func NewString(s string) *String {
	return &String{String: s}
}

解释器(Evaluator)的更改

我们需要在解释器(Evaluator)的Eval函数的switch分支中加入对字符串表达式的处理:

//eval.go

func Eval(node ast.Node, scope *Scope) (val Object) {
	switch node := node.(type) {
	//...
	case *ast.StringLiteral: //字符串表达式
		return evalStringLiteral(node, scope)
	}

	return nil
}

//解释'字符串表达式'
func evalStringLiteral(s *ast.StringLiteral, scope *Scope) Object {
	return NewString(s.Value) //生成一个新的字符串对象返回
}

另外,我们希望给字符串加入连接操作符,即允许两个字符串相加,生成一个新的字符串,类似下面这样:

let x = "Hello " + "World!" // x = "Hello World!"

因此我们需要修改evalInfixExpression()函数:

//eval.go
func evalInfixExpression(node *ast.InfixExpression, left, right Object, scope *Scope) Object {
	switch {
	case left.Type() == NUMBER_OBJ && right.Type() == NUMBER_OBJ:
		return evalNumberInfixExpression(node, left, right, scope)
	case left.Type() == STRING_OBJ && right.Type() == STRING_OBJ:
		return evalStringInfixExpression(node, left, right, scope)
}

func evalStringInfixExpression(node *ast.InfixExpression, left, right Object, scope *Scope) Object {
	leftVal := left.(*String).String //取出字符串对象中保存的字符串
	rightVal := right.(*String).String

	switch node.Operator {
	case "+":
		return NewString(leftVal + rightVal)
	default:
		return newError(node.Pos().Sline(), ERR_INFIXOP, 
                        left.Type(), node.Operator, right.Type())
	}
}

我们给evalInfixExpression()函数的switch语句增加了一个case分支。第10行的evalStringInfixExpression函数是实现字符串拼接的函数。

是不是有一种【水到渠成】的感觉?别漂,我们的喜鹊(magpie)的翅膀还没有涨硬呢。

测试

下面我们写一个简单的程序测试一下:

//main.go
func TestEval() {
	tests := []struct {
		input    string
		expected string
	}{
		{`let x = "hello world"; x`, "hello world"},
        {`let x = "hello " + "world"; x`, "hello world"}
	}

	for _, tt := range tests {
		l := lexer.NewLexer(tt.input)
		p := parser.NewParser(l)
		program := p.ParseProgram()

		scope := eval.NewScope(nil, os.Stdout)
		evaluated := eval.Eval(program, scope)
		if evaluated != nil {
			if evaluated.Inspect() != tt.expected {
				fmt.Printf("%s\n", evaluated.Inspect())
			} else {
				fmt.Printf("%s = %s\n", tt.input, tt.expected)
			}
		}
	}
}

func main() {
	TestEval()
}

通过前十节的学习,我们的小喜鹊(magpie)的五脏六腑已经长全,也有了强有力的腿部,翅膀也涨慢慢变得更硬了。

第二部分,小喜鹊就将进入一个新的领域:飞行。