Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
1096 lines (996 sloc)
30.4 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Complete VM Translator for the Nand2Tetris Hack Computer | |
// Author: Leo Robinovitch | |
use std::fs; | |
use std::path::Path; | |
use std::path::PathBuf; | |
use std::io::BufReader; | |
use std::io::prelude::*; | |
use std::env; | |
use regex::Regex; | |
#[macro_use] | |
extern crate lazy_static; | |
// create an enum type called CommandType | |
// that implements the Debug, etc. traits | |
// (printing with {:?} tells type) | |
#[derive(PartialEq, Eq, Debug)] | |
enum CommandType { | |
CArithmetic, | |
CPush, | |
CPop, | |
CLabel, | |
CGoTo, | |
CIfGoTo, | |
CFunction, | |
CReturn, | |
CCall | |
} | |
#[derive(PartialEq, Eq, Debug)] | |
enum SegType { | |
SConstant, | |
SLocal, | |
SArgument, | |
SThis, | |
SThat, | |
STemp, | |
SPointer, | |
SStatic | |
} | |
/// Returns a String of the file contents at path | |
/// Note: path is referenced from the root directory of the project | |
/// | |
/// # Arguments | |
/// | |
/// * `path` - A std::path::Path that contains the input file path | |
/// * `extension` - required extension for file | |
fn get_file_contents(path: &PathBuf, extension: &str) -> String { | |
assert_eq!(path.extension().unwrap(), extension); | |
let file = fs::File::open(path).expect("Failed to open file"); | |
let mut buf_reader = BufReader::new(file); // buffer reader for file | |
let mut contents = String::new(); // mutable string variable for contents | |
buf_reader.read_to_string(&mut contents) // mutable reference to contents | |
.expect("Error reading to string"); | |
contents // returns contents | |
} | |
/// Create and return writable file based on path | |
/// | |
/// # Arguments | |
/// | |
/// * `path` | |
fn create_file(path: &Path) -> fs::File { | |
fs::File::create(&path).unwrap() | |
} | |
/// Write line to file | |
/// | |
/// # Arguments | |
/// | |
/// * `file` - writable file | |
/// * `line` - line to write to file | |
fn write_to_file(mut file: &fs::File, line: String) { | |
// Write the `LOREM_IPSUM` string to `file`, returns `io::Result<()>` | |
file.write_all(format!("{}\n", line).as_bytes()) | |
.expect("Failed to write line to file!"); | |
} | |
/// Get .vm extension filepaths in a directory. If .vm file specified, wraps | |
/// input filename in vector. | |
/// | |
/// # Arguments | |
/// | |
/// * vector of pathbufs in dir with .vm extensions | |
fn get_vm_filepaths(dir: &str) -> Vec<PathBuf>{ | |
match PathBuf::from(dir).extension() { | |
Some(ext) => { | |
if ext == "vm" { | |
return vec![PathBuf::from(dir)]; | |
} else { | |
panic!("Cannot translate non-.vm file"); | |
} | |
} | |
_ => { | |
let paths = fs::read_dir(dir).unwrap(); | |
let mut vm_paths: Vec<PathBuf> = Vec::new(); | |
for direntry in paths { | |
println!("{:?}", direntry); | |
let path = direntry.unwrap().path(); | |
if path.extension().unwrap() == "vm" { | |
println!("{:?}", path); | |
vm_paths.push(path); | |
} | |
} | |
vm_paths | |
} | |
} | |
} | |
/// Parse command line arguments and return input file | |
/// contents and output file to write to | |
fn parse_args() -> (Vec<String>, Vec<String>, fs::File, String) { | |
// get user args | |
let args: Vec<String> = env::args().collect(); | |
// check user args | |
if args.len() < 2 { | |
println!("\nMissing required argument"); | |
println!("Usage: cargo run FILENAME\n"); | |
panic!(); | |
}; | |
// .vm file contents | |
let vm_filepaths = get_vm_filepaths(&args[1]); | |
let mut file_contents: Vec<String> = Vec::new(); | |
let mut input_files: Vec<String> = Vec::new(); | |
for filepath in &vm_filepaths { | |
file_contents.push(get_file_contents(&filepath, "vm")); | |
input_files.push(filepath.as_path().to_str().unwrap().to_string()); | |
} | |
// output file | |
let out_path_str = match vm_filepaths.len() { | |
0 => { | |
panic!("No .vm files to translate found") | |
}, | |
_ => { | |
let dir = PathBuf::from(&args[1]); | |
let filename = dir.join(dir.file_name().unwrap()); | |
let mut out_path_str = String::from(filename.to_str().unwrap()); | |
out_path_str.push_str(&".asm"); | |
out_path_str | |
} | |
}; | |
let out_path = Path::new(&out_path_str); | |
let output_file = create_file(out_path); | |
(file_contents, input_files, output_file, out_path_str) | |
} | |
/// Returns a cleaned string slice after removing comments and white space | |
/// | |
/// # Arguments | |
/// | |
/// * `line` - the raw line | |
fn remove_comments(line: &str) -> &str { | |
// find the index where comments begin on the line | |
let idx_comment = match line.find("//") { | |
Some(idx) => idx, | |
_ => line.len() | |
}; | |
// return a reference to the reduced str with no start/end whitespace | |
// note that memory contents are the same, just pointer and/or len changed | |
&line[0..idx_comment].trim() | |
} | |
#[test] | |
fn test_stripped_line() { | |
assert_eq!("", remove_comments("")); | |
assert_eq!("", remove_comments(" ")); | |
assert_eq!("", remove_comments("// ")); | |
assert_eq!("nand2tetris", remove_comments("nand2tetris // is so cool")); | |
assert_eq!("nand2tetris is so cool", | |
remove_comments("nand2tetris is so cool // eh?")); | |
} | |
/// Checks if VM code line is one of the 9 possible | |
/// CArithmetic commands | |
/// | |
/// # Arguments | |
/// | |
/// * `line` - string slice that holds possible arithmetic command | |
fn is_arithmetic(line: &str) -> bool { | |
lazy_static! { // lazy_static ensures compilation only happens once | |
static ref RE : Regex = Regex::new( | |
r"^(add|sub|neg|eq|gt|lt|and|or|not)$" | |
).unwrap(); | |
}; | |
RE.captures(line).is_some() | |
} | |
#[test] | |
fn test_is_arithmetic() { | |
assert_eq!(true, is_arithmetic("add")); | |
assert_eq!(true, is_arithmetic("sub")); | |
assert_eq!(true, is_arithmetic("neg")); | |
assert_eq!(true, is_arithmetic("eq")); | |
assert_eq!(true, is_arithmetic("gt")); | |
assert_eq!(true, is_arithmetic("lt")); | |
assert_eq!(true, is_arithmetic("and")); | |
assert_eq!(true, is_arithmetic("or")); | |
assert_eq!(true, is_arithmetic("not")); | |
assert_eq!(false, is_arithmetic("butt")); | |
assert_eq!(false, is_arithmetic("add sub")); | |
} | |
/// Gets command type for VM code line | |
/// | |
/// # Arguments | |
/// | |
/// * `line` - string slice that holds the current line | |
fn get_command_type(line: &str) -> CommandType { | |
if is_arithmetic(line) { | |
CommandType::CArithmetic | |
} else if &line[0..3] == "pop" { | |
CommandType::CPop | |
} else if &line[0..4] == "push" { | |
CommandType::CPush | |
} else if &line[0..4] == "goto"{ | |
CommandType::CGoTo | |
} else if &line[0..4] == "call"{ | |
CommandType::CCall | |
} else if &line[0..5] == "label" { | |
CommandType::CLabel | |
} else if &line[0..6] == "return"{ | |
CommandType::CReturn | |
} else if &line[0..7] == "if-goto"{ | |
CommandType::CIfGoTo | |
} else if &line[0..8] == "function"{ | |
CommandType::CFunction | |
} else { | |
panic!("Line not a valid command!"); | |
} | |
} | |
#[test] | |
fn test_get_command_type() { | |
assert_eq!(CommandType::CPush, get_command_type("push constant 1")); | |
assert_eq!(CommandType::CPop, get_command_type("pop constant 1")); | |
assert_eq!(CommandType::CArithmetic, get_command_type("eq")); | |
} | |
/// Get segment and index from push/pop command | |
/// | |
/// # Arguments | |
/// | |
/// * `line` - push/pop command | |
fn parse_push_pop(line: &str) -> (SegType, i32) { | |
lazy_static! { // lazy_static ensures compilation only happens once | |
static ref RE : Regex = Regex::new( | |
r"^(push|pop)\s*(\w*)\s*(\d*)" // TO DO may need refining | |
).unwrap(); | |
}; | |
let capture = RE.captures(line) | |
.expect("Invalid push/pop command!"); | |
let segment = match capture.get(2).unwrap().as_str() { | |
"constant" => SegType::SConstant, | |
"local" => SegType::SLocal, | |
"argument" => SegType::SArgument, | |
"this" => SegType::SThis, | |
"that" => SegType::SThat, | |
"temp" => SegType::STemp, | |
"pointer" => SegType::SPointer, | |
"static" => SegType::SStatic, | |
_ => panic!("Invalid segment") | |
}; | |
let index = capture.get(3).unwrap().as_str().parse::<i32>().unwrap(); | |
(segment, index) | |
} | |
#[test] | |
fn test_parse_push_pop() { | |
assert_eq!((SegType::SStatic, 1), parse_push_pop("push static 1")); | |
assert_eq!((SegType::SLocal, 10), parse_push_pop("pop local 10")); | |
let result = std::panic::catch_unwind(|| parse_push_pop("eq")); // % is invalid | |
assert!(result.is_err()); | |
} | |
/// Get file name from path | |
/// | |
/// # Arguments | |
/// | |
/// * `path` - path | |
fn get_file_name(path: &str) -> String { | |
match path.rfind('/') { | |
Some(idx_slash) => { | |
match path.rfind('.') { | |
Some(idx_dot) => path[idx_slash+1..idx_dot].to_string(), | |
_ => path.to_string() | |
} | |
}, | |
_ => { | |
match path.rfind('.') { | |
Some(idx_dot) => path[..idx_dot].to_string(), | |
_ => path.to_string() | |
} | |
} | |
} | |
} | |
#[test] | |
fn test_get_file_name() { | |
assert_eq!("test", get_file_name("/asdfasdf/asdfasdf/test.vm")); | |
assert_eq!("test", get_file_name("/test.vm")); | |
assert_eq!("test", get_file_name("test.vm")); | |
assert_eq!("test", get_file_name("test")); | |
} | |
/// Write assembly code for push commands | |
/// | |
/// # Arguments | |
/// | |
/// * `file` - output file | |
/// * `input_filename` - input filename, used for static vars | |
/// * `line` - push command | |
/// * `segment` - memory segment | |
/// * `index` | |
fn write_push(file: &fs::File, input_filename: &str, line: &str, segment: SegType, index: i32) { | |
match segment { | |
SegType::SConstant => { | |
// in constant segment, index is treated as value to push on to stack | |
let asm_code = format!("// {line}\n\ | |
@{index}\n\ | |
D=A\n\ | |
@SP\n\ | |
A=M\n\ | |
M=D\n\ | |
@SP\n\ | |
M=M+1", line=line, index=index); | |
write_to_file(file, asm_code) | |
}, | |
SegType::SLocal => { | |
// in local segment, push puts index in to local memory on to stack | |
let asm_code = format!("// {line}\n\ | |
@{index}\n\ | |
D=A\n\ | |
@LCL\n\ | |
A=M\n\ | |
A=D+A\n\ | |
D=M\n\ | |
@SP\n\ | |
A=M\n\ | |
M=D\n\ | |
@SP\n\ | |
M=M+1", line=line, index=index); | |
write_to_file(file, asm_code) | |
}, | |
SegType::SArgument => { | |
// in argument segment, push puts index in to argument memory on to stack | |
let asm_code = format!("// {line}\n\ | |
@{index}\n\ | |
D=A\n\ | |
@ARG\n\ | |
A=M\n\ | |
A=D+A\n\ | |
D=M\n\ | |
@SP\n\ | |
A=M\n\ | |
M=D\n\ | |
@SP\n\ | |
M=M+1", line=line, index=index); | |
write_to_file(file, asm_code) | |
}, | |
SegType::SThis => { | |
// in this segment, push puts index in to this memory on to stack | |
let asm_code = format!("// {line}\n\ | |
@{index}\n\ | |
D=A\n\ | |
@THIS\n\ | |
A=M\n\ | |
A=D+A\n\ | |
D=M\n\ | |
@SP\n\ | |
A=M\n\ | |
M=D\n\ | |
@SP\n\ | |
M=M+1", line=line, index=index); | |
write_to_file(file, asm_code) | |
}, | |
SegType::SThat => { | |
// in that segment, push puts index in to that memory on to stack | |
let asm_code = format!("// {line}\n\ | |
@{index}\n\ | |
D=A\n\ | |
@THAT\n\ | |
A=M\n\ | |
A=D+A\n\ | |
D=M\n\ | |
@SP\n\ | |
A=M\n\ | |
M=D\n\ | |
@SP\n\ | |
M=M+1", line=line, index=index); | |
write_to_file(file, asm_code) | |
}, | |
SegType::STemp => { | |
// in temp segment, push puts index in to temp memory on to stack | |
let asm_code = format!("// {line}\n\ | |
@{index}\n\ | |
D=A\n\ | |
@5\n\ | |
A=D+A\n\ | |
D=M\n\ | |
@SP\n\ | |
A=M\n\ | |
M=D\n\ | |
@SP\n\ | |
M=M+1", line=line, index=index); | |
write_to_file(file, asm_code) | |
}, | |
SegType::SPointer => { | |
// in pointer segment, push puts index in to pointer memory on to stack | |
let asm_code = format!("// {line}\n\ | |
@{index}\n\ | |
D=A\n\ | |
@3\n\ | |
A=D+A\n\ | |
D=M\n\ | |
@SP\n\ | |
A=M\n\ | |
M=D\n\ | |
@SP\n\ | |
M=M+1", line=line, index=index); | |
write_to_file(file, asm_code) | |
}, | |
SegType::SStatic => { | |
// in static segment, push puts index in to static memory on to stack | |
let asm_code = format!("// {line}\n\ | |
@{input_filename}.{index}\n\ | |
D=M\n\ | |
@SP\n\ | |
A=M\n\ | |
M=D\n\ | |
@SP\n\ | |
M=M+1", line=line, input_filename=input_filename, index=index); | |
write_to_file(file, asm_code) | |
} | |
} | |
} | |
// Write assembly code for pop commands | |
/// | |
/// # Arguments | |
/// | |
/// * `file` - output file | |
/// * `input_filename` - input filename, used for static vars | |
/// * `line` - pop command | |
/// * `segment` - memory segment | |
/// * `index` | |
fn write_pop(file: &fs::File, input_filename: &str, line: &str, segment: SegType, index: i32) { | |
match segment { | |
SegType::SConstant => { | |
// in constant segment, pop should not be implemented | |
panic!("pop constant command is invalid"); | |
}, | |
SegType::SLocal => { | |
// in local segment, pop puts top of stack at index in to local memory | |
let asm_code = format!("// {line}\n\ | |
@{index}\n\ | |
D=A\n\ | |
@LCL\n\ | |
A=M\n\ | |
D=D+A\n\ | |
@R13\n\ | |
M=D\n\ | |
@SP\n\ | |
AM=M-1\n\ | |
D=M\n\ | |
@R13\n\ | |
A=M\n\ | |
M=D", line=line, index=index); | |
write_to_file(file, asm_code) | |
}, | |
SegType::SArgument => { | |
// in argument segment, pop puts top of stack at index in to argument memory | |
let asm_code = format!("// {line}\n\ | |
@{index}\n\ | |
D=A\n\ | |
@ARG\n\ | |
A=M\n\ | |
D=D+A\n\ | |
@R13\n\ | |
M=D\n\ | |
@SP\n\ | |
AM=M-1\n\ | |
D=M\n\ | |
@R13\n\ | |
A=M\n\ | |
M=D", line=line, index=index); | |
write_to_file(file, asm_code) | |
}, | |
SegType::SThis => { | |
// in this segment, pop puts top of stack at index in to this memory | |
let asm_code = format!("// {line}\n\ | |
@{index}\n\ | |
D=A\n\ | |
@THIS\n\ | |
A=M\n\ | |
D=D+A\n\ | |
@R13\n\ | |
M=D\n\ | |
@SP\n\ | |
AM=M-1\n\ | |
D=M\n\ | |
@R13\n\ | |
A=M\n\ | |
M=D", line=line, index=index); | |
write_to_file(file, asm_code) | |
}, | |
SegType::SThat => { | |
// in that segment, pop puts top of stack at index in to that memory | |
let asm_code = format!("// {line}\n\ | |
@{index}\n\ | |
D=A\n\ | |
@THAT\n\ | |
A=M\n\ | |
D=D+A\n\ | |
@R13\n\ | |
M=D\n\ | |
@SP\n\ | |
AM=M-1\n\ | |
D=M\n\ | |
@R13\n\ | |
A=M\n\ | |
M=D", line=line, index=index); | |
write_to_file(file, asm_code) | |
}, | |
SegType::STemp => { | |
// in temp segment, pop puts top of stack at index in to temp memory | |
let asm_code = format!("// {line}\n\ | |
@{index}\n\ | |
D=A\n\ | |
@5\n\ | |
D=D+A\n\ | |
@R13\n\ | |
M=D\n\ | |
@SP\n\ | |
AM=M-1\n\ | |
D=M\n\ | |
@R13\n\ | |
A=M\n\ | |
M=D", line=line, index=index); | |
write_to_file(file, asm_code) | |
}, | |
SegType::SPointer => { | |
// in pointer segment, pop puts top of stack at index in to pointer memory | |
let asm_code = format!("// {line}\n\ | |
@{index}\n\ | |
D=A\n\ | |
@3\n\ | |
D=D+A\n\ | |
@R13\n\ | |
M=D\n\ | |
@SP\n\ | |
AM=M-1\n\ | |
D=M\n\ | |
@R13\n\ | |
A=M\n\ | |
M=D", line=line, index=index); | |
write_to_file(file, asm_code) | |
}, | |
SegType::SStatic => { | |
// in static segment, pop puts top of stack at index in to static memory | |
let asm_code = format!("// {line}\n\ | |
@SP\n\ | |
AM=M-1\n\ | |
D=M\n\ | |
@{input_filename}.{index}\n\ | |
M=D", line=line, input_filename=input_filename, index=index); | |
write_to_file(file, asm_code) | |
} | |
} | |
} | |
/// Writes assembly code for arithmetic commands to output file | |
/// | |
/// # Arguments | |
/// | |
/// * `file` - output file | |
/// * `line` - input arithmetic command ("add", "sub", "eq", etc.) | |
/// * `cmp_count` - count of previous comparison operations ("eq", "lt", and "gt"). | |
/// Used to mark jump and continue locations in these operations such that each | |
/// label is unique. | |
fn write_arithmetic(file: &fs::File, line: &str, cmp_count: i32) { | |
match line { | |
"add" => { | |
let asm_code = format!("// {line}\n\ | |
@SP\n\ | |
AM=M-1\n\ | |
D=M\n\ | |
@SP\n\ | |
A=M-1\n\ | |
M=D+M", line=line); | |
write_to_file(file, asm_code) | |
}, | |
"sub" => { | |
let asm_code = format!("// {line}\n\ | |
@SP\n\ | |
AM=M-1\n\ | |
D=M\n\ | |
@SP\n\ | |
A=M-1\n\ | |
M=M-D", line=line); | |
write_to_file(file, asm_code) | |
}, | |
"neg" => { | |
let asm_code = format!("// {line}\n\ | |
@SP\n\ | |
A=M-1\n\ | |
M=-M", line=line); | |
write_to_file(file, asm_code) | |
}, | |
"eq" => { | |
let asm_code = format!("// {line}\n\ | |
@SP\n\ | |
AM=M-1\n\ | |
D=M\n\ | |
@SP\n\ | |
A=M-1\n\ | |
D=M-D\n\ | |
@EQUAL{cmp_count}\n\ | |
D;JEQ\n\ | |
@SP\n\ | |
A=M-1\n\ | |
M=0\n\ | |
@CONTINUE{cmp_count}\n\ | |
0;JMP\n\ | |
(EQUAL{cmp_count})\n\ | |
@SP\n\ | |
A=M-1\n\ | |
M=-1\n\ | |
(CONTINUE{cmp_count})", line=line, cmp_count=cmp_count); | |
write_to_file(file, asm_code) | |
}, | |
"gt" => { | |
let asm_code = format!("// {line}\n\ | |
@SP\n\ | |
AM=M-1\n\ | |
D=M\n\ | |
@SP\n\ | |
A=M-1\n\ | |
D=M-D\n\ | |
@GT{cmp_count}\n\ | |
D;JGT\n\ | |
@SP\n\ | |
A=M-1\n\ | |
M=0\n\ | |
@CONTINUE{cmp_count}\n\ | |
0;JMP\n\ | |
(GT{cmp_count})\n\ | |
@SP\n\ | |
A=M-1\n\ | |
M=-1\n\ | |
(CONTINUE{cmp_count})", line=line, cmp_count=cmp_count); | |
write_to_file(file, asm_code) | |
}, | |
"lt" => { | |
let asm_code = format!("// {line}\n\ | |
@SP\n\ | |
AM=M-1\n\ | |
D=M\n\ | |
@SP\n\ | |
A=M-1\n\ | |
D=M-D\n\ | |
@LT{cmp_count}\n\ | |
D;JLT\n\ | |
@SP\n\ | |
A=M-1\n\ | |
M=0\n\ | |
@CONTINUE{cmp_count}\n\ | |
0;JMP\n\ | |
(LT{cmp_count})\n\ | |
@SP\n\ | |
A=M-1\n\ | |
M=-1\n\ | |
(CONTINUE{cmp_count})", line=line, cmp_count=cmp_count); | |
write_to_file(file, asm_code) | |
}, | |
"and" => { | |
let asm_code = format!("// {line}\n\ | |
@SP\n\ | |
M=M-1\n\ | |
A=M\n\ | |
D=M\n\ | |
@SP\n\ | |
A=M-1\n\ | |
M=D&M", line=line); | |
write_to_file(file, asm_code) | |
}, | |
"or" => { | |
let asm_code = format!("// {line}\n\ | |
@SP\n\ | |
M=M-1\n\ | |
A=M\n\ | |
D=M\n\ | |
@SP\n\ | |
A=M-1\n\ | |
M=D|M", line=line); | |
write_to_file(file, asm_code) | |
}, | |
"not" => { | |
let asm_code = format!("// {line}\n\ | |
@SP\n\ | |
A=M-1\n\ | |
M=!M", line=line); | |
write_to_file(file, asm_code) | |
}, | |
_ => {} | |
} | |
} | |
/// Writes assembly code for label commands to output file | |
/// TODO: check if need to prepend filename or function? | |
/// | |
/// # Arguments | |
/// | |
/// * `file` - output file | |
/// * `line` - input label command | |
fn write_label(file: &fs::File, line: &str) { | |
lazy_static! { // lazy_static ensures compilation only happens once | |
static ref RE : Regex = Regex::new( | |
r"^label ([a-zA-Z0-9._:]+)$" | |
).unwrap(); | |
}; | |
let capture = RE.captures(line) | |
.expect("Invalid label command!"); | |
let label = capture.get(1).unwrap().as_str(); | |
let asm_code = format!("// {line}\n\ | |
({label})", line=line, label=label); | |
write_to_file(file, asm_code); | |
} | |
/// Writes assembly code for unconditional goto commands to output file | |
/// | |
/// # Arguments | |
/// | |
/// * `file` - output file | |
/// * `line` - input unconditional goto command | |
fn write_goto(file: &fs::File, line: &str) { | |
lazy_static! { // lazy_static ensures compilation only happens once | |
static ref RE : Regex = Regex::new( | |
r"^goto ([a-zA-Z0-9._:]+)$" | |
).unwrap(); | |
}; | |
let capture = RE.captures(line) | |
.expect("Invalid goto command!"); | |
let label = capture.get(1).unwrap().as_str(); | |
let asm_code = format!("// {line}\n\ | |
@{label}\n\ | |
0;JMP", line=line, label=label); | |
write_to_file(file, asm_code); | |
} | |
/// Writes assembly code for conditional goto commands to output file | |
/// | |
/// # Arguments | |
/// | |
/// * `file` - output file | |
/// * `line` - input unconditional goto command | |
fn write_ifgoto(file: &fs::File, line: &str) { | |
lazy_static! { // lazy_static ensures compilation only happens once | |
static ref RE : Regex = Regex::new( | |
r"^if-goto ([a-zA-Z0-9._:]+)$" | |
).unwrap(); | |
}; | |
let capture = RE.captures(line) | |
.expect("Invalid if-goto command!"); | |
let label = capture.get(1).unwrap().as_str(); | |
let asm_code = format!("// {line}\n\ | |
@SP\n\ | |
AM=M-1\n\ | |
D=M\n\ | |
@{label}\n\ | |
D;JNE", line=line, label=label); | |
write_to_file(file, asm_code); | |
} | |
/// Writes assembly code for function declarations to output file | |
/// | |
/// # Arguments | |
/// | |
/// * `file` - output file | |
/// * `line` - input unconditional goto command | |
fn write_function(file: &fs::File, line: &str) { | |
lazy_static! { // lazy_static ensures compilation only happens once | |
static ref RE : Regex = Regex::new( | |
r"^function ([a-zA-Z0-9._:]+) ([0-9]+)$" | |
).unwrap(); | |
}; | |
let capture = RE.captures(line) | |
.expect("Invalid function command!"); | |
let func_name = capture.get(1).unwrap().as_str(); | |
let n_locals = capture.get(2).unwrap().as_str().parse::<i32>().unwrap(); | |
let mut asm_code = format!("// {line}\n\ | |
({func_name})", line=line, func_name=func_name); | |
let push_0_asy = "\n@SP\n\ | |
A=M\n\ | |
M=0\n\ | |
@SP\n\ | |
M=M+1"; | |
for _ in 0..n_locals { | |
asm_code.push_str(push_0_asy); | |
} | |
write_to_file(file, asm_code); | |
} | |
/// Writes assembly code for return statement to output file | |
/// | |
/// # Arguments | |
/// | |
/// * `file` - output file | |
/// * `line` - input unconditional goto command | |
fn write_return(file: &fs::File, line: &str) { | |
let asm_code = format!("// {line}\n\ | |
// FRAME = LCL = M[R13]\n\ | |
@LCL\n\ | |
D=M // D = M[LCL]\n\ | |
@R13\n\ | |
M=D // M[R13] = M[LCL]\n\ | |
// RET = *(FRAME-5) = M[R14]\n\ | |
@5\n\ | |
D=A // D = 5\n\ | |
@R13\n\ | |
A=M-D // A = LCL - 5\n\ | |
D=M // D = M[LCL-5]\n\ | |
@R14\n\ | |
M=D // M[R14] = M[LCL-5]\n\ | |
// *ARG = pop()\n\ | |
@SP\n\ | |
AM=M-1\n\ | |
D=M // D = pop()\n\ | |
@ARG\n\ | |
A=M\n\ | |
M=D\n\ | |
// SP = ARG+1\n\ | |
D=A+1\n\ | |
@SP\n\ | |
M=D // M[SP] = M[ARG] + 1\n\ | |
// THAT = *(FRAME-1)\n\ | |
@R13\n\ | |
D=M // D = M[R13] = LCL\n\ | |
@1\n\ | |
A=D-A // A = LCL - 1\n\ | |
D=M // D = M[LCL - 1]\n\ | |
@THAT\n\ | |
M=D\n\ | |
// THIS = *(FRAME-2)\n\ | |
@R13\n\ | |
D=M // D = M[R13] = LCL\n\ | |
@2\n\ | |
A=D-A // A = LCL - 2\n\ | |
D=M // D = M[LCL - 2]\n\ | |
@THIS\n\ | |
M=D\n\ | |
// ARG = *(FRAME-3)\n\ | |
@R13\n\ | |
D=M // D = M[R13] = LCL\n\ | |
@3\n\ | |
A=D-A // A = LCL - 3\n\ | |
D=M // D = M[LCL - 3]\n\ | |
@ARG\n\ | |
M=D\n\ | |
// LCL = *(FRAME-4)\n\ | |
@R13\n\ | |
D=M // D = M[R13] = LCL\n\ | |
@4\n\ | |
A=D-A // A = LCL - 4\n\ | |
D=M // D = M[LCL - 4]\n\ | |
@LCL\n\ | |
M=D\n\ | |
// goto RET\n\ | |
@R14\n\ | |
A=M // A = M[R14] = RET\n\ | |
0;JMP", line=line); | |
write_to_file(file, asm_code); | |
} | |
/// Writes assembly code for call statement to output file | |
/// | |
/// # Arguments | |
/// | |
/// * `file` - output file | |
/// * `line` - input unconditional goto command | |
fn write_call(file: &fs::File, line: &str, call_count: i32) { | |
lazy_static! { // lazy_static ensures compilation only happens once | |
static ref RE : Regex = Regex::new( | |
r"^call ([a-zA-Z0-9._:]+) ([0-9]+)$" | |
).unwrap(); | |
}; | |
let capture = RE.captures(line) | |
.expect("Invalid call command!"); | |
let func_name = capture.get(1).unwrap().as_str(); | |
let n_args = capture.get(2).unwrap().as_str().parse::<i32>().unwrap(); | |
let asm_code = format!("// {line}\n\ | |
// push returnaddr\n\ | |
@returnaddr{call_count}\n\ | |
D=A\n\ | |
@SP\n\ | |
A=M\n\ | |
M=D\n\ | |
@SP\n\ | |
M=M+1\n\ | |
// push LCL\n\ | |
@LCL\n\ | |
D=M\n\ | |
@SP\n\ | |
A=M\n\ | |
M=D\n\ | |
@SP\n\ | |
M=M+1\n\ | |
// push ARG\n\ | |
@ARG\n\ | |
D=M\n\ | |
@SP\n\ | |
A=M\n\ | |
M=D\n\ | |
@SP\n\ | |
M=M+1\n\ | |
// push THIS\n\ | |
@THIS\n\ | |
D=M\n\ | |
@SP\n\ | |
A=M\n\ | |
M=D\n\ | |
@SP\n\ | |
M=M+1\n\ | |
// push THAT\n\ | |
@THAT\n\ | |
D=M\n\ | |
@SP\n\ | |
A=M\n\ | |
M=D\n\ | |
@SP\n\ | |
M=M+1\n\ | |
// ARG = SP - n - 5\n\ | |
@SP\n\ | |
D=M\n\ | |
@{n_args}\n\ | |
D=D-A\n\ | |
@5\n\ | |
D=D-A\n\ | |
@ARG\n\ | |
M=D\n\ | |
// LCL = SP\n\ | |
@SP\n\ | |
D=M\n\ | |
@LCL\n\ | |
M=D\n\ | |
// goto f\n\ | |
@{func_name}\n\ | |
0;JMP\n\ | |
// declare (returnaddr)\n\ | |
(returnaddr{call_count})", line=line, n_args=n_args, | |
func_name=func_name, call_count=call_count); | |
write_to_file(file, asm_code); | |
} | |
/// Bootstrap to ensure Sys.init gets called first when multiple files | |
/// | |
/// # Arguments | |
/// | |
/// * output_file: file to bootstrap | |
fn bootstrap(output_file: &fs::File) { | |
let set_stackpointer = "\n// Bootstrap\n\n@256\n\ | |
D=A\n\ | |
@SP\n\ | |
M=D".to_string(); | |
write_to_file(&output_file, set_stackpointer); | |
write_call(&output_file, "call Sys.init 0", 0); | |
} | |
/// ******************************** | |
/// ************* MAIN ************* | |
/// ******************************** | |
fn main () { | |
let (file_contents, in_paths, output_file, out_path) = parse_args(); | |
// bootstrap if multiple files | |
if in_paths.len() > 1 { | |
bootstrap(&output_file); | |
} | |
let mut cmp_count = 0; | |
let mut call_count = 1; // already called Sys.init, start at 1 | |
let mut it_file_contents = file_contents.iter(); | |
let mut it_in_paths = in_paths.iter(); | |
while let (Some(contents), Some(in_path)) = (it_file_contents.next(), it_in_paths.next()) { | |
let in_file_name = get_file_name(&in_path); | |
write_to_file(&output_file, format!("\n// {}\n", in_path)); | |
for line in contents.lines() { | |
let clean_line = remove_comments(line); | |
if clean_line == "" { continue }; | |
let command_type = get_command_type(clean_line); | |
match command_type { | |
CommandType::CPush => { | |
let (segment, index) = parse_push_pop(clean_line); | |
write_push(&output_file, &in_file_name, clean_line, segment, index); | |
}, | |
CommandType::CPop => { | |
let (segment, index) = parse_push_pop(clean_line); | |
write_pop(&output_file, &in_file_name, clean_line, segment, index); | |
}, | |
CommandType::CArithmetic => { | |
write_arithmetic(&output_file, clean_line, cmp_count); | |
cmp_count += 1; | |
}, | |
CommandType::CLabel => { | |
write_label(&output_file, clean_line); | |
}, | |
CommandType::CGoTo => { | |
write_goto(&output_file, clean_line); | |
}, | |
CommandType::CIfGoTo => { | |
write_ifgoto(&output_file, clean_line); | |
}, | |
CommandType::CFunction => { | |
write_function(&output_file, clean_line); | |
}, | |
CommandType::CCall => { | |
write_call(&output_file, clean_line, call_count); | |
call_count += 1; | |
}, | |
CommandType::CReturn => { | |
write_return(&output_file, clean_line); | |
}, | |
} | |
} | |
println!("\nTranslated {:?}\n -> {:?}\n", in_path, out_path); | |
} | |
} |