diff options
| -rw-r--r-- | Makefile | 5 | ||||
| -rw-r--r-- | atk16_asm/asm_pass0.py | 7 | ||||
| -rw-r--r-- | atk16_asm/asm_pass3.py | 35 | ||||
| -rw-r--r-- | atk16_asm/asm_pass4.py | 9 | ||||
| -rwxr-xr-x | atk16_asm/assembler.py | 36 | ||||
| -rw-r--r-- | atk16_emu/cli.py | 11 | ||||
| -rw-r--r-- | atk16_emu/debugger.py | 107 |
7 files changed, 179 insertions, 31 deletions
@@ -48,10 +48,7 @@ test: pytest --ignore resources install: - mkdir -p "$(PREFIX)/share/atk16/asm" - cp atk16_asm/*.py "$(PREFIX)/share/atk16/asm/" - printf "#!/bin/sh\ncd \"$(PREFIX)/share/atk16\"\n/usr/bin/env python -m asm.assembler \$$@\n" > "$(PREFIX)/bin/atk16c" - chmod +x "$(PREFIX)/bin/atk16c" + pip install . run-single-test: java -cp $(digital_path) CLI test -verbose -circ $(circ) -tests $(tests) diff --git a/atk16_asm/asm_pass0.py b/atk16_asm/asm_pass0.py index 262e5c1..f6586cd 100644 --- a/atk16_asm/asm_pass0.py +++ b/atk16_asm/asm_pass0.py @@ -15,9 +15,12 @@ class Result0: lines: list[Result0Line] def pass_0(lines: list[str], file_name: str) -> Result0: + file_name = file_name if file_name.endswith(".atk16") else file_name + ".atk16" result_lines: list[Result0Line] = [] for (line_num, line) in enumerate(lines): + line_num += 1 # line numbers are 1-based + line = line.split(";")[0].strip() if line == "": continue @@ -25,8 +28,8 @@ def pass_0(lines: list[str], file_name: str) -> Result0: match keyword: case "@include": - asm_file_name = args[0] - path = os.path.join(os.path.dirname(file_name), asm_file_name + ".atk16") + asm_file_name = args[0] if args[0].endswith(".atk16") else args[0] + ".atk16" + path = os.path.join(os.path.dirname(file_name), asm_file_name) with open(path, "r") as f: incl_lines = f.readlines() diff --git a/atk16_asm/asm_pass3.py b/atk16_asm/asm_pass3.py index 09f58fa..ec8b20c 100644 --- a/atk16_asm/asm_pass3.py +++ b/atk16_asm/asm_pass3.py @@ -12,15 +12,43 @@ class Result3Line: original_parts: list[str] @dataclass +class DbgSourceInfo: + src_file: str + line_num: int + expanded_text: str + original_text: str + +DbgLabelTable = dict[int, list[str]] +DbgSourceTable = dict[int, DbgSourceInfo] + +@dataclass class Result3: lines: list[Result3Line] operations: OpExpansionDict symbols: dict[str, int] + dbg_label_table: DbgLabelTable + dbg_source_table: DbgSourceTable def pass_3(result2: Result2) -> Result3: result_lines: list[Result3Line] = [] symbols: dict[str, int] = {} address = 0 + dbg_label_table: DbgLabelTable = {} + dbg_source_table: DbgSourceTable = {} + + def save_dbg_label(addr: int, label: str): + if addr in dbg_label_table: + dbg_label_table[addr].append(label) + else: + dbg_label_table[addr] = [label] + + def save_dbg_source(addr: int, src_file: str, line_num: int, expanded_text: str, original_text: str): + dbg_source_table[addr] = DbgSourceInfo( + src_file=src_file, + line_num=line_num, + expanded_text=expanded_text, + original_text=original_text, + ) for line in result2.lines: keyword, *args = line.parts @@ -33,11 +61,15 @@ def pass_3(result2: Result2) -> Result3: if label in symbols: raise Exception(f"When defining label {label} as {address:>04x}, symbol {label} already defined as {symbols[label]:>04x}") symbols[args[0]] = address + save_dbg_label(address, label) continue case "@let": symbols[args[0]] = eval_expr(symbols, args[1]) continue case _: + dbg_original_text = " ".join(line.original_parts) + dbg_expanded_text = " ".join(line.parts) + save_dbg_source(address, line.src_file, line.line_num, dbg_expanded_text, dbg_original_text) result_lines.append(Result3Line( line_num=line.line_num, src_file=line.src_file, @@ -54,10 +86,13 @@ def pass_3(result2: Result2) -> Result3: if n > 1: formatted = format_overlapping_rows(rows_with_same_addr) raise Exception(f"Overlapping segments: address 0x{result_line.address:>04x} has conflicting definitions:\n{formatted}") + return Result3( operations=result2.operations, lines=result_lines, symbols=symbols, + dbg_label_table=dbg_label_table, + dbg_source_table=dbg_source_table, ) def format_overlapping_rows(rows: list[Result3Line]) -> str: diff --git a/atk16_asm/asm_pass4.py b/atk16_asm/asm_pass4.py index 7365416..52aaf68 100644 --- a/atk16_asm/asm_pass4.py +++ b/atk16_asm/asm_pass4.py @@ -17,6 +17,8 @@ class Result4: lines: list[Result4Line] operations: OpExpansionDict symbols: dict[str, int] + dbg_label_table: DbgLabelTable + dbg_source_table: DbgSourceTable def pass_4(result3: Result3) -> Result4: result_lines: list[Result4Line] = [] @@ -32,7 +34,10 @@ def pass_4(result3: Result3) -> Result4: if keyword in operations: fn = operations[keyword] - word = fn(meta, result3.symbols, *args) + try: + word = fn(meta, result3.symbols, *args) + except Exception as e: + raise Exception(f"Invalid assembly at {line.src_file}:{line.line_num + 1}\n\n{line.parts}\n\n{e}") from e result_lines.append(Result4Line( line_num=line.line_num, src_file=line.src_file, @@ -58,4 +63,6 @@ def pass_4(result3: Result3) -> Result4: operations=result3.operations, symbols=result3.symbols, lines=result_lines, + dbg_label_table=result3.dbg_label_table, + dbg_source_table=result3.dbg_source_table, ) diff --git a/atk16_asm/assembler.py b/atk16_asm/assembler.py index 993472e..4cd2cef 100755 --- a/atk16_asm/assembler.py +++ b/atk16_asm/assembler.py @@ -7,11 +7,17 @@ from .asm_eval import * from .asm_pass0 import pass_0 from .asm_pass1 import pass_1 from .asm_pass2 import pass_2 -from .asm_pass3 import pass_3 +from .asm_pass3 import pass_3, DbgLabelTable, DbgSourceTable from .asm_pass4 import pass_4 from .pad_bin import pad_binary -def assemble(source: str, file_name: str) -> bytearray: +@dataclass +class AssemblyResult: + program: bytearray + dbg_label_table: DbgLabelTable + dbg_source_table: DbgSourceTable + +def assemble(source: str, file_name: str) -> AssemblyResult: src_lines = source.splitlines() result0 = pass_0(src_lines, file_name) @@ -40,7 +46,11 @@ def assemble(source: str, file_name: str) -> bytearray: result[2 * line.address + 0] = ((line.word >> 8) & 0xff) result[2 * line.address + 1] = ((line.word >> 0) & 0xff) - return result + return AssemblyResult( + program=result, + dbg_label_table=result4.dbg_label_table, + dbg_source_table=result4.dbg_source_table, + ) def main(): if len(sys.argv) != 3: @@ -62,11 +72,27 @@ def main(): result = assemble(src, infile_path) with open(outfile_path, "wb") as f: - f.write(result) + f.write(result.program) pad_binary(outfile_path, 64 * 1024) - print(f"Wrote 64 KB to {outfile_path} ({len(result)} B without padding)") + print(f"Wrote 64 KB to {outfile_path} ({len(result.program)} B without padding)") + + dbg_outfile_path = outfile_path + ".dbg" + + with open(dbg_outfile_path, "w") as f: + for i in range(32 * 1024): # 32K words + line = f"0x{i:>04x};" + if i not in result.dbg_source_table: + continue + + dbg_source_info = result.dbg_source_table[i] + line += f"{dbg_source_info.src_file}:{dbg_source_info.line_num};{dbg_source_info.expanded_text};{dbg_source_info.original_text};" + + if i in result.dbg_label_table: + line += f"{';'.join(result.dbg_label_table[i])}" + + f.write(line + "\n") if __name__ == "__main__": main() diff --git a/atk16_emu/cli.py b/atk16_emu/cli.py index 824fbbf..fcea56b 100644 --- a/atk16_emu/cli.py +++ b/atk16_emu/cli.py @@ -39,13 +39,10 @@ def main(): print_help() sys.exit(1) - def load_rom_image_from_path(path: str) -> bytearray: - with open(path, "rb") as f: - return bytearray(f.read()) - - rom_image = load_rom_image_from_path(options.rom_image_path) - if not options.debugger_enabled: + with open(options.rom_image_path, "rb") as f: + rom_image = bytearray(f.read()) + machine = Machine() machine.load_rom_image(rom_image) machine.reset() @@ -58,7 +55,7 @@ def main(): else: debugger = Debugger() - debugger.load_rom_image(rom_image) + debugger.load_rom_image(options.rom_image_path) debugger.activate() if __name__ == "__main__": diff --git a/atk16_emu/debugger.py b/atk16_emu/debugger.py index 3d8f131..a1fd470 100644 --- a/atk16_emu/debugger.py +++ b/atk16_emu/debugger.py @@ -1,5 +1,13 @@ from getch import getche from .emu import Machine +from dataclasses import dataclass + +@dataclass +class DbgAddrInfo: + src_info: str + text: str + original_text: str + labels: list[str] class Debugger: def __init__(self): @@ -8,12 +16,50 @@ class Debugger: self.machine = Machine() self.machine.reset() self.machine.run() + self.dbg_addr_info: dict[int, DbgAddrInfo] = {} + + def load_rom_image(self, rom_image_path: str) -> None: + self.rom_image_path = rom_image_path + with open(rom_image_path, "rb") as f: + self.rom_image = bytearray(f.read()) + + self.machine.load_rom_image(self.rom_image) + + print(f"Loaded ROM image from \"{rom_image_path}\".") + + dbg_symbols_path = rom_image_path + ".dbg" + try: + with open(dbg_symbols_path, "r") as f: + dbg_file_lines = f.readlines() + except FileNotFoundError: + print(f"Note: debug symbols file \"{dbg_symbols_path}\" not found.") + return + + self.dbg_addr_info = {} + self.dbg_text_col_width = 0 + self.dbg_original_text_col_width = 0 + for line in dbg_file_lines: + addr, src_info, text, original_text, *labels_ = line.split(";") + addr = int(addr, 16) + + if len(text) > self.dbg_text_col_width: + self.dbg_text_col_width = len(text) - def load_rom_image(self, rom_image: bytearray) -> None: - self.rom_image = rom_image - self.machine.load_rom_image(rom_image) + if len(original_text) > self.dbg_original_text_col_width: + self.dbg_original_text_col_width = len(original_text) - print("Loaded rom image.") + labels = [] + for label in labels_: + label = label.strip() + if label != "": + labels.append(label) + + self.dbg_addr_info[addr] = DbgAddrInfo( + src_info=src_info, + text=text, + original_text=original_text, + labels=labels, + ) def print_pc_context(self): print("=== Program context") @@ -23,9 +69,16 @@ class Debugger: continue if i == 0: - print(f"> 0x{addr:>04x}: 0x{self.machine.mem_read(addr):>04x}") + print(f"> 0x{addr:>04x}: 0x{self.machine.mem_read(addr):>04x}", end="") + else: + print(f" 0x{addr:>04x}: 0x{self.machine.mem_read(addr):>04x}", end="") + + if addr in self.dbg_addr_info: + dbg_info = self.dbg_addr_info[addr] + labels: str = f"[{' '.join(dbg_info.labels)}] " if len(dbg_info.labels) > 0 else "" + print(f" {dbg_info.text:<{self.dbg_text_col_width}} {dbg_info.original_text:<{self.dbg_original_text_col_width}} {labels}({dbg_info.src_info})") else: - print(f" 0x{addr:>04x}: 0x{self.machine.mem_read(addr):>04x}") + print() print() @@ -42,13 +95,32 @@ class Debugger: if cmd == "q": break + elif cmd == "?": self.print_help() + + elif cmd == "l": + load_path = input(f"Path to rom image [default: {self.rom_image_path}]: ") + if load_path.strip() == "": + load_path = self.rom_image_path + + self.load_rom_image(load_path) + + self.machine.reset() + self.machine.run() + print("Machine reset.") + elif cmd == "r": while self.machine.running: self.machine.step() if self.machine.pc.value in self.breakpoints: - print(f"Breakpoint hit at 0x{self.machine.pc.value:>04x}") + for addr in self.breakpoints: + if addr in self.dbg_addr_info and len(self.dbg_addr_info[addr].labels) > 0: + label_suffix = f" [{' '.join(self.dbg_addr_info[addr].labels)}]" + else: + label_suffix = "" + + print(f"Breakpoint hit at 0x{self.machine.pc.value:>04x}{label_suffix}") break if not self.machine.running: @@ -57,13 +129,21 @@ class Debugger: elif cmd == "b": print("Set breakpoints:") for addr in self.breakpoints: - print(f" 0x{addr:>04x}") + if addr in self.dbg_addr_info and len(self.dbg_addr_info[addr].labels) > 0: + label_suffix = f" [{' '.join(self.dbg_addr_info[addr].labels)}]" + else: + label_suffix = "" + print(f" 0x{addr:>04x}{label_suffix}") if len(self.breakpoints) == 0: print("<no breakpoints>") print() try: - addr = eval(input("Breakpoint address: "), {}) + env: dict[str, int] = {} + for addr in self.dbg_addr_info: + for label in self.dbg_addr_info[addr].labels: + env[label] = addr + addr = eval(input("Breakpoint address: "), env) except: print("Cancelled") continue @@ -75,21 +155,23 @@ class Debugger: print(f"Removed breakpoint at 0x{addr:>04x}") else: self.breakpoints.add(addr) + elif cmd == "n": if not self.machine.running: print("Machine halted.") continue - self.machine.step() - if not self.machine.running: print("Machine halted.") + elif cmd == "s": self.machine.print_state_summary() + elif cmd == "0": self.machine.reset() self.machine.run() print("Machine reset.") + else: print(f"Unknown command: {cmd}") @@ -100,7 +182,8 @@ class Debugger: print(" b step backward") print(" b set or remove breakpoint") print(" s show state summary") - print(" 0 reset the machine state") + print(" 0 reset machine state") + print(" l load ROM image and reset machine state") print(" q quit") print(" ? show this help") print() |
