-
Notifications
You must be signed in to change notification settings - Fork 1
/
emulator.py
257 lines (217 loc) · 9.38 KB
/
emulator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
import re
from codes import CodeDecoder
from executor import Executor
from utils import arg_parse
# TODO: try mult
class Emulator:
"""
Abandon all hope, ye who enter here.
"""
def __init__(self):
self._MAX_BITS = 16
self._REG_IOBUFFER_1 = 0xff04
self._register = {i: 0 for i in range(self._MAX_BITS)}
self._mult_register = [0] # Immutables cannot be passed in by value
self._memory = {i: '' for i in range(2 ** self._MAX_BITS)}
self._debug_memory = {i: '' for i in range(2 ** self._MAX_BITS)}
self._decoder = CodeDecoder()
self._executor = Executor(register=self._register,
mult_register=self._mult_register,
memory=self._memory)
self._pc = 0
self._message = ''
self._io_buffer = []
self._reg_io_init()
# FIXME: needs to end properly if we don't have infinite loops
def run(self, debug_mode='default'):
"""
:param debug_mode: can say 'break'
:return:
"""
debug_mode = debug_mode.lower().strip()
while True:
# For debugging
old_register = {k: v for k, v in self._register.items()}
old_memory = {k: v for k, v in self._memory.items()}
self._execute(debug=debug_mode)
# Shows changes in registers and memory
if debug_mode:
print({self._decoder.get_reg_decoded(k): v for k, v in self._register.items()})
self.print_changes(old_register, self._register, 'register')
self.print_changes(old_memory, self._memory, 'memory')
if debug_mode == 'break':
# Auto break point
input("Process halted. Press enter to continue.\n")
# FIXME: convoluted
def _execute(self, debug='default'):
instruction = self._get_word(self._pc)
comment = self._debug_memory[self._pc]
if debug:
print(f"PC {self._pc}: {instruction} {comment}")
self._pc += 2
if self._is_break_point(instruction):
self._activate_break_point(instruction)
elif self._is_load_iobuffer1(instruction):
if not self._io_buffer:
self._io_buffer = self._prompt_user()
self._memory[self._REG_IOBUFFER_1] = self._io_buffer.pop()
op_exec_type = self._decoder.get_op_exec_type(instruction)
if op_exec_type == 'reg_mem':
self._executor.execute(instruction, comment)
elif op_exec_type == 'jump_branch':
target_addr = self._executor.calc_addr(instruction, self._pc, comment)
self._pc = target_addr
else:
raise SyntaxError(f"Undefined instruction: {instruction}"
f"\n Instruction: {self._debug_memory[self._pc - 2]}")
# Print result of stored iobuffer
if self._is_store_iobuffer1(instruction):
char_out = self._memory[self._REG_IOBUFFER_1]
char_out = chr(int(char_out, 16))
print(char_out, end='')
@staticmethod
def _is_break_point(machine_code: str) -> bool:
"""
Activate break point if we try to copy a register to itself.
:param machine_code: a string of machine code, i.e. 0113
:return: a boolean
"""
is_alu = machine_code[0] == '0'
is_copy = machine_code[3] == '3'
same_reg = machine_code[1] == machine_code[2]
return all((is_alu, is_copy, same_reg))
def _activate_break_point(self, machine_code: str):
print("\nBreak point activated.")
reg_value = int(machine_code[1], 16)
print(f"The value of register "
f"{self._decoder.get_reg_decoded(reg_value)} is "
f"{self._register[reg_value]}")
print({self._decoder.get_reg_decoded(k): v for
k, v in self._register.items()})
while True:
break_key = input("Press ^ to continue.")
if break_key == '^':
break
# FIXME: make this cleaner
@staticmethod
def _prompt_user() -> list:
prompt = "\n"
def is_valid(ascii_encoding: str, sign_bit=False) -> bool:
"""
ASCII TABLE
45 : '-'
48 ~ 57 : '1 ~ 9'
:param ascii_encoding: char in ascii, stored as string
:param sign_bit: if it's the first bit or not
:return: boolean
"""
ascii_code = int(ascii_encoding, 16)
is_neg_sign = False
if sign_bit:
is_neg_sign = ascii_code == 0x2d
is_num = 0x30 <= ascii_code <= 0x39
return any((is_neg_sign, is_num))
# Checks that input is valid
while True:
input_string = input(prompt)
if len(input_string) > 6:
print(f"Expected input of maximum 6 characters, "
f"but got {input_string}")
continue
# [2:] needed to remove the "0x"
ascii_array = [hex(ord(char))[2:] for char in input_string]
valid = True
# If any of our ascii characters are not
# - or any of our decimal numerals
if not is_valid(ascii_array[0], sign_bit=True):
valid = False
print(f"{ascii_array[0]} is not a proper number")
for i in range(1, len(ascii_array)):
ascii_char = ascii_array[i]
if not is_valid(ascii_char):
valid = False
print(f"{ascii_char} is not a proper number")
break
# Reverse and add null terminator for popping
if valid:
ascii_array.reverse()
ascii_array = ['00'] + ascii_array
return ascii_array
def _is_load_iobuffer1(self, machine_code: str):
r2 = int(machine_code[2], 16)
r2_val = self._register[r2]
is_iobuffer_1 = r2_val == self._REG_IOBUFFER_1
# Hack by using magic numbers
lb_code = 8
lw_code = 9
opcode = int(machine_code[0], 16)
is_load = (opcode == lb_code) or (opcode == lw_code)
return is_load and is_iobuffer_1
def _is_store_iobuffer1(self, machine_code: str):
r2 = int(machine_code[2], 16)
r2_val = self._register[r2]
is_iobuffer_1 = r2_val == self._REG_IOBUFFER_1
# Hack by using magic numbers
sb_code = 10
sw_code = 11
opcode = int(machine_code[0], 16)
is_store = (opcode == sb_code) or (opcode == sw_code)
return is_store and is_iobuffer_1
@staticmethod
def print_changes(old_values: dict, new_values: dict, type_name: str):
for key, value in new_values.items():
if new_values[key] != old_values[key]:
print(f"Value of {type_name} {key} changed from "
f"{old_values[key]} to {new_values[key]}")
def _get_word(self, addr: int):
low_byte = self._memory[addr]
high_byte = self._memory[addr + 1]
word = high_byte + low_byte
return word
def _reg_io_init(self):
"""
Sets all bits of REG_IOCONTROL to 1
:return: None
"""
REG_IOCONTROL = 0xff00
self._memory[REG_IOCONTROL] = 'ffff'
def load_mif(self, mif_text: str):
"""
Parses mif files and stores instructions into memory.
Note: mif file indices are word-addressable,
so we need to convert them into byte-addressable addresses.
:param mif_text: A line-delineated string, the mif file
:return: None
"""
for line in mif_text.split('\n'):
regex_pattern = re.compile(r'^([0-9a-f]+) : ([0-9a-f]+);')
regex_match = re.match(regex_pattern, line)
if regex_match:
mif_index, instruction = regex_match.groups()
# Parse instruction into bytes
assert len(instruction) == 4, f'Expected word size 16 bits, ' \
f'but got {len(instruction)}' \
f'Instruction: {line}'
one_byte = 2
high_byte = instruction[:one_byte]
low_byte = instruction[one_byte:]
# Save instructions to memory addresses parsed from mif index
addr = int(mif_index, 16) * 2
self._memory[addr] = low_byte
self._memory[addr + 1] = high_byte
# For debugging
where_comment = line.find('--')
assert where_comment == 13, f"Comment should start at the 13th char," \
f"but got {line}"
comment = line[where_comment:]
self._debug_memory[addr] = comment
@property
def memory(self):
return self._memory
if __name__ == "__main__":
input_file, debug_arg = arg_parse(file_extension='.mif',
enforce_second=False)
emulator = Emulator()
with open(input_file, 'r') as f:
emulator.load_mif(mif_text=f.read())
emulator.run(debug_mode=debug_arg)