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