一起来写个简单的解释器(12)
英文出处:Ruslan’s Blog
- 《一起来写个简单的解释器(1)》
- 《一起来写个简单的解释器(2)》
- 《一起来写个简单的解释器(3)》
- 《一起来写个简单的解释器(4)》
- 《一起来写个简单的解释器(5)》
- 《一起来写个简单的解释器(6)》
- 《一起来写个简单的解释器(7)》
- 《一起来写个简单的解释器(8)》
- 《一起来写个简单的解释器(9)》
- 《一起来写个简单的解释器(10)》
- 《一起来写个简单的解释器(11)》
“不怕慢, 只怕停。“ ——中国谚语。
你好,欢迎回来!
今天,我们将要用更多的婴儿步伐,来学习如何解析Pascal过程声明。
什么是过程声明? 过程声明是定义一个标识符(过程名称)并将其与一个Pascal代码块相关联的语言结构,
在我们介绍之前,先谈谈一下Pascal过程及其声明:
- Pascal过程没有返回语句。 当到达相应区块的结束位置时,它们退出。
- Pascal过程可以嵌套在一起。
- 为了简单起见,本文中的过程声明不会有任何形式参数。 但别担心,我们将在后面的章节中介绍。
这是我们今天的测试程序:
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中。 在上面的代码中,我使用了一个过程名的注释来清楚地指出每个过程的主体在哪里开始以及在哪里结束。
我们今天的目标非常明确:学习如何解析这样的代码。
首先,我们需要对语法做一些修改来添加过程声明。 搞起!
这是更新的声明语法规则:
过程声明子规则包含保留关键字PROCEDURE,后跟一个标识符(过程名称),后跟一个分号,接着跟一个以分号结尾的 block 规则。哇! 前面的句子又是一个我认为一图胜千言的例子 :)
以下是声明规则的更新语法图:
从上面的语法和图表你可以看到,你可以在同一个层次上想有多少过程声明就有多少。 例如,在下面的代码片段中,我们定义了两个过程声明P1和P1A,它们在同一个层次上:
PROGRAM Test;
VAR
a : INTEGER;
PROCEDURE P1;
BEGIN {P1}
END; {P1}
PROCEDURE P1A;
BEGIN {P1A}
END; {P1A}
BEGIN {Test}
a := 10;
END. {Test}
上面的图表和语法规则还表明,过程声明可以是嵌套的,因为过程声明子规则引用了 block 规则, block 规则又包含了的声明(declarations)规则,声明规则又包含过程声明子规则。 提醒一下,下面是第10部分 block 规则的语法图和语法:
好的,现在让我们把重点放在需要更新以支持过程声明的解释器组件上:
更新词法分析器
我们所需要做的就是添加一个名为PROCEDURE的新标记:
PROCEDURE = 'PROCEDURE'
并将“PROCEDURE”添加到保留关键字。 以下是保留关键字到标记的完整映射:
RESERVED_KEYWORDS = {
'PROGRAM': Token('PROGRAM', 'PROGRAM'),
'VAR': Token('VAR', 'VAR'),
'DIV': Token('INTEGER_DIV', 'DIV'),
'INTEGER': Token('INTEGER', 'INTEGER'),
'REAL': Token('REAL', 'REAL'),
'BEGIN': Token('BEGIN', 'BEGIN'),
'END': Token('END', 'END'),
'PROCEDURE': Token('PROCEDURE', 'PROCEDURE'),
}
更新解析器
以下是解析器更改的总结:
- 新的ProcedureDecl AST节点
- 更新解析器的声明方法以支持过程声明
来看看这些变化。
ProcedureDecl AST节点表示一个过程声明。 类构造函数将过程名称和过程引用代码块的AST节点作为参数。
class ProcedureDecl(AST): def __init__(self, proc_name, block_node): self.proc_name = proc_name self.block_node = block_node
这里是Parser类的更新的 declarations 方法
def 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
希望上面的代码是不言自明的。 它遵循本文前面的过程声明的语法/语法图。
更新符号表构建器
因为我们还没有准备好处理嵌套的过程范围,所以我们只需在SymbolTreeBuilder AST访问者类中添加一个空的visit_ProcedureDecl方法即可。 我们将在下一篇文章中填写。
def visit_ProcedureDecl(self, node):
pass
更新解释器
我们还需要在Interpreter类中添加一个空的visit_ProcedureDecl方法,这将导致我们的解释器默默地忽略我们所有的过程声明。
一切还好!
现在我们已经做了所有必要的修改,让我们看看有新的ProcedureDecl节点的抽象语法树是什么样的。
这里是我们的Pascal程序(可以直接从GitHub下载):
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}
让我们用genastdot.py程序看看产生的AST:
$ python genastdot.py part12.pas > ast.dot && dot -Tpng -o ast.png ast.dot
在上图中可以看到两个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
好,凭借我们所掌握的所有知识和经验,我们已经准备好处理我们需要了解的嵌套范围的主题,以便能够分析嵌套过程并准备处理过程和函数调用,而这正是我们下一篇文章要做的:深入嵌套的范围。 所以下次别忘了带上你的游泳装备! 敬请期待,再见!