diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5a3b1fe --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}\\nand2tetris\\projects\\7\\VMTranslator", + "env": { + "PYTHONPATH": "${workspaceFolder}\\nand2tetris\\projects\\7\\VMTranslator" + } + }, + { + "name": "Python Debugger: Python File", + "type": "debugpy", + "request": "launch", + "program": "${file}" + } + ] +} \ No newline at end of file diff --git a/nand2tetris/projects/7/StackArithmetic/.gitignore b/nand2tetris/projects/7/StackArithmetic/.gitignore new file mode 100644 index 0000000..7bdbb1b --- /dev/null +++ b/nand2tetris/projects/7/StackArithmetic/.gitignore @@ -0,0 +1 @@ +*.asm \ No newline at end of file diff --git a/nand2tetris/projects/7/VMTranslator/VMTranslator.py b/nand2tetris/projects/7/VMTranslator/VMTranslator.py index 30a18d0..4ec9a24 100644 --- a/nand2tetris/projects/7/VMTranslator/VMTranslator.py +++ b/nand2tetris/projects/7/VMTranslator/VMTranslator.py @@ -5,20 +5,15 @@ #### ---- import ---- # https://stackoverflow.com/a/37867717/3560695 -import importlib.util -import sys import re ## Import parser ## We have to use import_module since the path contains an illegal directory name (7). -parser_module = importlib.import_module("nand2tetris.projects.7.VMTranslator.src.parser.parser") -Parser = parser_module.parser - -writer_module = importlib.import_module("nand2tetris.projects.7.VMTranslator.src.codewriter.codewriter") -codewriter = writer_module.codewriter - - +# parser_module = importlib.import_module("src.parser.parser") +import src.parser.parser as p +# writer_module = importlib.import_module("src.codewriter.codewriter") +import src.codewriter.codewriter as cw #### ---- main ---- @@ -35,113 +30,59 @@ def main(filename): filename_write = re.sub(regex, r"\1\2.asm", filename) + Parser = p.parser + codewriter = cw.codewriter + with open(filename, 'r') as file, open(filename_write, "w") as file_write: parser = Parser(file) writer = codewriter(file_write) while parser.hasMoreCommands(): ## As long as file has more lines, do: parser.advance() ## Go to the next line in the file. - parser.getinstruction() ## sets parser.instruction to current instruction + parser.getinstruction() if not parser.instruction: ## If instruction is blank, skip. Line consisted only of a comment. continue - print(f"---- Line {parser.lineNo} ----") - print(f"instruction = {parser.instruction}") - print(f"commandType() = {parser.commandType()}") - print(f"arg1() = {parser.arg1()}") - print(f"arg2() = {parser.arg2()}") + parser.getVMinstruction() ## sets parser.VMinstruction to current instruction + # print(f"---- Line {parser.lineNo} ----") + # print(f"instruction = {parser.instruction}") + # print(f"commandType() = {parser.commandType()}, type = {type(parser.commandType())}") + # print(f"arg1() = {parser.arg1()}") + # print(f"arg2() = {parser.arg2()}") + + + commandType = parser.commandType() if parser.commandType() == "C_ARITHMETIC": # Write arithmetic from arithmetic.asm - ... + arg1 = parser.arg1() + lines = writer.writeArithmetic(arg1) + elif parser.commandType() in ["C_PUSH", "C_POP"]: - writer.writePushPop(parser.commandType(), parser.arg1(), parser.arg2()) + arg1 = parser.arg1() + arg2 = parser.arg2() + lines = writer.writePushPop(commandType, arg1, arg2) else: ... + + ## lines is now the full .asm script. Write to output asm file. + file_write.write(f'// {parser.instruction}\n') + for line in lines: + file_write.write(f'{line}\n') + -## Testing -filename = '/nand2tetris/projects/7/StackArithmetic/SimpleAdd/SimpleAdd.vm' - -main(filename[1:]) - - - - - -## Below is probably redundant - - - - - +import importlib +## Testing +filename = '.../StackArithmetic/SimpleAdd/SimpleAdd.vm' +filename = '.../StackArithmetic/Stacktest/Stacktest.vm' -# https://chat.openai.com/share/40c7964c-0f2f-44d2-81c7-e88d6a1868a1 -def changeExtension(string : str, newExtension : str): - """ - """ - if not isinstance(string, str): - raise TypeError("Input must be a string") - if not isinstance(newExtension, str): - raise TypeError("Input must be a string") - - # Regex pattern to capture the base name and extension - # https://regex101.com/r/SkENd5/1 - regex = r"^(?P[^.].*\.)(?P\w+)$|(?P^.*$)" - - # Replace the extension with the new extension - new_string = re.sub( - regex, - lambda m: ( - m.group('base') + newExtension - if m.group('base') - else m.group('fullname') - ), - string) - - return new_string - - -def writelines(input_data : str | list, filename) -> None: - """ - Write the given string or list of strings to the specified file. - - Args: - input_data (str | list): The string or list of strings to write to the file. - filename (str): The name of the file to write to. - - Raises: - TypeError: If input_data is neither a string nor a list, or if filename is not a string. - ValueError: If filename is an empty string. - IOError: If there is an issue writing to the file. - - Returns: - None - """ - # Check input types - if not isinstance(input_data, (str, list)): - raise TypeError("Input data must be a string or a list of strings") - if not isinstance(filename, str): - raise TypeError("Filename must be a string") - - # Check filename is not empty - if not filename: - raise ValueError("Filename cannot be an empty string") - - # Open the file in write mode - try: - with open(filename, 'w') as file: - # Write data to the file - if isinstance(input_data, str): - file.write(input_data) - else: - file.writelines(input_data) - except IOError as e: - raise IOError("Error writing to file: " + str(e)) - +importlib.reload(p) +importlib.reload(cw) +main(filename[1:]) diff --git a/nand2tetris/projects/7/VMTranslator/__init__.py b/nand2tetris/projects/7/VMTranslator/__init__.py new file mode 100644 index 0000000..d65e1a9 --- /dev/null +++ b/nand2tetris/projects/7/VMTranslator/__init__.py @@ -0,0 +1 @@ +__package__ = '' \ No newline at end of file diff --git a/nand2tetris/projects/7/VMTranslator/src/Archive/deprecated.py b/nand2tetris/projects/7/VMTranslator/src/Archive/deprecated.py new file mode 100644 index 0000000..83a845a --- /dev/null +++ b/nand2tetris/projects/7/VMTranslator/src/Archive/deprecated.py @@ -0,0 +1,72 @@ + + +# https://chat.openai.com/share/40c7964c-0f2f-44d2-81c7-e88d6a1868a1 +def changeExtension(string : str, newExtension : str): + """ + """ + if not isinstance(string, str): + raise TypeError("Input must be a string") + if not isinstance(newExtension, str): + raise TypeError("Input must be a string") + + # Regex pattern to capture the base name and extension + # https://regex101.com/r/SkENd5/1 + regex = r"^(?P[^.].*\.)(?P\w+)$|(?P^.*$)" + + # Replace the extension with the new extension + new_string = re.sub( + regex, + lambda m: ( + m.group('base') + newExtension + if m.group('base') + else m.group('fullname') + ), + string) + + return new_string + + +def writelines(input_data : str | list, filename) -> None: + """ + Write the given string or list of strings to the specified file. + + Args: + input_data (str | list): The string or list of strings to write to the file. + filename (str): The name of the file to write to. + + Raises: + TypeError: If input_data is neither a string nor a list, or if filename is not a string. + ValueError: If filename is an empty string. + IOError: If there is an issue writing to the file. + + Returns: + None + """ + # Check input types + if not isinstance(input_data, (str, list)): + raise TypeError("Input data must be a string or a list of strings") + if not isinstance(filename, str): + raise TypeError("Filename must be a string") + + # Check filename is not empty + if not filename: + raise ValueError("Filename cannot be an empty string") + + # Open the file in write mode + try: + with open(filename, 'w') as file: + # Write data to the file + if isinstance(input_data, str): + file.write(input_data) + else: + file.writelines(input_data) + except IOError as e: + raise IOError("Error writing to file: " + str(e)) + + + + + + + + diff --git a/nand2tetris/projects/7/VMTranslator/src/codewriter/codewriter.py b/nand2tetris/projects/7/VMTranslator/src/codewriter/codewriter.py index d215886..0d983fb 100644 --- a/nand2tetris/projects/7/VMTranslator/src/codewriter/codewriter.py +++ b/nand2tetris/projects/7/VMTranslator/src/codewriter/codewriter.py @@ -1,13 +1,36 @@ +import sys +# import importlib + +# sys.path.append("C:/Users/Bruger/OneDrive/Dokumenter/GitHub/BuildComputer/nand2tetris/projects/7/VMTranslator") + + +# parser_module = importlib.import_module("src.parser.parser") +import src.parser.parser as parser_module +# importlib.reload(parser_module) + +# dicts_filepath = "nand2tetris/projects/7/VMTranslator/src/utils/dict/dict" +# dicts = importlib.import_module(dicts_filepath.replace("/", ".")) +import src.utils.dict.dict as dicts + class codewriter: """ writes the assembly code that implements the parsed command. """ def __init__(self, file) -> None: self.file = file ## functions need to be able to grab file. + self.newline = '\n' + + self.Parser = parser_module.parser + + ## Initialize number of times arithmetic has been called + self.arithmeticNo = 0 + ## Used in some of the .asm templates. + self.segment = None + self.index = None - def writeArithmetic(command : str) -> None: + def writeArithmetic(self, command : str) -> None: """ Arguments command (string) @@ -15,32 +38,103 @@ def writeArithmetic(command : str) -> None: Writes to the output file the assembly code that implements the given arithmetic command. """ - def writePushPop(command : str, segment : str, index : int): + ## Unary or binary operation: + arity = dicts.arithmetic[command] + + + ## action is used in some of the .asm templates. + ## Contains Hack code for the arithmetic operations + action = dicts.arithmetic_action[command] + if command in ["eq", "lt", "gt"]: + action = dicts.compare_action.format(**locals()) + + with open(f"src/utils/asm/arithmetic_{arity}.asm", 'r') as asm: + parser = self.Parser(asm) + + lines = self.processCommands(parser, action = action) + + self.arithmeticNo += 1 + return lines + + + + + + def writePushPop(self, command : str, segment : str, index : int): """ Arguments ---- - command (C_PUSH or C_POP) - segment : str - index : int + command : ('C_PUSH' or 'C_POP') + segment : constant, local etc. + index : pointer number in the segment Function ---- Writes to the output file the assembly code that implements the given command, where command is either C_PUSH or C_POP. """ - # Write push and pop commands from push.asm and pop.asm - if parser.arg1().lower() == "constant": - print("constant.") - elif parser.arg1().lower == "static": - print("static.") + + segmentPointer = None ## Initialize - passed to self.processCommands but not always used. + + ## First handle 'pointer' logic translation to THIS/THAT + if segment == "pointer": + segment = ["this", "that"][index] ## e.g. 'push pointer 0' means 'push this' + index = 0 ## Silently 'push THIS' means 'push THIS 0' + + if command == "C_PUSH": + if segment == "constant": + asm_location = "pushConstant.asm" + elif segment in ["local", "argument", "this", "that", "temp"]: + ## Used in some of the .asm templates. + ## Contains Hack name convention for segments, e.g. local is "LCL" + segmentPointer = dicts.segment[segment] + asm_location = "pushSegment.asm" + elif segment == "static": + asm_location = "pushStatic.asm" + # elif segment == "temp": + # ... + # elif segment == "pointer": ## Handled above + # ... + # elif command == "C_POP": + # ... else: - print(parser.arg1()) + raise KeyError(f"Unexpected command '{command}' in writePushPop (should be 'C_PUSH' or 'C_POP')") + + lines = [] ## Initialize lines. + + with open(f"src/utils/asm/{asm_location}", 'r') as asm: + parser = self.Parser(asm) + + lines = self.processCommands(parser, segment = segment, segmentPointer = segmentPointer, index = index) + return lines - def close(): + def processCommands(self, parser, **kwargs): """ + Arguments + ---- + parser : parser object Function ---- - Closes the output file. - - Likely redundant for my method. + Process commands using the provided parser object until there are no more commands. """ + lines = [] + while parser.hasMoreCommands(): + parser.advance() + parser.getinstruction() + if not parser.instruction: ## If instruction is blank, skip. Line consisted only of a comment. + continue + + ## Replace any {} in the input file with its value, sent to this function through **kwargs. + ## Important that the arguments are named. E.g. + ## segment = 'local' + ## processCommands(..., segment = segment) + ## processCommands(..., segment = 'local') + line = parser.instruction.format(**kwargs) + + lines.append(line) + + return lines + + + + \ No newline at end of file diff --git a/nand2tetris/projects/7/VMTranslator/src/parser/parser.py b/nand2tetris/projects/7/VMTranslator/src/parser/parser.py index f879ded..5ef8266 100644 --- a/nand2tetris/projects/7/VMTranslator/src/parser/parser.py +++ b/nand2tetris/projects/7/VMTranslator/src/parser/parser.py @@ -1,11 +1,11 @@ -import importlib +# import importlib -dict_filepath = "nand2tetris/projects/7/VMTranslator/src/utils/dict/dict" -dict = importlib.import_module(dict_filepath.replace("/", ".")) +# dicts_filepath = "src/utils/dict/dict" +#dicts = importlib.import_module(dicts_filepath.replace("/", ".")) +import src.utils.dict.dict as dicts - -class Parser: +class parser: """ - Handles the parsing of a single .vm file. @@ -16,10 +16,11 @@ class Parser: """ def __init__(self, file) -> None: - self.line = None ## Initialize line. - self.lineNo = 0 ## Initialize line number. - self.instruction = None ## Initialize instruction. - self.file = file ## functions need to be able to grab file. + self.line = None ## Initialize line. + self.lineNo = 0 ## Initialize line number. + self.instruction = None ## Initialize instruction. + self.VMinstruction = None ## Initialize VM instruction (list) + self.file = file ## functions need to be able to grab file. def peek(self) -> str: @@ -63,7 +64,6 @@ def advance(self) -> None: Returns ---- None - Function ---- Reads the next command from the input and makes it the current command. @@ -92,7 +92,7 @@ def commandType(self) -> str: """ try: - return dict.commandType[self.instruction[0]] + return dicts.commandType[self.VMinstruction[0]] except: raise KeyError(f"Current command invalid: Cannot parse '{self.line.strip()}' on line {self.lineNo}.") @@ -114,11 +114,11 @@ def arg1(self) -> str: Should not be called if the current command is C_RETURN. """ if self.commandType() == "C_ARITHMETIC": - out = self.instruction[0] + out = self.VMinstruction[0] elif self.commandType() == "C_RETURN": return ValueError(f"arg1() called on invalid command type: '{self.commandType()}'") else: - out = self.instruction[1] + out = self.VMinstruction[1] return out @@ -139,12 +139,24 @@ def arg2(self) -> int: C_PUSH, C_POP, C_FUNCTION or C_CALL. """ if self.commandType() in ["C_PUSH", "C_POP", "C_FUNCTION", "C_CALL"]: - return int(self.instruction[2]) + return int(self.VMinstruction[2]) else: return ValueError(f"arg2() called on invalid command type: '{self.commandType()}'") def getinstruction(self): - self.instruction = self.line.split("//")[0].strip().split() + """ + Remove everything after the comment token (//) + """ + self.instruction = self.line.split("//")[0].strip() + + def getVMinstruction(self): + """ + Splits the instruction into a list of objects + """ + + # Call getinstruction to ensure instruction has been retrieved. + self.getinstruction() + self.VMinstruction = self.instruction.split() diff --git a/nand2tetris/projects/7/VMTranslator/src/utils/asm/arithmetic_1.asm b/nand2tetris/projects/7/VMTranslator/src/utils/asm/arithmetic_1.asm new file mode 100644 index 0000000..7704f7e --- /dev/null +++ b/nand2tetris/projects/7/VMTranslator/src/utils/asm/arithmetic_1.asm @@ -0,0 +1,12 @@ +// ---- unary action ---- +// SP-- +// *SP = {action}*SP +// SP++ +@SP +M = M - 1 + +A = M +M = {action}M + +@SP +M = M + 1 \ No newline at end of file diff --git a/nand2tetris/projects/7/VMTranslator/src/utils/asm/arithmetic_2.asm b/nand2tetris/projects/7/VMTranslator/src/utils/asm/arithmetic_2.asm new file mode 100644 index 0000000..9c2cdbf --- /dev/null +++ b/nand2tetris/projects/7/VMTranslator/src/utils/asm/arithmetic_2.asm @@ -0,0 +1,30 @@ +// ---- x {action} y ---- +// SP-- +// D = *SP +// SP-- +// *SP = *SP {action} D +// SP++ + +// SP-- +@SP +M = M - 1 + + +// D = *SP +A = M +D = M + +// SP-- +@SP +M = M - 1 + +// Arithmetic operation +// E.g. add: *SP = *SP + D +// E.g. eq: D = *SP - D, D; JEQ +A = M +{action} + +// SP++ +@SP +M = M + 1 + diff --git a/nand2tetris/projects/7/VMTranslator/src/utils/asm/pushConstant.asm b/nand2tetris/projects/7/VMTranslator/src/utils/asm/pushConstant.asm new file mode 100644 index 0000000..a1eda4a --- /dev/null +++ b/nand2tetris/projects/7/VMTranslator/src/utils/asm/pushConstant.asm @@ -0,0 +1,22 @@ +// ---- push {segment} {index} ---- + +// addr = segment + i, *SP = *addr, SP++ + + +// addr <- SegmentPointer + i +@{index} +D = A + +// Not used if segment is 'constant' +//@{segmentPointer} +//A = D + M +//D = M + +// RAM[SP] = RAM[addr] +@SP +A = M +M = D + +// SP++ +@SP +M = M + 1 \ No newline at end of file diff --git a/nand2tetris/projects/7/VMTranslator/src/utils/asm/pushSegment.asm b/nand2tetris/projects/7/VMTranslator/src/utils/asm/pushSegment.asm index 58a418f..7caa1a2 100644 --- a/nand2tetris/projects/7/VMTranslator/src/utils/asm/pushSegment.asm +++ b/nand2tetris/projects/7/VMTranslator/src/utils/asm/pushSegment.asm @@ -6,8 +6,11 @@ // addr <- SegmentPointer + i @{index} D = A + +// Not used if segment is 'constant' @{segmentPointer} -D = D + M +A = D + M +D = M // RAM[SP] = RAM[addr] @SP diff --git a/nand2tetris/projects/7/VMTranslator/src/utils/dict/dict.py b/nand2tetris/projects/7/VMTranslator/src/utils/dict/dict.py index 6407e95..930f176 100644 --- a/nand2tetris/projects/7/VMTranslator/src/utils/dict/dict.py +++ b/nand2tetris/projects/7/VMTranslator/src/utils/dict/dict.py @@ -23,7 +23,60 @@ segment = { "local" : "LCL", "argument" : "ARG", - "THIS" : "THIS", - "THAT" : "THAT", - + "this" : "THIS", + "that" : "THAT", + + "temp" : 5, +} + +arithmetic = { + "neg" : 1, + "not" : 1, + + "add" : 2, + "sub" : 2, + "eq" : 2, + "gt" : 2, + "lt" : 2, + "and" : 2, + "or" : 2, +} + +arithmetic_action = { + "neg" : "-", # M = -M + "not" : "!", # M = !M + + "add" : "M = M + D", + "sub" : "M = M - D", + "eq" : "JEQ", + "gt" : "JGT", + "lt" : "JLT", + "and" : "M = M & D", + "or" : "M = M | D", } + +compare_action = """ +D = M - D +@TRUE{self.arithmeticNo} +D; {action} +D = 0 +@SP +A = M +M = D +@CONTINUE{self.arithmeticNo} +0; JMP +(TRUE{self.arithmeticNo}) +D = -1 +@SP +A = M +M = D +(CONTINUE{self.arithmeticNo}) +""".strip() + + + +""" + "R13" : 13, + "R14" : 14, + "constant" : "CONST", +""" \ No newline at end of file diff --git a/nand2tetris/projects/7/VMTranslator/tests/conftest.py b/nand2tetris/projects/7/VMTranslator/tests/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/nand2tetris/projects/7/VMTranslator/tests/test_arithmetic.py b/nand2tetris/projects/7/VMTranslator/tests/test_arithmetic.py new file mode 100644 index 0000000..5c690e7 --- /dev/null +++ b/nand2tetris/projects/7/VMTranslator/tests/test_arithmetic.py @@ -0,0 +1,21 @@ + + + + +# import importlib + +# writer_module = importlib.import_module("src.codewriter.codewriter") +from src.codewriter import codewriter as cw +# importlib.reload(cw) + +# importlib.reload(cw) + +testwriter = cw.codewriter("") + +print("---- eq ----") +print(testwriter.writeArithmetic("lt")) + +# print("---- push local 7 ----") +# testwriter.writePushPop("C_PUSH", "constant", 7) + + diff --git a/nand2tetris/projects/7/VMTranslator/tests/test_path.py b/nand2tetris/projects/7/VMTranslator/tests/test_path.py new file mode 100644 index 0000000..1d92730 --- /dev/null +++ b/nand2tetris/projects/7/VMTranslator/tests/test_path.py @@ -0,0 +1,6 @@ + + + +import os + +print(os.getcwd()) \ No newline at end of file diff --git a/nand2tetris/projects/7/VMTranslator/tests/test_writePushPop.py b/nand2tetris/projects/7/VMTranslator/tests/test_writePushPop.py new file mode 100644 index 0000000..dd67e7d --- /dev/null +++ b/nand2tetris/projects/7/VMTranslator/tests/test_writePushPop.py @@ -0,0 +1,17 @@ + + + + +# import importlib + +# writer_module = importlib.import_module("src.codewriter.codewriter") +from src.codewriter import codewriter as cw +# importlib.reload(cw) + + +testwriter = cw.codewriter("") + +print("---- push local 7 ----") +print(testwriter.writePushPop("C_PUSH", "static", 7)) + +