"""Execute instructions in RAM."""
__author__ = "Jesse Wei <jesse@cs.unc.edu>"
import sys
from pathlib import Path
from typing import Any, Union
import SAPsim.utils.global_vars as global_vars
import SAPsim.utils.instructions as instructions
import SAPsim.utils.helpers as helpers
from SAPsim.utils.helpers import is_documented_by
import SAPsim.utils.exceptions as exceptions
import SAPsim.utils.parser as parser
[docs]
def execute_full_speed() -> None:
"""Execute instructions in ``RAM`` at full speed until ``EXECUTING`` is ``False`` or ``PC > max addr``.
:return: None"""
max_addr: int = 0
if global_vars.RAM.keys():
max_addr = max(global_vars.RAM.keys())
while global_vars.EXECUTING:
# Check that RAM is non-empty.
if global_vars.RAM.keys() and global_vars.PC > max_addr:
global_vars.EXECUTING = False
raise exceptions.DroppedOffBottom
# If we're executing an empty address but it's not a DroppedOffBottom, just skip and don't execute
if global_vars.PC not in global_vars.RAM:
global_vars.PC += 1
continue
instructions.OPCODE_TO_INSTR_PROCEDURE[
helpers.parse_opcode(global_vars.RAM[global_vars.PC])
](helpers.parse_arg(global_vars.RAM[global_vars.PC]))
[docs]
def execute_next() -> None:
"""Execute a single instruction at the current ``PC`` value if ``EXECUTING``.
If attempting to execute an empty address, ``PC += 1`` (i.e., doesn't skip to next filled address).
:return: None"""
if global_vars.EXECUTING:
if global_vars.RAM.keys() and global_vars.PC > max(global_vars.RAM.keys()):
global_vars.EXECUTING = False
raise exceptions.DroppedOffBottom
# If executing an empty address, just skip and don't execute
if global_vars.PC not in global_vars.RAM:
global_vars.PC += 1
else:
instructions.OPCODE_TO_INSTR_PROCEDURE[
helpers.parse_opcode(global_vars.RAM[global_vars.PC])
](helpers.parse_arg(global_vars.RAM[global_vars.PC]))
[docs]
def run(prog_path: str, **kwargs) -> Union[None, dict[str, Any]]:
r"""Run given .csv program in SAPsim format.
:param prog_path:
.csv file in SAPsim format.
:type prog_path: ``str``
:param \**kwargs:
See below
:Keyword Arguments:
* *debug* (``bool``) --
* Whether to run in debug mode (True) or at full speed (False)
* Default is full speed
* *change* (``dict[int, int]``) --
* dict[address, byte] of values to change in RAM
* The value at each address (0 to 15) will be overwritten to that byte
* Useful for debugging programs (edit a value without changing the CSV)
* Useful for autograding programs (overwrite a reserved instruction/data value)
* *table_format* (``str``) --
* Printed table format
* Options: https://github.com/astanin/python-tabulate#table-format
* Default value in ``global_vars`` is ``"simple_outline"``
* The rest of the parameters are pretty much exclusively for unit testing, and you should not use these
* *return_state* (``bool``) --
* If ``True``, then program state will be returned
* See ``utils.helpers.get_state()``
* Will probably cause type warnings since the return type is ``Union[None, dict[str, Any]]``
* To avoid type warnings, use ``run_and_return_state``
* *non_blocking* (``bool``) --
* This is used to unit test debug mode of ``run()``, you likely don't have a need for this
* If ``True``, then ``run()`` won't block on input
* ``input()`` won't be called in debug mode (i.e., don't have to press enter to continue execution)
* If this is ``True``, then debug mode will be on even if ``debug`` isn't in kwargs
* *no_print* (``bool``) --
* This is used to save computation time during unit testing
* If ``True``, then ``print_RAM()`` and ``print_info()`` won't be called
* In debug mode, "Program halted." will still be printed
* *bits* (``int``) --
* You should not modify this
* Number of bits in registers
* Default value in ``global_vars`` is 8
* 8 is also the maximum value since everything in RAM should fit in a byte
:return: ``None`` or program state if ``return_state``
:rtype: ``Union[None, dict[str, Any]]``
"""
if not isinstance(prog_path, str):
raise TypeError("Required parameter prog_path must be a str.")
debug: bool = False
if "debug" in kwargs:
if not isinstance(kwargs["debug"], bool):
raise TypeError("Keyword argument debug must be a bool.")
debug = kwargs["debug"]
# Not initializing it causes some syntax warnings but better than initializing it
change: dict[int, int]
if "change" in kwargs:
change = kwargs["change"]
if not isinstance(change, dict):
raise TypeError("Keyword argument change must be a dict[int, int].")
if not all(isinstance(key, int) for key in change.keys()) or not all(
isinstance(value, int) for value in change.values()
):
raise TypeError("Keyword argument change must be a dict[int, int].")
if "table_format" in kwargs and not isinstance(kwargs["table_format"], str):
raise TypeError("Keyword argument table_format must be a str.")
if "return_state" in kwargs and not isinstance(kwargs["return_state"], bool):
raise TypeError("Keyword argument return_state must be a bool.")
if "non_blocking" in kwargs:
if not isinstance(kwargs["non_blocking"], bool):
raise TypeError("Keyword argument non_blocking must be a bool.")
debug = True
if "no_print" in kwargs and not isinstance(kwargs["no_print"], bool):
raise TypeError("Keyword argument no_print must be a bool.")
if "bits" in kwargs and not isinstance(kwargs["bits"], int):
raise TypeError("Keyword argument bits must be an int.")
path: Path = Path(prog_path)
if not path.suffix == ".csv":
raise exceptions.FileNotCSV(path)
parser.parse_csv(path)
if "bits" in kwargs:
assert kwargs["bits"] > 1 and kwargs["bits"] < 8
global_vars.NUM_BITS_IN_REGISTERS = kwargs["bits"]
# Don't need to call setup.
# All it does is change NUM_BITS_IN_REGISTERS and reset globals, which was already done by parse_csv.
unmapped_addrs_changed: list[int] = []
if "change" in kwargs:
for addr in change:
if addr not in global_vars.RAM:
unmapped_addrs_changed.append(int(addr))
if addr < 0:
raise exceptions.ChangeAddressNegative(addr)
if addr > global_vars.MAX_PC:
raise exceptions.ChangeAddressGreaterThan15(addr)
if (
change[addr] < 0
or change[addr] > 2**global_vars.NUM_BITS_IN_REGISTERS - 1
):
raise exceptions.ChangeValueInvalid(change[addr])
global_vars.RAM[addr] = change[addr]
if unmapped_addrs_changed:
print(
f"WARNING: You attempted to change the following address(es) not mapped in the CSV: {', '.join(list(map(str, unmapped_addrs_changed)))}.\nThis is likely unintentional, but they are now mapped, and the program will continue.",
file=sys.stderr,
)
if "table_format" in kwargs:
global_vars.table_format = kwargs["table_format"]
if debug:
if not kwargs.get("no_print"):
print(f"Initial state of simulation of {prog_path}")
helpers.print_RAM()
helpers.print_info()
print("Debug mode: press Enter to execute next instruction ( > ).")
if not kwargs.get("non_blocking"):
input()
while global_vars.EXECUTING:
# Special case so that you don't have to press Enter twice to halt on a HLT instruction
if (
global_vars.PC in global_vars.RAM
and helpers.parse_opcode(global_vars.RAM[global_vars.PC]) == 0xF
):
execute_next()
break
execute_next()
if not kwargs.get("no_print"):
helpers.print_RAM()
helpers.print_info()
if not kwargs.get("non_blocking"):
input()
print("Program halted.")
else:
execute_full_speed()
if not kwargs.get("no_print"):
helpers.print_RAM()
helpers.print_info()
if kwargs.get("return_state"):
return helpers.get_state()
[docs]
@is_documented_by(
run,
2,
"",
r"""
:return: ``dict`` containing program state (see ``helpers.get_state``)
:rtype: ``dict[str, Any]``
""",
)
def run_and_return_state(prog_path: str, **kwargs: Any) -> dict[str, Any]:
if (
"return_state" in kwargs
and isinstance(kwargs["return_state"], bool)
and not kwargs["return_state"]
):
print(
"You called run_and_return_state but set return_state to False????",
file=sys.stderr,
)
exit(1)
run(prog_path, **kwargs)
return helpers.get_state()