开工一个月,我用Go造了一个编程语言!

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,因为猴子很聪明,很灵活,就像我们要创作的这个动态解释型语言一样,当然PythonJS也是此类型。

而我们的语言,是类似C语言的形式,但是又更加自由。

为什么是解释型

当然是简单啦!如果我们要做一个编译型语言,且不说难度问题,就算我们做出来了,那恐怕也不是简单的一篇文章能介绍完的。人类编程语言发展至今,针对编译器的优化,编译原理的分析,还有设计,光是论文和讨论,就足够喝一壶了。

哪怕是选择最简单的编译器去实现,涉及到的复杂度也难以形容。

选择解释型,可以依托宿主语言本身的设计,而仅仅实现简单的前端编译和计算,同时又可以系统的了解一本语言的诞生。这正好符合我们的目的。

对,没错,本文的目的是为了学习,而不是为了生产制造一门“I defeat Python by my new language!”,诚然Python性能已经拉胯到成为计量单位了。

不过随着时代的发展,现在的语言并不单单是某一种类型,就好比Java,一开始是动态强类型,而且Java比较特殊,使用了字节码这种中间形式作为初级编译的结果;而为了性能,Java也实现了JITAOT技术,所以现在很难说它是某一种类型的语言。

类似的,即使是被大家所熟知的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相加。

理解到这里,再思考一下,一个操作数(或者说表达式),除了首位的,都有两个操作符,称为opLopR,假设在首位前面添加一个op0,在末尾后面也添加一个op0,并假设op0“吸引力”最低,那么岂不是“格式化”整个序列为一个操作数对应两个操作符的行为了吗?

同样的,在原本的序列里,任何一个操作符都有左右两个操作数,numLnumR(或者expL和expR)。

既然如此,不妨让我们梳理一下:

  • 首先拿到第一个操作数,称为num,并拿到这个操作数的左右op,比较opLopR的“吸引力”大小。

  • opL大,说明当前操作数相比opR,更应该和前面的opL以及numL(opL前面的操作数)绑定。

    • 返回当前操作数,作为opLnumR,即opL的右操作数。
    • 此时上一级操作数变成(numL opL numR),即一个整体出现。
    • 更新上一级操作数为上述结果,继续。
  • opR大,说明当前操作数相比opL,更应该和后面的opRnumRopR后面的操作数)绑定。

    • 此时我们的工作变成了解析numR及其后面的表达式。
    • 开启新的循环,在新的循环里,num更新为numR
    • 等待新循环结束,得到后续表达式为numNew
    • 更新当前操作数为(num opR numNew),继续。
  • 相同的opopL更大。

  • 重复。

来看个例子: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 < ba + 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-elsefor,对于这俩的解析,其实和上面大同小异。

但是真正开始之前,注意到我们的实现,从原本的在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也是,这些都是执行器部分的事情,在解析器部分,只需要简单的把continuebreak当成特殊语句处理即可,这样方便执行器识别:

 

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的解析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值