一起来写个简单的解释器(14)嵌套作用域和源到源编译器

英文出处:Ruslan’s Blog

正如在上一篇文章中承诺的那样,今天我们将深入探讨作用域。

图1

这是我们今天要学习的内容:

  • 将学习作用域,为什么它们很有用,以及如何在符号表的代码中实现它们。
  • 将学习嵌套作用域以及如何使用链式作用域符号表来实现嵌套作用域。
  • 将学习如何解析有形式参数的过程声明,以及如何在代码中表示过程符号。
  • 将学习如何扩展我们的语义分析器,以在存在嵌套作用域的情况下进行语义检查。
  • 将学习更多关于名称解析的知识 ,以及当程序有嵌套作用域时,语义分析器如何将名称解析到它们的声明。
  • 将学习如何构建作用域树 。
  • 今天也将学习如何编写我们自己的 源到源编译器 ! 我们将在后面的文章中看到它与作用域讨论的相关性。

让我们开始吧! 或者我应该说,让我们投入进去!

目录

作用域和作用域的符号表

什么是作用域 ? 作用域 是名称可以使用的程序的文本区域。 让我们来看看下面的示例程序,例如:

program Main;
   var x, y: integer;
begin
   x := x + y;
end.

在Pascal中, PROGRAM关键字(不区分大小写)引入了一个通常被称为 全局作用域 的新作用域,所以上面的程序有一个全局作用域 ,声明的变量x和y在整个程序中是可见的和可访问的。 在上述情况下,文本区域以关键字 program 开始,以关键字 end 和点结束 。 在该文本区域中,可以使用名称x和y ,因此这些变量(变量声明)的作用域是整个程序:

图1

当你查看上面的源代码,特别是表达式 x := x + y 时 ,直观地知道它应该编译(或者被解释)而没有问题,因为表达式中变量x和y的作用域是全局作用域,然后表达式 x := x + y 中的变量引用x和y解析为已声明的整数变量x和y 。 如果你之前用任何主流的编程语言进行编程,这里就不会有什么惊讶。

当我们谈论变量的作用域时,我们实际上是在谈论其声明的作用域:

图1

在上图中,垂直线显示声明变量的作用域,声明名称x和y可以使用的文本区域,即它们是可见的文本区域。 正如你所看到的, x和y的作用域是整个程序,如垂直线所示。

Pascal程序被认为是 词法作用域 的 (或 静态作用域 ),因为甚至不需要执行程序,可以通过查看源代码,完全基于名称(引用)的文本规则来决定解析或引用哪个声明。 例如,在Pascal中,像 programend 这样的词汇关键词划分了作用域的文本边界:

图1

为什么作用域很有用?

  • 每个作用域都会创建一个独立的名称空间,这意味着在作用域中声明的变量不能从外部访问。
  • 您可以在不同的作用域内重复使用相同的名称,只需查看程序源代码即可清楚的知道,在程序中每个位置该名称所引用的具体变量声明。
  • 在嵌套的作用域中,可以重新声明一个与外部作用域相同名称的变量,有效地隐藏外部声明,从而可以控制对外部作用域的不同变量的访问。

除了全局作用域外 ,Pascal支持嵌套过程,每个过程声明都引入一个新的作用域,这意味着Pascal支持嵌套作用域。

当我们讨论嵌套作用域时,讨论作用域级别来展示它们的嵌套关系是很方便的。 按名称来引用作用域也很方便。 当开始讨论嵌套的作用域时,我们将同时使用作用域级别和作用域名称。

让我们来看看下面的示例程序和程序中的每个下标名称,以清楚说明:

  1. 每个变量(符号)在什么级别被声明
  2. 名称引用的哪个声明和哪个级别的变量:

图1

从上面的图片我们可以看到几件事情:

  • 我们有一个单一的作用域:全局作用域 ,由PROGRAM关键字引入
  • 全局作用域在级别1级
  • 变量(符号) x和y在第1级声明( 全局作用域 )。
  • 内建整型类型也在第1级声明
  • 程序名称Main有一个下标0。为什么程序的名字在0级,你可能想知道? 这是为了清楚说明程序的名称不在全局作用域内 ,而在其他外部作用域内,它的级别为零。
  • 变量x和y的作用域是整个程序,如垂直线所示
  • 作用域信息表为程序中的每个级别显示相应的作用域级别、作用域名称和在作用域中声明的名称。 该表的目的是总结和直观地显示程序中作用域的不同信息。

如何在代码中实现作用域的概念? 为了在代码中表示一个作用域,我们需要一个 作用域符号表 。 我们已经知道符号表,但什么是作用域符号表 ? 作用域符号表 是一个有些许修改的符号表,你马上就会看到。

从现在开始,我们将使用作用域这个词来表示作用域的概念以及作用域符号表的引用,这涉及到代码作用域的一个实现。

即使在代码中,作用域是由 ScopedSymbolTable 类的实例表示的,为了方便起见,我们将在整个代码中使用名为scope的变量。 所以当你在解释器的代码中看到一个变量 scope 时,你应该知道它实际上引用了作用域符号表 。

好吧,让我们通过将其重命名为 ScopedSymbolTable 类,添加两个新字段 scope_levelscope_name ,并更新作用域符号表的构造函数来增强我们的 SymbolTable 类。 同时,让我们更新 str 方法来打印附加信息,即 scope_levelscope_name 。 这是符号表的新版本, ScopedSymbolTable

class ScopedSymbolTable(object):
    def __init__(self, scope_name, scope_level):
        self._symbols = OrderedDict()
        self.scope_name = scope_name
        self.scope_level = scope_level
        self._init_builtins()

    def _init_builtins(self):
        self.insert(BuiltinTypeSymbol('INTEGER'))
        self.insert(BuiltinTypeSymbol('REAL'))

    def __str__(self):
        h1 = 'SCOPE (SCOPED SYMBOL TABLE)'
        lines = ['\n', h1, '=' * len(h1)]
        for header_name, header_value in (
            ('Scope name', self.scope_name),
            ('Scope level', self.scope_level),
        ):
            lines.append('%-15s: %s' % (header_name, header_value))
        h2 = 'Scope (Scoped symbol table) contents'
        lines.extend([h2, '-' * len(h2)])
        lines.extend(
            ('%7s: %r' % (key, value))
            for key, value in self._symbols.items()
        )
        lines.append('\n')
        s = '\n'.join(lines)
        return s

    __repr__ = __str__

    def insert(self, symbol):
        print('Insert: %s' % symbol.name)
        self._symbols[symbol.name] = symbol

    def lookup(self, name):
        print('Lookup: %s' % name)
        symbol = self._symbols.get(name)
        # 'symbol' is either an instance of the Symbol class or None
        return symbol

让我们更新语义分析器的代码,使用变量 scope 而不是 symtab ,并从 visit_VarDecl 方法中删除检查源程序中重复标识符的语义检查,以减少程序输出中的噪音。

下面是一段代码,显示了我们的语义分析器如何实例化ScopedSymbolTable类:

class SemanticAnalyzer(NodeVisitor):
    def __init__(self):
        self.scope = ScopedSymbolTable(scope_name='global', scope_level=1)

    ...

您可以在文件scope01.py中找到所有更改。下载文件,在命令行上运行它,并检查输出。这是我得到的:

$ python scope01.py
Insert: INTEGER
Insert: REAL
Lookup: INTEGER
Insert: x
Lookup: INTEGER
Insert: y
Lookup: x
Lookup: y
Lookup: x


SCOPE (SCOPED SYMBOL TABLE)
===========================
Scope name     : global
Scope level    : 1
Scope (Scoped symbol table) contents
------------------------------------
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
   REAL: <BuiltinTypeSymbol(name='REAL')>
      x: <VarSymbol(name='x', type='INTEGER')>
      y: <VarSymbol(name='y', type='INTEGER')>

大部分的输出应该看起来很熟悉。

现在您已经了解了作用域的概念,以及如何通过使用作用域化的符号表在代码中实现的作用域,现在是时候讨论嵌套作用域和对作用域符号表进行更戏剧性的修改,而不仅仅是添加两个简单的字段。

具有形式参数的过程的声明

我们来看一个包含过程声明的nestedscopes02.pas文件中的示例程序:

program Main;
   var x, y: real;

   procedure Alpha(a : integer);
      var y : integer;
   begin
      x := a + x + y;
   end;

begin { Main }

end.  { Main }

在这里注意到的第一件事是我们有一个带参数的过程,而我们还没有学会如何处理这种情况。 让我们通过绕个弯来学习如何处理形式过程参数来填补这个空白。

ASIDE : 形式参数是在程序声明中显示的参数。 参数 (也称为实际参数 )是在特定过程调用中传递给过程的不同变量和表达式。

这里是我们需要做的改变列表来支持带参数的过程声明:

  1. 添加 Param AST节点

    class Param(AST):
    def __init__(self, var_node, type_node):
        self.var_node = var_node
        self.type_node = type_node
    
  2. 更新 ProcedureDecl 节点的构造函数以获取参数: params

    class ProcedureDecl(AST):
    def __init__(self, proc_name, params, block_node):
        self.proc_name = proc_name
        self.params = params  # a list of Param nodes
        self.block_node = block_node
    
  3. 更新 declarations 规则以反映过程声明子规则中的更改

    def declarations(self):
    """declarations : (VAR (variable_declaration SEMI)+)*
                    | (PROCEDURE ID (LPAREN formal_parameter_list RPAREN)? SEMI block SEMI)*
                    | empty
    """
    
  4. 添加 formal_parameter_list 规则和方法

    def formal_parameter_list(self):
    """ formal_parameter_list : formal_parameters
                              | formal_parameters SEMI formal_parameter_list
    """
    
  5. 添加 formal_parameters 规则和方法

    def formal_parameters(self):
    """ formal_parameters : ID (COMMA ID)* COLON type_spec """
    param_nodes = []
    

通过添加上面的方法和规则,我们的解析器将能够解析像这样的过程声明(为了简洁起见,我没有显示声明过程的主体):

procedure Foo;

procedure Foo(a : INTEGER);

procedure Foo(a, b : INTEGER);

procedure Foo(a, b : INTEGER; c : REAL);

为我们的示例程序生成一个AST。下载 genastdot.py并在命令行上运行以下命令:

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

这里是生成的AST的图片:

图1

您现在可以看到图片中的 ProcedureDecl 节点具有 Param 节点作为其子节点。

您可以在spi.py文件中找到完整的更改。 花点时间研究一下这些变化。 你以前做过类似的改变,应该很容易理解,你应该能够自己实现它们。

过程符号

当我们谈论过程声明的话题的时候,我们也谈谈过程符号。

与变量声明和内置类型声明一样,过程也有一个单独的符号类别。 让我们为过程符号创建一个单独的符号类:

class ProcedureSymbol(Symbol):
    def __init__(self, name, params=None):
        super(ProcedureSymbol, self).__init__(name)
        # a list of formal parameters
        self.params = params if params is not None else []

    def __str__(self):
        return '<{class_name}(name={name}, parameters={params})>'.format(
            class_name=self.__class__.__name__,
            name=self.name,
            params=self.params,
        )

    __repr__ = __str__

过程符号有一个名称(它是过程的名字),它们的类别是过程(用类名编码),类型是 None ,因为在Pascal过程中不返回任何东西。

过程符号还包含有关程序声明的附加信息,即包含有关程序形式参数的信息,如上面代码中所示。

通过添加过程符号,我们的新符号层次结构如下所示:

图1

嵌套作用域

在绕弯之后,回到我们的程序和嵌套作用域的讨论:

program Main;
   var x, y: real;

   procedure Alpha(a : integer);
      var y : integer;
   begin
      x := a + x + y;
   end;

begin { Main }

end.  { Main }

实际上事情在这里变得更有趣。 通过声明一个新的过程,我们引入一个新的作用域,这个作用域嵌套在PROGRAM语句引入的全局作用域内,所以这是一个Pascal程序中嵌套作用域的情景。

过程的作用域是过程的整个作用域。 过程作用域的开始由 PROCEDURE 关键字标记,结尾用 END 关键字和分号标记。

让我们在程序中的下标名称,并显示一些额外的信息:

图1

图1

上图中可以观察到:

  • 这个Pascal程序有两个作用域级别:1级和2级
  • 嵌套关系图可视化地显示作用域 Alpha 嵌套在全局作用域内 ,因此有两个级别: 在级别1的全局作用域和在级别2的 Alpha 作用域。
  • 过程声明 Alpha 的作用域级别比在过程 Alpha 中声明的变量级别小1。 您可以看到,过程声明 Alpha 的作用域级别是1,过程中变量a和y的作用域级别是2。
  • Alpha 过程中y的变量声明隐藏了y在全局作用域内的声明。 你可以在y1的垂直栏中看到这个洞(顺便说一下,1是下标,它不是变量名的一部分,变量名只是y ),你可以看到y2变量声明的作用域是 Alpha 过程的整个身体。
  • 您已经知道,作用域信息表显示了作用域级别,这些级别的作用域名称以及这些作用域(在这些级别)中分别声明的相应名称。
  • 在图中,也可以看到,我省略了整数和实数类型的作用域(除了在作用域信息表中),因为它们总是在作用域级别1( 全局作用域)中声明 ,所以我不会对整型和实型加下标来节省视觉空间,但是在表示全局作用域的作用域符号表的内容中会一次又一次地看到这些类型。

下一步讨论实现的细节。

首先,让我们把重点放在变量和过程声明上。然后,我们将讨论变量引用以及名称解析如何在存在嵌套作用域的情况下工作。

对于我们的讨论,我们将使用该程序的精简版本。 以下版本没有变量引用:它只包含变量和过程声明:

program Main;
   var x, y: real;

   procedure Alpha(a : integer);
      var y : integer;
   begin

   end;

begin { Main }

end.  { Main }

您已经知道如何使用作用域符号表来代码中表示作用域。现在我们有两个作用域:全局作用域和过程 Alpha 引入的作用域。遵循我们的方法,我们现在应该有两个有作用域符号表:一个用于全局作用域 ,另一个用于 Alpha 作用域。 如何在代码中实现? 我们将扩展语义分析器来为每个作用域创建一个单独的作用域符号表,而不仅仅是为全局作用域。与往常一样,在遍历AST时,作用域构造将会发生。

首先,需要决定在语义分析器的哪个位置创建我们的作用域符号表。 回想一下, PROGRAMPROCEDURE 关键字引入了新的作用域。 在AST中 ,相应的节点是 ProgramProcedureDecl 。 所以要更新我们的 visit_Program 方法,并添加 visit_ProcedureDecl 方法来创建作用域符号表。 我们从 visit_Program 方法开始:

def visit_Program(self, node):
    print('ENTER scope: global')
    global_scope = ScopedSymbolTable(
        scope_name='global',
        scope_level=1,
    )
    self.current_scope = global_scope

    # visit subtree
    self.visit(node.block)

    print(global_scope)
    print('LEAVE scope: global')

该方法有不少变化:

  1. 当访问AST中的节点时,我们首先打印进入的作用域,在这个例子中是 全局
  2. 我们创建一个单独的作用域符号表来表示全局作用域 。 当构造一个 ScopedSymbolTable 的实例时,我们明确地将作用域名称和作用域级别参数传递给类构造函数。
  3. 我们将新创建的作用域分配给实例变量 current_scope 。 在作用域化符号表中插入和查找符号的其他访问者方法将使用 current_scope
  4. 我们访问一个子树(块)。 这是旧的部分。
  5. 在离开全局作用域之前,我们打印全局作用域的内容(作用域符号表)
  6. 我们还打印出我们正在离开全局作用域的信息

现在来添加 visit_ProcedureDecl 方法。 以下是完整的源代码:

def visit_ProcedureDecl(self, node):
    proc_name = node.proc_name
    proc_symbol = ProcedureSymbol(proc_name)
    self.current_scope.insert(proc_symbol)

    print('ENTER scope: %s' %  proc_name)
    # Scope for parameters and local variables
    procedure_scope = ScopedSymbolTable(
        scope_name=proc_name,
        scope_level=2,
    )
    self.current_scope = procedure_scope

    # Insert parameters into the procedure scope
    for param in node.params:
        param_type = self.current_scope.lookup(param.type_node.value)
        param_name = param.var_node.value
        var_symbol = VarSymbol(param_name, param_type)
        self.current_scope.insert(var_symbol)
        proc_symbol.params.append(var_symbol)

    self.visit(node.block_node)

    print(procedure_scope)
    print('LEAVE scope: %s' %  proc_name)

来看看方法的内容:

  1. 该方法所做的第一件事就是创建一个过程符号并将其插入到当前作用域中,这是我们示例程序的全局作用域 。
  2. 然后该方法打印有关进入过程作用域的消息。
  3. 然后我们为过程的参数和变量声明创建一个新的作用域。
  4. 我们将过程作用域指定给 self.current_scope 变量,表明这是我们当前的作用域,所有符号操作( 插入和查找 )将使用当前作用域。
  5. 然后将过程的形式参数插入当前作用域并将它们添加到过程符号中
  6. 然后我们访问AST子树的其余部分——过程的主体。
  7. 最后,我们在离开节点之前打印关于离开作用域的消息,并移动到另一个AST节点(如果有的话)。

现在,需要做的是更新其他的语义分析器访问方法以便在插入和查找符号时使用 self.current_scope 。 让我们这样做:

def visit_VarDecl(self, node):
    type_name = node.type_node.value
    type_symbol = self.current_scope.lookup(type_name)

    # We have all the information we need to create a variable symbol.
    # Create the symbol and insert it into the symbol table.
    var_name = node.var_node.value
    var_symbol = VarSymbol(var_name, type_symbol)

    self.current_scope.insert(var_symbol)

def visit_Var(self, node):
    var_name = node.value
    var_symbol = self.current_scope.lookup(var_name)
    if var_symbol is None:
        raise Exception(
            "Error: Symbol(identifier) not found '%s'" % var_name
        )

visit_VarDeclvisit_Var 现在都将使用 current_scope 来插入和/或查找符号。 具体而言,对于我们的示例程序, current_scope 可以指向全局作用域或 Alpha 作用域。

我们还需要更新语义分析器,并在构造函数中将 current_scope 设置为 None

class SemanticAnalyzer(NodeVisitor):
    def __init__(self):
        self.current_scope = None

克隆文章的GitHub仓库 ,运行scope02.py (它有我们刚刚讨论过的所有变化),检查输出,并确保你明白生成的每行的原因:

$ python scope02.py
ENTER scope: global
Insert: INTEGER
Insert: REAL
Lookup: REAL
Insert: x
Lookup: REAL
Insert: y
Insert: Alpha
ENTER scope: Alpha
Insert: INTEGER
Insert: REAL
Lookup: INTEGER
Insert: a
Lookup: INTEGER
Insert: y


SCOPE (SCOPED SYMBOL TABLE)
===========================
Scope name     : Alpha
Scope level    : 2
Scope (Scoped symbol table) contents
------------------------------------
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
   REAL: <BuiltinTypeSymbol(name='REAL')>
      a: <VarSymbol(name='a', type='INTEGER')>
      y: <VarSymbol(name='y', type='INTEGER')>


LEAVE scope: Alpha


SCOPE (SCOPED SYMBOL TABLE)
===========================
Scope name     : global
Scope level    : 1
Scope (Scoped symbol table) contents
------------------------------------
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
   REAL: <BuiltinTypeSymbol(name='REAL')>
      x: <VarSymbol(name='x', type='REAL')>
      y: <VarSymbol(name='y', type='REAL')>
  Alpha: <ProcedureSymbol(name=Alpha, parameters=[<VarSymbol(name='a', type='INTEGER')>])>


LEAVE scope: global

关于上面的输出有一些我认为值得一提的事情:

  1. 您可以看到,两行 Insert: INTEGERInsert: REAL 在输出中重复两次, INTEGERREAL 关键字在两个作用域(作用域符号表)中都存在: globalAlpha 。 原因是我们为每个作用域创建一个单独的作用域符号表,并且每次创建实例时,初始化内置类型符号。 稍后我们将在讨论嵌套关系时修改它以及如何在代码中表示它们。
  2. 看到行 Insert: Alpha 是如何在行 ENTER scope: Alpha 前打印的 。 这只是提示一个过程的名字被声明在一个比在过程本身中声明的变量级别低一级的级别。
  3. 您可以通过检查上面的作用域符号表的打印内容来看到它们包含哪些声明。 例如,全局作用域中有 Alpha 符号。
  4. 从全局作用域的内容中,还可以看到 Alpha 过程的过程符号还包含过程的形式参数。

在我们运行程序之后,在内存中的作用域看起来就像这样,只有两个独立的作用域符号表:

图1

作用域树:链接作用域符号表

好了,现在每个作用域都由一个单独的作用域符号表来表示,但是我们如何表示全局作用域和作用域 Alpha 之间的嵌套关系,就像之前在嵌套关系图中所显示的那样? 换句话说,我们如何在代码中表示作用域Alpha嵌套在全局作用域内 ? 答案是把表链接在一起。

我们将通过创建它们之间的链接来将作用域符号表链接在一起。 在某种程度上,它就像一棵树(我们称之为作用域树 ),只是一个不寻常的树,因为在这棵树中,一个孩子将指向一个父母,而不是相反的方向。 我们来看看下面的作用域树 :

图1

在上面的作用域树中,您可以看到作用域 Alpha 通过指向它链接到全局作用域 。 换句话说,作用域 Alpha 指向它的封闭作用域,即全局作用域 。 这一切意味着作用域 Alpha 嵌套在全局作用域内 。

我们如何实现作用域链接/连接? 有两个步骤:

  1. 我们需要更新 ScopedSymbolTable 类,并添加一个变量 enclosing_scope ,该变量将包含一个指向作用域的封闭作用域的指针。这将是上图中作用域之间的链接。
  2. 我们需要更新 visit_Programvisit_ProcedureDecl 方法,以使用 ScopedSymbolTable 类的更新版本创建到作用域的封闭作用域的实际链接。

让我们开始更新 ScopedSymbolTable 类并添加 enclosing_scope 字段。 我们也更新 initstr 方法。 init 方法将被修改为接受一个新的参数, enclosing_scope ,默认值设置为None 。 str 方法将被更新以输出封闭作用域的名称。 以下是更新后的 ScopedSymbolTable 类的完整源代码:

class ScopedSymbolTable(object):
    def __init__(self, scope_name, scope_level, enclosing_scope=None):
        self._symbols = OrderedDict()
        self.scope_name = scope_name
        self.scope_level = scope_level
        self.enclosing_scope = enclosing_scope
        self._init_builtins()

    def _init_builtins(self):
        self.insert(BuiltinTypeSymbol('INTEGER'))
        self.insert(BuiltinTypeSymbol('REAL'))

    def __str__(self):
        h1 = 'SCOPE (SCOPED SYMBOL TABLE)'
        lines = ['\n', h1, '=' * len(h1)]
        for header_name, header_value in (
            ('Scope name', self.scope_name),
            ('Scope level', self.scope_level),
            ('Enclosing scope',
             self.enclosing_scope.scope_name if self.enclosing_scope else None
            )
        ):
            lines.append('%-15s: %s' % (header_name, header_value))
        h2 = 'Scope (Scoped symbol table) contents'
        lines.extend([h2, '-' * len(h2)])
        lines.extend(
            ('%7s: %r' % (key, value))
            for key, value in self._symbols.items()
        )
        lines.append('\n')
        s = '\n'.join(lines)
        return s

    __repr__ = __str__

    def insert(self, symbol):
        print('Insert: %s' % symbol.name)
        self._symbols[symbol.name] = symbol

    def lookup(self, name):
        print('Lookup: %s' % name)
        symbol = self._symbols.get(name)
        # 'symbol' is either an instance of the Symbol class or None
        return symbol

现在让我们把注意力转移到 visit_Program 方法:

def visit_Program(self, node):
    print('ENTER scope: global')
    global_scope = ScopedSymbolTable(
        scope_name='global',
        scope_level=1,
        enclosing_scope=self.current_scope, # None
    )
    self.current_scope = global_scope

    # visit subtree
    self.visit(node.block)

    print(global_scope)

    self.current_scope = self.current_scope.enclosing_scope
    print('LEAVE scope: global')

这里有一些值得一提和重复的事情:

  1. 创建作用域时,我们显式传递 self.current_scope 作为 enclosing_scope 参数
  2. 我们将新创建的全局作用域赋值给变量 self.current_scope
  3. 在离开 Program 节点之前,我们将变量 self.current_scope 恢复到之前的值。 在完成处理节点之后,恢复 current_scope 的值很重要,否则当我们的程序中有两个以上的作用域时,作用域树的构造将被破坏。 我们马上就会明白为什么。

最后,让我们更新 visit_ProcedureDecl 方法:

def visit_ProcedureDecl(self, node):
    proc_name = node.proc_name
    proc_symbol = ProcedureSymbol(proc_name)
    self.current_scope.insert(proc_symbol)

    print('ENTER scope: %s' %  proc_name)
    # Scope for parameters and local variables
    procedure_scope = ScopedSymbolTable(
        scope_name=proc_name,
        scope_level=self.current_scope.scope_level + 1,
        enclosing_scope=self.current_scope
    )
    self.current_scope = procedure_scope

    # Insert parameters into the procedure scope
    for param in node.params:
        param_type = self.current_scope.lookup(param.type_node.value)
        param_name = param.var_node.value
        var_symbol = VarSymbol(param_name, param_type)
        self.current_scope.insert(var_symbol)
        proc_symbol.params.append(var_symbol)

    self.visit(node.block_node)

    print(procedure_scope)

    self.current_scope = self.current_scope.enclosing_scope
    print('LEAVE scope: %s' %  proc_name)

同样,与scope02.py中的版本相比,主要的变化是:

  1. 创建作用域时,我们显式传递 self.current_scope 作为 enclosing_scope 参数。
  2. 我们不再对过程声明的作用域级别进行硬编码,因为我们可以根据过程的封闭作用域的作用域级别自动计算出来级别:封闭作用域的级别加1。
  3. 在离开 ProcedureDecl 节点之前,我们将 self.current_scope 的值恢复为之前的值(对于我们的示例程序,以前的值将是全局作用域 )。

好的,让我们来看看在上面的变化中,作用域符号表的内容是什么样的。 您可以在scope03a.py中找到所有更改。 我们的示例程序是:

program Main;
   var x, y: real;

   procedure Alpha(a : integer);
      var y : integer;
   begin

   end;

begin { Main }

end.  { Main }

在命令行运行scope03a.py并检查输出:

$ python scope03a.py
ENTER scope: global
Insert: INTEGER
Insert: REAL
Lookup: REAL
Insert: x
Lookup: REAL
Insert: y
Insert: Alpha
ENTER scope: Alpha
Insert: INTEGER
Insert: REAL
Lookup: INTEGER
Insert: a
Lookup: INTEGER
Insert: y


SCOPE (SCOPED SYMBOL TABLE)
===========================
Scope name     : Alpha
Scope level    : 2
Enclosing scope: global
Scope (Scoped symbol table) contents
------------------------------------
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
   REAL: <BuiltinTypeSymbol(name='REAL')>
      a: <VarSymbol(name='a', type='INTEGER')>
      y: <VarSymbol(name='y', type='INTEGER')>


LEAVE scope: Alpha


SCOPE (SCOPED SYMBOL TABLE)
===========================
Scope name     : global
Scope level    : 1
Enclosing scope: None
Scope (Scoped symbol table) contents
------------------------------------
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
   REAL: <BuiltinTypeSymbol(name='REAL')>
      x: <VarSymbol(name='x', type='REAL')>
      y: <VarSymbol(name='y', type='REAL')>
  Alpha: <ProcedureSymbol(name=Alpha, parameters=[<VarSymbol(name='a', type='INTEGER')>])>


LEAVE scope: global

您可以在上面的输出中看到, 全局作用域没有封闭作用域,而且 Alpha 的封闭作用域是全局作用域 ,这是我们所期望的,因为 Alpha 作用域嵌套在全局作用域内 。

现在,按照承诺,让我们考虑为什么设置和恢复 self.current_scope 变量的值很重要。 让我们来看看下面的程序,我们在全局作用域内有两个过程声明:

program Main;
   var x, y : real;

   procedure AlphaA(a : integer);
      var y : integer;
   begin { AlphaA }

   end;  { AlphaA }

   procedure AlphaB(a : integer);
      var b : integer;
   begin { AlphaB }

   end;  { AlphaB }

begin { Main }

end.  { Main }

示例程序的嵌套关系图如下所示:

图1

程序的AST (我只留下与这个例子相关的节点)是这样的:

图1

如果在离开 ProgramProcedureDecl 节点时不恢复当前作用域,将会发生什么情况? 让我们来看看。

我们的语义分析器遍历树的方式是深度优先,从左到右的,所以它将首先遍历 AlphaAProcedureDecl 节点,然后访问 AlphaBProcedureDecl 节点。这里的问题是,如果在离开 AlphaA 前不恢复 self.current_scope 值, self.current_scope 会留下指向 AlphaA ,而不是全局作用域,因此,语义分析程序将在3级创建作用域 AlphaB ,就好像是嵌套在作用域 AlphaA 内,这当然是,不正确的。

要看到当前作用域在离开 Program 和/或 ProcedureDecl 节点之前没恢复值的不正常行为,,下载并在命令行运行scope03b.py

$ python scope03b.py
ENTER scope: global
Insert: INTEGER
Insert: REAL
Lookup: REAL
Insert: x
Lookup: REAL
Insert: y
Insert: AlphaA
ENTER scope: AlphaA
Insert: INTEGER
Insert: REAL
Lookup: INTEGER
Insert: a
Lookup: INTEGER
Insert: y


SCOPE (SCOPED SYMBOL TABLE)
===========================
Scope name     : AlphaA
Scope level    : 2
Enclosing scope: global
Scope (Scoped symbol table) contents
------------------------------------
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
   REAL: <BuiltinTypeSymbol(name='REAL')>
      a: <VarSymbol(name='a', type='INTEGER')>
      y: <VarSymbol(name='y', type='INTEGER')>


LEAVE scope: AlphaA
Insert: AlphaB
ENTER scope: AlphaB
Insert: INTEGER
Insert: REAL
Lookup: INTEGER
Insert: a
Lookup: INTEGER
Insert: b


SCOPE (SCOPED SYMBOL TABLE)
===========================
Scope name     : AlphaB
Scope level    : 3
Enclosing scope: AlphaA
Scope (Scoped symbol table) contents
------------------------------------
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
   REAL: <BuiltinTypeSymbol(name='REAL')>
      a: <VarSymbol(name='a', type='INTEGER')>
      b: <VarSymbol(name='b', type='INTEGER')>


LEAVE scope: AlphaB


SCOPE (SCOPED SYMBOL TABLE)
===========================
Scope name     : global
Scope level    : 1
Enclosing scope: None
Scope (Scoped symbol table) contents
------------------------------------
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
   REAL: <BuiltinTypeSymbol(name='REAL')>
      x: <VarSymbol(name='x', type='REAL')>
      y: <VarSymbol(name='y', type='REAL')>
 AlphaA: <ProcedureSymbol(name=AlphaA, parameters=[<VarSymbol(name='a', type='INTEGER')>])>


LEAVE scope: global

正如你所看到的,在语义分析器中如果有两个以上作用域的存在,作用域树构建是完全损坏的:

  1. 与在嵌套关系图所示的两个作用域的级别相反,我们有三个级别
  2. 在全局作用域内的内容不包含 AlphaB 在里面,只有 AlphaA

要正确地构建一个作用域树,我们需要遵循一个非常简单的过程:

  1. 当 ENTER 进一个 ProgramProcedureDecl 节点,创建一个新的作用域,并将其分配给 self.current_scope
  2. 当即将LEAVE ProgramProcedureDecl 节点,恢复 self.current_scope 的值。

你可以认为 self.current_scope 是栈指针,而作用域树是栈的集合:

  1. 当你访问 ProgramProcedureDecl 节点,将一个新的作用域入栈并调整栈指针 self.current_scope 指向栈的顶部,就是最近进栈的作用域。
  2. 当你要离开的节点,你从堆栈中弹出作用域,你还可以调整栈指针指向之前的作用域,这是现在的栈顶。

要查看多个作用域下的正确行为,在命令行上下载和运行scope03c.py。研究输出,确保你明白是怎么回事:

$ python scope03c.py
ENTER scope: global
Insert: INTEGER
Insert: REAL
Lookup: REAL
Insert: x
Lookup: REAL
Insert: y
Insert: AlphaA
ENTER scope: AlphaA
Insert: INTEGER
Insert: REAL
Lookup: INTEGER
Insert: a
Lookup: INTEGER
Insert: y


SCOPE (SCOPED SYMBOL TABLE)
===========================
Scope name     : AlphaA
Scope level    : 2
Enclosing scope: global
Scope (Scoped symbol table) contents
------------------------------------
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
   REAL: <BuiltinTypeSymbol(name='REAL')>
      a: <VarSymbol(name='a', type='INTEGER')>
      y: <VarSymbol(name='y', type='INTEGER')>


LEAVE scope: AlphaA
Insert: AlphaB
ENTER scope: AlphaB
Insert: INTEGER
Insert: REAL
Lookup: INTEGER
Insert: a
Lookup: INTEGER
Insert: b


SCOPE (SCOPED SYMBOL TABLE)
===========================
Scope name     : AlphaB
Scope level    : 2
Enclosing scope: global
Scope (Scoped symbol table) contents
------------------------------------
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
   REAL: <BuiltinTypeSymbol(name='REAL')>
      a: <VarSymbol(name='a', type='INTEGER')>
      b: <VarSymbol(name='b', type='INTEGER')>


LEAVE scope: AlphaB


SCOPE (SCOPED SYMBOL TABLE)
===========================
Scope name     : global
Scope level    : 1
Enclosing scope: None
Scope (Scoped symbol table) contents
------------------------------------
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
   REAL: <BuiltinTypeSymbol(name='REAL')>
      x: <VarSymbol(name='x', type='REAL')>
      y: <VarSymbol(name='y', type='REAL')>
 AlphaA: <ProcedureSymbol(name=AlphaA, parameters=[<VarSymbol(name='a', type='INTEGER')>])>
 AlphaB: <ProcedureSymbol(name=AlphaB, parameters=[<VarSymbol(name='a', type='INTEGER')>])>


LEAVE scope: global

在运行scope03c.py并正确构建作用域树后,这是作用域符号表的样子:

图1

同样,正如上面提到的,你可以认为上面的作用域树是作用域栈的集合。

现在,让我们继续谈论下存在嵌套作用域时名称解析如何工作。

嵌套作用域和名称解析

我们之前的重点是变量和过程的声明。让我们把变量引用加进来。

下面是变量引用的一个范例程序:

program Main;
   var x, y: real;

   procedure Alpha(a : integer);
      var y : integer;
   begin
      x := a + x + y;
   end;

begin { Main }

end.  { Main }

或视觉上展示一些附加信息:

图1

图1

让我们把注意力转向赋值语句 x := a + x + y; 这是下标版:

图1

我们看到,x 解析为级别为1级的声明,a 解析为级别2级的声明, y 也解析为在2级的声明。解析是怎样工作的?让我们看看。

词法(静态)作用域语言例如Pascal语言当涉及到名称解析时遵循 最紧密嵌套作用域 规则。这意味着,在每一个作用域,名字引用到和它词汇最接近的声明。对于赋值语句,让我们查看每变量引用并了解规则在实践中是如何工作的:

  1. 因为语义分析先访问赋值语句的右侧,我们从算术表达式 a + x + y 中变量引用 a 开始。我们开始在词法最接近的作用域搜索 a 声明,就是 Alpha 作用域。 Alpha 作用域包含 Alpha 过程中的变量声明,包括过程的形式参数。我们在 Alpha 作用域找到 a 的声明:它是 Alpha 过程的形式参数 a ——类型 integer 的变量符号。我们通常在解析名称时用眼睛扫描源代码进行搜索(记住,a2 不是变量的名称,2是这里的下标,变量名是 a ):

    图1

  2. 现在看下算术表达式 a + x + y. 中变量引用 x 。同样,我们首先在词法最接近作用域寻找 x 的声明。词法最接近的作用域是第2级的 Alpha 作用域。作用域包含了 Alpha 过程中的变量声明,包括过程的形式参数。我们在此作用域级别(在 Alpha 作用域)没有找到 x ,所以从链接上溯到全局作用域,并继续搜索。我们的搜索成功,因为全局作用域内有名称为 x 的变量符号:

    图1

  3. 现在,让我们来看看算术表达式 a + x + y. 中变量引用 y 。我们在词法最接近作用域 Alpha 作用域上发现它的声明。在 Alpha 作用域变量 y 具有类型 integer (如果在 Alpha 作用域内没有声明 y ,我们将扫描文本,并在外层/全局作用域找到 y 而在这种情况它是 real 类型):

    图1

  4. 最后,赋值语句 a + x + y. 的左侧变量 x ,它和右侧算术表达式变量引用 x 解析为相同的声明:

    图1

我们如何实现在当前作用域内查找,然后在封闭作用域内查找的行为,依此类推,直到我们要么找到我们要找的符号或者已经达到作用域树的顶部并且没有作用域剩下?我们只是需要扩展 ScopedSymbolTable 类的 lookup 方法在作用域树链上继续搜索:

def lookup(self, name):
    print('Lookup: %s. (Scope name: %s)' % (name, self.scope_name))
    # 'symbol' is either an instance of the Symbol class or None
    symbol = self._symbols.get(name)

    if symbol is not None:
        return symbol

    # recursively go up the chain and lookup the name
    if self.enclosing_scope is not None:
        return self.enclosing_scope.lookup(name)

更新后 lookup 方法的工作原理:

  1. 在当前作用域内搜索符号的名字。如果找到符号,返回它。
  2. 如果没有找到符号,递归遍历树并在作用域链上向上搜索符号。你不必做递归查询,可以把它改写成一个迭代的形式; 重要的部分是沿着嵌套作用域的链接到它的封闭作用域,在那搜索符号或上溯树直到符号被发现或没有遗留的作用域,因为你已经到达了作用域树的顶部。
  3. lookup 方法也在括号中打印了作用域的名称,当不能在当前作用域内找到符号时,在查找发生时能更清晰地表明在链条中上溯搜索符号的行为。

让我们看看已经修改了的 lookup 在作用域树搜索符号时,针对现在的示例程序语义分析的输出。下载scope04a.py并命令行上运行它:

$ python scope04a.py
ENTER scope: global
Insert: INTEGER
Insert: REAL
Lookup: REAL. (Scope name: global)
Insert: x
Lookup: REAL. (Scope name: global)
Insert: y
Insert: Alpha
ENTER scope: Alpha
Lookup: INTEGER. (Scope name: Alpha)
Lookup: INTEGER. (Scope name: global)
Insert: a
Lookup: INTEGER. (Scope name: Alpha)
Lookup: INTEGER. (Scope name: global)
Insert: y
Lookup: a. (Scope name: Alpha)
Lookup: x. (Scope name: Alpha)
Lookup: x. (Scope name: global)
Lookup: y. (Scope name: Alpha)
Lookup: x. (Scope name: Alpha)
Lookup: x. (Scope name: global)


SCOPE (SCOPED SYMBOL TABLE)
===========================
Scope name     : Alpha
Scope level    : 2
Enclosing scope: global
Scope (Scoped symbol table) contents
------------------------------------
      a: <VarSymbol(name='a', type='INTEGER')>
      y: <VarSymbol(name='y', type='INTEGER')>


LEAVE scope: Alpha


SCOPE (SCOPED SYMBOL TABLE)
===========================
Scope name     : global
Scope level    : 1
Enclosing scope: None
Scope (Scoped symbol table) contents
------------------------------------
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
   REAL: <BuiltinTypeSymbol(name='REAL')>
      x: <VarSymbol(name='x', type='REAL')>
      y: <VarSymbol(name='y', type='REAL')>
  Alpha: <ProcedureSymbol(name=Alpha, parameters=[<VarSymbol(name='a', type='INTEGER')>])>


LEAVE scope: global

检查上面的输出,并注意 ENTERLookup 消息。这里值得一提的是:

  1. 注意语义分析如何在插入变量符号 a 前查找 INTEGER 内置型符号。它首先在当前作用域 Alpha 搜索 INTEGER ,,没有找到,然后上溯树一路到全局作用域内并发现了符号有:

    ENTER scope: Alpha
    Lookup: INTEGER. (Scope name: Alpha)
    Lookup: INTEGER. (Scope name: global)
    Insert: a
    
  2. 还要注意分析器如何解析赋值语句 x := a + x + y 的变量引用:

    Lookup: a. (Scope name: Alpha)
    Lookup: x. (Scope name: Alpha)
    Lookup: x. (Scope name: global)
    Lookup: y. (Scope name: Alpha)
    Lookup: x. (Scope name: Alpha)
    Lookup: x. (Scope name: global)
    

    分析器在当前作用域内开始其搜索,然后上溯树一路到全局作用域。

让我们看看当Pascal程序有一个变量引用不能解析到变量声明时会发生什么,如下示例程序:

program Main;
   var x, y: real;

   procedure Alpha(a : integer);
      var y : integer;
   begin
      x := b + x + y; { ERROR here! }
   end;

begin { Main }

end.  { Main }

下载scope04b.py并在命令行上运行它:

$ python scope04b.py
ENTER scope: global
Insert: INTEGER
Insert: REAL
Lookup: REAL. (Scope name: global)
Insert: x
Lookup: REAL. (Scope name: global)
Insert: y
Insert: Alpha
ENTER scope: Alpha
Lookup: INTEGER. (Scope name: Alpha)
Lookup: INTEGER. (Scope name: global)
Insert: a
Lookup: INTEGER. (Scope name: Alpha)
Lookup: INTEGER. (Scope name: global)
Insert: y
Lookup: b. (Scope name: Alpha)
Lookup: b. (Scope name: global)
Error: Symbol(identifier) not found 'b'

正如你所看到的,分析器试图解析变量引用 b ,并首先在 Alpha 作用域搜索它,然后再在全局作用域,并且,不能够找到名称为 b 的符号,把抛出了语义错误。

好的,现在我们知道当程序有嵌套作用域时如何写一个语义分析器来分析程序的语义错误。

源到源编译器

现在,到完全不同的东西。让我们写一个 源到源编译器 !为什么要这么做呢?我们不是在谈论解释器和嵌套作用域吗?是的,但让我解释一下为什么认为现在学习如何编写源到源编译器是个好主意。

首先,让我们来谈谈定义。什么是 源到源编译器 ?对于本文的目的,让我们定义 源到源编译器 为翻译某些语言程序到相同(或几乎相同)的语言编写的程序的编译器。

因此,如果你写一个翻译器把Pascal程序作为输入并输出一个可能被修改,或增强的Pascal程序,在这种情况下,翻译器被称为源到源编译器。

一个好的源到源编译器例子作为我们研究的范例将是这样一个编译器:它采用Pascal程序作为输入并输出一个类Pascal的程序,其中的每个名称下标与其对应的作用域级别,并且除那之外,每个变量引用也有一个类型指示符。 因此,我们希望有一个源到源编译器将采用以下Pascal程序作为输入:

program Main;
   var x, y: real;

   procedure Alpha(a : integer);
      var y : integer;
   begin
      x := a + x + y;
   end;

begin { Main }

end.  { Main }

并把它变成下面的类Pascal程序:

program Main0;
   var x1 : REAL;
   var y1 : REAL;
   procedure Alpha1(a2 : INTEGER);
      var y2 : INTEGER;

   begin
      <x1:REAL> := <a2:INTEGER> + <x1:REAL> + <y2:INTEGER>;
   end; {END OF Alpha}

begin

end. {END OF Main}

下面是我们的源到源编译器应该对输入的Pascal程序进行的修改列表:

  1. 每个声明应打印在单独的行,所以,如果我们在输入的Pascal 程序中有多个声明,编译后的输出应该每个声明有单独的一行。我们可以在上面看到,例如,行 var x, y : real; 怎么被转换成多行的。
  2. 每个名字应该有下标对应于各自的作用域级别的数字。
  3. 每个变量引用,在除了被加下标,也应输出为下面的形式:
  4. 编译器也应该在每个块的末尾添加 {END OF … }形式的注释 ,这里的省略号将被程序名称或过程名称取代。这将帮助我们更快识别程序的文本边界。

正如你可以从上面生成的输出看到的,这个源到源编译器是一个有用的工具来了解一个名称解析是如何工作的,尤其是当程序有嵌套的作用域,因为由编译器产生的输出,使我们能够快速查看特定的变量引用解析为哪个声明和在哪个作用域内。这在了解符号,嵌套作用域和名称解析的时候是很好的帮助。

怎样才能实现一个这样的源到源编译器?我们实际上已经涵盖了所有必要的部分。现在我们需要做的是稍微扩展下语义分析器,以产生增强的输出。你可以在这里看到编译器的全部源代码。它基本上是一个语义分析器的原型,修改后生成并返回某些AST节点的字符串。

下载 src2srccompiler.py ,研究下,并通过传递不同的Pascal程序作为输入自己实验下。

对于以下程序,例如:

program Main;
   var x, y : real;
   var z : integer;

   procedure AlphaA(a : integer);
      var y : integer;
   begin { AlphaA }
      x := a + x + y;
   end;  { AlphaA }

   procedure AlphaB(a : integer);
      var b : integer;
   begin { AlphaB }
   end;  { AlphaB }

begin { Main }
end.  { Main }

编译器生成的输出如下:

$ python src2srccompiler.py nestedscopes03.pas
program Main0;
   var x1 : REAL;
   var y1 : REAL;
   var z1 : INTEGER;
   procedure AlphaA1(a2 : INTEGER);
      var y2 : INTEGER;

   begin
      <x1:REAL> := <a2:INTEGER> + <x1:REAL> + <y2:INTEGER>;
   end; {END OF AlphaA}
   procedure AlphaB1(a2 : INTEGER);
      var b2 : INTEGER;

   begin

   end; {END OF AlphaB}

begin

end. {END OF Main}

恭喜,现在你知道如何编写一个基本的源到源编译器啦!

用它来进一步理解嵌套作用域,名称解析,和当你有AST和关于程序的以符号表的形式一些额外信息,你可以做的任何事。

现在,我们有对程序做下标的非常有用的工具,让我们来看看一个关于嵌套作用域的更大的例子,你可以在nestedscopes04.pas找到:

program Main;
   var b, x, y : real;
   var z : integer;

   procedure AlphaA(a : integer);
      var b : integer;

      procedure Beta(c : integer);
         var y : integer;

         procedure Gamma(c : integer);
            var x : integer;
         begin { Gamma }
            x := a + b + c + x + y + z;
         end;  { Gamma }

      begin { Beta }

      end;  { Beta }

   begin { AlphaA }

   end;  { AlphaA }

   procedure AlphaB(a : integer);
      var c : real;
   begin { AlphaB }
      c := a + b;
   end;  { AlphaB }

begin { Main }
end.  { Main }

下面你可以看到声明的作用域,嵌套关系图,和作用域信息表:

图1

图1

让我们来运行我们的源到源编译器并检查输出。下标应和上图中作用域信息表格的内容匹配:

$ python src2srccompiler.py nestedscopes04.pas
program Main0;
   var b1 : REAL;
   var x1 : REAL;
   var y1 : REAL;
   var z1 : INTEGER;
   procedure AlphaA1(a2 : INTEGER);
      var b2 : INTEGER;
      procedure Beta2(c3 : INTEGER);
         var y3 : INTEGER;
         procedure Gamma3(c4 : INTEGER);
            var x4 : INTEGER;

         begin
            <x4:INTEGER> := <a2:INTEGER> + <b2:INTEGER> + <c4:INTEGER> + <x4:INTEGER> + <y3:INTEGER> + <z1:INTEGER>;
         end; {END OF Gamma}

      begin

      end; {END OF Beta}

   begin

   end; {END OF AlphaA}
   procedure AlphaB1(a2 : INTEGER);
      var c2 : REAL;

   begin
      <c2:REAL> := <a2:INTEGER> + <b1:REAL>;
   end; {END OF AlphaB}

begin

end. {END OF Main}

花一些时间来研究这两个图片和源到源编译器的输出。请确保你理解以下几点:

  • 垂直线条画的路线显示的声明的作用域。
  • 在一个作用域的洞表示变量在嵌套作用域重新声明。
  • AlphaAAlphaB 是在全局作用域内声明。
  • AlphaAAlphaB 的声明引入新的作用域。
  • 作用域如何相互嵌套,以及嵌套的关系。
  • 为什么不同的名称,包括在赋值语句中的变量引用的下标会是这样的。换句话说,名称解析和链式作用域符号表上的特殊 lookup 方法是的工作如何的。

同时运行下面的程序

$ python scope05.py nestedscopes04.pas

检查链式作用域符号表的内容,并与你的在上图中看到的作用域信息表比较。不要忘了genastdot.py,你可以用它来生成AST的视觉图并看程序怎么相互嵌套在树中。

在结束我们今天的嵌套作用域的讨论之前,回顾一下,我们删除了检查源程序重复的标识符的语义检查。让我们把它放回去。为了在存在嵌套作用域和新的 lookup 方法行为的情况下检查正常工作 ,需要做些更改。首先,我们需要更新的 lookup 方法,并添加一个额外的参数,它将能够限制搜索只在当前作用域:

def lookup(self, name, current_scope_only=False):
    print('Lookup: %s. (Scope name: %s)' % (name, self.scope_name))
    # 'symbol' is either an instance of the Symbol class or None
    symbol = self._symbols.get(name)

    if symbol is not None:
        return symbol

    if current_scope_only:
        return None

    # recursively go up the chain and lookup the name
    if self.enclosing_scope is not None:
        return self.enclosing_scope.lookup(name)

第二,我们需要修改 visit_VarDecl 方法并增加检查使用 lookup 方法的新参数 current_scope_only

def visit_VarDecl(self, node):
    type_name = node.type_node.value
    type_symbol = self.current_scope.lookup(type_name)

    # We have all the information we need to create a variable symbol.
    # Create the symbol and insert it into the symbol table.
    var_name = node.var_node.value
    var_symbol = VarSymbol(var_name, type_symbol)

    # Signal an error if the table alrady has a symbol
    # with the same name
    if self.current_scope.lookup(var_name, current_scope_only=True):
        raise Exception(
            "Error: Duplicate identifier '%s' found" % var_name
        )

    self.current_scope.insert(var_symbol)

如果我们对重复标识符不限制在当前作用域搜索,查找可能在外部作用域找到具有相同名称的变量符号作为一个结果,这将抛出一个错误,而在现实中,没有语义错误。

以下是运行scope05.py并将不具有重复标识错误的程序作为参数输入的输出。你可以看到下面的输出中有更多的行,这是因为重复标识符检查中在插入一个新的符号之前查找是否有重复的名字:

$ python scope05.py nestedscopes02.pas
ENTER scope: global
Insert: INTEGER
Insert: REAL
Lookup: REAL. (Scope name: global)
Lookup: x. (Scope name: global)
Insert: x
Lookup: REAL. (Scope name: global)
Lookup: y. (Scope name: global)
Insert: y
Insert: Alpha
ENTER scope: Alpha
Lookup: INTEGER. (Scope name: Alpha)
Lookup: INTEGER. (Scope name: global)
Insert: a
Lookup: INTEGER. (Scope name: Alpha)
Lookup: INTEGER. (Scope name: global)
Lookup: y. (Scope name: Alpha)
Insert: y
Lookup: a. (Scope name: Alpha)
Lookup: x. (Scope name: Alpha)
Lookup: x. (Scope name: global)
Lookup: y. (Scope name: Alpha)
Lookup: x. (Scope name: Alpha)
Lookup: x. (Scope name: global)


SCOPE (SCOPED SYMBOL TABLE)
===========================
Scope name     : Alpha
Scope level    : 2
Enclosing scope: global
Scope (Scoped symbol table) contents
------------------------------------
      a: <VarSymbol(name='a', type='INTEGER')>
      y: <VarSymbol(name='y', type='INTEGER')>


LEAVE scope: Alpha


SCOPE (SCOPED SYMBOL TABLE)
===========================
Scope name     : global
Scope level    : 1
Enclosing scope: None
Scope (Scoped symbol table) contents
------------------------------------
INTEGER: <BuiltinTypeSymbol(name='INTEGER')>
   REAL: <BuiltinTypeSymbol(name='REAL')>
      x: <VarSymbol(name='x', type='REAL')>
      y: <VarSymbol(name='y', type='REAL')>
  Alpha: <ProcedureSymbol(name=Alpha, parameters=[<VarSymbol(name='a', type='INTEGER')>])>


LEAVE scope: global

现在,运行scope05.py,看看它是如何捕捉一个重复的标识符语义错误。

例如,下面的错误程序Alpha 作用域有重复的声明 a

program Main;
   var x, y: real;

   procedure Alpha(a : integer);
      var y : integer;
      var a : real;  { ERROR here! }
   begin
      x := a + x + y;
   end;

begin { Main }

end.  { Main }

程序产生以下输出:

$ python scope05.py dupiderror.pas
ENTER scope: global
Insert: INTEGER
Insert: REAL
Lookup: REAL. (Scope name: global)
Lookup: x. (Scope name: global)
Insert: x
Lookup: REAL. (Scope name: global)
Lookup: y. (Scope name: global)
Insert: y
Insert: Alpha
ENTER scope: Alpha
Lookup: INTEGER. (Scope name: Alpha)
Lookup: INTEGER. (Scope name: global)
Insert: a
Lookup: INTEGER. (Scope name: Alpha)
Lookup: INTEGER. (Scope name: global)
Lookup: y. (Scope name: Alpha)
Insert: y
Lookup: REAL. (Scope name: Alpha)
Lookup: REAL. (Scope name: global)
Lookup: a. (Scope name: Alpha)
Error: Duplicate identifier 'a' found

它预期中的捕获了错误。

在这个积极乐观的地方,让我们结束今天的作用域、作用域符号表和嵌套作用域的讨论。

总结

我们已经讨论了很多。让我们快速回顾一下在这篇文章中学习到的知识:

  • 我们学习了作用域,为什么它很有用,以及如何在代码中实现它。
  • 我们学习了嵌套作用域和链式作用域符号表如何被用来实现嵌套作用域。
  • 我们学会了如何编写一个语义分析器:遍历AST,建立作用域符号表,把它们链在一起,并执行各种语义检查。
  • 我们学习了名称解析和语义分析如何使用它们的链式作用域符号表(作用域)解析名称到声明, 以及 lookup 方法如何递归的在作用域树上溯链接以找到对应于特定名称的声明。
  • 我们学习到在语义分析器遍历AST时构建作用域树,当 ENTER 某些AST节点时,在作用域符号表栈顶入栈一个新的作用域,当 LEAVing 节点时,在从栈顶中取出作用域,使得作用域树就像作用域符号表栈的集合。
  • 我们学习了如何编写一个源到源编译器,它对学习嵌套作用域、作用域级别和名称解析是一个非常有用的工具。

练习

练习时间,哦耶!

图1

  1. 你已经看到了整个文章图片中程序声明 Main 名称的下标为零。我还提到,程序的名字是不在全局作用域内的,它是在具有零级别的其他一些外部作用域中。扩展spi.py并创建在0级别的新作用域 builtins ,,并移动内置类型 INTEGER 和 REAL 到该作用域。为了好玩和实践,还可以更新代码把程序名字放入该作用域内。
  2. 对于在nestedscopes04.pas中源程序做以下几点:
    1. 在一张纸上写下源Pascal程序
    2. 标在程序中对每个名称做下表表明名称解析到的声明的作用域级别。
    3. 为每个名称声明(变量和过程)画垂直线,以在视觉上显示出其作用域。不要在绘画时忘了作用域空洞和他们的意思。
    4. 为程序写一个源到源编译器而不看这篇文章中的示例源到源编译器。
    5. 使用原始src2srccompiler.py程序来验证从你的编译器的输出,你是否在练习(2.2)正确对名称做下标。
  3. 修改源到源编译器为内置类型INTEGER和REAL添加下标
  4. 取消以下块在spi.py中的注释

    # interpreter = Interpreter(tree)
    # result = interpreter.interpret()
    # print('')
    # print('Run-time GLOBAL_MEMORY contents:')
    # for k, v in sorted(interpreter.GLOBAL_MEMORY.items()):
    #     print('%s = %s' % (k, v))
    

    part10.pas文件作为输入运行解释器:

    $ python spi.py part10.pas
    

    确认问题并将缺失的方法添加到语义分析器中。

以上就是今天所有内容。在接下来的文章中,我们将学习运行时、调用堆栈、执行过程调用,并编写我们的递归阶乘函数的第一个版本。敬请关注,再见!

如果你有兴趣,这里我准备文章时引用最多的书(链接)的列表:

  1. Language Implementation Patterns: Create Your Own Domain-Specific and General Programming Languages (Pragmatic Programmers)
  2. Engineering a Compiler, Second Edition
  3. Programming Language Pragmatics, Fourth Edition
  4. Compilers: Principles, Techniques, and Tools (2nd Edition)
  5. Writing Compilers and Interpreters: A Software Engineering Approach