DEMO
ini
let a = 1;
let b = 2;
let sum = 0;
for (let i = 0; i < 10; i = i + 1) {
sum = sum + a + b + i;
sum = sum + 2 * (a + b);
}
let addNewer = fn(x, y) {
return fn(z) {
return x + y + z;
}
}
let addOne = addNewer(1, 2);
let result = addOne(sum);
let str = "abcd";
let arr = ["bc", "cd", "de", "ef", "fg"];
let m = {"ab": 20, "fg": 10}
if (result > 100) {
return m[arr[len(str)]] + result; // 148
} else {
return result;
}
这个简单的Demo使用了几乎所有的语法,从let
语句到if-else
再到for
循环,从函数调用到闭包再到高级类型,比如字符串,数组,map和内建函数。
项目地址这里有详细的测试用例。
概览
在正式开始之前呢,先简单的概括一下。本文是基于《Writing an interrupter in golang》创作的,原文作者非常幽默,用词语气生动且鼓舞,所以很建议大家去读读原文。总共二百页的PDF,差不多一周就可以结束,在阅读这本书的期间,我受到了作者的多次鼓舞,也能感受到他的激情和对于读者理解他创作这本书的动力的期待。那么我,也会试着用这样的方式去表达我的"读后感",同时为了加深理解,这就是此文创作的初衷。
并非一比一复刻
本文对于原文,增加了一些不存在的,且重新设计的结构组织,所以并没有完全按照原文的方式来编写,可不要读的时候回来大骂我在乱写哦。
比如,原文中没有实现for
循环,这在编程语言中似乎是不太接受的,同时也没有实现赋值语句。
原文中也没有实现本地调用,虽然这一点给了类似的方案,不过我选择了自己的实现方式。
最后一点就是关于解析器部分的语句分类(详细看解析器章节),我按照个人理解划分了一些不同的选择。
原文中称我们所创作的编程语言是"猴子"语言,Monkey,因为猴子很聪明,很灵活,就像我们要创作的这个动态解释型 语言一样,当然Python
和JS
也是此类型。
而我们的语言,是类似C语言
的形式,但是又更加自由。
为什么是解释型
当然是简单啦!如果我们要做一个编译型语言,且不说难度问题,就算我们做出来了,那恐怕也不是简单的一篇文章能介绍完的。人类编程语言发展至今,针对编译器的优化,编译原理的分析,还有设计,光是论文和讨论,就足够喝一壶了。
哪怕是选择最简单的编译器去实现,涉及到的复杂度也难以形容。
选择解释型,可以依托宿主语言本身的设计,而仅仅实现简单的前端编译和计算,同时又可以系统的了解一本语言的诞生。这正好符合我们的目的。
对,没错,本文的目的是为了学习,而不是为了生产制造一门"I defeat Python by my new language!",诚然Python性能已经拉胯到成为计量单位了。
不过随着时代的发展,现在的语言并不单单是某一种类型,就好比Java
,一开始是动态强类型,而且Java比较特殊,使用了字节码这种中间形式作为初级编译的结果;而为了性能,Java
也实现了JIT
和AOT
技术,所以现在很难说它是某一种类型的语言。
类似的,即使是被大家所熟知的JS
,也会对热点代码标记分析,转化成二进制代码的形式提高性能。
less is more
我们的目的并不是要造一个牛逼哄哄的新时代语言,相反,我们的目的是为了通过制作一门简单的语言,去学习词法分析 ,AST抽象语法树 构建,基于AST执行,条件选择判断,执行流跳转和循环,外加上类型系统和表达式计算这些编程语言的组成,即,我们是为了学习,而不是为了生产,设计到的设计,都是尽可能简化和便于理解。
可能你会说很多优化,比如变量检查,边界消除,分支预测,表达式替换这些,并不是不能做,但是如果创作下去,不仅分担了心智,还增加了出错的可能。就好比解释器的选择,就有很多种,我们选择了最易于理解但是最完备的一种,它有完整的论文支持,足够简单强大。
那么这样一来,很多实现,都是为了学习和简单化理解,这点请读者知悉。
WhyGolang?
因为简单啦!是的,Go这本语言足够简单,语法简单,使用简单,自带GC,一切都恰好,而不用我们在实现解释器时,把更多的精力放在那些无关紧要的事上,比如内存问题,比如封装问题。
HereWeGo
Monkey是什么样的呢?它作为一个编程语言,需要有哪些呢?语句?类型?是面向对象还是面向过程?值类型是如何的呢?
那就让我们一个一个来分析吧!
类型系统
任何语言,都必须有一个类型系统,为什么?因为在执行过程中,必须确定当前被执行目标的解释方式,而类型,就是对它解释方式的定义。
integer
作为大家的老朋友,我们当然对整数不陌生,规避掉复杂的大小和有无符号,我们规定Monkey的整数类型为int64。且只有这么一种数字类型。
那浮点数呢?哈哈,朋友,我们当然可以实现,但是为了简单,也为了不必要的解析,这件事就留给你去做啦!
boolean
作为条件判断的必要元素,布尔类型当然必不可少咯!只是需要说明的是,在Monkey里,我们使用no-false
来作为判断依据,只要一个表达式的结果不是false,那就只能为true。
听起来很废话?null
应该返回false,1应该返回true,true返回true,false返回false,如果什么都没有呢?默认返回true。
string
字符串作为老熟客,肯定不能被忘记。只是我们并不会立即实现它,我们会在构建好我们自己的语言王国后,再回过头去实现它。
实现的方式,有很多。比方说:字符串本身就是一堆integer组成的不是吗?
slice
数组?动态数组?随便你怎么说,本质上是一堆值组成的值,实现的方式也类似string,我们先把坑踩完,再去讨论这个
map
作为一个解释型语言,JS和Python都有的玩意儿,我们怎么能缺失呢?
表达式
在正式开始介绍表达式之前,请大家思考一下,Monkey是一门C-like
的语言,那么假如我们用;
作为分隔符,去分割源代码,是不是会得到一条一条的语句呢?
以一个简单的C为例:
ini
int main() {
int a = 1;
if (a == 1) {
printf("ok\n");
} else {
int b = 2;
for (int j = 0; j < 2; j ++) {
if (b > 1) {
break;
}
continue;
}
}
int c = add(a, 2);
c = c + 2 * (1 + 2);
return c;
}
int add(int a, int b) {
return a + b;
}
几乎可以说,这段小小的代码包含了Monkey需要的所有语法,当然还有一些C没法表示。
分割完成之后,可以发现整个程序由一堆语句 (Statement)组成 ,而每个语句包含了0到多个的表达式 。那么想要解析一门语言,就必须区分语句和表达式。
什么是表达式?
表达式是可以产生值 ,由一个或多个Token组成的结构。Token先简单理解成空格分隔的块即可。所以表达式的重点是产生值。
且表达式可以组合 ,组合之后的表达式得到一个新的表达式。
所以此刻是不是清晰明了了呢?
表达式 ->
合成新的表达式 ->
语句 ->
多个语句 ->
程序。
此时一整个程序就被拆解啦!
函数呢?
ini
let add = fn(a, b) {return a + b;}
let c = add(1, 2);
函数似乎有点特殊,但是观察可以看到,我们把函数定义当成了一个表达式 ,并把它的值,即函数本身赋值给了一个标识符(变量名的意思),之后通过标识符找到函数本身调用它。
所以在Monkey中,函数是一等公民,是表达式本身,是可以像表达式一样使用的值。
所以闭包,高阶函数这些概念,在Monkey中也是存在的。我们后面细说。
标识符?表达式?
对的,标识符也是表达式。啊?真的嘛?
想一下:
ini
let a = 1;
let b = a;
这里的a
显然是标识符 ,但是在第二行里,a
又作为表达式给b
赋值,不是吗?所以标识符本身也是表达式的一种,它们可以赋值给另一个标识符或者作为参数把值传参给函数。
语句
上面说了语句是组成程序的基础,语句本身由表达式组成,但是语句不只是由表达式组成。
let
在Monkey中
ini
let a = 1;
let a = 1 + 1;
let a = a + 1;
let语句负责绑定一个表达式的值,到一个标识符 上,let不仅仅绑定了值,还同时创建了标识符。
总结一下就是:let <identifier> = <expression>;
assignment
在原文中,并没有赋值语句
ini
let a = 1;
a = 2;
a = a + 1;
区别很明显,就是少了let开头的let语句。但是区别也很大,赋值语句只能用于已经存在的标识符。所以赋值语句负责把表达式绑定到已经存在的标识符上。
总结一下就是:<identifier> = <expression>;
return
Monkey中的return语句很简单,而且仅支持一个值得返回。
kotlin
return 1;
return a;
return a + 1;
return add(1, 2);
总结一下就是:return <expression>;
if-else
ini
if (true) {
let a = 1;
} else {
let b = 1;
}
条件判断语句也很简单,一个if加一个条件,当然也可以只有if:
arduino
if (false) {}
总结一下就是:if (<expression>) {} else {}
,其中,else可选。
for
这是我们新增加的部分:
ini
for (let i = 0; i < 10; i = i + 1) {
let a = 1;
if (i == 5) {
continue;
}
if (i == 6) {
break;
}
}
语法很像C不是吗?
总结一下就是:for (<init statement>;<condition expression>;<update statement>) {}
注意到这里的for循环内部,由一个初始化语句,一个条件表达式和一个更新语句组成。其中初始化语句可以为let语句,也可以为赋值语句,而更新语句必须为赋值语句。
词法器
上面的概览部分,简单的介绍了Monkey语言,现在我们开始正式进行解释器的编写。
我们的目的是,创造一个解释器,输入程序源代码,输出一个值。在开始之前,我们需要先解析源代码。
为什么要解析呢?或者说,我们要解析成什么样呢?最通俗易懂的方式就是把空格去掉,把语句分隔符去掉,按照效果划分成一个又一个的Token。
什么是Token?
let
是Token,=
是Token,123
也是Token... ...所以Token是什么?Token就是组成程序的最小可理解语义 。语义又是什么呢?操作符加减乘除具备算术运算的语义 ,标识符(变量名)具备绑定值的语义 ,()
具备优先级和函数调用的语义 ,{}
具备划分语句块的语义。
所以我们的第一步,就是进行词法分析,把源代码所有的最小可理解语义包装成Token并输出。每个Token包含类型和文本值。
在go里,我们这样定义一个Token:
python
type Type string
// Token represents a token in the Monkey programming language
// every single item split by whitespace in the input is a token, so
// "let" is a token, "5" is a token, "+" is a token, etc.
type Token struct {
Type Type
Literal string // the text value corresponding to the token, like "let" or "5" or "+"
}
回过头看看前面演示的简单代码,很容易总结出所有的Token类型。不过还有些我们暂时用不到,就先一起放在这啦:
Token类型
ini
const (
ILLEGAL = "ILLEGAL"
EOF = "EOF"
// identifiers + literals
IDENT = "IDENT" // add, foobar, x, y, ...
INT = "INT" // 123456
STRING = "STRING"
// operators
ASSIGN = "="
PLUS = "+"
MINUS = "-"
BANG = "!"
ASTERISK = "*"
SLASH = "/"
LT = "<"
GT = ">"
// delimiters
COMMA = ","
SEMICOLON = ";"
LPAREN = "("
RPAREN = ")"
LBRACE = "{"
RBRACE = "}"
LBRACKET = "["
RBRACKET = "]"
COLON = ":"
// keywords
FUNCTION = "FUNCTION"
LET = "LET"
TRUE = "TRUE"
FALSE = "FALSE"
IF = "IF"
ELSE = "ELSE"
RETURN = "RETURN"
FOR = "FOR"
BREAK = "BREAK"
CONTINUE = "CONTINUE"
// comparison operators
EQ = "=="
NOT_EQ = "!="
)
我们涉及到的所有的类型如上所示,也基本对应了Monkey语言中的所有除了自定义标识符之外的字符。我想这里并没有什么特殊的地方吧,我们要做的只是在解析的时候,找到当前解析位置对应的类型即可。
文本解析器
现在有了Token,就开始把输入变成输出把!
输入一个string类型,表示程序源代码,输出一个Token列表,表示解析之后的结果,听起来很简单,事实也确实如此哈!
不过我们稍微变更一下,我们不输出Token列表,取而代之的是,我们使用一个方法,每次调用它,都可以得到下一个Token,然后更新当前读取的位置。
类似:for { token = lexer.next() }
同时我们还需要一个关键字列表,万一把关键词当标识符(变量名)解释掉,那可就麻烦大了!
vbnet
var (
keywords = map[string]token.Type{
"fn": token.FUNCTION,
"let": token.LET,
"true": token.TRUE,
"false": token.FALSE,
"if": token.IF,
"else": token.ELSE,
"return": token.RETURN,
"for": token.FOR,
"break": token.BREAK,
"continue": token.CONTINUE,
}
)
对应的解析器结构也就呼之欲出了:
csharp
type Lexer struct {
input string
currPosition int
nextPosition int // using this to peek ahead
char byte
}
func (l *Lexer) NextToken() token.Token {
var tok token.Token
l.skipWhitespace() // 跳过空白符
switch l.char { // 根据当前字符的类型,构建对应的Token
case '=':
if l.peekChar() == '=' { // 如果=后面还是=,说明是==比较运算符,特殊处理一下
ch := l.char
l.readChar()
tok = token.Token{Type: token.EQ, Literal: string(ch) + string(l.char)}
} else { // 否则就是普通的赋值操作
tok = newToken(token.ASSIGN, l.char)
}
case '!':
if l.peekChar() == '=' { // 如果!后面是=,说明是!=运算符,特殊处理一下
ch := l.char
l.readChar()
tok = token.Token{Type: token.NOT_EQ, Literal: string(ch) + string(l.char)}
} else { // 否则就是取反运算符
tok = newToken(token.BANG, l.char)
}
case ';':
tok = newToken(token.SEMICOLON, l.char)
case '(':
tok = newToken(token.LPAREN, l.char)
case ')':
tok = newToken(token.RPAREN, l.char)
case ',':
tok = newToken(token.COMMA, l.char)
case '+':
tok = newToken(token.PLUS, l.char)
case '-':
tok = newToken(token.MINUS, l.char)
case '*':
tok = newToken(token.ASTERISK, l.char)
case '/':
tok = newToken(token.SLASH, l.char)
case '<':
tok = newToken(token.LT, l.char)
case '>':
tok = newToken(token.GT, l.char)
case '{':
tok = newToken(token.LBRACE, l.char)
case '}':
tok = newToken(token.RBRACE, l.char)
case 0:
tok.Literal = ""
tok.Type = token.EOF
default: // 如果都不是,说明是一个标识符或者纯数字文本
if isLetter(l.char) {
tok.Literal = l.readIdentifier()
tok.Type = LookupIdent(tok.Literal) // 这里负责区分是否是关键词
return tok
} else if isDigit(l.char) {
tok.Literal = l.readNumber()
tok.Type = token.INT
return tok
} else { // 如果都不是,抛出错误
tok = newToken(token.ILLEGAL, l.char)
}
}
l.readChar() // 移动到下一个字符
return tok
}
上面提到的一些辅助方法,比如读取一个字符串并判断是否为标识符还是关键词,读取一个双引号""
分割的用户定义的字符串等,都在项目源代码里,我们为了篇幅就不展开了。
词法解析器结束!很简单?很简单!
解析器
解析器是本文的重点,因为它负责串联词法器和执行器,并把词法器的输出变成执行器可读的输入。
如何入手?
好,不是,什么好?现在到哪一步了?哦哦到了解析器了。到底怎么入手继续啊!
回到程序本身,先来看一段伪代码:
ini
let a = 1;
if (a == 1) {
put("ok")
} else {
let b = 2;
for (let j = 0; j < 2; j ++) {
if (b > 1) {
break;
} else {
continue;
}
}
}
let add = fn(a, b) {
return a + b;
}
let c = add(a, 2);
c = c + 2 * (1 + 2);
return c;
这就是一开始那段C代码的伪代码版本。却几乎用到了所有的语法。
注意到,整个程序包含多个Statement ,即语句;而每个语句包含零或多个表达式。比如:
let语句
包含表达式a
和表达式1
,还有一个=
运算符。
if语句
包含一个条件判断表达式 和一堆语句组成的语句块(BlockStatement);其中语句块包含多个语句。
for
也是同理,包含两个语句:一个let语句 一个赋值语句 ,还有一个条件表达式,最后来个语句块。
函数
的函数头则定义了多个标识符,并在执行时从中读取值,函数体则是语句块。
最后是赋值语句
,包含多个表达式,return语句
则包含一个表达式。
所以?所以?所以可以说一个程序是由多个包含零至多个表达式和运算符或其他链接方式的语句组成。
拨云见明
如果我们有一个函数,可以不断地循环调用lexer.NextToken()
并基于这些Token构建一个又一个Statement,之后从头遍历构建出来的Statement数组,逐个执行并保存执行结果为新的状态,当数组遍历完,程序结束。
听起来是不是很有道理呢?恭喜你结课了,这就是本文的全部。
哈哈!我知道你还会回来的。如果你还愿意继续读下去,那么我们可以开始本章节了。如果你稍微看了一下本文的组织,会发现逐个执行应该是后面执行器的内容,所以"循环调用NextToken()
并构建一个Statement数组
"就是本章的内容。
通过上面的梳理,大概可以给出如下的解析器定义:
- 一个
Program结构体
,包含一个Statement数组 - 一个
Statement接口
,其实现遍布上述语句,比如let,if-else,for等。 - 一个
Expression接口
,其实现代表着各种各样的表达式,比如使用纯数字给标识符赋值时,这个纯数字表达式就是它的实现之一,使用函数调用给标识符赋值时,这个函数调用就是一种表达式,所以也实现了这个接口,标识符也可以赋值给别人,所以标识符也是,等等。
既然一切清晰了起来,就开始着手搭建吧!
AST
简单来说,一个程序有控制跳转流,执行计算流,分支选择流等各种执行路径。
如果我们可以用某种方式,表示出这种路径,之后执行器执行时,只需要检查路径,并做出对应的操作,是不是就更简单了呢?
比如
xml
if (<expression>) {<block1>} else {<block2>}
如果可以描述为:
yaml
type: if-else
condition: expression
consequence: {<block1>}
alternative: {<block2>}
或者说一个这样的伪代码:
xml
for (<init>;<condition>;<update>) {<block>}
可以被这样描述:
yaml
type: for
init: <init>
condition: <condition>
update: <update>
body: <block>
或者简单的表达式,也可以这样描述:
css
a = b * (c / (d + e))
为:
less
type: expression
left: b:
op: *
right:
type: expression
left: c
op: /
right:
type: expression
left: d
op: +
right: e
那么对于我们后续的执行器来说,只要判断type
然后做出相应的计算,并执行指定的代码块,是不是就完美了呢?
这种抽象的,用来描述程序逻辑选择,执行路径和条件判断的结构称为AST(抽象语法树) 。
AST描述的执行流在执行完毕后,产生一个值,用来描述本次执行的结果,不过并不是每种AST都有值的,表达式一般会产生一个值,而语句一般不会。
为什么要叫它树呢?因为一个AST包含的部分可能引用了别的AST。举个例子,比如在if语句中,if的condition本身就是一个AST,它可能包含两个表达式,用来比较结果,而这两个表达式,本身又是两个AST计算出来的结果。
上面的表达式格式的AST便是一个例子,它不断地引用子AST的值。
AST是解析的核心,因为它包含了全部的执行信息和数据,解释器只需要按照AST计算出来的结果不断走下去,程序就可以被执行。
解释器到手
根据上面的分析,可以给出这样的定义结构:
csharp
type Node interface {
TokenLiteral() string
String() string // for debugging
}
type Statement interface {
Node
statementNode() // marker method
}
type Expression interface {
Node
expressionNode() // marker method
}
type Program struct {
Statements []Statement
}
分别是描述程序的Program,核心是Statement数组。表示语句的Statement和表示表达式的Expression,通过前面的分析,得知语句和表达式都可以被抽象成AST
,所以都实现了Node
类型,表示AST中的一个节点。
解析器怎么定义呢?
首先需要一个lexer
,作为读取Token的方式,并不断调用NextToken()
进行下一步。
一个curr
一个next
两个Token指针,方便进行位置的记录
go
type Parser struct {
l *lexer.Lexer
currToken token.Token
peekToken token.Token
}
很完美是不是!
小试牛刀------let语句
回想一下一个let语句需要什么?
一个**let
关键字**?一个标识符?还有一个表达式 用来给标识符赋值!所以let语句
节点在AST中的定义如下:
go
type LetStatement struct {
Token token.Token // token.LET
Name *Identifier
Value Expression
}
func (ls *LetStatement) statementNode() {}
func (ls *LetStatement) TokenLiteral() string {
return ls.Token.Literal // aka "let"
}
并且实现了Statement
接口。
那么接下来就是准备解析了(构建抽象语法树节点),解析也很简单,只要遇到表示let
的Token,就直接构建即可:
css
func (p *Parser) parseStatement() ast.Statement {
switch p.currToken.Type {
case token.LET:
return p.parseLetStatement()
}
}
func (p *Parser) parseLetStatement() *ast.LetStatement {
stmt := &ast.LetStatement{Token: p.currToken}
if !p.expectPeekAndAdvance(token.IDENT) {
return nil
}
stmt.Name = &ast.Identifier{Token: p.currToken, Value: p.currToken.Literal}
if !p.expectPeekAndAdvance(token.ASSIGN) {
return nil
}
p.nextToken()
stmt.Value = p.parseExpression(LOWEST)
if p.peekTokenIs(token.SEMICOLON) {
p.nextToken()
}
return stmt
}
既然当然currToken
已经是let
了,那么下一个一定是标识符Token ,所以直接读取下一个Token为标识符节点(后面会提到它的解析,这里先跳过)即可,再来一个赋值Token的判断 ,看是否符合语法规则,之后就是表达式了。
表达式怎么解析呢?或者说怎么把表达式构建成AST节点呢?这里交给parseExpression
方法完成,那LOWEST又是什么呢?
通关秘籍
首先,我们知道语句本身由各种各样的表达式组成
let
包含两个:标识符和数学运算表达式。for
包含一个let语句,一个表达式,一个更新语句,本质上也是数学运算表达式。if-else
包含一个表达式判断真假,两个数学表达式比较。return
返回一个表达式,也是数学运算表达式。函数
定义的参数列表定义了一堆标识符,但是函数体使用它们作为表达式计算函数调用
参数使用了一堆表达式,依旧是数学运算表达式。赋值
操作右侧也是一堆数学运算表达式。
表达式到底是什么?
ini
1;
1 + 1;
1 + 2 * 3;
a + 1;
a + b * 2;
1 + a + add(2, c);
2 * (3 - (1 + add(1, 2)));
!true;
1 < 2;
true != false;
上面的每个语句都是表达式,即使他们有的一些是纯数字,有的是标识符,有的则是函数调用。
但说到底,标识符本是代表了背后的值,函数调用会返回一个值,所以表达式是由子表达式和操作符混合组成的并最终产生一个结果的序列 。可以看到表达式包含了嵌套关系,而什么最适合处理嵌套逻辑呢?答案是递归。
解析的核心,到这里就变成了解析表达式,即,通过某种方式把表达式构建成AST节点。而语句不过是具有固定组合规律的表达式的集合。
上面观察到,想要解析表达式,就必须搞清楚数学运算,因为表达式本身就是使用具体的值替换掉标识符和函数调用的一个数学运算序列。
数学运算
想一想数学运算有哪些呢?
-
-
-
- /
- ()
- !
- <=
-
=
- ==
- !=
- <
似乎这些就是全部了,那么只要我们枚举所有的不就可以了吗?NoNoNo,那未免有点折腾自己了,而且更重要的是,我们枚举没法处理嵌套情况,比如:1 + 2 * (3 + 4 * (5 + 6))
,你会怎么枚举这种的处理呢?
那怎么入手呢?
小学数学告诉我们,乘法和除法优先级高,需要先于加法和减法运算。
比如,a + b * c
需要先计算b * c
,那么站在b
的视角,*
具有更大的吸引力,当同时遇到+
和*
时,b
会被*
"吸"过去,之后把b * c
当成一个整体,再和a
相加。
理解到这里,再思考一下,一个操作数(或者说表达式),除了首位的,都有两个操作符,称为opL
和opR
,假设在首位前面添加一个op0
,在末尾后面也添加一个op0
,并假设op0
"吸引力"最低,那么岂不是"格式化"整个序列为一个操作数对应两个操作符的行为了吗?
同样的,在原本的序列里,任何一个操作符都有左右两个操作数,numL
和numR
(或者expL和expR)。
既然如此,不妨让我们梳理一下:
-
首先拿到第一个操作数,称为
num
,并拿到这个操作数的左右op
,比较opL
和opR
的"吸引力"大小。 -
opL
大,说明当前操作数相比opR
,更应该和前面的opL
以及numL
(opL前面的操作数)绑定。- 返回当前操作数,作为
opL
的numR
,即opL
的右操作数。 - 此时上一级操作数变成
(numL opL numR)
,即一个整体出现。 - 更新上一级操作数为上述结果,继续。
- 返回当前操作数,作为
-
opR
大,说明当前操作数相比opL
,更应该和后面的opR
和numR
(opR
后面的操作数)绑定。- 此时我们的工作变成了解析
numR
及其后面的表达式。 - 开启新的循环,在新的循环里,
num
更新为numR
。 - 等待新循环结束,得到后续表达式为
numNew
。 - 更新当前操作数为
(num opR numNew)
,继续。
- 此时我们的工作变成了解析
-
相同的
op
取opL
更大。 -
重复。
来看个例子:a + b * c + d
-
首先拿到a作为num,对比op0和+。
-
+
更大-
开启新的递归,拿b作为num,对比
+
和*
。 -
*
更大-
开心新的递归,拿c作为num,对比
*
和+
。 -
*
更大- 返回
(b * c)
。
- 返回
-
-
更新num,从b变成为
(b * c)
,对比+
和+
。 -
前面的
+
更大。- 返回
(a + (b * c))
。
- 返回
-
-
更新num,从a变为
(a + (b * c))
,对比op0
和+
。 -
+
更大-
拿到d作为num,对比
+
和op0
-
+
更大- 返回b。
-
-
更新num,从
(a + (b * c))
为((a + (b * c)) + d)
。 -
结束。
上述过程变成伪代码,大概就是:
rust
fn parseExpression(prevPrecedence) Expression {
let num = currToken
for num->opR != ';' {
let nextPrecedence = num->opR->precedence
if prevPrecedence >= nextPrecedence { // opL >= opR
return num
} else { // opL < opR
nextToken() // advance currToken to opR
nextToken() // advance currToken to nextNum
num = (num opR parseExpression(nextPrecedenc))
}
}
return num
}
parseExpression(LOWEST)
前缀运算?中间运算?
现在我们来着手解决前缀运算的问题,因为前面的过程只是解决了最简单的加减乘除。那么,如果是!
和-
符号呢?
设想一下,如果是-a+b*c
,各位有什么好策略吗?
如果我们把-a
看成一个整体呢?是不是又可以套用上面的逻辑了呢?类似的,如果是!false == true
,而我们也把!false
看成一个整体呢?
这就是前缀运算符的处理。
在一开始获取num的那一步,设置一个预处理函数,把前缀运算符连同后面的表达式,包裹成一个整体。
此时伪代码变成:
scss
fn parseExpression(prevPrecedence) Expression {
let num = prefixExpression(currToken)
// same as above...
}
fn prefixExpression(currExpression) Expression {
let res = {
op: currToken // such as `!` or `-`
num: nil
}
nextToken()
res->num = parseExpression(PREFIX)
return res
}
注意到上面解析前缀运算符后面的Token时,传入的优先级变成了PREFIX
,按照我们的常识去理解,PREFIX
肯定大于加减乘除,事实也确实如此。所以我们可以立即拿到下一个Token和前缀运算符组成的新的表达式(在opL >= opR
的时候return掉了,所以不会继续执行下去),去替代初始的num,之后继续原本熟悉的过程。
前面我们省略掉了具体的计算过程的过程,这其实是中缀表达式 的解析,即,a op b
的过程,我们要做的就是根据op
的类型,做出对应的解析。
为了更加清晰的表述,我们重新整理一下上面的伪代码,把中缀表达式加上:
scss
let l = NewLexer() // constructure a lexer
fn parseExpression(prevPrecedence) Expression {
let num = prefixExpression()
for num->opR != ';' {
let nextPrecedence = l->peekNextTokenPrecedence()
if prevPrecedence >= nextPrecedence { // opL >= opR
return num
} else { // opL < opR
l->nextToken() // advance currToken to opR
num = infixExpression(num)
}
}
return num
}
fn prefixExpression() Expression {
let res = {
op: l->currToken() // such as `!` or `-`
num: nil
}
l->nextToken() // advance currToken to next num
res->num = parseExpression(PREFIX)
return res
}
fn infixExpression(left) Expression {
let res = {
numL: left
op: currToken
numR: nil
}
let prevPrecedence = l->currPrecedence()
l->nextToken() // advance token to next num
res->numR = parseExpression(prevPrecedence)
return res
}
parseExpression(LOWEST)
至此为止,表达式解析完成啦!
不对,你可能说,什么什么?这就完啦?对,就是这么简单!这也是最核心的地方,甚至是本文最核心的实现!因为它透露了递归的思想,也暗示自顶向下的解析与AST的构建路径。
优先级范围限制"()"
现在我们来处理括号()
吧!,因为他们限制了优先级,只有这样,才能让我们的表达式支持更多的数学运算!
回想一下,括号的结构是什么?
(<expression>)
对吗?所以我们是不是可以继续套用我们已经实现的prefixExpression
呢?只是在遇到前缀为(
时特殊处理一下呢?
bingo!然后把(
后面的部分丢给parseExpression
就可以了。至于)
呢?我们需要的是当parseExpression
遇到)
前面的num时,便不再继续往后走,所以如果我们给)
一个LOWEST
的优先级,介于opL >= opR
的判断,此时表达式一整个递归返回,岂不是可以达到这个目的呢?
聪明的你肯定猜到实现了:
scss
fn prefixExpression() Expression {
if l->currToken() == '(' {
reutrn parseGroupedExpression()
}
// same as above ...
}
fn parseGroupedExpression() {
l->nextToken() // skip '('
let exp = parseExpression(LOWEST)
if l->peekToken() != ')' { // grammar error
return nil
} else {
return exp
}
}
那么如果是嵌套的括号呢?比如a * (b * (c + d) + e)
,答案已经在上一步的讨论里,递归!我们的老朋友会在遇到新的括号时,自动递归处理,我们每次只关心当前阶段的处理。
比较运算符
现在来看,对于前缀表达式还剩下比较运算符。
比较运算符,比如:
- a < b
- a == b
- a >= b
- a != b
像是什么?是不是中缀表达式呢?所以我们直接使用中缀表达式构建即可。因为解析器不做运算,仅做解析,而在解析的视角下,a < b
和a + b
本质上都是num op num
的操作,至于他们的结果是新的num
还是false
,解析器不关心。
这也是我们的设计理念之一------各司其职,互不越界。
在我们进入下一个模块之前,还剩一些特殊的表达式需要处理:
-
boolean:直接对此类Token解析,他们自身就是值,比如true,false。
-
function:使用类似前缀表达式的方式,在识别到fn关键字之后,触发前缀表达式解析:
- 读取一个
(
作为参数列表的开头,之后连续以,
作为分割读取标识符作为参数列表,直到遇到)
结束。 - 读取一个
{
作为函数体的开头,之后尝试解析块语句,而块语句的解析则是不断尝试解析一条语句,并添加到新的Statement数组里。
- 读取一个
-
function call:首先
identifier ( identifier
格式的语法仅此一家(()
在表达式里,必然是作为prefix出现的,而不是infix),即函数调用,这是不是一种中间表达式呢? -
identifier:直接解析文本值,就是我们要的答案。
-
number literal:直接解析文本值,并转换为int64类型。
至此,我们所有的解析都完成了!才不是!下面我们通过代码补全来详细说说。
放在go里,代码是这样的:
scss
type Parser struct {
l *lexer.Lexer
currToken token.Token
peekToken token.Token
errors []string
prefixParseFns map[token.Type]prefixParseFn
infixParseFns map[token.Type]infixParseFn
}
func New(l *lexer.Lexer) *Parser {
p := &Parser{l: l, errors: make([]string, 0)}
p.prefixParseFns = make(map[token.Type]prefixParseFn)
p.infixParseFns = make(map[token.Type]infixParseFn)
// register prefix parse functions
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.registerPrefix(token.TRUE, p.parseBoolean)
p.registerPrefix(token.FALSE, p.parseBoolean)
p.registerPrefix(token.LPAREN, p.parseGroupedExpression)
p.registerPrefix(token.FUNCTION, p.parseFunctionLiteral)
// register infix parse functions
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)
// the brothers blow should be seen as a special case of infix expression
p.registerInfix(token.LPAREN, p.parseCallExpression)
p.registerInfix(token.ASSIGN, p.parseAssignmentExpression)
// initialize currToken and peekToken
p.nextToken()
// make sure currToken and peekToken are set correctly: curr for first token, peek for second token
p.nextToken()
return p
}
func (p *Parser) ParseProgram() *ast.Program {
program := &ast.Program{
Statements: make([]ast.Statement, 0),
}
for p.currToken.Type != token.EOF {
stmt := p.parseStatement()
if stmt != nil {
program.Statements = append(program.Statements, stmt)
}
p.nextToken()
}
return program
}
func (p *Parser) parseStatement() ast.Statement {
switch p.currToken.Type {
case token.LET:
return p.parseLetStatement()
default:
return p.parseExpressionStatement()
}
}
func (p *Parser) parseLetStatement() *ast.LetStatement {
stmt := &ast.LetStatement{Token: p.currToken}
// follow rule like let -> identifier -> assign -> expression -> semicolon
if !p.expectPeekAndAdvance(token.IDENT) {
return nil
}
stmt.Name = &ast.Identifier{Token: p.currToken, Value: p.currToken.Literal}
if !p.expectPeekAndAdvance(token.ASSIGN) {
return nil
}
p.nextToken()
stmt.Value = p.parseExpression(LOWEST)
if p.peekTokenIs(token.SEMICOLON) {
p.nextToken()
}
return stmt
}
const (
// the order of precedence
_ int = iota
LOWEST
ASSIGN // = for assignment
EQUALS // ==
LESSGREATER // > or <
SUM // +
PRODUCT // *
PREFIX // -X or !X
CALL // myFunction(X)
INDEX // array[index]
)
func (p *Parser) parseExpressionStatement() *ast.ExpressionStatement {
stmt := &ast.ExpressionStatement{Token: p.currToken}
stmt.Expression = p.parseExpression(LOWEST)
if p.peekTokenIs(token.SEMICOLON) {
p.nextToken()
}
return stmt
}
func (p *Parser) parseExpression(precedence int) ast.Expression {
prefix := p.prefixParseFns[p.currToken.Type]
if prefix == nil {
return nil
}
leftOrAnswerExp := prefix()
for !p.peekTokenIs(token.SEMICOLON) && precedence < p.peekPrecedence() {
infix := p.infixParseFns[p.peekToken.Type]
if infix == nil {
return leftOrAnswerExp
}
p.nextToken()
leftOrAnswerExp = infix(leftOrAnswerExp)
}
return leftOrAnswerExp
}
func (p *Parser) parsePrefixExpression() ast.Expression {
// get operator from currToken
expression := &ast.PrefixExpression{
Token: p.currToken,
Operator: p.currToken.Literal,
}
// advance token to get value-expression(operand)
p.nextToken()
expression.Right = p.parseExpression(PREFIX)
return expression
}
func (p *Parser) parseInfixExpression(left ast.Expression) ast.Expression {
// get operator from currToken
expression := &ast.InfixExpression{
Token: p.currToken,
Operator: p.currToken.Literal,
Left: left,
}
precedence := p.currPrecedence()
// advance token to get value-expression(right operand)
p.nextToken()
expression.Right = p.parseExpression(precedence)
return expression
}
func (p *Parser) parseBlockStatement() *ast.BlockStatement {
block := &ast.BlockStatement{Token: p.currToken}
block.Statements = make([]ast.Statement, 0)
p.nextToken()
for !p.currTokenIs(token.RBRACE) && !p.currTokenIs(token.EOF) {
stmt := p.parseStatement()
if stmt != nil {
block.Statements = append(block.Statements, stmt)
}
p.nextToken()
}
return block
}
怎么会少了AST节点呢?
scss
type Identifier struct {
Token token.Token // token.IDENT
Value string
}
// expressionNode is a marker method
// why identifier is an expression, case it can be used in expressions, like let a = x; the 'x' is an expression that produces a value
func (i *Identifier) expressionNode() {}
func (i *Identifier) TokenLiteral() string {
return i.Token.Literal // aka "x"
}
func (i *Identifier) String() string {
return i.Value
}
type ReturnStatement struct {
Token token.Token // token.RETURN
ReturnValue Expression
}
func (rs *ReturnStatement) statementNode() {}
func (rs *ReturnStatement) TokenLiteral() string {
return rs.Token.Literal
}
func (rs *ReturnStatement) String() string {
out := bytes.Buffer{}
out.WriteString(rs.TokenLiteral() + " ")
if rs.ReturnValue != nil {
out.WriteString(rs.ReturnValue.String())
}
out.WriteString(";")
return out.String()
}
type ExpressionStatement struct {
Token token.Token // the first token of the expression
Expression Expression
}
func (es *ExpressionStatement) statementNode() {}
func (es *ExpressionStatement) TokenLiteral() string {
return es.Token.Literal
}
func (es *ExpressionStatement) String() string {
if es.Expression != nil {
return es.Expression.String()
}
return ""
}
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
}
type PrefixExpression struct {
Token token.Token // the prefix token, like '!' or '-'
Operator string
Right Expression // like `15` as an integer-expression in `-15`
}
func (pe *PrefixExpression) expressionNode() {}
func (pe *PrefixExpression) TokenLiteral() string {
return pe.Token.Literal
}
func (pe *PrefixExpression) String() string {
out := bytes.Buffer{}
out.WriteString("(")
out.WriteString(pe.Operator)
out.WriteString(pe.Right.String())
out.WriteString(")")
return out.String()
}
type InfixExpression struct {
Token token.Token // the infix token, like '+', '-', '*', '/', '==', '!=', '<', '>', etc.
Left Expression
Operator string
Right Expression
}
func (ie *InfixExpression) expressionNode() {}
func (ie *InfixExpression) TokenLiteral() string {
return ie.Token.Literal
}
func (ie *InfixExpression) String() string {
out := bytes.Buffer{}
out.WriteString("(")
out.WriteString(ie.Left.String())
out.WriteString(" " + ie.Operator + " ")
out.WriteString(ie.Right.String())
out.WriteString(")")
return out.String()
}
type Boolean struct {
Token token.Token
Value bool
}
func (b *Boolean) expressionNode() {}
func (b *Boolean) TokenLiteral() string {
return b.Token.Literal
}
func (b *Boolean) String() string {
return b.Token.Literal
}
type BlockStatement struct {
Token token.Token // the '{' token
Statements []Statement
}
func (bs *BlockStatement) statementNode() {}
func (bs *BlockStatement) TokenLiteral() string {
return bs.Token.Literal
}
func (bs *BlockStatement) String() string {
out := bytes.Buffer{}
out.WriteString("{")
for _, s := range bs.Statements {
out.WriteString(s.String())
}
out.WriteString("}")
return out.String()
}
type FunctionLiteral struct {
Token token.Token // the 'fn' token
Parameters []*Identifier
Body *BlockStatement
}
func (fl *FunctionLiteral) expressionNode() {}
func (fl *FunctionLiteral) TokenLiteral() string {
return fl.Token.Literal
}
func (fl *FunctionLiteral) String() string {
out := bytes.Buffer{}
params := []string{}
for _, p := range fl.Parameters {
params = append(params, p.String())
}
out.WriteString(fl.TokenLiteral())
out.WriteString("(")
out.WriteString(strings.Join(params, ", "))
out.WriteString(")")
out.WriteString(fl.Body.String())
return out.String()
}
type CallExpression struct {
Token token.Token // the '(' token
Function Expression // Identifier or FunctionLiteral
Arguments []Expression
}
func (ce *CallExpression) expressionNode() {}
func (ce *CallExpression) TokenLiteral() string {
return ce.Token.Literal
}
func (ce *CallExpression) String() string {
out := bytes.Buffer{}
var args []string
for _, a := range ce.Arguments {
args = append(args, a.String())
}
out.WriteString(ce.Function.String())
out.WriteString("(")
out.WriteString(strings.Join(args, ", "))
out.WriteString(")")
return out.String()
}
type AssignmentExpression struct {
Token token.Token // the '=' token
Name *Identifier
Value Expression
}
func (ae *AssignmentExpression) expressionNode() {}
func (ae *AssignmentExpression) TokenLiteral() string {
return ae.Token.Literal
}
func (ae *AssignmentExpression) String() string {
out := bytes.Buffer{}
out.WriteString(ae.Name.String())
out.WriteString(" = ")
out.WriteString(ae.Value.String())
return out.String()
}
这里并没有给出完整的实现,一是为了篇幅,二是因为剩下的部分我相信读者可以自己补全。
但是似乎还有两个哥们没有出现:if-else
和for
,对于这俩的解析,其实和上面大同小异。
但是真正开始之前,注意到我们的实现,从原本的在prefixExpression
中判断前缀Token类型,变成了注册到一个map
里,然后根据Token类型去map里找对应的prefixExpression
实现,这是为了工程设计修改的部分,本质上和我们前面讨论的实现没有区别,类似的infixExpression
的实现也是如此,可能你会说:哎?怎么注册infix的部分有一些是别的呢?
比如函数调用的解析,虽然也是注册到infix的map,但怎么跑到parseCallExpre
上了呢?
scss
func (p *Parser) parseCallExpression(function ast.Expression) ast.Expression {
exp := &ast.CallExpression{Token: p.currToken, Function: function}
exp.Arguments = p.parseExpressionList(token.RPAREN)
return exp
}
func (p *Parser) parseExpressionList(end token.Type) []ast.Expression {
list := make([]ast.Expression, 0)
if p.peekTokenIs(end) {
p.nextToken()
return list
}
p.nextToken()
list = append(list, p.parseExpression(LOWEST))
for p.peekTokenIs(token.COMMA) {
p.nextToken()
p.nextToken()
list = append(list, p.parseExpression(LOWEST))
}
if !p.expectPeekAndAdvance(end) {
return nil
}
return list
}
嘿嘿,细细观察它们,发现是一样的,只是参数解析的部分替换成了原本对于numR
的解析,但本质都是infix中间表达式的处理。
回到if-else
的处理,可以猜到这应该和let
语句的处理一样:
go
func (p *Parser) parseStatement() ast.Statement {
switch p.currToken.Type {
// LET statement
case token.RETURN:
return p.parseReturnStatement()
case token.IF:
return p.parseIfExpression()
case token.FOR:
return p.parseForExpression()
case token.BREAK:
return p.parseBreakStatement()
case token.CONTINUE:
return p.parseContinueStatement()
default:
return p.parseExpressionStatement()
}
}
func (p *Parser) parseIfExpression() ast.Statement {
expression := &ast.IfStatement{Token: p.currToken}
if !p.expectPeekAndAdvance(token.LPAREN) {
return nil
}
p.nextToken()
expression.Condition = p.parseExpression(LOWEST)
if !p.expectPeekAndAdvance(token.RPAREN) {
return nil
}
if !p.expectPeekAndAdvance(token.LBRACE) {
return nil
}
expression.Consequence = p.parseBlockStatement()
if p.peekTokenIs(token.ELSE) {
p.nextToken()
if !p.expectPeekAndAdvance(token.LBRACE) {
return nil
}
expression.Alternative = p.parseBlockStatement()
}
return expression
}
对应的AST节点定义:
go
type IfStatement struct {
Token token.Token // the 'if' token
Condition Expression
Consequence *BlockStatement
Alternative *BlockStatement
}
func (ie *IfStatement) statementNode() {}
func (ie *IfStatement) TokenLiteral() string {
return ie.Token.Literal
}
func (ie *IfStatement) String() string {
out := bytes.Buffer{}
out.WriteString("if")
out.WriteString(ie.Condition.String())
out.WriteString(" ")
out.WriteString(ie.Consequence.String())
if ie.Alternative != nil {
out.WriteString("else ")
out.WriteString(ie.Alternative.String())
}
return out.String()
}
for
语句的处理,则多了循环条件和控制流的处理。但是for
语句多了执行流的终止!
如果我们条件判断在多次循环中得到了false,那么应该终止执行;如果我们得到了continue
的调用,此时执行也要被终止,同理break
也是,这些都是执行器部分的事情,在解析器部分,只需要简单的把continue
和break
当成特殊语句处理即可,这样方便执行器识别:
css
func (p *Parser) parseForExpression() ast.Statement {
stmt := &ast.ForExpression{Token: p.currToken}
if !p.expectPeekAndAdvance(token.LPAREN) {
return nil
}
p.nextToken()
stmt.Init = p.parseStatement()
p.nextToken()
stmt.Condition = p.parseExpression(LOWEST)
if !p.expectPeekAndAdvance(token.SEMICOLON) {
return nil
}
p.nextToken()
stmt.Update = p.parseExpression(LOWEST)
if !p.expectPeekAndAdvance(token.RPAREN) {
return nil
}
if !p.expectPeekAndAdvance(token.LBRACE) {
return nil
}
stmt.Body = p.parseBlockStatement()
return stmt
}
func (p *Parser) parseBreakStatement() *ast.BreakStatement {
stmt := &ast.BreakStatement{Token: p.currToken}
if !p.expectPeekAndAdvance(token.SEMICOLON) {
return nil
}
return stmt
}
func (p *Parser) parseContinueStatement() *ast.ContinueStatement {
stmt := &ast.ContinueStatement{Token: p.currToken}
if !p.expectPeekAndAdvance(token.SEMICOLON) {
return nil
}
return stmt
}
AST节点:
go
type ForExpression struct {
Token token.Token // the 'for' token
Init Statement
Condition Expression
Update Expression
Body *BlockStatement
}
func (fs *ForExpression) statementNode() {}
func (fs *ForExpression) TokenLiteral() string {
return fs.Token.Literal
}
func (fs *ForExpression) String() string {
out := bytes.Buffer{}
out.WriteString("for")
out.WriteString("(")
out.WriteString(fs.Init.String())
out.WriteString(fs.Condition.String())
out.WriteString("; ")
out.WriteString(fs.Update.String())
out.WriteString(" ")
out.WriteString(")")
out.WriteString(fs.Body.String())
return out.String()
}
type BreakStatement struct {
Token token.Token // the 'break' token
}
func (bs *BreakStatement) statementNode() {}
func (bs *BreakStatement) TokenLiteral() string {
return bs.Token.Literal
}
func (bs *BreakStatement) String() string {
return bs.Token.Literal
}
type ContinueStatement struct {
Token token.Token // the 'continue' token
}
func (cs *ContinueStatement) statementNode() {}
func (cs *ContinueStatement) TokenLiteral() string {
return cs.Token.Literal
}
func (cs *ContinueStatement) String() string {
return cs.Token.Literal
}
赋值语句属于特殊的infix,因为它的格式属于<identifier> = <expression>
,是不是类似函数调用哦?这哥俩都是结构上属于infix但是完全不是infix中缀表达式的语义,因为它们更像是不完整的语句而不是可以产生值的表达式:
css
func (p *Parser) parseAssignmentExpression(left ast.Expression) ast.Expression {
exp := &ast.AssignmentExpression{Token: p.currToken}
ident := left.(*ast.Identifier)
exp.Name = ident
precedence := p.currPrecedence()
p.nextToken()
exp.Value = p.parseExpression(precedence)
return exp
}
剩下的部分留在结尾去处理,即字符串,数组,map的解析。
执行器
终于到了开始让我们的AST产生值的时候了!
回想一下,我们的程序由一个Statement数组组成,而每一个Statement包含了零或多个表达式,每个表达式都是一棵AST。
这些Statement从头到尾执行下去,会不断地产生,更新,删除值。如果我们把整个程序看成一个有状态的对象,那么它的内部环境(上下文)则是一直更新的。而最后得到的状态,就是程序执行的结果!
既然如此,执行器要做的就是不断遍历Statement,记录程序上下文的变化,并得到一个最终的状态。
明白了这一点,我们就可以着手编写我们的执行器(Evalutor)了!
Environment(上下文)
想要得到更新的状态,记录状态的变化,我们就需要一种方式去记录每一条Statement的执行结果。
思考一下,各种语句大概有什么作用呢?
- let:创建一个标识符并绑定一个值
- assignment:更新一个标识符的值
- function call:使用一些值,并更新或者产生新的值
- if-else:使用一些值并更新一些值
- ... ...
那么一个map就很显然易见了,因为它刚好可以保存标识符和值的关系。
在正式开始构建我们自己的上下文保存机制之前,我们需要先创造一个内部的类型系统。
类型系统
简单来说,我们需要区分一个标识符或者变量背后的值到底是integer还是bool,当我们做赋值时会不会把string赋值给一个hashmap。这些都需要一个强类型的类型系统来做支持,虽然我们的Monkey看起来是弱类型语言,可是真的跑起来,go-runtime可不会偏袒你哦。
首先我们定义每种值都有一个类型Type,而每种值都是一个Object,Object通过内部的Type字段区分类型,当然还要一个方便我们排除问题的辅助字段:
ini
type Type string
type Object interface {
Type() Type
Inspect() string
}
const (
IntegerObj = "INTEGER"
BooleanObj = "BOOLEAN"
NullObj = "NULL"
ReturnObj = "RETURN"
ErrorObj = "ERROR"
FunctionObj = "FUNCTION"
BreakObj = "BREAK"
ContinueObj = "CONTINUE"
StringObj = "STRING"
BuiltInObj = "BUILTIN"
ArrayObj = "ARRAY"
HashObj = "HASH"
)
先来看看简单的类型是如何定义的呢:
go
type Integer struct {
Value int64
}
func (i *Integer) Type() Type {
return IntegerObj
}
func (i *Integer) Inspect() string {
return fmt.Sprintf("%d", i.Value)
}
type Boolean struct {
Value bool
}
func (b *Boolean) Type() Type {
return BooleanObj
}
func (b *Boolean) Inspect() string {
return fmt.Sprintf("%t", b.Value)
}
type Null struct{}
func (n *Null) Type() Type {
return NullObj
}
func (n *Null) Inspect() string {
return "null"
}
为了处理出错了的情况,我们还贴心的设置了一个Null类型,方便在无法继续时返回到上一级。
比如let a = 1;
则是一个let语句产生了一个类型为Integer
的值,并把a
和它做绑定。
而let a = 1 + 2;
则是产生了一个infix表达式作为值,这个值最终也是Integer
类型。
作为条件运算的表达式1 < 2
,产生一个Boolean
类型的值,是很合理的,这样才能在if-else中决定执行流的走向。
return的特殊?
倘若语句return 1;
是我们要处理的值,也是要简单的产生一个Integer
然后保存在环境中吗?
我们要做的不仅仅是得到值,还要终止执行流 。因为return
意味着后面的语句将不被执行。
类似的还有continue
和break
:
go
type ReturnValue struct {
Value Object
}
func (rv *ReturnValue) Type() Type {
return ReturnObj
}
func (rv *ReturnValue) Inspect() string {
return rv.Value.Inspect()
}
type Break struct{}
func (b *Break) Type() Type {
return BreakObj
}
func (b *Break) Inspect() string {
return "break"
}
type Continue struct{}
func (c *Continue) Type() Type {
return ContinueObj
}
func (c *Continue) Inspect() string {
return "continue"
}
有一些特殊的值,我们可以预先定义:
vbnet
var (
NULL = &object.Null{}
TRUE = &object.Boolean{Value: true}
FALSE = &object.Boolean{Value: false}
BREAK = &object.Break{}
CONTINUE = &object.Continue{}
)
这样方便我们后面直接使用,毕竟它们是不变的嘛!
谁都会犯错
当然,对于错误的语法,错误的计算结果,不被允许的表达式形式,都是需要我们返回并告知使用者的,那么一个合适的错误类型,必然是重要的一环:
go
type Error struct {
Message string
}
func (e *Error) Type() Type {
return ErrorObj
}
func (e *Error) Inspect() string {
return "ERROR: " + e.Message
}
回到一开始的上下文设置,既然得到了需要我们保存的值的类型,我们的上下文定义也可以轻松地给出了:
go
type Environment struct {
store map[string]Object
}
func NewEnvironment() *Environment {
s := make(map[string]Object)
return &Environment{store: s}
}
func (e *Environment) Get(name string) (Object, bool) {
obj, ok := e.store[name]
return obj, ok
}
func (e *Environment) Set(name string, val Object) Object {
e.store[name] = val
return val
}
就这么简单?就这么简单!就是一个wrap的map!
执行器本体
回想一下,如果想要处理一条语句,最好的方式是什么?答案是拆分+递归,正如我们在解析器部分做的一样。
遇到标识符则读取其名称,遇到表达式,则递归计算表达式,遇到赋值,就把计算之后的表达式和前面的标识符绑定到Environment中。
而遇到复杂的语句,按照逻辑依次解析,比如遇到一条函数调用的语句,我们需要计算函数名以找到对应的函数,参数列表使用的值的计算,以便拿到实际的值;如果是for
语句,还需要初始阶段的计算,判断表达式的计算等。
在开始列举这些实现之前,我们需要思考一个问题:如果某个函数,定义了一个和外部环境相同的同名变量,此时应该怎么处理?
覆写已经存在的映射关系吗?这样肯定会产生错误的逻辑:
ini
let a = 1;
let f = fn() {
let a = 2;
}
f()
a == 1 // true
所以我们需要为每一个函数调用,提供一个独立的环境,此外,函数的环境还必须可以访问到外部环境:
ini
let a = 1;
let b = 2;
let f = fn() {
let a = b + 2;
}
那么此时,我们的Environment可以适当的更新一下:
go
type Environment struct {
store map[string]Object
outer *Environment
}
func NewEnvironment() *Environment {
s := make(map[string]Object)
return &Environment{store: s}
}
func (e *Environment) Get(name string) (Object, bool) {
obj, ok := e.store[name]
if !ok && e.outer != nil {
obj, ok = e.outer.Get(name)
}
return obj, ok
}
func (e *Environment) Set(name string, val Object) Object {
e.store[name] = val
return val
}
func NewEnclosedEnvironment(outer *Environment) *Environment {
env := NewEnvironment()
env.outer = outer
return env
}
可以发现,如果是当前env没有发现的值,会尝试去外部寻找。
一切开始,Let's Go!
解决完这个问题,我们就可以着手搭建了:
kotlin
func Eval(node ast.Node, env *object.Environment) object.Object {
switch node := node.(type) {
case *ast.Program:
return evalProgram(node.Statements, env)
case *ast.ExpressionStatement:
return Eval(node.Expression, env)
case *ast.BlockStatement:
return evalBlockStatement(node, env)
case *ast.IntegerLiteral:
return &object.Integer{Value: node.Value}
case *ast.Boolean:
return nativeBoolToBooleanObject(node.Value)
case *ast.PrefixExpression:
right := Eval(node.Right, env)
if isError(right) {
return right
}
return evalPrefixExpression(node.Operator, right)
case *ast.InfixExpression:
left := Eval(node.Left, env)
if isError(left) {
return left
}
right := Eval(node.Right, env)
if isError(right) {
return right
}
return evalInfixExpression(node.Operator, left, right)
case *ast.LetStatement:
val := Eval(node.Value, env)
if isError(val) {
return val
}
env.Set(node.Name.Value, val)
case *ast.ReturnStatement:
val := Eval(node.ReturnValue, env)
if isError(val) {
return val
}
return &object.ReturnValue{Value: val}
case *ast.IfStatement:
return evalIfExpression(node, env)
case *ast.ForExpression:
return evalForStatement(node, env)
case *ast.BreakStatement:
return evalBreakStatement()
case *ast.ContinueStatement:
return evalContinueStatement()
case *ast.Identifier:
return evalIdentifier(node, env)
case *ast.FunctionLiteral:
params := node.Parameters
body := node.Body
return &object.Function{Parameters: params, Body: body, Env: env}
case *ast.CallExpression:
function := Eval(node.Function, env)
if isError(function) {
return function
}
args := evalExpressions(node.Arguments, env)
if len(args) == 1 && isError(args[0]) {
return args[0]
}
return applyFunction(function, args)
case *ast.AssignmentExpression:
return evalAssignmentExpression(node, env)
}
return nil
}
再补全一些实现:
kotlin
func nativeBoolToBooleanObject(input bool) *object.Boolean {
if input {
return TRUE
}
return FALSE
}
func evalPrefixExpression(operator string, right object.Object) object.Object {
switch operator {
case "!":
return evalBangOperatorExpression(right)
case "-":
return evalMinusPrefixOperatorExpression(right)
default:
return newError("unknown operator: %s%s", operator, right.Type())
}
}
func evalBangOperatorExpression(right object.Object) object.Object {
switch right {
case TRUE:
return FALSE
case FALSE:
return TRUE
case NULL:
return TRUE
default:
return FALSE
}
}
func evalMinusPrefixOperatorExpression(right object.Object) object.Object {
if right.Type() != object.IntegerObj {
return newError("unknown operator: -%s", right.Type())
}
value := right.(*object.Integer).Value
return &object.Integer{Value: -value}
}
func evalInfixExpression(operator string, left, right object.Object) object.Object {
switch {
case left.Type() == object.IntegerObj && right.Type() == object.IntegerObj:
return evalIntegerInfixExpression(operator, left, right)
case operator == "==":
return nativeBoolToBooleanObject(left == right)
case operator == "!=":
return nativeBoolToBooleanObject(left != right)
case left.Type() != right.Type():
return newError("type mismatch: %s %s %s", left.Type(), operator, right.Type())
default:
return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type())
}
}
func evalIntegerInfixExpression(operator string, left, right object.Object) object.Object {
leftVal := left.(*object.Integer).Value
rightVal := right.(*object.Integer).Value
switch operator {
case "+":
return &object.Integer{Value: leftVal + rightVal}
case "-":
return &object.Integer{Value: leftVal - rightVal}
case "*":
return &object.Integer{Value: leftVal * rightVal}
case "/":
return &object.Integer{Value: leftVal / rightVal}
case "<":
return nativeBoolToBooleanObject(leftVal < rightVal)
case ">":
return nativeBoolToBooleanObject(leftVal > rightVal)
case "==":
return nativeBoolToBooleanObject(leftVal == rightVal)
case "!=":
return nativeBoolToBooleanObject(leftVal != rightVal)
default:
return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type())
}
}
func newError(format string, a ...interface{}) *object.Error {
return &object.Error{Message: fmt.Sprintf(format, a...)}
}
func isError(obj object.Object) bool {
if obj != nil {
return obj.Type() == object.ErrorObj
}
return false
}
func evalIdentifier(node *ast.Identifier, env *object.Environment) object.Object {
if val, ok := env.Get(node.Value); ok {
return val
}
if val, ok := builtins[node.Value]; ok {
return val
}
return newError("identifier not found: " + node.Value)
}
func evalExpressions(exps []ast.Expression, env *object.Environment) []object.Object {
var result []object.Object
for _, e := range exps {
evaluated := Eval(e, env)
if isError(evaluated) {
return []object.Object{evaluated}
}
result = append(result, evaluated)
}
return result
}
func applyFunction(fn object.Object, args []object.Object) object.Object {
switch fn := fn.(type) {
case *object.Function:
extendedEnv := extendFunctionEnv(fn, args)
evaluated := Eval(fn.Body, extendedEnv)
return unwrapReturnValue(evaluated)
case *object.Builtin:
return fn.Fn(args...)
}
return newError("not a function: %s", fn.Type())
}
func extendFunctionEnv(fn *object.Function, args []object.Object) *object.Environment {
env := object.NewEnclosedEnvironment(fn.Env)
for paramIdx, param := range fn.Parameters {
env.Set(param.Value, args[paramIdx])
}
return env
}
func unwrapReturnValue(obj object.Object) object.Object {
if returnValue, ok := obj.(*object.ReturnValue); ok {
return returnValue.Value
}
return obj
}
func evalBreakStatement() object.Object {
return BREAK
}
func evalContinueStatement() object.Object {
return CONTINUE
}
我们可以很直白的看到,在Eval函数中,所有case列举到的实现,都是我们解析器部分给出的返回值。而且我们的计算过程非常直白,就是独立计算每个部分,然后组装起来,并且把子部分交给递归处理。
而具体的实现,我们以evalInfixExpression
为例:
swift
func evalInfixExpression(operator string, left, right object.Object) object.Object {
switch {
case left.Type() == object.IntegerObj && right.Type() == object.IntegerObj:
return evalIntegerInfixExpression(operator, left, right)
case operator == "==":
return nativeBoolToBooleanObject(left == right)
case operator == "!=":
return nativeBoolToBooleanObject(left != right)
case left.Type() != right.Type():
return newError("type mismatch: %s %s %s", left.Type(), operator, right.Type())
default:
return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type())
}
}
在这里,我们判断两个操作数的类型,并判断它们对应的操作符是否匹配,还记得我们在解析器那里,对表达式构建了递归形式的AST吗?那么在这里,也是同样的递归到最简单的表达式去处理,即上述的形式,之后不断返回到上一级完成计算。
对于块语句的解析,我们把它当成了一个特殊的Program去处理,因为它也是一个Statement数组组成的结构,很显然我们的evalProgora
没法一次处理多个Statement。
函数的上下文,要隔离!
刚刚我们提到,关于函数的调用,需要特殊为其做一个上下文:
go
func applyFunction(fn object.Object, args []object.Object) object.Object {
switch fn := fn.(type) {
case *object.Function:
extendedEnv := extendFunctionEnv(fn, args)
evaluated := Eval(fn.Body, extendedEnv)
return unwrapReturnValue(evaluated)
case *object.Builtin:
return fn.Fn(args...)
}
return newError("not a function: %s", fn.Type())
}
func extendFunctionEnv(fn *object.Function, args []object.Object) *object.Environment {
env := object.NewEnclosedEnvironment(fn.Env)
for paramIdx, param := range fn.Parameters {
env.Set(param.Value, args[paramIdx])
}
return env
}
这里的参数解析则是evalExpressions
完成的:
css
func evalExpressions(exps []ast.Expression, env *object.Environment) []object.Object {
var result []object.Object
for _, e := range exps {
evaluated := Eval(e, env)
if isError(evaluated) {
return []object.Object{evaluated}
}
result = append(result, evaluated)
}
return result
}
特殊的赋值语句
赋值语句,会把值赋给一个已经存在的标识符,特别的,如果是在一个函数中,把值赋给一个先前定义的标识符是可接受的,那么我们就需要"向外"查找最近的标识符赋值,因为函数拥有自己的环境,如果当前环境没有,则去赋值给外部环境,这和Get
方法恰恰相反:
kotlin
func (e *Environment) SetOuter(name string, val Object) bool {
m := e
for m != nil {
if _, ok := m.store[name]; ok {
m.Set(name, val)
return true
} else {
m = m.outer
}
}
return false
}
func evalAssignmentExpression(node *ast.AssignmentExpression, env *object.Environment) object.Object {
val := Eval(node.Value, env)
if isError(val) {
return val
}
if !env.SetOuter(node.Name.Value, val) {
return newError("identifier not found: " + node.Name.Value)
}
return NULL
}
被遗忘的俩兄弟
if-else
和for
的实现呢?
kotlin
func evalIfExpression(ie *ast.IfStatement, env *object.Environment) object.Object {
condition := Eval(ie.Condition, env)
if isError(condition) {
return condition
}
if isTruthy(condition) {
return Eval(ie.Consequence, env)
} else if ie.Alternative != nil {
return Eval(ie.Alternative, env)
} else {
return NULL
}
}
func evalForStatement(fs *ast.ForExpression, env *object.Environment) object.Object {
loopEnv := object.NewEnclosedEnvironment(env)
if fs.Init != nil {
Eval(fs.Init, loopEnv)
}
for {
condition := Eval(fs.Condition, loopEnv)
if isError(condition) {
return condition
}
if !isTruthy(condition) {
break
}
result := Eval(fs.Body, loopEnv)
if isError(result) {
return result
}
if result != nil {
if result.Type() == object.ReturnObj {
return result
}
if result.Type() == object.BreakObj {
break
}
if result.Type() == object.ContinueObj {
if fs.Update != nil {
Eval(fs.Update, loopEnv)
}
continue
}
}
if fs.Update != nil {
Eval(fs.Update, loopEnv)
}
}
return NULL
}
func isTruthy(obj object.Object) bool {
switch obj {
case NULL:
return false
case TRUE:
return true
case FALSE:
return false
default:
return true
}
}
这里涉及到控制流的处理,注意到这里,for
语句对于Continue
和Break
值的特殊处理,和上面evalBlockStatement
的处理很像,本质上都是快速中断执行流,然后判断是否要继续还是跳出,又或者是重新开始。
闭包和高阶函数
在一开始的地方,我们提到,Monkey是支持闭包的且,函数作为一等公民,这是什么意思呢?来看一个示例:
scss
let newAdder = fn(x) {
return fn(y) {
return x + y;
}
}
let addTwo = newAdder(2);
addTwo(3); // 5
let addFive = newAdder(5);
addFive(5); // 10
addTwo(1); // 3
注意到,函数newAdder
返回一个函数,所以它是高阶函数,而高阶函数的定义,就是接受一个函数作为参数或者返回一个函数作为返回值。
另一件有意思的事,是我们发现被返回的函数,似乎不受执行位置的影响,而仅受第一次创建时的环境影响,这就是闭包,即不同于函数被调用时才锁定上下文环境,闭包的上下文锁定在它们被创建那一刻,比如上述addTwo
和addFive
无论在哪里被调用,都是一样的执行逻辑。
特殊类型
string
string类型的处理,嗯... ...先处理字符串字面值吧!
一个典型的字面值如下:
ini
let a = "aaa";
所以我们需要在lexer词法器中,加入对"
的解析。之后则是在解析器里,使用prefixExpression解析文本值。然后在执行器里,直接包裹成一个String对象即可。
那么,说干就干:
go
func (l *Lexer) NextToken() token.Token {
var tok token.Token
l.skipWhitespace()
switch l.char { // 根据当前字符的类型,构建对应的Token
case '"': // 字符串字面值处理
tok.Type = token.STRING
tok.Literal = l.readString()
}
l.readChar()
return tok
解析器:
scss
func New(l *lexer.Lexer) *Parser {
p := &Parser{l: l, errors: make([]string, 0)}
p.prefixParseFns = make(map[token.Type]prefixParseFn)
p.infixParseFns = make(map[token.Type]infixParseFn)
// register prefix parse functions
p.registerPrefix(token.STRING, p.parseStringLiteral)
// register infix parse functions
// initialize currToken and peekToken
p.nextToken()
// make sure currToken and peekToken are set correctly: curr for first token, peek for second token
p.nextToken()
return p
}
func (p *Parser) parseStringLiteral() ast.Expression {
return &ast.StringLiteral{Token: p.currToken, Value: p.currToken.Literal}
}
和AST节点:
go
type StringLiteral struct {
Token token.Token
Value string
}
func (sl *StringLiteral) expressionNode() {}
func (sl *StringLiteral) TokenLiteral() string {
return sl.Token.Literal
}
func (sl *StringLiteral) String() string {
return sl.Token.Literal
}
最后就是执行器中:
go
func Eval(node ast.Node, env *object.Environment) object.Object {
switch node := node.(type) {
case *ast.Program:
return evalProgram(node.Statements, env)
case *ast.StringLiteral:
return &object.String{Value: node.Value}
}
return nil
}
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}
}
type String struct {
Value string
}
func (s *String) Type() Type {
return StringObj
}
func (s *String) Inspect() string {
return s.Value
}
这样我们就简单的解析了一个字符串字面值!
对了,字符串拼接操作怎么能给忘了呢!
csharp
func evalInfixExpression(operator string, left, right object.Object) object.Object {
switch {
case left.Type() == object.StringObj && right.Type() == object.StringObj:
return evalStringInfixExpression(operator, left, right)
default:
return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type())
}
}
另一种构建方法,则是使用integer数组的形式,所以数组就是下一个目标!
slice
对于数组的解析,先从语法入手咯:
ini
let a = [1, 2, 3];
let b = [1, a * b, 3 * 5];
还是老套路,先给出词法器:
csharp
func (l *Lexer) NextToken() token.Token {
var tok token.Token
l.skipWhitespace()
switch l.char
case '[':
tok = newToken(token.LBRACKET, l.char)
case ']':
tok = newToken(token.RBRACKET, l.char)
}
l.readChar()
return tok
}
接着是解析器:
scss
func New(l *lexer.Lexer) *Parser {
p := &Parser{l: l, errors: make([]string, 0)}
p.prefixParseFns = make(map[token.Type]prefixParseFn)
p.infixParseFns = make(map[token.Type]infixParseFn)
// register prefix parse functions
p.registerPrefix(token.LBRACKET, p.parseArrayLiteral)
// register infix parse functions
// initialize currToken and peekToken
p.nextToken()
// make sure currToken and peekToken are set correctly: curr for first token, peek for second token
p.nextToken()
return p
}
func (p *Parser) parseArrayLiteral() ast.Expression {
array := &ast.ArrayLiteral{Token: p.currToken}
array.Elements = p.parseExpressionList(token.RBRACKET)
return array
}
在AST中的定义:
go
type Array struct {
Elements []Object
}
func (a *Array) Type() Type {
return ArrayObj
}
func (a *Array) Inspect() string {
var out bytes.Buffer
var elements []string
for _, el := range a.Elements {
elements = append(elements, el.Inspect())
}
out.WriteString("[")
out.WriteString(strings.Join(elements, ", "))
out.WriteString("]")
return out.String()
}
执行器嘞:
go
func Eval(node ast.Node, env *object.Environment) object.Object {
switch node := node.(type) {
case *ast.ArrayLiteral:
elements := evalExpressions(node.Elements, env)
if len(elements) == 1 && isError(elements[0]) {
return elements[0]
}
return &object.Array{Elements: elements}
case *ast.IndexExpression:
left := Eval(node.Left, env)
if isError(left) {
return left
}
index := Eval(node.Index, env)
if isError(index) {
return index
}
return evalIndexExpression(left, index)
}
return nil
}
type Array struct {
Elements []Object
}
func (a *Array) Type() Type {
return ArrayObj
}
func (a *Array) Inspect() string {
var out bytes.Buffer
var elements []string
for _, el := range a.Elements {
elements = append(elements, el.Inspect())
}
out.WriteString("[")
out.WriteString(strings.Join(elements, ", "))
out.WriteString("]")
return out.String()
}
func evalIndexExpression(left, index object.Object) object.Object {
switch {
case left.Type() == object.ArrayObj && index.Type() == object.IntegerObj:
return evalArrayIndexExpression(left, index)
default:
return newError("index operator not supported: %s", left.Type())
}
}
func evalArrayIndexExpression(array, index object.Object) object.Object {
arrayObject := array.(*object.Array)
idx := index.(*object.Integer).Value
m := int64(len(arrayObject.Elements) - 1)
if idx < 0 || idx > m {
return NULL
}
return arrayObject.Elements[idx]
}
注意到,这里还是先索引操作,因为它的语法很像函数调用:<identifier><oprand><identifier>
!所以我们的处理也很直白,拿到索引的值,找到元素,OK!
map
最后来到了map
这里我们跳过前面重复的部分(在项目源代码里有完整实现),思考一下怎么确定唯一的键值。因为我们要面对的是integer,string,bool等类型,所以给出一个通用定义:
go
type HashKey struct {
Type Type
Value uint64
}
type Hashable interface {
HashKey() HashKey
}
func (b *Boolean) HashKey() HashKey {
var value uint64
if b.Value {
value = 1
} else {
value = 0
}
return HashKey{Type: b.Type(), Value: value}
}
func (i *Integer) HashKey() HashKey {
return HashKey{Type: i.Type(), Value: uint64(i.Value)}
}
func (s *String) HashKey() HashKey {
// djb2 hash function
hash := uint64(5381)
for _, c := range s.Value {
hash = ((hash << 5) + hash) + uint64(c)
}
return HashKey{Type: s.Type(), Value: hash}
}
这样一来,就可以拿到统一的表示方法了。
另外一个就是索引实现,数组和HashMap都是一样的语法:a[1]
,所以在解析器里亦是如此:
css
func (p *Parser) parseIndexExpression(left ast.Expression) ast.Expression {
exp := &ast.IndexExpression{Token: p.currToken, Left: left}
p.nextToken()
exp.Index = p.parseExpression(LOWEST)
if !p.expectPeekAndAdvance(token.RBRACKET) {
return nil
}
return exp
}
func (p *Parser) parseHashLiteral() ast.Expression {
hash := &ast.HashLiteral{Token: p.currToken}
hash.Pairs = make(map[ast.Expression]ast.Expression)
for !p.peekTokenIs(token.RBRACE) {
p.nextToken()
key := p.parseExpression(LOWEST)
if !p.expectPeekAndAdvance(token.COLON) {
return nil
}
p.nextToken()
value := p.parseExpression(LOWEST)
hash.Pairs[key] = value
if !p.peekTokenIs(token.RBRACE) && !p.expectPeekAndAdvance(token.COMMA) {
return nil
}
}
if !p.expectPeekAndAdvance(token.RBRACE) {
return nil
}
return hash
}
在执行器的实现则是这样:
go
func evalHashLiteral(node *ast.HashLiteral, env *object.Environment) object.Object {
pairs := make(map[object.HashKey]object.HashPair)
for keyNode, valueNode := range node.Pairs {
key := Eval(keyNode, env)
if isError(key) {
return key
}
hashKey, ok := key.(object.Hashable)
if !ok {
return newError("unusable as hash key: %s", key.Type())
}
value := Eval(valueNode, env)
if isError(value) {
return value
}
hashed := hashKey.HashKey()
pairs[hashed] = object.HashPair{Key: key, Value: value}
}
return &object.Hash{Pairs: pairs}
}
func evalHashIndexExpression(hash, index object.Object) object.Object {
hashObject := hash.(*object.Hash)
key, ok := index.(object.Hashable)
if !ok {
return newError("unusable as hash key: %s", index.Type())
}
pair, ok := hashObject.Pairs[key.HashKey()]
if !ok {
return NULL
}
return pair.Value
}
func evalIndexExpression(left, index object.Object) object.Object {
switch {
case left.Type() == object.ArrayObj && index.Type() == object.IntegerObj:
return evalArrayIndexExpression(left, index)
case left.Type() == object.HashObj:
return evalHashIndexExpression(left, index)
default:
return newError("index operator not supported: %s", left.Type())
}
}
所以关于map的索引,也没有很复杂不是嘛!
内建函数
内建函数,在用户视角,是一个普通的函数,调用,然后得到值对吧。但是在我们解释器视角,它是一个特殊的,可以被解释器识别的Object
,所以第一件事,就是为内建函数定义一个规范:
go
type BuiltInFunction func(args ...Object) Object
而我们的内建函数则是一个包含此类规范值的一个Object,这里展示一个len()
函数的实现,可以帮我们拿到字符串和数组的长度。
go
type BuiltInFunction func(args ...Object) Object
type Builtin struct {
Fn BuiltInFunction
}
func (b *Builtin) Type() Type {
return BuiltInObj
}
func (b *Builtin) Inspect() string {
return "builtin function"
}
我们尝试把内建函数视为一个特殊的类型,并把它放在内建map中来特殊处理:
go
func evalIdentifier(node *ast.Identifier, env *object.Environment) object.Object {
if val, ok := env.Get(node.Value); ok {
return val
}
if val, ok := builtins[node.Value]; ok {
return val
}
return newError("identifier not found: " + node.Value)
}
var (
builtins = map[string]*object.Builtin{
"len": {
Fn: func(args ...object.Object) object.Object {
if len(args) != 1 {
return newError("wrong number of arguments. got=%d, want=1", len(args))
}
switch arg := args[0].(type) {
case *object.String:
return &object.Integer{Value: int64(len(arg.Value))}
case *object.Array:
return &object.Integer{Value: int64(len(arg.Elements))}
}
return newError("argument to `len` not supported, got %s", args[0].Type())
},
},
}
)
通过这样,内建函数可以被解释器识别,并给予"特殊身份"来执行,剩下的流程类似把函数替换成执行结果,之后继续重复上述的所有操作。
举一反三,我们还可以创建更多的函数,比如追加数据的append
,获取首个和最后一个元素的first
和last
... ...
总结
在这里我们没有提及垃圾处理的相关知识,实际上这是由Go的GC自动完成的,我们的Monkey创建出来的对象,在不使用之后,便会被清理掉,所以内存的管理不是我们考虑的事,这也是使用Go的原因之一。
当然我们上面的实现,大量使用了字符串处理,类似各种类型,如果选用uint8
处理,会得到更好的性能,但是别忘了,这篇文章的主要目的,是为了学习,不是吗?清晰直白的表达和逻辑相比起复杂的实现,才是我们真正需要的"性能"。
如何实现本地调用呢?有一个简单的实现,就是把这些调用统一封装,做成类似syscall
的形式,并设置为内建函数,毕竟无论如何,这都是要依托宿主机语言实现的,即Go的实现。
还有一个小小的缺失,是我们并没有添加结构体,或者说复合类型的实现,这其实是很重要的,但是如果你坚持走到这里,去实现一个类似struct
的关键字,其实很简单:拿到语法格式 -> 词法分析 -> 语法解析 -> 执行的过程再来一遍就可以。可是该怎么解释呢?解释成语句?函数?又或者我们直接创造一种新的语法,直接对已经存在的标识符添加子字段?这些都是语言设计者的考量。