一个简单的计算器

2.1 简介

构建编译器或解释器的第一步是将源代码解析成树状结构。大多数解析工作都可以通过递归的方式完成,一旦你学会了如何进行解析,就可以解析大多数计算机语言。

解析是编译器中比较枯燥的部分,而且大多数实际应用的编程语言都需要花费大量精力来进行解析。幸运的是,有一种语法可以用最少的工作量来解析,那就是S表达式(sexpr)。我们将使用它来节省时间。

2.2 S表达式

一个S表达式要么是一个用括号括起来的嵌套列表,要么是一个不可分割的元素,称为 “原子”。一些例子如下:

;; 原子
a
123
;; 列表
(a)
(a b c d)
;; 嵌套列表
(a ((b) c))

它自然地表示了一棵树,而大多数解析工作就是将字符串表示转换为树状结构,所以为什么不直接使用S表达式作为语法呢?特别是当S表达式比其他选择更简单、更容易解析的时候。

除了明显的树状结构外,基于S表达式的语法通常使用前缀运算符,而不是中缀运算符。考虑以下情况:

中缀运算符:

1 + 2 * 3

前缀运算符:

(+ 1 (* 2 3))

中缀运算符由于不同的运算符优先级而变得复杂 —— 而前缀运算符则不存在这个问题。当前缀运算符在我们需要进一步解析时也更容易处理。这就是为什么我在本书中使用S表达式。如果你喜欢挑战,你可以尝试使用其他语法,比如类似C语言的带中缀运算符的语法。

我们将实现以下运算符:

二元运算符:(op a b)。

算术运算:

(+ 1 2)
(- 1 2)
(* 1 2)
(/ 1 2)

比较运算:

(lt 1 2),小于。
(gt 1 2),大于。
(eq 1 2),等于。
(ne 1 2),不等于。

布尔运算:

(and a b)
(or a b)

一元运算符:

(- a),取负。
(not a),布尔非。

条件运算:(? expr-cond expr-then expr-else)。

例如,(? (lt 1 2) "yes" "no") 的结果是 “yes”。

(print a b c)

2.3 开始编码

一个S表达式将被解析成Python列表或者Python字符串。例如,S表达式 (a b (+ 1 2)) 将被解析成:

["a", "b", ["+", ["val", 1], ["val", 2]]]

请注意,S表达式中的数字或字符串与符号a或b不同,它们被 ["val", x] 结构包裹着,这一点稍后会解释。

函数parse_expr将输入字符串和输入中的当前偏移量(idx)作为参数。它在解析过程中推进偏移量,直到遇到错误或者S表达式结束。

要解析一个S表达式,第一步是确定它是一个原子还是一个列表,这通过查看第一个非空白字符来完成。

def parse_expr(s: str, idx: int):
    idx = skip_space(s, idx)
    if s[idx] == '(':
        # 一个列表
       ...
    elif s[idx] == ')':
        raise Exception('括号错误')
    else:
        # 一个原子
       ...

如果它是一个列表,递归地解析,直到遇到右括号。

def parse_expr(s: str, idx: int):
    idx = skip_space(s, idx)
    if s[idx] == '(':
        # 一个列表
        idx += 1
        l = []
        while True:
            idx = skip_space(s, idx)
            if idx >= len(s):
                raise Exception('括号不匹配')
            if s[idx] == ')':
                idx += 1
                break

            idx, v = parse_expr(s, idx)
            l.append(v)
        return idx, l
    elif s[idx] == ')':
        raise Exception('括号错误')
    else:
        # 一个原子
        start = idx
        while idx < len(s) and (not s[idx].isspace()) and s[idx] not in '()':
            idx += 1
        if start == idx:
            raise Exception('程序为空')
        return idx, parse_atom(s[start:idx])

任何非空白字符都被视为原子的一部分。

def skip_space(s, idx):
    while idx < len(s) and s[idx].isspace():
        idx += 1
    return idx

由于我们要计算已解析的S表达式,从原子中提取诸如数字和字符串之类的值是有意义的。列表头部的 “val” 字符串用于区分已解析的值和其他原子(符号)。

# 布尔值、数字、字符串或一个符号
def parse_atom(s):
    # 待办事项:实际实现此函数
    import json
    try:
        return ['val', json.loads(s)]
    except json.JSONDecodeError:
        return s

上面的函数只是一个占位符,我们将在后面的章节中弃用它。

正如我们所看到的,解析的模式是:

  1. 1. 查看开头的内容并决定下一步操作。
  2. 2. 必要时进行递归。

即使对于像C或Java这样更复杂的语言,它们的语法也可以用这种方式进行解析。尽管这一点此时你可能还不太清楚。

最后,我们需要检查输入是否已完全处理:

def pl_parse(s):
    idx, node = parse_expr(s, 0)
    idx = skip_space(s, idx)
    if idx < len(s):
        raise ValueError('存在多余内容')
    return node

2.4 表达式求值

计算一个S表达式非常简单:

  1. 1. 查看列表中的第一个元素并确定操作是什么。
  2. 2. 递归地计算其余元素。
  3. 3. 执行操作。
def pl_eval(node):
    if len(node) == 0:
        raise ValueError('列表为空')

    # 布尔值、数字、字符串等
    if len(node) == 2 and node[0] == 'val':
        return node[1]

    # 二元运算符
    import operator
    binops = {
        '+': operator.add,
        '-': operator.sub,
        '*': operator.mul,
        '/': operator.truediv,
        'eq': operator.eq,
        'ne': operator.ne,
        'ge': operator.ge,
        'gt': operator.gt,
        'le': operator.le,
        'lt': operator.lt,
        'and': operator.and_,
        'or': operator.or_,
    }
    if len(node) == 3 and node[0in binops:
        op = binops[node[0]]
        return op(pl_eval(node[1]), pl_eval(node[2]))

    # 一元运算符
    unops = {
        '-': operator.neg,
        'not': operator.not_,
    }
    if len(node) == 2 and node[0in unops:
        op = unops[node[0]]
        return op(pl_eval(node[1]))

    # 条件运算
    if len(node) == 4 and node[0] == '?':
        _, cond, yes, no = node
        if pl_eval(cond):
            return pl_eval(yes)
        else:
            return pl_eval(no)

    # 打印
    if node[0] == 'print':
        return print(*(pl_eval(val) for val in node[1:]))

    raise ValueError('未知表达式')

它基本上是将运算符映射到Python运算符。这个表达式求值器可以用作一个(非传统的)计算器。

def test_eval():
    def f(s):
        return pl_eval(pl_parse(s))
    assert f('1') == 1
    assert f('(+ 1 3)') == 4
    assert f('(? (lt 1 3) "yes" "no")') == "yes"
    assert f('(print 1 2 3)'is None

我们已经实现了一个简单的计算器,它是下一章项目的基础 —— 一个带有变量、控制流和函数的编程语言。

阅读剩余
THE END