Rickey 裘 一无所知

构建解释器:part12(译)

2017-02-20
Ruslan, Kai Qiu(译)

英文原文

不要担心走得缓慢;害怕原地不动! - 中国谚语(我表示怀疑!!!)

大家好,欢迎回来!

今天我们将要走几小步来学习如何识别Pascal过程声明。

babysteps

什么是过程声明?一个过程声明是一个语言结构,它定义了一个标记符,以及一个关联的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中。在上述代码中,我用带有过程名字的注释来标记各个过程的开始位置和结束位置。

我们今天的目标相当明确:学习如何识别上面那样的代码。

首先,为了添加过程声明,我们需要调整文法。好了,我们开始!

grammar

过程声明子规则包含保留字–PROCEDURE,后面跟着一个标记符(过程名字),再跟一个分号,分号后面跟着代码块,最后以另外一个分号结尾。

这里是更新后的语法图,表示声明规则:

syntaxdiagram

从文法以及上述语法图可以看出,你可以声明任意数量想要的过程。例如,在下面这个代码片段中,我们定义了两个过程声明,P1和P2,位于同一层级。

PROGRAM Test;
VAR
   a : INTEGER;

PROCEDURE P1;
BEGIN {P1}

END;  {P1}

PROCEDURE P1A;
BEGIN {P1A}

END;  {P1A}

BEGIN {Test}
   a := 10;
END.  {Test}

上面的文法和图表也表明过程声明是可以嵌套的,因为过程声明子规则引用了程序块规则,而程序块规则又包含声明规则,声明规则包含过程声明,继而过程声明是嵌套的。作为提醒,这里是第十部分关于程序块规则的一个语法图和文法:

grammar

好了,现在让我们把目光聚焦到解释器组件,它也需要更新来支持过程声明:

更新词法分析器

我们只要添加新的标识符–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'),
}

更新语法分析器

语法分析器变化总结:

  1. 新的ProcedureDecl 抽象语法树节点
  2. 更新语法分析器中过程声明对应的方法,支持过程声明

实现这些变化.

1. ProcedureDecl抽象语法树节点代表了一个过程声明。类构造函数以过程名字以及该过程名字指向的程序块节点为参数。

class ProcedureDecl(AST):
    def __init__(self, proc_name, block_node):
        self.proc_name = proc_name
        self.block_node = block_node

2. 这是更新后的语法分析器的声明方法

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抽象语法树访问者类添加一个空的visit_ProcedureDecl方法。下一篇文章解决这个问题。

def visit_ProcedureDecl(self, node):
	pass

更新解释器

添加一个空的visit_ProcedureDecl方法,用来让我们的解释器忽略所有过程声明。

So far, so good.

既然我们已经做好了这些必要的准备,让我们看看带有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工具可视化。

$ python genastdot.py part12.pas > ast.dot && dot -Tpng -o ast.png ast.dot

grammar

在上述的图形中,你能看到两个ProcedureDecl节点:ProcDecl:P1和ProcDecl:P2,分别对应过程P1和过程P2。完成使命!

作为今天最后的一个条目,我们快速确认一下我们更新后的解释器能如从前般工作,处理好带有过程声明的Pascal程序。如果还没有下载,请下载解释器和测试程序,在命令行运行。你的输出看上去应该类似这样:

$ python spi.pi 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

好, 所有知识和经验都被我们拿下了,我们准备解决嵌套作用域的问题。理解嵌套作用域能帮助我们分析嵌套过程以及让我们能够处理过程和函数调用。这就是我们在下一篇文章中将要做的事情:深入理解嵌套作用域。下次见。


下一篇 《胭脂扣》

评论