解释器:控制流与函数

4.1 条件判断语句(If-Then-Else)

为实现条件判断语句(if-then-else)的控制流,添加了一个新命令:

(if condition yes)
(if condition yes no)

if 命令与计算器章节中的 (? yes no) 运算符类似。不同之处在于,“else” 部分是可选的。因此,代码需要进行修改以处理这两种情况。

def pl_eval(env, node):
   ...
    # 条件判断
    if len(node) in (34and node[0in ('?''if'):
        _, cond, yes, *no = node
        no = no[0if no else ['val'None# “else” 部分是可选的
        new_env = (dict(), env) # 新作用域
        if pl_eval(new_env, cond):
            return pl_eval(new_env, yes)
        else:
            return pl_eval(new_env, no)

请注意,在计算条件之前,我创建了一个新的作用域。这允许在条件中进行变量声明,例如:(if (var aaa bbb) (then use aaa here))。这只是一种语法糖。

4.2 循环

循环命令的语法:

(loop condition body)
(break)
(continue)

处理循环命令的代码与处理 if 命令的代码有一个共同点:它会转换为 Python 中的循环,就像 if 命令会转换为 Python 中的 if 语句一样。

def pl_eval(env, node):
   ...
    # 循环
    if node[0] == 'loop' and len(node) == 3:
        _, cond, body = node
        ret = None
        while True:
            new_env = (dict(), env)
            if not pl_eval(new_env, cond):
                break
            ret = pl_eval(new_env, body)
        return ret

我们还添加了 break 和 continue 命令。它们是通过(滥用)Python 异常来实现的。如果你不喜欢这种技巧,或者不能使用异常,也可以通过 pl_eval 的返回值来显式地传递这些命令。

def pl_eval(env, node):
   ...
    # 循环
    if node[0] == 'loop' and len(node) == 3:
        _, cond, body = node
        ret = None
        while True:
            new_env = (dict(), env)
            if not pl_eval(new_env, cond):
                break
            try:
                ret = pl_eval(new_env, body)
            except LoopBreak:
                break
            except LoopContinue:
                continue
        return ret
    # break 和 continue
    if node[0] == 'break' and len(node) == 1:
        raise LoopBreak
    if node[0] == 'continue' and len(node) == 1:
        raise LoopContinue
class LoopBreak(Exception):
    def __init__(self):
        super().__init__('`break` 出现在循环外')

class LoopContinue(Exception):
    def __init__(self):
        super().__init__('`continue` 出现在循环外')

4.3 函数

语法:

(def func-name (arg-names...) body)
(call func-name args...)

函数定义的代码并没有做什么特别复杂的事情。它只是进行一些合理性检查,并将函数名放入映射中。

def pl_eval(env, node):
   ...
    # 函数定义
    if node[0] == 'def' and len(node) == 4:
        _, name, args, body = node
        # 合理性检查
        for arg_name in args:
            if not isinstance(arg_name, str):
                raise ValueError('参数名错误')
        if len(args) != len(set(args)):
            raise ValueError('参数重复')
        # 将函数添加到作用域中
        dct, _ = env
        key = (name, len(args))
        if key in dct:
            raise ValueError('函数重复')
        dct[key] = (args, body, env)
        return

请注意,我在键中添加了参数的数量,这允许一种 “重载” 形式 —— 具有相同名称但参数数量不同的函数可以共存。这也区分了函数名和变量名。

现在来处理 call 命令。函数参数被视为新的变量。只需创建一个新的作用域,将参数放入其中,然后计算函数体。

def pl_eval(env, node):
   ...
    # 函数调用
    if node[0] == 'call' and len(node) >= 2:
        _, name, *args = node
        key = (name, len(args))
        fargs, fbody, fenv = name_loopup(env, key)[key]
        # 参数
        new_env = dict()
        for arg_name, arg_val in zip(fargs, args):
            new_env[arg_name] = pl_eval(env, arg_val)
        # 调用
        try:
            return pl_eval((new_env, fenv), fbody)
        except FuncReturn as ret:
            return ret.val

特别注意:父作用域不是调用者的作用域,而是函数定义时的作用域。(在定义函数时会保存该作用域)。

(return) 命令的处理方式与 (break) 或 (continue) 类似。

def pl_eval(env, node):
   ...
    # 返回
    if node[0] == 'return' and len(node) == 1:
        raise FuncReturn(None)
    if node[0] == 'return' and len(node) == 2:
        _, val = node
        raise FuncReturn(pl_eval(env, val))
class FuncReturn(Exception):
    def __init__(self, val):
        super().__init__('`return` 出现在函数外')
        self.val = val

4.4 完成

恭喜。我们已经为一门小型编程语言构建了一个解释器。

def test_eval():
    def f(s):
        return pl_eval(None, pl_parse_prog(s))
    assert f('''
        (def fib (n)
            (if (le n 0)
                (then 0)
                (else (+ n (call fib (- n 1))))))
        (call fib 5)
    ''') == 5 + 4 + 3 + 2 + 1
    assert f('''
        (def fib (n) (do
            (var r 0)
            (loop (gt n 0) (do
                (set r (+ r n))
                (set n (- n 1))
            ))
            (return r)
        ))
        (call fib 5)
    ''') == 5 + 4 + 3 + 2 + 1

我们的解释器并不比上一章中的计算器复杂多少。变量通过额外的状态来处理,控制流被转换为 Python 的控制流 —— 基本上仍然是简单的递归。

这个解释器在执行时仍然依赖于现有的语言。那么,我们如何将我们的语言编译成机器码,并在 CPU 上原生运行呢?这就是下一个挑战。

阅读剩余
THE END