Let's Build A Simple Interpreter 学习笔记(Part 16 ~ Part 18)
Part 16
本部分主要讲解如何实现函数调用,为了支持函数调用,Lexer 和 Parser 部分要做的更新和之前的没什么本质区别,因此这里不再赘述。
Part 17
为了支持函数调用,该部分主要讲解了如何为我们的解释器构造新的存储系统。
我们的新的存储系统是一个堆栈结构:
class CallStack:
def __init__(self):
self._records = []
def push(self, ar):
self._records.append(ar)
def pop(self):
return self._records.pop()
def peek(self):
return self._records[-1]
def __str__(self):
s = '\n'.join(repr(ar) for ar in reversed(self._records))
s = f'CALL STACK\n{s}\n'
return s
def __repr__(self):
return self.__str__()
堆栈中的元素:
class ActivationRecord:
def __init__(self, name, type, nesting_level):
self.name = name
self.type = type
self.nesting_level = nesting_level
self.members = {}
def __setitem__(self, key, value):
self.members[key] = value
def __getitem__(self, key):
return self.members[key]
def get(self, key):
return self.members.get(key)
其中每个元素中的核心数据成员为一个字典。
我们要如何去用这个新的存储系统呢?
首先当我们初始化解释器时我们需要初始化该 stack,当进入到 Program 语句时我们要新建并 push 一个 ActivationRecord 以存储在当前作用域新建和赋值的变量。 当我们离开该作用域时将该 ActivationRecord pop 出。
现在该存储系统还不能支持在内层作用域中访问外层作用域中的变量。
Part 18
本部分我们将最终实现函数调用功能。
包含以下几点:
- 当调用函数时,我们需要新建一个作用域,亦即 push 一个 ActivationRecord 到 CallStack 中。
- 我们需要获知函数的实参与形参,将其成对存储到 ActivationRecord 中。
- 我们需要知道 AST 中表示该函数的子树的根节点。
关键代码:
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
# 在此处我们将实参作为 key,形参作为 value 存入当前的 ActivationRecord 中。
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) # 这里的 block_ast 存储了指向 AST 中对应该函数的子树的根节点
self.log(f'LEAVE: PROCEDURE {proc_name}')
self.log(str(self.call_stack))
self.call_stack.pop()
Over.