Skip to the content.

Nand2Tetris Part 2 课程总结

距上次完成 Nand2Tetris Part 1 课程4个多月之后(2020-11-25),终于把 Nand2Tetris Part 2 也完成并拿到证书。这次就没有上次那么顺利了,完成整个课程的6个项目的时间跨度为4个多月,而花在项目上面的时间肯定比之前多得多。

其中Project9是启动最晚,耗时最长的项目。该项目我计划是实现一个俄罗斯方块,并且尽可能遵守 2009 Tetris Design Guideline。我分成两个步骤来完成,首先使用JS实现这个游戏,然后在使用Jack语言照着相同的逻辑实现一遍。在使用JS开发时,将渲染部分做的尽可能独立,这样在换成课程中的Jack语言时,其他逻辑基本可以等价的转化,而渲染和按键部分基本上都进行了重写。

每个Project完成情况如下,这里可以看到我完成的答案:

课程 Part 2 的侧重点是编译器和操作系统。首先引入了基于栈的字节码语言(VM Language),通过编写Translator,将其转化为 Part 1 中的机器码。然后引入类似Java的面向对象高级语言Jack,通过编写Compiler,将其编译为字节码,然后再通过Translator,转化为最终的机器码。此外,提供了一个极其简陋的OS API,提供基本的Memory、String、Screen、Keyboard、Math、Array等接口共应用程序调用。

项目总结

Project7和Project8

Project7和Project8分两步,实现一个VM语言(VM Language)到汇编语言(Hack Assembly Code,Project6实现了一个汇编语言到机器语言的汇编器)的转化器。其中字节码语言类似Java中的Bytecode,是一种基于栈的High Level IR。通过实现不同平台的VM,字节码可以在不修改的情况下在各个平台上执行,实现跨平台能力。

本项目实现的VM是堆栈机VM,数据使用一个后进先出的栈来存储,不同指令根据功能进行出栈入栈。Hack中的VM语言指令可以分成4类,分别是算术运算指令、内存操作指令、程序流程控制指令和函数调用指令。Project7实现前两类指令,Project8实现剩下的两类指令。

先来看下使用VM语言编写的斐波那契函数的大概样子:

// 定义
function fibonacci 0
  push argument 0
  push constant 1
  eq
  if goto RETURN_ONE
  push argument 0
  push constant 1
  sub
  call fibonacci 1
  push argument 0
  push constant 2
  sub
  call fibonacci 1
  add
  return
label RETURN_ONE
  push constant 1
  return

// 调用 fibonacci(8)
push constant 8
call fibonacci 1

下面按照指令类型分成4类,讲解各个指令的详细函数。

算术运算类指令:

add     将栈顶两个整数出栈后相加,并将结果入栈
sub     将栈顶两个整数出栈后相减,并将结果入栈
neg     将栈顶的整数出栈后取负,并将结果入栈
eq      将栈顶两个整数出栈后进行相等比较,并将比较结果入栈
gt      将栈顶两个整数出栈后进行大于比较,并将比较结果入栈
lt      将栈顶两个整数出栈后进行小于比较,并将比较结果入栈
and     将栈顶两个元素出栈后进行按位与运算,并将结果入栈
or      将栈顶两个元素出栈后进行按位或运算,并将结果入栈
not     将栈顶的元素出栈后进行按位取反运算,并将结果入栈

内存操作类指令(需要结合下面的Hack内存布局图来理解):

push segment index  将segment的第index个位置的内容push到栈顶
pop  segment index   将栈顶元素pop到segment的第index个位置

segment可选:
- argument      函数参数
- local         函数局部变量
- this          对象实例字段
- that          数组实例
- pointer       用于修改THIS和THAT中的内容,0表示操作THIS,1表示操作THAT。
                注意只是为了方便给编译器用,因为对象和数组实例的内存地址来自Heap,
                没有规律,所以需要给编译器一个修改THIS和THAT的方式。
                而像ARG、LCL则不需要,因为转化器在生成函数调用时,就可以知道ARG
                和LCL的位置,自己进行设置就可以,不需要暴露给编译器去设置。
- static        全局变量,开始地址固定为16
- temp          临时变量,开始地址固定为5,给编译器内部使用
- constant      常量,表示将常量push到栈顶,没有pop操作

程序流程控制类指令:

label   symbol      引入一个标签,供goto、if-goto语句使用
goto    symbol      无条件跳转
if-goto symbol      如果栈顶元素为true则跳转

函数调用类指令:

function functionName nLocals   函数声明,nLocals用于表面函数本地变量的个数
call     functionName nArgs     调用函数,nArgs用于设置被调函数的参数位置
return                          函数返回

Project9

Project9首先介绍了Jack高级编程语言,它是一种面向对象高级编程语言,跟Java语言非常类似。然后让同学们使用该语言编写一个应用程序供其他同学进行评审以获得分数。根据我实际使用的情况,虽然Jack和Java类似,但是语法和库相比Java还是极其有限,对开发者非常不友好,当作一个玩具语言就好。

Jack语言介绍:

我给Project9做的应用是俄罗斯方块游戏,刚好呼应课程的题目:)。并且我做的有一个特点,就是尽量按照现代的俄罗斯方块规则来做,我选择的俄罗斯方块标准是 2009 Tetris Design Guideline,完整支持SRS旋转系统(Super Rotation System)。做的时候,我是先用JS写了一个比较完整的版本,并且在写的时候将渲染部分做的比较独立,方便后面翻译为Jack语言时对其他部分的逻辑改动较小。做完JS版本之后,再通过人工将JS版本中的逻辑翻译为Jack版本。

编写过程遇到的一些问题:

Project10和Project11

Project10和Project11分两步,实现Jack高级编程语言到VM语言(字节码)的翻译。涉及到词法解析、语法解析(因为语法经过特殊设计,解析起来非常的简单)、符号处理和字节码生产等。每一块的内容都非常的简单,但是需要完整做完才能够通过测试,还是非常的有意义。

Jack语言BNF描述:

Class           -> 'class' ClassName '{' ClassVarDec* SubroutineDec* '}'
ClassVarDec     -> ('static' | 'field') Type VarName (',' VarName)* ';'
Type            -> 'int' | 'char' | 'boolean' | ClassName
SubroutineDec   -> ('constructor' | 'function' | 'method')
                   ('void' | Type) SubroutineName '(' ParameterList ')'
                   SubroutineBody
ParameterList   -> ((Type VarName) (',' Type VarName)*)?
SubroutineBody  -> '{' VarDec* Statements '}'
VarDec          -> 'var' Type VarName (',' VarName)* ';'
ClassName       -> Identifier
SubroutineName  -> Identifier
VarName         -> Identifier

Statements      -> Statement*
Statement       -> LetStatement | IfStatement | WhileStatement
                |  DoStatement | ReturnStatement
LetStatement    -> 'let' VarName ('[' Expression ']')? '=' Expression ';'
IfStatement     -> 'if' '(' Expression ')' '{' Statements '}'
                   ('else' '{' Statements '}')?
WhileStatement  -> 'while' '(' Expression ')' '{' Statements '}'
DoStatement     -> 'do' SubroutineCall ';'
ReturnStatement -> 'return' Expression? ';'

Expression      -> Term (Op Term)*
Term            -> IntegerConstant | StringConstant | KeywordConstant
                |  VarName | VarName '[' Expression ']' | SubroutineCall
                |  '(' Expression ')' | UnaryOp Term
SubroutineCall  -> SubroutineName '(' ExpressionList ')'
                |  (ClassName | VarName) '.' SubroutineName '(' ExpressionList ')'
ExpressionList  -> (Expression (',' Expression)*)?
Op              -> '+' | '-' | '*' | '/' | '&' | '|' | '<' | '>' | '='
UnaryOp         -> '-' | '~'
KeywordConstant -> 'true' | 'false' | 'null' | 'this'

Identifier      -> [a-zA-Z_] [a-zA-Z0-9_]*
IntegerConstant -> 0 ... 32767
StringConstant  -> '"' [^"\n]* '"'

Project12

Project12的目的是实现Jack中操作系统提供的库,包括内存分配和释放、键盘事件监听、字符和图案输出到屏幕等功能。比较有挑战。

Jack系统标准库(部分接口会补充实现的难点说明,其中init为内部接口不对外使用):

总结

至此,Nand2Tetris的两个课程都完成了,这是我上过的最好的课程,没有之一。课程内容安排循序渐进,课程Projects贴合课程内容,难度合理。尤其是两个课程都是按照下图所示的Roadmap来展开,每当达到一个新的点就展示当前所处的位置。真的非常感谢上课老师 Shimon SchockenNoam Nisan 的幸苦付出。