diff --git a/src/breakpoints.py b/src/breakpoints.py index 4b38a6f..bec311c 100644 --- a/src/breakpoints.py +++ b/src/breakpoints.py @@ -43,14 +43,17 @@ def should_break(self, ip: int, op_counter: int) -> bool: def get_address_str(self, address: int) -> str: if address in self.breakpoints and self.breakpoints[address] is not None: label_repr = get_nice_label_repr(self.breakpoints[address], pad=4) - return f'{hex(address)[2:]}:\n{label_repr}' + return f'{hex(address)}:\n{label_repr}' elif address in self.address_to_label: label_repr = get_nice_label_repr(self.address_to_label[address], pad=4) - return f'{hex(address)[2:]}:\n{label_repr}' + return f'{hex(address)}:\n{label_repr}' else: - address_before = max([a for a in self.address_to_label if a <= address]) - label_repr = get_nice_label_repr(self.address_to_label[address_before], pad=4) - return f'{hex(address)[2:]} ({hex(address - address_before)} after:)\n{label_repr}' + try: + address_before = max(a for a in self.address_to_label if a <= address) + label_repr = get_nice_label_repr(self.address_to_label[address_before], pad=4) + return f'{hex(address)} ({hex(address - address_before)} bits after:)\n{label_repr}' + except ValueError: + return f'{hex(address)}' def get_message_box_body(self, ip: int, mem: fjm.Reader, op_counter: int) -> str: address = self.get_address_str(ip) diff --git a/src/defs.py b/src/defs.py index b02fd69..7872b5a 100644 --- a/src/defs.py +++ b/src/defs.py @@ -3,10 +3,11 @@ import dataclasses import json import lzma +from collections import deque from enum import IntEnum # IntEnum equality works between files. from pathlib import Path from time import time -from typing import List, Dict +from typing import List, Dict, Deque from ops import CodePosition, Op @@ -22,14 +23,16 @@ def get_stl_paths() -> List[Path]: class TerminationCause(IntEnum): - Looping = 0 # Finished by jumping to the last op, without flipping it (the "regular" finish/exit) - EOF = 1 # Finished by reading input when there is no more input - NullIP = 2 # Finished by jumping back to the initial op 0 (bad finish) - UnalignedWord = 3 # FOR FUTURE SUPPORT - tried to access an unaligned word (bad finish) - UnalignedOp = 4 # FOR FUTURE SUPPORT - tried to access a dword-unaligned op (bad finish) + Looping = 0 # Finished by jumping to the last op, without flipping it (the "regular" finish/exit) + EOF = 1 # Finished by reading input when there is no more input + NullIP = 2 # Finished by jumping back to the initial op 0 (bad finish) + UnalignedWord = 3 # FOR FUTURE SUPPORT - tried to access an unaligned word (bad finish) + UnalignedOp = 4 # FOR FUTURE SUPPORT - tried to access a dword-unaligned op (bad finish) + RuntimeMemoryError = 5 # Finished by trying to read/write something out of the defined memory + # (probably a bug in the fj-program) def __str__(self) -> str: - return ['looping', 'EOF', 'ip<2w', 'unaligned-word', 'unaligned-op'][self.value] + return ['looping', 'EOF', 'ip<2w', 'unaligned-word', 'unaligned-op', 'runtime-memory-error'][self.value] macro_separator_string = "---" @@ -115,13 +118,14 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): self.paused_time += time() - self.pause_start_time - def __init__(self, w: int): + def __init__(self, w: int, *, number_of_saved_last_ops_addresses=10): self._op_size = 2 * w self._after_null_flip = 2 * w self.op_counter = 0 self.flip_counter = 0 self.jump_counter = 0 + self.last_ops_addresses: Deque[int] = deque(maxlen=number_of_saved_last_ops_addresses) self._start_time = time() self.pause_timer = self.PauseTimer() @@ -129,6 +133,9 @@ def __init__(self, w: int): def get_run_time(self) -> float: return time() - self._start_time - self.pause_timer.paused_time + def register_op_address(self, ip: int): + self.last_ops_addresses.append(ip) + def register_op(self, ip: int, flip_address: int, jump_address: int) -> None: self.op_counter += 1 if flip_address >= self._after_null_flip: diff --git a/src/exceptions.py b/src/exceptions.py index 8a098d5..78840b4 100644 --- a/src/exceptions.py +++ b/src/exceptions.py @@ -24,3 +24,7 @@ class FJReadFjmException(FJException): class FJWriteFjmException(FJException): pass + + +class FJRuntimeMemoryException(FJException): + pass diff --git a/src/fj.py b/src/fj.py index 8d68228..dcd77b6 100644 --- a/src/fj.py +++ b/src/fj.py @@ -119,7 +119,7 @@ def run(in_fjm_path: Path, debug_file: Path, args: argparse.Namespace, error_fun breakpoint_handler=breakpoint_handler ) if not args.silent: - print(termination_statistics) + termination_statistics.print(labels_handler=breakpoint_handler) except FJReadFjmException as e: print() print(e) diff --git a/src/fjm.py b/src/fjm.py index ca34627..f25b3e3 100644 --- a/src/fjm.py +++ b/src/fjm.py @@ -7,8 +7,7 @@ import lzma -from exceptions import FJReadFjmException, FJWriteFjmException - +from exceptions import FJReadFjmException, FJWriteFjmException, FJRuntimeMemoryException """ struct { @@ -174,7 +173,7 @@ def _get_memory_word(self, word_address: int) -> int: garbage_message = f'Reading garbage word at mem[{hex(word_address << self.w)[2:]}] = {hex(garbage_val)[2:]}' if GarbageHandling.Stop == self.garbage_handling: - raise FJReadFjmException(garbage_message) + raise FJRuntimeMemoryException(garbage_message) elif GarbageHandling.OnlyWarning == self.garbage_handling: print(f'\nWarning: {garbage_message}') elif GarbageHandling.SlowRead == self.garbage_handling: @@ -232,7 +231,7 @@ def get_word(self, bit_address: int) -> int: if bit_offset == 0: return self._get_memory_word(word_address) if word_address == ((1 << self.w) - 1): - raise FJReadFjmException(f'Accessed outside of memory (beyond the last bit).') + raise FJRuntimeMemoryException(f'Accessed outside of memory (beyond the last bit).') lsw = self._get_memory_word(word_address) msw = self._get_memory_word(word_address + 1) diff --git a/src/fjm_run.py b/src/fjm_run.py index f81d725..baea45c 100644 --- a/src/fjm_run.py +++ b/src/fjm_run.py @@ -1,10 +1,11 @@ from pathlib import Path -from typing import Optional +from typing import Optional, Deque import fjm from defs import TerminationCause, PrintTimer, RunStatistics from breakpoints import BreakpointHandler, handle_breakpoint +from exceptions import FJRuntimeMemoryException from io_devices.IODevice import IODevice from io_devices.BrokenIO import BrokenIO @@ -22,18 +23,42 @@ def __init__(self, run_statistics: RunStatistics, termination_cause: Termination self.op_counter = run_statistics.op_counter self.flip_counter = run_statistics.flip_counter self.jump_counter = run_statistics.jump_counter + self.last_ops_addresses: Deque[int] = run_statistics.last_ops_addresses self.termination_cause = termination_cause - def __str__(self): + @staticmethod + def beautify_address(address: int, breakpoint_handler: Optional[BreakpointHandler]): + if not breakpoint_handler: + return hex(address) + + return breakpoint_handler.get_address_str(address) + + def print(self, *, labels_handler: Optional[BreakpointHandler] = None): + """ + Prints the termination cause, run times, ops-statistics. + If ended not by looping - Then print the last-opcodes` addresses as well (and their label names if possible). + @param labels_handler: Used to find the label name for each address (from the last-opcodes` addresses). + """ + flips_percentage = self.flip_counter / self.op_counter * 100 jumps_percentage = self.jump_counter / self.op_counter * 100 - return f'Finished by {str(self.termination_cause)} after {self.run_time:.3f}s ' \ - f'(' \ - f'{self.op_counter:,} ops executed; ' \ - f'{flips_percentage:.2f}% flips, ' \ - f'{jumps_percentage:.2f}% jumps' \ - f').' + + last_ops_str = '' + if TerminationCause.Looping != self.termination_cause: + last_ops_str = f'\n\nLast {len(self.last_ops_addresses)} ops were at these addresses ' \ + f'(The most-recent op, the one that failed, is first):\n ' + \ + '\n '.join([self.beautify_address(address, labels_handler) + for address in self.last_ops_addresses][::-1]) + + print(f'Finished by {str(self.termination_cause)} after {self.run_time:.3f}s ' + f'(' + f'{self.op_counter:,} ops executed; ' + f'{flips_percentage:.2f}% flips, ' + f'{jumps_percentage:.2f}% jumps' + f').' + f'{last_ops_str}' + ) def handle_input(io_device: IODevice, ip: int, mem: fjm.Reader, statistics: RunStatistics) -> None: @@ -89,35 +114,41 @@ def run(fjm_path: Path, *, statistics = RunStatistics(w) - while True: - # handle breakpoints - if breakpoint_handler and breakpoint_handler.should_break(ip, statistics.op_counter): - breakpoint_handler = handle_breakpoint(breakpoint_handler, ip, mem, statistics) - - # read flip word - flip_address = mem.get_word(ip) - trace_flip(ip, flip_address, show_trace) - - # handle IO - handle_output(flip_address, io_device, w) - try: - handle_input(io_device, ip, mem, statistics) - except IOReadOnEOF: - return TerminationStatistics(statistics, TerminationCause.EOF) - - # FLIP! - mem.write_bit(flip_address, not mem.read_bit(flip_address)) - - # read jump word - jump_address = mem.get_word(ip+w) - trace_jump(jump_address, show_trace) - statistics.register_op(ip, flip_address, jump_address) - - # check finish? - if jump_address == ip and not ip <= flip_address < ip+2*w: - return TerminationStatistics(statistics, TerminationCause.Looping) - if jump_address < 2*w: - return TerminationStatistics(statistics, TerminationCause.NullIP) - - # JUMP! - ip = jump_address + try: + while True: + statistics.register_op_address(ip) + + # handle breakpoints + if breakpoint_handler and breakpoint_handler.should_break(ip, statistics.op_counter): + breakpoint_handler = handle_breakpoint(breakpoint_handler, ip, mem, statistics) + + # read flip word + flip_address = mem.get_word(ip) + trace_flip(ip, flip_address, show_trace) + + # handle IO + handle_output(flip_address, io_device, w) + try: + handle_input(io_device, ip, mem, statistics) + except IOReadOnEOF: + return TerminationStatistics(statistics, TerminationCause.EOF) + + # FLIP! + mem.write_bit(flip_address, not mem.read_bit(flip_address)) + + # read jump word + jump_address = mem.get_word(ip+w) + trace_jump(jump_address, show_trace) + statistics.register_op(ip, flip_address, jump_address) + + # check finish? + if jump_address == ip and not ip <= flip_address < ip+2*w: + return TerminationStatistics(statistics, TerminationCause.Looping) + if jump_address < 2*w: + return TerminationStatistics(statistics, TerminationCause.NullIP) + + # JUMP! + ip = jump_address + + except FJRuntimeMemoryException: + return TerminationStatistics(statistics, TerminationCause.RuntimeMemoryError) diff --git a/src/io_devices/FixedIO.py b/src/io_devices/FixedIO.py index e53e072..2f27296 100644 --- a/src/io_devices/FixedIO.py +++ b/src/io_devices/FixedIO.py @@ -39,12 +39,12 @@ def write_bit(self, bit: bool) -> None: self.current_output_byte = 0 self.bits_to_write_in_output_byte = 0 - def get_output(self) -> bytes: + def get_output(self, *, allow_incomplete_output=False) -> bytes: """ @raise IncompleteOutput when the number of outputted bits can't be divided by 8 @return: full output until now """ - if 0 != self.bits_to_write_in_output_byte: + if not allow_incomplete_output and 0 != self.bits_to_write_in_output_byte: raise IncompleteOutput("tries to get output when an unaligned number of bits was outputted " "(doesn't divide 8)") diff --git a/src/io_devices/StandardIO.py b/src/io_devices/StandardIO.py index 278086d..a355e77 100644 --- a/src/io_devices/StandardIO.py +++ b/src/io_devices/StandardIO.py @@ -51,8 +51,8 @@ def write_bit(self, bit: bool) -> None: self.current_output_byte = 0 self.bits_to_write_in_output_byte = 0 - def get_output(self) -> bytes: - if 0 != self.bits_to_write_in_output_byte: + def get_output(self, *, allow_incomplete_output=False) -> bytes: + if not allow_incomplete_output and 0 != self.bits_to_write_in_output_byte: raise IncompleteOutput("tries to get output when an unaligned number of bits was outputted " "(doesn't divide 8)") diff --git a/tests/conftest.py b/tests/conftest.py index bca8656..76dddef 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -40,6 +40,7 @@ RUN_ORDER_INDEX = 2 +NO_DEBUG_INFO_FLAG = 'nodebuginfo' ALL_FLAG = 'all' REGULAR_FLAG = 'regular' COMPILE_FLAG = 'compile' @@ -77,31 +78,33 @@ def argument_line_iterator(csv_file_path: Path, num_of_args: int) -> Iterable[Li yield map(str.strip, line) -def get_compile_tests_params_from_csv(csv_file_path: Path) -> List: +def get_compile_tests_params_from_csv(csv_file_path: Path, save_debug_info: bool) -> List: """ read the compile-tests from the csv @param csv_file_path: read tests from this csv + @param save_debug_info: should save the debugging info file @return: the list of pytest.params(CompileTestArgs, ) """ params = [] - for line in argument_line_iterator(csv_file_path, CompileTestArgs.num_of_args): - args = CompileTestArgs(*line) + for line in argument_line_iterator(csv_file_path, CompileTestArgs.num_of_csv_line_args): + args = CompileTestArgs(*line, save_debug_info) params.append(pytest.param(args, marks=pytest.mark.run(order=COMPILE_ORDER_INDEX))) return params -def get_run_tests_params_from_csv(csv_file_path: Path) -> List: +def get_run_tests_params_from_csv(csv_file_path: Path, use_debug_info: bool) -> List: """ read the run-tests from the csv @param csv_file_path: read tests from this csv + @param use_debug_info: should use the debugging info file @return: the list of pytest.params(RunTestArgs, depends=) """ params = [] - for line in argument_line_iterator(csv_file_path, RunTestArgs.num_of_args): - args = RunTestArgs(*line) + for line in argument_line_iterator(csv_file_path, RunTestArgs.num_of_csv_line_args): + args = RunTestArgs(*line, use_debug_info) params.append(pytest.param(args, marks=pytest.mark.run(order=RUN_ORDER_INDEX))) return params @@ -115,10 +118,15 @@ def pytest_addoption(parser) -> None: colliding_keywords = set(TEST_TYPES) & SAVED_KEYWORDS assert not colliding_keywords + parser.addoption(f"--{NO_DEBUG_INFO_FLAG}", action="store_true", + help="don't show the last executed opcodes on tests that failed during their run" + "(thus the tests are ~15% faster, and takes ~half the size)." + "Anyway doesn't show last executed opcodes on parallel tests.") + for test_type in TEST_TYPES: parser.addoption(f"--{test_type}", action="store_true", help=f"run {test_type} tests") parser.addoption(f"--{REGULAR_FLAG}", action="store_true", help=f"run all regular tests ({', '.join(REGULAR_TYPES)})") - parser.addoption(f"--{ALL_FLAG}", action="store_true", help=f"run all tests") + parser.addoption(f"--{ALL_FLAG}", action="store_true", help="run all tests") parser.addoption(f"--{COMPILE_FLAG}", action='store_true', help='only test compiling .fj files') parser.addoption(f"--{RUN_FLAG}", action='store_true', help='only test running .fjm files') @@ -295,12 +303,14 @@ def get_tests_from_csvs(get_option: Callable[[str], Any]) -> Tuple[List, List]: types_to_run__heavy_first = get_test_types_to_run__heavy_first(get_option) + use_debug_info = not is_parallel_active() and not get_option(NO_DEBUG_INFO_FLAG) + compile_tests = [] if check_compile_tests: compiles_csvs = {test_type: TESTS_PATH / f"test_compile_{test_type}.csv" for test_type in types_to_run__heavy_first} for test_type in types_to_run__heavy_first: - compile_tests.extend(get_compile_tests_params_from_csv(compiles_csvs[test_type])) + compile_tests.extend(get_compile_tests_params_from_csv(compiles_csvs[test_type], use_debug_info)) compile_tests = filter_by_test_name(compile_tests, get_option) run_tests = [] @@ -308,7 +318,7 @@ def get_tests_from_csvs(get_option: Callable[[str], Any]) -> Tuple[List, List]: run_csvs = {test_type: TESTS_PATH / f"test_run_{test_type}.csv" for test_type in types_to_run__heavy_first} for test_type in types_to_run__heavy_first: - run_tests.extend(get_run_tests_params_from_csv(run_csvs[test_type])) + run_tests.extend(get_run_tests_params_from_csv(run_csvs[test_type], use_debug_info)) run_tests = filter_by_test_name(run_tests, get_option) return compile_tests, run_tests diff --git a/tests/test_fj.py b/tests/test_fj.py index 8005a25..f4e18b0 100644 --- a/tests/test_fj.py +++ b/tests/test_fj.py @@ -3,6 +3,7 @@ from threading import Lock from pathlib import Path +from breakpoints import BreakpointHandler, load_labels_dictionary from src import assembler, fjm from src import fjm_run from src.defs import TerminationCause, get_stl_paths, io_bytes_encoding @@ -13,6 +14,9 @@ CSV_BOOLEAN = (CSV_TRUE, CSV_FALSE) +DEBUGGING_FILE_SUFFIX = '.fj_debugging_info' + + ROOT_PATH = Path(__file__).parent.parent @@ -25,11 +29,12 @@ class CompileTestArgs: Arguments class for a compile test """ - num_of_args = 8 + num_of_csv_line_args = 8 def __init__(self, test_name: str, fj_paths: str, fjm_out_path: str, word_size__str: str, version__str: str, flags__str: str, - use_stl__str: str, warning_as_errors__str: str): + use_stl__str: str, warning_as_errors__str: str, + save_debug_info: bool): """ handling a line.split() from a csv file """ @@ -38,6 +43,8 @@ def __init__(self, test_name: str, fj_paths: str, fjm_out_path: str, self.use_stl = use_stl__str == CSV_TRUE self.warning_as_errors = warning_as_errors__str == CSV_TRUE + self.save_debug_info = save_debug_info + self.test_name = test_name included_files = get_stl_paths() if self.use_stl else [] @@ -77,8 +84,14 @@ def test_compile(compile_args: CompileTestArgs) -> None: fjm_writer = fjm.Writer(compile_args.fjm_out_path, compile_args.word_size, compile_args.version, flags=compile_args.flags, lzma_preset=lzma.PRESET_DEFAULT) + + debugging_file_path = None + if compile_args.save_debug_info: + debugging_file_path = Path(f'{compile_args.fjm_out_path}{DEBUGGING_FILE_SUFFIX}') + assembler.assemble(compile_args.fj_files_tuples, compile_args.word_size, fjm_writer, - warning_as_errors=compile_args.warning_as_errors) + warning_as_errors=compile_args.warning_as_errors, + debugging_file_path=debugging_file_path) class RunTestArgs: @@ -86,11 +99,12 @@ class RunTestArgs: Arguments class for a run test """ - num_of_args = 6 + num_of_csv_line_args = 6 def __init__(self, test_name: str, fjm_path: str, in_file_path: str, out_file_path: str, - read_in_as_binary__str: str, read_out_as_binary__str: str): + read_in_as_binary__str: str, read_out_as_binary__str: str, + use_debug_info: bool): """ @note handling a line.split() (each is stripped) from a csv file """ @@ -99,6 +113,8 @@ def __init__(self, test_name: str, fjm_path: str, self.read_in_as_binary = read_in_as_binary__str == CSV_TRUE self.read_out_as_binary = read_out_as_binary__str == CSV_TRUE + self.use_debug_info = use_debug_info + self.test_name = test_name self.fjm_path = ROOT_PATH / fjm_path @@ -154,11 +170,17 @@ def test_run(run_args: RunTestArgs) -> None: print(f'Running test {run_args.test_name}:') io_device = FixedIO(run_args.get_defined_input()) + + breakpoint_handler = None + if run_args.use_debug_info: + label_to_address = load_labels_dictionary(Path(f'{run_args.fjm_path}{DEBUGGING_FILE_SUFFIX}'), True) + breakpoint_handler = BreakpointHandler({}, {label_to_address[label]: label for label in label_to_address}) + termination_statistics = fjm_run.run(run_args.fjm_path, io_device=io_device, time_verbose=True) - print(termination_statistics) + termination_statistics.print(labels_handler=breakpoint_handler) expected_termination_cause = TerminationCause.Looping assert termination_statistics.termination_cause == expected_termination_cause