10 执行过程调用 | 《Let’s Build A Simple Interpreter》

本文最后更新于:2021年9月6日

本文将解读执行过程调用的代码。

创建活动记录

class ARType(Enum):
    PROGRAM   = 'PROGRAM'
    PROCEDURE = 'PROCEDURE'
def visit_ProcedureCall(self, node):
    proc_name = node.proc_name

    ar = ActivationRecord(
        name=proc_name,
        type=ARType.PROCEDURE,
        nesting_level=2,
    )

在活动记录中保存过程实参

以下是描述解释器在活动记录中保存过程参数所需采取的步骤:

  • 获取过程的形参列表
  • 获取过程的实参列表
  • 对于每个形参,获取对应的实参,以形参名称为键,实参求值后作为值保存在过程的活动记录中

过程调用的形参被保存在 ProcedureSymbol 中:

class Symbol:
    def __init__(self, name, type=None):
        self.name = name
        self.type = type


class ProcedureSymbol(Symbol):
    def __init__(self, name, formal_params=None):
        super().__init__(name)
        # a list of VarSymbol objects
        self.formal_params = [] if formal_params is None else formal_params

代码令 ProcedureCall 节点持有 ProcedureSymbol 数据:

class ProcedureCall(AST):
    def __init__(self, proc_name, actual_params, token):
        self.proc_name = proc_name
        self.actual_params = actual_params  # a list of AST nodes
        self.token = token
        # a reference to procedure declaration symbol
        self.proc_symbol = None

这样在语义分析的时候就可以获取到过程调用节点的形参:

class SemanticAnalyzer(NodeVisitor):
    def visit_ProcedureCall(self, node):
        for param_node in node.actual_params:
            self.visit(param_node)

        proc_symbol = self.current_scope.lookup(node.proc_name)
        # accessed by the interpreter when executing procedure call
        node.proc_symbol = proc_symbol

这样 interpreter 就可以将形参和实参对应起来:

class Interpreter(NodeVisitor):
    def visit_ProcedureCall(self, node):
        proc_name = node.proc_name

        ar = ActivationRecord(
            name=proc_name,
            type=ARType.PROCEDURE,
            nesting_level=2,
        )

        proc_symbol = node.proc_symbol

        formal_params = proc_symbol.formal_params
        actual_params = node.actual_params

        for formal_param_symbol, actual_param_node in zip(formal_params, actual_params):
            ar[formal_param_symbol.name] = self.visit(actual_param_node)

将活动记录推到调用栈上

class Interpreter(NodeVisitor):
    def visit_ProcedureCall(self, node):
        proc_name = node.proc_name

        ar = ActivationRecord(
            name=proc_name,
            type=ARType.PROCEDURE,
            nesting_level=2,
        )

        proc_symbol = node.proc_symbol

        formal_params = proc_symbol.formal_params
        actual_params = node.actual_params

        for param_symbol, argument_node in zip(formal_params, actual_params):
            ar[param_symbol.name] = self.visit(argument_node)

        self.call_stack.push(ar)

执行程序的主体

在解释阶段遍历 AST 树并访问 ProcedureCall AST 节点时,解释器需要访问对应 ProcedureDecl 节点的 block_node 变量。block_node 变量保存代表过程主体的 AST 子树。可以令语义分析器的 visit_ProcedureDecl 方法访问过程符号和过程主体,即指向过程主体的 AST 子树的 ProcedureDecl AST 节点的 block_node 字段。如下图所示:

相应的代码如下:

class ProcedureSymbol(Symbol):
    def __init__(self, name, formal_params=None):
        ...
        # a reference to procedure's body (AST sub-tree)
        self.block_ast = None


class SemanticAnalyzer(NodeVisitor):
    def visit_ProcedureDecl(self, node):
        proc_name = node.proc_name
        proc_symbol = ProcedureSymbol(proc_name)
        ...
        self.log(f'LEAVE scope: {proc_name}')

        # accessed by the interpreter when executing procedure call
        proc_symbol.block_ast = node.block_node

这样的话,在过程调用中执行过程主体变得像访问过程声明的 Block AST 节点一样简单,该节点可通过过程的 proc_symbol 的 block_ast 字段访问:

class Interpreter(NodeVisitor):
    def visit_ProcedureCall(self, node):
        proc_name = node.proc_name

        ar = ActivationRecord(
            name=proc_name,
            type=ARType.PROCEDURE,
            nesting_level=2,
        )

        proc_symbol = node.proc_symbol

        formal_params = proc_symbol.formal_params
        actual_params = node.actual_params

        for param_symbol, argument_node in zip(formal_params, actual_params):
            ar[param_symbol.name] = self.visit(argument_node)

        self.call_stack.push(ar)

        # evaluate procedure body
        self.visit(proc_symbol.block_ast)

弹出调用堆栈的活动记录

在我们完成对过程主体的执行以后,过程活动记录将不再需要,所以在离开 visit_ProcedureCall 方法之前将它从调用栈中弹出:

class Interpreter(NodeVisitor):
    def visit_ProcedureCall(self, node):
        proc_name = node.proc_name

        ar = ActivationRecord(
            name=proc_name,
            type=ARType.PROCEDURE,
            nesting_level=2,
        )

        proc_symbol = node.proc_symbol

        formal_params = proc_symbol.formal_params
        actual_params = node.actual_params

        for param_symbol, argument_node in zip(formal_params, actual_params):
            ar[param_symbol.name] = self.visit(argument_node)

        self.call_stack.push(ar)

        self.log(f'ENTER: PROCEDURE {proc_name}')
        self.log(str(self.call_stack))

        # evaluate procedure body
        self.visit(proc_symbol.block_ast)

        self.log(f'LEAVE: PROCEDURE {proc_name}')
        self.log(str(self.call_stack))

        self.call_stack.pop()