一、前言
项目来源:
该项目是著名课程Nand2Tetris的课程项目,总共分12部分,从零开始构建属于自己的hack计算机。该文项目属于第八个子项目。
项目路线介绍:
在硬件部分,你将进入 01 的世界,用与非门构造出逻辑电路,并逐步搭建出一个 CPU 来运行一套课程作者定义的简易汇编代码。在软件部分,你将编写一个编译器,将作者开发的一个名为Jack的高级语言编译为可以运行在虚拟机上的字节码,然后进一步翻译为汇编代码。你还将开发一个简易的 OS,让你的计算机支持输入输出图形界面。至此,你可以用 Jack 开发一个俄罗斯方块的小游戏,将它编译为汇编代码,运行在你用与非门搭建出的 CPU 上,通过你开发的 OS 进行交互。
二、项目介绍
1.涉及知识
疑问:
在学习完第八章后我们知道,我们的VM机要有可以进行跨存储域进行函数调用的能力,即这个文件内的函数有权调用存储在别的文件里面的函数(所有函数的访问权限都是支持不同模块调用的,类似Java里的public),这其实是仅仅我们在VM汇编器上无法解决的,单单不说个返回值问题,仅仅在调用上就无法实现,以为汇编器是针对单个.asm文件,如果这个.asm文件里面只有一个模块的细节,那我们也无法将之与其他模块进行联结,所以本章中一个对于翻译的处理细节是把多个vm文件翻译成一整个的.asm文件,所有的函数细节都在这里面,这样的效果就是对函数调用进行行为拆解了——函数调用=修改存储结构+命令跳转+恢复存储结构+命令跳转。
项目理解:
第八章是在第七章的基础上完成的,所以我们大可以以7章的项目框架来进行增补操作,来完成第八章要求的程序控制流命令和子函数调用命令翻译工作,像前面说的那样,第八章完成后要进行多个vm文件翻译成一个asm文件,所以翻译器的输入输出结构不再是一个vm文件对应一个asm文件,而是一至多个vm文件对应一个asm文件,这就要求我们对主类(VmTranslator类进行大修改),以及CodeWriter类的输出文件进行协调成一个通用的文件。
2.实践要求
目标:
- VMTranslator的后半段
要求:推荐使用一门高级语言,来完成一个可以读取一至多个.vm文件,并将其翻译成一个.asm文件的VM翻译程序
细节:
- 推荐使用书里面的分成三个模块来实现(可以把CommandType模块独立出去)
- Parser:分析.vm文件,封装输入代码的访问,其功能有二:1.解析命令的类型;2.对命令进行洁净化处理
- CodeWriter:翻译.vm文件,封装输出代码的通道
- VMTranslator:主程序,程序的入口
- 个人对于这个翻译器的理解是:加强版HackAssembler,它的工作模式依旧是:获取命令和命令类型->根据类型对命令进行不一样的翻译工作,不一样的点是汇编器的翻译工作相近性高,而这个翻译器低一点,比如:一个Hack命令对应一条01命令,而一个VM命令可能需要几条hack命令来达成。
- 流程控制命令翻译:流程控制语句说到底是高级一点的跳转语句,在hack汇编语言中有很多的基于D寄存器可用的跳转语句,像
0;JMP
就是无条件跳转,但是我们需要考虑的是怎么跳转?依据标签跳转。标签规范如何?文件名$函数名
。依次我们把流程控制语句分为三部分实现(vm语言的流程控制命令也有三种类型)- writeLabel:翻译标签语句
- writeGoto:翻译无条件跳转语句
- writeIf:翻译条件跳转语句
- 子函数调用命令翻译:这部分的翻译是该项目最难的部分,因为我们在这一部分中不仅要兼顾于函数调用和返回,还要把函数这一概念引入我们的翻译机中,否则按照我们实现至此的功能,我们的翻译机是无法识别函数的,所以我们还要处理FUNCTION命令,其中我们不仅仅是直接翻译函数名,我们还要给函数名一个通用的标签,以便实现基于标签跳转的函数调用,所以在此我们也把子函数调用功能分四部分实现
- writeFunction:翻译函数声明命令
- writeCall:翻译函数调用命令
- writeReturn:翻译函数返回命令
- writeSave
三、项目展示
1.程序结构图
2.2.CodeWriter
/**
* 此堆栈用于存放调用的函数. <br/>
* 因为标签在不同的函数中需要不同的函数名来区分。
*/
private Stack<String> callStack;
/**
* 构造函数,初始化编写器。<br>
* 由于一个代码编写器面向一个文件目录,我们需要判断文件参数 <br>
* 是否为 file。但在以后的应用程序中,这通常是目录。
*/
public CodeWriter(File file) throws FileNotFoundException {
this.callStack = new Stack<>();
if(file.isFile()) {
this.setFile(file);
// 首先将文件名推入堆栈,以避免空堆栈
this.callStack.push(file.getName());
setFileName(file.getPath());
this.writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputFile)));
}else {
// 首先将文件名推入堆栈,以避免空堆栈
this.callStack.push(file.getName());
String outputFilePath = file.getPath()+"\\"+file.getName()+".asm";
File oldFile = new File(outputFilePath);
if(oldFile.exists()) {
oldFile.delete();
}
this.outputFile = new File(outputFilePath);
this.writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputFile)));
}
}
/**
* 编写初始化代码。
* 事实上,只有在解析 Sys.vm 文件时才调用这个函数。
* 并且这个方法应该由 writeFunctionmethod 调用。
* 如果函数名是 Sys.init,writeFunctionmethod 就调用这个方法。
* @throws IOException
*/
private void writeInit() throws IOException {
// this.writer.write("// initialize sp=256\r\n"+
// "@256\r\n" + // "D=A\r\n" + // "@SP\r\n" + // "M=D\r\n");
this.writer.write("// Sys.init function start\r\n" +
"(Sys.init)\r\n");
this.callStack.push("Sys.init");
this.writer.write("\r\n");
}
/**
* 翻译标签命令
* @throws IOException
*/
public void writeLabel(Parser parser) throws IOException {
String arg = parser.args1();
this.writer.write("// vm command: "+parser.getCurrentCommand()+"\r\n");
this.writer.write("("+this.callStack.peek()+"$"+arg+")\r\n");
this.writer.write("\r\n");
}
public void writeGoto(Parser parser) throws IOException {
String arg = parser.args1();
this.writer.write("// vm command: "+parser.getCurrentCommand()+"\r\n");
this.writer.write("@"+this.callStack.peek()+"$"+arg+"\r\n"
+ "0;JMP\r\n");
this.writer.write("\r\n");
}
public void writeIf(Parser parser) throws IOException {
String arg = parser.args1();
this.writer.write("// vm command: "+parser.getCurrentCommand()+"\r\n");
getTopSP();
this.writer.write("@"+this.callStack.peek()+"$"+arg+"\r\n" +
"D;JNE\r\n");
this.writer.write("\r\n");
}
private void saveScene() throws IOException {
this.writer.write("// save work\r\n" +
"@"+this.callStack.peek()+"$retAddr"+i+"\r\n" + //这是函数返回地址
"D=A\r\n");
pushValueIntoStack("D");
this.writer.write("@LCL\r\n" +
"D=M\r\n");
pushValueIntoStack("D");
this.writer.write("@ARG\r\n" +
"D=M\r\n");
pushValueIntoStack("D");
this.writer.write("@THIS\r\n" +
"D=M\r\n");
pushValueIntoStack("D");
this.writer.write("@THAT\r\n" +
"D=M\r\n");
pushValueIntoStack("D");
}
public void writeCall(Parser parser) throws IOException {
String arg1 = parser.args1();
String arg2 = parser.args2();
/*
* 当程序遇到调用函数时,我们将函数名推入调用堆栈,
* 调用堆栈可以通过写标签方法使用。
* 当程序返回方法时,我们弹出函数名。*/
this.callStack.push(arg1+(++this.i));
this.writer.write("// vm command: "+parser.getCurrentCommand()+"\r\n");
saveScene();
this.writer.write("// argument process\r\n" +
"@SP\r\n" +
"D=M\r\n" +
"@5\r\n" +
"D=D-A\r\n" +
"@"+arg2+"\r\n" +
"D=D-A\r\n" +
"@ARG\r\n" +
"M=D\r\n" +
"// LCL=SP\r\n" +
"@SP\r\n" +
"D=M\r\n" +
"@LCL\r\n" +
"M=D\r\n" +
"// go to called function\r\n" +
"@"+arg1+"\r\n" +
"0;JMP\r\n" +
"("+this.callStack.peek()+"$retAddr"+i+")\r\n");
this.writer.write("\r\n");
}
public void writeReturn(Parser parser) throws IOException {
this.writer.write("// vm command: "+parser.getCurrentCommand()+"\r\n");
//获取返回地址并存入R14寄存器
this.writer.write("@LCL\r\n" +
"D=M\r\n" +
"@R13\r\n" +
"M=D // temporarily store the endFrame\r\n" +
"@R13\r\n" +
"D=M\r\n" +
"@5\r\n" +
"A=D-A // get the return address\r\n" +
"D=M\r\n" +
"@R14\r\n" +
"M=D // temporarily store the return address\r\n");
//将栈顶的值(返回值)存入以被调用函数为视角的ARG 0 内存段,作为返回值
//先获取ARG内存段的首地址,并存入R15寄存器
this.writer.write("@ARG\r\n"
+ "D=M\r\n"
+ "@0\r\n"
+ "D=D+A\r\n");
storeValueInGR("R15");
//获取栈顶元素并存入R15寄存器所保存的地址的块
getTopSP();
this.writer.write("// store the top value\r\n"
+ "@R15\r\n"
+ "A=M\r\n"
+ "M=D\r\n");
//把ARG段的下一个地址作为SP地址
this.writer.write("// set the SP\r\n" +
"@ARG\r\n" +
"D=M\r\n" +
"@SP\r\n" +
"M=D+1\r\n" +
//以LCL地址为基准,逐步做减一操作,获得调用者的切片并映射回去
"// restore scene\r\n" +
"@R13\r\n" +
"D=M\r\n" +
"@R15\r\n" +
"M=D\r\n" +
"\r\n" +
"@R15\r\n" +
"M=M-1\r\n" +
"A=M\r\n" +
"D=M\r\n" +
"@THAT\r\n" +
"M=D\r\n" +
"\r\n" +
"@R15\r\n" +
"M=M-1\r\n" +
"A=M\r\n" +
"D=M\r\n" +
"@THIS\r\n" +
"M=D\r\n" +
"\r\n" +
"@R15\r\n" +
"M=M-1\r\n" +
"A=M\r\n" +
"D=M\r\n" +
"@ARG\r\n" +
"M=D\r\n" +
"\r\n" +
"@R15\r\n" +
"M=M-1\r\n" +
"A=M\r\n" +
"D=M\r\n" +
"@LCL\r\n" +
"M=D\r\n" +
"\r\n" +
"// goto return address\r\n" +
"@R14\r\n" +
"A=M\r\n" +
"0;JMP\r\n");
this.writer.write("\r\n");
}
public void writeFunction(Parser parser) throws IOException {
// 获取两个参数
String arg1 = parser.args1();
String arg2 = parser.args2();
if(arg1.equals("Sys.init")) {
this.writer.write("// vm command: "+parser.getCurrentCommand()+"\r\n");
writeInit();
}else {
this.writer.write("// vm command: "+parser.getCurrentCommand()+"\r\n");
this.writer.write("("+arg1+")\r\n" +
"// initialize local segment\r\n" +
"@"+arg2+"\r\n" +
"D=A\r\n" +
"("+arg1+"$LOOP)\r\n" +
"D=D-1\r\n" +
"@"+arg1+"$END\r\n" +
"D;JLT\r\n");
pushValueIntoStack("0");
this.writer.write("@"+arg1+"$LOOP\r\n" +
"0;JMP\r\n" +
"("+arg1+"$END)\r\n");
this.writer.write("\r\n");
}
}
2.3.VMTranslator
/**
*实际上,我们只需要构造一个代码编写器,因为输出文件是一个代码编写器。
* 但是解析器定位文件,换句话说,每个文件都需要一个解析器。
* 因此,在这个类中,每个目录都需要构造一个 VM 转换器。
* 所以这门课是根据这个原则设计的。
* @param file
* @throws IOException
*/
/**
* 使用此函数开始编译文件。
* @param file
* @throws IOException
*/
public void doCompile(File file) throws IOException {
if(file.isFile()) {
this.parser = new Parser(file, new FileInputStream(file));
}else {
this.parseDirectory(file);
}
}
/**
* 阅读给定的目录。
* 这个方法将区分 Sys.vm 并做相关的工作。
* @param fileDirectory
* parsing file path
* @throws IOException
*/
private void parseDirectory(File fileDirectory) throws IOException {
File[] files = fileDirectory.listFiles();
ArrayList<File> VMFileList = new ArrayList<>();
// select the vm file from given directory
for(File i : files) {
String fileName = i.getName();
String laterName = fileName.substring(fileName.lastIndexOf(".")+1, fileName.length());
if(laterName.equals("vm")) {
VMFileList.add(i);
}else {
continue;
}
}
// firstly choose Sys.vm file to parse
for(File i : VMFileList) {
if(i.getName().equals("Sys.vm")) {
this.parseFile(i);
VMFileList.remove(i);
break;
}
}
// parse other vm file
for(File i : VMFileList) {
this.parseFile(i);
}
}
/**
* @param file the file needed to parse
* @throws IOException
*/
private void parseFile(File file) throws IOException {
this.parser = new Parser(file, new FileInputStream(file));
do {
this.doWrite();
}while(parser.hasMoreCommands());
}
/**
* Write assembly code. * @throws IOException
*/
private void doWrite() throws IOException {
// get a command
this.parser.advance();
CommandType commandType = this.parser.commandType();
if(commandType.equals(CommandType.C_ARITHMETIC)) {
codeWriter.writeArithmetic(this.parser);
}else if(commandType.equals(CommandType.C_PUSH) || commandType.equals(CommandType.C_POP)) {
codeWriter.writePushPop(this.parser);
}else if(commandType.equals(CommandType.C_CALL)) {
codeWriter.writeCall(this.parser);
}else if(commandType.equals(CommandType.C_FUNCTION)) {
codeWriter.writeFunction(this.parser);
}else if(commandType.equals(CommandType.C_GOTO)) {
codeWriter.writeGoto(this.parser);
}else if(commandType.equals(CommandType.C_IF)) {
codeWriter.writeIf(this.parser);
}else if(commandType.equals(CommandType.C_LABEL)) {
codeWriter.writeLabel(this.parser);
}else if(commandType.equals(CommandType.C_RETURN)) {
codeWriter.writeReturn(this.parser);
}else {
System.out.println("unknown command type!");
}
this.codeWriter.getWriter().flush();
}