# 作业4:错误处理与堆
**注意:**这是一个为期1周的作业(发布后1周截止)。
在本次作业中,你将以两种方式实现列表:首先使用对(pair),然后使用数组。你将练习处理运行时错误和处理堆上的数据。
在本次作业结束时,你的解释器和编译器应支持以下语法(我们已突出显示你将添加的部分):
| (
| (
| (
| (if
| (let ((
| (do
+ | list?
+ | vector?
+ | vector-length
+ | vector
+ | vector-get
我们**不会**对本次作业的测试进行评分。
然而,当你将实现提交到Gradescope(作业`hw4`)时,`examples/`目录中的示例套件将针对参考解释器和编译器运行。如果参考实现在你的任何示例上失败,Gradescope将向你展示其输出与你提供的示例预期输出的差异(如果你提供了预期输出)。
你可以根据需要多次进行此操作。我们鼓励你在开始编写解释器和编译器之前使用此选项来开发一组良好的示例!
你可以使用`dune runtest -f`来运行`examples/`目录中的所有测试。与之前一样,测试框架支持`.lisp`/`.out`文件和`examples/examples.csv`文件。
### 手动运行编译器和解释器
除了使用`dune runtest -f`,你还可以在输入文件上手动运行解释器和编译器。
要在文件上运行解释器,请执行以下命令:
dune exec bin/interp.exe —
要在文件上运行编译器,请执行以下命令:
dune exec bin/compile.exe —
生成的`.s`文件(包含汇编代码)和`.exe`文件(可执行文件)将在`output/`目录中。然后你可以使用以下命令运行可执行文件:
./output/
你还可以告诉编译器在编译后立即运行可执行文件,只需添加`-r`,例如:
dune exec bin/compile.exe —
### 检查和调试汇编代码
如果你在`.lisp`文件上运行编译器(如上所述),你可以查看`output`目录中的`.s`文件来查看生成的汇编代码。这是了解你的编译器整体工作方式以及调试可能遇到的问题的非常有用的方法。
你也可以查看`_build/default/test/test_output/*.s`来查看你的测试用例的汇编代码,包括`examples/examples.csv`中的那些。
有关更交互式的调试汇编代码的方法,请查看课程网站上的第4节!
### 手动汇编`.s`文件
如果你的编译器生成的汇编代码无效且你不确定原因,你可以尝试手动编译一个测试文件(见上文),然后在`output`目录中的`.s`文件上手动运行`nasm`:
nasm
这可能会给你比自动化测试基础设施更有用的错误消息。
## 四、列表(5个子任务)
正如我们在课堂上看到的,列表可以使用对来实现。实际上,这就是列表在Scheme和其他类似Lisp的语言中通常的实现方式。一个列表要么是:
– 空列表,我们在课堂上用`false`表示。
– 一个对,其中第二个元素是一个列表。
Scheme中使用一个特殊的值`()`(发音为“nil”)来表示空列表,而不是使用`false`。
**任务1.1:**在解释器中添加对`()`的支持。
**任务1.2:**在编译器中添加对`()`的支持。使用`0b11111111`作为`()`的运行时表示。
提示:你需要修改运行时来正确显示nil值。
现在我们可以使用`pair`和`()`构建列表。以下是一些这样的列表:
– `(pair 1 ())`
– `(pair 1 (pair 2 ()))`
– `(pair (pair 1 2) ())`
我们现在可以定义一个原语`(list? e)`,当它的参数是一个列表时,它的值为`true`,否则为`false`。
**任务1.3(不评分):**在`examples/`目录中为`list?`原语编写测试。
**任务1.4:**在解释器中添加对`list?`的支持。
提示:你可以在解释器中使用递归辅助函数来实现`list?`。
**任务1.5:**在编译器中添加对`list?`的支持。
提示:你可以生成一个循环来执行这个原语;为此,你需要定义一个标签并反复跳回到该标签。
## 五、向量(3个子任务)
我们也可以使用数组来实现列表。在Scheme中,基于数组的列表被称为_向量_。在这个任务中,你将实现以下五个向量原语:
– `(vector n e)`,它创建一个长度为`n`的向量,其中所有元素都是`e`。
– **前提条件:**`n`的值为正整数。
– `(vector? e)`,如果`e`是向量,则返回`true`,否则返回`false`。
– **前提条件:**无。
– `(vector-length v)`,返回向量`v`的长度。
– **前提条件:**`v`是向量。
– `(vector-get v n)`,返回向量`v`在(基于0的)索引`n`处的元素。
– **前提条件:**`v`是向量且`n`的值为非负整数且小于`v`的长度。
– `(vector-set v n e)`,将向量`v`在索引`n`处的元素设置为`e`并求值为向量。
– **前提条件:**`v`是向量且`n`的值为非负整数且小于`v`的长度。
### 关于错误处理的重要说明
对于本次作业,与之前的作业不同,**你需要在编译器中实现错误处理**。
不满足其相应前提条件的表达式**必须**导致运行时错误。(这有时就是所谓的语言是“安全的”的意思。)当评估不满足其前提条件的表达式时,你的代码应该做以下事情:
– 在解释器中,你必须抛出异常。任何异常都可以;我们不会检查异常类型及其内容。
– 在编译器中,你必须跳转到`lib/runtime/runtime.c`中定义的`lisp_error`符号。查看`ensure_num`和`ensure_pair`以了解我们到目前为止实现的错误处理的一些示例。
提示:C函数假设它们的第一个参数存储在寄存器`Rdi`中,并且`dq`指令允许你将字节嵌入到指令存储的内存位置中。
### 显示向量输出
向量应该显示为方括号括起来的用空格分隔的值列表。例如,以下是一些简单的数字向量:
– `[1 2 3]`
请注意,我们的语法不支持这种语法,因此你不能使用`[1 1]`作为`(vector 2 1)`的简写,例如。
**任务2.1(不评分):**在`examples/`目录中为五个向量原语编写测试。
**任务2.2:**在解释器中添加对向量和五个向量原语的支持。在解释器中,你应该使用OCaml的内置`array`类型和`Array`模块中的函数来实现向量。
**任务2.3:**在编译器中添加对向量和五个向量原语的支持。在编译器中,你应该在堆上实现向量:长度为`n`的向量应该占用`n + 1`个8字节的单元格,其中第一个单元格应该用于存储其长度。使用三位标签`0b101`表示向量值。
提示:在`compile.ml`中,查看我们调用`compile_binary_primitive`的地方,以了解第二个参数保存在`Rax`中,第一个参数在堆栈上。查看我们调用`compile_trinary_primitive`的地方,以了解我们为接受三个参数的原语存储参数的位置。
提示:你可能需要使用一个额外的寄存器来实现一些向量原语。如果需要,你可以除了常用的`Rax`和`R8`之外使用`R9`。
提示:你需要修改运行时来正确显示向量值。为了做到这一点,将向量的堆指针转换为指向`uint64_t`的C指针,然后使用[指针算术](https://www.tutorialspoint.com/cprogramming/c_pointer_arithmetic.htm)来访问向量中的每个值可能会很有帮助。回想一下我们在课堂上如何处理对。
我们不会测试大小超过100的向量,也不会测试循环向量的显示。