原文地址:Let’s Build A Simple Interpreter. Part 12.
不怕慢,就怕停。 ── 中国谚语
嗨,欢迎回来!
今天我们会多走几步并学习怎样解析 Pascal 过程声明。
Figure 1: babysteps
什么是一个 过程声明 ?过程声明是一个定义一个标识符(过程名)的语言结构,它将该 标识符与一个 Pascal 代码块关联起来。
在深入之前,先说一点 Pascal 过程和它的声明:
- Pascal 过程没有返回语句。他们在到达相应代码块结尾时退出。
- Pascal 过程可以互相嵌套。
- 为了简单,本文中的过程声明不会有正式的参数。不过别担心,在后续文章中会解决这个 问题的。
下面是今天的测试程序:
1 | PROGRAM Part12; VAR a : INTEGER; PROCEDURE P1; VAR a : REAL; k : INTEGER; PROCEDURE P2; VAR a, z : INTEGER; BEGIN {P2} z := 777; END; {P2} BEGIN {P1} END; {P1} BEGIN {Part12} a := 10; END. {Part12} |
从上面可以看出,我们定义了两个过程(P1和P2)且 P2 是嵌套在 P1 里的。在上面的代码 中,我在注释中用过程名来清楚地表示每个过程的开始和结束位置。
我们今天的目标很明确:学习怎样解析这样的代码。
首先,我们需要对语法进行一些修改以添加过程声明。好了,让我们动手吧!
下面是修改过的语法规则:
Figure 2: grammar
declarations : VAR (variable_declaration SEMI)+ | (PROCEDURE ID SEMI block SEMI)* | empty
过程声明子规则包含了保留关键字 PROCEDURE 后跟一个标识符(过程名),后跟一个分
号,后再跟一个 block
规则,最后由分号结尾。哇!这里我觉得,不管上句话说得多好
都不如一幅图能说清楚!:)
下面是规则 declarations
修改后的句法图:
Figure 3: syntaxdiagram
从语法和上面的图中可以看出,在同一级别你可以有任意多的过程声明。例如,在下面的代 码片段中我们在同一级别上定义了两个过程声明,P1 和 P1A:
1 | PROGRAM Test; VAR a : INTEGER; PROCEDURE P1; BEGIN {P1} END; {P1} PROCEDURE P1A; BEGIN {P1A} END; {P1A} BEGIN {Test} a := 10; END. {Test} |
上面的句法图和语法规则也表明了过程声明可以嵌套,因为 procedure declaration
子
规则引用了 block=规则,而 =block
规则包含了 declarations
规则,而
declarations
规则反过来又包含了 procedure declaration
子规则。作为提醒,下面
是第十部分中 block
规则的句法图和语法:
Figure 4: block rule from part10
好了,现在让我们把注意力集中在解释器中需要修改以支持过程声明的部分上:
更新 Lexer
我们只需要增加一个名为 PROCEDURE 的 token:
1 | PROCEDURE = 'PROCEDURE' |
并将 ‘PROCEDURE’ 添加到保留关键字中。下面是完整的保留关键字到 token 的映射:
1 | RESERVED_KEYWORDS = { |
更新 Parser
下面是 parser 所要做修改的概述:
- 新的
ProcedureDecl
AST 结点 - 修改 parser 的
declarations
方法以支持过程声明
让我们看一下这些修改。
ProcedureDecl
AST 结点代表了一个过程声明。它的构建函数接受过程名和代码块的 AST 结点作为参数。1
2
3
4class ProcedureDecl(AST):
def __init__(self, proc_name, block_node):
self.proc_name = proc_name
self.block_node = block_node下面是
Parser
类中修改过的declarations
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25def declarations(self):
"""declarations : VAR (variable_declaration SEMI)+
| (PROCEDURE ID SEMI block SEMI)*
| empty
"""
declarations = []
if self.current_token.type == VAR:
self.eat(VAR)
while self.current_token.type == ID:
var_decl = self.variable_declaration()
declarations.extend(var_decl)
self.eat(SEMI)
while self.current_token.type == PROCEDURE:
self.eat(PROCEDURE)
proc_name = self.current_token.value
self.eat(ID)
self.eat(SEMI)
block_node = self.block()
proc_decl = ProcedureDecl(proc_name, block_node)
declarations.append(proc_decl)
self.eat(SEMI)
return declarations希望上面的代码是无需解释的。它符合文章前面的过程声明的语法/句法图。
更新符号表构建器
我们还需要在 Interpreter
类中增加一个新的空的 visit_ProcedureDecl
方法,它会
让我们的解释器忽略所有的过程声明。
到现在为止还不错。
既然我们已经做了所有必要的修改,让我们看一看加入新的 ProcedureDecl
结点后抽象
语法树是什么样的。
下面又是我们的 Pascal 程序(你可以直接从 GitHub 下载):
1 | PROGRAM Part12; VAR a : INTEGER; PROCEDURE P1; VAR a : REAL; k : INTEGER; PROCEDURE P2; VAR a, z : INTEGER; BEGIN {P2} z := 777; END; {P2} BEGIN {P1} END; {P1} BEGIN {Part12} a := 10; END. {Part12} |
让我们生成一个 AST 然后用程序 genastdot.py 来可视化一下:
1 | $ python genastdot.py part12.pas > ast.dot && dot -Tpng -o ast.png ast.dot |
Figure 5: procdecl ast
在上面的图片中你可以看到两个 ProcedureDecl
结点:ProcDecl:P1 和 ProcDecl:P2 分
别对应于过程 P1 和 P2。任务完成。:)
作为今天最后一项,让我们像以前一样快速地检查一下我们修改过的解释器,这次这个 Pascal 程序里包含了过程声明。如果你还没下载解释器和测试程序,下载下来并在命令行 上运行它。输出应该像下面这样:
$ python spi.py part12.pas Define: INTEGER Define: REAL Lookup: INTEGER Define: <a:INTEGER> Lookup: a Symbol Table contents: Symbols: [INTEGER, REAL, <a:INTEGER>] Run-time GLOBAL_MEMORY contents: a = 10
好了,在掌握了这些知识和经验的基础上,我们已经准备好解决嵌套作用域的问题,我们需 要理解它来使得我们可以分析嵌套作用域,并帮助我们做好处理过程和函数调用的准备。而 这正是我们下一篇文章中要做的:深入到嵌套作用域中。所以下次别忘了带上游泳装备!敬 请关注,下次再见!
本系列的所有文章:
- Let’s Build A Simple Interpreter. Part 1.
- Let’s Build A Simple Interpreter. Part 2.
- Let’s Build A Simple Interpreter. Part 3.
- Let’s Build A Simple Interpreter. Part 4.
- Let’s Build A Simple Interpreter. Part 5.
- Let’s Build A Simple Interpreter. Part 6.
- Let’s Build A Simple Interpreter. Part 7.
- Let’s Build A Simple Interpreter. Part 8.
- Let’s Build A Simple Interpreter. Part 9.
- Let’s Build A Simple Interpreter. Part 10.
- Let’s Build A Simple Interpreter. Part 11.
- Let’s Build A Simple Interpreter. Part 12.
- Let’s Build A Simple Interpreter. Part 13.
- Let’s Build A Simple Interpreter. Part 14.