CS143-Lab/CS143体验报告.md
2023-03-29 19:50:49 +08:00

268 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CS143 体验报告
## 理论课程部分
> 待施工
## Written Assignment 部分
> 既然它网站上都放了,那我就姑且做一下?
>
> 还没咋做捏
## Programming Assignment 部分
### 开始之前
这门课的官方材料好像每隔几年就会换一个地方,导致找起来十分的困难(之前做的 xv6 就很棒多少年的东西都放在同一个网站上现在这个时间点2023年3月的官网应该是在 edx 上面需要注册收费之类的才能获取相关材料。不过这门课历史悠久基本上从2000年左右就有了自然是被大家复制传播到了各种地方。这个 repo 里面的 starter code 是从 github 上别人的仓库里面的分发包(`student-dist.tar.gz`)解压出来的,我对比了几个仓库的代码内容都是一样的,那么做这个版本的应该问题不大。不过和课程网站上的内容还是有一些不同的,这就没有办法了,大家也拿不到 Stanford 的内部版本。
### PA1
第一个实验是用 COOL 写一个简单的程序,程序的功能是解释一个简单的 Stack Machine 语言,会输入一些序列然后按照文档的说明进行操作。
推荐的实现方式是,定义一个 `StackCommand` 类,然后为每一条命令实现一个子类用来执行具体的操作。这里还给了一个工具类 `atoi.cl`,里面实现了 `String``int` 的转换。不需要实现错误处理,假设给出的序列都是合法的。不过本人才疏学浅,就写了几个 if 就写完了,压根没写这么复杂。
首先我们需要一个 List 作为栈,这个可以从 `examples/list.cl` 里面抄过来,然后把数据类型改成 String 就可以了。然后就是处理输入输出,定义一个输入变量,一个大循环判断是不是 `input = "x"`,里面一个大 `if-else-then` 判断输入的命令,如果是 `d` 就调用前面抄过来的 `print_list`(需要修改一下格式和类型);如果是 `e` 需要再来一个 `if-else-then` 来处理求值操作;其他的就直接塞进栈里面,这里不做错误处理,假设都是合法的。具体到 `e` 命令里面,`pop` 并判断栈顶,如果是 `+``pop` 两次,调用 `a2i` 做运算然后再 `i2a` 变回字符串 `push` 到栈上;如果是 `s`,依然需要 `pop` 两次并保存 `pop` 出来的东西,然后逆序 `push` 进去就完成了交换操作。
主要的困难在于 COOL 的智障语法,其他的倒是没啥难度。
- 最智障的是它的 `expr`。绝大部分的东西都是个表达式,两个大括号也是表达式 `{ [[expr; ]]+}`,两个大括号中间的表达式后面必须跟分号(其他的不需要),`if - fi` 后面都必须跟分号我也是绷不住了。然后它的函数定义后面跟着那两个大括号是个语法符号(摆设)而不是 `expr`,这种就很不直觉,让我困惑了半天然后翻它的文法声明才发现这个事情。`feature::=ID( [ formal [[, formal]] ] ) : TYPE { expr }`,也就是说,只能写一个表达式,不然就得再加一个单独的大括号然后里面写多个分号隔开的表达式。
- 其次是它的 `if-then-else` 居然不能没有 `else`!这导致我不得不像个傻子一样定义一个 `dummy(): Object {0};`,然后放在根本不需要的 `else` 子句里面。
- 还有它的 `while` 循环居然没有 `break`
- `let` 语法也是究极奇葩,我是不能理解为什么要设计成这个样子。想定义一个局部变量都得加一层嵌套,这个嵌套多得让我觉得我 tm 是在写 `scheme`
- 它的 `case` 也是奇葩,居然是用来做动态类型匹配的,匹配的是类型而不是值,和正常的 `switch-case` 完全不一样,倒是有点像 `rust` 里面的那种感觉。
测试的话,虽然 handout 里面说是直接 `make test` 然后对比输出,不过我也没看到它哪里有所谓的 reference implementation自己看了看测例觉着没啥问题就行了。
### PA2
PA 2-5 正式写编译器。PA2 写词法分析器,首先读一遍 README 和 handout。
> 环境配置
>
> 因为这个项目的结构非常的智障,导致需要进行一些配置才能让 `clangd` 正常工作。因为是 `Makefile` 项目,所以不能直接生成 `compile_commands.json`。
> 1. 安装 `apt install bear`,这个工具可以拦截 `make` 命令来生成上述的文件。
> 2. 在 PA2 目录下,运行 `make clean && bear -- make lexer`,然后 `clangd` 应该就不会找不到头文件之类的了
> 获取评测脚本
>
> 现在已经没有办法在线提交测试了(除非花钱?),因此需要从一些奇怪的地方获取测试工具,这里我从 https://github.com/shootfirst/CS143 这个 repo 里面扒了评测脚本(测试数据已经包含在了脚本里了) `pa[2-5]-grading.pl`,测试的话直接用 `perl ./pa2-grading.pl` 就行了。
> 这个东西确实测出来一些自己没考虑到的边角问题,虽然我已经很努力的在编测例了。
写完了发现还是很折磨的,写了快两天的样子,一方面是对于 `flex` 工具和配套的基础设施不是很了解,另一方面是各种细节问题需要处理。
#### 实现说明
比较简单的部分是那些只需要返回一个 token 类型的东西关键字和符号这种pattern 就是他们自己,然后 action 就一个 `{return (TOKEN)}`
然后稍微想了一下的,标识符和 Int、Bool 常量,需要写一个简单的正则外加操作 String Table 和 `cool_yylval`
需要折腾一会的是注释,需要用到 flex 的状态。
- 比较简单的是单行注释,检测到 `--` 就进入 `<SCOMMENT>`Single-line Comment然后检测到换行就回到 `INITIAL` 状态。这里是 `.``\n` 这一组互斥状态EOF 不需要单独处理,可知已经覆盖了所有的情况。
- 然后是多行注释,不过这里需要支持 nested comment。这玩意的意思是说需要在注释里面也要成对匹配 `(*` `*)`,不能简单的只考虑最外层或者最内层的符号对。这里需要多加一个变量 `comment_nest_level` 来维护嵌套的层数,流程也更复杂一些。
首先遇到 `(*` 进入 `<NCOMMENT>`Nested Comment同时初始化嵌套层数然后在该状态下过滤 `(*`,发现一个就叠一层,过滤 `*)` 发现一个减一层或者到底了就退出状态就行了;这里需要特殊处理 EOF如果在该状态下匹配到 EOF需要报错但是因为反正输入流已经结束了就不需要做 resume 了;遇到 `\n` 维护一下行号;剩下的就交给 `.`。前面其实不需要写 exclusion因为最长匹配`(* *)` 都是2个字符`.` 和 `\n` 都是1个 EOF 不可能匹配到其他的东西上),所以可以简单的囊括所有的情况并维护优先级。
- 除了注释里面的部分,还需要考虑一个单独的 `*)`,需要匹配并报错(而不是`*` `)` 两个 token。这个是4.1节第5条的要求。这样一来我们就解决了嵌套注释不匹配的两种错误情况左符号比右符号多 => `EOF in comment` 报错;右符号比左符号多(等价于单独的 `*)` => `Unmatched *)` 报错。
最折腾的是字符串,因为它要考虑多行的情况,而且需要做特殊的 resume 处理。好消息是这些细节文档都有描述,坏消息是需要看好几遍才能完全理解。
- 为了方便处理出错恢复处理,用了两个状态 `<STRING>``<STRINGREC>`String Recovery不然需要在 action 里面写很多的特判)。
- 正常情况下,遇到 `"` 进入 `<STRING>`;然后对需要特殊处理的部分编写规则:
- `\` 转义:匹配两个字符,根据 `\` 后面的字符决定把什么东西写进最终的字符串常量里面,这个东西在 handout 的第4.3节和 manual 的10.2节有描述;需要注意的是,合法多行转义可以(在最后加一个 `\` 然后换行,后面没有其他空白符)在这里一并处理掉,只不过需要写成`(.|\n)`。
- EOF根据 manual除了 `\0` 和 EOF其他字符均可出现在字符串中。因此需要单独处理这两玩意。EOF 和前面的注释类似,比较简单。
- 0字符0字符需要恢复。除了返回 ERROR Token还要把状态切换到 `<STRINGREC>`,剩下的部分交给恢复规则。
- 未转义换行:根据最长匹配,可以直接写 `<STRING>\n`因为转义过的是2个字符。这个不需要进恢复状态因为它直接返回 `<INITIAL>` 然后继续下一个字符就相当于处理了恢复的过程。
- 对于除了以上情况的所有字符(可以 exclude 或者把 `.` 写到最后),用 boilerplate 给我们定义的 `string_buf` 存放。这里需要处理一个字符串过长的问题,判断指针超限之后,进 `<STRINGREC` 并返回 ERROR Token具体的边界情况可以对拍对比标准实现来获知。最后就是遇到了未转义的 `"`(根据最长匹配可以直接写 `<STRING>\"`),把 `string_buf` 塞进 `stringtable` 里面,回到 `<INITIAL>`
- 对于恢复状态 `<STRINGREC>`,根据手册,从下一个未转义的换行符或者未转义的`"`(因为要求是 `closing "`)开始继续正常的词法分析。所以在前面需要把 `\\\n``\\\"` 给处理掉,然后遇到 `\"|\n` 这两个之后就直接返回 `<INITIAL>` 就行,也不需要 return 任何东西。期间也要记得维护行号。不过事实证明,不需要单独处理 EOF因为恢复了也没东西了。
- 这里有一个细节问题,就是报错的行号必须等于出错的地方的行号,因此不能留到后面统一返回报错,而是一旦出错就立刻返回。
容易忘掉的点,一个是空白符匹配,需要一个 patternaction 留空),不然遇到空白符 lexer 会行为异常,同时遇到换行还要维护行号;第二个是不合法字符,有些 ASCII 我们并没有用到,因此需要最后一个 `.` 来匹配并报错、跳过。同样的,`.` 和 `\n`(包含在空白符规则里面)的组合至少保证了所有的字符都会被匹配、处理。
不过其实,最离谱的是自己设计测试样例,这很考验对于手册的理解,不然就会漏掉点什么(虽然漏掉一些 edge case 对于后面也没啥影响就是了)。
在用了官方的评测脚本之后,又发现了一个问题:`\\0``0x5C 0x00` 是不允许的。这个其实判定起来不难,但是会想不到,因为 PA2 的 handout 没写这一条,虽然 manual 里面 `A string may not contain the null` 的确是不允许这种情况(因为它转义完了还是 `\0`),但是谁会想到测这个呢?
#### 基础设施说明
个人感觉,一开始做的比较迷惑的主要原因在于它的代码框架比较凌乱,文档也有点谜语人,读了好几遍文档才知道该写点啥。
写之前多读几遍文档,首先看一遍 PA2 的实验说明,了解一下需要干什么,里面也有一些读其他文档和代码的指导。然后是 `cool-manual.pdf` 看第10节主要是写 pattern 的时候看和第12节里面那个大的 BNF 文法(里面描述了所有用到的符号),以及 `cool-tour.pdf` 看第3节在写 action 的时候会用到它来存字符串。最后看一下代码,主要是 `/include/cool-parse.h`,这里面定义了非 ASCII 符号的 TOKEN还有 `YYSTYPE` 枚举,这个枚举是用来存 lexeme 的,`lextest.cc` 也要看一下,至少知道输出的都是什么。
这个里面用到的基础设施主要是一个 String Table用来存放所有遇到的字符串。里面有个 `add_string` 方法,会先查再加还带内存分配,所以直接调用就行。碰到需要存字符串值的东西,各种 identifier、String 常量和 Int 常量(不检查不转换,直接存),这三个每个都有一个单独的实例对象(`idtable` `stringtable` `inttable`),需要根据不同的 token 用。
另外一个就是 `YYSTYPE cool_yylval` 这个枚举。如果有需要存的信息lexeme 或者错误信息参考第5节 `Notes for the C++ Version of the Assignment`),每个 action 往里面写一个值,这个对应 PA2 文档第4节开头的部分当时看了半天才理解到是要存这东西里面。用到里面的3个枚举值`boolean`、`symbol`、`error_msg`。如果是解析出来 `Bool` 常量,那么直接写进去 `true/false`;如果出错了,给字符指针 `error_msg` 赋值,我猜需要用到 `strdup`,但是单就这个实验看不出来,总感觉要出内存 bug也不知道后面会不会 free 掉);剩下来那些需要存 lexeme 的就先 `add_string` 然后它会返回一个 Entry这东西就是所谓的 Symbol 啦。
#### flex 工具踩坑
最后记录一下 `flex` 这个工具的一些坑。
- **版本**:因为不知道在网上的什么地方看到了一个 blog 说是新版本的 `flex` 在处理 c++ 时的行为和旧版不一样,所以选择了旧版本,也就是 `apt install flex-old` 安装的版本2.5.4)。问题在于我看的文档版本是最新的,遇到了一个不支持的语法,不过其他的倒是没有特别需要注意的地方。旧版本的 `flex` 似乎不支持 `(?i:xxx)` 这种写法,这个在关键字忽略大小写的时候会比较省事(因为只有关键字需要忽略大小写,所以不能开全局忽略),可惜的是旧版本只能像个傻子一样写成 `CLASS [Cc][Ll][Aa][Ss][Ss]`
- **奇怪的缩进规则**:它居然和 python 一样,依靠缩进来分辨一些东西,每个 Section 的规则还稍有不同。比如在 Rule Section就是实际写 action 的那一节pattern 必须无缩进,然后其他的东西(比如注释)必须前面有空白符,不然就会被认为是一个 pattern。在看文档的时候要注意一下 `(un)indented` 这个词,可以避免很多奇怪的事情。
一开始没看到 `the pattern must be unindented and the action must begin on the same line` 这一句,没给注释加前面的空格,然后就调了半天的 `unrecognized rule`
还有就是 action 可以没有大括号但是不能没有分号,以及它其实是可以写多行的,只要前面有 indent 就行(不然会写出很长一串)。
- **EOF 符号**`flex` 里面的 EOF 专门有一个 `<<EOF>>` 来表示,但是它不能和其他的 pattern 写在一起,也就是需要单独一行写一个这东西,不然报错。
- **0 字符null character**:这个本身没什么特别的,直接用就行,不过需要注意的是,`.` 通配符是包含 `\0` 的,也就是一不小心就用这玩意把 `\0` 给匹配进去了,单独写的那个 `\0` 规则就不生效,会导致一些费解的事情发生。
- **VSCode 插件**:搜到两个还算下载量比较多的,但是都不行,有一个着色有 bug可能是因为它太老了接口不兼容了还有一个加了莫名其妙的、极具误导性的语法检查还关不掉一点问题都没有的代码给我每行一个红色波浪线。建议用第二个然后大脑忽略他的错误提示。
剩下的就看两眼文档就会了,跟课上教的 Regular Expression 基本上大差不差。
### PA3
这个作业写 Parser。主要问题依然来自于 bison 工具不会用以及各种奇形怪状的 edge case折磨程度和前一个差不多写了3天的样子。写的时候一直在想如果有 `ANTLR` 就好了,之前玩过,感觉比 `Flex` + `Bison` 这种老古董要舒服多了。
#### Bison 踩坑
前期主要是 Bison 不会用,这里记录一些坑。基本上的工作就是在翻译手册上的 BNF 然后写 semantic action。写着写着突然发现它的课上好像根本没讲这玩意就挺抽象的。
一开始需要了解的东西(不至于迷惑):`$$` `$[1-n]` 分别表示 `:` 左边的 non-terminal 和右边的第n个符号所关联的值这些值可以理解为树的结点。这些东西是有类型的所有可用类型定义在 `%union` 里面,对应到产生式上需要单独指定(貌似新版的 bison 提供了推导功能,但是这个实验在设计的时候显然没有)。对于终结符(这里就是那些 `TOKEN`),默认 `int`,如果带 `lexeme` 值的需要特别指定 `%token<typename> token-name` 这种;对于非终结符,需要指定 `%type<typename> non-terminal-name`,这里就类似于 non-terminal 的声明。bison 会检查这些所有的类型是不是匹配,当然这里的检查只是检查这些语法符号之间的类型匹配,具体到 C++ 的函数调用还是交给编译器。`@$` `@[1-n]` 是行号相关的东西。详细的信息似乎在 bison 文档里面有一个单独的小节列出了符号的含义来着,忘了。
1. 关于 Bison 手册的阅读这里我直接用的最新的3.8.2,直接看最新的文档就行了。
1. 1.1-1.4简单了解一下第2章可以看也可以去看看《Flex与Bison》都是用例子讲解然后就是具体的东西3.2-3.43.75.1-5.7这些都会用到;看看这些基本上就能把框架搭起来了。
2. 然后到了第一次编译第8章关于 debug 的说明还是可以看看的
3. 最后是看第6章关于错误处理的说明虽然这个说明也没啥大用。
2. 框架代码会导致 Shift-Reduce Conflict如果是框架提供的那种、用空规则定义各种 `xx_list` 的写法,会出现 SR Conflict。虽然实际上应该是可以用的大雾因为有一些 bison 的默认行为会使得这么做能够得到正常的结果),但是我还是把它拆掉了(不像看到 warning也就是把用到 `xx_list` 的地方写成有 `xx_list` 和无 `xx_list` 两种,而不是交给 `xx_list` 这个 `nterm` 来处理空的情况。这样就没有 warning 了。
3. 一些东西是 bison 的扩展,因为开了兼容性警告 `-Wyacc`,所以用了会 warning比如 `%empty` `%precedence` 这种。
剩下的倒是没啥,因为没有用旧版本的东西,所以没有出现 `flex` 那么多奇怪的东西。
#### 基础设施说明
这里的代码库主要是他们自己写的奇怪的 AST 的东西,虽然用起来也没啥大问题,不过总觉得有点抽象。这里主要参考 `cool-tour.pdf` 的第6节描述还算是比较详细的。
具体的东西没必要了解,也不需要修改它的结构,说一下基本的逻辑就能上手写 semantic action 了。
- 首先它的功能是提供一种 AST 的描述方法,定义了一堆不同类型的 node写语义动作的过程就是用这些 node 来组装一个 AST。
- 用到的接口基本上就是类似于构造函数一类的东西(这么理解其实对于写这个 lab 而言没有任何问题),每种 node 都有一个自己的类型,能够接收一组特定类型的参数进行构造。不同的类型通过 bison 文件前面的 `%union` 进行定义,构造器就是在填当前 node`$$`)的信息和子节点关系。写文法就类似于递归下降(虽然实际上 bison 用的是 LALR一种自然的构造语法树的过程。
- node 主要有两种,一种是 list另一种是单独的 node。list 表达的是一堆同类型的子节点,构造的时候和链表类似,这个在 skeleton 里面有例子class list。然后我们在定义的时候可以参考着写。
- 除了简单看一看那个 `cool-tour`,具体的定义去看那个 `aps` 文件,描述了详细的参数类型。
- 这个实验只需要用构造器函数就行了,生成 C++ 时候添加的方法用不到,是给后面计算 AST 用的。
#### 实现说明
要修改的文件主要是 `cool.y`,但是为了处理行号的问题,还需要修改 `/include/PA3/cool-parse.h:754`
最简单的部分是翻译文法定义,照着 manual 12节抄就行了。在此过程中可选的部分要稍微处理一下描述的时候拆成有和没有可选部分两条规则然后还要填上默认值比如省略初始化的时候需要填个 `no_expr()`、函数调用省略对象的时候需要补上默认的 `object(idtable.add_string("self"))` 。这里面唯一要动一动脑子的就是前面提到的 list 的书写方法,一方面是 Shift-Reduce Conflict 最好不要写空规则,另一方面就是链表的构造,不过这个可以抄前面的 `class_list` 的定义,不需要自己折腾了。
然后的是优先级定义,虽然也没啥好说的,照抄 manual 的优先级表。值得注意的是,这里不需要理会 `dangling else` 问题,因为文法不允许省略 else 子句;这种设计虽然对于 PA1 写代码很不友好,但是实现起来还是很舒服的。
比较头疼的是 `let`,这东西没办法写在 `expression` 规则里面,需要自己再开一个 non-terminal 来写递归。之所以麻烦,是因为它的定义列表是没有长度限制的,所以要用文法描述就必须通过递归。还有就是它需要把有多个符号定义的 `let` 转换为多个只有一个符号定义的 `let` 的嵌套。比较好理解的写法是写右递归,类似于 lisp 里面的链表,看到逗号就继续递归加尾巴,把 `IN expression` 作为终止条件。不过这个最后的 `expression` 会导致冲突,因为 `let` 本身就是一个 `expr`,所以你也不知道这个后面的东西是接在里面这个 `expr` 的后面还是接在 `let` 这个大的 `expr` 的后面。一个具体的例子:`let id1:T1 in expression.f()`,有两种加括号的方法 `(let id1:T1 in expression).f()``let id1:T1 in (expression.f())`,也就是在这个 `.` 的地方产生了 Shift-Reduce。根据提示我们可以用 `%prec` 来解决问题,在优先级列表最低级里面加一个 `LETEXPR`,然后在 `IN expr %prec LETEXPR`,这样就会优先匹配其他的可能属于 `expr` 的产生式,最后再匹配 `let` 后面的那个。
写完以上这些东西,这个文法实际上已经完成了,能够解析所有合法的情况。
最麻烦的事情是错误恢复。不过说难也还好,关键是没个例子不知道咋写。基本的写法就是有一个内置的 terminal `error`,可以匹配所有的出错情况,然后就可以一直跳过直到我们指定的某一个 token。基本上就是在每个 list 那边加一条 `| error ';' ` 类似的东西这样就可以把错误跳过去直到一个合法的结束符handout 里面的要求就是如果有合法的结束,那么从下一个同类型的结构继续)。不过 `let` 那边需要特别注意一下,因为除了中间的声明部分的情况,我们还希望继续检查最后的那个 `expr`,因此不能只写一个 `error ','`,如果只有一个变量声明的话,那么后面的 `expr` 就会无法匹配规则,因此还要写一个 `error IN`
不过以上都是我的写法,我看 Github 上还有别的写法,有的有问题,有的写的不太好但是能过(就比如说用空规则),总之能用就行,我也不能保证我写的一定没问题,毕竟对于 bison 还不是很熟悉。
最后还有一个行号的问题,建议把所有的行号都设置成最后一个符号的行号,不过实际上可以不改。感觉需要改一下的是 `cool-parse.h` 那边,因为 `no_expr` 的行号设为0比较合理我这里好像是会继承基类`tree_node` 的构造函数把行号设置为当前的行号但是其实应该写成0我也不知道当年的编译器是咋做的。
### PA4
终于不用折腾老古董了,虽然但是这个 PA 的代码量有点大啊,而且要考虑的东西变多了,为啥越做越难了。
这个 PA 的代码量特别大写了我三天半而且是周末的三天半每天写12小时代码主要是实现语义检查有很多细节需要考虑包括生成的一些错误信息之类的。将近1/3的时间会花费在读文档、写测例、对着参考编译器复制错误信息虽然不一定要求和参考程序一样但是写成一样的会使得 diff 比较方便)以及考虑各种细节问题上。
#### 写之前先读文档
需要读的 Manual: Typing rulesscoping rules。除了13节 operational semantics 之外,应该都要看完。
主要任务:
- 遍历所有的类、构建继承关系图、检查继承关系
- 对于每一个类,先遍历一遍构建符号表,然后类型检查并在 AST 上做类型注解
需要修改的文件:
- `cool-tree.h` 扩展 AST 的定义;
- `semant.cc` `semant.h` 实现主要的逻辑:`semant()` 方法会被主程序外部调用,`ClassTable` 写了一点 starter code构建继承关系用这个东西。
如何遍历:
- 看代码 `dump_with_types`
继承关系构建:建个图然后检查环路。从 basic class 继承有限制Int、String和Bool这三个都是不可继承、不可重定义的其他的basic class 不能重新定义),同时不能继承一个不存在的类。
推荐的分析过程:第一步先检查继承关系,第二步检查其他的语义条件。
作用域:`self` 自动引入其他带需要考虑命名覆盖的问题。注意class、method、attribute 这些的命名可以在定义前被使用,因此可能需要多趟处理。符号表在 support code 里面有定义,甚至有一个 `symtab_example.cc` 的样例程序。
类型检查:对于无法确定类型的表达式,赋一个 Object 然后尝试恢复。需要给出错误信息,不过正如课上讲的 cascading error 是可以接受的。
代码生成接口约束:所有表达式节点的 `type` 必须设置为 `Symbol`,具体的值由 type checker 决定。`no_expr` 赋值为预定义的 `Symbol` 变量 `No_type`
错误输出:对于不存在继承相关错误的程序,需要报告所有的语义错误(不要求和参考实现完全一致)。这个作业里面需要手动调用报错方法 `ClassTable::semant_error()`前两个PA是工具自动生成的
调试skeleton 提供了一个命令行开关 `-s`,对应全局变量 `semant_debug`,可以用来条件打开调试信息。
#### 实现说明
需要修改的文件有四个,`semant.cc` `semant.h` `cool-tree.h` `cool-tree.handcode.h`。前两个主要是自己实现,主要的代码都在这里;后两个是为了定义 AST 节点上的接口。
接口方面,我增加了一个 `semant(Class_, ClassTable*)` 来做类型检查以及一些必要的 getter 供父节点访问子节点内部信息。不过也不是所有类型的都写了,为了省事,我只写了 `Feature``Expression` 两个类的 `semant` 方法。写的时候可以在 `cool-tree.handcode.h` 的宏里面定义这些接口,这样就不用手动把定义复制到每个子类里面去了。剩下的各种列表、`Branch` 和 `Formal` 的处理逻辑就一起写到他们的父节点里面了,因为他们被用到的地方是唯一的,写在哪里都是写一遍。`semant` 方法接受两个参数,一个当前的类,对应规则里面的环境 `C`;还要一个是 `ClassTable` 这里面存放了所有需要用到的工具以及符号表(对应规则里的环境 `O,M`然后递归向下处理整个AST。
整个过程分为两个部分实现,分别处理继承关系和其余部分(主要是类型检查)。
- 继承关系检查又需要扫描三次类列表,第一遍去除重复的类定义并检查预定义类冲突;第二遍初步建立继承关系树,检查基类存在性;第三遍遍历继承关系树,检查是否存在环形继承。这些工作完成后,产生了一个继承树和一些其他的辅助记录结构,后面的检查会依赖这些信息。
- 检查环路其实很简单,只需要从 Object 开始遍历一遍树,没访问到的肯定有环。因为语法分析保证了一个类只能有一个基类,如果存在环那么它向根肯定跳不到 Object不过向上跳不好写反过来就是从 Object 开始遍历树无法访问到的节点在环上,就很简单了。
- 不过 skeleton 并没有提供相应的数据结构,需要自己实现。写一个 node 类型,然后写上父节点和子节点指针(列表),另外需要一个指针存对应的类以及一个辅助变量(用来后面写 LCA 和判定环路)。
- 在实现的过程中比较遗憾的是,指针乱飞,而且它提供的符号表还内存泄漏,实在是一言难尽。不过累了,懒得改 RAII 了,
- 其余部分需要两次遍历,一次线性扫描所有的类,遍历每个类的 Features 列表,收集每个类里面的属性和方法定义加入符号表并检查重复定义和方法覆盖的情况;同时还要顺便保证一下 Main 类和 main 方法的存在。第二次就是类型检查,从 AST 的 program 开始(这个 skeleton 已经写好了)向下递归调用子节点的 `semant` 方法来进行检查。
之所以要两遍,是因为属性和方法可以在定义前被使用,因此需要提前收集,不然没法做类型检查。
- 这里涉及到符号表的设计。根据类型检查规则,需要两个表,一个是标识符,一个是方法。我的做法是给每个类分了一组符号表(标识符和方法),用 map 索引起来,在第一次扫描的时候把类的属性和方法就塞进去,同时还有 `self:SELF_TYPE` 这个保留定义。
- 第一次检查的时候,需要处理重复定义和方法覆盖。对于属性,重复定义还需要考虑继承的属性;对于方法,特别注意方法覆盖,需要比较长度、返回类型(完全一致,不可用派生类)和每个形式参数的完全一致。
- 第二次就比较的繁琐,需要把每一条类型检查规则写成 C++ 代码。为此,还需要定义运算符 `join``conform`(相当于课上的 `lub``<=`)。前者是因为 `if``case` 两个语句的返回类型要取它们的最小上界。这个过程中需要特别注意的是对 `SELF_TYPE` 的处理,因为 `SELF_TYPE` 不在类定义表里,而且很多规则都涉及到这个东西。
具体的实现因为太多了,就不写了。
对着手册写完了一堆规则之后,还有究极麻烦的错误处理。这里列几个基本的原则,实在难绷的会记录在下面的细节说明中。
- **只有**在无法确定类型的时候赋值 Object而不是一有类型错误就返回 Object。比如使用了未定义的类型只需要报错就行了在后续的检查中不需要把他改成 Object。还有算术无论两边的操作数类型是啥这个算术表达式都是返回 `Int` 类型的。
- `SELF_TYPE``SELF_TYPE_c`,这两个东西在 AST 里面都写成 `SELF_TYPE` 这个 `Symbol`,在实际检查的时候再特别判定。另外关于这两个玩意的错误很多都是特别判定的,再参考实现里面这东西的错误不和其他的一起判断,我也推荐这样的写法,比如一开始先不考虑这两玩意儿,写完之后再加上。
- 尽量多检查一些东西,有些能继续的就继续,只有一阶段检查完之后可以 abort其他的尽量不直接 return。
#### 细节说明
特别注意一下的情况统计,会比较有用。
> 引入新变量绑定的四种情况
> - attribute definitions;
> - formal parameters of methods;
> - let expressions;
> - branches of case statements.
> 允许使用 `SELF_TYPE` 的四种情况
> - new SELF TYPE
> - return type of a method,
> - declared type of a let variable
> - declared type of an attribute
这个 PA 的坑细节很多调了大半天bug。
- 关于 `self``SELF_TYPE` 的特别判定
- `self` 不能被赋值、不能重定义(属性名、 `let` 变量名、形式参数名、`case` 绑定名)
- `SELF_TYPE` 不能重定义(类检查)、只能出现在四个地方(因此不能用于形参定义、静态函数调用)
- 表达式的返回值可能是 `SELF_TYPE`,这是就需要结合当前类来执行 `conform``join` 操作了,如果不考虑的话会段错误。
- 记得在返回类型的时候同时改 AST 上的类型标注(主要是 Expression 类下面的Feature 和 Formal 一开始就标好了 `type_decl` 这个反而不能改)
- 方法、属性查询的时候,记得要查父类,不然继承的变量和方法就找不到了。
- 还有各种未定义的处理,一不小心就段错误了
- 返回 Object 的情况objectid 未定义、new 未定义类、函数调用中无法确定被调用函数的情况类未定义、函数未定义、函数参数个数不对、loop 返回值。
剩下的基本上能从手册的12章中比较清晰的看出来。
### PA5
#### 读文档
最后一个 PA那必然是要把剩下来的文档全部读完。需要预先看一遍带是 `cool-tour.pdf` 的第7章 Runtime System 和 PA5 handout至于 `cool-manual.pdf` 的第13章语义部分倒是可以一边实现一边看。
先看 handout
- 代码量巨大无比,竟然是 PA4 的2倍
- 文件简述:`cgen.{cc|hh}` 大部分需要写的代码,和 PA4 类似的结构,从 AST 的根节点开始进行 `cgen``cool-tree.h` 和 PA4 类似;`cgen_supp.cc` 定义辅助函数;`emit.h` 里面有一些 MIPS 汇编和符号宏定义;剩下的都是老熟人了。
- 主要任务:
- 生成全局常量prototype objects
- 生成全局表,`class_nameTab` `class_objTab` 还有方法调用表
- 生成每个类的初始化代码
- 生成方法定义
推荐的实现方法还是分两部分,先生成对象布局,然后第二遍在生成每个表达式的代码。
- 提醒注意:这次没有必要去“逆向”参考编译器了,因为它实现了一些高级功能比如寄存器分配优化,这个 PA5 并不要求这个。
- 运行时错误处理manual 规定了6种运行时错误生成的代码需要检测三种(static) dispatch on void, case on void, missing branch除零可以交给模拟器剩下两种由 Runtime 处理。
- GC有一个3个命令行开关控制垃圾回收系统相关的功能。默认情况下不打开 `-g` 开关,此时不启用 GC也就是说这是一个选做功能`-t` 迫使 GC 系统在每次分配对象的时候进行回收;`-T` 的功能交给实现,可能会用来实现一些其他的运行时检查。实现 GC 功能的时候,需要认真阅读 Runtime 手册相关内容。这里可以去看看 CS143 课程网站上的那个单独的 `cool-runtime.pdf`,似乎写的更加详细一些。
- 测试工具:和 PA4 类似,提供了一个 `-c` 选项来设置全局变量 `cgen_debug`。同时提供了一个第三方实现的 `Coolaid` 工具对生成的 MIPS 汇编进行一些检查,说不定会有帮助。最后关于 `Spim` 的 warning 可能会有用。
然后看看 Runtime System
- 首先是对象布局GC Tag 设为-1Object Size 也得填上dispatch pointer 因为不会被 runtime 用到,所以需要自己设计 dispatch 表;对于属性,`Int` 只有一个32位整数、`Bool` 也是如此、`String` 有一个32位的长度+后面全部是 ASCII 字节(最后需要 word 对齐),然后还有空指针 void。
- 然后是一个叫原型对象Prototype Object的东西COOL 里面新建对象的方法是使用 `Object.copy()`,因此我们需要生成这些供其复制的东西,也就是 prototype object。生成的时候需要正确设置前面的头部对于属性三个基本类型有自己的规定其余类型的属性随意设置。
- 栈和寄存器约定:方法调用参数放在栈上、从左到右依次压栈,`a0` 寄存器里面放 `self` 对象指针。指定了一组 Scratch registers 供 runtime routine 存放临时数据,因此需要调用者保存;还有堆指针和堆界限两个寄存器,完全由 runtime 控制。其他的都可以用。
- Label生成的代码需要和 runtime 一起变成最后执行的机器码,因此有一些 label 是指定的,就类似于接口一样的东西。有些 label 是 runtime 提供给我们使用的,也有一些需要我们生成供 runtime 使用。有一句话 `There is no need for code that initializes an object of class Bool if the generated code contains definitions of both Bool objects in the static data area.` 没看懂
- 执行初始化:需要生成一些代码来调用 main 方法。首先通过 Main prototype 生成一个 Main 类的对象并用 `Main_init` 初始化,该初始化方法依次执行 Main 的基类的初始化最后初始化 Main然后调用 `Main.main`,在 `a0` 里面放上 `Main` 的指针并设置 `ra`;执行结束后,`Main.main` 返回,这里打印出提示信息并终止执行。
#### 读框架
这次的框架代码非常的多,而且写的很抽象,因此讲一讲。其实最好的方法是自己写个测试代码,然后用参考编译器生成出来看一看具体是啥样子的,光看他的 skeleton 是真的要绕半天。