diff --git a/.gitignore b/.gitignore index 31a2ae473b61b58e212f445c2e5511401fc19ff1..dae54c3ada7b570b393b6ec95dc20570e1ef06c3 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,10 @@ test_save_to_file test_save_to_file_layout cpu_save_file +# mock files +mock* +dummy + *~ *.autosave *.a diff --git a/dummy b/dummy new file mode 100644 index 0000000000000000000000000000000000000000..50f62b930aa7bfb53b977188d487bb992fcd103b --- /dev/null +++ b/dummy @@ -0,0 +1,3 @@ +a: +value:: 1 + diff --git a/mock_file b/mock_file new file mode 100644 index 0000000000000000000000000000000000000000..704d1e4125be1a06e285f4c73c2210b690a96287 --- /dev/null +++ b/mock_file @@ -0,0 +1,75 @@ +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + +a: +value:: 1 + diff --git a/mock_file.txt b/mock_file.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/simudator/cli/cli.py b/src/simudator/cli/cli.py index 8cece34ab1733200d85c52e342092bc5f697c8fc..25017547485eddca37d2b7846442f12c377d712f 100644 --- a/src/simudator/cli/cli.py +++ b/src/simudator/cli/cli.py @@ -3,6 +3,7 @@ from __future__ import annotations import ast from os.path import exists, isdir +from simudator.cli.printing import pretty_print, pretty_print_verbose from simudator.core.processor import Processor HELP_TEXT = """Here is a list of possible commands: @@ -195,12 +196,12 @@ class CLI: case ["pp"] | ["pretty print"]: # pretty print the modules of the processor - self._processor.pretty_print() + pretty_print(self._processor) case ["ppv"] | ["pretty print verbose"]: # pretty print the modules of the processor # with all available information - self._processor.pretty_print_verbose() + pretty_print_verbose(self._processor) # Breakpoints ------------------------------------------------- case ["p", "br"] | ["print", "breaks"]: diff --git a/src/simudator/cli/printing.py b/src/simudator/cli/printing.py new file mode 100644 index 0000000000000000000000000000000000000000..42530c15ce3f01555278069070a2a8767fafb0af --- /dev/null +++ b/src/simudator/cli/printing.py @@ -0,0 +1,262 @@ +from simudator.core.module import Module +from simudator.core.modules.memory import Memory +from simudator.core.processor import Processor +from simudator.processor.mia.modules.bus import Bus +from simudator.processor.mia.modules.micro_memory import MicroMemory + + +def pretty_print(processor: Processor) -> None: + """Print the processor state in a readable and compact format.""" + pretty_print_verbose(processor, processor._ignore_keys) + + +def pretty_print_verbose( + processor: Processor, ignore_keys: list[str] | None = None +) -> None: + """ + Print the most relevant information about each module in a compact and + readable format. + + State variables of modules can be ignored with the optional argument. + + Parameters + ---------- + ignore_keys : list[str] + List of names of state variables of modules to exclude when + printing module states. + """ + # TODO: ignore keys per module and not for all modules + memory_modules = [] + other_modules = [] + + if ignore_keys is None: + ignore_keys = [] + + # TODO: remove isinstance(module, micro_memory) + for module in processor.get_modules(): + if not isinstance(module, Bus): + if isinstance(module, Memory) or isinstance(module, MicroMemory): + memory_modules.append(module) + else: + other_modules.append(module) + + # sort the modules by name to ensure that they appear in the + # same order + memory_modules.sort(key=lambda x: x.name, reverse=True) + other_modules.sort(key=lambda x: x.name, reverse=True) + + # specify which keys to ignore from the modules 'get_state' + # function + # TODO: ignore fields per module, this will ignore 'mask' + # for every module + + module_to_line_length = {} + # get the longest line length for all other_modules + for module in other_modules: + # +3 for padding + line_len = module.get_longest_line_len(ignore_keys) + 3 + + # TODO: what to do if two or more modules has the same name + module_to_line_length[module] = line_len + + groups = group_pp_modules(module_to_line_length) + + print(Processor.LINE_SEPARATOR * Processor.MAX_LINE_LEN) + + # print each group separate + for group in groups: + pretty_print_names(group) + + # Build a new string containing the information to show + # the user + for row in range(get_most_fields(group, ignore_keys)): + string = "" + total_padding = 0 + + # Get the information from each module + for module in group: + module_state = module.get_state() + keys = fields_to_list(module) + + # Get the keys that we want to print to the user + # If this is not done, the keys we want to print + # could end up on an index that is higher than the + # number of rows printed and will therefore be missed + real_keys = [key for key in keys if key not in ignore_keys] + + # Dont go out of index + # Needed since one module might want to print 4 + # fields and another only 1 + if row < len(real_keys) and real_keys[row] not in ignore_keys: + string += real_keys[row] + ": " + str(module_state[real_keys[row]]) + + # pad the string so each string has the same length + total_padding += module_to_line_length[module] + string = string.ljust(total_padding) + # replace last to chars with a separator and padding + string = string[0:-2] + "| " + + print(string) + print(Processor.LINE_SEPARATOR * Processor.MAX_LINE_LEN) + + for memory_module in memory_modules: + pretty_print_memory(memory_module) + + +def pretty_print_memory(module: Memory) -> None: + """Print a memory module in a compact and readable format. + + Parameters + ---------- + module : Memory + Memory module to print the state of. + """ + print(module.name) + print(Processor.LINE_SEPARATOR * Processor.MAX_LINE_LEN) + + longest_line_len = module.get_longest_line_len() + # longest_line_len = processor.get_longest_memory_value(module.memory) + string = "" + last_mem_len = module.get_largest_mem_adr() + + for i, value in enumerate(module.get_state()["memory"]): + + # create a new string containing the address and the value + # of the memory address formatted to fit with the largest + # address and largest memory value + new_row = (str(i).ljust(last_mem_len) + ": " + str(value)).ljust( + longest_line_len + last_mem_len + 3 + ) + "|" + + # only add the string if there is space for it, else + # print the string and start a new + if len(string + new_row) + 1 > Processor.MAX_LINE_LEN: + print(string) + string = new_row + else: + # First iteration string will be + # empty and should not be padded + if string: + string += " " + new_row + else: + string = new_row + print(string) + print(Processor.LINE_SEPARATOR * Processor.MAX_LINE_LEN) + print() + + +def pretty_print_names(module_to_line_length: dict[Module, int]) -> None: + """ + Print the name of the modules in one row with formatting. + + Adds spacing between the names so that the longest state variable of + each module has space to be printed below the name. + + Parameters + ---------- + module_to_line_length : dict[Module, int] + Mapping from module to length of the longest state variable of + the module. + """ + name_string = "" + total_len = 0 + for module in module_to_line_length: + name_string += module.name + total_len += module_to_line_length[module] + name_string = name_string.ljust(total_len) + name_string = name_string[0:-2] + "| " + + print(name_string) + + +def group_pp_modules( + module_to_line_length: dict[Module, int] +) -> list[dict[Module, int]]: + """Group the modules to be pretty printed into groups with a + total line length lower than 'Processor.MAX_LINE_LEN'. + + Parameters + ---------- + module_to_line_length : dict[Module, int] + Mapping from module to length of the longest state variable of + the module (takes as the line length). + + Returns + ------- + list[dict[Module, int]] + List of mappings from module to line length, each mapping + representing a group of modules for printing. + """ + + groups = [{}] + group_index = 0 + line_len = 0 + for module in module_to_line_length: + + # Make sure the line is not to long + if line_len + module_to_line_length[module] < Processor.MAX_LINE_LEN: + line_len += module_to_line_length[module] + groups[group_index][module] = module_to_line_length[module] + + # If it would have been, start a new group + else: + groups.append({}) + group_index += 1 + groups[group_index][module] = module_to_line_length[module] + line_len = module_to_line_length[module] + return groups + + +def fields_to_list(module: Module, ignore_keys=[]) -> list[str]: + """ + Return a list containing all state variable names, excluding module + name, for a module. + Optional argument to ignore specific state variables. + + Parameters + ---------- + module : Module + Module to get state variable names from. + ignore_keys : list[str] + List of state variable names to exclude. + + Returns + ------- + list[str] + List of state variable names, excluding module name, of a module. + """ + return [ + key for key in module.get_state() if key != "name" and key not in ignore_keys + ] + + +def get_most_fields(modules: dict[Module, int], ignore_keys=None) -> int: + """Get the maximum number of state variables among all modules. + + Can optionally ignore keys. + + Parameters + ---------- + modules : dict[Module, int] + Mapping from module to length of the longest state variable of + the module. Used as a list of modules to take the maximum over. + ignore_keys : list[str] + State variables to exclude. + + Returns + ------- + int + The maximum of the number of module state variables among all + modules of the processor. + """ + + if ignore_keys is None: + ignore_keys = [] + + fields = 0 + for module in modules: + module_fields = len(fields_to_list(module, ignore_keys)) + if module_fields > fields: + fields = module_fields + + return fields diff --git a/src/simudator/core/processor.py b/src/simudator/core/processor.py index 061bec12e280e77fa84bd28235f918e781fec6db..2485c5389b0f328ace9742e30eb5b25a93e485de 100644 --- a/src/simudator/core/processor.py +++ b/src/simudator/core/processor.py @@ -7,9 +7,6 @@ from simudator.core.breakpoint_lambda import LambdaBreakpoint from simudator.core.breakpoint_memory import MemoryBreakpoint from simudator.core.breakpoint_state import StateBreakpoint from simudator.core.module import Module -from simudator.core.modules.memory import Memory -from simudator.processor.mia.modules.bus import Bus -from simudator.processor.mia.modules.micro_memory import MicroMemory from .signal import Signal @@ -19,30 +16,38 @@ class Processor: Main class for controlling the processor. Uses modules and signals to simulate processor components behaviour. + + Attributes + ---------- + is_stopped : bool + Used to indicate that the processor has stopped, e.g. from a halt + instruction. + breakpoint_reached : bool + Used to indicate that the processor has reached a breakpoint at + the current clock cycle. + last_breakpoint : Breakpoint | None + Stores the last reached breakpoint if any. + """ __slots__ = ( - "modules", - "signals", - "clock", - "update_queue", - "module_history", - "signal_history", - "breakpoint_id_counter", - "breakpoints", + "_modules", + "_signals", + "_clock", + "_update_queue", + "_module_history", + "_signal_history", + "_breakpoint_id_counter", + "_breakpoints", "breakpoint_reached", "last_breakpoint", - "cycles_to_save", - "removed_cycles", - "max_line_len", - "line_separator", + "_removed_cycles", "is_stopped", - "new_instruction", - "current_instructions", - "ignore_keys", - "lambdas", - "assembly_cycles", - "line_separator", + "_new_instruction", + "_current_instructions", + "_ignore_keys", + "_lambdas", + "_assembly_cycles", ) LINE_SEPARATOR = "-" @@ -51,47 +56,42 @@ class Processor: def __init__(self) -> None: # For simulation - self.modules: dict[str, Module] = dict() - self.signals = [] - self.clock = 0 - self.update_queue = [] - self.is_stopped = False + self._modules: dict[str, Module] = dict() + self._signals: list[Signal] = [] + self._clock: int = 0 + self._update_queue: list[Module] = [] + self.is_stopped: bool = False # Breakpoint handling - self.breakpoint_id_counter = 1 - self.breakpoints: dict[int, Breakpoint] = {} - self.breakpoint_reached = False - self.last_breakpoint = None - self.lambdas: dict[str, Callable[..., bool]] = {} + self._breakpoint_id_counter: int = 1 + self._breakpoints: dict[int, Breakpoint] = {} + self.breakpoint_reached: bool = False + self.last_breakpoint: Breakpoint | None = None + self._lambdas: dict[str, Callable[..., bool]] = {} # Saving processor state for saving/loading to file and undoing ticks - self.removed_cycles = 0 - self.assembly_cycles = [0] # Map asm instruction to clock cycle - self.module_history: list[dict[str, dict[str, Any]]] = [] - self.signal_history: list[list] = [] # TODO: Is this needed? + self._removed_cycles: int = 0 + self._assembly_cycles: list[int] = [0] # Map asm instruction to clock cycle + self._module_history: list[dict[str, dict[str, Any]]] = [] + self._signal_history: list[list] = [] # TODO: Is this needed? # For showing which instructions are being done and signalling # the start of a new one - self.new_instruction = False + self._new_instruction: bool = False # It is the responsibility of the CPU to tell the gui:s pipeline # diagram what information to be displayed and how to display it. # Thus each CPU keeps track of its current instructions together with # each instructions position in the pipeline diagram. - self.current_instructions: list[tuple[str, int, int]] = [] + self._current_instructions: list[tuple[str, int, int]] = [] # TODO: keeping track of what pieces of info not to show # show not be done at the processor level. # Maybe implement a 'get_pretty_print_state' at module # level? - self.ignore_keys = [ + self._ignore_keys: list[str] = [ "bit_length", "mask", - "increment", - "read_from_bus", - "read_from_uADR", - "decrement_by_one", - "bus_id", ] def do_tick(self) -> None: @@ -101,25 +101,25 @@ class Processor: Also check for breakpoints that are reached in this cycle, and save clock cycles when new assembly instructions are started. """ - if len(self.module_history) > self.clock - self.removed_cycles: + if len(self._module_history) > self._clock - self._removed_cycles: # If a previous stored cycle has been loaded, discard # all stored cycles from that cycle and onward in # the history of saved cycles - self.module_history = self.module_history[0 : self.clock] - self.signal_history = self.signal_history[0 : self.clock] + self._module_history = self._module_history[0 : self._clock] + self._signal_history = self._signal_history[0 : self._clock] self.unstop() self.save_cycle() - self.clock += 1 + self._clock += 1 - for module in self.modules.values(): + for module in self._modules.values(): module.update_register() - for module in self.modules.values(): + for module in self._modules.values(): module.output_register() - while self.update_queue: - module = self.update_queue.pop(0) + while self._update_queue: + module = self._update_queue.pop(0) module.update_logic() self.stop_at_breakpoints() @@ -129,11 +129,11 @@ class Processor: # Set new_instruction and save clock cycle for each # new assembly instruction if self.is_new_instruction(): - self.new_instruction = True - self.current_instructions = self.get_current_instructions() - self.assembly_cycles.append(self.get_clock()) + self._new_instruction = True + self._current_instructions = self.get_current_instructions() + self._assembly_cycles.append(self.get_clock()) else: - self.new_instruction = False + self._new_instruction = False def get_current_instructions(self) -> list[tuple[str, int, int]]: """Return a list of the current instructions with their positions. @@ -207,14 +207,14 @@ class Processor: Number of assembly instructions to undo. """ - current_clock_cycle = self.clock - index = len(self.assembly_cycles) - saved_cycle = self.assembly_cycles[index - 1] + current_clock_cycle = self._clock + index = len(self._assembly_cycles) + saved_cycle = self._assembly_cycles[index - 1] # Make sure we are undoing the instruction(s) we are currently on while saved_cycle >= current_clock_cycle: index -= 1 - saved_cycle = self.assembly_cycles[index - 1] + saved_cycle = self._assembly_cycles[index - 1] index -= num_instructions @@ -222,7 +222,7 @@ class Processor: if index < 0: raise IndexError - clockcycle = self.assembly_cycles[index] + clockcycle = self._assembly_cycles[index] self.load_cycle(clockcycle) @@ -230,7 +230,7 @@ class Processor: # load the start state. This is done since we only append clock # cycles to the list self.assembly_cycles when we reach a new state # that has uPC set to 0, which wont happen when we load a new file. - self.assembly_cycles = self.assembly_cycles[: index + 1] + self._assembly_cycles = self._assembly_cycles[: index + 1] def run_continuously(self) -> None: """ @@ -275,7 +275,7 @@ class Processor: Should be implemented per CPU, return field new_instruction. It is up to each processor to set this field to True/False correctly. """ - return self.new_instruction + return self._new_instruction def stop_at_breakpoints(self) -> None: """Stop the execution if any breakpoint has been reached during this @@ -284,7 +284,7 @@ class Processor: Also record the breakpoint that was reached. """ self.breakpoint_reached = False - for _, bp in self.breakpoints.items(): + for _, bp in self._breakpoints.items(): # TODO: Can make this more efficient by only checking enabled # breakpoints if bp.is_break() and bp.is_enabled: @@ -300,18 +300,18 @@ class Processor: This resets all modules and removes any saved states for undoing clock cycles. A round of value propagation is done to reset signals too. """ - self.clock = 0 - self.module_history.clear() - self.signal_history.clear() - self.removed_cycles = 0 - self.assembly_cycles = [0] + self._clock = 0 + self._module_history.clear() + self._signal_history.clear() + self._removed_cycles = 0 + self._assembly_cycles = [0] - for module in self.modules.values(): + for module in self._modules.values(): module.reset() module.output_register() - while self.update_queue: - module = self.update_queue.pop(0) + while self._update_queue: + module = self._update_queue.pop(0) module.update_logic() def add_modules_to_update(self, module: Module) -> None: @@ -327,8 +327,8 @@ class Processor: Module.update_logic : Method for updating a module. """ - if module not in self.update_queue: - self.update_queue.append(module) + if module not in self._update_queue: + self._update_queue.append(module) def add_module(self, module: Module) -> None: """Add module to be simulated by the processor. @@ -338,7 +338,7 @@ class Processor: module : Module Module to add. """ - self.modules[module.name] = module + self._modules[module.name] = module def get_module(self, name: str) -> Module: """Get module with specific name. @@ -353,7 +353,7 @@ class Processor: Module The module with the specified name. """ - return self.modules[name] + return self._modules[name] def get_modules(self) -> list[Module]: """Get list of all modules. @@ -363,7 +363,7 @@ class Processor: list[Module] List of all modules in the processor. """ - return list(self.modules.values()) + return list(self._modules.values()) def add_signals(self, signals: list[Signal]) -> None: """Add signals to the processor. @@ -373,7 +373,7 @@ class Processor: list[Signal] List of signals to add for simulation. """ - self.signals += signals + self._signals += signals def get_clock(self) -> int: """Get the current clockcycle number. @@ -383,7 +383,7 @@ class Processor: int Current clock cycle number of the processor. """ - return self.clock + return self._clock def set_clock(self, value: int) -> None: """Set current clockcycle number. @@ -393,7 +393,7 @@ class Processor: value : int Cycle number to set the clock to. """ - self.clock = value + self._clock = value def save_cycle(self) -> None: """ @@ -404,13 +404,13 @@ class Processor: # Only save a specified number of cycles, # saving every cycle can easily eat all ram - if len(self.module_history) > Processor.CYCLES_TO_SAVE: - self.module_history.pop(0) - self.removed_cycles += 1 + if len(self._module_history) > Processor.CYCLES_TO_SAVE: + self._module_history.pop(0) + self._removed_cycles += 1 - for module in self.modules.values(): + for module in self._modules.values(): module_states[module.name] = module.get_state() - self.module_history.append(module_states) + self._module_history.append(module_states) def load_cycle(self, cycle: int) -> None: """Load the state of all modules as they were at the specified clock @@ -424,26 +424,26 @@ class Processor: Number of the cycle to load. """ - cycle_index = cycle - self.removed_cycles + cycle_index = cycle - self._removed_cycles if cycle_index < 0: raise ValueError("The cycle to be loaded is not saved") try: - module_states = self.module_history[cycle_index] + module_states = self._module_history[cycle_index] except IndexError: raise IndexError for module_name, module_state in module_states.items(): - self.modules[module_name].set_state(module_state) + self._modules[module_name].set_state(module_state) - self.clock = cycle + self._clock = cycle - for module in self.modules.values(): + for module in self._modules.values(): module.output_register() - while self.update_queue: - module = self.update_queue.pop(0) + while self._update_queue: + module = self._update_queue.pop(0) module.update_logic() def load_state_from_file(self, file_path: str) -> None: @@ -512,265 +512,26 @@ class Processor: file.write("") file.close() - for module in self.modules.values(): - res =module.save_state_to_file(file_path) + for module in self._modules.values(): + res = module.save_state_to_file(file_path) if not res: return False - - return True - - def pretty_print(self) -> None: - """Print the processor state in a readable and compact format.""" - self.pretty_print_verbose(self.ignore_keys) - - def pretty_print_verbose(self, ignore_keys=None) -> None: - """ - Print the most relevant information about each module in a compact and - readable format. - - State variables of modules can be ignored with the optional argument. - - Parameters - ---------- - ignore_keys : list[str] - List of names of state variables of modules to exclude when - printing module states. - """ - # TODO: ignore keys per module and not for all modules - memory_modules = [] - other_modules = [] - - if ignore_keys is None: - ignore_keys = [] - - # TODO: remove isinstance(module, micro_memory) - for module in self.modules.values(): - if not isinstance(module, Bus): - if isinstance(module, Memory) or isinstance(module, MicroMemory): - memory_modules.append(module) - else: - other_modules.append(module) - - # sort the modules by name to ensure that they appear in the - # same order - memory_modules.sort(key=lambda x: x.name, reverse=True) - other_modules.sort(key=lambda x: x.name, reverse=True) - - # specify which keys to ignore from the modules 'get_state' - # function - # TODO: ignore fields per module, this will ignore 'mask' - # for every module - - module_to_line_length = {} - # get the longest line length for all other_modules - for module in other_modules: - # +3 for padding - line_len = module.get_longest_line_len(ignore_keys) + 3 - - # TODO: what to do if two or more modules has the same name - module_to_line_length[module] = line_len - - groups = self.group_pp_modules(module_to_line_length) - - print(Processor.LINE_SEPARATOR * Processor.MAX_LINE_LEN) - - # print each group separate - for group in groups: - self.pretty_print_names(group) - - # Build a new string containing the information to show - # the user - for row in range(self.get_most_fields(group, ignore_keys)): - string = "" - total_padding = 0 - - # Get the information from each module - for module in group: - module_state = module.get_state() - keys = self.fields_to_list(module) - - # Get the keys that we want to print to the user - # If this is not done, the keys we want to print - # could end up on an index that is higher than the - # number of rows printed and will therefore be missed - real_keys = [key for key in keys if key not in ignore_keys] - - # Dont go out of index - # Needed since one module might want to print 4 - # fields and another only 1 - if row < len(real_keys) and real_keys[row] not in ignore_keys: - string += ( - real_keys[row] + ": " + str(module_state[real_keys[row]]) - ) - - # pad the string so each string has the same length - total_padding += module_to_line_length[module] - string = string.ljust(total_padding) - # replace last to chars with a separator and padding - string = string[0:-2] + "| " - - print(string) - print(Processor.LINE_SEPARATOR * Processor.MAX_LINE_LEN) - - for memory_module in memory_modules: - self.pretty_print_memory(memory_module) - - def pretty_print_memory(self, module: Memory) -> None: - """Print a memory module in a compact and readable format. - - Parameters - ---------- - module : Memory - Memory module to print the state of. - """ - print(module.name) - print(Processor.LINE_SEPARATOR * Processor.MAX_LINE_LEN) - - longest_line_len = module.get_longest_line_len() - # longest_line_len = self.get_longest_memory_value(module.memory) - string = "" - last_mem_len = module.get_largest_mem_adr() - - for i, value in enumerate(module.get_state()["memory"]): - - # create a new string containing the address and the value - # of the memory address formatted to fit with the largest - # address and largest memory value - new_row = (str(i).ljust(last_mem_len) + ": " + str(value)).ljust( - longest_line_len + last_mem_len + 3 - ) + "|" - - # only add the string if there is space for it, else - # print the string and start a new - if len(string + new_row) + 1 > Processor.MAX_LINE_LEN: - print(string) - string = new_row - else: - # First iteration string will be - # empty and should not be padded - if string: - string += " " + new_row - else: - string = new_row - print(string) - print(Processor.LINE_SEPARATOR * Processor.MAX_LINE_LEN) - print() - - def pretty_print_names(self, module_to_line_length: dict[Module, int]) -> None: - """ - Print the name of the modules in one row with formatting. - - Adds spacing between the names so that the longest state variable of - each module has space to be printed below the name. - - Parameters - ---------- - module_to_line_length : dict[Module, int] - Mapping from module to length of the longest state variable of - the module. - """ - name_string = "" - total_len = 0 - for module in module_to_line_length: - name_string += module.name - total_len += module_to_line_length[module] - name_string = name_string.ljust(total_len) - name_string = name_string[0:-2] + "| " - - print(name_string) - - def group_pp_modules( - self, module_to_line_length: dict[Module, int] - ) -> list[dict[Module, int]]: - """Group the modules to be pretty printed into groups with a - total line length lower than 'Processor.MAX_LINE_LEN'. - - Parameters - ---------- - module_to_line_length : dict[Module, int] - Mapping from module to length of the longest state variable of - the module (takes as the line length). - Returns - ------- - list[dict[Module, int]] - List of mappings from module to line length, each mapping - representing a group of modules for printing. - """ - - groups = [{}] - group_index = 0 - line_len = 0 - for module in module_to_line_length: - - # Make sure the line is not to long - if line_len + module_to_line_length[module] < Processor.MAX_LINE_LEN: - line_len += module_to_line_length[module] - groups[group_index][module] = module_to_line_length[module] - - # If it would have been, start a new group - else: - groups.append({}) - group_index += 1 - groups[group_index][module] = module_to_line_length[module] - line_len = module_to_line_length[module] - return groups + return True - def fields_to_list(self, module: Module, ignore_keys=[]) -> list[str]: + def get_breakpoints(self) -> dict[int, Breakpoint]: """ - Return a list containing all state variable names, excluding module - name, for a module. - Optional argument to ignore specific state variables. - - Parameters - ---------- - module : Module - Module to get state variable names from. - ignore_keys : list[str] - List of state variable names to exclude. + Get the breakpoints in the processor as a map from breakpoint id to + breakpoint. Returns ------- - list[str] - List of state variable names, excluding module name, of a module. + dict[int, Breakpoint] + Map from breakpoint id to breakpoint, containing all breakpoints + in the processor. """ - return [ - key - for key in module.get_state() - if key != "name" and key not in ignore_keys - ] - - def get_most_fields(self, modules: dict[Module, int], ignore_keys=None) -> int: - """Get the maximum number of state variables among all modules. - - Can optionally ignore keys. - - Parameters - ---------- - modules : dict[Module, int] - Mapping from module to length of the longest state variable of - the module. Used as a list of modules to take the maximum over. - ignore_keys : list[str] - State variables to exclude. - - Returns - ------- - int - The maximum of the number of module state variables among all - modules of the processor. - """ - - if ignore_keys is None: - ignore_keys = [] - - fields = 0 - for module in modules: - module_fields = len(self.fields_to_list(module, ignore_keys)) - if module_fields > fields: - fields = module_fields - - return fields + return self._breakpoints def add_state_breakpoint( self, module_name: str, state_name: str, value: Any @@ -794,17 +555,17 @@ class Processor: exist in the module. """ - if module_name not in self.modules: + if module_name not in self._modules: raise ValueError(f"No module named {module_name}") - module = self.modules[module_name] + module = self._modules[module_name] if state_name not in module.get_state(): raise ValueError(f"No state named {state_name} in " f"module {module_name}") bp = StateBreakpoint(module, state_name, value) - self.breakpoints[self.breakpoint_id_counter] = bp - self.breakpoint_id_counter += 1 + self._breakpoints[self._breakpoint_id_counter] = bp + self._breakpoint_id_counter += 1 def remove_breakpoint(self, id: int) -> bool: """Remove a specific breakpoint. @@ -814,17 +575,17 @@ class Processor: id : int ID of the breakpoint to be removed. """ - if id in self.breakpoints: - del self.breakpoints[id] + if id in self._breakpoints: + del self._breakpoints[id] return True return False def print_breakpoints(self) -> None: """Print all breakpoints of the processor.""" - if not self.breakpoints: + if not self._breakpoints: print("There are no breakpoints.") else: - for bp_id, bp in self.breakpoints.items(): + for bp_id, bp in self._breakpoints.items(): print(f"BP {bp_id}: {bp}") def add_memory_breakpoint(self, module_name: str, address: int, value: Any) -> None: @@ -845,16 +606,16 @@ class Processor: If the module does not exist or is not a memory. """ - if module_name not in self.modules: + if module_name not in self._modules: raise ValueError(f"No module named {module_name}") - module = self.modules[module_name] + module = self._modules[module_name] if "memory" not in module.get_state(): raise ValueError(f"Module {module_name} is not a memory.") bp = MemoryBreakpoint(module, address, value) - self.breakpoints[self.breakpoint_id_counter] = bp - self.breakpoint_id_counter += 1 + self._breakpoints[self._breakpoint_id_counter] = bp + self._breakpoint_id_counter += 1 def get_breakpoint_lambdas(self) -> list[str]: """Get all functions available for lambda breakpoints. @@ -866,7 +627,7 @@ class Processor: for adding lambda breakpoints. """ - return list(self.lambdas.keys()) + return list(self._lambdas.keys()) def add_lambda_breakpoint(self, lambda_name: str, **kwargs) -> None: """Add a lambda breakpoint to the processor. @@ -894,16 +655,16 @@ class Processor: # modules as arguments without having direct access to them for key in kwargs.keys(): value = kwargs[key] - if value in self.modules: - kwargs[key] = self.modules[value] + if value in self._modules: + kwargs[key] = self._modules[value] - if lambda_name not in self.lambdas: + if lambda_name not in self._lambdas: raise ValueError(f"No lambda named {lambda_name}.") - lambda_func = self.lambdas[lambda_name] + lambda_func = self._lambdas[lambda_name] bp = LambdaBreakpoint(lambda_func, **kwargs) - self.breakpoints[self.breakpoint_id_counter] = bp - self.breakpoint_id_counter += 1 + self._breakpoints[self._breakpoint_id_counter] = bp + self._breakpoint_id_counter += 1 def set_enabled_breakpoint(self, bp_id: int, is_enabled: bool) -> None: """Toggle a breakpoint to enabled or disabled. @@ -915,4 +676,4 @@ class Processor: is_enabled : bool ``True`` to enable, ``False`` to disable the breakpoint. """ - self.breakpoints[bp_id].set_enabled(is_enabled) + self._breakpoints[bp_id].set_enabled(is_enabled) diff --git a/src/simudator/gui/breakpoint_window.py b/src/simudator/gui/breakpoint_window.py index ca19345436f974868d30ba1bdd3539bd02b60068..649364e0b6690d87e959c3161a9df2f72770c008 100644 --- a/src/simudator/gui/breakpoint_window.py +++ b/src/simudator/gui/breakpoint_window.py @@ -53,7 +53,7 @@ class BreakpointWindow(QWidget): stop_icon = self.style().standardIcon(QStyle.SP_DialogCancelButton) # Add an list item for each breakpoint in cpu - for bp_id, breakpoint in self.cpu.breakpoints.items(): + for bp_id, breakpoint in self.cpu.get_breakpoints().items(): bp_str = str(bp_id) + ": " + breakpoint.__str__() bp_item = QListWidgetItem(bp_str) @@ -95,7 +95,7 @@ class BreakpointWindow(QWidget): if bp_id is None: return - del self.cpu.breakpoints[bp_id] + self.cpu.remove_breakpoint(bp_id) self.update() def getSelectedItemId(self) -> int: @@ -120,4 +120,4 @@ class BreakpointWindow(QWidget): # Do nothing if there are no breakpoints or if id is nonetype if bp_id is None or bp_id == -1: return - return self.cpu.breakpoints[bp_id] + return self.cpu.get_breakpoints()[bp_id] diff --git a/src/simudator/gui/cpu_graphics_scene.py b/src/simudator/gui/cpu_graphics_scene.py index 47292c4ec7552e6f75e0243cbb44009a5bdee77b..a64e417b1dfa6a4b9d834bb031373a2f9364a3f1 100644 --- a/src/simudator/gui/cpu_graphics_scene.py +++ b/src/simudator/gui/cpu_graphics_scene.py @@ -1,8 +1,22 @@ -from qtpy.QtWidgets import QGraphicsItem, QGraphicsScene +import json +from json.decoder import JSONDecodeError + +from qtpy.QtCore import QPointF +from qtpy.QtCore import Signal as pyqtSignal +from qtpy.QtCore import Slot +from qtpy.QtWidgets import ( + QErrorMessage, + QFileDialog, + QGraphicsItem, + QGraphicsScene, + QMessageBox, +) from simudator.core.processor import Processor from simudator.gui.color_scheme import ColorScheme from simudator.gui.module_graphics_item.module_graphics_item import ModuleGraphicsItem +from simudator.gui.orientation import Orientation +from simudator.gui.port_graphics_item import PortGraphicsItem from simudator.gui.signal_graphics_item import SignalGraphicsItem @@ -10,69 +24,78 @@ class CpuGraphicsScene(QGraphicsScene): """ This class creates the graphics scene for visualising the processor. It takes each module representation as a ModuleGraphicsItem - and handels mouse inputs for interacting with these graphicsitems. + and handles mouse inputs for interacting with these graphicsitems. Can create a default layout on creation and can save/load new layouts. """ MODULE_SPACEING = 100 - def __init__(self, cpu: Processor): + def __init__(self): super().__init__() - self.cpu = cpu - self.module_graphics_items = dict() - self.signal_graphics_items = [] + self._module_graphics_items = dict() + self._signal_graphics_items = [] + self._error_msg_box = QErrorMessage() self.setBackgroundBrush(ColorScheme.Window) - def resetSignals(self) -> None: + def reset_signals(self) -> None: """ - Resets all graphical signals to their default visual representation + Reset all graphical signals to their default visual representation when initialised. """ - for graphics_Signal in self.signal_graphics_items: - graphics_Signal.reset() + for graphics_signal in self._signal_graphics_items: + graphics_signal.reset() - def addModuleGraphicsItem(self, graphics_item: ModuleGraphicsItem) -> None: - """ - Takes a ModuleGraphicsItem and adds it to the scene. - Will give it a new position according to the deafult layout. + def add_module(self, item: ModuleGraphicsItem) -> None: """ - self.module_graphics_items[graphics_item.name] = graphics_item - self.placeModuleGraphicsItemDefault(graphics_item) + Add a graphical module item to the processor scene at a position + according to the default layout. - def placeModuleGraphicsItemDefault(self, graphics_item: ModuleGraphicsItem) -> None: - """ - Places a module graphics items at a position where the x value - is equal to the y value. Used to place all graphics items in a - diagonal. + Parameters + ---------- + item : ModuleGraphicsItem + Graphical module item to add to the scene. """ - placement = len(self.module_graphics_items) * self.MODULE_SPACEING - graphics_item.setPos(placement, placement) - self.addItem(graphics_item) + self._module_graphics_items[item.name] = item + self._place_module_default(item) - def replaceModuleGraphicsItem( - self, graphics_item: ModuleGraphicsItem, pos_x: int, pos_y: int - ) -> None: + def _place_module_default(self, item: ModuleGraphicsItem) -> None: """ - Changes the postions of an existing modules graphics item. + Place a graphical module item at a position according to the default + layout, i.e. placing all module items along a diagonal. + + Parameters + ---------- + item : ModuleGraphicsItem """ - graphics_item.setPos(pos_x * self.MODULE_SPACEING, pos_y * self.MODULE_SPACEING) + placement = len(self._module_graphics_items) * self.MODULE_SPACEING + item.setPos(placement, placement) + self.addItem(item) - def updateGraphicsItems(self): + def move_module_to(self, item: ModuleGraphicsItem, pos_x: int, pos_y: int) -> None: """ - Used to update graphicsitems when modules in the processor has chnaged values. + Place an existing graphical module item at some position. + + Parameters + ---------- + item : ModuleGraphicsItem + Graphical module item to place at a position. + pos_x : int + Position on x-axis to place the module item at. + pos_y : int + Position on y-axis to place the module item at. """ - for graphics_item in self.module_graphics_items.values(): - graphics_item.update() + item.setPos(pos_x * self.MODULE_SPACEING, pos_y * self.MODULE_SPACEING) - def addAllSignals(self) -> None: + def add_all_signals(self) -> None: """ - Instantiates signals between all matching ports of all modules. + Instantiate graphical signals between all matching ports of all + graphical modules. """ # Map which ports that are connected using their signals' names # (Every port keeps a reference to a simulation signal which has # a name) signal_to_ports = {} - for module_w in self.module_graphics_items.values(): + for module_w in self._module_graphics_items.values(): ports = module_w.getPorts() for port in ports: s_name = port.getSignalName() @@ -88,96 +111,317 @@ class CpuGraphicsScene(QGraphicsScene): port_1 = module_graphics_items[0] port_2 = module_graphics_items[1] signal_w = SignalGraphicsItem(port_1, port_2) - self.signal_graphics_items.append(signal_w) + self._signal_graphics_items.append(signal_w) self.addItem(signal_w) port_1.moved.connect(signal_w.move) port_2.moved.connect(signal_w.move) port_1.toggled.connect(signal_w.toggleVisibility) port_2.toggled.connect(signal_w.toggleVisibility) - def getModulesGraphicsItems(self) -> list[ModuleGraphicsItem]: - return list(self.module_graphics_items.values()) + def get_modules(self) -> list[ModuleGraphicsItem]: + """ + Get a list of all graphical module items in the scene. - def moduleGraphicsItemsDict(self) -> dict[str, ModuleGraphicsItem]: - return self.module_graphics_items + Returns + ------- + list[ModuleGraphicsItem] + List of graphical module items in the scene. + """ + return list(self._module_graphics_items.values()) - def getSignalGraphicsItems(self) -> list[SignalGraphicsItem]: - return self.signal_graphics_items + def get_signals(self) -> list[SignalGraphicsItem]: + """ + Get a list of all graphical signal items in the scene. + + Returns + ------- + list[SignalGraphicsItem] + List of graphical signal items in the scene. + """ + return self._signal_graphics_items def mousePressEvent(self, event): super().mousePressEvent(event) - def load_layout_from_file(self, file_path: str) -> None: + @Slot() + def load_layout(self) -> None: """ - Loads a layout for a processor from a saved filed. + Prompt the user for a layout file and load the layout from the file. """ - file = open(file_path) - - graphics_item_name = None - graphics_item_str = "" - # Go through each line in the file - for line in file.readlines(): - # If no name currently saved then get name from current line - if graphics_item_name is None: - graphics_item_name = line.partition(":")[0] + # Prompt the user for a file + dialog = QFileDialog() + dialog.setFileMode(QFileDialog.AnyFile) + dialog.setAcceptMode(QFileDialog.AcceptOpen) + dialog.setDirectory("~/simudator") # TODO: does this work when exported? + path = dialog.getOpenFileName()[0] - # if already has name get line content - else: - # if not empty line, then save content - if line.strip(): - graphics_item_str += line + # If no file was selected, do nothing + if path == '': + return - # if line is empty then give saved content to name file - else: - graphics_item = self.get_module(graphics_item_name) - try: - graphics_item.load_state_from_str(graphics_item_str) - except Exception as exc: - raise exc - - # set back to empty - graphics_item_str = "" - graphics_item_name = None + try: + self.load_layout_from_file(path) - # give module anything that is left - if graphics_item_name and graphics_item_str: - graphics_item = self.get_module(graphics_item_name) - graphics_item.load_state_from_str(graphics_item_str) + # Anything goes wrong with loading the selected one, + # we dont care about what went wrong + except (OSError, JSONDecodeError): + self.load_default_layout() + self._error_msg_box.showMessage( + "Unable to load given file.", "load_layout_err" + ) + else: + QMessageBox.information( + self.parent(), "SimuDator", "File loaded succesfully." + ) - def save_layout_to_file(self, file_path: str) -> None: + def _save_layout_to_file(self, file_path: str) -> None: """ - Saves the positions and visibility of graphicsitems in the scene (this - includes module and signal representations) to a file. + Save the layout of the scene to file. + + Parameters + ---------- + file_path : str + Path to the file to save to layout to. """ layout_str = "" - for graphics_item in self.module_graphics_items.values(): + for graphics_item in self._module_graphics_items.values(): layout_str += graphics_item.save_state_as_str() + "\n" file = open(file_path, "w") file.write(layout_str) file.close() - def setAllSignalsVisibility(self, is_signals_visible: bool) -> None: - for item in self.signal_graphics_items: - item.setVisible(is_signals_visible) + @Slot(bool) + def show_all_signals(self, value: bool) -> None: + """ + Set the visibility of all graphical signals in the scene. - def setPortNamesVisibility(self, is_ports_visible: bool) -> None: - for item in self.module_graphics_items.values(): + Parameters + ---------- + value : bool + ``True`` to show all graphical signals, ``False`` to hide them. + """ + for item in self._signal_graphics_items: + item.setVisible(value) + + @Slot(bool) + def show_port_names(self, value: bool) -> None: + """ + Set the visibility of the names of all ports of graphical modules + in the scene. + + Parameters + ---------- + value : bool + ``True`` to show the port names, ``False`` to hide them. + """ + for item in self._module_graphics_items.values(): for port in item.ports: - port.setNameVisibility(is_ports_visible) + port.setNameVisibility(value) - def setLayoutLock(self, is_layout_locked: bool) -> None: + def toggle_layout_lock(self, value: bool) -> None: """ - Toggles the layout lock making it so items in the scene can not be moved. + Toggle the layout between locked and unlocked. Locked means that + nothing can be moved around in the layout. + + Parameters + ---------- + value : bool + ``True`` to lock the layout, ``False`` to unlock it. """ - for item in self.module_graphics_items.values(): - item.setFlag(QGraphicsItem.ItemIsMovable, not is_layout_locked) - item.setLocked(is_layout_locked) + for item in self._module_graphics_items.values(): + item.setFlag(QGraphicsItem.ItemIsMovable, not value) + item.setLocked(value) - for item in self.signal_graphics_items: - item.setFlag(QGraphicsItem.ItemIsMovable, not is_layout_locked) + for item in self._signal_graphics_items: + item.setFlag(QGraphicsItem.ItemIsMovable, not value) # We use this value so lines in the signal can not be moved or edited - item.is_locked = is_layout_locked + item.is_locked = value + + @Slot() + def save_layout(self) -> None: + """ + Prompt the user for a file and save the scene layout to the file. + + This overwrites the previous content of the file. + """ + # Prompt the user for a file + dialog = QFileDialog() + dialog.setFileMode(QFileDialog.AnyFile) + dialog.setAcceptMode(QFileDialog.AcceptOpen) + path = dialog.getSaveFileName()[0] + + # Open the given file erasing the previous content + with open(path, "w") as fp: + graphics_modules_data = {} + ports_data = {} + graphics_signals_data = {} + + graphics_modules = self.get_modules() + for graphics_module in graphics_modules: + pos = (graphics_module.x(), graphics_module.y()) + graphics_modules_data[graphics_module.getName()] = pos + + for port in graphics_module.getPorts(): + visibility = port.isVisible() + orientation = int(port.getOrientation()) + data = (port.x(), port.y(), orientation, visibility) + ports_data[port.getID()] = data + + for graphics_signal in self.get_signals(): + visibility = graphics_signal.isVisible() + points = [] + for point in graphics_signal.getPoints(): + points.append((point.x(), point.y())) + data = (points, visibility) + graphics_signals_data[graphics_signal.getID()] = data + + data = (graphics_modules_data, ports_data, graphics_signals_data) + json.dump(data, fp) + + fp.close() + + @Slot() + def load_default_layout(self) -> None: + """ + Place all graphical modules in the scene according to the default + layout, i.e. along a diagonal. + """ + counter = 0 + for key in self._module_graphics_items: + module_graphic = self._module_graphics_items[key] + module_graphic.showPorts() + counter += 1 + self.move_module_to(module_graphic, counter, counter) + self.reset_signals() + + def load_layout_from_file(self, file_path: str) -> None: + """ + Load a layout from a file. + + If at anypoint this function would error, the default layout + is loaded instead. + + Parameters + ---------- + file_path : str + Path to a file containing a layout for the processor scene. + """ + graphics_modules = self._module_graphics_items + ports = {} + graphics_signals = {} + + for graphics_module in graphics_modules.values(): + for port in graphics_module.getPorts(): + ports[port.getID()] = port + + for graphics_signal in self.get_signals(): + graphics_signals[graphics_signal.getID()] = graphics_signal + + # Open the file in 'read-only' + with open(file_path, 'rb') as fp: + data = json.load(fp) + graphics_modules_data = data[0] + ports_data = data[1] + graphics_signals_data = data[2] + + for g_module_name, g_module_data in graphics_modules_data.items(): + g_module = graphics_modules[g_module_name] + x = g_module_data[0] + y = g_module_data[1] + self._load_module(g_module, x, y) + + for port_id, port_data in ports_data.items(): + port = ports[int(port_id)] + self._load_port(port, *port_data) + + for g_signal_id, g_signal_data in graphics_signals_data.items(): + g_signal = graphics_signals[int(g_signal_id)] + self._load_signal(g_signal, *g_signal_data) + + fp.close() + + def _load_signal( + self, + signal: SignalGraphicsItem, + signal_points: list[tuple[float, float]], + visibility: bool, + ) -> None: + """ + Set the positions and visibility of a graphical signal. Helper method + for loading a layout from file. + + Parameters + ---------- + signal : SignalGraphicsItem + Graphical signal to modify. + signal_points : list[tuple[float, float]] + List of points for visually drawing the signal as line. + visibility : bool + Visibility of the signal. ``True`` to show it, ``False`` to hide it. + """ + qpoints = [] + # Turn points -> QPointF + # list[int, int] -> QPointF object + for point in signal_points: + qpoints.append(QPointF(point[0], point[1])) + + # Set the new points + signal.setPoints(qpoints) + signal.setVisible(visibility) + + def _load_module( + self, + module: ModuleGraphicsItem, + pos_x: float, + pos_y: float, + ) -> None: + """ + Set the position of a graphical module in the scene. Helper method + for loading a layout from file. + + Parameters + ---------- + module : ModuleGraphicsItem + Graphical module of which to set the position. + pos_x : float + Position on the x-axis in the scene. + pos_y : float + Position on the y-axis in the scene. + """ + module.setX(pos_x) + module.setY(pos_y) + + def _load_port( + self, + port: PortGraphicsItem, + pos_x: float, + pos_y: float, + orientation: Orientation, + visibility: bool, + ) -> None: + """ + Set position, orientation and visibility of a port of a graphical + module. Helper method for loading a layout from file. + + Parameters + ---------- + port : PortGraphicsItem + Port to modify. + pos_x : float + Position on the x-axis in the scene. + pos_y : float + Position on the y-axis in the scene. + orientation : Orientation + Orientation of the port. + visibility : bool + Visibility of the port. ``True`` to show the port, ``False`` to + hide it. + """ + port.setOrientation(orientation) + port.setX(pos_x) + port.setY(pos_y) + port.setVisible(visibility) diff --git a/src/simudator/gui/custom_toolbar.py b/src/simudator/gui/custom_toolbar.py deleted file mode 100644 index b55049f2cb0848b4ca1b8b294e734d06fd9a429b..0000000000000000000000000000000000000000 --- a/src/simudator/gui/custom_toolbar.py +++ /dev/null @@ -1,14 +0,0 @@ -from qtpy.QtWidgets import QToolBar - - -class CustomToolBar(QToolBar): - """ - A custom implementation of QToolBar that reimplemented contextMenuEvent - so that the toolbar is no longer removable. - """ - - def __init__(self, text): - super().__init__(text) - - def contextMenuEvent(self, event): - pass diff --git a/src/simudator/gui/gui.py b/src/simudator/gui/gui.py index a5a30b14f0e83e811a211e3c7de90780a1056cc6..9b3fdc9cb05e293a70869cc1c078057c23668475 100644 --- a/src/simudator/gui/gui.py +++ b/src/simudator/gui/gui.py @@ -1,139 +1,65 @@ import ast -import json import sys -from json import JSONDecodeError -from threading import Thread from qtpy import QtCore, QtWidgets -from qtpy.QtCore import QPointF from qtpy.QtCore import Signal as pyqtSignal from qtpy.QtCore import Slot from qtpy.QtWidgets import ( QAction, QApplication, QErrorMessage, - QFileDialog, - QGraphicsView, - QInputDialog, - QLabel, QMainWindow, QMessageBox, - QSpinBox, - QStyle, - QWidget, ) from simudator.core.processor import Processor from simudator.core.signal import Signal from simudator.gui.breakpoint_window import BreakpointWindow from simudator.gui.cpu_graphics_scene import CpuGraphicsScene -from simudator.gui.custom_toolbar import CustomToolBar from simudator.gui.dialogs.lambda_breakpoint_dialog import LambdaBreakpointDialog +from simudator.gui.menu_bar import MainMenuBar from simudator.gui.module_graphics_item.module_graphics_item import ModuleGraphicsItem -from simudator.gui.orientation import Orientation from simudator.gui.pipeline import PipeLine -from simudator.gui.port_graphics_item import PortGraphicsItem -from simudator.gui.run_continuously_thread import RunThread -from simudator.gui.signal_graphics_item import SignalGraphicsItem +from simudator.gui.processor_handler import ProcessorHandler from simudator.gui.signal_viewer import SignalViewer - - -class View(QGraphicsView): - """ - This class views all QGraphicsItems for the user. It takes the - QGraphicsScene as input and inherits all funcitonality from the - QGraphicsView and overrides the wheelEvent function. - This allows the users to navigate the view with their trackpads - and zoom in/out with their trackpads + ctrl. - """ - - def __init__(self, QGraphicsScene): - super().__init__(QGraphicsScene) - self.scene = QGraphicsScene - - def wheelEvent(self, event): - """ - Default behaviour if ctrl is not pressed, otherwise zoom in/out. - """ - modifiers = QtWidgets.QApplication.keyboardModifiers() - if modifiers == QtCore.Qt.ControlModifier: - # Factor above 1 zooms in, below zooms out - factor = 1.03 - if event.angleDelta().y() < 0: - factor = 0.97 - - # If event got triggered due to the x axis, do nothing - if event.pixelDelta().x() != 0: - return - - view_pos = event.globalPosition() - scene_pos = self.mapToScene(int(view_pos.x()), int(view_pos.y())) - - self.centerOn(scene_pos) - self.scale(factor, factor) - - old_mapToScene = self.mapToScene(int(view_pos.x()), int(view_pos.y())) - new_mapToScene = self.mapToScene(self.viewport().rect().center()) - - delta = old_mapToScene - new_mapToScene - - self.centerOn(scene_pos - delta) - - else: - # Default behaviour - super().wheelEvent(event) +from simudator.gui.simulation_toolbar import SimulationToolBar +from simudator.gui.view import View class GUI(QMainWindow): """ - Main gui class. Handles gui windows, toolbar and visualizes modules. + Main gui class for visualizing the SimuDator processor simulator. - This is the main class for the GUI. It handles creating the window for - the gui, aswell as the toolbar for controlling the simultaion. - It takes a processor and visualizes its modules as boxes with the signals as lines - between them. Graphics items for the modules and signals need to be created - and added individually. - """ + Graphics items for modules need to be created and added individually using + the provided methods. Signals are also not added automatically but need + not be created. - cpu_tick_signal = pyqtSignal(int) - HALT_MESSAGE_THRESHOLD = 100 + Parameters + ---------- + processor : Processor + The simulated processor to visualize and control. + """ - def __init__(self, cpu: Processor): + def __init__(self, processor: Processor): super().__init__() - self.cpu = cpu + self._processor = processor + self._processor_handler = ProcessorHandler(processor, parent=self) self.setWindowTitle("SimuDator") - self.cpu_graphics_scene = CpuGraphicsScene(cpu) - self.graphics_view = View(self.cpu_graphics_scene) + self._graphics_scene = CpuGraphicsScene() + self._graphics_view = View(self._graphics_scene) # self.graphics_view.setDragMode(True) - self.moduleActions: dict[str, QAction] = {} - - self.setCentralWidget(self.graphics_view) - - self.errorMessageWidget = QErrorMessage(self) - - self.createToolBar() - - # Threadpool for running cpu and gui concurently - self.threadpool = QtCore.QThreadPool() - - # Signal to tell gui when cpu has halted - self.cpu_tick_signal.connect(self.handleCpuTick) + self._module_actions: dict[str, QAction] = {} - # Used to set if all values in the gui should be updated each tick - # or only the clock counter - self.update_all_values = False + self.setCentralWidget(self._graphics_view) - # Used to set the update delay - # Useful when watching the values being updated while running - self.update_delay = 0.00 + self._error_msg_widget = QErrorMessage(self) - # Used to lock some actions in ui when cpu is running in another thread - # Using the cpu's internal status directly could case problems - self.cpu_running = False + self._create_menu_bar() + self._create_tool_bar() - self.pipeline = PipeLine() + self._pipeline = PipeLine() self.init_pipeline() # Set Style, THESE ARE TEST AND DONT WORK @@ -142,196 +68,58 @@ class GUI(QMainWindow): self.setAttribute(QtCore.Qt.WA_StyledBackground) # Add this so we can save the window in scope later, otherwise it disappears - self.breakpoint_window = None + self._breakpoint_window = None - def createToolBar(self) -> None: + def _create_menu_bar(self) -> None: """ - Creates the toolbar containing file, layout and toolbar buttons. - - - Creates the toolbar containing the file, layout and toolbar - buttons with their respective sub buttons. + Create a main menu bar and connect its signals to appropriate slots of + other widgets. """ - # Create toolbar - toolbar = CustomToolBar("Main toolbar") - self.addToolBar(toolbar) - - # Create load action - self.load_action = QAction("Load", self) - self.load_action.setStatusTip("Load processor state from file") - self.load_action.triggered.connect(self.loadToolBarButtonClick) + self._menu_bar = MainMenuBar(self) + self.setMenuBar(self._menu_bar) - # Create save action - self.save_action = QAction("Save", self) - self.save_action.setStatusTip("Save processor state to file") - self.save_action.triggered.connect(self.saveStateMenuButtonClick) - - self.reset_action = QAction("Reset", self) - self.reset_action.setStatusTip("Reset processor") - self.reset_action.triggered.connect(self.resetMenuButtonClick) + # Connect signals for processor related actions + self._menu_bar.load.connect(self._processor_handler.load_state) + self._menu_bar.save.connect(self._processor_handler.save_state) + self._menu_bar.reset.connect(self._processor_handler.reset) + self._menu_bar.update_all_values.connect( + self._processor_handler.toggle_value_update_on_run + ) + self._menu_bar.set_delay.connect(self._processor_handler.set_update_delay) - # Create File menu for load and save - menu = self.menuBar() - file_menu = menu.addMenu("&File") - file_menu.addAction(self.load_action) - file_menu.addAction(self.save_action) - file_menu.addAction(self.reset_action) + # Connect signals for managing the processor layout + self._menu_bar.load_layout.connect(self._graphics_scene.load_layout) + self._menu_bar.load_default_layout.connect( + self._graphics_scene.load_default_layout + ) + self._menu_bar.save_layout.connect(self._graphics_scene.save_layout) + self._menu_bar.lock_layout.connect(self._graphics_scene.toggle_layout_lock) + self._menu_bar.show_all_signals.connect(self._graphics_scene.show_all_signals) + self._menu_bar.show_port_names.connect(self._graphics_scene.show_port_names) - # create load layout action - load_layout_action = QAction("Load layout", self) - load_layout_action.setStatusTip("Loads the selected layout from file.") - load_layout_action.triggered.connect(self.loadLayoutToolBarButtonClick) + # Connect signal for managing breakpoints + self._menu_bar.show_breakpoints.connect(self.openBreakpointWindow) - # create load default layout action - load_default_layout_action = QAction("Load default layout", self) - load_default_layout_action.setStatusTip("Loads the default layout from file.") - load_default_layout_action.triggered.connect( - self.loadDefaultLayoutToolBarButtonClick + self._processor_handler.running.connect( + self._menu_bar.set_disabled_when_running ) - # create save layout action - save_layout_action = QAction("Save layout", self) - save_layout_action.setStatusTip("Saves the current layout to a file.") - save_layout_action.triggered.connect(self.saveLayoutToolBarButtonClick) - - # create layout lock action - self.lock_layout_action = QAction("Lock layout", self, checkable=True) - self.lock_layout_action.setStatusTip("Lock layout so items can't be moved.") - self.lock_layout_action.triggered.connect(self.toggleLayoutLockMenuButtonClick) - - # Create show signals actions - self.signal_vis_action = QAction("Show signals", self, checkable=True) - self.signal_vis_action.setChecked(True) - self.signal_vis_action.setStatusTip("Toggle the visibility of signal.") - self.signal_vis_action.triggered.connect(self.showSignalsMenuButtonClick) - - # Create show port name actions - self.port_vis_action = QAction("Show port names", self, checkable=True) - self.port_vis_action.setChecked(True) - self.port_vis_action.setStatusTip("Toggle the visibility of port names.") - self.port_vis_action.triggered.connect(self.showPortNamesBarButtonClick) - - # Create Layout menu for layout actions - layout_menu = menu.addMenu("&Layout") - layout_menu.addAction(load_layout_action) - layout_menu.addAction(load_default_layout_action) - layout_menu.addAction(save_layout_action) - layout_menu.addAction(self.lock_layout_action) - layout_menu.addAction(self.signal_vis_action) - layout_menu.addAction(self.port_vis_action) - - # Create breakpoint window action - self.breakpoint_action = QAction("Breakpoints", self) - self.breakpoint_action.setStatusTip("Open breakpoint window.") - self.breakpoint_action.triggered.connect(self.openBreakpointWindow) - - # Create 'update value' window button - self.update_value_action = QAction( - "Update values while running", self, checkable=True - ) - self.update_value_action.setChecked(False) - self.update_value_action.setStatusTip("Toggle value updates while running.") - self.update_value_action.triggered.connect(self.toggle_value_update_on_run) - - # Create 'set delay' window button - self.set_delay_action = QAction("Set update delay", self) - self.set_delay_action.setStatusTip( - "Sets the delay between each update when the cpu is running." - ) - self.set_delay_action.triggered.connect(self.set_update_delay) - - # Create Tools menu for tool actions actions - tools_menu = menu.addMenu("&Tools") - tools_menu.addAction(self.breakpoint_action) - tools_menu.addAction(self.update_value_action) - tools_menu.addAction(self.set_delay_action) - - # Add run button on toolbar - arrow_icon = self.style().standardIcon(QStyle.SP_MediaPlay) - self.run_action = QAction(arrow_icon, "Run", self) - self.run_action.setStatusTip("Run until halt") - self.run_action.triggered.connect(self.runToolBarButtonClick) - toolbar.addAction(self.run_action) - - # Add stop button on toolbar - stop_icon = self.style().standardIcon(QStyle.SP_MediaStop) - self.stop_action = QAction(stop_icon, "Stop", self) - self.stop_action.setStatusTip("Stop running") - self.stop_action.triggered.connect(self.stopToolBarButtonClick) - toolbar.addAction(self.stop_action) - - # Add Asm label - self.clock_cycle_label = QLabel("Clock cycle: ", self) - toolbar.addWidget(self.clock_cycle_label) - - # Add undo button on toolbar - backward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekBackward) - self.undo_action = QAction(backward_arrow_icon, "Undo", self) - self.undo_action.setStatusTip("Undo the last processor tick") - self.undo_action.triggered.connect(self.undoToolBarButtonClick) - toolbar.addAction(self.undo_action) - - # Add step button on toolbar - forward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekForward) - self.step_action = QAction(forward_arrow_icon, "Step", self) - self.step_action.setStatusTip("Run one clock cycle") - self.step_action.triggered.connect(self.stepToolBarButtonClick) - toolbar.addAction(self.step_action) - - # Add box for jump value - self.jump_value_box = QSpinBox() - self.jump_value_box.setMinimum(999999) - self.jump_value_box.setMinimum(1) - self.jump_value_box.setValue(1) - toolbar.addWidget(self.jump_value_box) - - # Add seperator so clock gets better spacing - spacing = QWidget() - spacing.setFixedWidth(10) - toolbar.addWidget(spacing) - toolbar.addSeparator() - spacing = QWidget() - spacing.setFixedWidth(10) - toolbar.addWidget(spacing) - - # Add clock counter - self.clock_label = QLabel("Clock cycles: " + str(self.cpu.get_clock()), self) - toolbar.addWidget(self.clock_label) - - # Add seperator so clock gets better spacing - spacing = QWidget() - spacing.setFixedWidth(10) - toolbar.addWidget(spacing) - toolbar.addSeparator() - spacing = QWidget() - spacing.setFixedWidth(10) - toolbar.addWidget(spacing) - - # Add Asm label - self.asm_label = QLabel("Assembler Instructions: ", self) - toolbar.addWidget(self.asm_label) - - # Add undo asm button on toolbar - backward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekBackward) - self.undo_asm_action = QAction(backward_arrow_icon, "Undo Asm", self) - self.undo_asm_action.setStatusTip("Undo the last assembler instruction") - self.undo_asm_action.triggered.connect(self.undoAsmToolBarButtonClick) - toolbar.addAction(self.undo_asm_action) - - # Add step button on toolbar - forward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekForward) - self.step_asm_action = QAction(forward_arrow_icon, "Step Asm", self) - self.step_asm_action.setStatusTip("Run one assembler instruction") - self.step_asm_action.triggered.connect(self.stepAsmToolBarButtonClick) - toolbar.addAction(self.step_asm_action) - - # Add box for jump value - self.asm_jump_value_box = QSpinBox() - self.asm_jump_value_box.setMinimum(999999) - self.asm_jump_value_box.setMinimum(1) - self.asm_jump_value_box.setValue(1) - toolbar.addWidget(self.asm_jump_value_box) + def _create_tool_bar(self) -> None: + """ + Create a simulation toolbar and connect its signals to appropriate + slots of other widgets. + """ + self._toolbar = SimulationToolBar("Main toolbar", self) + self.addToolBar(self._toolbar) + self._toolbar.run.connect(self._processor_handler.run_simulation) + self._toolbar.stop.connect(self._processor_handler.stop_simulation) + self._toolbar.step.connect(self._processor_handler.step_cycles) + self._toolbar.undo.connect(self._processor_handler.undo_cycles) + self._toolbar.step_asm.connect(self._processor_handler.step_asm_instructions) + self._toolbar.undo_asm.connect(self._processor_handler.undo_asm_instructions) + self._processor_handler.cycle_changed.connect(self._toolbar.update_clock) + self._processor_handler.running.connect(self._toolbar.set_disabled_when_running) def connectModuleActions(self, action_signals: []) -> None: """ @@ -359,50 +147,30 @@ class GUI(QMainWindow): # LambdaBPs are not tested since we dont # actually have any modules that make them yet signal.connect(self.addLambdaBreakpoint) - case "UPDATE": - signal.connect(self.updateCpuListeners) - case "CLOCKUPDATE": - signal.connect(self.updateCpuClockCycle) def init_pipeline(self) -> None: """Initialize the pipeline diagram. Sets its height, width and instructions. """ - size = self.cpu.get_pipeline_dimensions() - - self.pipeline.set_height(size[0]) - self.pipeline.set_width(size[1]) - self.update_pipeline() - - self.pipeline.show() - - def updateCpuListeners(self) -> None: - """ - Updates the graphics items in the scene, the clock, and the pipeline diagram. + size = self._processor.get_pipeline_dimensions() - Used after the cpu has run or when the user has edited somehting. - """ - self.cpu_graphics_scene.updateGraphicsItems() - self.updateCpuClockCycle() - self.update_pipeline() - - def update_pipeline(self) -> None: - self.pipeline.set_instructions(self.cpu.get_current_instructions()) + self._pipeline.set_height(size[0]) + self._pipeline.set_width(size[1]) - def updateCpuClockCycle(self) -> None: - """ - Update the clock cycle counter. - - Used while the program is running to show the user nothing has crashed. - """ - self.clock_label.setText("Clockcycle: " + str(self.cpu.get_clock())) + self._processor_handler.changed_instruction.connect( + self._pipeline.set_instructions + ) + # TODO: Find prettier way of making the processor handler emit its + # signal for the current processor instructions + self._processor_handler._signal_processor_changed() + self._pipeline.show() def lambdaBreakpointDialog(self) -> None: """ Opens dialog window for user to create a breakpoint. """ - lambdas = self.cpu.get_breakpoint_lambdas() + lambdas = self._processor.get_breakpoint_lambdas() lambda_br_dialog = LambdaBreakpointDialog(lambdas, self) lambda_br_dialog.accepted.connect(self.addLambdaBreakpoint) @@ -411,8 +179,8 @@ class GUI(QMainWindow): Updates the breakpoint window when new breakpoints are added. """ # Don't do anything if window is closed - if self.breakpoint_window is not None: - self.breakpoint_window.update() + if self._breakpoint_window is not None: + self._breakpoint_window.update() """ @Slot is used to explicitly mark a python method as a Qt slot @@ -430,13 +198,13 @@ class GUI(QMainWindow): try: parsed_value = ast.literal_eval(value) except SyntaxError as e: - self.errorMessageWidget.showMessage(str(e)) + self._error_msg_widget.showMessage(str(e)) else: - module = self.cpu.get_module(module_name) + module = self._processor.get_module(module_name) module_state = module.get_state() module_state[state] = parsed_value module.set_state(module_state) - self.cpu_graphics_scene.updateGraphicsItems() + self._graphics_scene.update_modules() @Slot(str, str, str) def editMemoryContent(self, module_name: str, adress: str, value: str) -> None: @@ -449,17 +217,17 @@ class GUI(QMainWindow): parsed_adress = int(adress, 16) parsed_value = ast.literal_eval(value) except SyntaxError as e: - self.errorMessageWidget.showMessage(str(e)) + self._error_msg_widget.showMessage(str(e)) except ValueError: - self.errorMessageWidget.showMessage( + self._error_msg_widget.showMessage( "You must enter a hexadecimal" "number preceeded by '0x' (e.g." "0xc3)." ) else: - module = self.cpu.get_module(module_name) + module = self._processor.get_module(module_name) module_state = module.get_state() module_state['memory'][parsed_adress] = parsed_value module.set_state(module_state) - self.cpu_graphics_scene.updateGraphicsItems() + self._graphics_scene.update_modules() @Slot(str, str, str) def addStateBreakpoint(self, module_name: str, state: str, value: str) -> None: @@ -468,10 +236,10 @@ class GUI(QMainWindow): """ try: parsed_value = ast.literal_eval(value) - self.cpu.add_state_breakpoint(module_name, state, parsed_value) + self._processor.add_state_breakpoint(module_name, state, parsed_value) self.updateBreakpointWindow() except (ValueError, SyntaxError) as e: - self.errorMessageWidget.showMessage(str(e)) + self._error_msg_widget.showMessage(str(e)) @Slot(str, str) def addLambdaBreakpoint(self, lambda_name, kwargs_str) -> None: @@ -483,10 +251,10 @@ class GUI(QMainWindow): for kwarg in kwargs_str.split(' '): key, value = kwarg.split('=') lambda_kwargs[key] = ast.literal_eval(value) - self.cpu.add_lambda_breakpoint(lambda_name, **lambda_kwargs) + self._processor.add_lambda_breakpoint(lambda_name, **lambda_kwargs) self.updateBreakpointWindow() except (ValueError, SyntaxError) as e: - self.errorMessageWidget.showMessage(str(e)) + self._error_msg_widget.showMessage(str(e)) @Slot(str, str, str) def addMemoryBreakpoint(self, module_name: str, adress: str, value: str) -> None: @@ -496,498 +264,56 @@ class GUI(QMainWindow): try: parsed_adress = int(adress) parsed_value = int(value) - self.cpu.add_memory_breakpoint(module_name, parsed_adress, parsed_value) + self._processor.add_memory_breakpoint( + module_name, parsed_adress, parsed_value + ) self.updateBreakpointWindow() except SyntaxError as e: - self.errorMessageWidget.showMessage(str(e)) - - def setDisabledWhenRunning(self, is_disable): - """ - Greys out buttons for actions that can't be done while cpu is running. - """ - self.load_action.setDisabled(is_disable) - self.save_action.setDisabled(is_disable) - self.reset_action.setDisabled(is_disable) - self.run_action.setDisabled(is_disable) - self.step_action.setDisabled(is_disable) - self.undo_action.setDisabled(is_disable) - - def stepToolBarButtonClick(self): - """ - Runs the cpu a specified number of clock cycles according to the jump value box. - """ - - # Don't do steps if cpu is running - if self.cpu_running: - return - - steps = self.jump_value_box.value() - self.cpu_running = True - self.setDisabledWhenRunning(True) - simulation_thread = RunThread( - self.cpu, self.cpu_tick_signal, self.update_delay, False, False, steps - ) - self.threadpool.start(simulation_thread) - - def stepAsmToolBarButtonClick(self): - """ - Runs the cpu a specified number of asm instructions according to the jump value box. - """ - - # Don't do steps if cpu is running - if self.cpu_running: - return - - steps = self.asm_jump_value_box.value() - self.cpu_running = True - self.setDisabledWhenRunning(True) - simultaion_thread = RunThread( - self.cpu, self.cpu_tick_signal, self.update_delay, False, True, steps - ) - self.threadpool.start(simultaion_thread) - self.updateCpuListeners() - - def runToolBarButtonClick(self) -> None: - """ - Runs the cpu until it is stopped by user input, breakpoint or similar. - """ - - # Don't run if already running - if self.cpu_running: - return - - # Create own thread for cpu simulation so gui dosent freeze - self.cpu_running = True - self.setDisabledWhenRunning(True) - simulation_thread = RunThread(self.cpu, self.cpu_tick_signal, self.update_delay) - self.threadpool.start(simulation_thread) - - @Slot(int) - def handleCpuTick(self, steps: int) -> None: - """ - Called from other thread after every cpu tick. - Will inform the user and update visuals. - """ - - # Update cpu clock counter every tick - self.updateCpuClockCycle() - - if self.update_all_values: - self.updateCpuListeners() - - # A signal of 0 steps signifies end of execution, i.e. the CPU has - # halted or run the specified amount of ticks - # => Enable the relevant parts of the GUI again - if steps == 0: - self.cpu_running = False - self.setDisabledWhenRunning(False) - self.updateCpuListeners() - - # Inform user of reached break point - if self.cpu.breakpoint_reached: - self.messageBox( - "Reached breakpoint: " + self.cpu.last_breakpoint.__str__() - ) - - # Inform user of halt - if self.cpu.should_halt(): - self.messageBox("The processor halted.") - - def stopToolBarButtonClick(self) -> None: - """ - Tells the cpu to stop. It will then stop at an appropriate in its own thread. - """ - self.cpu.stop() - - def folderSaveDialog(self) -> str: - """ - Open a file explorer in a new window. Return the absolute path to the selected file. - - Can return existing as well as non-existing files. - If the selected files does not exist it will be created. - """ - dialog = QFileDialog() - dialog.setFileMode(QFileDialog.AnyFile) - dialog.setAcceptMode(QFileDialog.AcceptOpen) - dialog.setDirectory("~/simudator") # TODO: does this work when exported? - - # getSaveFileName() can returncan return existing file but also create new ones - return dialog.getSaveFileName()[0] - - def folderLoadDialog(self) -> str: - """ - Open a file explorer in a new window. Return the absolute path to the selected file. - - Can only return existing files. - """ - dialog = QFileDialog() - dialog.setFileMode(QFileDialog.AnyFile) - dialog.setAcceptMode(QFileDialog.AcceptOpen) - dialog.setDirectory("~/simudator") # TODO: does this work when exported? - - # getOpenFileName() will only return already existing files - return dialog.getOpenFileName()[0] - - def loadToolBarButtonClick(self) -> None: - """ - Loads the processor state from selected file. - """ - - # not safe to load while cpu is running - if self.cpu_running: - return - - path = self.folderLoadDialog() - try: - self.cpu.load_state_from_file(path) - except KeyError: - self.errorBox("Could not load selected file.") - except ValueError: - self.errorBox("Selected file was empty.") - except FileNotFoundError: - # This error is triggered when no file was selected by - # by the user. The user should know they did not select a - # file and therefore there no need to display a box telling - # them that. - pass - # self.errorBox("No vaild file was selected.") - else: - self.messageBox("Loaded file successfully.") - self.updateCpuListeners() - - def resetMenuButtonClick(self) -> None: - """ - Will reset processor to inital values. - """ - - # not safe to reset while cpu running - if self.cpu_running: - return - - answer = QMessageBox.question( - self, "Reset Processor", "Are you sure you want to reset the processor?" - ) - - if answer == QMessageBox.Yes: - self.cpu.reset() - self.updateCpuListeners() - - def undoNToolBarButtonClick(self) -> None: - # TODO: I dont think this is used - """ - Undo zero to N cycles. - """ - - # Don't try to undo while running - if self.cpu_running: - return - - cycles, ok = QInputDialog(self).getInt( - self, - "Input number of cycles to run", - "Input number of cycles to run", - ) - if ok: - if cycles < 1: - self.errorBox("Please input a number larger than 0.") - return - try: - # Make sure we load 0 as lowest - self.cpu.load_cycle(max(self.cpu.get_clock() - cycles, 0)) - self.updateCpuListeners() - except (ValueError, IndexError): - self.errorBox("Unable to undo the cycle(s).") - - def saveLayoutToolBarButtonClick(self) -> None: - """ - Saves the layout of all the modules in a (somewhat) human readable format. - - This also erases the previous content of the file before saving - the data. - """ - path = self.folderSaveDialog() - - # Change file to the selected file if a file was selected - if path == '': - return - - # Open the given file erasing the previous content - with open(path, "w") as fp: - graphics_modules_data = {} - ports_data = {} - graphics_signals_data = {} - - graphics_modules = self.cpu_graphics_scene.getModulesGraphicsItems() - for graphics_module in graphics_modules: - pos = (graphics_module.x(), graphics_module.y()) - graphics_modules_data[graphics_module.getName()] = pos - - for port in graphics_module.getPorts(): - visibility = port.isVisible() - orientation = int(port.getOrientation()) - data = (port.x(), port.y(), orientation, visibility) - ports_data[port.getID()] = data - - for graphics_signal in self.cpu_graphics_scene.getSignalGraphicsItems(): - visibility = graphics_signal.isVisible() - points = [] - for point in graphics_signal.getPoints(): - points.append((point.x(), point.y())) - data = (points, visibility) - graphics_signals_data[graphics_signal.getID()] = data - - data = (graphics_modules_data, ports_data, graphics_signals_data) - json.dump(data, fp) - - fp.close() - - def loadDefaultLayoutToolBarButtonClick(self) -> None: - """ - Places all the module_graphic objects in a diagonal going down to the right. - """ - counter = 0 - for key in self.cpu_graphics_scene.module_graphics_items: - module_graphic = self.cpu_graphics_scene.module_graphics_items[key] - module_graphic.showPorts() - counter += 1 - self.cpu_graphics_scene.replaceModuleGraphicsItem( - module_graphic, counter, counter - ) - self.cpu_graphics_scene.resetSignals() - - def loadLayoutFromFile(self, file) -> None: - """ - Loads a layout for the current cpu from the selected file. - - If at anypoint this funciton would error, the default layout - is loaded instead. - """ - # TODO: speed(?) - graphics_modules = self.cpu_graphics_scene.moduleGraphicsItemsDict() - ports = {} - graphics_signals = {} - - for graphics_module in graphics_modules.values(): - for port in graphics_module.getPorts(): - ports[port.getID()] = port - - for graphics_signal in self.cpu_graphics_scene.getSignalGraphicsItems(): - graphics_signals[graphics_signal.getID()] = graphics_signal - - # Open the file in 'read-only' - with open(file, 'rb') as fp: - data = json.load(fp) - graphics_modules_data = data[0] - ports_data = data[1] - graphics_signals_data = data[2] - - for g_module_name, g_module_data in graphics_modules_data.items(): - g_module = graphics_modules[g_module_name] - x = g_module_data[0] - y = g_module_data[1] - self.loadGraphicsModule(g_module, x, y) - - for port_id, port_data in ports_data.items(): - port = ports[int(port_id)] - self.loadPort(port, *port_data) - - for g_signal_id, g_signal_data in graphics_signals_data.items(): - g_signal = graphics_signals[int(g_signal_id)] - self.loadSignal(g_signal, *g_signal_data) - - fp.close() - - def loadSignal( - self, - graphic_signal: SignalGraphicsItem, - signal_points: list[tuple[float, float]], - visibility: bool, - ) -> None: - """ - Changes the graphical signal to have the positions given as argument. - """ - qpoints = [] - # Turn points -> QPointF - # list[int, int] -> QPointF object - for point in signal_points: - qpoints.append(QPointF(point[0], point[1])) - - # Set the new points - graphic_signal.setPoints(qpoints) - graphic_signal.setVisible(visibility) - - def loadGraphicsModule( - self, - graphics_module: ModuleGraphicsItem, - graphics_module_x: float, - graphics_module_y: float, - ) -> None: - """ - Changes the positions of graphical modules to the ones given as argument. - """ - graphics_module.setX(graphics_module_x) - graphics_module.setY(graphics_module_y) - - def loadPort( - self, - port: PortGraphicsItem, - x: float, - y: float, - orientation: Orientation, - visibility: bool, - ) -> None: - port.setOrientation(orientation) - port.setX(x) - port.setY(y) - port.setVisible(visibility) - - def saveStateMenuButtonClick(self) -> None: - """ - Save state of cpu to file. - - This erases the previous content of the file before saving the data. - """ - - # Not safe to save while running - if self.cpu_running: - return - - path = self.folderSaveDialog() - - # Change file to the selected file if a file was selected - if path == '': - return - - res = self.cpu.save_state_to_file(path) - - if res: - self.messageBox("File saved.") - else: - self.errorBox("Unable to save.") + self._error_msg_widget.showMessage(str(e)) + @Slot() def openBreakpointWindow(self) -> None: """ Opens window for editing breakpoints. """ - self.breakpoint_window = BreakpointWindow(self.cpu) - self.breakpoint_window.show() - - def toggle_value_update_on_run(self): - """ - Toggles whether all values or only clock cycle is being updated each tick. - """ - self.update_all_values = not self.update_all_values - - def set_update_delay(self): - """ - Sets the update delay for the visual updates while the cpu is running. - """ - delay, ok = QInputDialog.getDouble( - self, "Input Dialog", "Enter a float value:", decimals=5 - ) - if ok: - self.update_delay = delay - - def showPortNamesBarButtonClick(self): - """ - Toggles showing port names in the graphics scene. - """ - self.cpu_graphics_scene.setPortNamesVisibility(self.port_vis_action.isChecked()) - - def showSignalsMenuButtonClick(self) -> None: - """ - Toggle shoing the signals in the graphics scene. - """ - self.cpu_graphics_scene.setAllSignalsVisibility( - self.signal_vis_action.isChecked() - ) - - def toggleLayoutLockMenuButtonClick(self) -> None: - """ - Toggles so the layout can not be edited. - """ - self.cpu_graphics_scene.setLayoutLock(self.lock_layout_action.isChecked()) - - def loadLayoutToolBarButtonClick(self) -> None: - """ - Loads a given layout from a selected file. - - If the layout was - unable to load, an error message will pop up informing the user - and the default layout will be loaded. - """ + self._breakpoint_window = BreakpointWindow(self._processor) + self._breakpoint_window.show() - path = self.folderLoadDialog() - - # If no file was selected, do nothing - if path == '': - return - - try: - self.loadLayoutFromFile(path) - - # Anything goes wrong with loading the selected one, - # we dont care about what went wrong - except (OSError, JSONDecodeError): - self.loadDefaultLayoutToolBarButtonClick() - self.errorBox("Unable to load given file.") - else: - self.messageBox("File loaded successfully.") - - def errorBox(self, message="Something went wrong.") -> None: - """ - Displays a simple box with the given error message. - """ - - dlg = QMessageBox(self) - dlg.setWindowTitle("Error") - dlg.setText(message) - dlg.exec() - - def messageBox(self, message="Something happend.") -> None: + def messageBox(self, message, title="Message") -> None: """ Displays a simple box with the given message. """ - dlg = QMessageBox(self) - dlg.setWindowTitle("Message") + dlg.setWindowTitle(title) dlg.setText(message) dlg.exec() - def undoToolBarButtonClick(self) -> None: + def add_module_graphics_item(self, item: ModuleGraphicsItem) -> None: """ - Undos as many processor cycles as the number entered in the box. + Add a module graphics item to the graphics scene and connect its + QT signals and slots. """ - try: - steps = self.jump_value_box.value() - self.cpu.load_cycle(max(self.cpu.get_clock() - steps, 0)) - self.updateCpuListeners() - except (ValueError, IndexError): - self.errorBox("Unable to undo the cycle.") + self._graphics_scene.add_module(item) + self.connectModuleActions(item.getActionSignals()) + self._processor_handler.changed.connect(item.update) - def undoAsmToolBarButtonClick(self) -> None: + def add_all_signals(self) -> None: """ - Undos as many processor cycles as the number entered in the box. + Add visual representations of all processor signals between all modules + added to the GUI. """ - try: - steps = self.asm_jump_value_box.value() - self.cpu.undo_asm_instruction(steps) - self.updateCpuListeners() - except (ValueError, IndexError): - self.errorBox("Unable to undo the cycle.") + self._graphics_scene.add_all_signals() - def addModuleGraphicsItem(self, graphics_item: ModuleGraphicsItem) -> None: - """ - Adds an item to the graphics scene. - """ - self.cpu_graphics_scene.addModuleGraphicsItem(graphics_item) - self.connectModuleActions(graphics_item.getActionSignals()) + def load_layout(self, file_path: str) -> None: + """Load a processor layout from file. + + Parameters + ---------- + file_path : str + Path to the file containing a processor layout to load. - def addAllSignals(self) -> None: - """ - Add signals depending on modules in the graphics scene. """ - self.cpu_graphics_scene.addAllSignals() + self._graphics_scene.load_layout_from_file(file_path) def add_signal_viewer(self, signal: Signal, label: str | None = None) -> None: """ @@ -1001,7 +327,8 @@ class GUI(QMainWindow): Optional label of the signal viewer. """ viewer = SignalViewer(signal, label) - self.cpu_graphics_scene.addItem(viewer) + self._processor_handler.changed.connect(viewer.update) + self._graphics_scene.addItem(viewer) if __name__ == '__main__': diff --git a/src/simudator/gui/menu_bar.py b/src/simudator/gui/menu_bar.py new file mode 100644 index 0000000000000000000000000000000000000000..29183a9df0ef1db0a99dd035a3407b83e93eead9 --- /dev/null +++ b/src/simudator/gui/menu_bar.py @@ -0,0 +1,192 @@ +import typing + +from PyQt5.QtWidgets import QAction +from qtpy.QtCore import Signal as pyqtSignal +from qtpy.QtCore import Slot +from qtpy.QtWidgets import QMenuBar, QWidget + + +class MainMenuBar(QMenuBar): + """ + Main menu bar of the simudator GUI. Contains submenus for file I/O, + layout managing and miscellaneous tools. + + Parameters + ---------- + parent : QWidget | None + Parent of the widget. The widget will be a new window if it is ``None`` + (default). + """ + + load = pyqtSignal() + """ + QT signal emitted when the user has requested to load a processor state. + """ + + save = pyqtSignal() + """ + QT signal emitted when the user has requested to save the processor state. + """ + + reset = pyqtSignal() + """ + QT signal emitted when the user has request to reset the processor state. + """ + + load_layout = pyqtSignal() + """ + QT signal emitted when the user has request to load a processor layout. + """ + + load_default_layout = pyqtSignal() + """ + QT signal emitted when the user has request to load the default processor + layout. + """ + + save_layout = pyqtSignal() + """ + QT signal emitted when the user has request to save the processor layout. + """ + + lock_layout = pyqtSignal(bool) + """ + QT signal emitted when the user has request to toggle the lock of editing + the processor layout. + """ + + show_breakpoints = pyqtSignal() + """ + QT signal emitted when the user has requested to show and manage + the processor breakpoints. + """ + + show_all_signals = pyqtSignal(bool) + """ + QT signal emitted when the user has requested to toggle showing all + processor signals. + """ + + show_port_names = pyqtSignal(bool) + """ + QT signal emitted when the user has requested to toggle showing the names + of processor module ports. + """ + + update_all_values = pyqtSignal(bool) + """ + QT signal emitted when the user has requested to see all values of + processor modules update in real time when running the processor. + """ + + set_delay = pyqtSignal() + """ + QT signal emitted when the user has requested to set the delay between + clock cycles when running the processor. + """ + + def __init__(self, parent: typing.Optional[QWidget] = None) -> None: + super().__init__(parent) + + # Create load action + self._load_action = QAction("Load", self) + self._load_action.setStatusTip("Load processor state from file") + self._load_action.triggered.connect(self.load) + + # Create save action + self._save_action = QAction("Save", self) + self._save_action.setStatusTip("Save processor state to file") + self._save_action.triggered.connect(self.save) + + self._reset_action = QAction("Reset", self) + self._reset_action.setStatusTip("Reset processor") + self._reset_action.triggered.connect(self.reset) + + # Create File menu for load and save + file_menu = self.addMenu("&File") + file_menu.addAction(self._load_action) + file_menu.addAction(self._save_action) + file_menu.addAction(self._reset_action) + + # create load layout action + load_layout_action = QAction("Load layout", self) + load_layout_action.setStatusTip("Loads the selected layout from file.") + load_layout_action.triggered.connect(self.load_layout) + + # create load default layout action + load_default_layout_action = QAction("Load default layout", self) + load_default_layout_action.setStatusTip("Loads the default layout from file.") + load_default_layout_action.triggered.connect(self.load_default_layout) + + # create save layout action + save_layout_action = QAction("Save layout", self) + save_layout_action.setStatusTip("Saves the current layout to a file.") + save_layout_action.triggered.connect(self.save_layout) + + # create layout lock action + self._lock_layout_action = QAction("Lock layout", self, checkable=True) + self._lock_layout_action.setStatusTip("Lock layout so items can't be moved.") + self._lock_layout_action.triggered.connect(self.lock_layout) + + # Create show signals actions + self._signal_vis_action = QAction("Show signals", self, checkable=True) + self._signal_vis_action.setChecked(True) + self._signal_vis_action.setStatusTip("Toggle the visibility of signal.") + self._signal_vis_action.triggered.connect(self.show_all_signals) + + # Create show port name actions + self._port_vis_action = QAction("Show port names", self, checkable=True) + self._port_vis_action.setChecked(True) + self._port_vis_action.setStatusTip("Toggle the visibility of port names.") + self._port_vis_action.triggered.connect(self.show_port_names) + + # Create Layout menu for layout actions + layout_menu = self.addMenu("&Layout") + layout_menu.addAction(load_layout_action) + layout_menu.addAction(load_default_layout_action) + layout_menu.addAction(save_layout_action) + layout_menu.addAction(self._lock_layout_action) + layout_menu.addAction(self._signal_vis_action) + layout_menu.addAction(self._port_vis_action) + + # Create breakpoint window action + self._breakpoint_action = QAction("Breakpoints", self) + self._breakpoint_action.setStatusTip("Open breakpoint window.") + self._breakpoint_action.triggered.connect(self.show_breakpoints) + + # Create 'update value' window button + self._update_value_action = QAction( + "Update values while running", self, checkable=True + ) + self._update_value_action.setChecked(False) + self._update_value_action.setStatusTip("Toggle value updates while running.") + self._update_value_action.triggered.connect(self.update_all_values) + + # Create 'set delay' window button + self._set_delay_action = QAction("Set update delay", self) + self._set_delay_action.setStatusTip( + "Sets the delay between each update when the cpu is running." + ) + self._set_delay_action.triggered.connect(self.set_delay) + + # Create Tools menu for tool actions actions + tools_menu = self.addMenu("&Tools") + tools_menu.addAction(self._breakpoint_action) + tools_menu.addAction(self._update_value_action) + tools_menu.addAction(self._set_delay_action) + + @Slot(bool) + def set_disabled_when_running(self, value: bool) -> None: + """ + Toggle the menu bar between enabled and disabled. When disabled, + actions which should not be performed while a processor is running + are disabled and greyed out. + + Parameters + ---------- + value : bool + ``True`` to disable the menu bar, ``False`` to enable it. + """ + self._load_action.setDisabled(value) + self._save_action.setDisabled(value) + self._reset_action.setDisabled(value) diff --git a/src/simudator/gui/module_graphics_item/integer_memory_graphic.py b/src/simudator/gui/module_graphics_item/integer_memory_graphic.py new file mode 100644 index 0000000000000000000000000000000000000000..a177b6228b5395a5c163bf8d1c371f4e0f0a923a --- /dev/null +++ b/src/simudator/gui/module_graphics_item/integer_memory_graphic.py @@ -0,0 +1,253 @@ +from enum import Enum + +from qtpy.QtWidgets import QButtonGroup, QHBoxLayout, QHeaderView, QRadioButton + +from simudator.gui.module_graphics_item.memory_graphic import ( + Memory, + MemoryTable, + MemoryWindow, +) + + +class Base(Enum): + NONE = 0 + DECIMAL_SIGNED = 1 + DECIMAL_UNSIGNED = 2 + BINARY = 3 + HEXADECIMAL = 4 + + +class ValueTooBig(Exception): + """ + A class representing the error of a user inputting a value that does not fit in the memory. + + Used to differentiate between wrong input using the exception python raises by default (ValueError) + and the error of a value that does not fit. + """ + + pass + + +class IntegerMemoryWindow(MemoryWindow): + """ + A class showing the contents of a memory module in a new window. + + Parameters + ---------- + memory_module: Memory + An instance of the Memory base class. + bit_length: int + An integer specifying the number of bits of each address. + """ + + def __init__(self, memory_module: Memory, bit_length: int): + + # Do not let parent create edit/view buttons + super().__init__(memory_module, False) + + # Remove the memory table the parent created + self.layout.removeWidget(self._memory_table) + + # Add our own + self._memory_table = IntegerMemoryTable(memory_module, bit_length) + self.layout.addWidget(self._memory_table) + + # Create base buttons, they are exclusive by default + # so need a seperate QButtonGroup since these four + # have nothing to do with the edit/view buttons + self.dec_signed_button = QRadioButton("Decimal Signed") + self.dec_unsigned_button = QRadioButton("Decimal Unsigned") + self.bin_button = QRadioButton("Binary") + self.hex_button = QRadioButton("Hexadecimal") + self.base_group = QButtonGroup() + self.base_group.addButton(self.dec_signed_button, 1) + self.base_group.addButton(self.dec_unsigned_button, 2) + self.base_group.addButton(self.bin_button, 3) + self.base_group.addButton(self.hex_button, 4) + + # Create edit/view buttons, they are exclusive by default + self.edit_button = QRadioButton("Edit") + self.view_button = QRadioButton("View") + self.edit_group = QButtonGroup() + self.edit_group.addButton(self.edit_button, 1) + self.edit_group.addButton(self.view_button, 2) + + # Connect them to the 'set_base' function + self.base_group.buttonClicked.connect(self._set_base) + # Connect them to the 'set_edit' function + self.edit_group.buttonClicked.connect(self._set_edit) + + # Create a layout that expands horizontally, so that + # all buttons appear in one row + self.first_button_layout = QHBoxLayout() + self.first_button_layout.addWidget(self.dec_signed_button) + self.first_button_layout.addWidget(self.dec_unsigned_button) + self.first_button_layout.addWidget(self.bin_button) + self.first_button_layout.addWidget(self.hex_button) + self.layout.addLayout(self.first_button_layout) + + self.second_button_layout = QHBoxLayout() + self.second_button_layout.addWidget(self.edit_button) + self.second_button_layout.addWidget(self.view_button) + self.layout.addLayout(self.second_button_layout) + + # Set the default base to decimal and disable editing + # from start + self.bin_button.toggle() + self.view_button.toggle() + self._set_base() + self._set_edit() + + # Sets the size of each column to the smallest possible + # width that allows all content in each box to be visible + self._memory_table.horizontalHeader().setSectionResizeMode( + QHeaderView.ResizeToContents + ) + + def _set_base(self): + pressed_button_id = self.base_group.checkedId() + if pressed_button_id == 1: + base = Base.DECIMAL_SIGNED + elif pressed_button_id == 2: + base = Base.DECIMAL_UNSIGNED + elif pressed_button_id == 3: + base = Base.BINARY + else: + base = Base.HEXADECIMAL + + self._memory_table.set_base(base) + self._memory_table.update() + + +class IntegerMemoryTable(MemoryTable): + """ + A class showing the contents of a memory module in a QTableWidget. + + This class assumes that the size of the memory module will remain constant. + + Parameters + ---------- + memory_module: Memory + An instance of the Memory base class. + bit_length: int + An integer specifying the number of bits of each address. + column_size: int + An integer specifying the number of columns, optional. + + """ + + def __init__(self, memory_module: Memory, bit_length: int, column_size=-1): + self._base = Base.DECIMAL_UNSIGNED + self._bit_length = bit_length + super().__init__(memory_module, column_size) + + self.update() + # Signal used to detect when the user has edited a cell + # This code can only be called after the initial self.update(). + # When the QTableWidget is initialized it currently fills each cell with empty strings. + # It also signals cellChanged for each cell it initializes. Thus _on_cell_changed + # is called for each cell and tries to save the cells empty string into the modules memory as + # an integer. + self.cellChanged.connect(self._on_cell_changed) + + def update(self): + """ + Update the content of this widget to reflect the content of the memory module. + """ + memory_content = self._memory.get_state()["memory"] + for i in range(self._memory_size): + value = memory_content[i] + row = i // self._column_size + col = i % self._column_size + if self._base == Base.DECIMAL_UNSIGNED or self._base == Base.DECIMAL_SIGNED: + value = self._get_dec_from_sign(value) + elif self._base == Base.BINARY: + value = str(bin(int(value)))[2:] # remove '0b' + else: # Hex + value = str(hex(int(value)))[2:] # remove '0x' + self.set_item(row, col, value) + + def set_base(self, base: Base): + """ + Change what base the content is viewed in. + + Parameters + ---------- + base: Base + An instance of the base enum representing what base to change to. + """ + self._base = base + + def _get_dec_from_sign(self, value: int) -> str: + if self._base == Base.DECIMAL_UNSIGNED: + return str(int(value)) + + # Any value above or equal to 'threshold' must have the MSB set to 1 + threshold = 2 ** (self._bit_length - 1) + + if value < threshold: + return str(int(value)) + + # If the MSB is set to one subtract its value twice, + # once so we dont count the positive value from it, + # and the second time so we count it as negative + return str(value - (threshold * 2)) + + def _on_cell_changed(self, row, col): + state = self._memory.get_state() + + item = self.item(row, col) + value = item.text() + max_value = 2**self._bit_length + min_value = -(2 ** (self._bit_length - 1)) + + # Turn every value into a positive int + # in base 10 + # This makes converting between bases + # much easier since we can assume what base + # and format each value is in + + try: + if self._base == Base.BINARY: + value = int(value, 2) + elif self._base == Base.HEXADECIMAL: + value = int(value, 16) + elif ( + self._base == Base.DECIMAL_SIGNED or self._base == Base.DECIMAL_UNSIGNED + ): + value = int(value) + + if value > max_value or value < min_value: + raise ValueTooBig + + except ValueError: + msg = None + if self._base == Base.BINARY: + msg = "You must enter a binary number (e.g. 0b101)." + elif ( + self._base == Base.DECIMAL_SIGNED or self._base == Base.DECIMAL_UNSIGNED + ): + msg = "You must enter a decimal number (e.g. 107)." + elif self._base == Base.HEXADECIMAL: + msg = "You must enter a hexadecimal number (e.g. 0xc3)." + self._errorMessageWidget.showMessage(msg) + self.update() + return + + except ValueTooBig: + self._errorMessageWidget.showMessage( + f"Input value does not fit within the bit length of {self._bit_length}." + ) + self.update() + return + + # Turn negative signed numbers into their positive unsigned + # counter part + # Ex 4 bits: + # -7 = 1001 + # -7 + 16 % 16 = 9 = 1001 = -7 + value = (value + max_value) % max_value + + index = row * 4 + col + state['memory'][index] = value + self._memory.set_state(state) diff --git a/src/simudator/gui/module_graphics_item/memory_graphic.py b/src/simudator/gui/module_graphics_item/memory_graphic.py index 2817042cc772ef63fbbce36395542b585fda8eec..c4a944d213270e0f99330c5dede4d914dfc2a611 100644 --- a/src/simudator/gui/module_graphics_item/memory_graphic.py +++ b/src/simudator/gui/module_graphics_item/memory_graphic.py @@ -5,10 +5,17 @@ from qtpy.QtCore import Signal as pyqtSignal from qtpy.QtCore import Slot from qtpy.QtWidgets import ( QAction, + QButtonGroup, + QErrorMessage, QGraphicsRectItem, QGraphicsSimpleTextItem, + QHBoxLayout, + QHeaderView, + QRadioButton, QTableWidget, QTableWidgetItem, + QVBoxLayout, + QWidget, ) from simudator.core.modules import Memory @@ -18,7 +25,66 @@ from simudator.gui.orientation import Orientation from simudator.gui.port_graphics_item import PortGraphicsItem -class MemoryWindow(QTableWidget): +class MemoryWindow(QWidget): + """ + A class showing the contents of a memory module in a new window. + + Parameters + ---------- + memory_module: Memory + An instance of the Memory base class. + edit_buttons: bool + A bool deciding whether or not to add buttons to the widget + for toggling between setting the memory contents to + editable or not. + """ + + def __init__(self, memory_module: Memory, edit_buttons=True): + super().__init__() + + # Create a layout that expands vertically + # so that the buttons can be displayed below the + # memory content + self.layout = QVBoxLayout(self) + self._memory_table = MemoryTable(memory_module) + self.layout.addWidget(self._memory_table) + + if edit_buttons: + # Create edit/view buttons, they are exclusive by default + self.edit_button = QRadioButton("Edit") + self.view_button = QRadioButton("View") + self.edit_group = QButtonGroup() + self.edit_group.addButton(self.edit_button, 1) + self.edit_group.addButton(self.view_button, 2) + + # Connect them to the 'set_base' function + self.edit_group.buttonClicked.connect(self._set_edit) + self.second_button_layout = QHBoxLayout() + self.second_button_layout.addWidget(self.edit_button) + self.second_button_layout.addWidget(self.view_button) + self.layout.addLayout(self.second_button_layout) + + # Set the default base to decimal and disable editing + # from start + self.view_button.toggle() + self._set_edit() + + # Sets the size of each column to the smallest possible + # width that allows all content in each box to be visible + self._memory_table.horizontalHeader().setSectionResizeMode( + QHeaderView.ResizeToContents + ) + self._memory_table.update() + + def _set_edit(self): + pressed_button_id = self.edit_group.checkedId() + if pressed_button_id == 1: + self._memory_table.set_editable(True) + if pressed_button_id == 2: + self._memory_table.set_editable(False) + + +class MemoryTable(QTableWidget): """ A class showing the contents of a memory module in a QTableWidget. @@ -26,8 +92,10 @@ class MemoryWindow(QTableWidget): Parameters ---------- - memory_module: An instance of the Memory base class. - column_size: An integer specifying the number of columns, optional. + memory_module: Memory + An instance of the Memory base class. + column_size: int + An integer specifying the number of columns, optional. """ @@ -38,8 +106,12 @@ class MemoryWindow(QTableWidget): self._memory_size = len(self._memory.get_state()["memory"]) self._set_column_size() self.setColumnCount(self._column_size) - self.setRowCount(ceil(self._memory_size / self._column_size)) + rows = ceil(self._memory_size / self._column_size) + self.setRowCount(rows) + self._init_table_items(self._column_size, rows) self.setHorizontalHeaderLabels(["+" + str(i) for i in range(4)]) + self.set_editable(False) + self._errorMessageWidget = QErrorMessage() vertical_headers = [] for i in range(0, self._memory_size, self._column_size): @@ -49,6 +121,14 @@ class MemoryWindow(QTableWidget): self.update() + # Signal used to detect when the user has edited a cell + # This code can only be called after the initial self.update(). + # When the QTableWidget is initialized it currently fills each celll with empty strings. + # It also signals cellChanged for each cell it initializes. Thus _on_cell_changed + # is called for each cell and tries to save the cells empty string into the modules memory as + # an integer. + self.cellChanged.connect(self._on_cell_changed) + def update(self): """ Update the content of this widget to reflect the content of the memory module. @@ -58,7 +138,8 @@ class MemoryWindow(QTableWidget): value = memory_content[i] row = i // self._column_size col = i % self._column_size - self.set_item(row, col, str(value)) + value = str(value) + self.set_item(row, col, value) def set_item(self, row: int, col: int, text: str) -> None: """Set the text at specified table cell to the given text. @@ -66,9 +147,9 @@ class MemoryWindow(QTableWidget): Parameters ---------- row: int - The items row position in the pipeline diagram. + The items row position in the table. col: int - The items column position in the pipeline diagram. + The items column position in the table. text: str The text to be displayed. """ @@ -76,6 +157,18 @@ class MemoryWindow(QTableWidget): self.setItem(row, col, item) + def set_editable(self, editable: bool) -> None: + if editable: + self.setEditTriggers(self.AllEditTriggers) + else: + self.setEditTriggers(self.NoEditTriggers) + + def _init_table_items(self, cols: int, rows: int) -> None: + for col in range(cols): + for row in range(rows): + item = QTableWidgetItem() + self.setItem(row, col, item) + def _set_column_size(self) -> None: """ Set the column size to a reasonable value if the size was not given to the constructor. @@ -97,6 +190,16 @@ class MemoryWindow(QTableWidget): self._column_size = 1 return + def _on_cell_changed(self, row, col): + state = self._memory.get_state() + item = self.item(row, col) + value = item.text() + if value == "": + return + index = row * 4 + col + state['memory'][index] = value + self._memory.set_state(state) + class MemoryGraphicsItem(ModuleGraphicsItem): """ @@ -172,7 +275,7 @@ class MemoryGraphicsItem(ModuleGraphicsItem): """ Create and show a MemoryWindow that displays the contents of the module. """ - self.memory_window = MemoryWindow(self.module) + self.memory_window = MemoryWindow(self._module) self.memory_window.show() def shouldIgnoreAction(self, action: QAction) -> bool: @@ -192,10 +295,6 @@ class MemoryGraphicsItem(ModuleGraphicsItem): memory_br_action.triggered.connect(self.memoryBreakpointDialog) self.actions.append(memory_br_action) - edit_memory_action = QAction('Edit memory content', self) - edit_memory_action.triggered.connect(self.editMemoryContentDialog) - self.actions.append(edit_memory_action) - show_content_action = QAction('Show memory content', self) show_content_action.triggered.connect(self.showMemoryContents) self.actions.append(show_content_action) @@ -241,7 +340,8 @@ class MemoryGraphicsItem(ModuleGraphicsItem): module_state = self.module.get_state() module_state['memory'][parsed_address] = value self.module.set_state(module_state) - self.update_graphics_signal.emit() + self.module_edited.emit() + self.update() def getActionSignals(self) -> []: # Do parent signals and then add memory specific signals diff --git a/src/simudator/gui/module_graphics_item/module_graphics_item.py b/src/simudator/gui/module_graphics_item/module_graphics_item.py index 30ae67a723d2f6c1fd73596e917d49a19c63b8d6..2860e9e50e343f03d66e29b1927522e2cf8d314e 100644 --- a/src/simudator/gui/module_graphics_item/module_graphics_item.py +++ b/src/simudator/gui/module_graphics_item/module_graphics_item.py @@ -6,6 +6,7 @@ from qtpy.QtCore import Slot from qtpy.QtGui import QCursor from qtpy.QtWidgets import ( QAction, + QErrorMessage, QGraphicsItem, QGraphicsObject, QGraphicsRectItem, @@ -38,12 +39,16 @@ class ModuleGraphicsItem(QGraphicsObject, QGraphicsItem): new_state_breakpoint_signal = pyqtSignal(str, str, str) update_graphics_signal = pyqtSignal() + module_edited = pyqtSignal() + def __init__(self, module: Module, name: str = None): super().__init__() # Save module for later updates self.module = module + self.errorMessageWidget = QErrorMessage() + # Use modules name if no name is given if name is None: self.name = self.module.name @@ -183,15 +188,38 @@ class ModuleGraphicsItem(QGraphicsObject, QGraphicsItem): """ try: parsed_value = ast.literal_eval(value) + + # ast.literal_eval only raises an error the the + # parsed value isn't a valid data type. + # If the user edits GRX and enters a string "a" + # it will crash + + if isinstance(parsed_value, list): + for value in parsed_value: + if isinstance(value, str): + raise TypeError + if isinstance(parsed_value, str): + raise TypeError + except SyntaxError as e: self.errorMessageWidget.showMessage(str(e)) + except ValueError as e: + self.errorMessageWidget.showMessage( + "Value Error. Unable to parse input. Make sure it is in the correct base." + ) + except TypeError as e: + self.errorMessageWidget.showMessage( + "Type Error. Unable to parse input. Make sure it is the correct type." + ) else: module_state = self.module.get_state() module_state[state] = parsed_value self.module.set_state(module_state) # Since we have changed a value we send a signal to the gui to update - self.update_graphics_signal.emit() + self.module_edited.emit() + self.update() + @Slot() def update(self): """ Update the visuals of the graphics item to match it's module. diff --git a/src/simudator/gui/pipeline.py b/src/simudator/gui/pipeline.py index fc9d78012115a011bfc6574414ba6c8a478b6a67..3ba4ffea08234d266b38e1b1526f6803c6059432 100644 --- a/src/simudator/gui/pipeline.py +++ b/src/simudator/gui/pipeline.py @@ -1,5 +1,6 @@ from typing import Any +from qtpy.QtCore import Slot from qtpy.QtWidgets import QTableWidget, QTableWidgetItem @@ -13,7 +14,9 @@ class PipeLine(QTableWidget): def __init__(self): super().__init__() + self.setEditTriggers(self.NoEditTriggers) + @Slot(list) def set_instructions(self, instructions: list[tuple[str, int, int]]) -> None: """ Give the pipeline the current CPU instructions. diff --git a/src/simudator/gui/processor_handler.py b/src/simudator/gui/processor_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..91612fd9a6e5af44e70f145e0745729c632d44a7 --- /dev/null +++ b/src/simudator/gui/processor_handler.py @@ -0,0 +1,335 @@ +from qtpy import QtCore +from qtpy.QtCore import QObject, QThreadPool +from qtpy.QtCore import Signal as pyqtSignal +from qtpy.QtCore import Slot +from qtpy.QtWidgets import QErrorMessage, QFileDialog, QInputDialog, QMessageBox + +from simudator.core.processor import Processor +from simudator.gui.run_continuously_thread import RunThread + + +class ProcessorHandler(QObject): + """ + Wrapper class for interacting with the processor in the GUI. + + Parameters + ---------- + processor : Processor + parent : QObject | None + Optional parent QObject that takes ownership of this QObject. + """ + + running = pyqtSignal(bool) + """ + PyQT signal emitted when the processor has started or finished running. + Emits ``True`` when it has started and ``False`` when it has finished. + """ + cycle_changed = pyqtSignal(int) + """ + PyQT signal emitted when the current clock cycle count of the processor + has changed. Emits the current clock cycle count as an ``int``. + """ + changed = pyqtSignal() + """ + PyQT signal emitted when the processor has changed state. + """ + changed_instruction = pyqtSignal(list) + """ + PyQT signal emitted when the processor has changed the current instruction(s) + being executed. Emits a list of instructions. + + See Also + -------- + simudator.core.processor.get_current_instructions : + Method of the processor used to get instructions that are emitted. + """ + + def __init__( + self, + processor: Processor, + parent: QObject | None = None, + ) -> None: + super().__init__(parent) + self._processor = processor + # Threadpool for running cpu and gui concurrently + self._threadpool = QThreadPool.globalInstance() + + # Used to set if all values in the gui should be updated each tick + # or only the clock counter + self._update_all_values = False + + # Used to set the update delay + # Useful when watching the values being updated while running + self._update_delay = 0.00 + + # Used to lock some actions in ui when cpu is running in another thread + # Using the cpu's internal status directly could case problems + self._processor_running = False + + self._error_msg_box = QErrorMessage() + + def _signal_processor_changed(self) -> None: + """ + Emit signals for signifying that the processor has changed its state. + """ + self.cycle_changed.emit(self._processor.get_clock()) + self.changed.emit() + self.changed_instruction.emit(self._processor.get_current_instructions()) + + @Slot(int) + def step_cycles(self, cycles: int): + """ + Run some number of clock cycles of the processor. + + Parameters + ---------- + Number of clock cycles to perform. + """ + + # Don't do steps if cpu is running + if self._processor_running: + return + + self._processor_running = True + self.running.emit(True) + simulation_thread = RunThread( + self._processor, + self._update_delay, + False, + False, + cycles, + ) + simulation_thread.processor_tick.connect(self.on_processor_tick) + self._threadpool.start(simulation_thread) + + @Slot(int) + def step_asm_instructions(self, instructions: int): + """ + Run some numer of asm instructions. + + Parameters + ---------- + instructions : int + Number of asm instructions to perform. + """ + + # Don't do steps if cpu is running + if self._processor_running: + return + + self._processor_running = True + self.running.emit(True) + simulation_thread = RunThread( + self._processor, self._update_delay, False, True, instructions + ) + simulation_thread.processor_tick.connect(self.on_processor_tick) + self._threadpool.start(simulation_thread) + + @Slot() + def run_simulation(self) -> None: + """ + Run the processor continuously until it is stopped by user input, + reaches a breakpoint or halts on its own. + """ + + # Don't run if already running + if self._processor_running: + return + + # Create own thread for cpu simulation so gui dosent freeze + self._processor_running = True + self.running.emit(True) + simulation_thread = RunThread(self._processor, self._update_delay) + simulation_thread.processor_tick.connect(self.on_processor_tick) + self._threadpool.start(simulation_thread) + + @Slot(int) + def on_processor_tick(self, steps: int) -> None: + """ + Called from other thread after every cpu tick. + Will inform the user and update visuals. + """ + + # Update cpu clock counter every tick + self.cycle_changed.emit(self._processor.get_clock()) + + if self._update_all_values: + self._signal_processor_changed() + + # A signal of 0 steps signifies end of execution, i.e. the CPU has + # halted or run the specified amount of ticks + # => Enable the relevant parts of the GUI again + if steps == 0: + self._processor_running = False + self.running.emit(False) + self._signal_processor_changed() + + # Inform user of reached break point + if self._processor.breakpoint_reached: + QMessageBox.information( + self.parent(), + "SimuDator", + f"Reached breakpoint: {str(self._processor.last_breakpoint)}", + ) + + # Inform user of halt + if self._processor.should_halt(): + QMessageBox.information( + self.parent(), "SimuDator", "The processor halted." + ) + + @Slot() + def stop_simulation(self) -> None: + """ + Stop the processor. + """ + self._processor.stop() + + def get_save_path(self) -> str: + """ + Prompt the user for a file path meant for saving to file. Creates the + specified file if it does not exist. + """ + dialog = QFileDialog() + dialog.setFileMode(QFileDialog.AnyFile) + dialog.setAcceptMode(QFileDialog.AcceptOpen) + + return dialog.getSaveFileName()[0] + + def get_file_path(self) -> str: + """ + Prompt the user for a file path meant for loading the processor state. + """ + dialog = QFileDialog() + dialog.setFileMode(QFileDialog.AnyFile) + dialog.setAcceptMode(QFileDialog.AcceptOpen) + + return dialog.getOpenFileName()[0] + + @Slot() + def load_state(self) -> None: + """ + Load the processor state from selected file. + """ + + # not safe to load while cpu is running + if self._processor_running: + return + + path = self.get_file_path() + try: + self._processor.load_state_from_file(path) + except KeyError: + self._error_msg_box.showMessage("Could not load selected file.", "load_err") + except ValueError: + self._error_msg_box.showMessage("Selected file was empty.", "load_empty") + except FileNotFoundError: + # This error is triggered when no file was selected by + # by the user. The user should know they did not select a + # file and therefore there no need to display a box telling + # them that. + # self.messageBox("No valid file was selected.") + pass + else: + QMessageBox.information(None, "SimuDator", "Loaded file successfully.") + self._signal_processor_changed() + + @Slot() + def reset(self) -> None: + """ + Reset the processor to initial values and signal the processor state + change. + """ + + # not safe to reset while cpu running + if self._processor_running: + return + + answer = QMessageBox.question( + self.parent(), + "Reset Processor", + "Are you sure you want to reset the processor?", + ) + + if answer == QMessageBox.Yes: + self._processor.reset() + self._signal_processor_changed() + + @Slot() + def save_state(self) -> None: + """ + Save the state of the processor to file. + + This erases the previous content of the file before saving the data. + """ + + # Not safe to save while running + if self._processor_running: + return + + path = self.get_save_path() + + # Change file to the selected file if a file was selected + if path == '': + return + + res = self._processor.save_state_to_file(path) + + if res: + QMessageBox.information(self.parent(), "SimuDator", "File saved.") + else: + self._error_msg_box.showMessage("Unable to save.", "save_err") + + @Slot(int) + def undo_cycles(self, cycles: int) -> None: + """ + Undo some number of processor clock cycles. + + Parameters + ---------- + cycles : int + Number of clock cycles to undo. + """ + try: + self._processor.load_cycle(max(self._processor.get_clock() - cycles, 0)) + self._signal_processor_changed() + except (ValueError, IndexError): + self._error_msg_box.showMessage("Unable to undo the cycle.") + + @Slot(int) + def undo_asm_instructions(self, instructions: int) -> None: + """ + Undo some number of asm instructions. + + Parameters + ---------- + instructions : int + Number of instructions to undo. + """ + try: + self._processor.undo_asm_instruction(instructions) + self._signal_processor_changed() + except (ValueError, IndexError): + self._error_msg_box("Unable to undo the instruction.") + + @Slot() + def toggle_value_update_on_run(self): + """ + Toggle whether all values in the GUI related to the processor should be + updated after each clock cycle or after having run several cycles at a + time / running continuously. The clock cycle counter is always updated + after each tick regardless. + """ + self._update_all_values = not self._update_all_values + + @Slot() + def set_update_delay(self): + """ + Set the delay waited between each consecutive clock cycle when running + several cycles or continuously. Prompts the user for the delay. + """ + delay, ok = QInputDialog.getDouble( + None, "Input Dialog", "Enter a float value:", decimals=5 + ) + if ok: + self._update_delay = delay diff --git a/src/simudator/gui/run_continuously_thread.py b/src/simudator/gui/run_continuously_thread.py index 72d0fdcbb58e3352901f21c5fca428a17200f2d4..13368de4f10407cce26c1fb8cf2bb1bc46c10186 100644 --- a/src/simudator/gui/run_continuously_thread.py +++ b/src/simudator/gui/run_continuously_thread.py @@ -1,39 +1,54 @@ import time -from qtpy.QtCore import QRunnable +from qtpy.QtCore import QObject, QRunnable +from qtpy.QtCore import Signal as pyqtSignal + + +class SignalHolder(QObject): + """ + Helper class for RunThread as the thread cannot inherit from QObject to + have signals directly. Instead, an instance of this class is used to hold + signals for the thread. + """ + + processor_tick = pyqtSignal(int) + class RunThread(QRunnable): """ - This class is used to run the simulated cpu several ticks or continuously on - a seperate thread. This allows the user to interact with the GUI while the - simulation is running. + This class is used to run the simulated cpu several ticks or continuously + on a separate thread. This allows the user to interact with the GUI while + the simulation is running. - After each CPU tick, this thread will emit to its given QT signal so that - the GUI can update itself and possibly inform the user of when the execution + After each CPU tick, this thread will emit to its QT signal so that the GUI + can update itself and possibly inform the user of when the execution has halted. """ - def __init__(self, cpu, signal, delay: float, run_continuously=True, - step_asm=False, steps=0): + def __init__( + self, cpu, delay: float, run_continuously=True, step_asm=False, steps=0 + ): + super().__init__() self.cpu = cpu - self.signal = signal self.run_continuously = run_continuously self.steps = steps self.delay = delay self.step_asm = step_asm + self.signals = SignalHolder() + self.processor_tick = self.signals.processor_tick def run(self): if self.step_asm: while self.steps > 0: self.cpu.do_tick() + self.processor_tick.emit(1) # We only care about asm instructions if self.cpu.is_new_instruction(): self.steps -= 1 - self.signal.emit(1) time.sleep(self.delay) if self.cpu.is_stopped: @@ -44,18 +59,17 @@ class RunThread(QRunnable): self.cpu.unstop() while not self.cpu.is_stopped: self.cpu.do_tick() - self.signal.emit(1) + self.processor_tick.emit(1) time.sleep(self.delay) else: for _ in range(self.steps): self.cpu.do_tick() - self.signal.emit(1) + self.processor_tick.emit(1) time.sleep(self.delay) if self.cpu.is_stopped: break - # Signal end of execution as having run 0 ticks - self.signal.emit(0) + self.processor_tick.emit(0) diff --git a/src/simudator/gui/simulation_toolbar.py b/src/simudator/gui/simulation_toolbar.py new file mode 100644 index 0000000000000000000000000000000000000000..090627eaae00362cee96f3aa2f61159b6fc9faf7 --- /dev/null +++ b/src/simudator/gui/simulation_toolbar.py @@ -0,0 +1,193 @@ +from PyQt5.QtWidgets import QAction +from qtpy import QtGui +from qtpy.QtCore import Signal as pyqtSignal +from qtpy.QtCore import Slot +from qtpy.QtWidgets import QLabel, QSpinBox, QStyle, QToolBar, QWidget + + +class SimulationToolBar(QToolBar): + """ + A toolbar for basic controls of a processor simulation. This includes + running and stopping the processor, stepping and undoing clock cycles and + stepping and undoing asm instructions. + + Parameters + ---------- + title : str + Title of the toolbar. + parent : QWidget | None + Parent of the widget. The widget will be a new window if it is ``None`` + (default). + """ + + run = pyqtSignal() + """ + QT signal emitted when the user has requested to run the processor + continuously. + """ + + stop = pyqtSignal() + """ + QT signal emitted when the user has requested to stop the processor. + """ + + step = pyqtSignal(int) + """ + QT signal emitted when the user has requested to step some clock cycles + of the processor. + """ + + undo = pyqtSignal(int) + """ + QT signal emitted when the user has requested to undo some clock cycles of + the processor. + """ + + step_asm = pyqtSignal(int) + """ + QT signal emitted when the user has requested to step some asm instructions + of the processor. + """ + + undo_asm = pyqtSignal(int) + """ + QT signal emitted when the user has requested to undo some asm instructions + of the processor. + """ + + def __init__(self, title, parent: QWidget | None = None): + super().__init__(title, parent) + # Run continuously button + arrow_icon = self.style().standardIcon(QStyle.SP_MediaPlay) + self._run_action = QAction(arrow_icon, "Run", self) + self._run_action.setStatusTip("Run until halt") + self._run_action.triggered.connect(self.run.emit) + self.addAction(self._run_action) + + # Stop button + stop_icon = self.style().standardIcon(QStyle.SP_MediaStop) + self._stop_action = QAction(stop_icon, "Stop", self) + self._stop_action.setStatusTip("Stop running") + self._stop_action.triggered.connect(self.stop.emit) + self.addAction(self._stop_action) + + # Separator and spacing between basic controls and stepping clock cycles + spacing = QWidget() + spacing.setFixedWidth(10) + self.addWidget(spacing) + self.addSeparator() + spacing = QWidget() + spacing.setFixedWidth(10) + self.addWidget(spacing) + + # Clock cycle counter + self._clock_cycle_label = QLabel("Clock cycles: 0", self) + self._clock_cycle_label.setMinimumWidth(150) + self.addWidget(self._clock_cycle_label) + + # Undo clock cycle button + backward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekBackward) + self._undo_action = QAction(backward_arrow_icon, "Undo", self) + self._undo_action.setStatusTip("Undo the last processor tick") + self._undo_action.triggered.connect(self._signal_undo) + self.addAction(self._undo_action) + + # Step clock cycle button + forward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekForward) + self._step_action = QAction(forward_arrow_icon, "Step", self) + self._step_action.setStatusTip("Run one clock cycle") + self._step_action.triggered.connect(self._signal_step) + self.addAction(self._step_action) + + # Spin box for the number of clock cycles to step/undo at a time + self._jump_value_box = QSpinBox() + self._jump_value_box.setMinimum(999999) + self._jump_value_box.setMinimum(1) + self._jump_value_box.setValue(1) + self.addWidget(self._jump_value_box) + + # Separator and spacing between steping clock cycles and + # stepping asm instructions + spacing = QWidget() + spacing.setFixedWidth(10) + self.addWidget(spacing) + self.addSeparator() + spacing = QWidget() + spacing.setFixedWidth(10) + self.addWidget(spacing) + + # Asm instruction counter + self._asm_label = QLabel("Assembler instructions: ", self) + self.addWidget(self._asm_label) + + # Undo asm instruction button + backward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekBackward) + self._undo_asm_action = QAction(backward_arrow_icon, "Undo Asm", self) + self._undo_asm_action.setStatusTip("Undo the last assembler instruction") + self._undo_asm_action.triggered.connect(self._signal_undo_asm) + self.addAction(self._undo_asm_action) + + # Step asm instruction button + forward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekForward) + self._step_asm_action = QAction(forward_arrow_icon, "Step Asm", self) + self._step_asm_action.setStatusTip("Run one assembler instruction") + self._step_asm_action.triggered.connect(self._signal_step_asm) + self.addAction(self._step_asm_action) + + # Spin box for the number of asm instructions to step/undo at a time + self._asm_jump_value_box = QSpinBox() + self._asm_jump_value_box.setMinimum(999999) + self._asm_jump_value_box.setMinimum(1) + self._asm_jump_value_box.setValue(1) + self.addWidget(self._asm_jump_value_box) + + def contextMenuEvent(self, a0: QtGui.QContextMenuEvent | None) -> None: + # Override to remove the functionality of hiding the toolbar as there + # is currently no way of showing it again + pass + + @Slot(int) + def update_clock(self, clock_cycle: int) -> None: + """ + Set the displayed number of the clock cycle counter. + + Parameters + ---------- + clock_cycle : int + Number of clock cycles to display. + """ + self._clock_cycle_label.setText(f"Clock cycles: {clock_cycle}") + + @Slot(bool) + def set_disabled_when_running(self, value: bool) -> None: + """ + Disable relevant parts of the toolbar related to controlling a + processor. Intended to be used to disable stepping functionality + while the processor is running. + + All buttons except for the stop button are disabled. The disabled + buttons are greyed out and cannot be interacted with while the toolbar + is disabled. + + Parameters + ---------- + value : bool + ``True`` to disable the toolbar, ``False`` to enable it. + """ + self._run_action.setDisabled(value) + self._step_action.setDisabled(value) + self._undo_action.setDisabled(value) + self._step_asm_action.setDisabled(value) + self._undo_asm_action.setDisabled(value) + + def _signal_step(self): + self.step.emit(self._jump_value_box.value()) + + def _signal_undo(self): + self.undo.emit(self._jump_value_box.value()) + + def _signal_step_asm(self): + self.step_asm.emit(self._asm_jump_value_box.value()) + + def _signal_undo_asm(self): + self.undo_asm.emit(self._asm_jump_value_box.value()) diff --git a/src/simudator/gui/view.py b/src/simudator/gui/view.py new file mode 100644 index 0000000000000000000000000000000000000000..08e1b3535b69629ce2fa6227024ce9e95e71cb43 --- /dev/null +++ b/src/simudator/gui/view.py @@ -0,0 +1,49 @@ +from qtpy import QtCore, QtWidgets +from qtpy.QtGui import QWheelEvent +from qtpy.QtWidgets import QGraphicsView + + +class View(QGraphicsView): + """ + This class views all QGraphicsItems for the user. It takes the + QGraphicsScene as input and inherits all funcitonality from the + QGraphicsView and overrides the wheelEvent function. + This allows the users to navigate the view with their trackpads + and zoom in/out with their trackpads + ctrl. + """ + + def __init__(self, QGraphicsScene): + super().__init__(QGraphicsScene) + self.scene = QGraphicsScene + + def wheelEvent(self, event: QWheelEvent | None) -> None: + """ + Default behaviour if ctrl is not pressed, otherwise zoom in/out. + """ + modifiers = QtWidgets.QApplication.keyboardModifiers() + if modifiers == QtCore.Qt.ControlModifier: + # Factor above 1 zooms in, below zooms out + factor = 1.03 + if event.angleDelta().y() < 0: + factor = 0.97 + + # If event got triggered due to the x axis, do nothing + if event.pixelDelta().x() != 0: + return + + view_pos = event.globalPosition() + scene_pos = self.mapToScene(int(view_pos.x()), int(view_pos.y())) + + self.centerOn(scene_pos) + self.scale(factor, factor) + + old_mapToScene = self.mapToScene(int(view_pos.x()), int(view_pos.y())) + new_mapToScene = self.mapToScene(self.viewport().rect().center()) + + delta = old_mapToScene - new_mapToScene + + self.centerOn(scene_pos - delta) + + else: + # Default behaviour + super().wheelEvent(event) diff --git a/src/simudator/processor/mia/gui/mia_alu_graphic.py b/src/simudator/processor/mia/gui/mia_alu_graphic.py index 737c7325cdaef600f745f4040fe317e782c80887..c5ef0e2e0b807e45fc07bb24a29a66216ffa4e49 100644 --- a/src/simudator/processor/mia/gui/mia_alu_graphic.py +++ b/src/simudator/processor/mia/gui/mia_alu_graphic.py @@ -25,12 +25,16 @@ class AluGraphicsItem(ModuleGraphicsItem): ) # make port for input a - port_a = PortGraphicsItem(self.module.signals["in_input_a"], Orientation.UP, self) + port_a = PortGraphicsItem( + self.module.signals["in_input_ar"], Orientation.UP, self + ) port_a.setPos(self.RECT_WIDTH * 3 / 4, 0) self.ports.append(port_a) # make port for input b - port_b = PortGraphicsItem(self.module.signals["in_input_b"], Orientation.UP, self) + port_b = PortGraphicsItem( + self.module.signals["in_input_bus"], Orientation.UP, self + ) port_b.setPos(self.RECT_WIDTH / 4, 0) self.ports.append(port_b) diff --git a/src/simudator/processor/mia/gui/mia_grx_graphic.py b/src/simudator/processor/mia/gui/mia_grx_graphic.py index d7d94295ee0adc6f33fc7dc5f1c60d5beaa9bfc5..54dba094d489c8f8493d02cf60550c3cc5fb6c8a 100644 --- a/src/simudator/processor/mia/gui/mia_grx_graphic.py +++ b/src/simudator/processor/mia/gui/mia_grx_graphic.py @@ -3,6 +3,7 @@ import math from qtpy.QtCore import QPointF from qtpy.QtGui import QPolygonF from qtpy.QtWidgets import ( + QErrorMessage, QGraphicsLineItem, QGraphicsPolygonItem, QGraphicsRectItem, @@ -126,14 +127,18 @@ class GrxGraphicsItem(ModuleGraphicsItem): ) # Add ports to and from bus - from_bus_port = PortGraphicsItem(self.module.signals["in_input"], Orientation.RIGHT, self) + from_bus_port = PortGraphicsItem( + self.module.signals["in_input"], Orientation.RIGHT, self + ) from_bus_port.setPos( rect_width + self.LINE_LENGTH + self.MUX_WIDTH, # Use the buses port margins so the ports align nicely mux_height / 2 - BusGraphicsItem.PORT_MARGIN / 2, ) - to_bus_port = PortGraphicsItem(self.module.signals["out_content"], Orientation.RIGHT, self) + to_bus_port = PortGraphicsItem( + self.module.signals["out_content"], Orientation.RIGHT, self + ) to_bus_port.setPos( rect_width + self.LINE_LENGTH + self.MUX_WIDTH, # Use the buses port margins so the ports align nicely diff --git a/src/simudator/processor/mia/gui/mia_memory_graphic.py b/src/simudator/processor/mia/gui/mia_memory_graphic.py index ec29b7717ebc86efe8b0762dc07f3abef6ca34a1..21d61b027d8bc3386cf0ff66eb3809969b47ce0d 100644 --- a/src/simudator/processor/mia/gui/mia_memory_graphic.py +++ b/src/simudator/processor/mia/gui/mia_memory_graphic.py @@ -1,17 +1,13 @@ import ast from qtpy.QtCore import Slot -from qtpy.QtWidgets import ( - QErrorMessage, - QGraphicsRectItem, - QGraphicsSimpleTextItem, - QTextEdit, - QVBoxLayout, - QWidget, -) +from qtpy.QtWidgets import QErrorMessage, QGraphicsRectItem, QGraphicsSimpleTextItem from simudator.core.modules import Memory from simudator.gui.color_scheme import ColorScheme as CS +from simudator.gui.module_graphics_item.integer_memory_graphic import ( + IntegerMemoryWindow, +) from simudator.gui.module_graphics_item.memory_graphic import ( MemoryGraphicsItem, MemoryWindow, @@ -72,7 +68,8 @@ class MiaMemoryGraphicsItem(MemoryGraphicsItem): Create and show a MemoryWindow that displays the contents of the memory module associated with this graphics item. """ - self.memory_window = MemoryWindow(self.module) + bit_length = 16 + self.memory_window = IntegerMemoryWindow(self.module, bit_length) self.memory_window.show() def memoryBreakpointDialog(self) -> None: diff --git a/src/simudator/processor/mia/mia.py b/src/simudator/processor/mia/mia.py index b1a283daa0d51d208afeb6090b5535a48037bfe0..eb89efe3adf5197f7621593bb7c57d7bd787a401 100644 --- a/src/simudator/processor/mia/mia.py +++ b/src/simudator/processor/mia/mia.py @@ -38,7 +38,16 @@ class MIA_CPU(Processor): super().__init__() # Creating all signals # Signals follow the naming convention 'to_from' - self.max_line_len = 100 + + # Add state variable names to skip when pretty printing the MIA + # processor in the CLI + self._ignore_keys += [ + "increment", + "read_from_bus", + "read_from_uADR", + "decrement_by_one", + "bus_id", + ] # A signal used by all modules connected to the bus bus_control = Signal(self, "bus_control", (0, 0)) @@ -50,54 +59,54 @@ class MIA_CPU(Processor): # Needed since optional arguments need to be in the correct order default_value = 0 - pm_bus = Signal(self, "PM-Bus") - bus_pm = Signal(self, "Bus-PM") - asr_bus = Signal(self, "asr_bus") - pm_asr = Signal(self, "pm_asr") - alu_ar = Signal(self, "alu_ar") - ar_alu = Signal(self, "ar_alu") - bus_ar = Signal(self, "bus_ar") - alu_hr = Signal(self, "alu_hr") - hr_alu = Signal(self, "hr_alu") - bus_hr = Signal(self, "bus_hr") - hr_bus = Signal(self, "hr_bus") - alu_bus = Signal(self, "alu_bus") - alu_uM = Signal(self, "alu_uM") - z_alu = Signal(self, "z_alu") - n_alu = Signal(self, "n_alu") - o_alu = Signal(self, "o_alu") - c_alu = Signal(self, "c_alu") - grx_bus = Signal(self, "grx_bus") - bus_grx = Signal(self, "bus_grx") - grx_control = Signal(self, "grx_control") # used to index registers with GRx - m_control = Signal(self, "m_control") # used to index registers with M - s_control = Signal(self, "s_control") # decides if GRx or M is used - pc_uM = Signal(self, "pc_uM") - bus_pc = Signal(self, "bus_pc") - pc_bus = Signal(self, "pc_bus") - pm_bus = Signal(self, "pm_bus") - bus_pm = Signal(self, "bus_pm") + pm_bus = Signal(self, "PM-Bus", 0) + bus_pm = Signal(self, "Bus-PM", 0) + asr_bus = Signal(self, "asr_bus", 0) + pm_asr = Signal(self, "pm_asr", 0) + alu_ar = Signal(self, "alu_ar", 0) + ar_alu = Signal(self, "ar_alu", 0) + bus_ar = Signal(self, "bus_ar", 0) + alu_hr = Signal(self, "alu_hr", 0) + hr_alu = Signal(self, "hr_alu", 0) + bus_hr = Signal(self, "bus_hr", 0) + hr_bus = Signal(self, "hr_bus", 0) + alu_bus = Signal(self, "alu_bus", 0) + alu_uM = Signal(self, "alu_uM", 0) + z_alu = Signal(self, "z_alu", 0) + n_alu = Signal(self, "n_alu", 0) + o_alu = Signal(self, "o_alu", 0) + c_alu = Signal(self, "c_alu", 0) + grx_bus = Signal(self, "grx_bus", 0) + bus_grx = Signal(self, "bus_grx", 0) + grx_control = Signal(self, "grx_control", 0) # used to index registers with GRx + m_control = Signal(self, "m_control", 0) # used to index registers with M + s_control = Signal(self, "s_control", 0) # decides if GRx or M is used + pc_uM = Signal(self, "pc_uM", 0) + bus_pc = Signal(self, "bus_pc", 0) + pc_bus = Signal(self, "pc_bus", 0) + pm_bus = Signal(self, "pm_bus", 0) + bus_pm = Signal(self, "bus_pm", 0) always_write_signal = Signal(self, value=(999, 0)) - uPC_k1 = Signal(self, "uPC_k1") - k1_ir = Signal(self, "k1_ir") - uPC_k2 = Signal(self, "uPC_k2") - ir_bus = Signal(self, "ir_bus") - bus_ir = Signal(self, "bus_ir") - SuPC_uPC = Signal(self, "SuPC_uPC") - uPC_SuPC = Signal(self, "uPC_SuPC") - uM_uPC = Signal(self, "uM_uPC") - uPC_uM_cont = Signal(self, "uPC_uM_cont") - uPC_uM = Signal(self, "uPC_uM") - bus_uM = Signal(self, "bus_uM") - lc_uM = Signal(self, "lc_uM") - lc_uM_uADR = Signal(self, "lc_uM_uADR") - uM_z = Signal(self, "uM_z") - uM_n = Signal(self, "uM_n") - uM_c = Signal(self, "uM_c") - uM_o = Signal(self, "uM_o") - uM_l = Signal(self, "uM_l") - lc_bus = Signal(self, "lc_bus") - l_lc = Signal(self, "l_lc") + uPC_k1 = Signal(self, "uPC_k1", 0) + k1_ir = Signal(self, "k1_ir", 0) + uPC_k2 = Signal(self, "uPC_k2", 0) + ir_bus = Signal(self, "ir_bus", 0) + bus_ir = Signal(self, "bus_ir", 0) + SuPC_uPC = Signal(self, "SuPC_uPC", 0) + uPC_SuPC = Signal(self, "uPC_SuPC", 0) + uM_uPC = Signal(self, "uM_uPC", 0) + uPC_uM_cont = Signal(self, "uPC_uM_cont", 0) + uPC_uM = Signal(self, "uPC_uM", 0) + bus_uM = Signal(self, "bus_uM", 0) + lc_uM = Signal(self, "lc_uM", 0) + lc_uM_uADR = Signal(self, "lc_uM_uADR", 0) + uM_z = Signal(self, "uM_z", 0) + uM_n = Signal(self, "uM_n", 0) + uM_c = Signal(self, "uM_c", 0) + uM_o = Signal(self, "uM_o", 0) + uM_l = Signal(self, "uM_l", 0) + lc_bus = Signal(self, "lc_bus", 0) + l_lc = Signal(self, "l_lc", 0) # ASR specific asr_bus_id = 0b111 @@ -112,11 +121,10 @@ class MIA_CPU(Processor): # HR specific hr_bus_id = 0b101 hr_bit_length = 16 - hr = HR(alu_hr, hr_alu, bus_hr, hr_bus, bus_control, hr_bus_id, hr_bit_length) + hr = HR(alu_hr, hr_alu, hr_bus, bus_hr, bus_control, hr_bus_id, hr_bit_length) # ALU specific alu = ALU( - "ALU", alu_ar, alu_bus, alu_hr, @@ -127,6 +135,7 @@ class MIA_CPU(Processor): n_alu, o_alu, c_alu, + "ALU", ) # PC specific @@ -260,9 +269,9 @@ class MIA_CPU(Processor): for module in ALLTHEMODULES: self.add_module(module) - self.micro_memory = uM + self._micro_memory = uM - self.lambdas = {} + self._lambdas = {} def is_new_instruction(self) -> bool: return self.get_module("uPC").value == 0 @@ -305,7 +314,7 @@ class MIA_CPU(Processor): return (2, 2) def should_halt(self) -> bool: - micro_memory_state = self.micro_memory.get_state() + micro_memory_state = self._micro_memory.get_state() return micro_memory_state["halt"] def launch_gui(self): @@ -334,7 +343,7 @@ class MIA_CPU(Processor): (None, None), (pc.signals["in_input"], pc.signals["out_content"]), (None, None), - (alu.signals["in_input_b"], None), + (alu.signals["in_input_bus"], None), (None, None), (None, ar.signals["out_content"]), (None, None), @@ -343,36 +352,36 @@ class MIA_CPU(Processor): (grx.signals["in_input"], grx.signals["out_content"]), ] - gui.addModuleGraphicsItem( + gui.add_module_graphics_item( BusGraphicsItem(self.get_module("Bus"), bus_signal_pairs) ) - gui.addModuleGraphicsItem(uPcGraphicsItem(self.get_module("uPC"))) - gui.addModuleGraphicsItem(SupcGraphicsItem(self.get_module("SuPC"))) - gui.addModuleGraphicsItem(PcGraphicsItem(self.get_module("PC"))) - gui.addModuleGraphicsItem(ArGraphicsItem(self.get_module("AR"))) - gui.addModuleGraphicsItem(AsrGraphicsItem(self.get_module("ASR"))) - gui.addModuleGraphicsItem(HrGraphicsItem(self.get_module("HR"))) - gui.addModuleGraphicsItem(MicroMemoryGraphicsItem(self.get_module("uM"))) - gui.addModuleGraphicsItem(AluGraphicsItem(self.get_module("ALU"))) - gui.addModuleGraphicsItem(GrxGraphicsItem(self.get_module("GRx"))) - gui.addModuleGraphicsItem(IrGraphicsItem(self.get_module("IR"))) + gui.add_module_graphics_item(uPcGraphicsItem(self.get_module("uPC"))) + gui.add_module_graphics_item(SupcGraphicsItem(self.get_module("SuPC"))) + gui.add_module_graphics_item(PcGraphicsItem(self.get_module("PC"))) + gui.add_module_graphics_item(ArGraphicsItem(self.get_module("AR"))) + gui.add_module_graphics_item(AsrGraphicsItem(self.get_module("ASR"))) + gui.add_module_graphics_item(HrGraphicsItem(self.get_module("HR"))) + gui.add_module_graphics_item(MicroMemoryGraphicsItem(self.get_module("uM"))) + gui.add_module_graphics_item(AluGraphicsItem(self.get_module("ALU"))) + gui.add_module_graphics_item(GrxGraphicsItem(self.get_module("GRx"))) + gui.add_module_graphics_item(IrGraphicsItem(self.get_module("IR"))) flag_modules = ["Z-Flag", "N-Flag", "C-Flag", "O-Flag", "L-Flag", "LC"] for name in flag_modules: module = self.get_module(name) widget = FlagGraphicsItem(module) - gui.addModuleGraphicsItem(widget) + gui.add_module_graphics_item(widget) memory_modules = ["PM", "K1", "K2"] for name in memory_modules: module = self.get_module(name) widget = MiaMemoryGraphicsItem(module) - gui.addModuleGraphicsItem(widget) + gui.add_module_graphics_item(widget) - gui.addAllSignals() + gui.add_all_signals() gui.show() - gui.loadLayoutFromFile("mia_layout") + gui.load_layout("mia_layout") app.exec() def launch_cli(self): diff --git a/src/simudator/processor/mia/modules/alu.py b/src/simudator/processor/mia/modules/alu.py index 482da580be76ab06e8f941cbf00ef8febd577e5c..30c27eea53e9e1399f980f2f326924e6684bb18c 100644 --- a/src/simudator/processor/mia/modules/alu.py +++ b/src/simudator/processor/mia/modules/alu.py @@ -1,70 +1,104 @@ from simudator.core.module import Module from simudator.core.signal import Signal +from simudator.processor.mia.modules.alu_arithmetics import ( + add_binary_numbers, + arithmetic_right_shift, + bin_to_int, + binary_and, + binary_or, + bitwise_not, + int_to_bin, + left_rotate, + logical_left_shift, + logical_right_shift, +) class ALU(Module): """ - Module used for arithmetic logic calculations. - - Takes two input signals that are used to recive values - for calculations. - Takes one control signal to control what operand should - be executed. - Takes four flags too give extra infromation from calulations. - Will do all calulations as two's complement binary numbers. + Module used for arithmetic logic calculations in MIA. + + Parameters + ---------- + input_ar : Signal + Input from the AR register, used as the first operand in calculations. + input_bus : Signal + Input from the bus, used as the second operand in calculations. + input_hr : Signal + Input from the HR register, used in 32-bit calculations. + control : Signal + Control signal for choosing ALU operation. + output_ar : Signal + Output signal to the AR register onto which the operation result is + written. + output_hr : Signal + Output signal to the HR register onto which the lower 16-bits of the + result of a 32-bit calculation are written. + z_flag : Signal + Output for setting the z-flag after a calculation. + n_flag : Signal + Output for setting the n-flag after a calculation. + o_flag : Signal + Output for setting the o-flag after a calculation. + c_flag : Signal + Output for setting the c-flag after a calculation. + name : Signal + Name of the ALU. """ - __slots__ = ("z_value", "n_value", "o_value", "c_value", "hr_value") + __slots__ = ("_z_value", "_n_value", "_o_value", "_c_value", "_hr_value") WORD_LENGTH = 16 def __init__( self, + input_ar: Signal, + input_bus: Signal, + input_hr: Signal, + control: Signal, + output_ar: Signal, + output_hr: Signal, + z_flag: Signal, + n_flag: Signal, + o_flag: Signal, + c_flag: Signal, name: str, - input_signal_a: Signal, - input_signal_b: Signal, - input_signal_hr: Signal, - control_signal: Signal, - output_signal: Signal, - output_signal_hr: Signal, - z_signal: Signal, - n_signal: Signal, - o_signal: Signal, - c_signal: Signal, ): signals = { - "in_input_a": input_signal_a, - "in_input_b": input_signal_b, - "in_control": control_signal, - "in_hr": input_signal_hr, - "out_result": output_signal, - "out_hr": output_signal_hr, - "out_flag_z": z_signal, - "out_flag_n": n_signal, - "out_flag_o": o_signal, - "out_flag_c": c_signal, + "in_input_ar": input_ar, + "in_input_bus": input_bus, + "in_control": control, + "in_hr": input_hr, + "out_result": output_ar, + "out_hr": output_hr, + "out_flag_z": z_flag, + "out_flag_n": n_flag, + "out_flag_o": o_flag, + "out_flag_c": c_flag, } super().__init__(signals, name) # flag values - self.z_value = 0 - self.n_value = 0 - self.o_value = 0 - self.c_value = 0 + self._z_value = 0 + self._n_value = 0 + self._o_value = 0 + self._c_value = 0 # internal values - self.hr_value = None + self._hr_value = None def update_register(self) -> None: - """ - The alu has no register. + """Do nothing. + + The alu has no internal register. """ pass def output_register(self) -> None: - """ - The alu has no register. + """Do nothing. + + The alu has no internal register. """ pass @@ -77,103 +111,131 @@ class ALU(Module): def update_logic(self) -> None: """ - Will calculate and operation depending on given contorl signal + Will calculate and operation depending on given control signal and update outputs with result. """ - value_a = self.signals["in_input_a"].get_value() - value_b = self.signals["in_input_b"].get_value() + value_ar = self.signals["in_input_ar"].get_value() + value_bus = self.signals["in_input_bus"].get_value() value_hr = self.signals["in_hr"].get_value() - - # bitmask control value to get first 4 bits control_value = self.signals["in_control"].get_value() - # If control value is none do nothing - if control_value is None: - return - # Mask control value control_value = control_value & 0b1111 # Define output value - output_value = None + bin_ar = int_to_bin(value_ar, self.WORD_LENGTH) + bin_bus = int_to_bin(value_bus, self.WORD_LENGTH) + update_z_n_flags = True + update_o_flag = False + bin_res = None + carry = self._c_value # Check control value for action match control_value: case 0b0000: # Do nothing + update_z_n_flags = False return case 0b0001: # set output to only b - output_value = value_b + update_z_n_flags = False + bin_res = bin_bus case 0b0010: # calculate bitwise not of b - output_value = self.ones_complement(value_b) + update_z_n_flags = False + bin_res = bitwise_not(bin_bus) case 0b0011: # returns 0 - output_value = 0 + bin_res = int_to_bin(0, self.WORD_LENGTH) case 0b0100: # return sum of a and b - output_value = self.add_numbers(value_a, value_b, True) + update_o_flag = True + bin_res, carry = add_binary_numbers(bin_ar, bin_bus) case 0b0101: - # returns the differense of a and b - output_value = self.subtract_numbers(value_a, value_b, True) + # returns the difference of a and b + update_o_flag = True + bin_bus = bitwise_not(bin_bus) + bin_res, carry = add_binary_numbers(bin_ar, bin_bus, 1) case 0b0110: # returns the bitwise AND of a and b - output_value = self.and_numbers(value_a, value_b) + bin_res = binary_and(bin_ar, bin_bus) case 0b0111: # returns the bitwise OR of a and b - output_value = self.or_numbers(value_a, value_b) + bin_res = binary_or(bin_ar, bin_bus) case 0b1000: # return sum of a and b without changing flags - output_value = self.add_numbers(value_a, value_b, False) + update_z_n_flags = False + update_o_flag = False + bin_res, _ = add_binary_numbers(bin_ar, bin_bus) case 0b1001: - # returns the leftshifted a - output_value = self.logical_left_shift(value_a) + # returns the leftshifted AR + bin_res, carry = logical_left_shift(bin_ar) case 0b1010: - # returns the leftshifted a (with HR) - output_value = self.logical_left_shift(value_a, value_hr) + # returns the leftshifted ARHR + long_word = bin_ar + int_to_bin(value_hr, self.WORD_LENGTH) + bin_res, carry = logical_left_shift(long_word) + self._hr_value = bin_to_int(bin_res[self.WORD_LENGTH :]) case 0b1011: - # returns the arithmetic righshifted a - output_value = self.arithmetic_right_shift(value_a) + # returns the arithmetic righshifted AR + bin_res, carry = arithmetic_right_shift(bin_ar) case 0b1100: - # returns the arithmetic righshifted a (with HR) - output_value = self.arithmetic_right_shift(value_a, value_hr) + # returns the arithmetic righshifted ARHR + long_word = bin_ar + int_to_bin(value_hr, self.WORD_LENGTH) + bin_res, carry = arithmetic_right_shift(long_word) + self._hr_value = bin_to_int(bin_res[self.WORD_LENGTH :]) case 0b1101: - # returns the logically righshifted a - output_value = self.logical_right_shift(value_a) + # returns the logically righshifted AR + bin_res, carry = logical_right_shift(bin_ar) case 0b1110: - # returns a one step rotated left - output_value = self.left_rotate(value_a) + # returns AR one step rotated left + bin_res, carry = left_rotate(bin_ar) case 0b1111: - # returns a one step rotated left (with HR) - output_value = self.left_rotate(value_a, value_hr) + # returns ARHR one step rotated left + long_word = bin_ar + int_to_bin(value_hr, self.WORD_LENGTH) + bin_res, carry = left_rotate(long_word) + self._hr_value = bin_to_int(bin_res[self.WORD_LENGTH :]) case _: return # Update output signal and flags - self.signals["out_result"].update_value(output_value) - self.signals["out_flag_z"].update_value(self.z_value) - self.signals["out_flag_n"].update_value(self.n_value) - self.signals["out_flag_o"].update_value(self.o_value) - self.signals["out_flag_c"].update_value(self.c_value) - self.signals["out_hr"].update_value(self.hr_value) + self._update_flags( + bin_ar, + bin_bus, + bin_res, + carry, + update_z_n_flags, + update_o_flag, + ) + + if len(bin_res) == 2 * self.WORD_LENGTH: + # In case of a 32-bit calculation, extract the upper 16-bits + # that should go into the AR register + # (Not done earlier to properly update the flags as a 32-bit number) + bin_res = bin_res[: self.WORD_LENGTH] + res = bin_to_int(bin_res) + self.signals["out_result"].update_value(res) + self.signals["out_flag_z"].update_value(self._z_value) + self.signals["out_flag_n"].update_value(self._n_value) + self.signals["out_flag_o"].update_value(self._o_value) + self.signals["out_flag_c"].update_value(self._c_value) + self.signals["out_hr"].update_value(self._hr_value) def print_module(self) -> None: print( @@ -181,496 +243,71 @@ class ALU(Module): self.name, "\n -----", "\n a_value: ", - self.signals["in_input_a"].get_value(), + self.signals["in_input_ar"].get_value(), "\n b_value: ", - self.signals["in_input_b"].get_value(), + self.signals["in_input_bus"].get_value(), "\n control_signal: ", self.signals["in_control"].get_value(), ) def reset(self) -> None: - self.z_value = 0 - self.n_value = 0 - self.o_value = 0 - self.c_value = 0 - self.hr_value = None - - # ----- ALU operations ----- - - def ones_complement(self, number: int) -> int: - """ - Returns the ones complement of an int. - """ - binary_number = self.int_to_bin(number) - binary_complement = self.bitwise_not(binary_number) - result = self.bin_to_int(binary_complement) - return result - - def add_numbers(self, number_a: int, number_b: int, change_flags: bool) -> int: - """ - Sum two numbers as binary numbers. - - Will update flags if asked to - """ - - # Convert to binary - binary_a = self.int_to_bin(number_a) - binary_b = self.int_to_bin(number_b) - - # Add the numbers - result_tuple = self.add_binary_numbers(binary_a, binary_b) - - # Change flags - if change_flags: - self.update_flags(binary_a, binary_b, result_tuple[0], result_tuple[1]) - - # Convert back to int - result = self.bin_to_int(result_tuple[0]) - return result - - def subtract_numbers(self, number_a: int, number_b: int, change_flags: bool) -> int: - """ - Calculates difference between to two numbers as binary numbers. - - Will update flags if asked to. - """ - - # Convert to binary - binary_a = self.int_to_bin(number_a) - # Convert b to a negative number - binary_b = self.int_to_negative_bin(number_b) - - # Add the numbers - result_tuple = self.add_binary_numbers(binary_a, binary_b) - - # Change flags - if change_flags: - self.update_flags(binary_a, binary_b, result_tuple[0], result_tuple[1]) - - # Convert back to int - result = self.bin_to_int(result_tuple[0]) - return result - - def and_numbers(self, number_a: int, number_b: int) -> int: - """ - Bitwise and two int's and return the result. - - Will update flags. - """ - - # Convert to binary - binary_a = self.int_to_bin(number_a) - binary_b = self.int_to_bin(number_b) - - # Calulate and - binary_and = self.binary_and(binary_a, binary_b) - - # Update flags, send data only to update z and n flags - self.update_flags(None, None, binary_and, None) - - # Convert to int - result = self.bin_to_int(binary_and) - - return result - - def or_numbers(self, number_a: int, number_b: int) -> int: - """ - Bitwise or two int's and return the result. - - Will update flags. - """ - # Convert to binary - binary_a = self.int_to_bin(number_a) - binary_b = self.int_to_bin(number_b) - - # Calulate and - binary_or = self.binary_or(binary_a, binary_b) - - # Update flags, send data only to update z and n flags - self.update_flags(None, None, binary_or, None) - - # Convert to int - result = self.bin_to_int(binary_or) - - return result - - def logical_left_shift(self, number: int, hr_value: int = None) -> int: - """ - Left shifts given number as a binary with word. - - Fills right with 0's after shift. - Will update carry with removed bit. - """ - - # Convert to binary representation - binary_number = self.int_to_bin(number) - - new_binary_number = None - carry = None - - if hr_value is not None: - # get hr as a binary - hr_binary = self.int_to_bin(hr_value) - - # do the shift - long_word_binary = hr_binary[1:] + binary_number + [0] - - # get hr number - new_hr_number = self.bin_to_int(long_word_binary[: self.WORD_LENGTH]) - - # update hr - self.hr_value = new_hr_number - - # get new value from binary - new_binary_number = long_word_binary[self.WORD_LENGTH :] - - # Update carry with removed bit - carry = hr_binary[0] - - else: - # Do shift - new_binary_number = binary_number[1:] + [0] - - # Update carry with removed bit - carry = binary_number[0] - - # Convert shifted to int - value = self.bin_to_int(new_binary_number) - - self.update_flags(None, None, new_binary_number, carry) - - return value - - def logical_right_shift(self, number: int) -> int: - """ - Left shifts given number as a binary. - - Fills right with 0's after shift. - Will update carry with removed bit. - """ - # Conert to binary - binary_number = self.int_to_bin(number) - - # Do shift - new_binary_number = [0] + binary_number[:-1] - - # Convert shifted to int - value = self.bin_to_int(new_binary_number) - - # Update flags - self.update_flags(None, None, new_binary_number, binary_number[-1]) - - return value - - def arithmetic_right_shift(self, number: int, hr_value: int = None) -> int: - """ - Right shifts given number as a binary with word length WORD_LENGTH. - - Fills right with most significant bit. - Will update carry with removed bit. - """ - # Convert to binary representation - binary_number = self.int_to_bin(number) - - new_binary_number = None - carry = None - - if hr_value is not None: - # get hr as a binary - hr_binary = self.int_to_bin(hr_value) - - # do shift - long_word_binary = [binary_number[-1]] + hr_binary + binary_number[:-1] - - # get hr number - new_hr_number = self.bin_to_int(long_word_binary[: self.WORD_LENGTH]) - - # update hr - self.hr_value = new_hr_number - - # get new value from binary - new_binary_number = long_word_binary[self.WORD_LENGTH :] - - # Update carry with removed bit - carry = binary_number[-1] + self._z_value = 0 + self._n_value = 0 + self._o_value = 0 + self._c_value = 0 + self._hr_value = None - else: - # Do shift - new_binary_number = [binary_number[0]] + binary_number[:-1] - - # Update carry with removed bit - carry = binary_number[-1] - - # Convert shifted to int - value = self.bin_to_int(new_binary_number) - - # Update flags - self.update_flags(None, None, new_binary_number, carry) - - return value - - def left_rotate(self, number: int, hr_value: int = None) -> int: - """ - Rotates given number left as a binary with word length WORD_LENGTH. - - Fills right with 0's after shift. - """ - - # Convert to binary representation - binary_number = self.int_to_bin(number) - - new_binary_number = None - carry = None - - if hr_value is not None: - # get hr as a binary - hr_binary = self.int_to_bin(self.signals["in_hr"].get_value()) - - long_word_binary = hr_binary[1:] + binary_number + [hr_binary[0]] - - # get hr number - new_hr_number = self.bin_to_int(long_word_binary[: self.WORD_LENGTH]) - - # update hr - self.hr_value = new_hr_number - - # get new value from binary - new_binary_number = long_word_binary[self.WORD_LENGTH :] - - # Update carry with removed bit - carry = hr_binary[0] - - else: - # Do rotation - new_binary_number = binary_number[1:] + [binary_number[0]] - - # Update carry with removed bit - carry = binary_number[0] - - # Convert shifted to int - value = self.bin_to_int(new_binary_number) - - self.update_flags(None, None, new_binary_number, carry) - - return value - - # ----- Converters ----- - - def bin_to_int(self, binary_number: [int], word_length: int = None) -> int: - """ - Converts from binary list representation to int. - - """ - - # Set default word length if none is specified - if word_length is None: - word_length = self.WORD_LENGTH - - number = 0 - - # Get values for bits and add them - for bit_index in range(0, word_length): - number += binary_number[bit_index] * 2 ** (word_length - bit_index - 1) - - return number - - def int_to_bin(self, number: int, word_length: int = None) -> [int]: - """ - Converts an int to a list binary representation where - each index in the list represents one bit of the word. - - This is done to more easily handle edge cases that occur - when doing arithmetic with fixed word lenghts. - This version can not handel negative numbers. - """ - - # Set default word length if none is specified - if word_length is None: - word_length = self.WORD_LENGTH - - # Get list representation with correct length - binary_number = [0 for i in range(word_length)] - - # We can now look at the other bits - for bit_index in range(0, word_length): - # If number is larger then bit we subtract the - # bits decimal value from the number and make the bit 1. - if number >= 2 ** (word_length - bit_index - 1): - number -= 2 ** (word_length - bit_index - 1) - binary_number[bit_index] = 1 - - return binary_number - - def int_to_negative_bin(self, number: int) -> [int]: - """ - Returns the negative of a given int as a binary list. - """ - - binary_number = self.int_to_bin(number) - not_binary_number = self.bitwise_not(binary_number) - binary_one = [0 for i in range(15)] + [1] - result_tuple = self.add_binary_numbers(not_binary_number, binary_one) - negativt_binary = result_tuple[0] - return negativt_binary - - def int_to_bin_twocomp(self, number: int) -> [int]: - """ - Converts an int to a list binary representation where - each index in the list represents one bit of the word. - - This is done to more easily handle edge cases that occur - when doing arithmetic with fixed word lenghts. - """ - - # Get list representation with correct length - binary_number = [0 for i in range(self.WORD_LENGTH)] - - # Using twos complement representation - # so handel negativ numbers accordingly - if number < 0: - binary_number[0] = 1 - - # We add this so the rest of the caclulation is - # the same for nagativ numbers - number += 2 ** (self.WORD_LENGTH - 1) - - # We can now look at the other bits - for bit_index in range(1, self.WORD_LENGTH): - # If number is larger then bit we subtract the - # bits decimal value from the number and make the bit 1. - if number >= 2 ** (self.WORD_LENGTH - bit_index - 1): - number -= 2 ** (self.WORD_LENGTH - bit_index - 1) - binary_number[bit_index] = 1 - - return binary_number - - def bin_to_int_twocomp(self, binary_number: [int]) -> int: - """ - Converts from two's complement binary list representation to int. - """ - number = 0 - - # two's complement representation so most significant - # bit will be negativ - if binary_number[0] == 1: - number = -(2 ** (self.WORD_LENGTH - 1)) - - # Get values for other bits - for bit_index in range(1, self.WORD_LENGTH): - number += binary_number[bit_index] * 2 ** (self.WORD_LENGTH - bit_index - 1) - - return number - - # ----- Internal operations ----- - - def update_flags( + def _update_flags( self, - binary_a: list[int], - binary_b: list[int], - binary_result: list[int], + a: list[int], + b: list[int], + res: list[int], carry: int, + z_n: bool = True, + o: bool = False, ) -> None: - """ - Will update the flags depending on inputed values and the result. - This function can take none for any input and will update the - as well as possible for all flags. - """ - - if binary_result is not None: - # Update z flag if we have result - # For this we check if result is zero - if self.bin_to_int(binary_result, len(binary_result)) == 0: - self.z_value = 1 + """Update the outputted values to the flags of the ALU depending on + the operands and resulting from the ALU operation. + + It is possible to not update specific flags. For convenience, the + c-flag is always updated with the supplied carry value. + + Parameters + ---------- + a : list[int] + First ALU operand. + b : list[int] + Second ALU operand. + res : list[int] + Result of the ALU operation. + carry : int + Carry bit from the ALU operation. + z_n : bool + ``True`` if the z- and n-flags should be updated, ``False`` if not. + o : bool + ``True`` if the o-flag should be updated, ``False`` if not + """ + + if z_n: + # Set the z flag if the result is 0 + if bin_to_int(res) == 0: + self._z_value = 1 else: - self.z_value = 0 + self._z_value = 0 - # Update n flag if we have result - # For this we check if the value is negativ in twos complement - if binary_result[0] == 1: - self.n_value = 1 + # Set the n flag if the most significant bit is 1, + # i.e. the number is negative if interpreted as two's complement + if res[0] == 1: + self._n_value = 1 else: - self.n_value = 0 + self._n_value = 0 - # If we also have given values we can calculate o flag - if binary_a is not None and binary_b is not None: - # Check if last bit in result is diffrent from both iven values - if (binary_a[0] == binary_b[0]) and (binary_a[0] != binary_result[0]): - self.o_value = 1 - else: - self.o_value = 0 - - # Set carry if we have it - if carry is not None: - self.c_value = carry - - def bitwise_not(self, binary_number: [int]) -> [int]: - """ - Returns bitwise not of given binary number - """ - - # Make empty binary number for result - new_binary_number = [0 for _ in range(self.WORD_LENGTH)] - - # Change each bits value - for bit_index in range(self.WORD_LENGTH): - if binary_number[bit_index] == 0: - new_binary_number[bit_index] = 1 - elif binary_number[bit_index] == 1: - new_binary_number[bit_index] = 0 - - return new_binary_number - - def add_binary_numbers( - self, binary_a: list[int], binary_b: list[int] - ) -> tuple[list[int], int]: - """ - Returns a tuple with the sum of two binary numbers - and the carry bit from the calculations - """ - - # Make new binary number to store results - binary_sum = [0 for _ in range(self.WORD_LENGTH)] - - # Need carry for calculation - carry = 0 - - # Do addition bit by bit and save carry between each loop - for bit_index in range(self.WORD_LENGTH - 1, -1, -1): - bit_sum = binary_a[bit_index] + binary_b[bit_index] + carry - match bit_sum: - case 0: - binary_sum[bit_index] = 0 - carry = 0 - case 1: - binary_sum[bit_index] = 1 - carry = 0 - case 2: - binary_sum[bit_index] = 0 - carry = 1 - case 3: - binary_sum[bit_index] = 1 - carry = 1 - - return (binary_sum, carry) - - def binary_and(self, binary_a: list[int], binary_b: list[int]) -> list[int]: - """ - Calculate and between binary two numbers - """ - binary_and = [0 for _ in range(self.WORD_LENGTH)] - - for binary_index in range(self.WORD_LENGTH): - binary_and[binary_index] = binary_a[binary_index] & binary_b[binary_index] - - return binary_and - - def binary_or(self, binary_a: list[int], binary_b: list[int]) -> list[int]: - """ - Calculate or between two binary numbers - """ - binary_or = [0 for _ in range(self.WORD_LENGTH)] - - for binary_index in range(self.WORD_LENGTH): - binary_or[binary_index] = binary_a[binary_index] | binary_b[binary_index] + if o: + # Set the o flag if the MSB is the same for both a and b and this + # bit is different to the MSB of the result + if (a[0] == b[0]) and (a[0] != res[0]): + self._o_value = 1 + else: + self._o_value = 0 - return binary_or + self._c_value = carry diff --git a/src/simudator/processor/mia/modules/alu_arithmetics.py b/src/simudator/processor/mia/modules/alu_arithmetics.py new file mode 100644 index 0000000000000000000000000000000000000000..f0f00d39c62cfe63f87c9c756d4a30f5061578ec --- /dev/null +++ b/src/simudator/processor/mia/modules/alu_arithmetics.py @@ -0,0 +1,293 @@ +def bin_to_int(binary_number: list[int]) -> int: + """ + Convert from binary bit list representation to integer. + + Parameters + ---------- + binary_number : list[int] + Binary number represented as a list of bits. + + Returns + ------- + int + The binary number converted to an integer. + """ + number = 0 + word_length = len(binary_number) + + # Get values for bits and add them + for bit_index in range(0, word_length): + number += binary_number[bit_index] * 2 ** (word_length - bit_index - 1) + + return number + + +def int_to_bin(number: int, word_length: int) -> list[int]: + """Convert a non-negative integer to a binary number represented as a list + of bits. + + Parameters + ---------- + number : int + Non-negative integer to convert to binary. + word_length : int + Number of bits to represent the binary number. + + Returns + ------- + list[int] + List of bits representing the number converted to binary. + """ + # Get list representation with correct length + binary_number = [0 for _ in range(word_length)] + + # We can now look at the other bits + for bit_index in range(0, word_length): + # If number is larger then bit we subtract the + # bits decimal value from the number and make the bit 1. + if number >= 2 ** (word_length - bit_index - 1): + number -= 2 ** (word_length - bit_index - 1) + binary_number[bit_index] = 1 + + return binary_number + + +def bin_to_int_twocomp(binary_number: list[int]) -> int: + """Convert from two's complement binary bit list representation to integer. + + Parameters + ---------- + binary_number : list[int] + Binary number in two's complement, represented as a bit list. + + Returns + ------- + int + The binary number converted to an integer. + """ + number = 0 + word_length = len(binary_number) + + # two's complement representation so most significant + # bit will be negative + if binary_number[0] == 1: + number = -(2 ** (word_length - 1)) + + # Get values for other bits + for bit_index in range(1, word_length): + number += binary_number[bit_index] * 2 ** (word_length - bit_index - 1) + + return number + + +def bitwise_not(binary_number: list[int]) -> list[int]: + """Perform bitwise not on a binary number. + + Parameters + ---------- + binary_number : list[int] + Binary number represented as a list of bits. + + Returns + ------- + list[int] + Resulting bit-flipped binary number as a list of bits. + """ + + # Make empty binary number for result + new_binary_number = [0 for _ in range(len(binary_number))] + + # Change each bits value + for bit_index in range(len(binary_number)): + if binary_number[bit_index] == 0: + new_binary_number[bit_index] = 1 + elif binary_number[bit_index] == 1: + new_binary_number[bit_index] = 0 + + return new_binary_number + + +def add_binary_numbers( + a: list[int], b: list[int], carry: int = 0 +) -> tuple[list[int], int]: + """Add two binary numbers together. + + Assumes both numbers are represented with the same number of bits, + i.e. have the same word length. + + Parameters + ---------- + a : list[int] + First operand represented as a list of bits. + b : list[int] + Second operand represented as a list of bits. + carry : int + Carry in bit for the least significant bit. Useful for subtraction. + + Returns + ------- + sum : list[int] + Resulting binary sum represented as a list of bits. + carry : int + Carry bit from performing the summation. + """ + + # Make new binary number to store results + word_length = len(a) + binary_sum = [0 for _ in range(word_length)] + + # Need carry for calculation + # carry = 0 + + # Do addition bit by bit and save carry between each loop + for bit_index in range(word_length - 1, -1, -1): + bit_sum = a[bit_index] + b[bit_index] + carry + match bit_sum: + case 0: + binary_sum[bit_index] = 0 + carry = 0 + case 1: + binary_sum[bit_index] = 1 + carry = 0 + case 2: + binary_sum[bit_index] = 0 + carry = 1 + case 3: + binary_sum[bit_index] = 1 + carry = 1 + + return (binary_sum, carry) + + +def binary_and(a: list[int], b: list[int]) -> list[int]: + """ + Calculate bitwise and between two binary numbers. + + Assumes both numbers are represented with the same number of bits, + i.e. have the same word length. + + Parameters + ---------- + a : list[int] + First operand represented as a list of bits. + b : list[int] + Second operand represented as a list of bits. + + Returns + ------- + Resulting binary number as a list of bits. + """ + word_length = len(a) + binary_and = [0 for _ in range(word_length)] + + for binary_index in range(word_length): + binary_and[binary_index] = a[binary_index] & b[binary_index] + + return binary_and + + +def binary_or(a: list[int], b: list[int]) -> list[int]: + """Calculate bitwise or between two binary numbers + + Assumes both numbers are represented with the same number of bits, + i.e. have the same word length. + + Parameters + ---------- + a : list[int] + First operand represented as a list of bits. + b : list[int] + Second operand represented as a list of bits. + + Returns + ------- + Resulting binary number as a list of bits. + """ + word_length = len(a) + binary_or = [0 for _ in range(word_length)] + + for binary_index in range(word_length): + binary_or[binary_index] = a[binary_index] | b[binary_index] + + return binary_or + + +def logical_left_shift(bin_num: list[int]) -> tuple[list[int], int]: + """Logically left shift a binary number. + + Parameters + ---------- + bin_num : list[int] + Binary number represented as a list of bits. + + Returns + ------- + shifted_bin_num : list[int] + Resulting shifted binary number as a list of bits. + carry : int + Carry bit from the left shift, i.e. the bit that is shifted out. + """ + new_bin_num = bin_num[1:] + [0] + carry = bin_num[0] + + return new_bin_num, carry + + +def logical_right_shift(bin_num: list[int]) -> tuple[list[int], int]: + """Logically right shift a binary number. + + Parameters + ---------- + bin_num : list[int] + Binary number represented as a list of bits. + + Returns + ------- + shifted_bin_num : list[int] + Resulting shifted binary number as a list of bits. + carry : int + Carry bit from the right shift, i.e. the bit that is shifted out. + """ + new_bin_num = [0] + bin_num[:-1] + carry = bin_num[-1] + return new_bin_num, carry + + +def arithmetic_right_shift(bin_num: list[int]) -> tuple[list[int], int]: + """Arithmetically right shift a binary number. + + Parameters + ---------- + bin_num : list[int] + Binary number represented as a list of bits. + + Returns + ------- + shifted_bin_num : list[int] + Resulting shifted binary number as a list of bits. + carry : int + Carry bit from the right shift, i.e. the bit that is shifted out. + """ + new_bin_num = [bin_num[0]] + bin_num[:-1] + carry = bin_num[-1] + return new_bin_num, carry + + +def left_rotate(bin_num: list[int]) -> tuple[list[int], int]: + """Rotate the bits of a number to the left. + + Parameters + ---------- + bin_num : list[int] + Binary number represented as a list of bits. + + Returns + ------- + rotated_bin_num : list[int] + Resulting rotated binary number as a list of bits. + carry : int + Carry bit from the rotation, i.e. the bit that is shifted "out". + """ + new_bin_num = bin_num[1:] + [bin_num[0]] + carry = bin_num[0] + return new_bin_num, carry diff --git a/test/test_core/test_breakpoint.py b/test/test_core/test_breakpoint.py index 01f115d012ba1a4cf042244d71761be28dd28673..37992bfb0010ebe54ef7c69cbab4f2bcf062905e 100644 --- a/test/test_core/test_breakpoint.py +++ b/test/test_core/test_breakpoint.py @@ -5,6 +5,7 @@ from simudator.core.module import Module from simudator.core.modules.memory import Memory from simudator.core.processor import Processor from simudator.core.signal import Signal +from simudator.processor.mia.modules.lc import LC class DummyModule(Module): @@ -184,3 +185,120 @@ def test_lambda_breakpoint_ref_args(): dummy.value = value assert bp.is_break() == func_dummy_val(dummy, value) assert bp.is_break() == (not func_dummy_val(dummy, not value)) + + +def test_add_remove_memory_breakpoint(): + """ + Test that the cpu properly add/removes memory breakpoints. + + Id counter starts at 1 and is only incremented. + """ + cpu = Processor() + in_s = Signal(cpu) + out_s = Signal(cpu) + ctrl_s = Signal(cpu) + adr_s = Signal(cpu) + mem = Memory(in_s, out_s, ctrl_s, adr_s, 8) + cpu.add_module(mem) + + assert len(cpu._breakpoints) == 0 + assert cpu._breakpoint_id_counter == 1 + cpu.add_memory_breakpoint("Memory", 1, 1) + assert cpu._breakpoint_id_counter == 2 + assert len(cpu._breakpoints) == 1 + assert cpu.remove_breakpoint(1) == True + assert cpu._breakpoints == {} + assert len(cpu._breakpoints) == 0 + + +def test_stop_on_memory_breakpoint(): + """ + Test that the cpu properly stops on a memory breakpoint. + """ + cpu = Processor() + in_s = Signal(cpu) + out_s = Signal(cpu) + ctrl_s = Signal(cpu) + adr_s = Signal(cpu) + mem = Memory(in_s, out_s, ctrl_s, adr_s, 8) + cpu.add_module(mem) + assert mem._memory == [0 for _ in range(8)] + assert cpu._clock == 0 + + in_s.update_value(1) + ctrl_s.update_value(True) + adr_s.update_value(2) + + # Add breakpoint if address 3 reaches value 1 + cpu.add_memory_breakpoint("Memory", 2, 1) + cpu.set_enabled_breakpoint(1, True) + cpu.run_continuously() + assert mem._memory == [0, 0, 1, 0, 0, 0, 0, 0] + assert cpu.is_stopped == True + assert cpu._clock == 2 + + mem._memory = [0 for _ in range(8)] + in_s.update_value(1) + ctrl_s.update_value(True) + adr_s.update_value(2) + cpu.set_enabled_breakpoint(1, False) + cpu.do_tick() + assert cpu.is_stopped == False + + +def test_add_remove_state_breakpoint(): + """ + Test that the cpu properly adds/removes state breakpoints. + + Id counter starts at 1 and is only incremented. + """ + cpu = Processor() + s = Signal(cpu) + lc = LC(s, s, s, s) + cpu.add_module(lc) + + assert len(cpu._breakpoints) == 0 + assert cpu._breakpoint_id_counter == 1 + cpu.add_state_breakpoint("LC", "value", 1) + assert len(cpu._breakpoints) == 1 + assert cpu._breakpoint_id_counter == 2 + assert cpu.remove_breakpoint(1) == True + assert cpu._breakpoints == {} + assert len(cpu._breakpoints) == 0 + + +def test_stop_on_state_breakpoint(): + """ + Test that the cpu properly stops on a state breakpoint. + """ + cpu = Processor() + mM_control = Signal(cpu) + + # Sets the behaviour of the LC to decrement by one each tick + mM_control.set_value(1) + + bus_input = Signal(cpu) + l_flag = Signal(cpu) + mM_uADR = Signal(cpu) + lc = LC(mM_control, bus_input, l_flag, mM_uADR) + + lc._value = 10 + cpu.add_module(lc) + + cpu.add_state_breakpoint("LC", "value", 5) + cpu.run_continuously() + assert lc._value == 5 + assert cpu.is_stopped == True + + cpu.add_state_breakpoint("LC", "value", 2) + cpu.run_continuously() + assert lc._value == 2 + assert cpu.is_stopped == True + + # 'Reset' loop counter and turn off previous breakpoint + lc._value = 10 + cpu.set_enabled_breakpoint(1, False) + cpu.set_enabled_breakpoint(2, False) + for _ in range(10): + cpu.do_tick() + assert cpu.is_stopped == False diff --git a/test/test_core/test_reset.py b/test/test_core/test_reset.py new file mode 100644 index 0000000000000000000000000000000000000000..bec0a7312bed9912a65b0bf9dabd3cf1cb5990d3 --- /dev/null +++ b/test/test_core/test_reset.py @@ -0,0 +1,56 @@ +from simudator.core.module import Module +from simudator.core.modules.memory import Memory +from simudator.core.processor import Processor +from simudator.core.signal import Signal +from simudator.processor.mia.modules.lc import LC + + +class DummyModule(Module): + def __init__(self): + self.name = "name" + self.a = 4 + self.b = "a" + self.c = [1, 2, 3] + + def reset(self): + self.name = "dummy" + self.a = 0 + self.b = "" + self.c = [] + + def get_state(self): + return { + "name": self.name, + "a": self.a, + "b": self.b, + "c": self.c, + } + + def output_register(self): + pass + + def update_logic(self): + pass + + +def test_reset(): + cpu = Processor() + cpu._signal_history = ["this", "should", "be", "removed"] + dummy = DummyModule() + cpu._clock = 100 + cpu._removed_cycles = 1337 + cpu._module_history.append(dummy.get_state()) + cpu._signal_history.clear() + cpu._removed_cycles = 0 + cpu._assembly_cycles = [i for i in range(9)] + cpu.add_module(dummy) + cpu.reset() + assert dummy.name == "dummy" + assert dummy.a == 0 + assert dummy.b == "" + assert dummy.c == [] + assert cpu._clock == 0 + assert cpu._removed_cycles == 0 + assert cpu._module_history == [] + assert cpu._signal_history == [] + assert cpu._assembly_cycles == [0] diff --git a/test/test_core/test_save_state.py b/test/test_core/test_save_state.py new file mode 100644 index 0000000000000000000000000000000000000000..16199e77a9b1e2795d0563d55ff97a0c0e046ef7 --- /dev/null +++ b/test/test_core/test_save_state.py @@ -0,0 +1,43 @@ +from unittest.mock import mock_open, patch + +from simudator.core.module import Module +from simudator.core.modules.register import Register +from simudator.core.signal import Signal +from simudator.processor.simple.simple import SIMPLE_CPU + + +def test_module_save_state(): + module = Module({}) + + dummy_file_path = "dummy" + + mock_file = mock_open() + + content = module.name + with patch("builtins.open", mock_file): + module._helper_save_state_to_file(dummy_file_path, content) + mock_file.assert_called_once_with(dummy_file_path, "a") + mock_file().write.assert_called_once_with(content) + + module.name = "aaa" + content = module.name + with patch("builtins.open", mock_file): + module._helper_save_state_to_file(dummy_file_path, content) + mock_file().write.assert_called_with(content) + + +def test_default_register_save_state(): + in_sigal = Signal(None) + register = Register(in_sigal, None) + register.name = "a" + register._value = "a" + + dummy_file_path = "dummy" + + mock_file = mock_open() + + content = register.name + ":\nvalue:: " + str(register._value) + "\n\n" + with patch("builtins.open", mock_file): + register.save_state_to_file(dummy_file_path) + mock_file.assert_called_once_with(dummy_file_path, "a") + mock_file().write.assert_called_once_with(content) diff --git a/test/test_mia/test_alu.py b/test/test_mia/test_alu.py index 09ebf6405f7bf7f32dcd05a932a828b9b3f8289f..c87caf98f60e602ee9905bc5b1451d72d2f2da6e 100644 --- a/test/test_mia/test_alu.py +++ b/test/test_mia/test_alu.py @@ -9,12 +9,11 @@ input_signal_hr = Signal(cpu) control_signal = Signal(cpu) output_signal = Signal(cpu) hr_signal = Signal(cpu) -z_signal = Signal(cpu) -n_signal = Signal(cpu) -o_signal = Signal(cpu) -c_signal = Signal(cpu) +z_signal = Signal(cpu, value=0) +n_signal = Signal(cpu, value=0) +o_signal = Signal(cpu, value=0) +c_signal = Signal(cpu, value=0) alu = ALU( - "alu", input_signal_a, input_signal_b, input_signal_hr, @@ -25,273 +24,413 @@ alu = ALU( n_signal, o_signal, c_signal, + "alu", ) cpu.add_module(alu) -def test_int_binary_converter(): - # test int to binary - assert alu.int_to_bin(0) == [0 for i in range(alu.WORD_LENGTH)] - assert alu.int_to_bin(1) == [0] * (alu.WORD_LENGTH - 1) + [1] - assert alu.int_to_bin(32767) == [0] + [1] * (alu.WORD_LENGTH - 1) - assert alu.int_to_bin(9) == [0] * (alu.WORD_LENGTH - 4) + [1, 0, 0, 1] - - # test binary to int - assert alu.bin_to_int(alu.int_to_bin(15)) == 15 - assert alu.bin_to_int(alu.int_to_bin(0)) == 0 - assert alu.bin_to_int(alu.int_to_bin(2**16)) == 2**16 - 1 - assert alu.bin_to_int(alu.int_to_bin(3576)) == 3576 - assert alu.bin_to_int([0 for i in range(alu.WORD_LENGTH)]) == 0 - assert alu.bin_to_int([0] * (alu.WORD_LENGTH - 1) + [1]) == 1 - assert alu.bin_to_int([1 for i in range(alu.WORD_LENGTH)]) == 2**16 - 1 - assert alu.bin_to_int([0] + [1] * (alu.WORD_LENGTH - 1)) == 32767 - assert alu.bin_to_int([0] * (alu.WORD_LENGTH - 4) + [1, 0, 0, 1]) == 9 - - -def test_ones_complement(): - # test bitwise not - assert alu.ones_complement(0) == 2**16 - 1 - assert alu.ones_complement(1) == 2**16 - 1 - 1 - assert alu.ones_complement(2) == 2**16 - 1 - 2 - assert alu.ones_complement(300) == 2**16 - 1 - 300 - - -def test_negative_bin(): - assert alu.bin_to_int_twocomp(alu.int_to_negative_bin(0)) == 0 - assert alu.bin_to_int_twocomp(alu.int_to_negative_bin(10)) == -10 - assert alu.bin_to_int_twocomp(alu.int_to_negative_bin(1337)) == -1337 - assert alu.bin_to_int_twocomp(alu.int_to_negative_bin(2**15 - 1)) == -( - 2**15 - 1 +def create_test_case(op_ar, op_bus, op_code, res, z=None, n=None, o=None, c=None): + """ + Create a test case for an ALU operation, optionally checking the flags set. + + Parameters + ---------- + op_ar : int + Input value to the ALU from the AR register, i.e. operand A. + op_bus : int + Input value to the ALU from the bus, i.e. operand B. + op_code : int + ALU operation code. + res: int + Result from the ALU operation. + z : int | None + Expected z-flag value after the operation. Set it to ``None`` if the + flag should not be affected by the operation. + n : int | None + Expected n-flag value after the operation. Set it to ``None`` if the + flag should not be affected by the operation. + o : int | None + Expected o-flag value after the operation. Set it to ``None`` if the + flag should not be affected by the operation. + c : int | None + Expected c-flag value after the operation. Set it to ``None`` if the + flag should not be affected by the operation. + + Returns + ------- + tuple + Test case for the ALU. + """ + return op_ar, op_bus, op_code, res, z, n, o, c, None, None + + +def create_hr_test_case(op_ar, op_hr, op_code, ar_res, hr_res, z, n, c): + """ + Create a test case for a 32-bit ALU operation, i.e. one using the values + of the AR and HR registers together. + + The 32-bit ALU operations always affect the z-, n- and c-flag while + not affecting the o-flag. As such, those 3 flags must be provided with + expected values. + + Parameters + ---------- + op_ar : int + Input value to the ALU from the AR register. + op_hr : int + Input value to the ALU from the HR register. + op_code : int + ALU operation code. + ar_res: int + Result to the AR register from the ALU operation. + hr_res: int + Result to the HR register from the ALU operation. + z : int + Expected z-flag value after the operation. + n : int + Expected n-flag value after the operation. + c : int + Expected c-flag value after the operation. + + Returns + ------- + tuple + Test case for the ALU. + """ + return op_ar, 0, op_code, ar_res, z, n, None, c, op_hr, hr_res + + +def check_test_case(test_case): + """ + Assert that the ALU passes a test case for some operation. + + Parameters + ---------- + test_case : tuple + Test case to test the ALU against. + """ + (op_1, op_2, op_code, res, z, n, o, c, hr_in, hr_out) = test_case + input_signal_a.update_value(op_1) + input_signal_b.update_value(op_2) + input_signal_hr.update_value(hr_in) + control_signal.update_value(op_code) + + # Flags set to None in the test case should not be affected by the ALU + # instruction, so save their values before performing the operation + if z is None: + z = alu._z_value + if n is None: + n = alu._n_value + if o is None: + o = alu._o_value + if c is None: + c = alu._c_value + + alu.update_logic() + + assert output_signal.get_value() == res + assert hr_signal.get_value() == hr_out + assert z_signal.get_value() == z + assert n_signal.get_value() == n + assert o_signal.get_value() == o + assert c_signal.get_value() == c + + +def check_test_cases(test_cases): + """ + Assert that the ALU passes some test cases for some operations. + + Note that the ALU is not reset between each test case, meaning that the + order of the test cases may matter depending on how they affect the flags. + + Parameters + ---------- + test_case : list[tuple] + List of test cases to test the ALU against. + """ + for case in test_cases: + check_test_case(case) + + +def test_nop(): + op_code = 0b0000 + # Set the flag values of the ALU and make sure that they are not affected + alu.reset() + alu._z_value = 1 + alu._n_value = 1 + alu._o_value = 1 + alu._c_value = 1 + z_signal.set_value(1) + n_signal.set_value(1) + o_signal.set_value(1) + c_signal.set_value(1) + test_case = create_test_case(10, 5, op_code, None) + check_test_case(test_case) + + +def test_read_bus(): + op_code = 0b0001 + # Set the flag values of the ALU and make sure that they are not affected + alu.reset() + alu._z_value = 1 + alu._n_value = 1 + alu._o_value = 1 + alu._c_value = 1 + test_cases = ( + create_test_case(0x0000, 0x0000, op_code, 0x0000), + create_test_case(0x0000, 0x0174, op_code, 0x0174), + create_test_case(0x0000, 0xFFFFF, op_code, 0xFFFF), # Overflow + create_test_case(0x0AEF, 0x0360, op_code, 0x0360), # Random AR value + ) + check_test_cases(test_cases) + + +def test_bitwise_not(): + op_code = 0b0010 + # Set the flag values of the ALU and make sure that they are not affected + alu.reset() + alu._z_value = 1 + alu._n_value = 1 + alu._o_value = 1 + alu._c_value = 1 + test_cases = ( + # Random values in the AR input which should not affect the result + create_test_case(0x0000, 0x0000, op_code, 0xFFFF), + create_test_case(0x0F38, 0x0004, op_code, 0xFFFB), + create_test_case(0x0000, 0xFFFF, op_code, 0x0000), + create_test_case(0x2E6D, 0x8531, op_code, 0x7ACE), + ) + check_test_cases(test_cases) + + +def test_reset_ar(): + op_code = 0b0011 + # Set the o- and c-flag values of the ALU and make sure that they are + # not affected + alu.reset() + alu._o_value = 1 + alu._c_value = 1 + # (Random values in the AR and bus input which should not affect the result) + test_case = create_test_case(0xFD12, 0x34B7, op_code, 0, 1, 0) + check_test_case(test_case) + + +def test_add(): + op_code = 0b0100 + test_cases = ( + create_test_case(0x0001, 0x0000, op_code, 0x0001, 0, 0, 0, 0), + create_test_case(0x0000, 0x0001, op_code, 0x0001, 0, 0, 0, 0), + # Test all possible flag combinations + create_test_case(0x0004, 0x0008, op_code, 0x000C, 0, 0, 0, 0), + create_test_case(0xC000, 0x4001, op_code, 0x0001, 0, 0, 0, 1), + create_test_case(0x8000, 0x8001, op_code, 0x0001, 0, 0, 1, 1), + create_test_case(0x8000, 0x0001, op_code, 0x8001, 0, 1, 0, 0), + create_test_case(0xC000, 0xC000, op_code, 0x8000, 0, 1, 0, 1), + create_test_case(0x4000, 0x4001, op_code, 0x8001, 0, 1, 1, 0), + create_test_case(0x0000, 0x0000, op_code, 0x0000, 1, 0, 0, 0), + create_test_case(0x0001, 0xFFFF, op_code, 0x0000, 1, 0, 0, 1), + create_test_case(0x8000, 0x8000, op_code, 0x0000, 1, 0, 1, 1), + ) + # Clear flags as they will be affected by the tests + alu.reset() + check_test_cases(test_cases) + + +def test_subtract(): + op_code = 0b0101 + test_cases = ( + # Test all possible flag combinations + create_test_case(0x0001, 0xFFFF, op_code, 0x0002, 0, 0, 0, 0), + create_test_case(0x0001, 0x0000, op_code, 0x0001, 0, 0, 0, 1), + create_test_case(0x0005, 0x0003, op_code, 0x0002, 0, 0, 0, 1), + create_test_case(0x8000, 0x0001, op_code, 0x7FFF, 0, 0, 1, 1), + create_test_case(0x0005, 0x000A, op_code, 0xFFFB, 0, 1, 0, 0), + create_test_case(0xFFFF, 0x0001, op_code, 0xFFFE, 0, 1, 0, 1), + create_test_case(0xA093, 0x0000, op_code, 0xA093, 0, 1, 0, 1), + create_test_case(0x4000, 0xBFFF, op_code, 0x8001, 0, 1, 1, 0), + create_test_case(0x0000, 0x0000, op_code, 0x0000, 1, 0, 0, 1), + create_test_case(0x0032, 0x0032, op_code, 0x0000, 1, 0, 0, 1), + ) + alu.reset() + check_test_cases(test_cases) + + +def test_and(): + op_code = 0b0110 + # Set the o- and c-flag values of the ALU and make sure that they are + # not affected + alu.reset() + alu._o_value = 1 + alu._c_value = 1 + test_cases = ( + create_test_case(0x0F0F, 0xF00F, op_code, 0x000F, 0, 0), + create_test_case(0x80DA, 0xF000, op_code, 0x8000, 0, 1), + create_test_case(0x0000, 0x0000, op_code, 0x0000, 1, 0), + create_test_case(0xFFFF, 0x0000, op_code, 0x0000, 1, 0), + ) + check_test_cases(test_cases) + + +def test_or(): + op_code = 0b0111 + # Set the o- and c-flag values of the ALU and make sure that they are + # not affected + alu.reset() + alu._o_value = 1 + alu._c_value = 1 + test_cases = ( + create_test_case(0x0017, 0x0000, op_code, 0x0017, 0, 0), + create_test_case(0x0000, 0x0385, op_code, 0x0385, 0, 0), + create_test_case(0xAAAA, 0x5555, op_code, 0xFFFF, 0, 1), + create_test_case(0x0000, 0x0000, op_code, 0x0000, 1, 0), + ) + check_test_cases(test_cases) + + +def test_add_no_flags(): + op_code = 0b1000 + # Set the flag values of the ALU and make sure that they are not affected + alu.reset() + alu._z_value = 1 + alu._n_value = 1 + alu._o_value = 1 + alu._c_value = 1 + test_cases = ( + # Test cases that hit all possible flag combinations during normal add + create_test_case(0x0004, 0x0008, op_code, 0x000C), + create_test_case(0xC000, 0x4001, op_code, 0x0001), + create_test_case(0x8000, 0x8001, op_code, 0x0001), + create_test_case(0x8000, 0x0001, op_code, 0x8001), + create_test_case(0xC000, 0xC000, op_code, 0x8000), + create_test_case(0x4000, 0x4001, op_code, 0x8001), + create_test_case(0x0000, 0x0000, op_code, 0x0000), + create_test_case(0x0001, 0xFFFF, op_code, 0x0000), + create_test_case(0x8000, 0x8000, op_code, 0x0000), ) - assert alu.bin_to_int_twocomp(alu.int_to_negative_bin(2**16 - 1)) == 1 - assert alu.bin_to_int_twocomp(alu.int_to_negative_bin(2**16 - 10)) == 10 - assert alu.bin_to_int_twocomp(alu.int_to_negative_bin(2**16 - 4567)) == 4567 - - -def test_add_numbers(): - # test binary sum - assert alu.add_numbers(1, 1, False) == 2 - - assert alu.add_numbers(1, 1, True) == 2 - assert alu.z_value == 0 - assert alu.n_value == 0 - assert alu.o_value == 0 - assert alu.c_value == 0 - - assert alu.add_numbers(14, 20, False) == 34 - - assert alu.add_numbers(0, 0, True) == 0 - assert alu.z_value == 1 - assert alu.n_value == 0 - assert alu.o_value == 0 - assert alu.c_value == 0 - - assert alu.add_numbers(1, 2**16 - 1, True) == 0 - assert alu.z_value == 1 - assert alu.n_value == 0 - assert alu.o_value == 0 - assert alu.c_value == 1 - - -def test_binary_diff(): - assert alu.subtract_numbers(1, 1, True) == 0 - assert alu.z_value == 1 - assert alu.n_value == 0 - assert alu.o_value == 0 - assert alu.c_value == 1 - assert alu.subtract_numbers(0, 1, True) == 65535 - assert alu.z_value == 0 - assert alu.n_value == 1 - assert alu.o_value == 0 - assert alu.c_value == 0 - assert alu.subtract_numbers(0xA100, 0xA010, True) == (0xA100 - 0xA010) - assert alu.z_value == 0 - assert alu.n_value == 0 - assert alu.o_value == 0 - assert alu.c_value == 1 - assert alu.subtract_numbers(0xA010, 0xA100, True) == 0xFFFF + 1 + (0xA010 - 0xA100) - assert alu.z_value == 0 - assert alu.n_value == 1 - assert alu.o_value == 0 - assert alu.c_value == 0 - assert alu.subtract_numbers(30, 29, False) == 1 - assert alu.subtract_numbers(10, 3, False) == 7 - assert alu.subtract_numbers(4, 2, False) == 2 - assert alu.subtract_numbers(500, 2, False) == 498 - assert alu.subtract_numbers(10, 2, False) == 8 - - -def test_and_numbers(): - assert alu.and_numbers(1, 2) == 0 - assert alu.and_numbers(1, 1) == 1 - assert alu.and_numbers(3, 1) == 1 - assert alu.and_numbers(3, 2) == 2 - assert alu.and_numbers(0, 0) == 0 - assert alu.and_numbers(20, 0) == 0 - assert alu.and_numbers(5, 0) == 0 - - -def test_or_numbers(): - assert alu.or_numbers(1, 2) == 3 - assert alu.or_numbers(1, 1) == 1 - assert alu.or_numbers(0, 0) == 0 - assert alu.or_numbers(20, 0) == 20 - - -def test_binary_logical_left_shift(): - assert alu.logical_left_shift(0b01) == 0b10 - assert alu.c_value == 0 - assert alu.logical_left_shift(0b11) == 0b110 - assert alu.c_value == 0 - assert alu.logical_left_shift(0b0) == 0b0 - assert alu.c_value == 0 - assert alu.logical_left_shift(0b1000000000000000) == 0b0 - assert alu.c_value == 1 - - -def test_arithmetic_right_shift(): - assert alu.arithmetic_right_shift(0) == 0 - assert alu.c_value == 0 - assert alu.arithmetic_right_shift(1) == 0 - assert alu.c_value == 1 - assert alu.arithmetic_right_shift(2) == 1 - assert alu.c_value == 0 - assert alu.arithmetic_right_shift(0b1111111111111111) == 0b1111111111111111 - assert alu.c_value == 1 - assert alu.arithmetic_right_shift(0b0111111111111111) == 0b0011111111111111 - assert alu.c_value == 1 - - -def test_logical_right_shift(): - assert alu.logical_right_shift(0) == 0 - assert alu.c_value == 0 - assert alu.logical_right_shift(1) == 0 - assert alu.c_value == 1 - assert alu.logical_right_shift(2) == 1 - assert alu.c_value == 0 + check_test_cases(test_cases) + + +def test_shift_left(): + # 16-bit logical left shift, AR only + op_code = 0b1001 + # Set the o-flag value of the ALU and make sure that it is not affected + alu.reset() + alu._o_value = 1 + test_cases = ( + create_test_case(0x0006, 0x0000, op_code, 0x000C, 0, 0, None, 0), + create_test_case(0x0006, 0xF593, op_code, 0x000C, 0, 0, None, 0), + create_test_case(0x8001, 0x0000, op_code, 0x0002, 0, 0, None, 1), + create_test_case(0x4001, 0x0000, op_code, 0x8002, 0, 1, None, 0), + create_test_case(0xC001, 0x0000, op_code, 0x8002, 0, 1, None, 1), + create_test_case(0x0000, 0x0000, op_code, 0x0000, 1, 0, None, 0), + create_test_case(0x0000, 0x0256, op_code, 0x0000, 1, 0, None, 0), + ) + check_test_cases(test_cases) + + # 32-bit logical left shift, ARHR + op_code = 0b1010 + # Set the o-flag value of the ALU and make sure that it is not affected + alu.reset() + alu._o_value = 1 + test_cases = ( + create_hr_test_case(0x0000, 0x0001, op_code, 0x0000, 0x0002, 0, 0, 0), + create_hr_test_case(0x0001, 0x0000, op_code, 0x0002, 0x0000, 0, 0, 0), + create_hr_test_case(0x0001, 0x0001, op_code, 0x0002, 0x0002, 0, 0, 0), + create_hr_test_case(0x8000, 0x8000, op_code, 0x0001, 0x0000, 0, 0, 1), + create_hr_test_case(0x4000, 0x8000, op_code, 0x8001, 0x0000, 0, 1, 0), + create_hr_test_case(0xC000, 0x8000, op_code, 0x8001, 0x0000, 0, 1, 1), + create_hr_test_case(0x0000, 0x0000, op_code, 0x0000, 0x0000, 1, 0, 0), + create_hr_test_case(0x8000, 0x0000, op_code, 0x0000, 0x0000, 1, 0, 1), + ) + check_test_cases(test_cases) + + +def test_arithmetic_shift_right(): + # 16-bit arithmetic right shift, AR only + op_code = 0b1011 + # Set the o-flag value of the ALU and make sure that it is not affected + alu.reset() + alu._o_value = 1 + test_cases = ( + create_test_case(0x0002, 0x0000, op_code, 0x0001, 0, 0, None, 0), + create_test_case(0x0002, 0x0934, op_code, 0x0001, 0, 0, None, 0), + create_test_case(0x0003, 0x0000, op_code, 0x0001, 0, 0, None, 1), + create_test_case(0x8000, 0x0000, op_code, 0xC000, 0, 1, None, 0), + create_test_case(0x8001, 0x0000, op_code, 0xC000, 0, 1, None, 1), + create_test_case(0x0000, 0x0000, op_code, 0x0000, 1, 0, None, 0), + create_test_case(0x0001, 0x0000, op_code, 0x0000, 1, 0, None, 1), + ) + check_test_cases(test_cases) + + # 32-bit arithmetic right shift, ARHR + op_code = 0b1100 + # Set the o-flag value of the ALU and make sure that it is not affected + alu.reset() + alu._o_value = 1 + test_cases = ( + # Focus on AR part only + create_hr_test_case(0x0002, 0x0000, op_code, 0x0001, 0x0000, 0, 0, 0), + # Focus on HR part only + create_hr_test_case(0x0000, 0x0002, op_code, 0x0000, 0x0001, 0, 0, 0), + # Both AR and HR are non-zero + create_hr_test_case(0x400A, 0x0F06, op_code, 0x2005, 0x0783, 0, 0, 0), + create_hr_test_case(0x000B, 0x0006, op_code, 0x0005, 0x8003, 0, 0, 0), + # Check all possible combinations of flags + create_hr_test_case(0x0003, 0x0000, op_code, 0x0001, 0x8000, 0, 0, 0), + create_hr_test_case(0x0003, 0x0003, op_code, 0x0001, 0x8001, 0, 0, 1), + create_hr_test_case(0x8000, 0x0000, op_code, 0xC000, 0x0000, 0, 1, 0), + create_hr_test_case(0x800A, 0x8006, op_code, 0xC005, 0x4003, 0, 1, 0), + create_hr_test_case(0xC005, 0x0003, op_code, 0xE002, 0x8001, 0, 1, 1), + create_hr_test_case(0x0000, 0x0000, op_code, 0x0000, 0x0000, 1, 0, 0), + create_hr_test_case(0x0000, 0x0001, op_code, 0x0000, 0x0000, 1, 0, 1), + ) + check_test_cases(test_cases) + + +def test_logical_shift_right(): + op_code = 0b1101 + # Set the o-flag value of the ALU and make sure that it is not affected + alu.reset() + alu._o_value = 1 + test_cases = ( + create_test_case(0x0002, 0x0000, op_code, 0x0001, 0, 0, None, 0), + create_test_case(0x8002, 0x9654, op_code, 0x4001, 0, 0, None, 0), + create_test_case(0x0005, 0xA634, op_code, 0x0002, 0, 0, None, 1), + create_test_case(0x8001, 0x0000, op_code, 0x4000, 0, 0, None, 1), + create_test_case(0x0000, 0x45F2, op_code, 0x0000, 1, 0, None, 0), + create_test_case(0x0001, 0x3D9E, op_code, 0x0000, 1, 0, None, 1), + ) + check_test_cases(test_cases) def test_rotate_left(): - assert alu.left_rotate(0) == 0 - assert alu.c_value == 0 - assert alu.left_rotate(1) == 2 - - -def test_signals(): - # Test just out - control_signal.update_value(1) - input_signal_b.update_value(15) - cpu.do_tick() - assert output_signal.get_value() == 15 - - # Test do nothing - control_signal.update_value(0) - cpu.do_tick() - assert output_signal.get_value() == 15 - - # Test sum - control_signal.update_value(4) - input_signal_b.update_value(1) - input_signal_a.update_value(1) - cpu.do_tick() - assert output_signal.get_value() == 2 - - input_signal_b.update_value(5) - input_signal_a.update_value(7) - control_signal.update_value(4) - cpu.do_tick() - assert output_signal.get_value() == 12 - assert z_signal.get_value() == 0 - assert n_signal.get_value() == 0 - assert o_signal.get_value() == 0 - assert c_signal.get_value() == 0 - - # test diff - input_signal_b.update_value(7) - input_signal_a.update_value(5) - control_signal.update_value(5) - cpu.do_tick() - assert output_signal.get_value() == 2**16 - 2 - assert z_signal.get_value() == 0 - assert n_signal.get_value() == 1 - assert o_signal.get_value() == 0 - assert c_signal.get_value() == 0 - - # test diff - input_signal_a.update_value(10) - input_signal_b.update_value(2) - control_signal.update_value(5) - cpu.do_tick() - assert output_signal.get_value() == 8 - assert z_signal.get_value() == 0 - assert n_signal.get_value() == 0 - assert o_signal.get_value() == 0 - assert c_signal.get_value() == 1 - - -def test_hr_signal(): - # test left shift - input_signal_a.update_value(0b1000000000000000) - input_signal_hr.update_value(0b0000000000000000) - control_signal.update_value(0b1010) - cpu.do_tick() - assert output_signal.get_value() == 0 - assert hr_signal.get_value() == 1 - assert z_signal.get_value() == 1 - assert n_signal.get_value() == 0 - assert c_signal.get_value() == 0 - - # test left shift - input_signal_a.update_value(0b1000000000000001) - input_signal_hr.update_value(0b1000000000000010) - control_signal.update_value(0b1010) - cpu.do_tick() - assert output_signal.get_value() == 0b0000000000000010 - assert hr_signal.get_value() == 0b000000000000101 - assert z_signal.get_value() == 0 - assert n_signal.get_value() == 0 - assert c_signal.get_value() == 1 - - # test arithmetic right shift - input_signal_a.update_value(0b1000000000000000) - input_signal_hr.update_value(0b0000000000000000) - control_signal.update_value(0b1100) - cpu.do_tick() - assert output_signal.get_value() == 0b0100000000000000 - assert hr_signal.get_value() == 0b0000000000000000 - assert z_signal.get_value() == 0 - assert n_signal.get_value() == 0 - assert c_signal.get_value() == 0 - - # test arithmetic right shift - input_signal_a.update_value(0b0000000110000001) - input_signal_hr.update_value(0b1000000000000001) - control_signal.update_value(0b1100) - cpu.do_tick() - assert output_signal.get_value() == 0b1000000011000000 - assert hr_signal.get_value() == 0b1100000000000000 - assert z_signal.get_value() == 0 - assert n_signal.get_value() == 1 - assert c_signal.get_value() == 1 - - # test left rotate - input_signal_a.update_value(0b1000000000000000) - input_signal_hr.update_value(0b1000000000000000) - control_signal.update_value(0b1111) - cpu.do_tick() - assert output_signal.get_value() == 0b0000000000000001 - assert hr_signal.get_value() == 0b0000000000000001 - assert z_signal.get_value() == 0 - assert n_signal.get_value() == 0 - assert c_signal.get_value() == 1 - - # test left rotate - input_signal_a.update_value(0b0000000110000001) - input_signal_hr.update_value(0b0100000000000001) - control_signal.update_value(0b1111) - cpu.do_tick() - assert output_signal.get_value() == 0b0000001100000010 - assert hr_signal.get_value() == 0b1000000000000010 - assert z_signal.get_value() == 0 - assert n_signal.get_value() == 0 - assert c_signal.get_value() == 0 + # 16-bit left rotate, AR input only + op_code = 0b1110 + # Set the o-flag value of the ALU and make sure that it is not affected + alu.reset() + alu._o_value = 1 + test_cases = ( + create_test_case(0x0001, 0x0000, op_code, 0x0002, 0, 0, None, 0), + create_test_case(0x0008, 0x2971, op_code, 0x0010, 0, 0, None, 0), + create_test_case(0x8001, 0x0952, op_code, 0x0003, 0, 0, None, 1), + create_test_case(0x5020, 0xAECD, op_code, 0xA040, 0, 1, None, 0), + create_test_case(0xC001, 0xF408, op_code, 0x8003, 0, 1, None, 1), + create_test_case(0x0000, 0x1987, op_code, 0x0000, 1, 0, None, 0), + ) + check_test_cases(test_cases) + + # 32-bit left rotate, ARHR input + op_code = 0b1111 + # Set the o-flag value of the ALU and make sure that it is not affected + alu.reset() + alu._o_value = 1 + test_cases = ( + create_hr_test_case(0x0410, 0x0000, op_code, 0x0820, 0x0000, 0, 0, 0), + create_hr_test_case(0x0000, 0x0410, op_code, 0x0000, 0x0820, 0, 0, 0), + create_hr_test_case(0x0080, 0x8410, op_code, 0x0101, 0x0820, 0, 0, 0), + create_hr_test_case(0xA000, 0x8001, op_code, 0x4001, 0x0003, 0, 0, 1), + create_hr_test_case(0x7001, 0xC001, op_code, 0xE003, 0x8002, 0, 1, 0), + create_hr_test_case(0xE021, 0xC001, op_code, 0xC043, 0x8003, 0, 1, 1), + create_hr_test_case(0x0000, 0x0000, op_code, 0x0000, 0x0000, 1, 0, 0), + ) + check_test_cases(test_cases) diff --git a/test/test_mia/test_alu_arithmetics.py b/test/test_mia/test_alu_arithmetics.py new file mode 100644 index 0000000000000000000000000000000000000000..8ee910d5f41a13d28854b9712073999b034b0be9 --- /dev/null +++ b/test/test_mia/test_alu_arithmetics.py @@ -0,0 +1,240 @@ +from simudator.processor.mia.modules.alu import ALU +from simudator.processor.mia.modules.alu_arithmetics import ( + add_binary_numbers, + arithmetic_right_shift, + bin_to_int, + bin_to_int_twocomp, + binary_and, + binary_or, + bitwise_not, + int_to_bin, + left_rotate, + logical_left_shift, + logical_right_shift, +) + + +def create_unary_test_case(op, res, carry=None): + """ + Create a test case for a unary arithmetic function. + + Parameters + ---------- + op : int + Operand to the arithmetic function. + res : int + Expected result from the arithmetic function. + carry : int + Expected carry out from the function if any. Set it to ``None`` to + signify that the function does not return a carry out bit. + + Returns + ------- + tuple[int, int, int] + Test case for a unary arithmetic function. + """ + return op, res, carry + + +def check_unary_test(test_case, fun): + """ + Assert that a unary arithmetic function passes a test case. + + Parameters + ---------- + test_case : tuple[int, int, int] + Test case for the unary arithmetic function. + fun : callable + Unary arithmetic function to test. Should operate on binary numbers + represented as lists of bits. + ``fun(list[int]) -> list[int] | tuple[list[int], int]`` + """ + (op, res, carry) = test_case + bin_op = int_to_bin(op, ALU.WORD_LENGTH) + bin_res = int_to_bin(res, ALU.WORD_LENGTH) + if carry is None: + assert fun(bin_op) == bin_res + else: + assert fun(bin_op) == (bin_res, carry) + + +def check_unary_tests(test_cases, fun): + """ + Assert that a unary arithmetic function passes some test cases. + + Parameters + ---------- + test_cases : list[tuple[int, int, int]] + List of test cases for the unary arithmetic function. + fun : callable + Unary arithmetic function to test. Should operate on binary numbers + represented as lists of bits. + ``fun(list[int]) -> list[int] | tuple[list[int], int]`` + """ + for case in test_cases: + check_unary_test(case, fun) + + +def create_binary_test_case(op_1, op_2, res, carry=None): + """ + Create a test case for a binary arithmetic function. + + Parameters + ---------- + op_1 : int + First operand to the arithmetic function. + op_2 : int + Second operand to the arithmetic function. + res : int + Expected result from the arithmetic function. + carry : int + Expected carry out from the function if any. Set it to ``None`` to + signify that the function does not return a carry out bit. + + Returns + ------- + tuple[int, int, int, int] + Test case for a binary arithmetic function. + """ + return op_1, op_2, res, carry + + +def check_binary_test(test_case, fun): + """ + Assert that a binary arithmetic function passes a test case. + + Parameters + ---------- + test_case : tuple[int, int, int, int] + Test case for the binary arithmetic function. + fun : callable + Binary arithmetic function to test. Should operate on binary numbers + represented as lists of bits. + ``fun(list[int], list[int]) -> list[int] | tuple[list[int], int]`` + """ + (op_1, op_2, res, carry) = test_case + bin_op_1 = int_to_bin(op_1, ALU.WORD_LENGTH) + bin_op_2 = int_to_bin(op_2, ALU.WORD_LENGTH) + bin_res = int_to_bin(res, ALU.WORD_LENGTH) + if carry is None: + assert fun(bin_op_1, bin_op_2) == bin_res + else: + assert fun(bin_op_1, bin_op_2) == (bin_res, carry) + + +def check_binary_tests(test_cases, fun): + """ + Assert that a binary arithmetic function passes some test cases. + + Parameters + ---------- + test_case : list[tuple[int, int, int, int]] + List of test cases for the binary arithmetic function. + fun : callable + Binary arithmetic function to test. Should operate on binary numbers + represented as lists of bits. + ``fun(list[int], list[int]) -> list[int] | tuple[list[int], int]`` + """ + for case in test_cases: + check_binary_test(case, fun) + + +def test_int_binary_converter(): + # test int to binary + word_length = ALU.WORD_LENGTH + assert int_to_bin(0, word_length) == [0 for _ in range(word_length)] + assert int_to_bin(1, word_length) == [0] * (word_length - 1) + [1] + assert int_to_bin(32767, word_length) == [0] + [1] * (word_length - 1) + assert int_to_bin(9, word_length) == [0] * (word_length - 4) + [1, 0, 0, 1] + + # test binary to int + assert bin_to_int(int_to_bin(15, word_length)) == 15 + assert bin_to_int(int_to_bin(0, word_length)) == 0 + assert bin_to_int(int_to_bin(2**16, word_length)) == 2**16 - 1 + assert bin_to_int(int_to_bin(3576, word_length)) == 3576 + assert bin_to_int([0 for _ in range(word_length)]) == 0 + assert bin_to_int([0] * (word_length - 1) + [1]) == 1 + assert bin_to_int([1 for _ in range(word_length)]) == 2**16 - 1 + assert bin_to_int([0] + [1] * (word_length - 1)) == 32767 + assert bin_to_int([0] * (word_length - 4) + [1, 0, 0, 1]) == 9 + + +def test_bitwise_not(): + test_cases = ( + create_unary_test_case(0, 2**16 - 1), + create_unary_test_case(1, 2**16 - 1 - 1), + create_unary_test_case(2, 2**16 - 1 - 2), + create_unary_test_case(300, 2**16 - 1 - 300), + ) + check_unary_tests(test_cases, bitwise_not) + + +def test_add(): + test_cases = ( + create_binary_test_case(0b01, 0b10, 0b11, 0), + create_binary_test_case(0b00101, 0b10101, 0b11010, 0), + create_binary_test_case(0, 0, 0, 0), + create_binary_test_case(1, 2**16 - 1, 0, 1), + ) + check_binary_tests(test_cases, add_binary_numbers) + + +def test_and(): + test_cases = ( + create_binary_test_case(0b01, 0b10, 0b00), + create_binary_test_case(0b1, 0b1, 0b1), + create_binary_test_case(0b11111, 0b00000, 0b00000), + create_binary_test_case(0b11111, 0b10101, 0b10101), + ) + check_binary_tests(test_cases, binary_and) + + +def test_or(): + test_cases = ( + create_binary_test_case(0b01, 0b10, 0b11), + create_binary_test_case(0b000, 0b000, 0b00), + create_binary_test_case(0b11111, 0b00000, 0b11111), + create_binary_test_case(0b01010, 0b10101, 0b11111), + create_binary_test_case(0b01110, 0b10001, 0b11111), + ) + check_binary_tests(test_cases, binary_or) + + +def test_logical_left_shift(): + test_cases = ( + create_unary_test_case(0b01, 0b10, 0), + create_unary_test_case(0b11, 0b110, 0), + create_unary_test_case(0b0, 0b0, 0), + create_unary_test_case(0b1000000000000000, 0b0, 1), + ) + check_unary_tests(test_cases, logical_left_shift) + + +def test_arithmetic_right_shift(): + test_cases = ( + create_unary_test_case(0b0, 0b0, 0), + create_unary_test_case(0b1, 0b0, 1), + create_unary_test_case(0b10, 0b1, 0), + create_unary_test_case(0b1111111111111111, 0b1111111111111111, 1), + create_unary_test_case(0b0111111111111110, 0b0011111111111111, 0), + ) + check_unary_tests(test_cases, arithmetic_right_shift) + + +def test_logical_right_shift(): + test_cases = ( + create_unary_test_case(0b0, 0b0, 0), + create_unary_test_case(0b1, 0b0, 1), + create_unary_test_case(0b10, 0b01, 0), + create_unary_test_case(0b1101, 0b0110, 1), + ) + check_unary_tests(test_cases, logical_right_shift) + + +def test_rotate_left(): + test_cases = ( + create_unary_test_case(0b1000000000000000, 0b0000000000000001, 1), + create_unary_test_case(0b0, 0b0, 0), + create_unary_test_case(0b01, 0b10, 0), + ) + check_unary_tests(test_cases, left_rotate) diff --git a/test/test_mia/test_ir.py b/test/test_mia/test_ir.py index 113a19c1cd885e960b6ea3641626136e764a0e7d..ef4a45f2f8611106f18cd71c540ee456475f0f3a 100644 --- a/test/test_mia/test_ir.py +++ b/test/test_mia/test_ir.py @@ -1,3 +1,5 @@ +from unittest.mock import mock_open, patch + from simudator.core.processor import Processor from simudator.core.signal import Signal from simudator.processor.mia.modules.ir import IR @@ -24,3 +26,27 @@ def test_ir(): assert m_s.get_value() == (instruction >> 8) & 0b11 assert grx_s.get_value() == (instruction >> 10) & 0b11 assert op_s.get_value() == (instruction >> 12) & 0b1111 + + +def test_save_state(): + cpu = Processor() + s = Signal(cpu) + ir = IR( + s, + s, + s, + s, + s, + s, + ) + + dummy_file_path = "dummy" + + mock_file = mock_open() + + content = ir.name + ":\n" + content += "Instruction: " + hex(ir.instruction)[2:] + "\n\n" + with patch("builtins.open", mock_file): + ir.save_state_to_file(dummy_file_path) + mock_file.assert_called_once_with(dummy_file_path, "a") + mock_file().write.assert_called_once_with(content) diff --git a/test/test_mia/test_lc.py b/test/test_mia/test_lc.py index f0877605fbe8436aaf81888c566f3f2e259bb5c6..56714f5ce12021172adc146e70f34d20cedab6c7 100644 --- a/test/test_mia/test_lc.py +++ b/test/test_mia/test_lc.py @@ -1,3 +1,5 @@ +from unittest.mock import mock_open, patch + from simudator.core.processor import Processor from simudator.core.signal import Signal from simudator.processor.mia.modules.lc import LC @@ -212,3 +214,20 @@ def test_get_state(): state = lc.get_state() assert state["name"] == "LC" assert state["value"] == 10 + + +def test_save_state(): + cpu = Processor() + s = Signal(cpu) + lc = LC(s, s, s, s) + + dummy_file_path = "dummy" + + mock_file = mock_open() + + content = lc.name + ":\n" + content += "value: " + hex(lc._value)[2:] + "\n\n" + with patch("builtins.open", mock_file): + lc.save_state_to_file(dummy_file_path) + mock_file.assert_called_once_with(dummy_file_path, "a") + mock_file().write.assert_called_once_with(content) diff --git a/test/test_mia/test_mia_grx.py b/test/test_mia/test_mia_grx.py index 7740e2a673c800c3264a2fd7303d0ee75de70cae..69e9d41009dd83821bc6a68adc6ebd2963c509bc 100644 --- a/test/test_mia/test_mia_grx.py +++ b/test/test_mia/test_mia_grx.py @@ -1,6 +1,7 @@ from simudator.core.processor import Processor from simudator.core.signal import Signal from simudator.processor.mia.modules.mia_grx import GRX +from unittest.mock import patch, mock_open def test_default_name(): @@ -145,3 +146,24 @@ def test_set_value_from_bus(): error = True assert error is True + +def test_save_state(): + cpu = Processor() + s = Signal(cpu) + grx = GRX(s, s, s, s, s, s, 1) + + dummy_file_path = "dummy" + + mock_file = mock_open() + + content = grx.name + ":\n" + for index, value in enumerate(grx.registers): + content += str(hex(index)[2:]) + ": " + content += str(hex(value)[2:]) + content += "\n" + + content += "\n" + with patch("builtins.open", mock_file): + grx.save_state_to_file(dummy_file_path) + mock_file.assert_called_once_with(dummy_file_path, "a") + mock_file().write.assert_called_once_with(content) diff --git a/test/test_mia/test_micro_memory.py b/test/test_mia/test_micro_memory.py index efa544c6c1028b8c19aa757a43c783eed365f896..8cfe4206ff751966dfe54cf561dbdd775ebce165 100644 --- a/test/test_mia/test_micro_memory.py +++ b/test/test_mia/test_micro_memory.py @@ -1,6 +1,7 @@ from simudator.core.processor import Processor from simudator.core.signal import Signal from simudator.processor.mia.modules.micro_memory import MicroMemory +from unittest.mock import patch, mock_open def test_alu_field(): @@ -393,3 +394,26 @@ def test_seq_field(): # uPC should be reset to 0, corresponding to control signal 011 assert upc_control_s.get_value() == 0b011 # TODO: Test for some sort of HALT signal + +def test_save_state(): + cpu = Processor() + s = Signal(cpu) + uM = MicroMemory(s,s,s,s,s,s,s,s,s,s,s,s,s,s,s) + + dummy_file_path = "dummy" + + mock_file = mock_open() + + content = uM.name + ":\n" + for index, value in enumerate(uM.memory): + content += ( + str(hex(index)[2:].rjust(uM.MEMORY_ADDRESS_PADDING, "0")) + ": " + ) + content += str(hex(value)[2:].rjust(uM.MEMORY_VALUE_PADDING, "0")) + content += "\n" + + content += "\n" + with patch("builtins.open", mock_file): + uM.save_state_to_file(dummy_file_path) + mock_file.assert_called_once_with(dummy_file_path, "a") + mock_file().write.assert_called_once_with(content) diff --git a/test/test_mia/test_micro_pc.py b/test/test_mia/test_micro_pc.py index 1772b552331e538153f4170570d8c7b4a23320b7..91073e409c9da4566227df1ac29c45e329e21153 100644 --- a/test/test_mia/test_micro_pc.py +++ b/test/test_mia/test_micro_pc.py @@ -1,6 +1,7 @@ from simudator.core.processor import Processor from simudator.core.signal import Signal from simudator.processor.mia.modules.micro_pc import MicroPC +from unittest.mock import patch, mock_open def test_upc(): @@ -69,3 +70,19 @@ def test_upc(): assert upc.value == f_um.get_value() assert t_supc.get_value() == temp assert t_um.get_value() == upc.value + +def test_save_state(): + cpu = Processor() + s = Signal(cpu) + upc = MicroPC(s,s,s,s,s,s,s) + + dummy_file_path = "dummy" + + mock_file = mock_open() + + content = upc.name + ":\n" + content += "value: " + hex(upc.value)[2:] + "\n\n" + with patch("builtins.open", mock_file): + upc.save_state_to_file(dummy_file_path) + mock_file.assert_called_once_with(dummy_file_path, "a") + mock_file().write.assert_called_once_with(content) diff --git a/test/test_mia/test_pc.py b/test/test_mia/test_pc.py index 48d216cfd83ef37ae7cfea4a73efd9e54d3f06a3..5c417e37f034da909c8aa5031851ef3b85fe5073 100644 --- a/test/test_mia/test_pc.py +++ b/test/test_mia/test_pc.py @@ -1,6 +1,7 @@ from simudator.core.processor import Processor from simudator.core.signal import Signal from simudator.processor.mia.modules.pc import PC +from unittest.mock import patch, mock_open def test_default_name(): @@ -198,3 +199,19 @@ def test_set_state(): assert pc.name == "pc" assert pc.value == 23 assert pc.increase_by_one is True + +def test_save_state(): + cpu = Processor() + s = Signal(cpu) + pc = PC(s,s,s,s,0) + + dummy_file_path = "dummy" + + mock_file = mock_open() + + content = pc.name + ":\n" + content += "value: " + hex(pc.value)[2:] + "\n\n" + with patch("builtins.open", mock_file): + pc.save_state_to_file(dummy_file_path) + mock_file.assert_called_once_with(dummy_file_path, "a") + mock_file().write.assert_called_once_with(content)