背景
让我们按照 From Nand to Tetris 里 Project 6: Assembler 的要求,来设计一个 Assembler (汇编器) 。点击下图红色箭头所示位置,可以看到详细的描述。
《计算机系统要素 (第2版)》 一书的第 6 章也有相关描述。
正文
在 《计算机系统要素 (第2版)》 一书的第 79,80,81 页有如下描述
《计算机系统要素 (第2版)》 中第 6 章的内容,对我完成 Project 6 颇有帮助(但在本文中限于篇幅,不便一一截图)。如果读者朋友在实现 Project 6 遇到问题的话,可以参考
- 《计算机系统要素 (第2版)》 中第
6章的内容 - From Nand to Tetris 里的相关描述
后者的位置如下图红色箭头所示
我写的 java 代码
我在 Project 6 中,一共写了下列 5 class。
ParserCodeAssemblerSymbolTableInstructionType
它们都在 Assembler.java 中,Assembler.java 的具体内容如下
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
public class Assembler {
private final String filename;
private final Parser parser;
private final Code code;
private final SymbolTable symbolTable;
private final List<String> hackCodeLines;
public Assembler(String filename) throws Exception {
this.filename = filename;
parser = new Parser(filename);
code = new Code();
symbolTable = new SymbolTable();
hackCodeLines = new ArrayList<>();
}
/**
* Scan the first round to handle labels (i.e. {@link InstructionType#L_INSTRUCTION})
*/
private void scanFirstRound() {
int actualLineNum = 0;
while (parser.hasMoreLines()) {
parser.advance();
if (parser.instructionType() == InstructionType.L_INSTRUCTION) {
symbolTable.addEntry(parser.symbol(), actualLineNum);
} else {
actualLineNum++;
}
}
}
private void scanSecondRound() {
parser.reset();
while (parser.hasMoreLines()) {
parser.advance();
switch (parser.instructionType()) {
case A_INSTRUCTION -> hackCodeLines.add(translateAType());
case C_INSTRUCTION -> hackCodeLines.add(translateCType());
case L_INSTRUCTION -> {
// just do nothing
}
}
}
}
private int getAddress(String symbol) {
if (!symbolTable.contains(symbol)) {
symbolTable.addEntry(symbol, symbolTable.consumeAvailableAddress());
}
return symbolTable.getAddress(symbol);
}
private boolean isConstant(String symbol) {
char firstChar = symbol.charAt(0);
return firstChar >= '0' && firstChar <= '9';
}
private String translateAType() {
String symbol = parser.symbol();
if (isConstant(symbol)) {
return translateAType(Integer.parseInt(symbol));
}
int address = getAddress(symbol);
return translateAType(address);
}
private String translateAType(int value) {
String result = Integer.toBinaryString(value);
int lengthDiff = 16 - result.length();
return "0".repeat(lengthDiff) + result;
}
private String translateCType() {
String comp = code.comp(parser.comp());
String dest = code.dest(parser.dest());
String jump = code.jump(parser.jump());
return String.format("111%s%s%s", comp, dest, jump);
}
public List<String> translate() {
scanFirstRound();
scanSecondRound();
return hackCodeLines;
}
private Path buildOutputPath() {
var path = Paths.get(filename);
var asmFilename = path.toFile().getName();
var hackFilename = asmFilename.substring(0, asmFilename.length() - ".asm".length()) + ".hack";
var directory = path.toAbsolutePath().toFile().getParent();
return Paths.get(directory, hackFilename);
}
public static void main(String[] args) throws Exception {
var assembler = new Assembler(args[0]);
List<String> hackCodeLines = assembler.translate();
String allHackCode = String.join(System.lineSeparator(), hackCodeLines);
var outputPath = assembler.buildOutputPath();
Files.writeString(outputPath, allHackCode);
}
}
class Parser {
private final List<String> lines;
private int nextLineNum;
private final int totalLineNum;
private String currentLine;
private InstructionType currentType;
public Parser(String fileName) throws Exception {
try (var rawLines = Files.lines(Paths.get(fileName))) {
this.lines = preProcess(rawLines);
}
nextLineNum = 0;
totalLineNum = lines.size();
currentLine = null;
currentType = null;
}
public void reset() {
nextLineNum = 0;
currentLine = null;
currentType = null;
}
public boolean hasMoreLines() {
return nextLineNum < totalLineNum;
}
/**
* Pre-process
* 1. Skip empty lines and comment lines
* 2. Trim each line
*/
private List<String> preProcess(Stream<String> rawLines) {
return rawLines.map(String::trim)
.filter(line -> !line.isEmpty())
.filter(line -> !line.startsWith("//"))
.toList();
}
public void advance() {
if (!hasMoreLines()) {
throw new IllegalStateException("The file is already exhausted");
}
currentLine = lines.get(nextLineNum++);
currentType = instructionType();
}
public InstructionType instructionType() {
if (currentLine.startsWith("@")) {
return InstructionType.A_INSTRUCTION;
}
if (currentLine.startsWith("(")) {
return InstructionType.L_INSTRUCTION;
}
return InstructionType.C_INSTRUCTION;
}
public String symbol() {
if (!EnumSet.of(InstructionType.A_INSTRUCTION, InstructionType.L_INSTRUCTION).contains(currentType)) {
throw new IllegalStateException("The current instruction belongs to neither A type nor L type!");
}
if (currentLine.startsWith("(")) {
return currentLine.substring(1, currentLine.length() - 1).trim();
}
if (currentLine.startsWith("@")) {
return currentLine.substring(1);
}
throw new IllegalArgumentException("Unexpected line: " + currentLine);
}
public String dest() {
if (currentType != InstructionType.C_INSTRUCTION) {
throw new IllegalStateException("The current instruction doesn't belong to C type!");
}
int index = currentLine.indexOf("=");
if (index >= 0) {
return currentLine.substring(0, index);
}
return null;
}
public String comp() {
if (currentType != InstructionType.C_INSTRUCTION) {
throw new IllegalStateException("The current instruction doesn't belong to C type!");
}
int index1 = currentLine.indexOf("=");
int index2 = currentLine.indexOf(";");
if (index1 < 0 && index2 < 0) {
return currentLine;
}
if (index1 < 0) {
return currentLine.substring(0, index2);
}
if (index2 < 0) {
return currentLine.substring(index1 + 1);
}
return currentLine.substring(index1 + 1, index2);
}
public String jump() {
if (currentType != InstructionType.C_INSTRUCTION) {
throw new IllegalStateException("The current instruction doesn't belong to C type!");
}
int index = currentLine.indexOf(";");
if (index >= 0) {
return currentLine.substring(index + 1);
}
return null;
}
}
class Code {
public String dest(String rawDest) {
if (rawDest == null) {
return "000";
}
char[] cs = new char[]{'0', '0', '0'};
for (char item : rawDest.toCharArray()) {
switch (item) {
case 'A' -> cs[0] = '1';
case 'D' -> cs[1] = '1';
case 'M' -> cs[2] = '1';
default -> throw new IllegalArgumentException("Unexpected dest: " + rawDest);
}
}
return new String(cs);
}
public String comp(String rawComp) {
int a = (rawComp.contains("M")) ? 1 : 0;
return a + switch (rawComp) {
case "0" -> "101010";
case "1" -> "111111";
case "-1" -> "111010";
case "D" -> "001100";
case "A", "M" -> "110000";
case "!D" -> "001101";
case "!A", "!M" -> "110001";
case "-D" -> "001111";
case "-A", "-M" -> "110011";
case "D+1" -> "011111";
case "A+1", "M+1" -> "110111";
case "D-1" -> "001110";
case "A-1", "M-1" -> "110010";
case "D+A", "D+M" -> "000010";
case "D-A", "D-M" -> "010011";
case "A-D", "M-D" -> "000111";
case "D&A", "D&M" -> "000000";
case "D|A", "D|M" -> "010101";
default -> throw new IllegalArgumentException("Unexpected comp: " + rawComp);
};
}
public String jump(String rawJump) {
if (rawJump == null) {
return "000";
}
return switch (rawJump) {
case "JGT" -> "001";
case "JEQ" -> "010";
case "JGE" -> "011";
case "JLT" -> "100";
case "JNE" -> "101";
case "JLE" -> "110";
case "JMP" -> "111";
default -> throw new IllegalArgumentException("Unexpected jump: " + rawJump);
};
}
}
class SymbolTable {
private final Map<String, Integer> table;
private int nextAvailableAddress;
public SymbolTable() {
table = new HashMap<>();
init();
}
/**
* Put pre-defined entries into the symbol table
*/
private void init() {
for (int i = 0; i < 16; i++) {
addEntry("R" + i, i);
}
addEntry("SP", 0);
addEntry("LCL", 1);
addEntry("ARG", 2);
addEntry("THIS", 3);
addEntry("THAT", 4);
addEntry("SCREEN", 16384);
addEntry("KBD", 24576);
nextAvailableAddress = 16;
}
public void addEntry(String symbol, int address) {
if (table.containsKey(symbol)) {
String message = String.format("Symbol table already contains symbol: %s!", symbol);
throw new IllegalArgumentException(message);
}
table.put(symbol, address);
}
public int consumeAvailableAddress() {
return nextAvailableAddress++;
}
public boolean contains(String symbol) {
return table.containsKey(symbol);
}
public int getAddress(String symbol) {
return table.get(symbol);
}
}
enum InstructionType {
A_INSTRUCTION,
C_INSTRUCTION,
L_INSTRUCTION
}
1. Parser
Parser (分析器)的整体设计参考了 《计算机系统要素 (第2版)》 一书第 77,78 页的相关描述 ⬇️
Parser 中各个方法的简介如下表所示
| 方法 | 简介 |
|---|---|
构造函数 Parser(String) | 略 |
reset() | 重置 Parser,以便进行第二遍扫描 |
hasMoreLines() | 判断输入是否还有更多行 |
preProcess(Stream<String>) | 对输入进行预处理 1. 将其中的空白行和注释行移除 2. 进行 trim 操作 |
advance() | 从输入中读取下一条指令,并将其设置为当前指令 |
instructionType() | 返回当前指令的类型 |
symbol() | 1. 如果当前指令是 ,则返回符号 。 2.如果当前指令是 ,则将该符号或十进制数 (作为字符串)返回。 |
dest() | 返回当前 C 指令的 dest 部分 |
comp() | 返回当前 C 指令的 comp 部分 |
jump() | 返回当前 C 指令的 jump 部分 |
2. Code
Code 模块的整体设计参考了 《计算机系统要素 (第2版)》 一书第 78,79 页的相关描述 ⬇️
Code 中各个方法的简介如下表所示
| 方法 | 简介 |
|---|---|
dest(String) | 返回 dest 助记符的二进制编码 |
comp(String) | 返回 comp 助记符的二进制编码 |
jump(String) | 返回 comp 助记符的二进制编码 |
3. Assembler
Assembler (汇编器)的整体设计参考了 《计算机系统要素 (第2版)》 一书第 79 页的相关描述 ⬇️
Assembler 中各个方法的简介如下表所示
| 方法 | 简介 |
|---|---|
构造函数 Assembler(String) | 略 |
scanFirstRound() | 进行第一遍扫描。在这一轮扫描中,会处理 label |
scanSecondRound() | 进行第二遍扫描。 |
getAddress(String) | 获取 symbol 对应的 address |
isConstant(String) | 判断输入是否是常量(即 指令中的 是否为常量) |
translateAType() | 将 A 类型指令翻译为 hack 指令 |
translateAType(int) | 将 A 类型指令翻译为 hack 指令 |
translateCType() | 将 C 类型指令翻译为 hack 指令 |
translate() | 将 asm 指令全部翻译为 hack 指令 |
buildOutputPath() | 生成和输出文件对应的 Path |
main(String[]) | main 方法,Assembler 的入口 |
4. SymbolTable
SymbolTable (符号表)的整体设计参考了 《计算机系统要素 (第2版)》 一书第 79 页的相关描述 ⬇️
SymbolTable 中各个方法的简介如下表所示
| 方法 | 简介 |
|---|---|
构造函数 SymbolTable() | 略 |
init() | 将预定义的 entry 保存在 SymbolTable 中 |
addEntry(String, int) | 向 SymbolTable 中添加一个 条目 |
consumeAvailableAddress() | 返回当前可用的最小可用地址,并令最小可用地址加 1 |
contains(String) | 判断 symbol 是否在 SymbolTable 中 |
getAddress(String) | 返回与 symbol 相关联的地址 |
5. InstructionType
InstructionType 里定义了指令的类型 ⬇️
有了 Assembler.java 文件后,就可以对 asm 程序进行汇编操作了。
验证
Add.asm
前往 nand2tetris.github.io/web-ide/asm ,点击 Load file 按钮
选择 projects/06 中的 Add.asm
点击 Translate all 按钮
之后就能看到对应的 hack 代码 ⬇️
对 Add.asm 进行 download 操作,然后将其移动到和 java 文件相同的目录,再执行如下命令
javac Assembler.java
java Assembler Add.asm
之后应该会生成 Add.hack 文件。
在 Compare code 面板中,可以选择本地文件
选择本地的 Add.hack 文件之后,再点击 Compare,之后应该可以看到 Comparison successful 的提示 ⬇️
Max.asm
操作步骤和 Add.asm 类似,这里就不赘述了,下图是对比结果 ⬇️
MaxL.asm
操作步骤和 Add.asm 类似,下图是对比结果 ⬇️
Pong.asm
操作步骤和 Add.asm 类似,但是 Pong.asm 很长,可以在 Translate all 之前,将 Execution speed 按钮移动到最右侧 ⬇️
下图是最终的对比结果 ⬇️
PongL.asm
操作步骤和 Add.asm 类似,但是 PongL.asm 很长,可以在 Translate all 之前,将 Execution speed 按钮移动到最右侧 ⬇️
下图是最终的对比结果 ⬇️
Rect.asm
操作步骤和 Add.asm 类似,下图是对比结果 ⬇️
RectL.asm
操作步骤和 Add.asm 类似,下图是对比结果 ⬇️