下载 lab11.zip 。在该压缩包中,你将找到本实验中问题的起始文件,以及 Ok 自动评分器的副本。
在 Scheme 项目 中,你将为 Scheme 实现一个 Python 解释器。
解释 Scheme 表达式的过程的一部分是能够将一串 Scheme 代码作为我们的输入解析到我们的解释器对 Scheme 表达式的内部 Python 表示中。由于所有的 Scheme 表达式都是 Scheme 列表(因此也是链表),我们使用 Pair 类来表示所有的 Scheme 表达式,它的行为就像一个链表。 这个类在 pair.py 中定义。
当给定一个诸如 (+ 1 2) 的输入时,有两个主要步骤是我们要做的。
解释表达式的第一部分是采取输入并将其分解为每个组成部分。在我们的例子中,我们想把 ( 、 + 、 1 、 2 和 ) 中的每一个都当作一个单独的标记,然后我们可以找出如何表示。这被称为 词法分析 ,在 scheme_tokens.py 的 tokenize_lines 函数中已经为你实现。
现在我们已经把输入的内容分解成了它的组成部分,我们想把这些 Scheme tokens 变成我们的解释器对它们的内部表示。这就是所谓的 语法分析 ,在 scheme_reader.py 的 scheme_read 和 read_tail 函数中进行。
( 告诉我们正在开始一个调用表达式。+ 是运算符,因为它是调用表达式中的第一个元素。1 是我们的第一个操作数。2 是我们的第二个操作数。) 告诉我们,我们正在结束这个调用表达式。主要的想法是,在我们进行任何计算或在操作数上调用运算符之前,我们首先要认识到输入代表什么,等等。
这个实验的目标是研究解析的各个部分;虽然在这个实验和这个项目中,我们关注的是 Scheme 语言,但我们如何设置 Scheme 解释器的一般想法也可以适用于其他语言 —— 比如 Python 本身!
请看 介绍 ,了解本实验的背景。
我们在 Buffer 实例中存储准备被解析的标记。例如,一个包含输入 (+ (2 3)) 的缓冲区会有 '('、 '+'、 '('、 2、 3、 ')' 和 ')' 的标记。
在这一部分,我们将实现 Buffer 类。
Buffer 提供了一种访问跨行标记序列的方法。
它的构造函数需要一个迭代器,称为“source”,每次查询都会返回下一行标记的列表,直到它的行数用尽。
例如, source 可以被定义如下:
line1 = ['(', '+', 6, 1, ')'] # (+ 6 1)
line2 = ['(', 'quote', 'A', ')'] # (quote A)
line3 = [2, 1, 0] # 2 1 0
input_lines = [line1, line2, line3]
source = iter(input_lines)
实际上, Buffer 结合了从其源头返回的序列,然后通过其 pop_first 方法一次提供其中的项目,只有在需要时才调用 source 的更多项目序列。
此外, Buffer 提供了一个 current 实例属性,可以查看下一个要供应的项目,而不需要移过它。
重要提示: 这部分的代码应该放在
buffer.py中。
你在这部分的工作是实现 Buffer 类的 create_generator 、 __init__ 和 pop_first 方法。
注意: 对于这个问题,你可能想使用内置的函数
next及其默认参数。这里有一个例子:>>> iterator = iter([1, 2]) >>> next(iterator) # Here, there is no default arg given. 1 >>> next(iterator, 5) # Here, there is a default arg given, but not used. 2 >>> next(iterator, 5) # The iterator is exhausted, so next returns default. 5关于
next的更多信息,请随时阅读nextPython 文档。
create_generator实现 create_generator ,这是一个生成器函数,它接收 source ,是一个行的迭代器,每个行是一个包含标记的列表。
这个函数应该每次从 source 的某一行产生一个标记。如果某行没有更多的标记,那么它应该产生 EOL_TOKEN (一个代表行末标记的对象)。
如果整个 source 中没有更多的标记,它就不应该有更多的 yields 。在这种情况下,如果你在这个函数的生成器上调用 next ,就会产生一个 StopIteration ,因为不再有适用的 yield 了。
你可以在你实现的 __init__ 和 pop_first 中引用这个函数。
记住,生成器函数可以如下使用:
>>> gen = some_generator_function() >>> next(gen) # Returns the first yield from some_generator_function >>> next(gen) # Returns the next yield from some_generator_function
__init____init__ 接收输入源 source 。你应该定义以下实例属性:
create_generator 基于 source 创建的生成器,以及self.current 代表 Buffer 实例所处的生成器的当前标记。在 __init__ 中,当前 token 应该是生成器产生的第一个 token 。如果你愿意,你可以定义更多你认为合适的实例属性。
pop_first实现 pop_first ,其作用如下:
Buffer 实例的当前 token ,以便以后返回。Buffer 实例的当前 token 更新为其生成器实例的下一个 token 。None 。(提示:见本问题开头关于 next 的默认参数的说明)。使用 Ok 来测试你的代码:
python3 ok -q buffer
阅读器将把 Scheme 代码解析成 Python 值,其表示方法如下:
| 输入示例 | Scheme 表达式类型 | 我们的内部表述 |
|---|---|---|
| `scm> 1` | Numbers | Python 的内置 `int` 和 `float` 值 |
| `scm> x` | Symbols | Python 的内置 `string` 值 |
| `scm> #t` | Booleans (`#t`, `#f`) | Python 内置的 `True`、 `False` 值 |
| `scm> (+ 2 3)` | Combinations | `Pair` 类的实例,定义在 `scheme_reader.py` 中。这个例子被表示为: `Pair('+', Pair(2, Pair(3, nil)))` 。 |
| `scm> nil` | `nil` | `nil` 对象,定义在 `scheme_reader.py` 中。 |
当我们在这里提到组合时,我们指的是调用表达和特殊形式。
重要提示: 你这部分的代码应该放在
scheme_reader.py中。
重要提示: 在解锁这个问题时,如果从
Buffer实例中产生的 token 是EOL_TOKEN,它将根据EOL_TOKEN类的__repr__函数来显示。具体来说,你会得到:>>> EOL_TOKEN This is a token representing the end of a line.
你在这一部分的工作是编写解析功能,它由两个相互递归的函数组成: scheme_read 和 read_tail 。每个函数都接收一个 src 参数,它是一个 Buffer 实例。
scheme_read 从 src 中删除足够多的标记以形成一个表达式,并以正确的 内部表示法 返回该表达式。read_tail 希望读取 list 或 Pair 的其余部分,前提是该 list 或 Pair 的开放括号已经被 scheme_read 删除。它将读取表达式(并因此移除标记),直到看到匹配的结束括号 ) 。这个表达式的列表被作为一个 Pair 实例的链表返回。简而言之, scheme_read 返回缓冲区中下一个完整的表达式, read_tail 返回缓冲区中剩余的列表或 Pair 。这两个函数都会改变缓冲区,删除已经处理过的标记。
两个函数的行为都取决于当前在 src 中的第一个标记。它们应该按如下方式实现:
scheme_read:
"nil" ,返回 nil 对象。( ,则表达式是一个对或列表。对 src 的其余部分调用 read_tail 并返回其结果。' ,则缓冲区的其余部分应作为一个引号 quote 表达式来处理。你将在下一个问题中实现这一部分。read_tail:
) ,那么我们已经到达了列表或配对的末端。 从缓冲区中删除这个 token ,并返回 nil 对象。src 可能包含 + 2 3) 。要解析这个:
scheme_read 缓冲区中的下一个完整表达式。read_tail 来读取组合的其余部分,直到匹配的结束括号。Pair 实例返回,其中第一个元素是来自 (1) 的下一个完整表达式,第二个元素是来自 (2) 的其余组合。使用 Ok 来解锁和测试你的代码:
python3 ok -q scheme_read -u
python3 ok -q scheme_read
重要提示: 这一部分的代码应该放在
scheme_reader.py中。
在这个问题上,你的任务是完成 scheme_read 的实现,让这个函数现在能够处理带引号的表达式。
在 Scheme 中,像 '<expr> 这样的带引号的表达式等同于 (quote <expr>) 。这意味着我们需要将 ' 后面的表达式(你可以通过递归调用 scheme_read 得到)包装成 quote 的特殊形式,它是一个 Scheme 列表(和所有的特殊形式一样)。
在我们的表述中,一个 Pair 代表一个 Scheme 列表。因此,你应该把 ' 后面的表达式包在 Pair 中。
例如, 'bagel ,或者说 ["'", "bagel"] 在被标记后,应该表示为 Pair('quote', Pair('bagel', nil)) 。 '(1 2) 或 ["'", "(", 1, 2, ")"] 应该表示为 Pair('quote', Pair(Pair(1, Pair(2, nil)), nil)) 。
使用 Ok 来解锁和测试你的代码:
python3 ok -q quote -u
python3 ok -q quote
现在你的解析器已经完成了,你可以通过运行 读取-计算-打印 循环来测试:
python3 scheme_reader.py --repl
每次你在提示符中输入一个值,解析后的表达式的 str 和 repr 值都会被打印出来。你可以尝试以下输入方式:
read> 42
str : 42
repr: 42
read> nil
str : ()
repr: nil
read> (1 (2 3) (4 (5)))
str : (1 (2 3) (4 (5)))
repr: Pair(1, Pair(Pair(2, Pair(3, nil)), Pair(Pair(4, Pair(Pair(5, nil), nil)), nil)))
要退出解释器,你可以输入 exit 。
请确保提交这项作业:
python3 ok --submit