Skip to content

Commit 9bab497

Browse files
committed
Chapter 6 - Assembler
Python3 for Hack Assembler and Hack Assembler(No Symbols). Python3 for Assembler Modules Code, Command, Parser, and SymbolTable.
1 parent 57421f4 commit 9bab497

File tree

8 files changed

+389
-1
lines changed

8 files changed

+389
-1
lines changed

06/assembler.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env python3
2+
3+
4+
"""Hack ASM Assembler
5+
Assembles the supplied Hack ASM file into its
6+
Hack Machine Language equivalent. If input file
7+
is '{some_file}.asm', output file will be in the
8+
same directory as '{some_file}.hack'.
9+
"""
10+
11+
12+
import sys
13+
from pathlib import Path
14+
from assembler_modules import code
15+
from assembler_modules.parser import Parser
16+
from assembler_modules.command import Command
17+
from assembler_modules.symbol_table import SymbolTable
18+
19+
20+
__author__ = "Merrick Ryman"
21+
__version__ = "1.0"
22+
23+
24+
def main():
25+
asm_path = Path(sys.argv[-1])
26+
if not (asm_path.is_file() and (asm_path.suffix == '.asm')):
27+
raise IOError('Valid ASM file not supplied.')
28+
hack_path = str(asm_path.parent) + '/' + (asm_path.name[:asm_path.name.find('.')] + '.hack')
29+
asm_parser = Parser(asm_path)
30+
asm_symbols = SymbolTable()
31+
32+
#Initialize Symbol Table
33+
for i in range(0,16):
34+
asm_symbols.add_entry('R'+str(i), i)
35+
asm_symbols.add_entry('SP', 0)
36+
asm_symbols.add_entry('LCL', 1)
37+
asm_symbols.add_entry('ARG', 2)
38+
asm_symbols.add_entry('THIS', 3)
39+
asm_symbols.add_entry('THAT', 4)
40+
asm_symbols.add_entry('SCREEN', 16384)
41+
asm_symbols.add_entry('KBD', 24576)
42+
43+
#First Pass - Build label symbol maps
44+
ROM_address = 0
45+
while asm_parser.has_more_commands():
46+
if asm_parser.command_type() is Command.A_COMMAND:
47+
ROM_address += 1
48+
elif asm_parser.command_type() is Command.C_COMMAND:
49+
ROM_address += 1
50+
elif asm_parser.command_type() is Command.L_COMMAND:
51+
asm_symbols.add_entry(asm_parser.symbol(), ROM_address)
52+
else:
53+
raise ValueError('This command type does not exist.')
54+
55+
#Second Pass - Build variable symbol maps and assemble.
56+
asm_parser.reset_iter()
57+
RAM_address = 16
58+
with open(hack_path, 'w+') as hack_file:
59+
while asm_parser.has_more_commands():
60+
if asm_parser.command_type() is Command.A_COMMAND:
61+
symbol = asm_parser.symbol()
62+
try:
63+
symbol = int(symbol)
64+
except ValueError:
65+
if asm_symbols.contains(symbol):
66+
symbol = asm_symbols.get_address(symbol)
67+
else:
68+
asm_symbols.add_entry(symbol, RAM_address)
69+
symbol = RAM_address
70+
RAM_address += 1
71+
hack_file.write(f'{symbol:016b}' + '\n')
72+
elif asm_parser.command_type() is Command.C_COMMAND:
73+
comp = ''.join(str(bit) for bit in code.comp(asm_parser.comp()))
74+
dest = ''.join(str(bit) for bit in code.dest(asm_parser.dest()))
75+
jump = ''.join(str(bit) for bit in code.jump(asm_parser.jump()))
76+
hack_file.write('111' + comp + dest + jump + '\n')
77+
elif asm_parser.command_type() is Command.L_COMMAND:
78+
continue
79+
else:
80+
hack_file.close()
81+
raise ValueError('This command type does not exist.')
82+
83+
84+
if __name__ == "__main__":
85+
main()

06/assembler_modules/__init__.py

Whitespace-only changes.

06/assembler_modules/code.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env python3
2+
3+
4+
"""The Code Module
5+
Translates Hack ASM mnemonics into Hack Machine Language.
6+
"""
7+
8+
9+
__author__ = "Merrick Ryman"
10+
__version__ = "1.0"
11+
12+
13+
def dest(mnemonic):
14+
"""Returns the Hack Machine Language equivalent of a
15+
dest mnemonic (3 bits).
16+
"""
17+
return { #A D M
18+
'null': [0,0,0],
19+
'M': [0,0,1],
20+
'D': [0,1,0],
21+
'MD': [0,1,1],
22+
'A': [1,0,0],
23+
'AM': [1,0,1],
24+
'AD': [1,1,0],
25+
'AMD': [1,1,1]
26+
}[mnemonic]
27+
28+
29+
def comp(mnemonic):
30+
"""Returns the Hack Machine Language equivalent of a
31+
comp mnemonic (7 bits).
32+
"""
33+
return { #a c1 c2 c3 c4 c5 c6
34+
'0': [0,1,0,1,0,1,0],
35+
'1': [0,1,1,1,1,1,1],
36+
'-1': [0,1,1,1,0,1,0],
37+
'D': [0,0,0,1,1,0,0],
38+
'A': [0,1,1,0,0,0,0], 'M': [1,1,1,0,0,0,0],
39+
'!D': [0,0,0,1,1,0,1],
40+
'!A': [0,1,1,0,0,0,1], '!M': [1,1,1,0,0,0,1],
41+
'-D': [0,0,0,1,1,1,1],
42+
'-A': [0,1,1,0,0,1,1], '-M': [1,1,1,0,0,1,1],
43+
'D+1': [0,0,1,1,1,1,1],
44+
'A+1': [0,1,1,0,1,1,1], 'M+1': [1,1,1,0,1,1,1],
45+
'D-1': [0,0,0,1,1,1,0],
46+
'A-1': [0,1,1,0,0,1,0], 'M-1': [1,1,1,0,0,1,0],
47+
'D+A': [0,0,0,0,0,1,0], 'D+M': [1,0,0,0,0,1,0],
48+
'D-A': [0,0,1,0,0,1,1], 'D-M': [1,0,1,0,0,1,1],
49+
'A-D': [0,0,0,0,1,1,1], 'M-D': [1,0,0,0,1,1,1],
50+
'D&A': [0,0,0,0,0,0,0], 'D&M': [1,0,0,0,0,0,0],
51+
'D|A': [0,0,1,0,1,0,1], 'D|M': [1,0,1,0,1,0,1]
52+
}[mnemonic]
53+
54+
55+
def jump(mnemonic):
56+
"""Returns the Hack Machine Language equivalent of a
57+
jump mnemonic (3 bits).
58+
"""
59+
return { #A D M
60+
'null': [0,0,0],
61+
'JGT': [0,0,1],
62+
'JEQ': [0,1,0],
63+
'JGE': [0,1,1],
64+
'JLT': [1,0,0],
65+
'JNE': [1,0,1],
66+
'JLE': [1,1,0],
67+
'JMP': [1,1,1]
68+
}[mnemonic]

06/assembler_modules/command.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env python3
2+
3+
4+
"""Command Enum
5+
Provides an enum for the command types associated with
6+
the source Hack ASM (A_COMMAND, C_COMMAND, L_COMMAND)
7+
"""
8+
9+
10+
import enum
11+
12+
13+
__author__ = "Merrick Ryman"
14+
__version__ = "1.0"
15+
16+
17+
class Command(enum.Enum):
18+
A_COMMAND = 1
19+
C_COMMAND = 2
20+
L_COMMAND = 3

06/assembler_modules/parser.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/usr/bin/env python3
2+
3+
4+
"""The Parser Module
5+
Encapsulates access to the input code (ASM file). Reads
6+
a Hack ASM command, parses it, and provides convenient access
7+
to the command's components (fields and symbols). In addition,
8+
removes all whitespace, comments, and empty lines.
9+
"""
10+
11+
12+
import re
13+
from .command import Command
14+
15+
16+
__author__ = "Merrick Ryman"
17+
__version__ = "1.0"
18+
19+
20+
class Parser:
21+
def __init__(self, asm_path):
22+
self.asm_path = asm_path
23+
24+
self._asm_commands = None
25+
self._asm_command_current = None
26+
self._asm_commands_iter = None
27+
self._parse_asm_file()
28+
self.reset_iter()
29+
30+
31+
def _parse_asm_file(self):
32+
"""Parse the Hack ASM file into memory so it
33+
is easier to work with. Remove comments, spaces,
34+
empty lines, etc, so all that remains are commands
35+
(and pseudo-commands). Also, create an iterable
36+
to get the next command.
37+
"""
38+
with open(self.asm_path, 'r') as asm_file:
39+
lines = asm_file.readlines() # Get the list of lines (strings) from the asm file
40+
lines = list(map(lambda x: x[:x.find('//')].replace(' ', ''), lines)) # Remove comments and whitespace
41+
self._asm_commands = list(filter(None, lines)) # Remove empty lines, extract to instance
42+
43+
44+
def reset_iter(self):
45+
"""Resets (or sets) the iterator object that
46+
iterates over the asm commands. Useful for
47+
re-parsing the file as necessary.
48+
"""
49+
self._asm_commands_iter = iter(self._asm_commands)
50+
51+
52+
def has_more_commands(self):
53+
"""Are there any more commands in the file?
54+
Returns True if command iterable successfully
55+
gets the next command. Otherwise, return False.
56+
"""
57+
try:
58+
self._asm_command_current = next(self._asm_commands_iter)
59+
return True
60+
except StopIteration:
61+
return False
62+
63+
64+
def command_type(self):
65+
"""Returns the type of the current ASM command.
66+
If command starts with '@', this is an A-Instruction
67+
command. If command starts with '(', this is an
68+
L-Instruction command. Otherwise, this is a
69+
C-Instruction command.
70+
"""
71+
return {
72+
'@': Command.A_COMMAND,
73+
'(': Command.L_COMMAND
74+
}.get(self._asm_command_current[0], Command.C_COMMAND)
75+
76+
77+
def symbol(self):
78+
"""Returns the symbol or decimal constant xyz
79+
of the current command @xyz or (xyz). Should
80+
only be called when the command type is
81+
A_COMMAND or L_COMMAND.
82+
"""
83+
return re.sub('[@()]', '', self._asm_command_current)
84+
85+
86+
def is_dest_c_instruction(self):
87+
""" Returns True if the dest mnemonic is
88+
present in the current C_COMMAND. Otherwise,
89+
return False because this is a jump. Should
90+
only be called when the command type is
91+
C_COMMAND.
92+
"""
93+
if self._asm_command_current.find('=') < 0: # '=' not found
94+
return False
95+
return True
96+
97+
98+
def dest(self):
99+
"""Returns the dest mnemonic in the current
100+
C_COMMAND (8 possibilities). Should only be
101+
called when the command type is C_COMMAND.
102+
"""
103+
if self.is_dest_c_instruction():
104+
return self._asm_command_current[:self._asm_command_current.find('=')]
105+
return 'null'
106+
107+
108+
def comp(self):
109+
"""Returns the comp mnemonic in the current
110+
C_COMMAND (28 possibilities). Should only be
111+
called when the command type is C_COMMAND.
112+
"""
113+
if self.is_dest_c_instruction():
114+
return self._asm_command_current[self._asm_command_current.find('=')+1:]
115+
return self._asm_command_current[:self._asm_command_current.find(';')]
116+
117+
118+
def jump(self):
119+
"""Returns the jump mnemonic in the current
120+
C_COMMAND (8 possibilities). Should only be
121+
called when the command type is C_COMMAND.
122+
"""
123+
if self.is_dest_c_instruction():
124+
return 'null'
125+
return self._asm_command_current[self._asm_command_current.find(';')+1:]

06/assembler_modules/symbol_table.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env python3
2+
3+
4+
"""The SymbolTable Module
5+
Keeps a correspondence between symbolic labels and numeric
6+
addresses. This is done easily in Python using dictionaries.
7+
Addresses are kept as decimal integers inside the table map.
8+
"""
9+
10+
11+
__author__ = "Merrick Ryman"
12+
__version__ = "1.0"
13+
14+
15+
class SymbolTable:
16+
def __init__(self):
17+
self._table = {}
18+
19+
20+
def add_entry(self, symbol, address):
21+
"""Adds the mapping {symbol: address} to the table.
22+
"""
23+
self._table.update({symbol:address})
24+
25+
26+
def contains(self, symbol):
27+
"""Returns True if the table map contains the given
28+
symbol.
29+
"""
30+
return symbol in self._table
31+
32+
33+
def get_address(self, symbol):
34+
"""Returns the address mapped to the symbol from the table.
35+
"""
36+
return self._table.get(symbol)

06/assembler_ns.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env python3
2+
3+
4+
"""Hack ASM Assembler (No Symbols)
5+
Assembles the supplied Hack ASM file into its
6+
Hack Machine Language equivalent. If input file
7+
is '{some_file}.asm', output file will be in the
8+
same directory as '{some_file}.hack'. Keep in
9+
mind that this particular assembler does not have
10+
support for symbols, i.e. a mapping of text to
11+
decimal addresses. Please use the other supplied
12+
assembler for this feature ('assembler.py').
13+
"""
14+
15+
16+
import sys
17+
from pathlib import Path
18+
from assembler_modules import code
19+
from assembler_modules.parser import Parser
20+
from assembler_modules.command import Command
21+
22+
23+
__author__ = "Merrick Ryman"
24+
__version__ = "1.0"
25+
26+
27+
def main():
28+
asm_path = Path(sys.argv[-1])
29+
if not (asm_path.is_file() and (asm_path.suffix == '.asm')):
30+
raise IOError('Valid ASM file not supplied.')
31+
asm_parser = Parser(asm_path)
32+
33+
hack_path = str(asm_path.parent) + '/' + (asm_path.name[:asm_path.name.find('.')] + '.hack')
34+
with open(hack_path, 'w+') as hack_file:
35+
while asm_parser.has_more_commands():
36+
if asm_parser.command_type() is Command.A_COMMAND:
37+
hack_file.write(f'{int(asm_parser.symbol()):016b}' + '\n')
38+
elif asm_parser.command_type() is Command.C_COMMAND:
39+
comp = ''.join(str(bit) for bit in code.comp(asm_parser.comp()))
40+
dest = ''.join(str(bit) for bit in code.dest(asm_parser.dest()))
41+
jump = ''.join(str(bit) for bit in code.jump(asm_parser.jump()))
42+
hack_file.write('111' + comp + dest + jump + '\n')
43+
elif asm_parser.command_type() is Command.L_COMMAND:
44+
hack_file.close()
45+
print('Encountered an L_COMMAND. This version of the assembler does not support symbols!')
46+
raise NotImplementedError
47+
else:
48+
hack_file.close()
49+
raise ValueError('This command type does not exist.')
50+
51+
52+
if __name__ == "__main__":
53+
main()

0 commit comments

Comments
 (0)