从零开始实现 Lua 解释器之 AST

警告⚠️:这将是一个又臭又长的系列教程,教程结束的时候,你将拥有一个除了性能差劲、扩展性差、标准库不完善之外,其他方面都和官方相差无几的 Lua 语言解释器。说白了,这个系列的教程实现的是一个玩具语言,仅供学习,无实用性。请谨慎 Follow,请谨慎 Follow,请谨慎 Follow。

这是本系列教程的第四篇,如果你没有看过之前的文章,请从头观看。

前言


从本节起,我们开始实现语法分析器,也就是一直被神化的所谓的 Parser。也许看完本系列教程之后,你就会觉得 Parser 并没有那么难。关于 Parser,我不想解释太多概念性的东西,如果你不了解它,请先看这篇文章:《谈谈Parser》 - 王垠,看完以后再继续往下看。

在实现 Parser 之前,我们首先要定义抽象语法树(Abstract Syntax Tree,AST),将词法分析器生成的 Token 列表转换为 AST 是 Parser 的主要任务。在 AST 中,每个节点都表示源代码中的一种结构,之所以说它是抽象的,是因为它并没有表示出真实语法中出现的每个细节,而是使用一种抽象的树形结构来表现。

语法分析阶段生成的 AST 中并没有包含相应的语义信息,即上下文无关。

语义信息指的是上下文相关的信息,例如数字和 nil 不能相加,break 语句必须在某个循环之内,未定义的变量不能使用(在 Lua 中未定义的变量被看作 nil)等等。

在语义分析阶段,相应的语义信息才会被加入到 AST 中,这时的 AST 也就变为上下文相关的了。

语法分析流程


举个例子来说,下面这段程序:

local name, age = 'Bob', 18
age = age + 1

首先会被词法分析器解析为下面 Token 列表:

类型
TokenLocal <空>
TokenID name
TokenComma <空>
TokenID age
TokenAssign <空>
TokenString Bob
TokenComma <空>
TokenNumber 18
TokenID age
TokenAssign <空>
TokenID age
TokenAdd <空>
TokenNumber 1

然后这个列表被送入 Parser 中,产出下图所示的抽象语法树:

AST 图例

由上图可知,AST 的节点有很多种类型组成,每种语法结构都会生成固定类型的子树。下面我们具体解释一下 AST 中节点的组成以及每个节点对应的源代码。

AST 组成


首先,为了后续的方便,我们统一使用 SyntaxTree 来表示一切节点,而不是使用具体的类型。它的定义如下:

type SyntaxTree interface{}

Chunk

Chunk 是 AST 的根节点,用来表示整个程序,它在语法结构上和 Block 相似,但因为是根节点,所以并不能包含自身。它包含且仅包含一个 Block。

type Chunk struct {
    Block SyntaxTree
}

Block

Block 表示一个代码块,它由零到多个语句组成。首先,整个源文件中的所有代码被看作是一个 Block;其次,do ... end 语句、循环以及函数等内部包含的代码块也都是一个 Block。

type Block struct {
    Stmts []SyntaxTree
}

DoStatement

用来表示 do ... end 语句。注意到,它包含一个 Block。

type DoStatement struct {
    Block SyntaxTree
}

WhileStatement

表示 While 循环语句。其中 Exp 表示判断循环是否继续的表达式,Block 表示的是函数内部的代码块。

type WhileStatement struct {
    Exp   SyntaxTree
    Block SyntaxTree
}

举例:

while a < 10 do a = a + 1 end

IfStatement

用来表示 If 语句。其中 Exp 表示判断条件是否成立的表达式,TrueBranch 表示条件成立时要执行的代码块,FalseBranch 则可能是 ElseifStatement、ElseStatement 或者为空(nil)

IfStatement struct {
    Exp         SyntaxTree
    TrueBranch  SyntaxTree
    FalseBranch SyntaxTree
}

ElseifStatement

用来表示 Elseif 语句。其中 Exp 表示判断条件是否成立的表达式,TrueBranch 表示条件成立时要执行的代码块,FalseBranch 则可能是 ElseifStatement、ElseStatement 或者为空(nil)

ElseifStatement struct {
    Exp         SyntaxTree
    TrueBranch  SyntaxTree
    FalseBranch SyntaxTree
}

ElseStatement

用来表示 Else 语句。其中 Block 是要执行的代码块。

ElseStatement struct {
    Block SyntaxTree
}

举例:为了帮助读者更好的理解 if ... elseif ... else ... end 语句生成的 AST 的样子,我们举个例子:

if (exp1) then block
elseif (exp2) then block
else block end

对于上面的代码,生成的 AST 如下所示:

AST 实例

LocalNameListStatement

用来表示定义 Local 变量的语句。其中 NameList 表示的是欲定义的局部变量列表,ExpList 是赋值给前面的 NameList 的表达式列表。

LocalNameListStatement struct {
    NameList SyntaxTree
    ExpList  SyntaxTree
}

举例:

local a, b, c = 12, "abc", 3.14

AssignmentStatement

用来表示变量的赋值和全局变量的声明。其中 VarList 表示欲赋值的(欲申请的全局)变量列表,ExpList 是赋值给前面的 VarList 的表达式列表。

AssignmentStatement struct {
    VarList SyntaxTree
    ExpList SyntaxTree
}

举例:

a, b = "abc", 3.14

VarList

用于 AssignmentStatement 中的 VarList。

VarList struct {
    VarList []SyntaxTree
}

Terminator

终结符,表示无法再往下分解最小的语法单元,AST 中的叶子节点。

Terminator struct {
    Tok *scanner.Token
}

BinaryExpression

双目运算符表达式。

BinaryExpression struct {
    Left    SyntaxTree
    Right   SyntaxTree
    OpToken *scanner.Token
}

UnaryExpression

单目运算符表达式。

UnaryExpression struct {
    Exp     SyntaxTree
    OpToken *scanner.Token
}

NameList

用于 LocalNameListStatement 中的 NameList。

NameList struct {
    Names []*scanner.Token
}

ExpressionList

用于 LocalNameListStatement 和 AssignmentStatement 中的 ExpList。

ExpressionList struct {
    ExpList []SyntaxTree
}

至此,构建 AST 所需要的类型已经全部完成了。你可以在此查看完整的源代码:地址

获取源代码


代码已托管到 Github 上:SLua,每一个阶段的代码我都会创建一个 release,你可以直接下载作为参照。虽然提供了源代码,但并不建议直接复制粘贴,因为这样学到的知识会很容易忘记。

刚开始玩 Github 和简书,所以没有任何粉丝和关注量(哭),如果你觉得这篇教程有帮助,请不要吝啬给文章点个喜欢,给 Github 上的项目点个 Star。如果能 Follow 一下简书和 Github 的账号就更好啦,我也会更加有动力将这个系列写下去。:)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容