diff --git a/src/simudator/cli/cli.py b/src/simudator/cli/cli.py index a375743567c885d6d9f284ab2d6413db0828b4b2..8cece34ab1733200d85c52e342092bc5f697c8fc 100644 --- a/src/simudator/cli/cli.py +++ b/src/simudator/cli/cli.py @@ -6,16 +6,16 @@ from os.path import exists, isdir from simudator.core.processor import Processor HELP_TEXT = """Here is a list of possible commands: -CLI control: +CLI control: - h, help : Shows this message. - q, quit : Closes the program. Nothing will be saved. -Simulation control: +Simulation control: - n [x], next [x]: Simulates x ticks. If no x is given, simulates one tick. - - rc, run_continuously: + - rc, run_continuously: Continuously simulate ticks until a HALT is signalled or a breakpoint is reached. @@ -30,72 +30,78 @@ State information: - p, print: Prints all states of all modules. - pl, printlist [add/remove] [module name] ...: - Add or remove the given modules to a list of modules to print. If no + Add or remove the given modules to a list of modules to print. If no arguments are given, i.e. "pl" or "printlist" on their own, all the modules in the list will be printed. - - pp, pretty print: Prints relevant processor state information in a + - pp, pretty print: Prints relevant processor state information in a readable and compact format. - - ppv, pretty print verbose: Prints all processor state information in a + - ppv, pretty print verbose: Prints all processor state information in a readable and compact format. -Breakpoints: +Breakpoints: - p br, print breaks: Prints all breakpoints. - - p br lam, print break lambdas: - Prints all available lambdas of this processor to use for lambda - breakpoints. + - p br lam, print break lambdas: + Prints all available lambdas of this processor to use for lambda + breakpoints. - br state, break state [module name] [state name] [value]: - Add a breakpoint for the module with the given name. The breakpoint brakes + Add a breakpoint for the module with the given name. The breakpoint brakes when the specified state of the module has the specified value. - - br mem, break memory [module name] [adress] [value]: - Add a breakpoint for a memory module with the given name. The breakpoint - breaks when the specified adress of the memory has the specified value. + - br mem, break memory [module name] [address] [value]: + Add a breakpoint for a memory module with the given name. The breakpoint + breaks when the specified address of the memory has the specified value. - br lam, break lambda [lambda_name] [keyword argument] ...: - Add a breakpoint that uses the referenced lambda to evaluate when to - break. Any module names in the keyword arguments will be substituted with + Add a breakpoint that uses the referenced lambda to evaluate when to + break. Any module names in the keyword arguments will be substituted with the actual module. Strings must be enclosed with quotation marks. - rm br [id], remove break [id]: Remove the breakpoint with the given ID. -File I/O: +File I/O: - l, load [path]: Loads a processors state from a given file path. - sf, save to file [path]: - Saves the processor state to the specified file. + Saves the processor state to the specified file. """ class CLI: """ Terminal based program for controlling a processor. + Mostly used for testing and debugging. + + Parameters + ---------- + processor : Processor """ def __init__(self, processor: Processor): - self.running = True - self.processor = processor - self.print_list = [] + self._running = True + self._processor = processor + self._print_list = [] def run(self) -> None: - """ - Runs the CLI. - Will put the user in an input loop where they can send commands to the program + """Run the CLI. + + Put the user in an input loop where they can send commands to the + program. """ print("Welcome to simuDAtor!") - while self.running: + while self._running: # Get user commands here user_input = input("> ") - # Note: Keep the order of the cases consistent with the help text + # Note: Keep the order of the cases consistent with the help text # for convenience match user_input.split(): # CLI control ------------------------------------------------- @@ -106,7 +112,7 @@ class CLI: case ["q"] | ["quit"]: # Ends the loop print("Goodbye") - self.running = False + self._running = False # Simulation control ------------------------------------------ case ["n"] | ["next"]: @@ -114,7 +120,7 @@ class CLI: # this could take time for larger simulations, # could be good to add loading icon and stop user input - self.processor.do_tick() + self._processor.do_tick() case ["n", _] | ["next", _]: # runs a given number of ticks @@ -123,32 +129,32 @@ class CLI: try: for _ in range(int(user_input.split()[1])): - self.processor.do_tick() - if self.processor.is_stopped: + self._processor.do_tick() + if self._processor.is_stopped: break - if self.processor.breakpoint_reached: - bp = self.processor.last_breakpoint + if self._processor.breakpoint_reached: + bp = self._processor.last_breakpoint print(f"Reached breakpoint: {bp}") - if self.processor.should_halt(): + if self._processor.should_halt(): print("The processor halted") except ValueError: print("Invalid value") case ["rc"] | ["run_continuously"]: - self.processor.run_continuously() - if self.processor.breakpoint_reached: - bp = self.processor.last_breakpoint + self._processor.run_continuously() + if self._processor.breakpoint_reached: + bp = self._processor.last_breakpoint print(f"Reached breakpoint: {bp}") - if self.processor.should_halt(): + if self._processor.should_halt(): print("The processor halted") case ["u"] | ["undo"]: # undo one clock cycles - current_cycle = self.processor.get_clock() + current_cycle = self._processor.get_clock() try: - self.processor.load_cycle(max(current_cycle - 1, 0)) + self._processor.load_cycle(max(current_cycle - 1, 0)) except IndexError: print("Index out of range") @@ -162,46 +168,46 @@ class CLI: print("Invalid value") continue - current_cycle = self.processor.get_clock() + current_cycle = self._processor.get_clock() try: - self.processor.load_cycle(max(current_cycle - number, 0)) + self._processor.load_cycle(max(current_cycle - number, 0)) except IndexError: print("Index out of range") case ["r"] | ["resets"]: # self.processor.load_cycle(0) - self.processor.reset() + self._processor.reset() # State information ------------------------------------------- case ["c"] | ["clock"]: # shows current clockcycle - print("Clock is at: ", str(self.processor.get_clock())) + print("Clock is at: ", str(self._processor.get_clock())) case ["p"] | ["print"]: print("\n") # prints all module states, is most prbably hard to read - for module in self.processor.get_modules(): + for module in self._processor.get_modules(): module.print_module() print("\n") case ["pl", *_] | ["printlist", *_]: - self.print_list_command(user_input.split()) + self._print_list_command(user_input.split()) case ["pp"] | ["pretty print"]: # pretty print the modules of the processor - self.processor.pretty_print() + self._processor.pretty_print() case ["ppv"] | ["pretty print verbose"]: # pretty print the modules of the processor # with all available information - self.processor.pretty_print_verbose() + self._processor.pretty_print_verbose() # Breakpoints ------------------------------------------------- case ["p", "br"] | ["print", "breaks"]: - self.processor.print_breakpoints() + self._processor.print_breakpoints() case ["p", "br", "lam"] | ["print", "break", "lambdas"]: - lambdas = self.processor.get_breakpoint_lambdas() + lambdas = self._processor.get_breakpoint_lambdas() if not lambdas: print("There are no lambdas for this processor.") else: @@ -212,7 +218,7 @@ class CLI: state_name = user_input.split()[3] val = ast.literal_eval(user_input.split()[4]) try: - self.processor.add_state_breakpoint( + self._processor.add_state_breakpoint( module_name, state_name, val ) except ValueError as e: @@ -221,9 +227,11 @@ class CLI: case ["br", "mem", _, _, _] | ["break", "memory", _, _, _]: module_name = user_input.split()[2] try: - adress = ast.literal_eval(user_input.split()[3]) + address = ast.literal_eval(user_input.split()[3]) value = ast.literal_eval(user_input.split()[4]) - self.processor.add_memory_breakpoint(module_name, adress, value) + self._processor.add_memory_breakpoint( + module_name, address, value + ) except ValueError as e: print(e) @@ -235,14 +243,14 @@ class CLI: key, value = kwarg.split('=') kwargs[key] = ast.literal_eval(value) try: - self.processor.add_lambda_breakpoint(lambda_name, **kwargs) + self._processor.add_lambda_breakpoint(lambda_name, **kwargs) except ValueError as e: print(e) case ["rm", "br", _] | ["remove", "break", _]: try: bp_id = int(user_input.split()[2]) - if not self.processor.remove_breakpoint(bp_id): + if not self._processor.remove_breakpoint(bp_id): print(f"No breakpoint with ID {bp_id}") except ValueError: print("Breakpoint ID must be an integer") @@ -251,36 +259,38 @@ class CLI: case ["l", _] | ["load", _]: # load processor state from file try: - self.processor.reset() - self.processor.load_state_from_file(user_input.split()[1]) + self._processor.reset() + self._processor.load_state_from_file(user_input.split()[1]) except FileNotFoundError: print("No file called ", user_input.split()[1], " was found") case ["sf"] | ["save to file"]: file_path = "cpu_save_file" - self.processor.save_state_to_file(file_path) + self._processor.save_state_to_file(file_path) case ["sf", _] | ["save to file", _]: file_path = user_input.split()[1] - self.check_file_path(file_path) + self._save_to_file(file_path) # ------------------------------------------------------------- case _: # message for unknown command print("Unknown command. Write \"help\" for assistance.") - def check_file_path(self, file_path) -> None: - """ - Checks the file path user gave as input. If it is a directory - this funciton will simply return and not write anything. If it - is a file that already exists it will demand a 'yes' or 'no' from - user to decide if it should write over the existing file. - If the given file does not exist it will simply create the file. + def _save_to_file(self, file_path: str) -> None: + """Save the processor state to the given file path. + + No save is made if the file path is a directory. If the file already + exists, the user is given the choice to overwrite the file. + + Parameters + ---------- + file_path : str """ # Given file does not exist, save_state_to_file will create it if not exists(file_path): - self.processor.save_state_to_file(file_path) + self._processor.save_state_to_file(file_path) else: # We dont touch directories @@ -299,17 +309,27 @@ class CLI: print("Please give an input of 'yes' or 'no'!") if overwrite == "yes": - self.processor.save_state_to_file(file_path) - return - else: - return - - def print_list_command(self, user_input): + self._processor.save_state_to_file(file_path) + + def _print_list_command(self, user_input: list[str]) -> None: + """Add/remove modules to internal list or print all modules in the + internal list. + + Parameters + ---------- + user_input : list[str] + A print list command split by spaces. Should be on the format + ``["printlist", "add", MODULE_NAME, ...]`` or + ``["printlist", "remove", MODULE_NAME, ...]`` to add or remove + modules, where MODULE_NAME is the name of a module as a string, + or ``["printlist"]`` to print all added modules. + (``"printlist"`` can be substituted with ``"pl"``) + """ # no additional commands so only print if len(user_input) == 1: print("\n") - for name in self.print_list: - self.processor.get_module(name).print_module() + for name in self._print_list: + self._processor.get_module(name).print_module() print("\n") return @@ -319,17 +339,19 @@ class CLI: for name in names: # check if name exists try: - self.processor.get_module(name) - if name not in self.print_list: - self.print_list.append(name) + self._processor.get_module(name) + if name not in self._print_list: + self._print_list.append(name) except KeyError: - print("No module named", name,) - + print( + "No module named", + name, + ) elif user_input[1] == "remove": for name in names: - if name in self.print_list: - self.print_list.remove(name) + if name in self._print_list: + self._print_list.remove(name) else: print(name, "not in print list") else: diff --git a/src/simudator/core/breakpoint.py b/src/simudator/core/breakpoint.py index 0727aba719f10c7dfe7786f231d0e3e91cf8fb14..4bf78fb64a631ef3a0fb562c72113305f1329bc0 100644 --- a/src/simudator/core/breakpoint.py +++ b/src/simudator/core/breakpoint.py @@ -1,12 +1,34 @@ class Breakpoint: - __slots__ = "is_enabled" + __slots__ = "_is_enabled" + """ + Base class for beakpoints, intended to be subclassed. + + Attributes + ---------- + is_enabled : bool + Used to check and toggle if a breakpoint is on or off. + """ def __init__(self): self.is_enabled = True def is_break(self) -> bool: + """Check if the breakpoint has been reached. + + Returns + ------- + bool + ``True`` if the breakpoint has been reached, ``False`` otherwise. + """ return False def set_enabled(self, is_enabled: bool) -> None: + """Toggle the breakpoint on or off. + + Parameters + ---------- + is_enabled : bool + ``True`` to toggle on, ``False`` to toggle off. + """ self.is_enabled = is_enabled diff --git a/src/simudator/core/breakpoint_lambda.py b/src/simudator/core/breakpoint_lambda.py index 28c1bf8204169bfb6f33946fb90e18e0a81999c6..52bff4bb3f88d631197bbc45cfd08f45f977c093 100644 --- a/src/simudator/core/breakpoint_lambda.py +++ b/src/simudator/core/breakpoint_lambda.py @@ -5,16 +5,31 @@ from simudator.core.breakpoint import Breakpoint class LambdaBreakpoint(Breakpoint): - __slots__ = ("lambda_func", "kwargs") + __slots__ = ("_lambda_func", "_kwargs") + """ + A breakpoint that uses a given function to decide whether the breakpoint + has been reached or not. + + Parameters + ---------- + lambda_func : Callable + The function used to decide if the breakpoint has been reached. + + ``lambda_func(**kwargs) -> bool`` + Should return ``True`` if the breakpoint has been reached, ``False`` + otherwise. + **kwargs + Arguments to pass to `lambda_func`. + """ def __init__(self, lambda_func: Callable[..., bool], **kwargs) -> None: super().__init__() - self.lambda_func = lambda_func - self.kwargs = kwargs + self._lambda_func = lambda_func + self._kwargs = kwargs def is_break(self): - return self.lambda_func(**self.kwargs) + return self._lambda_func(**self._kwargs) def __str__(self) -> str: - s = f"Lambda {self.lambda_func.__name__} with kwargs {self.kwargs}" + s = f"Lambda {self._lambda_func.__name__} with kwargs {self._kwargs}" return s diff --git a/src/simudator/core/breakpoint_memory.py b/src/simudator/core/breakpoint_memory.py index 8a0af7b8ee2a7b7eccb5865f41af05393fa09b56..8dbd66348eea3a2da753a1c1402bde9f638301e9 100644 --- a/src/simudator/core/breakpoint_memory.py +++ b/src/simudator/core/breakpoint_memory.py @@ -6,17 +6,31 @@ from simudator.core.modules import Memory class MemoryBreakpoint(Breakpoint): - __slots__ = ("memory", "address", "value") + __slots__ = ("_memory", "_address", "_value") + + """ + A breakpoint class for memory modules to break when a certain address + of the memory has a certain value. + + Parameters + ---------- + memory : Memory + Memory module to check the contents of. + address : int + Address of the memory module to break at. + value : Any + Value of the address that triggers the breakpoint. + """ def __init__(self, memory: Memory, address: int, value: Any) -> None: super().__init__() - self.memory = memory - self.address = address - self.value = value + self._memory = memory + self._address = address + self._value = value def is_break(self): - memory_content = self.memory.get_state()["memory"] - return memory_content[self.address] == self.value + memory_content = self._memory.get_state()["memory"] + return memory_content[self._address] == self._value def __str__(self) -> str: - return f"{self.memory.name} {self.address}: {self.value}" + return f"{self._memory.name} {self._address}: {self._value}" diff --git a/src/simudator/core/breakpoint_state.py b/src/simudator/core/breakpoint_state.py index d32d9daf980164cebc07590f64ef5a17fed1e73a..58b042cbabf727bfb5ec1f523617592c0035595c 100644 --- a/src/simudator/core/breakpoint_state.py +++ b/src/simudator/core/breakpoint_state.py @@ -6,16 +6,29 @@ from simudator.core.module import Module class StateBreakpoint(Breakpoint): - __slots__ = ("module", "state", "value") + __slots__ = ("_module", "_state", "_value") + """ + A breakpoint class to break when the state variable for a module reaches + a specified value. + + Parameters + ---------- + module : Module + Module to check the state of. + state : str + Name of the state variable of the module to check the value of. + value : Any + Value of the state variable to break at. + """ def __init__(self, module: Module, state: str, value: Any) -> None: super().__init__() - self.module = module - self.state = state - self.value = value + self._module = module + self._state = state + self._value = value def is_break(self): - return self.module.get_state()[self.state] == self.value + return self._module.get_state()[self._state] == self._value def __str__(self) -> str: - return self.module.name + " " + self.state + " == " + str(self.value) + return self._module.name + " " + self._state + " == " + str(self._value) diff --git a/src/simudator/core/module.py b/src/simudator/core/module.py index f0869bedd6b28ec80fb5af60663c9a504355bcc2..6ae3b787de0d85be3457f5d7d9db3042e1b993df 100644 --- a/src/simudator/core/module.py +++ b/src/simudator/core/module.py @@ -8,6 +8,24 @@ class Module: This class specifies the basic functionality of all module types. The modules are processor components in the processor, such as ALU, busses, registers and more. + + Parameters + ---------- + signals : dict[str, Signal] + Dictionary containing all signals connected to the module. Input + signals should have keys beginning with 'in\_' and output signals keys + beginning with 'out\_'. + name : str + Name of the module. + + Attributes + ---------- + name : str + Name of the module. + signals : dict[str, Signal] + The signals connected to this module. The key for a signal can be + regarded as the name of a signal 'port' of the module. The keys should + be prefixed with 'in\_' for input signals and 'out\_' for output signals. """ __slots__ = ("name", "signals") @@ -21,29 +39,38 @@ class Module: signal.add_destination(self) def update_register(self) -> None: + """Simulate module behaviour for saving input into the internal state + during a clock tick. + + This method should be used for any module that has any 'internal' + registers. Exact behaviour is specified in each module subclass. """ - Simulates module behaviour for saving input into the internal state - during a clock tick. Exact behaviour is specified in each module type. - """ - raise NotImplemented + raise NotImplementedError def output_register(self) -> None: + """Simulate module behaviour for outputting data from internal + registers, if any, to output signals during a clock tick. + + Exact behaviour is specified in each module subclass. """ - Simulates module behaviour for giving data to output signals - during a clock tick. Exact behaviour is specified in each module type. - """ - raise NotImplemented + raise NotImplementedError def update_logic(self) -> None: """ - Simulates behaviour for sending output using the internal state + Simulate behaviour for sending output using the internal state during a clock tick. Exact behaviour is specified in each module type. """ - raise NotImplemented + raise NotImplementedError def get_state(self) -> dict: """ - Returns a dict of the module states. + Return the state of the module. + + Returns + ------- + dict[str, Any] + State of the module represented as a dictionary with one key for + each state variable. """ state_dict = dict() state_dict["name"] = self.name @@ -51,7 +78,12 @@ class Module: def get_gui_state(self) -> dict: """ - Returns a dict of the module states that should be displayed in a GUI. + Return the state of the module as should be displayed in a GUI. + + See Also + -------- + get_state : + Similar method that includes state variables not related to the GUI. """ state_dict = dict() state_dict["name"] = self.name @@ -59,42 +91,56 @@ class Module: def set_state(self, state: dict) -> None: """ - Sets the modules state to one given in dict. Dict format will be - diffrent for each type of module so use get_state to get correct format. + Set the state of the module. + + Parameters + ---------- + state : dict[str, Any] + Module state represented as a dictionary. """ self.name = state["name"] def get_output_signals(self) -> list[Signal]: - """ - Returns the modules output signals. Assumes all output signals are - stored with a key beginning in 'out' in the signals dictionary. + """Return the output signals of the module. + + Assumes all output signals are stored with a key beginning in 'out\_' in + the signals dictionary. + + Returns + ------- + list[Signal] + List of all output signals of this module. """ return [signal for key, signal in self.signals.items() if key[0:3] == "out"] def get_input_signals(self) -> list[Signal]: - """ - Returns the modules input signals. Assumes all input signals are stored - with a key beginning in 'in' in the signals dictionary. + """Return the input signals of the module. + + Assumes all input signals are stored with a key beginning in 'in\_' in + the signals dictionary. + + Returns + ------- + list[Signal] + List of all input signals of this module. """ return [signal for key, signal in self.signals.items() if key[0:2] == "in"] def reset(self) -> None: - """ - Resets the module to its default state. - """ - raise NotImplemented + """Reset the module to its default state.""" + raise NotImplementedError - def load_from_str(self, state_string): - """ - Sets the moudles state according to a string - Each module type will decide how the string should be formated. + def load_from_str(self, state_string) -> None: + """Set the module state according to a string. + + Each module subclass decides the format of the string. """ - raise NotImplemented + raise NotImplementedError def get_longest_line_len(self, ignore_keys=None) -> int: - """ - Helper function for pretty_print that returns the length of - the longest line to print for a module. + """Return the length of the longest line when printing the module. + + Helper function for pretty_print. """ if ignore_keys == None: @@ -112,17 +158,23 @@ class Module: return longest_line_len def print_module(self) -> None: - """ - Prints the module directly to terminal - """ + """Print the module.""" print(self.name) def save_state_to_file(self, file_path: str) -> bool: + """Save the modules state to file. + + Parameters + ---------- + file_path : str + Path to the file to save to. + + Returns + ------- + bool + True if saving succeeded, False otherwise. """ - Tries to save the modules state to a given file. - Returns true on success and false if something went wrong. - """ - raise NotImplemented + raise NotImplementedError def __str__(self) -> str: return self.name diff --git a/src/simudator/core/modules/demux.py b/src/simudator/core/modules/demux.py index 014cfa5d589e89424038ca97711e598f0bf2a89c..2aeb731c2e414cd161b71ec38ea2743d0be6ffbf 100644 --- a/src/simudator/core/modules/demux.py +++ b/src/simudator/core/modules/demux.py @@ -6,12 +6,31 @@ from simudator.core.signal import Signal class Demux(Module): """ - A general demux that allows an arbitrary amount of outputs to map - to a single input. The output side is controlled by the control - signal 'from_demux_s'. + A general demultiplexer that allows an arbitrary amount of outputs to map + to a single input. + + Assumes the value sent from input to outputs is an integer. + + Parameters + ---------- + control : Signal + Control signal of which the value is used to index which + output signal to output to. + bit_length : int + Maximum number of bits for the value outputted by the demux. All extra + bits of the input value are discarded. + input : Signal + Input signal of which the value is forwarded to the currently + selected output signal. + outputs : list[Signal] + List of output signals to demultiplex the input signal value to. + name : str + Name of the demultiplexer module. + value : Any + Initial value to output to the currently selected output signal. """ - __slots__ = ("bit_length", "mask", "value") + __slots__ = ("_bit_length", "_value") def __init__( self, @@ -33,51 +52,51 @@ class Demux(Module): super().__init__(signals, name) - # mask and bit_length - self.bit_length = bit_length - self.mask = 2**self.bit_length - 1 + self._bit_length = bit_length # Value to be read/written - self.value = value + self._value = value def update_register(self) -> None: """ - Read the input value and update the value of the demux. + Read the input signal value and update the value of the demux. """ + input_value = self.signals["in_input"].get_value() - self.value = input_value & self.mask + mask = 2**self._bit_length - 1 + self._value = input_value & mask - def output_register(self): - """ - Read which signal to write to from the control signal - 'from_demux_s' and forward the input to that output signal. + def output_register(self) -> None: + """Read which signal to write to from the control signal and forward + the input to that output signal. """ output_index = self.signals["in_control"].get_value() output_signal_key = f"out_output_{output_index}" - self.signals[output_signal_key].update_value(self.value) + self.signals[output_signal_key].update_value(self._value) - def update_logic(self): - """ - The demux has no logic + def update_logic(self) -> None: + """Do nothing. + + The demux has no logic. """ pass def get_state(self) -> dict[str, Any]: - """ - Returns a dict of the demux state. - """ state = super().get_state() - state["value"] = self.value - state["bit_length"] = self.bit_length - state["mask"] = self.mask + state["value"] = self._value + state["bit_length"] = self._bit_length return state - def set_state(self, state: dict) -> None: - """ - Sets the name and value of the demux. + def set_state(self, state: dict[str, Any]) -> None: + """Set the name and value of the demux. + + Parameters + ---------- + state : dict[str, Any] + The state of the demux to load. Should contain the keys "value" and + "bit_length", both with values of type ``int``. """ super().set_state(state) - self.value = state["value"] - self.bit_length = state["bit_length"] - self.mask = state["mask"] + self._value = state["value"] + self._bit_length = state["bit_length"] diff --git a/src/simudator/core/modules/memory.py b/src/simudator/core/modules/memory.py index 5cdce8bb5fbd2889388573d81e75c5147087fbc4..8f946bac71d900be0c087eeb00fdf1995cee9ab5 100644 --- a/src/simudator/core/modules/memory.py +++ b/src/simudator/core/modules/memory.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + from simudator.core.module import Module from simudator.core.signal import Signal @@ -7,51 +9,66 @@ from simudator.core.signal import Signal class Memory(Module): """ A general size memory module. + + Parameters + ---------- + input : Signal + Signal of which the value may be written to the current address. + output : Signal + Signal to which the content of the current address is written. + control : Signal + Signal used to enable or disable write to the current address. Assumed + to output ``bool``. + address : Signal + Signal used to address the memory. Assumed to output ``int``. + size : int + Size of the address space of the memory. + name : str + Name of the module. """ __slots__ = ( - "memory", - "current_address", - "is_write", - "address_padding", - "value_padding", + "_memory", + "_current_address", + "_is_write", ) + """ + Constants used to textually pad the values of the memory when saving to + file. + """ + MEMORY_VALUE_PADDING = 2 + MEMORY_ADDRESS_PADDING = 2 def __init__( self, - input_signal: Signal, - output_signal: Signal, - control_signal: Signal, - address_signal: Signal, + input: Signal, + output: Signal, + control: Signal, + address: Signal, size: int, - value_padding: int = 2, - address_padding: int = 2, name="Memory", ) -> None: # signals signals = { - "in_input": input_signal, - "in_control": control_signal, - "in_address": address_signal, - "out_content": output_signal, + "in_input": input, + "in_control": control, + "in_address": address, + "out_content": output, } # Init super class super().__init__(signals, name) # Internal state - self.memory = [0 for _ in range(size)] - self.current_address = 0 - self.is_write = False - - # Values used to format the strings when saving state to file - self.address_padding = address_padding - self.value_padding = value_padding + self._memory = [0 for _ in range(size)] + self._current_address = 0 + self._is_write = False def update_register(self): - if self.is_write: - self.memory[self.current_address] = self.signals["in_input"].get_value() + if self._is_write: + value = self.signals["in_input"].get_value() + self._memory[self._current_address] = value def output_register(self) -> None: pass @@ -63,44 +80,44 @@ class Memory(Module): ctrl_sig = self.signals["in_control"] out_sig = self.signals["out_content"] if adr_sig.get_value() is not None and ctrl_sig.get_value() is not None: - self.is_write = ctrl_sig.get_value() - self.current_address = adr_sig.get_value() - out_sig.update_value(self.memory[self.current_address]) + self._is_write = ctrl_sig.get_value() + self._current_address = adr_sig.get_value() + out_sig.update_value(self._memory[self._current_address]) def print_module(self) -> None: print("", self.name, "\n -----") - for address, value in enumerate(self.memory): + for address, value in enumerate(self._memory): if value: print("", str(hex(address)), "", str(hex(value)), "\n") - def get_state(self) -> dict: + def get_state(self) -> dict[str, Any]: state = super().get_state() - state["is_write"] = self.is_write - state["current_address"] = self.current_address - state["memory"] = self.memory[:] + state["is_write"] = self._is_write + state["current_address"] = self._current_address + state["memory"] = self._memory[:] return state - def get_gui_state(self) -> dict: + def get_gui_state(self) -> dict[str, Any]: state = super().get_gui_state() - state["current_address"] = self.current_address - state["memory"] = self.memory[:] + state["current_address"] = self._current_address + state["memory"] = self._memory[:] return state - def set_state(self, state: dict) -> None: + def set_state(self, state: dict[str, Any]) -> None: super().set_state(state) if "is_write" in state: - self.is_write = state["is_write"] + self._is_write = state["is_write"] if "current_address" in state: - self.current_address = state["current_address"] + self._current_address = state["current_address"] if "memory" in state: - self.memory = state["memory"] + self._memory = state["memory"] def reset(self) -> None: """ Reset the memory to 0 for each address. """ - for i in range(len(self.memory)): - self.memory[i] = 0 + for i in range(len(self._memory)): + self._memory[i] = 0 def get_longest_line_len(self, ignore_keys=None) -> int: """ @@ -113,7 +130,7 @@ class Memory(Module): longest_memory_line = 0 - for value in self.memory: + for value in self._memory: value_len = len(str(value)) if value_len > longest_memory_line: longest_memory_line = value_len @@ -125,19 +142,16 @@ class Memory(Module): Helper function for pretty_print that returns the length of the largest address in the memory to print for a module. """ - return len(str(len(self.memory))) + return len(str(len(self._memory))) - def save_state_to_file(self, file_path: str) -> None: - """ - Tries to save the modules state to a given file. - """ + def save_state_to_file(self, file_path: str) -> bool: file = open(file_path, "a") file.write(self.name + ":\n") - for index, value in enumerate(self.memory): + for index, value in enumerate(self._memory): # Write the address in hex and add ': ' to the end - file.write(hex(index)[2:].rjust(self.address_padding, "0") + ": ") + file.write(hex(index)[2:].rjust(Memory.MEMORY_ADDRESS_PADDING, "0") + ": ") # Write the value in hex - file.write(hex(value)[2:].rjust(self.value_padding, "0")) + file.write(hex(value)[2:].rjust(Memory.MEMORY_VALUE_PADDING, "0")) file.write("\n") file.write("\n") diff --git a/src/simudator/core/modules/mux.py b/src/simudator/core/modules/mux.py index 23e13ef535d3663f7a7a58f7c7f09f96b6a4094e..018698e17e42ecc83ba737939135a370906cb14b 100644 --- a/src/simudator/core/modules/mux.py +++ b/src/simudator/core/modules/mux.py @@ -6,16 +6,33 @@ from simudator.core.signal import Signal class Mux(Module): """ - A general mux that allows an arbitrary amount of input to map - to a single output. The input side is controlled by the control - signal 'to_mux_s'. + A general mux that allows an arbitrary amount of input to map to a single + output. + + Parameters + ---------- + control : Signal + Control signal of which the value is used to index which + input signal to read from. + bit_length : int + Maximum number of bits for the value outputted by the mux. All extra + bits of the input value are discarded. + inputs : list[Signal] + List of input signals to select from to output onto the output signal. + output : Signal + Signal onto which the value of the currently selected input is + outputted. + name : str + Name of the multiplexer module. + value : Any + Initial value to output to the output signal. """ - __slots__ = ("bit_length", "mask", "value") + __slots__ = ("_bit_length", "_value") def __init__( self, - to_mux: Signal, + control: Signal, bit_length: int, output: Signal, inputs: list[Signal] = [], @@ -25,7 +42,7 @@ class Mux(Module): # Signals signals = { - "in_control": to_mux, + "in_control": control, "out": output, } for i, s in enumerate(inputs): @@ -35,62 +52,61 @@ class Mux(Module): super().__init__(signals, name) # mask and bit_length - self.bit_length = bit_length - self.mask = 2**self.bit_length - 1 + self._bit_length = bit_length # Value to be read/written - self.value = value + self._value = value def update_register(self) -> None: - """ - Read which signal to read from the control signal - 'to_mux_s' and forward that signal to the output. + """Read which signal to read from the control signal and forward that + signal to the output. """ input_index = self.signals["in_control"].get_value() input_signal_key = f"in_input_{input_index}" input_value = self.signals[input_signal_key].get_value() - self.value = input_value & self.mask + mask = 2**self._bit_length - 1 + self._value = input_value & mask def output_register(self): + """Output the value of the currently selected input signal to the + output signal. """ - Output the value of the mux to its output. - """ - self.signals["out"].update_value(self.value) + self.signals["out"].update_value(self._value) def update_logic(self): - """ + """Do nothing. + The mux has no logic. """ pass def get_state(self) -> dict[str, Any]: - """ - Returns a dict of the mux state. - """ state = super().get_state() - state["value"] = self.value - state["bit_length"] = self.bit_length - state["mask"] = self.mask + state["value"] = self._value + state["bit_length"] = self._bit_length return state - def set_state(self, state: dict) -> None: - """ - Sets the name and value of the mux. + def set_state(self, state: dict[str, Any]) -> None: + """Set the name, value and bit length of the demux. + + Parameters + ---------- + state : dict[str, Any] + The state of the demux to load. Should contain the keys "name", + "value" and "bit_length" with values of type ``str``, ``int`` and + ``int`` respectively. """ super().set_state(state) - self.value = state["value"] - self.bit_length = state["bit_length"] - self.mask = state["mask"] + self._value = state["value"] + self._bit_length = state["bit_length"] def print_module(self) -> None: print( self.name, "\n-----", "\nvalue: ", - self.value, + self._value, "\nbit length: ", - self.bit_length, - "\nmask: ", - self.mask, + self._bit_length, ) diff --git a/src/simudator/core/modules/register.py b/src/simudator/core/modules/register.py index b23e57cc04188b5670cd847e674227f83460fa64..e2b70cd4d6e0096ae841e8cbc370e0bb58cc7d3c 100644 --- a/src/simudator/core/modules/register.py +++ b/src/simudator/core/modules/register.py @@ -8,10 +8,21 @@ from simudator.core.signal import Signal class Register(Module): """ - A simple module that can store a value. + A simple module that can store and output a value. + + Parameters + ---------- + input : Signal + Signal from which the value is stored in the register. + output : Signal + Signal onto which the value of the register is outputted. + value : Any + Initial value of the register. + name : str + Name of the register. """ - __slots__ = "value" + __slots__ = "_value" def __init__( self, @@ -29,39 +40,33 @@ class Register(Module): # init the instance super().__init__(signals, name) - self.value = value + self._value = value def update_register(self) -> None: - """ - Propagate the input signal to the registers internal state. - Throw away bits larger than the length of the register. + """Update the internal value of the register with the input signal. + + Any extra bits that do not fit the bit length of the register are + thrown away. """ input_value = self.signals["in_content"].get_value() if input_value is None: return - self.value = input_value + self._value = input_value def output_register(self) -> None: - """ - Propagate the value of the register to the output signal. - It is the response of the destination to know when it should - read the output. - """ - self.signals["out_content"].update_value(self.value) + """Output the value of the register onto the output signal.""" + self.signals["out_content"].update_value(self._value) def update_logic(self): - """ + """Do nothing. + The register has no logic. """ pass def get_state(self) -> dict[str, Any]: - """ - Returns a dict of the register state. - These states are changable via set_states. - """ state = super().get_state() - state["value"] = self.value + state["value"] = self._value return state def get_gui_state(self) -> dict: @@ -70,25 +75,27 @@ class Register(Module): def set_state(self, state: dict[str, Any]) -> None: """ - Sets the register state to one given in dict. + Set the state of the register. + + Parameters + ---------- + state : dict[str, Any] + The state of the register to load. Should contain the keys "name" + and "value" with values of type ``str`` and ``int`` respectively. """ self.name = state["name"] - self.value = state["value"] + self._value = state["value"] def reset(self) -> None: """ - Resets the register to 0. + Reset the register to 0. """ - self.value = 0 + self._value = 0 - def save_state_to_file(self, file_path: str) -> None: - """ - Tries to save the modules state to a given file. - Returns true on success and false if something went wrong. - """ + def save_state_to_file(self, file_path: str) -> bool: file = open(file_path, "a") file.write(self.name + ":\n") - file.write("value: " + str(self.value) + "\n\n") + file.write("value: " + str(self._value) + "\n\n") file.close() def print_module(self) -> None: @@ -97,78 +104,74 @@ class Register(Module): self.name, "\n -----", "\n value: ", - self.value, + self._value, ) class IntegerRegister(Register): """ - A simple module that can store an integer value with a given bit-length. + A register intended to store integers only. + + Parameters + ---------- + input : Signal + Signal from which the value is stored in the register. + output : Signal + Signal onto which the value of the register is outputted. + bit_length : int + Maximum number of bits of the input to store in the register. All extra + bits of the input value are discarded. + value : Any + Initial value of the register. + name : str + Name of the register. """ - __slots__ = ("bit_length", "mask") + __slots__ = "_bit_length" def __init__( self, - input_signal: Signal, - output_signal: Signal, + input: Signal, + output: Signal, bit_length: int, value: int = 0, name: str | None = None, ) -> None: - # set the registers name + # set the name if name is None: name = f"{bit_length}-bit register" - super().__init__(input_signal, output_signal, value=value, name=name) + super().__init__(input, output, value=value, name=name) # set the bit length of the register - self.bit_length = bit_length - - # set the registers mask. An 8 bit register should - # have the mask 1111 1111, aka one '1' for every bit - self.mask = 2**bit_length - 1 + self._bit_length = bit_length def update_register(self) -> None: - """ - Propagate the input signal to the registers internal state. - Throw away bits larger than the length of the register. - """ super().update_register() - self.value = self.value & self.mask + mask = 2**self._bit_length - 1 + self._value = self._value & mask def get_state(self) -> dict[str, Any]: - """ - Returns a dict of the register state. - These states are changable via set_states. - """ state = super().get_state() - state["bit_length"] = self.bit_length + state["bit_length"] = self._bit_length return state def set_state(self, state: dict[str, Any]) -> None: - """ - Sets the register state to one given in dict. - """ super().set_state(state) if "bit_length" in state: - self.bit_length = state["bit_length"] - self.mask = 2**self.bit_length - 1 + self._bit_length = state["bit_length"] - def save_state_to_file(self, file_path: str) -> None: - """ - Tries to save the modules state to a given file. - """ + def save_state_to_file(self, file_path: str) -> bool: file = open(file_path, "a") file.write(self.name + ":\n") - file.write("value: " + hex(self.value)[2:] + "\n\n") + file.write("value: " + hex(self._value)[2:] + "\n\n") file.close() - def load_from_str(self, state_string): + def load_from_str(self, state_string) -> None: string_pair = state_string.split(": ") # TODO: Maybe check if it starts with value: ? - self.value = int(string_pair[1], 16) + self._value = int(string_pair[1], 16) class Flag(IntegerRegister): @@ -188,8 +191,8 @@ class Flag(IntegerRegister): # set the flags name super().__init__( - input_signal=input_signal, - output_signal=output_signal, + input=input_signal, + output=output_signal, bit_length=bit_length, value=value, name=name, @@ -203,20 +206,21 @@ class Flag(IntegerRegister): to activate (ex: loop counter reaches zero -> set L flag to 1) """ input_value = self.signals["in_content"].get_value() - self.value = input_value & self.mask + mask = 2**self._bit_length - 1 + self._value = input_value & mask def get_gui_state(self) -> dict: state = super().get_gui_state() - state["value"] = self.value + state["value"] = self._value return state - def save_state_to_file(self, file_path: str) -> None: + def save_state_to_file(self, file_path: str) -> bool: """ Tries to save the modules state to a given file. """ file = open(file_path, "a") file.write(self.name + ":\n") - file.write("value: " + str(self.value) + "\n\n") + file.write("value: " + str(self._value) + "\n\n") file.close() def print_module(self) -> None: @@ -225,9 +229,7 @@ class Flag(IntegerRegister): self.name, "\n -----", "\n value: ", - self.value, + self._value, "\n bit length: ", - self.bit_length, - "\n mask: ", - self.mask, + self._bit_length, ) diff --git a/src/simudator/core/processor.py b/src/simudator/core/processor.py index 8b7d31aaa2f97635bdf98fff9b09c2af6dc202ca..0106327f75025ec87a47c75555a0041cfb301486 100644 --- a/src/simudator/core/processor.py +++ b/src/simudator/core/processor.py @@ -17,42 +17,67 @@ from .signal import Signal class Processor: """ Main class for controlling the processor. + Uses modules and signals to simulate processor components behaviour. """ - __slots__ = ("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", "is_stopped", - "new_instruction", "current_instructions", - "ignore_keys", "lambdas", "assembly_cycles", - "line_separator") - - def __init__(self): + __slots__ = ( + "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", + "is_stopped", + "new_instruction", + "current_instructions", + "ignore_keys", + "lambdas", + "assembly_cycles", + "line_separator", + ) + + LINE_SEPARATOR = "-" + MAX_LINE_LEN = 170 + CYCLES_TO_SAVE = 10000 + + def __init__(self) -> None: + # For simulation self.modules: dict[str, Module] = dict() self.signals = [] self.clock = 0 self.update_queue = [] - self.module_history: list[dict[str, dict[str, Any]]] = [] - self.signal_history: list[list] = [] # TODO: Is this needed? + self.is_stopped = False + + # Breakpoint handling self.breakpoint_id_counter = 1 self.breakpoints: dict[int, Breakpoint] = {} self.breakpoint_reached = False self.last_breakpoint = None - self.cycles_to_save = 10000 + self.lambdas: dict[str, Callable[..., bool]] = {} + + # Saving processor state for saving/loading to file and undoing ticks self.removed_cycles = 0 - self.max_line_len = 170 - self.line_separator = "-" - self.is_stopped = False + 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? + + # For showing which instructions are being done and signalling + # the start of a new one self.new_instruction = 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. + # 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]] = [] # TODO: keeping track of what pieces of info not to show @@ -68,14 +93,9 @@ class Processor: "decrement_by_one", "bus_id", ] - self.lambdas: dict[str, Callable[..., bool]] = {} - - # List containing all clock cycles where a new asm instruction started - self.assembly_cycles = [0] def do_tick(self) -> None: - """ - Simulate one clock cycle of the processor. + """Simulate one clock cycle of the processor. Run each module's tick function, then handle the following updates. Also check for breakpoints that are reached in this cycle, and save @@ -116,51 +136,74 @@ class Processor: self.new_instruction = False def get_current_instructions(self) -> list[tuple[str, int, int]]: - """ - Return a list of the current instructions with their positions. + """Return a list of the current instructions with their positions. - Each entry in the list is a tuple of a instruction with its column and row position - for the pipeline diagram. + Each entry in the list is a tuple of a instruction with its column and + row position for the pipeline diagram. + + Returns + ------- + list[tuple[str, int, int]] + A list of instructions represented as tuples. Each tuple contains + the instruction and its position in the pipeline. """ - raise NotImplemented + raise NotImplementedError def get_pipeline_dimensions(self) -> tuple[int, int]: - """ - Return a tuple on the form (x, y) for the processors preffered pipeline display size. + """Return the dimensions of the pipeline of the processor. - (x, y) represents the number of columns respective rows the pipeline should have. + Returns + ------- + width : int + The width of the pipeline (number of cycles to display at a time). + depth : int + Depth of the pipeline. """ - raise NotImplemented + raise NotImplementedError def get_asm_instruction(self) -> str: - """ - Return a string containing the 'code' for the current assembly instruction. + """Return a string containing the 'code' for the current assembly + instruction. - What 'code' refers to is up to each processor to define, could be a memory address, - an instruction label or something else. + What 'code' refers to is up to each processor to define. It could be a + memory address, an instruction label or something else. + + Returns + ------- + str + The current assembly instruction. """ - raise NotImplemented + raise NotImplementedError def run_asm_instruction(self, num_instructions=1) -> None: - """ - Runs assembler instructions on the processors 'num_instructions' times. + """Run a number of assembly instructions. - Default argument is one, but it is possible to specify any number of instructions. - This should be implemented per processor and thus we raise NotImplemented. + Defaults to running 1 instruction unless some other number is provided. + + Parameters + ---------- + num_instructions : int + Number of assembly instructions to perform. + + Notes + ----- + This should be implemented per processor and thus we + raise NotImplementedError. """ - raise NotImplemented + raise NotImplementedError def undo_asm_instruction(self, num_instructions=1) -> None: """ - Undos 'num_instructions' numbers of assembler instructions on the CPU. + Undo a number of assembly instructions on the CPU. - Default argument is one, but it is possible to specify any number of instructions. - Undo assembler instruction assumes the CPU is always at the end of the list - 'self.assembly_cycles'. + Default argument is one, but it is possible to specify any number of + instructions. - Undos asm instructions by finding the corresponding clock cycle before - said number of asm intructions started and loading that clock cycle. + Parameters + ---------- + num_instructions : int + Number of assembly instructions to undo. """ current_clock_cycle = self.clock @@ -185,7 +228,7 @@ class Processor: # Need +1 here since we save the start state to enable to # 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 happened when we load a new file. + # that has uPC set to 0, which wont happen when we load a new file. self.assembly_cycles = self.assembly_cycles[: index + 1] def run_continuously(self) -> None: @@ -198,48 +241,51 @@ class Processor: def should_halt(self) -> bool: """ - Return True when the processor should halt, otherwise False. + Return whether the processor should halt this clock cycle or not. + + Returns + ------- + bool + ``True`` if the processor should halt, ``False`` otherwise. - Note + Notes ---- Each processor should implement the condition for True in its subclass. - If we instead raise NotImplemented as we do in all other cases when the - functionality should be implemented in the subclass we lose the ability - to test modules on the base processor class. + If we instead raise NotImplementedError as we do in all other cases + when the functionality should be implemented in the subclass we lose + the ability to test modules on the base processor class. """ return False def stop(self) -> None: - """ - Signal to stop the execution of the processor until the processor is + """Signal to stop the execution of the processor until the processor is instructed to run continuously or do some ticks again. """ self.is_stopped = True def unstop(self) -> None: - """ - Reset the stop execution signal. - """ + """Reset the stop execution signal.""" self.is_stopped = False def is_new_instruction(self) -> bool: """ Return true on the same clock cycle the CPU starts a new instruction. - Should be implemented per CPU, return field new_instruction. It is up to each - processor to set this field to True/False correctly. + 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 def stop_at_breakpoints(self) -> None: - """ - Stop the execution if any breakpoint has been reached during this cycle. + """Stop the execution if any breakpoint has been reached during this + clock cycle. Also record the breakpoint that was reached. """ self.breakpoint_reached = False for _, bp in self.breakpoints.items(): - # TODO: Can make this more efficient by only checking enabled breakpoints + # TODO: Can make this more efficient by only checking enabled + # breakpoints if bp.is_break() and bp.is_enabled: self.breakpoint_reached = True self.last_breakpoint = bp @@ -248,8 +294,7 @@ class Processor: self.stop() def reset(self): - """ - Reset the processor to its start state. + """Reset the processor to its start state. This resets all modules and removes any saved states for undoing clock cycles. A round of value propagation is done to reset signals too. @@ -269,45 +314,83 @@ class Processor: module.update_logic() def add_modules_to_update(self, module: Module) -> None: - """ - Queues module to be updated at the end of clock cycle. + """Queue a module to be updated at the end of clock cycle. + + Parameters + ---------- + module : Module + Module to queue for update. + + See Also + -------- + Module.update_logic : + Method for updating a module. """ if module not in self.update_queue: self.update_queue.append(module) def add_module(self, module: Module) -> None: - """ - Add module into the processor. + """Add module to be simulated by the processor. + + Parameters + ---------- + module : Module + Module to add. """ self.modules[module.name] = module def get_module(self, name: str) -> Module: - """ - Get module with specific name. + """Get module with specific name. + + Parameters + ---------- + name : str + Name of the module to get. + + Returns + ------- + Module + The module with the specified name. """ return self.modules[name] def get_modules(self) -> list[Module]: - """ - Get list of all modules. + """Get list of all modules. + + Returns + ------- + list[Module] + List of all modules in the processor. """ return list(self.modules.values()) def add_signals(self, signals: list[Signal]) -> None: - """ - Add signals into the processor. + """Add signals to the processor. + + Parameters + ---------- + list[Signal] + List of signals to add for simulation. """ self.signals += signals def get_clock(self) -> int: - """ - Get current clockcycle. + """Get the current clockcycle number. + + Returns + ------- + int + Current clock cycle number of the processor. """ return self.clock def set_clock(self, value: int) -> None: - """ - Set current clockcycle. + """Set current clockcycle number. + + Parameters + ---------- + value : int + Cycle number to set the clock to. """ self.clock = value @@ -320,7 +403,7 @@ class Processor: # Only save a specified number of cycles, # saving every cycle can easily eat all ram - if len(self.module_history) > self.cycles_to_save: + if len(self.module_history) > Processor.CYCLES_TO_SAVE: self.module_history.pop(0) self.removed_cycles += 1 @@ -329,10 +412,15 @@ class Processor: 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 cycle. + """Load the state of all modules as they were at the specified clock + cycle. This does not erase the history of states for the later cycles. + + Parameters + ---------- + cycle : int + Number of the cycle to load. """ cycle_index = cycle - self.removed_cycles @@ -357,13 +445,13 @@ class Processor: module = self.update_queue.pop(0) module.update_logic() - def load_state_from_file(self, file_path) -> None: - """ - Loads states for modules from a file. + def load_state_from_file(self, file_path: str) -> None: + """Load states for modules from a file. - Appends the lines from the loaded file to a long string. This string - is given to each module in the function 'load_from_str'. Each module - will thus load itself. + Parameters + ---------- + file_path : str + Path to the file containing the processor state to load. """ self.reset() @@ -376,6 +464,9 @@ class Processor: else: raise ValueError # Else raise error + # Appends the lines from the loaded file to a long string. This string + # is given to each module in the function 'load_from_str'. Each module + # will thus load itself. module_name = None module_str = "" @@ -407,10 +498,14 @@ class Processor: module.load_from_str(module_str) def save_state_to_file(self, file_path: str) -> None: - """ - Saves the current state of the cpu to a given file. + """Save the current state of the cpu to file. Will overwrite the given file. + + Parameters + ---------- + file_path : str + Path to the file to write to. """ file = open(file_path, "w") file.write("") @@ -421,13 +516,21 @@ class Processor: module.save_state_to_file(file_path) 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: """ - Prints the most relevant information about each module - compact and pretty. Fields of modules can be ignored with the - optional argument 'ignore_keys'. + 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 = [] @@ -457,14 +560,15 @@ class Processor: module_to_line_length = {} # get the longest line length for all other_modules for module in other_modules: - line_len = module.get_longest_line_len(ignore_keys) + 3 # +3 for padding + # +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(self.line_separator * self.max_line_len) + print(Processor.LINE_SEPARATOR * Processor.MAX_LINE_LEN) # print each group separate for group in groups: @@ -495,32 +599,38 @@ class Processor: real_keys[row] + ": " + str(module_state[real_keys[row]]) ) - # padd the string so each string has the same - # length + # 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(self.line_separator * self.max_line_len) + 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: Module) -> None: + 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(self.line_separator * self.max_line_len) + 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.memory): + for i, value in enumerate(module.get_state()["memory"]): # create a new string containing the address and the value - # of the memory address formattet to fit with the largest + # 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 @@ -528,7 +638,7 @@ class Processor: # only add the string if there is space for it, else # print the string and start a new - if len(string + new_row) + 1 > self.max_line_len: + if len(string + new_row) + 1 > Processor.MAX_LINE_LEN: print(string) string = new_row else: @@ -539,16 +649,21 @@ class Processor: else: string = new_row print(string) - print(self.line_separator * self.max_line_len) + print(Processor.LINE_SEPARATOR * Processor.MAX_LINE_LEN) print() def pretty_print_names(self, module_to_line_length: dict[Module, int]) -> None: """ - Prints the name of the modules in one row, but pretty. + 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. - Adds spacing between the names so that the modules - longest field will have space to be printed below. + 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 @@ -563,9 +678,20 @@ class Processor: def group_pp_modules( self, module_to_line_length: dict[Module, int] ) -> list[dict[Module, int]]: - """ - Groups the modules to be pretty printed into groups with a - total line length lower than 'self.max_line_len'. + """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 = [{}] @@ -574,7 +700,7 @@ class Processor: for module in module_to_line_length: # Make sure the line is not to long - if line_len + module_to_line_length[module] < self.max_line_len: + 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] @@ -588,8 +714,21 @@ class Processor: def fields_to_list(self, module: Module, ignore_keys=[]) -> list[str]: """ - Return a list containing all the keys for a module except the name. - Optinal argument to ignore specific keys. + 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 @@ -597,12 +736,29 @@ class Processor: if key != "name" and key not in ignore_keys ] - def get_most_fields(self, modules: dict[Module: int], ignore_keys=[]) -> int: - """ - Return the most number of a fields a module has. + 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)) @@ -614,6 +770,24 @@ class Processor: def add_state_breakpoint( self, module_name: str, state_name: str, value: Any ) -> None: + """Add a module state breakpoint to the processor. + + Parameters + ---------- + module_name : str + Name of the module for which a breakpoint will be added. + state_name : str + Name of the state variable of the module to put a breakpoint + on. + value : Any + Value of the state variable that triggers the breakpoint. + + Raises + ------ + ValueError + If the module does not exist or if the state variable does not + exist in the module. + """ if module_name not in self.modules: raise ValueError(f"No module named {module_name}") @@ -628,12 +802,20 @@ class Processor: self.breakpoint_id_counter += 1 def remove_breakpoint(self, id: int) -> bool: + """Remove a specific breakpoint. + + Parameters + ---------- + id : int + ID of the breakpoint to be removed. + """ 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: print("There are no breakpoints.") else: @@ -641,6 +823,22 @@ class Processor: print(f"BP {bp_id}: {bp}") def add_memory_breakpoint(self, module_name: str, address: int, value: Any) -> None: + """Add a breakpoint for a memory module. + + Parameters + ---------- + module_name : str + Name of the memory module for which a breakpoint will be added. + address : int + Address of the memory module to break at. + value : Any + Value of the address that triggers the breakpoint. + + Raises + ------ + ValueError + If the module does not exist or is not a memory. + """ if module_name not in self.modules: raise ValueError(f"No module named {module_name}") @@ -654,9 +852,37 @@ class Processor: self.breakpoint_id_counter += 1 def get_breakpoint_lambdas(self) -> list[str]: + """Get all functions available for lambda breakpoints. + + Returns + ------- + list[str] + List of names of all functions of the processor that are available + for adding lambda breakpoints. + """ + return list(self.lambdas.keys()) def add_lambda_breakpoint(self, lambda_name: str, **kwargs) -> None: + """Add a lambda breakpoint to the processor. + + Module names used in the keyword arguments are substituted by the + actual modules before being passed on to the lambda breakpoint + function. + + Parameters + ---------- + lambda_name : str + Name of the lambda breakpoint function to be used by the + breakpoint. + **kwargs + Arguments to pass to the breakpoint function. + + Raises + ------ + ValueError + If the lambda breakpoint function does not exist. + """ # Substitute any module names for the module instances # This is to be able to externally create general lambda breakpoints # that can take any values as arguments while also having a way to take @@ -675,4 +901,13 @@ class Processor: self.breakpoint_id_counter += 1 def set_enabled_breakpoint(self, bp_id: int, is_enabled: bool) -> None: + """Toggle a breakpoint to enabled or disabled. + + Parameters + ---------- + bp_id : int + ID of the breakpoint to toggle. + is_enabled : bool + ``True`` to enable, ``False`` to disable the breakpoint. + """ self.breakpoints[bp_id].set_enabled(is_enabled) diff --git a/src/simudator/core/signal.py b/src/simudator/core/signal.py index 18e3193b9a85a2a4db629c117bef53b24ad197f0..8db1d14133278add895bc9b776f778addb5116df 100644 --- a/src/simudator/core/signal.py +++ b/src/simudator/core/signal.py @@ -5,57 +5,79 @@ from typing import Any class Signal: """ - Handles datatransportation between modules in the processor. - Each signal has one source module and can have multiple - destination modules. + Signal class to send data between modules in a processor. + + Parameters + ---------- + processor : Processor + Processor that the signal belongs to. Needed to queue processor modules + for update. + name : str + Name of the signal. + value : Any + Initial value of the signal. """ - __slots__ = ("processor", "name", "source", "destinations", "value") + __slots__ = ("_processor", "_name", "_source", "_destinations", "_value") def __init__(self, processor: Processor, name: str = "Signal", value=None): - self.processor = processor - self.name = name - self.source = None - self.destinations = set() - self.value = value + self._processor = processor + self._name = name + self._source = None + self._destinations = set() + self._value = value def add_destination(self, module: Module) -> None: - """ - Add a destination module that the signal will send new + """Add a module as a destination that the signal will send new values to. """ - self.destinations.add(module) - - def set_source(self, module: Module) -> None: - """ - Set source module that will send values through the signal. - """ - self.source = module + self._destinations.add(module) def update_value(self, value) -> None: """ - Sets value in signal and queues destination moduels for update. + Set the value of the signal and queue destination modules for update. + + Parameters + ---------- + value : Any + Value to set the signal to. """ - if value != self.value: - self.value = value + if value != self._value: + self._value = value # Inform the processor on what modules have been given # signal so they can update - for destination in self.destinations: - self.processor.add_modules_to_update(destination) + for destination in self._destinations: + self._processor.add_modules_to_update(destination) def get_value(self) -> Any: - """ - Get the signals current value. - """ - return self.value + """Get the current value of the signal. - def set_value(self, value) -> None: + Returns + ------- + Any + Value of the signal. """ - Set the value of the signal without queuing the destination - modules of the signal for update. This method should not - be called by a module. + return self._value + + def set_value(self, value: Any) -> None: + """Set the value of the signal without queuing the destination + modules of the signal for update. + + This method should not be called by a module. + + Parameters + ---------- + value : any + Value to set the signal to. """ - self.value = value + self._value = value def get_name(self) -> str: - return self.name + """Return the name of the signal. + + Returns + ------- + str + Name of the signal. + """ + return self._name diff --git a/src/simudator/processor/mia/modules/ar.py b/src/simudator/processor/mia/modules/ar.py index bca9795267a761968984bd26eef6069031551c36..a1e2c6ff115f840fb9bc4b260fa07eb6a7679a6f 100644 --- a/src/simudator/processor/mia/modules/ar.py +++ b/src/simudator/processor/mia/modules/ar.py @@ -33,7 +33,7 @@ class AR(IntegerRegister, MiaBusConnector): def output_register(self) -> None: # always update alu - self.signals["out_alu"].update_value(self.value) + self.signals["out_alu"].update_value(self._value) # only update bus when asked too if self.write_to_bus(): @@ -53,7 +53,7 @@ class AR(IntegerRegister, MiaBusConnector): self.name, "\n -----", "\n value: ", - hex(self.value), + hex(self._value), "\n bit length: ", self.bit_length, ) diff --git a/src/simudator/processor/mia/modules/asr.py b/src/simudator/processor/mia/modules/asr.py index ae096c07b38ddf85e82366c617e474a8eb56a3e9..08fa4c07a4c453198c0dab1d43b7b729d60f8f1e 100644 --- a/src/simudator/processor/mia/modules/asr.py +++ b/src/simudator/processor/mia/modules/asr.py @@ -41,7 +41,7 @@ class ASR(IntegerRegister, MiaBusConnector): self.name, "\n -----", "\n value: ", - hex(self.value), + hex(self._value), "\n bit length: ", self.bit_length, ) diff --git a/src/simudator/processor/mia/modules/hr.py b/src/simudator/processor/mia/modules/hr.py index 506c4d5b14a543f9e3c3095cfa03cf73461735b7..bd74491b83632dcbe52679eaba363326fece177c 100644 --- a/src/simudator/processor/mia/modules/hr.py +++ b/src/simudator/processor/mia/modules/hr.py @@ -21,11 +21,11 @@ class HR(IntegerRegister, MiaBusConnector): bus_output_signal: Signal, bus_control_signal: Signal, bus_id: int, - bit_lenght: int, + bit_length: int, name="HR", ) -> None: IntegerRegister.__init__( - self, bus_input_signal, bus_output_signal, bit_lenght, name=name + self, bus_input_signal, bus_output_signal, bit_length, name=name ) MiaBusConnector.__init__(self, bus_control_signal, bus_id) @@ -45,7 +45,8 @@ class HR(IntegerRegister, MiaBusConnector): elif self.signals["in_alu"].get_value() is not None: input_value = self.signals["in_alu"].get_value() - self.value = input_value & self.mask + mask = 2**self._bit_length - 1 + self._value = input_value & mask def output_register(self) -> None: """ @@ -54,7 +55,7 @@ class HR(IntegerRegister, MiaBusConnector): output the alu signal. """ - self.signals["out_alu"].update_value(self.value) + self.signals["out_alu"].update_value(self._value) if self.write_to_bus(): IntegerRegister.output_register(self) @@ -71,7 +72,7 @@ class HR(IntegerRegister, MiaBusConnector): self.name, "\n -----", "\n value: ", - hex(self.value), + hex(self._value), "\n bit length: ", - self.bit_length, + self._bit_length, ) diff --git a/src/simudator/processor/mia/modules/mia_memory.py b/src/simudator/processor/mia/modules/mia_memory.py index 988f506b9934f616751b4aca28aac7e5cb5b49a0..4b2ca42c137f718c4260e5ab9064f2a5a8b2d573 100644 --- a/src/simudator/processor/mia/modules/mia_memory.py +++ b/src/simudator/processor/mia/modules/mia_memory.py @@ -11,7 +11,7 @@ class MiaMemory(MiaBusConnector, Memory): to/from the mia bus. """ - # Python does not allow multiple inherintence if more than one of the + # Python does not allow multiple inherintence if more than one of the # parent classes uses __slots__. Thus we also includes the __slots__ # from MiaBusConnector. __slots__ = ("label_adress_mapping", "bus_id", "bus_control_s") @@ -24,8 +24,6 @@ class MiaMemory(MiaBusConnector, Memory): bus_control_s: Signal, size: int = 1, bus_id: int = 0, - value_padding: int = 2, - address_padding: int = 2, name: str = "PM", ) -> None: MiaBusConnector.__init__(self, bus_control_s, bus_id) @@ -37,8 +35,6 @@ class MiaMemory(MiaBusConnector, Memory): address_signal, size, name=name, - value_padding=value_padding, - address_padding=address_padding, ) self.bus_control_s.add_destination(self) @@ -49,14 +45,16 @@ class MiaMemory(MiaBusConnector, Memory): Updates the value of the current address. """ if self.read_from_bus(): - self.memory[self.current_address] = self.signals["in_input"].get_value() + self._memory[self._current_address] = self.signals["in_input"].get_value() def output_register(self) -> None: """ Outputs the value of the current address. """ if self.write_to_bus(): - self.signals["out_content"].update_value(self.memory[self.current_address]) + self.signals["out_content"].update_value( + self._memory[self._current_address] + ) else: self.signals["out_content"].update_value(None) @@ -67,17 +65,17 @@ class MiaMemory(MiaBusConnector, Memory): """ address_value = self.signals["in_address"].get_value() if address_value is not None: - self.current_address = address_value + self._current_address = address_value if self.write_to_bus(): self.signals["out_content"].update_value( - self.memory[self.current_address] + self._memory[self._current_address] ) else: self.signals["out_content"].update_value(None) def reset(self) -> None: - size = len(self.memory) - self.memory = [0 for i in range(size)] + size = len(self._memory) + self._memory = [0 for _ in range(size)] def load_from_str(self, state_string) -> None: """ @@ -95,7 +93,7 @@ class MiaMemory(MiaBusConnector, Memory): # Last character of the adress is a semicolon adress = int(line_data[0][:-1], 16) - self.memory[adress] = value + self._memory[adress] = value # There is an asm instruction label if len(line_data) == 3: diff --git a/src/simudator/processor/mia/modules/micro_pc.py b/src/simudator/processor/mia/modules/micro_pc.py index f574fc345be55899556ef5587332c4d0e90bf376..adcd99ffefb2c09e95fb0f3d68544a09c9eea770 100644 --- a/src/simudator/processor/mia/modules/micro_pc.py +++ b/src/simudator/processor/mia/modules/micro_pc.py @@ -22,12 +22,10 @@ class MicroPC(Module): from_supc: Signal, to_um: Signal, from_um: Signal, - bit_length: int = 8, name: str = "uPC", ) -> None: self.value = 0 - self.bit_length = bit_length # Signals signals = { @@ -71,7 +69,6 @@ class MicroPC(Module): def get_state(self) -> dict: state = super().get_state() state["value"] = self.value - state["bit_length"] = self.bit_length return state def get_gui_state(self) -> dict: @@ -84,8 +81,6 @@ class MicroPC(Module): def set_state(self, state: dict) -> None: super().set_state(state) self.value = state["value"] - if "bit_length" in state: - self.bit_length = state["bit_length"] def reset(self) -> None: """ diff --git a/test/test_core/test_demux.py b/test/test_core/test_demux.py index acf92510f17faaf59c475e6426384214ac68eec8..e4c59c3d04f7dc5f73d11c131cfabf87b5301120 100644 --- a/test/test_core/test_demux.py +++ b/test/test_core/test_demux.py @@ -38,7 +38,6 @@ def test_get_state(): assert state["name"] == "Demux" assert state["value"] == 7 assert state["bit_length"] == 5 - assert state["mask"] == 31 def test_set_state(): @@ -46,7 +45,6 @@ def test_set_state(): state["name"] = "Demux" state["value"] = 6 state["bit_length"] = 2 - state["mask"] = 3 cpu = Processor() from_demux = Signal(cpu) @@ -57,10 +55,10 @@ def test_set_state(): demux.set_state(state) - assert demux.name == "Demux" - assert demux.value == 6 - assert demux.bit_length == 2 - assert demux.mask == 3 + state = demux.get_state() + assert state["name"] == "Demux" + assert state["value"] == 6 + assert state["bit_length"] == 2 def test_writing_to_output(): diff --git a/test/test_core/test_register.py b/test/test_core/test_register.py index ae6f51127da4fb86673387e86e9abb1b2b83d000..1d1d85589a2a2851b7e9cf9710282b2c4eca8180 100644 --- a/test/test_core/test_register.py +++ b/test/test_core/test_register.py @@ -26,7 +26,7 @@ def test_set_state(): register.set_state(state) assert register.name == "Reg" - assert register.value == 14 + assert register._value == 14 def test_get_state(): @@ -51,4 +51,4 @@ def test_signal_propagation(): input_s.update_value(10) cpu.do_tick() - assert register.value == 10 + assert register._value == 10 diff --git a/test/test_mia/test_lc.py b/test/test_mia/test_lc.py index aaa591c611a661a94302578b79dd0c2ba4400d0d..407979aead137cf1d3d965f5377442dbc7f822c6 100644 --- a/test/test_mia/test_lc.py +++ b/test/test_mia/test_lc.py @@ -84,7 +84,7 @@ def test_write_to_l_flag(): # The loop counter will now change its value from 1 to 0 which # should set the L flag to 1 in the same cycle cpu.do_tick() - assert l_flag.value == 1 + assert l_flag._value == 1 def test_reset_l_flag(): @@ -110,7 +110,7 @@ def test_reset_l_flag(): mM_uADR_s.update_value(10) cpu.do_tick() - assert l_flag.value == 0 + assert l_flag._value == 0 def test_lc_do_nothing(): diff --git a/test/test_mia/test_step.py b/test/test_mia/test_step.py index 028945db7c22021a701b68765ee734d70608f9e0..e83de422418ab74ad1b6adce9b5bdba52f6e0423 100644 --- a/test/test_mia/test_step.py +++ b/test/test_mia/test_step.py @@ -1,3 +1,4 @@ +from simudator.core.processor import Processor from simudator.processor.mia.mia import MIA_CPU @@ -16,7 +17,7 @@ def test_increment_10(): def test_max_undo(): cpu = MIA_CPU() cpu.load_state_from_file("mia_uppg3.txt") - max_undo = cpu.cycles_to_save + max_undo = Processor.CYCLES_TO_SAVE for _ in range(2 * max_undo): cpu.do_tick()