From d96930518619265a4d76e38a4d3aaba32bcc7c2e Mon Sep 17 00:00:00 2001 From: sql-hkr Date: Sun, 18 Jan 2026 21:14:32 +0900 Subject: [PATCH 1/5] feat: expand to 64KB memory space and synchronize AVR instruction set --- examples/bubblesort.asm | 4 +- examples/count_bits.asm | 5 +- examples/factorial.asm | 13 +- examples/gcd.asm | 27 +- examples/is_prime.asm | 39 +- examples/multiply_by_shift.asm | 12 +- examples/power.asm | 19 +- src/tiny8/assembler.py | 134 +-- src/tiny8/cpu.py | 1539 +++++++++++++++++++------------- src/tiny8/memory.py | 173 ++-- tests/test_arithmetic.py | 104 +-- tests/test_assembler.py | 9 + tests/test_control_flow.py | 64 +- tests/test_cpu_branches.py | 347 ------- tests/test_cpu_core.py | 72 +- tests/test_cpu_edge_cases.py | 249 +++++- tests/test_data_transfer.py | 27 + tests/test_memory.py | 110 +++ 18 files changed, 1637 insertions(+), 1310 deletions(-) delete mode 100644 tests/test_cpu_branches.py diff --git a/examples/bubblesort.asm b/examples/bubblesort.asm index 0aba750..3da2278 100644 --- a/examples/bubblesort.asm +++ b/examples/bubblesort.asm @@ -11,6 +11,7 @@ init_loop: ; Generate random byte: seed = (seed * 75) + 1 mul r18, r25 + mov r18, r0 inc r18 st r16, r18 ; Store random value @@ -39,9 +40,10 @@ inner_loop: add r22, r23 ld r24, r22 ; r24 = B - ; Swap if A > B (ascending order) + ; Swap if A > B (ascending order: smallest first) cp r21, r24 brcs no_swap ; Skip if A < B + breq no_swap ; Skip if A == B st r20, r24 ; RAM[addr_A] = B st r22, r21 ; RAM[addr_B] = A diff --git a/examples/count_bits.asm b/examples/count_bits.asm index 59fc4cc..f3959c1 100644 --- a/examples/count_bits.asm +++ b/examples/count_bits.asm @@ -20,9 +20,8 @@ loop: inc r16 no_inc: - ; shift right (divide by 2) - ldi r20, 2 - div r18, r20 + ; shift right (logical shift right by 1 bit) + lsr r18 jmp loop diff --git a/examples/factorial.asm b/examples/factorial.asm index 7ba28af..7f0c279 100644 --- a/examples/factorial.asm +++ b/examples/factorial.asm @@ -2,22 +2,23 @@ ; Output: R16 = 120 start: - ldi r17, 5 ; n = 5 + ldi r18, 5 ; n = 5 ldi r16, 1 ; result = 1 ; if n <= 1, done - cpi r17, 2 + cpi r18, 2 brlt done loop: - ; result *= n - mul r16, r17 + ; result *= n (stores product in R1:R0 per AVR specification) + mul r16, r18 + mov r16, r0 ; n-- - dec r17 + dec r18 ; continue if n > 1 - cpi r17, 2 + cpi r18, 2 brge loop done: diff --git a/examples/gcd.asm b/examples/gcd.asm index c0a5a63..4d8e9fd 100644 --- a/examples/gcd.asm +++ b/examples/gcd.asm @@ -4,25 +4,18 @@ start: ldi r16, 48 ; a = 48 - ldi r17, 18 ; b = 18 - - cpi r17, 0 - breq done + ldi r18, 18 ; b = 18 loop: - ; Compute remainder: a % b - mov r18, r16 ; Save a - div r16, r17 ; a / b - mul r16, r17 ; (a / b) * b - mov r19, r18 - sub r19, r16 ; remainder = a - (a/b)*b - - ; Shift: a = b, b = remainder - mov r16, r17 - mov r17, r19 - - cpi r17, 0 - brne loop + cp r16, r18 ; compare a and b + breq done ; if a == b, gcd found in R16 + brlt less ; if a < b, branch + sub r16, r18 ; a = a - b + jmp loop + +less: + sub r18, r16 ; b = b - a + jmp loop done: jmp done diff --git a/examples/is_prime.asm b/examples/is_prime.asm index bc36f07..670adff 100644 --- a/examples/is_prime.asm +++ b/examples/is_prime.asm @@ -14,30 +14,35 @@ start: cpi r17, 2 breq is_prime - ; test divisibility: check divisors from 2 to n/2 - ldi r18, 2 ; divisor = 2 (start with smallest prime) + ; test divisibility: check divisors from 2 upwards + ldi r18, 2 ; divisor = 2 check_loop: - ; optimization: if divisor * 2 > n, no need to continue - mov r19, r18 - ldi r20, 2 - mul r19, r20 ; r19 = divisor * 2 - cp r19, r17 ; compare with n - brge is_prime ; if divisor * 2 > n, number is prime - ; check if n is divisible by current divisor: n % divisor == 0 mov r19, r17 - div r19, r18 ; r19 = n / divisor (quotient) - mul r19, r18 ; r19 = quotient * divisor - mov r20, r17 - sub r20, r19 ; r20 = n - (quotient * divisor) = remainder - - ; if remainder == 0, n is divisible (not prime) - cpi r20, 0 + +mod_loop: + cp r19, r18 + brlt mod_done + sub r19, r18 + jmp mod_loop + +mod_done: + ; if remainder in r19 == 0, n is divisible (not prime) + cpi r19, 0 breq not_prime ; try next divisor - inc r18 ; divisor++ + inc r18 + + ; optimization: if divisor * 2 > n, no need to continue (number is prime) + mov r19, r18 + ldi r20, 2 + mul r19, r20 ; product in R1:R0 per AVR specification + mov r19, r0 ; r19 = divisor * 2 + cp r17, r19 ; compare n with divisor * 2 + brlt is_prime ; if n < divisor * 2, number is prime + jmp check_loop is_prime: diff --git a/examples/multiply_by_shift.asm b/examples/multiply_by_shift.asm index 9404688..0f6f4ab 100644 --- a/examples/multiply_by_shift.asm +++ b/examples/multiply_by_shift.asm @@ -13,19 +13,17 @@ loop: ; if multiplier LSB is 1, add multiplicand mov r19, r18 - ldi r20, 1 - and r19, r20 + andi r19, 1 cpi r19, 1 brne no_add add r16, r17 no_add: - ; shift multiplicand left (*2) - ldi r20, 2 - mul r17, r20 + ; shift multiplicand left (*2) using AVR LSL + lsl r17 - ; shift multiplier right (/2) - div r18, r20 + ; shift multiplier right (/2) using AVR LSR + lsr r18 jmp loop diff --git a/examples/power.asm b/examples/power.asm index 3f23f1b..ce7dc71 100644 --- a/examples/power.asm +++ b/examples/power.asm @@ -3,31 +3,32 @@ start: ldi r16, 3 ; base = 3 - ldi r17, 4 ; exp = 4 + ldi r18, 4 ; exp = 4 ; save base - mov r18, r16 + mov r19, r16 ; if exp == 0, result = 1 - cpi r17, 0 + cpi r18, 0 breq exp_zero ; if exp == 1, done - cpi r17, 1 + cpi r18, 1 breq done ; counter = 1 - ldi r19, 1 + ldi r20, 1 loop: - ; result *= base - mul r16, r18 + ; result *= base (stores into R1:R0 per AVR specification) + mul r16, r19 + mov r16, r0 ; counter++ - inc r19 + inc r20 ; continue if counter < exp - cp r19, r17 + cp r20, r18 brlt loop jmp done diff --git a/src/tiny8/assembler.py b/src/tiny8/assembler.py index 96917d9..c2c3d52 100644 --- a/src/tiny8/assembler.py +++ b/src/tiny8/assembler.py @@ -1,22 +1,17 @@ -"""Simple assembler for tiny8 - converts assembly text into program tuples. +"""Assembler for tiny8 converting assembly text into program listings.""" -This module provides a tiny assembler that tokenizes and parses a simple -assembly syntax and returns a list of instructions and a label mapping. -""" - -import re from dataclasses import dataclass, field @dataclass class AsmResult: - """Result of assembling source code. + """Result of assembling assembly source code. Attributes: - program: List of instruction tuples (mnemonic, operands). - labels: Mapping from label names to program counter addresses. - pc_to_line: Mapping from program counter to original source line number. - source_lines: Original source text split into lines for display. + program: List of instruction tuples in (mnemonic, operands) format. + labels: Mapping of label names to program counter addresses. + pc_to_line: Mapping of program counter to source line numbers. + source_lines: Original source lines for display and debugging. """ program: list[tuple[str, tuple]] = field(default_factory=list) @@ -26,143 +21,106 @@ class AsmResult: def _parse_number(token: str) -> int: - """Parse a numeric assembly token and return its integer value. - - The function accepts tokens used by a simple assembler and supports - several notations: + """Parse numeric literal token into integer. - - Optional immediate marker '#' prefixes the token and is ignored. - - Hexadecimal with a leading '$' (e.g. '$FF') or Python-style '0x' - (e.g. '0xFF'). - - Binary with a leading '0b' (e.g. '0b1010'). - - Decimal integers, including negative values (e.g. '42', '-7'). + Supports hexadecimal ($FF, 0xFF), binary (0b1010), and decimal values, + with an optional leading '#' immediate prefix. Args: - token: The input token to parse. + token: Numeric token string to parse. Returns: - The integer value represented by the token. + Parsed integer value. Raises: - ValueError: If the token cannot be interpreted as a numeric literal - (e.g. when it is a label or otherwise non-numeric). + ValueError: If token cannot be parsed as a number. """ - t = token.strip() - # strip immediate marker - if t.startswith("#"): - t = t[1:] - # $nn as hex - if t.startswith("$"): + t = token.strip().removeprefix("#") + if not t: + raise ValueError(f"Unable to parse numeric token: {token}") + c0 = t[0] + if c0 == "$": return int(t[1:], 16) - if t.startswith("0x"): + if t.startswith(("0x", "0X")): return int(t, 16) - if t.startswith("0b"): + if t.startswith(("0b", "0B")): return int(t, 2) - if t.isdigit() or (t.startswith("-") and t[1:].isdigit()): + if c0.isdigit() or (c0 == "-" and len(t) > 1 and t[1:].isdigit()): return int(t) - # otherwise return as-is (label) raise ValueError(f"Unable to parse numeric token: {token}") def parse_asm(text: str) -> AsmResult: - """Parse assembly source text into a program listing and a label table. - - The function scans the given assembly text line-by-line and produces - an AsmResult containing the parsed program, labels, source line - mapping, and original source text. + """Parse assembly source text into an AsmResult structure. Args: - text: Assembly source code as a single string. May contain comments, - blank lines and labels. + text: Assembly source text. Returns: - AsmResult object containing program, labels, pc_to_line mapping, - and source_lines. - - Note: - - Comments begin with ';' and extend to the end of the line. - - Labels may appear on a line by themselves or before an instruction - with the form "label: instr ...". - - Registers are encoded as ("reg", N) where N is the register number. - - Numeric operands are parsed using _parse_number; non-numeric tokens - are preserved as strings (symbols) for later resolution. + AsmResult containing parsed program, labels, and line mappings. """ result = AsmResult() pc = 0 lines = text.splitlines() result.source_lines = lines.copy() - for line_num, line in enumerate(lines): - line = line.split(";", 1)[0].strip() + + for line_num, raw_line in enumerate(lines): + line = raw_line.split(";", 1)[0].strip() if not line: continue if ":" in line: left, right = line.split(":", 1) - lbl = left.strip() - result.labels[lbl] = pc + result.labels[left.strip()] = pc line = right.strip() if not line: continue - parts = [p for p in re.split(r"[\s,]+", line) if p != ""] + + parts = line.replace(",", " ").split() instr = parts[0].upper() ops = [] for p in parts[1:]: - pl = p.lower() - if pl.startswith("r") and pl[1:].isdigit(): - ops.append(("reg", int(pl[1:]))) + if (p[0] == "r" or p[0] == "R") and len(p) > 1 and p[1:].isdigit(): + ops.append(("reg", int(p[1:]))) + elif p.upper() in ("X", "Y", "Z"): + reg_map = {"X": 26, "Y": 28, "Z": 30} + ops.append(("reg", reg_map[p.upper()])) else: try: - n = _parse_number(p) - ops.append(n) + ops.append(_parse_number(p)) except ValueError: ops.append(p) + result.program.append((instr, tuple(ops))) result.pc_to_line[pc] = line_num pc += 1 + return result def assemble(text: str) -> AsmResult: - """Parse assembly source text and return parsed instructions and label map. + """Assemble source text into program instructions and label mappings. Args: - text: Assembly source code as a single string. May contain - multiple lines, labels and comments. + text: Assembly source code string. Returns: - AsmResult object containing program, labels, source line mapping, - and original source text. - - Raises: - Exception: Propagates parsing errors from the underlying parser. - - Example: - >>> src = "start: MOV R1, 5\\nJMP start" - >>> result = assemble(src) - >>> result.labels - {'start': 0} + AsmResult containing parsed program, labels, and line mappings. """ return parse_asm(text) def assemble_file(path: str) -> AsmResult: - """Assemble the contents of a source file. + """Assemble source file into program instructions and label mappings. Args: - path: Path to the source file to assemble. + path: Path to the assembly source file. Returns: - The result produced by calling assemble(source_text). + AsmResult containing parsed program, labels, and line mappings. Raises: - FileNotFoundError: If the specified file does not exist. - OSError: For other I/O related errors when opening or reading the file. - Exception: Any exception raised by assemble(...) will be propagated. - - Note: - The file is opened in text mode and read entirely into memory. - - Example: - >>> result = assemble_file("program.asm") + FileNotFoundError: If the source file does not exist. + OSError: If reading the file fails. """ - with open(path, "r") as f: + with open(path, "r", encoding="utf-8") as f: return assemble(f.read()) diff --git a/src/tiny8/cpu.py b/src/tiny8/cpu.py index 447752c..bc876da 100644 --- a/src/tiny8/cpu.py +++ b/src/tiny8/cpu.py @@ -1,12 +1,7 @@ -"""A simplified AVR-like 8-bit CPU simulator. +"""Simplified AVR-like 8-bit CPU simulator.""" -This module provides a lightweight CPU model inspired by the ATmega family. -The :class:`CPU` class is the primary export and implements a small, extensible instruction-dispatch model. -The implementation favors readability over step-accurate emulation. Add -instruction handlers by defining methods named ``op_`` on ``CPU``. -""" - -from typing import TYPE_CHECKING, Optional +from collections import deque +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from .assembler import AsmResult @@ -14,68 +9,152 @@ from .memory import Memory from .utils import ProgressBar -# SREG flag bit positions and short descriptions. -SREG_I = 7 # Global Interrupt Enable -SREG_T = 6 # Bit copy storage (temporary) -SREG_H = 5 # Half Carry -SREG_S = 4 # Sign (N ^ V) -SREG_V = 3 # Two's complement overflow -SREG_N = 2 # Negative -SREG_Z = 1 # Zero -SREG_C = 0 # Carry +SREG_I = 7 +SREG_T = 6 +SREG_H = 5 +SREG_S = 4 +SREG_V = 3 +SREG_N = 2 +SREG_Z = 1 +SREG_C = 0 + + +def _make_invoker(handler, decoded_ops: tuple): + """Create a specialized zero-overhead callable for instruction execution.""" + if handler is None: + return None + n = len(decoded_ops) + if n == 2: + a, b = decoded_ops[0], decoded_ops[1] + + def inv2() -> None: + handler(a, b) + + return inv2 + if n == 1: + a = decoded_ops[0] + + def inv1() -> None: + handler(a) + + return inv1 + if n == 0: + return handler + + def inv_n() -> None: + handler(*decoded_ops) + + return inv_n class CPU: - """In-memory 8-bit AVR-like CPU model. + """Lightweight 8-bit AVR CPU emulator compliant with Microchip DS40002198A. - The CPU implements a compact instruction-dispatch model. Handlers are - methods named ``op_`` and are invoked by :meth:`step` for the - currently-loaded program. + Args: + memory: Optional Memory instance. Defaults to a new Memory instance. Attributes: - regs (list[int]): 32 8-bit general purpose registers (R0..R31). - pc (int): Program counter (index into ``program``). - sp (int): Stack pointer (index into RAM in the associated - :class:`tiny8.memory.Memory`). - sreg (int): Status register bits stored in a single integer (I, T, - H, S, V, N, Z, C). - step_count (int): Instruction execution counter. - reg_trace (list[tuple[int, int, int]]): Per-step register change - trace entries of the form ``(step, reg, new_value)``. - mem_trace (list[tuple[int, int, int]]): Per-step memory change - trace entries of the form ``(step, addr, new_value)``. - step_trace (list[dict]): Full per-step snapshots useful for - visualization and debugging. - - Note: - This implementation simplifies many AVR specifics (flag semantics, - exact step counts, IO mapping) in favor of clarity. Extend or - replace individual ``op_`` handlers to increase fidelity. + mem: Attached Memory instance. + regs: List of 32 8-bit general purpose registers (R0..R31). + pc: Current program counter address. + sp: Stack pointer pointing to current top of stack in RAM. + sreg: Status register integer containing I, T, H, S, V, N, Z, C flags. + step_count: Number of instructions executed so far. + interrupts: Mapping of interrupt vector addresses to enabled flags. + reg_trace: Change history of registers as (step, reg_idx, new_val) tuples. + mem_trace: Change history of RAM as (step, addr, new_val) tuples. + step_trace: Full per-step snapshots for tracing and visualization. + program: List of instruction tuples in (mnemonic, operands) format. + labels: Mapping of symbolic labels to program counter addresses. + pc_to_line: Mapping of program counter to source line numbers. + source_lines: Original assembly source text lines. + running: Flag indicating whether execution loop is active. """ - def __init__(self, memory: Optional[Memory] = None): + __slots__ = ( + "mem", + "max_trace_steps", + "max_trace_history", + "trace_enabled", + "regs", + "pc", + "sp", + "sreg", + "step_count", + "interrupts", + "reg_trace", + "mem_trace", + "step_trace", + "program", + "labels", + "pc_to_line", + "source_lines", + "running", + "_predecoded", + ) + + def __init__( + self, + memory: Memory | None = None, + max_trace_steps: int = 1000, + max_trace_history: int = 2000, + trace_enabled: bool = True, + ) -> None: self.mem = memory or Memory() + self.max_trace_steps = max_trace_steps + self.max_trace_history = max_trace_history + self.trace_enabled = trace_enabled self.regs: list[int] = [0] * 32 self.pc: int = 0 self.sp: int = self.mem.ram_size - 1 self.sreg: int = 0 self.step_count: int = 0 self.interrupts: dict[int, bool] = {} - self.reg_trace: list[tuple[int, int, int]] = [] - self.mem_trace: list[tuple[int, int, int]] = [] - self.step_trace: list[dict] = [] + self.reg_trace: deque[tuple[int, int, int]] = deque(maxlen=max_trace_history) + self.mem_trace: deque[tuple[int, int, int]] = deque(maxlen=max_trace_history) + self.step_trace: deque[dict] = deque(maxlen=max_trace_steps) self.program: list[tuple[str, tuple]] = [] self.labels: dict[str, int] = {} self.pc_to_line: dict[int, int] = {} self.source_lines: list[str] = [] - self.running = False + self.running: bool = False + self._predecoded: list[tuple] = [] + + def clear_traces(self) -> None: + """Clear execution traces and history to free memory.""" + self.step_trace.clear() + self.reg_trace.clear() + self.mem_trace.clear() + self.mem.clear_changes() + + def snapshot(self) -> dict[str, Any]: + """Capture complete CPU and memory execution state snapshot.""" + return { + "step": self.step_count, + "pc": self.pc, + "sp": self.sp, + "sreg": self.sreg, + "regs": list(self.regs), + "mem": dict(self.mem.nonzero_ram), + "running": self.running, + } + + def restore_snapshot(self, snap: dict[str, Any]) -> None: + """Restore CPU and memory execution state from a snapshot.""" + self.step_count = snap["step"] + self.pc = snap["pc"] + self.sp = snap["sp"] + self.sreg = snap["sreg"] + self.regs = list(snap["regs"]) + self.running = snap.get("running", True) + self.mem.restore_ram(snap["mem"]) def set_flag(self, bit: int, value: bool) -> None: - """Set or clear a specific SREG flag bit. + """Set or clear a status register flag bit. Args: - bit: Integer bit index (0..7) representing the flag position. - value: True to set the bit, False to clear it. + bit: SREG bit index (0..7). + value: True to set flag, False to clear. """ if value: self.sreg |= 1 << bit @@ -83,104 +162,101 @@ def set_flag(self, bit: int, value: bool) -> None: self.sreg &= ~(1 << bit) def get_flag(self, bit: int) -> bool: - """Return the boolean value of a specific SREG flag bit. + """Return boolean status of a specific SREG bit. Args: - bit: Integer bit index (0..7). + bit: SREG bit index (0..7). Returns: - True if the bit is set, False otherwise. + True if bit is set, False otherwise. """ return bool((self.sreg >> bit) & 1) - # --- AVR-like flag helpers (Z, C, N, V, S, H) --- def _set_flags_add(self, a: int, b: int, carry_in: int, result: int) -> None: - """Set flags for ADD/ADC (AVR semantics). + """Set SREG flags for addition operations (ADD, ADC). Args: a: First operand (0..255). b: Second operand (0..255). - carry_in: Carry input (0 or 1). - result: Integer sum result. + carry_in: Carry-in bit (0 or 1). + result: Sum result before 8-bit truncation. """ r = result & 0xFF c = (result >> 8) & 1 h = (((a & 0x0F) + (b & 0x0F) + carry_in) >> 4) & 1 n = (r >> 7) & 1 - v = 1 if (((~(a ^ b) & 0xFF) & (a ^ r) & 0x80) != 0) else 0 + v = 1 if ((~(a ^ b) & (a ^ r)) & 0x80) else 0 s = n ^ v z = 1 if r == 0 else 0 - - self.set_flag(SREG_C, bool(c)) - self.set_flag(SREG_H, bool(h)) - self.set_flag(SREG_N, bool(n)) - self.set_flag(SREG_V, bool(v)) - self.set_flag(SREG_S, bool(s)) - self.set_flag(SREG_Z, bool(z)) + self.sreg = ( + (self.sreg & 0xC0) + | (h << 5) + | (s << 4) + | (v << 3) + | (n << 2) + | (z << 1) + | c + ) def _set_flags_sub(self, a: int, b: int, borrow_in: int, result: int) -> None: - """Set flags for SUB/CP/CPI (AVR semantics). + """Set SREG flags for subtraction operations (SUB, SBC, CP, CPI). Args: a: Minuend (0..255). b: Subtrahend (0..255). - borrow_in: Borrow input (0 or 1). - result: Signed difference (a - b - borrow_in). + borrow_in: Borrow-in bit (0 or 1). + result: Difference result before 8-bit truncation. """ r = result & 0xFF c = 1 if (a - b - borrow_in) < 0 else 0 h = 1 if ((a & 0x0F) - (b & 0x0F) - borrow_in) < 0 else 0 n = (r >> 7) & 1 - v = 1 if ((((a ^ b) & (a ^ r)) & 0x80) != 0) else 0 + v = 1 if (((a ^ b) & (a ^ r)) & 0x80) else 0 s = n ^ v z = 1 if r == 0 else 0 - - self.set_flag(SREG_C, bool(c)) - self.set_flag(SREG_H, bool(h)) - self.set_flag(SREG_N, bool(n)) - self.set_flag(SREG_V, bool(v)) - self.set_flag(SREG_S, bool(s)) - self.set_flag(SREG_Z, bool(z)) + self.sreg = ( + (self.sreg & 0xC0) + | (h << 5) + | (s << 4) + | (v << 3) + | (n << 2) + | (z << 1) + | c + ) def _set_flags_logical(self, result: int) -> None: - """Set flags for logical operations (AND, OR, EOR) per AVR semantics. + """Set SREG flags for logical operations (AND, OR, EOR). + + Status Register (SREG) per AVR Instruction Set Manual DS40002198A: + S: N ⊕ V (For signed tests) + V: 0 (Cleared) + N: R7 (Set if MSB of result is set; cleared otherwise) + Z: Set if result is $00; cleared otherwise + H, C, T, I: Unaffected (-) Args: result: Operation result. - - Note: - Logical ops clear C and V, set N and Z, S = N ^ V, and clear H. """ r = result & 0xFF n = (r >> 7) & 1 z = 1 if r == 0 else 0 - s = n # v is 0, so s = n ^ 0 = n - - self.set_flag(SREG_C, False) - self.set_flag(SREG_V, False) - self.set_flag(SREG_N, bool(n)) - self.set_flag(SREG_S, bool(s)) - self.set_flag(SREG_Z, bool(z)) - self.set_flag(SREG_H, False) + self.sreg = (self.sreg & 0xE1) | (n << 4) | (n << 2) | (z << 1) def _set_flags_inc(self, old: int, new: int) -> None: - """Set flags for INC (affects V, N, Z, S). Does not affect C or H. + """Set SREG flags for increment operation (INC). Args: - old: Value before increment. - new: Value after increment. + old: Register value before increment. + new: Register value after increment. """ n = (new >> 7) & 1 v = 1 if old == 0x7F else 0 - z = 1 if new == 0 else 0 s = n ^ v - self.set_flag(SREG_N, bool(n)) - self.set_flag(SREG_V, bool(v)) - self.set_flag(SREG_S, bool(s)) - self.set_flag(SREG_Z, bool(z)) + z = 1 if new == 0 else 0 + self.sreg = (self.sreg & 0xE1) | (s << 4) | (v << 3) | (n << 2) | (z << 1) def _set_flags_add16(self, a: int, b: int, carry_in: int, result: int) -> None: - """Set flags for 16-bit add (ADIW semantics approximation). + """Set SREG flags for 16-bit addition (ADIW). Args: a: First operand (0..0xFFFF). @@ -194,277 +270,269 @@ def _set_flags_add16(self, a: int, b: int, carry_in: int, result: int) -> None: v = 1 if (((~(a ^ b) & 0xFFFF) & (a ^ r) & 0x8000) != 0) else 0 s = n ^ v z = 1 if r == 0 else 0 - - self.set_flag(SREG_C, bool(c)) - self.set_flag(SREG_N, bool(n)) - self.set_flag(SREG_V, bool(v)) - self.set_flag(SREG_S, bool(s)) - self.set_flag(SREG_Z, bool(z)) - self.set_flag(SREG_H, False) + self.sreg = (self.sreg & 0xE0) | (s << 4) | (v << 3) | (n << 2) | (z << 1) | c def _set_flags_sub16(self, a: int, b: int, borrow_in: int, result: int) -> None: - """Set flags for 16-bit subtraction (SBIW semantics approximation). + """Set SREG flags for 16-bit subtraction (SBIW). Args: a: Minuend (0..0xFFFF). b: Subtrahend (0..0xFFFF). borrow_in: Borrow input (0 or 1). - result: Integer difference a - b - borrow_in. + result: Integer difference. """ r = result & 0xFFFF c = 1 if (a - b - borrow_in) < 0 else 0 n = (r >> 15) & 1 - v = 1 if ((((a ^ b) & (a ^ r)) & 0x8000) != 0) else 0 + v = 1 if (((a ^ b) & (a ^ r)) & 0x8000) != 0 else 0 s = n ^ v z = 1 if r == 0 else 0 - - self.set_flag(SREG_C, bool(c)) - self.set_flag(SREG_N, bool(n)) - self.set_flag(SREG_V, bool(v)) - self.set_flag(SREG_S, bool(s)) - self.set_flag(SREG_Z, bool(z)) - self.set_flag(SREG_H, False) + self.sreg = (self.sreg & 0xE0) | (s << 4) | (v << 3) | (n << 2) | (z << 1) | c def _set_flags_dec(self, old: int, new: int) -> None: - """Set flags for DEC (affects V, N, Z, S). Does not affect C or H. + """Set SREG flags for decrement operation (DEC). Args: - old: Value before decrement. - new: Value after decrement. + old: Register value before decrement. + new: Register value after decrement. """ n = (new >> 7) & 1 v = 1 if old == 0x80 else 0 - z = 1 if new == 0 else 0 s = n ^ v - self.set_flag(SREG_N, bool(n)) - self.set_flag(SREG_V, bool(v)) - self.set_flag(SREG_S, bool(s)) - self.set_flag(SREG_Z, bool(z)) + z = 1 if new == 0 else 0 + self.sreg = (self.sreg & 0xE1) | (s << 4) | (v << 3) | (n << 2) | (z << 1) - # Register access def read_reg(self, r: int) -> int: - """Return the 8-bit value from register ``r``. + """Return 8-bit value from register r. Args: r: Register index (0..31). Returns: - The 8-bit value (0..255) stored in the register. + 8-bit unsigned integer value. """ - return self.regs[r] & 0xFF + return self.regs[r] def write_reg(self, r: int, val: int) -> None: - """Write an 8-bit value to register ``r`` and record the change. + """Write 8-bit value to register r and trace changes. Args: r: Register index (0..31). - val: Value to write; will be truncated to 8 bits. - - Note: - A trace entry is appended only when the register value actually - changes to avoid noisy traces. + val: Integer value to store (masked to 8-bit). """ newv = val & 0xFF - if self.regs[r] != newv: + if self.trace_enabled: + if self.regs[r] != newv: + self.regs[r] = newv + self.reg_trace.append((self.step_count, r, newv)) + else: self.regs[r] = newv - self.reg_trace.append((self.step_count, r, newv)) - # Memory access wrappers def read_ram(self, addr: int) -> int: - """Read a byte from RAM at the given address. + """Read byte from RAM address. Args: - addr: RAM address to read. + addr: Memory address. Returns: - Byte value stored at ``addr`` (0..255). + Byte value stored at address. """ return self.mem.read_ram(addr) def write_ram(self, addr: int, val: int) -> None: - """Write an 8-bit value to RAM at ``addr`` and record the trace. + """Write byte to RAM address and record memory trace. Args: - addr: RAM address to write. - val: Value to write; will be truncated to 8 bits. - - Note: - The underlying :class:`Memory` object stores the value; a - ``(step, addr, val)`` tuple is appended to ``mem_trace`` for - visualizers/tests. + addr: Target RAM address. + val: Byte value to write. """ self.mem.write_ram(addr, val, self.step_count) - self.mem_trace.append((self.step_count, addr, val & 0xFF)) + if self.trace_enabled: + self.mem_trace.append((self.step_count, addr, val & 0xFF)) - # Program loading def load_program( self, program: "list[tuple[str, tuple]] | AsmResult", - labels: Optional[dict[str, int]] = None, - pc_to_line: Optional[dict[int, int]] = None, - source_lines: Optional[list[str]] = None, - ): - """Load an assembled program into the CPU. - - Args: - program: Either a list of ``(mnemonic, operands)`` tuples or an - AsmResult object. If AsmResult, other params are ignored. - labels: Mapping of label strings to instruction indices (ignored if - program is AsmResult). - pc_to_line: Optional mapping from PC to source line number for tracing - (ignored if program is AsmResult). - source_lines: Optional original assembly source lines for display - (ignored if program is AsmResult). - - Note: - After loading the program, the program counter is reset to zero. - """ - # Check if program is an AsmResult + labels: dict[str, int] | None = None, + pc_to_line: dict[int, int] | None = None, + source_lines: list[str] | None = None, + ) -> None: + """Load assembled program instructions into CPU. + + Args: + program: AsmResult object or list of (mnemonic, operands) tuples. + labels: Label mapping if program is a raw list. + pc_to_line: PC to line mapping if program is a raw list. + source_lines: Source text lines if program is a raw list. + """ if hasattr(program, "program") and hasattr(program, "labels"): - # It's an AsmResult - asm = program - self.program = asm.program - self.labels = asm.labels - self.pc_to_line = asm.pc_to_line - self.source_lines = asm.source_lines + self.program = program.program + self.labels = program.labels + self.pc_to_line = program.pc_to_line + self.source_lines = program.source_lines else: - # Legacy tuple-based format self.program = program self.labels = labels or {} self.pc_to_line = pc_to_line or {} self.source_lines = source_lines or [] self.pc = 0 + self._build_predecoded() + + def _build_predecoded(self) -> None: + """Pre-decode program instructions for maximum execution throughput.""" + self._predecoded = [] + for instr, operands in self.program: + handler = getattr(self, f"op_{instr.lower()}", None) + decoded_ops = [] + for o in operands: + if isinstance(o, tuple) and len(o) == 2 and o[0] == "reg": + decoded_ops.append(int(o[1])) + else: + decoded_ops.append(o) + + def fmt_op(o) -> str: + if isinstance(o, tuple) and len(o) == 2 and o[0] == "reg": + return f"R{o[1]}" + return str(o) - # Instruction execution - def step(self) -> bool: - """Execute a single instruction at the current program counter. + try: + ops_text = ", ".join(fmt_op(o) for o in operands) + except Exception: + ops_text = "" + instr_text = f"{instr.upper()} {ops_text}".strip() - Performs one fetch-decode-execute step. A pre-step snapshot of - registers and non-zero RAM is recorded, the instruction handler - (``op_``) is invoked, and a post-step trace entry is - appended to ``step_trace``. + ops_tuple = tuple(decoded_ops) + invoker = _make_invoker(handler, ops_tuple) + self._predecoded.append((handler, ops_tuple, instr_text, invoker)) + + def step(self, record_trace: bool | None = None) -> bool: + """Execute next single instruction at current program counter. + + Args: + record_trace: Optional override to enable or disable trace recording for this step. Returns: - True if an instruction was executed; False if the PC is out of - range and execution should stop. - """ + True if instruction executed successfully, False if PC is out of range. - if self.pc < 0 or self.pc >= len(self.program): + Raises: + NotImplementedError: If instruction mnemonic has no handler. + """ + if not (0 <= self.pc < len(self.program)): self.running = False return False pre_exec_pc = self.pc - instr, operands = self.program[self.pc] - - # Build textual form of the instruction for tracing (uppercase mnemonic - # and register names like R0..R31). Operands decoded for display only. - def fmt_op(o): - if isinstance(o, tuple) and len(o) == 2 and o[0] == "reg": - return f"R{o[1]}" - return str(o) + if not self._predecoded or len(self._predecoded) != len(self.program): + self._build_predecoded() - try: - ops_text = ", ".join(fmt_op(o) for o in operands) - except Exception: - ops_text = "" - instr_text = f"{instr.upper()} {ops_text}".strip() - - # record pre-step snapshot - regs_snapshot = list(self.regs) - # memory snapshot: capture all non-zero RAM addresses (helps visualization of higher addresses) - mem_snapshot = {} - for i in range(0, self.mem.ram_size): - v = self.read_ram(i) - if v != 0: - mem_snapshot[i] = v - - # Convert register operand markers back to raw ints for handlers, and - # call the appropriate handler (handlers are named op_ and - # expect plain ints/strings as originally implemented). - handler = getattr(self, f"op_{instr.lower()}", None) - if handler is None: + handler, decoded_ops, instr_text, invoker = self._predecoded[pre_exec_pc] + if invoker is None: + instr, _ = self.program[pre_exec_pc] raise NotImplementedError(f"Instruction {instr} not implemented") - # decode operands for handler call - decoded_ops = [] - for o in operands: - if isinstance(o, tuple) and len(o) == 2 and o[0] == "reg": - decoded_ops.append(int(o[1])) - else: - decoded_ops.append(o) + should_trace = self.trace_enabled if record_trace is None else record_trace - handler(*tuple(decoded_ops)) + if should_trace: + regs_snapshot = list(self.regs) + mem_snapshot = dict(self.mem.nonzero_ram) - # record step trace after execution (post-state) - self.step_count += 1 + invoker() - source_line = self.pc_to_line.get(pre_exec_pc, -1) - self.step_trace.append( - { - "step": self.step_count, - "pc": pre_exec_pc, - "instr": instr_text, - "regs": regs_snapshot, - "mem": mem_snapshot, - "sreg": self.sreg, - "sp": self.sp, - "source_line": source_line, - } - ) + self.step_count += 1 + if should_trace: + source_line = self.pc_to_line.get(pre_exec_pc, -1) + self.step_trace.append( + { + "step": self.step_count, + "pc": pre_exec_pc, + "instr": instr_text, + "regs": regs_snapshot, + "mem": mem_snapshot, + "sreg": self.sreg, + "sp": self.sp, + "source_line": source_line, + } + ) self.pc += 1 return True def run(self, max_steps: int = 100000, show_progress: bool = True) -> None: - """Run instructions until program end or ``max_steps`` is reached. + """Run instructions until completion or max_steps limit is reached. Args: - max_steps: Maximum number of instruction steps to execute - (default 100000). - show_progress: If True, display a progress bar during execution - (default True). - - Note: - This repeatedly calls :meth:`step` until it returns False or the - maximum step count is reached. + max_steps: Maximum instruction step executions allowed. + show_progress: Whether to show progress bar in terminal. """ self.running = True - steps = 0 - if show_progress: - pb = ProgressBar(total=max_steps, desc="CPU execution") + if not show_progress and not self.trace_enabled: + # Fast-path execution for batch and headless runs + if not self._predecoded or len(self._predecoded) != len(self.program): + self._build_predecoded() + predec = self._predecoded + prog_len = len(predec) + steps = 0 + while self.running and steps < max_steps: + curr_pc = self.pc + if not (0 <= curr_pc < prog_len): + self.running = False + break + item = predec[curr_pc] + invoker = item[3] + if invoker is None: + instr, _ = self.program[curr_pc] + raise NotImplementedError(f"Instruction {instr} not implemented") + invoker() + self.step_count += 1 + self.pc += 1 + steps += 1 + return + + steps = 0 + pb = ( + ProgressBar(total=max_steps, desc="CPU execution") + if show_progress + else None + ) try: while self.running and steps < max_steps: - ok = self.step() - if not ok: + if not self.step(): break steps += 1 - - if show_progress: + if pb: pb.update(1) finally: - if show_progress: + if pb: pb.close() - def op_nop(self): - """No-operation: does nothing for one step.""" - pass + def op_nop(self) -> None: + """No Operation (NOP). - def op_ldi(self, reg_idx: int, imm: int): - """Load an immediate value into a register. + Per AVR Instruction Set Manual DS40002198A: + Operation: No operation. + Status Register (SREG): None. + """ - Args: - reg_idx: Destination register index. - imm: Immediate value to load. + def op_ldi(self, reg_idx: int, imm: int) -> None: + """Load Immediate (LDI). - Note: - The AVR LDI instruction is normally restricted to R16..R31; this - simplified implementation accepts any register index. + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- K (16 <= d <= 31, 0 <= K <= 255) + Status Register (SREG): None. + + Args: + reg_idx: Destination register index (16..31). + imm: Immediate byte value. """ self.write_reg(reg_idx, imm) - def op_mov(self, rd: int, rr: int): - """Copy the value from register ``rr`` into ``rd``. + def op_mov(self, rd: int, rr: int) -> None: + """Copy Register (MOV). + + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- Rr (0 <= d, r <= 31) + Status Register (SREG): None. Args: rd: Destination register index. @@ -472,15 +540,16 @@ def op_mov(self, rd: int, rr: int): """ self.write_reg(rd, self.read_reg(rr)) - def op_add(self, rd: int, rr: int): - """Add register ``rr`` to ``rd`` (Rd := Rd + Rr) and update flags. + def op_add(self, rd: int, rr: int) -> None: + """Add without Carry (ADD). + + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- Rd + Rr + Status Register (SREG): H, S, V, N, Z, C Args: rd: Destination register index. rr: Source register index. - - Note: - Sets C, H, N, V, S, Z per AVR semantics. """ a = self.read_reg(rd) b = self.read_reg(rr) @@ -488,8 +557,12 @@ def op_add(self, rd: int, rr: int): self.write_reg(rd, res & 0xFF) self._set_flags_add(a, b, 0, res) - def op_and(self, rd: int, rr: int): - """Logical AND (Rd := Rd & Rr) — updates N, Z, V=0, C=0, H=0, S. + def op_and(self, rd: int, rr: int) -> None: + """Logical AND (AND). + + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- Rd • Rr + Status Register (SREG): S, V=0, N, Z (H, C unaffected) Args: rd: Destination register index. @@ -499,8 +572,12 @@ def op_and(self, rd: int, rr: int): self.write_reg(rd, res) self._set_flags_logical(res) - def op_or(self, rd: int, rr: int): - """Logical OR (Rd := Rd | Rr) — updates N, Z, V=0, C=0, H=0, S. + def op_or(self, rd: int, rr: int) -> None: + """Logical OR (OR). + + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- Rd v Rr + Status Register (SREG): S, V=0, N, Z (H, C unaffected) Args: rd: Destination register index. @@ -510,8 +587,12 @@ def op_or(self, rd: int, rr: int): self.write_reg(rd, res) self._set_flags_logical(res) - def op_eor(self, rd: int, rr: int): - """Exclusive OR (Rd := Rd ^ Rr) — updates N, Z, V=0, C=0, H=0, S. + def op_eor(self, rd: int, rr: int) -> None: + """Exclusive OR (EOR). + + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- Rd ⊕ Rr + Status Register (SREG): S, V=0, N, Z (H, C unaffected) Args: rd: Destination register index. @@ -521,8 +602,12 @@ def op_eor(self, rd: int, rr: int): self.write_reg(rd, res) self._set_flags_logical(res) - def op_sub(self, rd: int, rr: int): - """Subtract (Rd := Rd - Rr) and set flags C,H,N,V,S,Z. + def op_sub(self, rd: int, rr: int) -> None: + """Subtract without Carry (SUB). + + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- Rd - Rr + Status Register (SREG): H, S, V, N, Z, C Args: rd: Destination register index. @@ -534,8 +619,12 @@ def op_sub(self, rd: int, rr: int): self.write_reg(rd, res_full & 0xFF) self._set_flags_sub(a, b, 0, res_full) - def op_inc(self, rd: int): - """Increment (Rd := Rd + 1) — updates V,N,S,Z; does not change C/H. + def op_inc(self, rd: int) -> None: + """Increment (INC). + + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- Rd + 1 + Status Register (SREG): S, V, N, Z (C unaffected) Args: rd: Destination register index. @@ -545,8 +634,12 @@ def op_inc(self, rd: int): self.write_reg(rd, new) self._set_flags_inc(old, new) - def op_dec(self, rd: int): - """Decrement (Rd := Rd - 1) — updates V,N,S,Z; does not change C/H. + def op_dec(self, rd: int) -> None: + """Decrement (DEC). + + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- Rd - 1 + Status Register (SREG): S, V, N, Z (C unaffected) Args: rd: Destination register index. @@ -556,30 +649,77 @@ def op_dec(self, rd: int): self.write_reg(rd, new) self._set_flags_dec(old, new) - def op_mul(self, rd: int, rr: int): - """Multiply 8x8 -> 16: store low in Rd, high in Rd+1. + def op_mul(self, rd: int, rr: int) -> None: + """Multiply Unsigned (MUL). - Args: - rd: Destination register index for low byte. - rr: Source register index. + AVR Instruction Set Manual DS40002198A: + R1:R0 <- Rd * Rr (unsigned). + SREG C: Set if bit 15 of result is set; cleared otherwise. + SREG Z: Set if result is $0000; cleared otherwise. - Note: - Updates Z and C flags. Z set if product == 0; C set if high != 0. + Args: + rd: Multiplicand register index (0..31). + rr: Multiplier register index (0..31). """ a = self.read_reg(rd) b = self.read_reg(rr) prod = a * b - low = prod & 0xFF - high = (prod >> 8) & 0xFF - self.write_reg(rd, low) - if rd + 1 < 32: - self.write_reg(rd + 1, high) - self.set_flag(SREG_Z, prod == 0) - self.set_flag(SREG_C, high != 0) - self.set_flag(SREG_H, False) + self.write_reg(0, prod & 0xFF) + self.write_reg(1, (prod >> 8) & 0xFF) + self.set_flag(SREG_Z, (prod & 0xFFFF) == 0) + self.set_flag(SREG_C, bool((prod >> 15) & 1)) + + def op_muls(self, rd: int, rr: int) -> None: + """Multiply Signed (MULS). + + AVR Instruction Set Manual DS40002198A: + R1:R0 <- Rd * Rr (signed). + SREG C: Set if bit 15 of result is set; cleared otherwise. + SREG Z: Set if result is $0000; cleared otherwise. + + Args: + rd: Multiplicand signed register index (0..31). + rr: Multiplier signed register index (0..31). + """ + a = self.read_reg(rd) + b = self.read_reg(rr) + sa = a - 256 if a >= 128 else a + sb = b - 256 if b >= 128 else b + prod = sa * sb + prod_u16 = prod & 0xFFFF + self.write_reg(0, prod_u16 & 0xFF) + self.write_reg(1, (prod_u16 >> 8) & 0xFF) + self.set_flag(SREG_Z, prod_u16 == 0) + self.set_flag(SREG_C, bool((prod_u16 >> 15) & 1)) + + def op_mulsu(self, rd: int, rr: int) -> None: + """Multiply Signed with Unsigned (MULSU). - def op_adc(self, rd: int, rr: int): - """Add with carry (Rd := Rd + Rr + C) and update flags. + AVR Instruction Set Manual DS40002198A: + R1:R0 <- Rd * Rr (Rd signed, Rr unsigned). + SREG C: Set if bit 15 of result is set; cleared otherwise. + SREG Z: Set if result is $0000; cleared otherwise. + + Args: + rd: Multiplicand signed register index (0..31). + rr: Multiplier unsigned register index (0..31). + """ + a = self.read_reg(rd) + b = self.read_reg(rr) + sa = a - 256 if a >= 128 else a + prod = sa * b + prod_u16 = prod & 0xFFFF + self.write_reg(0, prod_u16 & 0xFF) + self.write_reg(1, (prod_u16 >> 8) & 0xFF) + self.set_flag(SREG_Z, prod_u16 == 0) + self.set_flag(SREG_C, bool((prod_u16 >> 15) & 1)) + + def op_adc(self, rd: int, rr: int) -> None: + """Add with Carry (ADC). + + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- Rd + Rr + C + Status Register (SREG): H, S, V, N, Z, C Args: rd: Destination register index. @@ -587,119 +727,106 @@ def op_adc(self, rd: int, rr: int): """ a = self.read_reg(rd) b = self.read_reg(rr) - carry_in = 1 if self.get_flag(SREG_C) else 0 + carry_in = int(self.get_flag(SREG_C)) res = a + b + carry_in self.write_reg(rd, res & 0xFF) self._set_flags_add(a, b, carry_in, res) - def op_clr(self, rd: int): - """Clear register (Rd := 0). Behaves like EOR Rd,Rd for flags. + def op_clr(self, rd: int) -> None: + """Clear Register: Rd <- Rd ⊕ Rd = $00 (EOR Rd, Rd). + + Status Register (SREG) per AVR Manual DS40002198A: + S: 0, V: 0, N: 0, Z: 1; H, C, T, I unaffected. + Alternative name to the EOR Rd, Rd instruction. Args: rd: Destination register index. """ - self.write_reg(rd, 0) - self.set_flag(SREG_N, False) - self.set_flag(SREG_V, False) - self.set_flag(SREG_S, False) - self.set_flag(SREG_Z, True) - self.set_flag(SREG_C, False) - self.set_flag(SREG_H, False) + self.op_eor(rd, rd) + + def op_ser(self, rd: int) -> None: + """Set Register: Rd <- $FF (LDI Rd, $FF). - def op_ser(self, rd: int): - """Set register all ones (Rd := 0xFF). Update flags conservatively. + Status Register (SREG) per AVR Manual DS40002198A: + None affected. Alternative name to the LDI Rd, $FF instruction. Args: rd: Destination register index. """ self.write_reg(rd, 0xFF) - self.set_flag(SREG_N, True) - self.set_flag(SREG_V, False) - self.set_flag(SREG_S, True) - self.set_flag(SREG_Z, False) - self.set_flag(SREG_C, False) - self.set_flag(SREG_H, False) - def op_div(self, rd: int, rr: int): - """Unsigned divide convenience instruction: quotient -> Rd, remainder -> Rd+1. + def op_in(self, rd: int, port: int) -> None: + """Load an I/O Location to Register (IN). - Args: - rd: Destination register index for quotient. - rr: Divisor register index. - - Note: - If divisor is zero, sets C and Z flags to indicate error. - """ - a = self.read_reg(rd) - b = self.read_reg(rr) - if b == 0: - self.write_reg(rd, 0) - self.set_flag(SREG_C, True) - self.set_flag(SREG_Z, True) - return - q = a // b - r = a % b - self.write_reg(rd, q) - if rd + 1 < 32: - self.write_reg(rd + 1, r) - self.set_flag(SREG_Z, q == 0) - self.set_flag(SREG_C, False) - self.set_flag(SREG_H, False) - self.set_flag(SREG_V, False) - - def op_in(self, rd: int, port: int): - """Read from I/O port into register. + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- P + Status Register (SREG): None. Args: rd: Destination register index. - port: Port address to read from. + port: Port/memory address to read from. """ - val = self.read_ram(port) - self.write_reg(rd, val) + self.write_reg(rd, self.read_ram(port)) - def op_out(self, port: int, rr: int): - """Write register value to I/O port. + def op_out(self, port: int, rr: int) -> None: + """Store Register to an I/O Location (OUT). + + Per AVR Instruction Set Manual DS40002198A: + Operation: P <- Rr + Status Register (SREG): None. Args: - port: Port address to write to. + port: Port/memory address to write to. rr: Source register index. """ - val = self.read_reg(rr) - self.write_ram(port, val) + self.write_ram(port, self.read_reg(rr)) + + def op_jmp(self, label: str | int) -> None: + """Jump (JMP). - def op_jmp(self, label: str | int): - """Jump to a given label or numeric address by updating the program counter. + Per AVR Instruction Set Manual DS40002198A: + Operation: PC <- k + Status Register (SREG): None. Args: - label: The jump target. If a string, it is treated as a symbolic label - and looked up in self.labels. If an int, it is used directly as the - numeric address. + label: Target label name or instruction address. - Note: - Sets PC to target - 1 because the instruction dispatcher will - increment PC after the current instruction completes. + Raises: + KeyError: If target label string is not found. """ - if isinstance(label, str): - if label not in self.labels: - raise KeyError(f"Label {label} not found") - self.pc = self.labels[label] - 1 + if isinstance(label, int): + self.pc = label - 1 else: - self.pc = int(label) - 1 + target = self.labels.get(label) + if target is not None: + self.pc = target - 1 + else: + try: + self.pc = int(label) - 1 + except (ValueError, TypeError): + raise KeyError(f"Label {label} not found") + + def op_cpi(self, rd: int, imm: int) -> None: + """Compare with Immediate (CPI). - def op_cpi(self, rd: int, imm: int): - """Compare register with immediate (sets flags but doesn't modify register). + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd - K (16 <= d <= 31, 0 <= K <= 255) + Status Register (SREG): H, S, V, N, Z, C Args: - rd: Register index to compare. - imm: Immediate value to compare against. + rd: Register index to compare (16..31). + imm: Immediate byte value. """ a = self.read_reg(rd) b = imm & 0xFF - res = a - b - self._set_flags_sub(a, b, 0, res) + self._set_flags_sub(a, b, 0, a - b) - def op_cp(self, rd: int, rr: int): - """Compare two registers (sets flags but doesn't modify registers). + def op_cp(self, rd: int, rr: int) -> None: + """Compare (CP). + + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd - Rr + Status Register (SREG): H, S, V, N, Z, C Args: rd: First register index. @@ -707,30 +834,26 @@ def op_cp(self, rd: int, rr: int): """ a = self.read_reg(rd) b = self.read_reg(rr) - res = a - b - self._set_flags_sub(a, b, 0, res) + self._set_flags_sub(a, b, 0, a - b) + + def op_lsl(self, rd: int) -> None: + """Logical Shift Left: C <- Rd7, Rd(n+1) <- Rd(n), Rd0 <- 0. - def op_lsl(self, rd: int): - """Logical shift left (Rd := Rd << 1). + Status Register (SREG) per AVR Manual DS40002198A: + H: Rd3, S: N ⊕ V, V: N ⊕ C, N: R7, Z: Set if result is $00, C: Rd7 + Alternative name to the ADD Rd, Rd instruction. Args: rd: Destination register index. """ - v = self.read_reg(rd) - carry = (v >> 7) & 1 - nv = (v << 1) & 0xFF - self.write_reg(rd, nv) - self.set_flag(SREG_C, bool(carry)) - n = (nv >> 7) & 1 - vflag = n ^ carry - self.set_flag(SREG_N, bool(n)) - self.set_flag(SREG_V, bool(vflag)) - self.set_flag(SREG_S, bool(n ^ vflag)) - self.set_flag(SREG_Z, nv == 0) - self.set_flag(SREG_H, False) + self.op_add(rd, rd) + + def op_lsr(self, rd: int) -> None: + """Logical Shift Right: 0 -> Rd7 -> ... -> Rd0 -> C. - def op_lsr(self, rd: int): - """Logical shift right (Rd := Rd >> 1). + Status Register (SREG) per AVR Manual DS40002198A: + S: N ⊕ V, V: C, N: 0, Z: Set if result is $00, C: Rd0 + H, T, I unaffected (-). Args: rd: Destination register index. @@ -739,51 +862,78 @@ def op_lsr(self, rd: int): carry = v & 1 nv = (v >> 1) & 0xFF self.write_reg(rd, nv) - self.set_flag(SREG_C, bool(carry)) - self.set_flag(SREG_N, False) - self.set_flag(SREG_V, bool(carry)) - self.set_flag(SREG_S, bool(carry)) - self.set_flag(SREG_Z, nv == 0) - self.set_flag(SREG_H, False) + c = carry + vflag = c + s = c + z = 1 if nv == 0 else 0 + self.sreg = (self.sreg & 0xE0) | (s << 4) | (vflag << 3) | (z << 1) | c - def op_rol(self, rd: int): - """Rotate left through carry. + def op_asr(self, rd: int) -> None: + """Arithmetic Shift Right: Rd7 -> Rd7 -> ... -> Rd0 -> C. + + Status Register (SREG) per AVR Manual DS40002198A: + S: N ⊕ V, V: N ⊕ C, N: R7, Z: Set if result is $00, C: Rd0 + H, T, I unaffected (-). Args: rd: Destination register index. """ v = self.read_reg(rd) - carry_in = 1 if self.get_flag(SREG_C) else 0 - carry_out = (v >> 7) & 1 - nv = ((v << 1) & 0xFF) | carry_in + carry = v & 1 + sign = v & 0x80 + nv = (v >> 1) | sign self.write_reg(rd, nv) - self.set_flag(SREG_C, bool(carry_out)) - self.set_flag(SREG_N, bool((nv >> 7) & 1)) - self.set_flag(SREG_Z, nv == 0) - self.set_flag(SREG_V, False) - self.set_flag(SREG_S, bool((nv >> 7) & 1)) - self.set_flag(SREG_H, False) + n = (nv >> 7) & 1 + c = carry + vflag = n ^ c + s = n ^ vflag + z = 1 if nv == 0 else 0 + self.sreg = ( + (self.sreg & 0xE0) | (s << 4) | (vflag << 3) | (n << 2) | (z << 1) | c + ) + + def op_rol(self, rd: int) -> None: + """Rotate Left Through Carry: C <- Rd7, Rd(n+1) <- Rd(n), Rd0 <- C. + + Status Register (SREG) per AVR Manual DS40002198A: + H: Rd3, S: N ⊕ V, V: N ⊕ C, N: R7, Z: Set if result is $00, C: Rd7 + Alternative name to the ADC Rd, Rd instruction. + + Args: + rd: Destination register index. + """ + self.op_adc(rd, rd) + + def op_ror(self, rd: int) -> None: + """Rotate Right Through Carry: Rd7 <- C, Rd(n) <- Rd(n+1), C <- Rd0. - def op_ror(self, rd: int): - """Rotate right through carry. + Status Register (SREG) per AVR Manual DS40002198A: + S: N ⊕ V, V: N ⊕ C, N: R7, Z: Set if result is $00, C: Rd0 + H, T, I unaffected (-). Args: rd: Destination register index. """ v = self.read_reg(rd) - carry_in = 1 if self.get_flag(SREG_C) else 0 + carry_in = int(self.get_flag(SREG_C)) carry_out = v & 1 nv = (v >> 1) | (carry_in << 7) self.write_reg(rd, nv) - self.set_flag(SREG_C, bool(carry_out)) - self.set_flag(SREG_N, bool((nv >> 7) & 1)) - self.set_flag(SREG_Z, nv == 0) - self.set_flag(SREG_V, False) - self.set_flag(SREG_S, bool((nv >> 7) & 1)) - self.set_flag(SREG_H, False) + n = (nv >> 7) & 1 + c = carry_out + vflag = n ^ c + s = n ^ vflag + z = 1 if nv == 0 else 0 + self.sreg = ( + (self.sreg & 0xE0) | (s << 4) | (vflag << 3) | (n << 2) | (z << 1) | c + ) - def op_com(self, rd: int): - """One's complement: Rd := ~Rd. Updates N,V,S,Z,C per AVR-ish semantics. + def op_com(self, rd: int) -> None: + """One's Complement: Rd <- $FF - Rd. + + Status Register (SREG) per AVR Manual DS40002198A: + S: N ⊕ V, V: 0, N: R7, Z: Set if result is $00, C: 1 + H, T, I unaffected (-). Args: rd: Destination register index. @@ -792,15 +942,14 @@ def op_com(self, rd: int): nv = (~v) & 0xFF self.write_reg(rd, nv) n = (nv >> 7) & 1 - self.set_flag(SREG_N, bool(n)) - self.set_flag(SREG_V, False) - self.set_flag(SREG_S, bool(n)) - self.set_flag(SREG_Z, nv == 0) - self.set_flag(SREG_C, True) - self.set_flag(SREG_H, False) + z = 1 if nv == 0 else 0 + self.sreg = (self.sreg & 0xE0) | (n << 4) | (n << 2) | (z << 1) | 1 + + def op_neg(self, rd: int) -> None: + """Two's Complement: Rd <- $00 - Rd. - def op_neg(self, rd: int): - """Two's complement (negate): Rd := 0 - Rd. Flags as subtraction from 0. + Status Register (SREG) per AVR Manual DS40002198A: + H: R3 + ~Rd3, S: N ⊕ V, V: Rd == $80, N: R7, Z: Set if result is $00, C: Rd != $00 Args: rd: Destination register index. @@ -810,64 +959,69 @@ def op_neg(self, rd: int): self.write_reg(rd, res_full & 0xFF) self._set_flags_sub(0, a, 0, res_full) - def op_swap(self, rd: int): - """Swap nibbles in register: Rd[7:4] <-> Rd[3:0]. Does not affect SREG. + def op_swap(self, rd: int) -> None: + """Swap Nibbles: Rd(7..4) <-> Rd(3..0). + + Status Register (SREG) per AVR Manual DS40002198A: + None affected. Args: rd: Destination register index. """ v = self.read_reg(rd) - nv = ((v & 0x0F) << 4) | ((v >> 4) & 0x0F) - self.write_reg(rd, nv) + self.write_reg(rd, ((v & 0x0F) << 4) | ((v >> 4) & 0x0F)) + + def op_tst(self, rd: int) -> None: + """Test for Zero or Minus: Rd & Rd. - def op_tst(self, rd: int): - """Test: perform AND Rd,Rd and update flags but do not store result. + Status Register (SREG) per AVR Manual DS40002198A: + S: N ⊕ V, V: 0, N: R7, Z: Set if Rd is $00 + Alternative name to the AND Rd, Rd instruction. Args: rd: Register index to test. """ - v = self.read_reg(rd) - res = v & v - self._set_flags_logical(res) + self.op_and(rd, rd) + + def op_andi(self, rd: int, imm: int) -> None: + """Logical AND with Immediate (ANDI). - def op_andi(self, rd: int, imm: int): - """Logical AND with immediate (Rd := Rd & K). + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- Rd • K (16 <= d <= 31, 0 <= K <= 255) + Status Register (SREG): S, V=0, N, Z (H, C unaffected) Args: - rd: Destination register index. + rd: Destination register index (16..31). imm: Immediate value. """ res = self.read_reg(rd) & (imm & 0xFF) self.write_reg(rd, res) self._set_flags_logical(res) - def op_ori(self, rd: int, imm: int): - """Logical OR with immediate (Rd := Rd | K). + def op_ori(self, rd: int, imm: int) -> None: + """Logical OR with Immediate (ORI). + + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- Rd v K (16 <= d <= 31, 0 <= K <= 255) + Status Register (SREG): S, V=0, N, Z (H, C unaffected) Args: - rd: Destination register index. + rd: Destination register index (16..31). imm: Immediate value. """ res = self.read_reg(rd) | (imm & 0xFF) self.write_reg(rd, res) self._set_flags_logical(res) - def op_eori(self, rd: int, imm: int): - """Logical EOR with immediate (Rd := Rd ^ K). + def op_subi(self, rd: int, imm: int) -> None: + """Subtract Immediate (SUBI). - Args: - rd: Destination register index. - imm: Immediate value. - """ - res = self.read_reg(rd) ^ (imm & 0xFF) - self.write_reg(rd, res) - self._set_flags_logical(res) - - def op_subi(self, rd: int, imm: int): - """Subtract immediate (Rd := Rd - K). + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- Rd - K (16 <= d <= 31, 0 <= K <= 255) + Status Register (SREG): H, S, V, N, Z, C Args: - rd: Destination register index. + rd: Destination register index (16..31). imm: Immediate value. """ a = self.read_reg(rd) @@ -876,8 +1030,12 @@ def op_subi(self, rd: int, imm: int): self.write_reg(rd, res_full & 0xFF) self._set_flags_sub(a, b, 0, res_full) - def op_sbc(self, rd: int, rr: int): - """Subtract with carry (Rd := Rd - Rr - C). + def op_sbc(self, rd: int, rr: int) -> None: + """Subtract with Carry (SBC). + + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- Rd - Rr - C + Status Register (SREG): H, S, V, N, Z, C Args: rd: Destination register index. @@ -885,35 +1043,53 @@ def op_sbc(self, rd: int, rr: int): """ a = self.read_reg(rd) b = self.read_reg(rr) - borrow_in = 1 if self.get_flag(SREG_C) else 0 + borrow_in = int(self.get_flag(SREG_C)) res_full = a - b - borrow_in self.write_reg(rd, res_full & 0xFF) self._set_flags_sub(a, b, borrow_in, res_full) - def op_sbci(self, rd: int, imm: int): - """Subtract immediate with carry: Rd := Rd - K - C. + def op_sbci(self, rd: int, imm: int) -> None: + """Subtract Immediate with Carry (SBCI). + + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- Rd - K - C (16 <= d <= 31, 0 <= K <= 255) + Status Register (SREG): H, S, V, N, Z, C Args: - rd: Destination register index. + rd: Destination register index (16..31). imm: Immediate value. """ a = self.read_reg(rd) b = imm & 0xFF - borrow_in = 1 if self.get_flag(SREG_C) else 0 + borrow_in = int(self.get_flag(SREG_C)) res_full = a - b - borrow_in self.write_reg(rd, res_full & 0xFF) self._set_flags_sub(a, b, borrow_in, res_full) - def op_sei(self): - """Set Global Interrupt Enable (I bit).""" + def op_sei(self) -> None: + """Set Global Interrupt Flag (SEI). + + Per AVR Instruction Set Manual DS40002198A: + Operation: I <- 1 + Status Register (SREG): I + """ self.set_flag(SREG_I, True) - def op_cli(self): - """Clear Global Interrupt Enable (I bit).""" + def op_cli(self) -> None: + """Clear Global Interrupt Flag (CLI). + + Per AVR Instruction Set Manual DS40002198A: + Operation: I <- 0 + Status Register (SREG): I + """ self.set_flag(SREG_I, False) - def op_cpse(self, rd: int, rr: int): - """Compare and Skip if Equal: compare Rd,Rr; if equal, skip next instruction. + def op_cpse(self, rd: int, rr: int) -> None: + """Compare Skip if Equal (CPSE). + + Per AVR Instruction Set Manual DS40002198A: + Operation: If Rd = Rr then PC <- PC + 2 (or 3) + Status Register (SREG): None (all flags unaffected) Args: rd: First register index. @@ -921,106 +1097,129 @@ def op_cpse(self, rd: int, rr: int): """ a = self.read_reg(rd) b = self.read_reg(rr) - self._set_flags_sub(a, b, 0, a - b) if a == b: self.pc += 1 - def op_sbrs(self, rd: int, bit: int): - """Skip next if bit in register is set. + def op_sbrs(self, rd: int, bit: int) -> None: + """Skip if Bit in Register is Set (SBRS). + + Per AVR Instruction Set Manual DS40002198A: + Operation: If Rd(b) = 1 then PC <- PC + 2 (or 3) + Status Register (SREG): None. Args: rd: Register index. - bit: Bit position to test. + bit: Bit index (0..7). """ - v = self.read_reg(rd) - if ((v >> (bit & 7)) & 1) == 1: + if ((self.read_reg(rd) >> (bit & 7)) & 1) == 1: self.pc += 1 - def op_sbrc(self, rd: int, bit: int): - """Skip next if bit in register is clear. + def op_sbrc(self, rd: int, bit: int) -> None: + """Skip if Bit in Register is Cleared (SBRC). + + Per AVR Instruction Set Manual DS40002198A: + Operation: If Rd(b) = 0 then PC <- PC + 2 (or 3) + Status Register (SREG): None. Args: rd: Register index. - bit: Bit position to test. + bit: Bit index (0..7). """ - v = self.read_reg(rd) - if ((v >> (bit & 7)) & 1) == 0: + if ((self.read_reg(rd) >> (bit & 7)) & 1) == 0: self.pc += 1 - def op_sbis(self, io_addr: int, bit: int): - """Skip if bit in IO/RAM-mapped address is set. + def op_sbis(self, io_addr: int, bit: int) -> None: + """Skip if Bit in I/O Register is Set (SBIS). + + Per AVR Instruction Set Manual DS40002198A: + Operation: If P(b) = 1 then PC <- PC + 2 (or 3) + Status Register (SREG): None. Args: io_addr: I/O or RAM address. - bit: Bit position to test. + bit: Bit index (0..7). """ - v = self.read_ram(io_addr) - if ((v >> (bit & 7)) & 1) == 1: + if ((self.read_ram(io_addr) >> (bit & 7)) & 1) == 1: self.pc += 1 - def op_sbic(self, io_addr: int, bit: int): - """Skip if bit in IO/RAM-mapped address is clear. + def op_sbic(self, io_addr: int, bit: int) -> None: + """Skip if Bit in I/O Register is Cleared (SBIC). + + Per AVR Instruction Set Manual DS40002198A: + Operation: If P(b) = 0 then PC <- PC + 2 (or 3) + Status Register (SREG): None. Args: io_addr: I/O or RAM address. - bit: Bit position to test. + bit: Bit index (0..7). """ - v = self.read_ram(io_addr) - if ((v >> (bit & 7)) & 1) == 0: + if ((self.read_ram(io_addr) >> (bit & 7)) & 1) == 0: self.pc += 1 - def op_sbiw(self, rd_word_low: int, imm_word: int): - """Subtract immediate from word register pair (Rd:Rd+1) — simplified. + def op_sbiw(self, rd_word_low: int, imm_word: int) -> None: + """Subtract Immediate from Word (SBIW). + + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd+1:Rd <- Rd+1:Rd - K (d in {24, 26, 28, 30}, 0 <= K <= 63) + Status Register (SREG): S, V, N, Z, C Args: - rd_word_low: Low register of the pair (even register index). - imm_word: 16-bit immediate to subtract. + rd_word_low: Low register index of register pair (e.g. 24, 26, 28, 30). + imm_word: Immediate value to subtract. """ lo = self.read_reg(rd_word_low) hi = self.read_reg(rd_word_low + 1) if (rd_word_low + 1) < 32 else 0 word = (hi << 8) | lo new = (word - (imm_word & 0xFFFF)) & 0xFFFF - new_lo = new & 0xFF - new_hi = (new >> 8) & 0xFF - self.write_reg(rd_word_low, new_lo) + self.write_reg(rd_word_low, new & 0xFF) if rd_word_low + 1 < 32: - self.write_reg(rd_word_low + 1, new_hi) + self.write_reg(rd_word_low + 1, (new >> 8) & 0xFF) self._set_flags_sub16(word, imm_word & 0xFFFF, 0, word - (imm_word & 0xFFFF)) - def op_adiw(self, rd_word_low: int, imm_word: int): - """Add immediate to word register pair (Rd:Rd+1) - simplified. + def op_adiw(self, rd_word_low: int, imm_word: int) -> None: + """Add Immediate to Word (ADIW). + + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd+1:Rd <- Rd+1:Rd + K (d in {24, 26, 28, 30}, 0 <= K <= 63) + Status Register (SREG): S, V, N, Z, C Args: - rd_word_low: Low register of the pair (even register index). - imm_word: 16-bit immediate to add. + rd_word_low: Low register index of register pair (e.g. 24, 26, 28, 30). + imm_word: Immediate value to add. """ lo = self.read_reg(rd_word_low) hi = self.read_reg(rd_word_low + 1) if (rd_word_low + 1) < 32 else 0 word = (hi << 8) | lo new = (word + (imm_word & 0xFFFF)) & 0xFFFF - new_lo = new & 0xFF - new_hi = (new >> 8) & 0xFF - self.write_reg(rd_word_low, new_lo) + self.write_reg(rd_word_low, new & 0xFF) if rd_word_low + 1 < 32: - self.write_reg(rd_word_low + 1, new_hi) + self.write_reg(rd_word_low + 1, (new >> 8) & 0xFF) self._set_flags_add16(word, imm_word & 0xFFFF, 0, word + (imm_word & 0xFFFF)) - def op_rjmp(self, label: str): - """Relative jump — label may be an int or string label. + def op_rjmp(self, label: str | int) -> None: + """Relative Jump (RJMP). + + Per AVR Instruction Set Manual DS40002198A: + Operation: PC <- PC + k + 1 + Status Register (SREG): None. Args: - label: Jump target (label name or relative offset). + label: Jump target offset or symbolic label. """ if isinstance(label, int): - self.pc = self.pc + int(label) + self.pc += label else: self.op_jmp(label) - def op_rcall(self, label: str): - """Relative call — push return address and jump relatively or to label. + def op_rcall(self, label: str | int) -> None: + """Relative Call to Subroutine (RCALL). + + Per AVR Instruction Set Manual DS40002198A: + Operation: PC <- PC + k + 1, STACK <- PC + 1 + Status Register (SREG): None. Args: - label: Call target (label name or relative offset). + label: Call target offset or symbolic label. """ ret = self.pc + 1 self.write_ram(self.sp, (ret >> 8) & 0xFF) @@ -1028,175 +1227,263 @@ def op_rcall(self, label: str): self.write_ram(self.sp, ret & 0xFF) self.sp -= 1 if isinstance(label, int): - target = self.pc + int(label) - self.pc = int(target) - 1 + self.pc = self.pc + label - 1 else: self.op_jmp(label) - def op_sbi(self, io_addr: int, bit: int): - """Set a bit in an I/O/memory-mapped address (use RAM area). + def op_sbi(self, io_addr: int, bit: int) -> None: + """Set Bit in I/O Register (SBI). + + Per AVR Instruction Set Manual DS40002198A: + Operation: I/O(P, b) <- 1 + Status Register (SREG): None. + + Args: + io_addr: Memory address. + bit: Bit index (0..7). + """ + self.write_ram(io_addr, self.read_ram(io_addr) | (1 << (bit & 7))) + + def op_cbi(self, io_addr: int, bit: int) -> None: + """Clear Bit in I/O Register (CBI). + + Per AVR Instruction Set Manual DS40002198A: + Operation: I/O(P, b) <- 0 + Status Register (SREG): None. + + Args: + io_addr: Memory address. + bit: Bit index (0..7). + """ + self.write_ram(io_addr, self.read_ram(io_addr) & ~(1 << (bit & 7))) + + def op_ld(self, rd: int, addr_reg: int) -> None: + """Load Indirect from Data Space to Register (LD). + + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- (X / Y / Z) + Status Register (SREG): None. + + Supports 16-bit pointer pairs (X=R27:R26, Y=R29:R28, Z=R31:R30) when high byte is set. Args: - io_addr: I/O or RAM address to modify. - bit: Bit index to set (0..7). + rd: Destination register index. + addr_reg: Pointer register index. """ - # set bit in I/O location (use RAM address space) - val = self.read_ram(io_addr) - nval = val | (1 << (bit & 7)) - self.write_ram(io_addr, nval) + high = self.read_reg(addr_reg + 1) if addr_reg in (26, 28, 30) else 0 + low = self.read_reg(addr_reg) + addr = (high << 8) | low if high > 0 else low + self.write_reg(rd, self.read_ram(addr)) + + def op_st(self, addr_reg: int, rr: int) -> None: + """Store Indirect from Register to Data Space (ST). - def op_cbi(self, io_addr: int, bit: int): - """Clear a bit in an I/O/memory-mapped address (use RAM area). + Per AVR Instruction Set Manual DS40002198A: + Operation: (X / Y / Z) <- Rr + Status Register (SREG): None. + + Supports 16-bit pointer pairs (X=R27:R26, Y=R29:R28, Z=R31:R30) when high byte is set. Args: - io_addr: I/O or RAM address to modify. - bit: Bit index to clear (0..7). + addr_reg: Pointer register index. + rr: Source register index. """ - val = self.read_ram(io_addr) - nval = val & ~(1 << (bit & 7)) - self.write_ram(io_addr, nval) + high = self.read_reg(addr_reg + 1) if addr_reg in (26, 28, 30) else 0 + low = self.read_reg(addr_reg) + addr = (high << 8) | low if high > 0 else low + self.write_ram(addr, self.read_reg(rr)) + + def op_lds(self, rd: int, addr: int) -> None: + """Load Direct from Data Space (LDS). - def op_ld(self, rd: int, addr_reg: int): - """Load from RAM at address contained in register ``addr_reg`` into - ``rd``. + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- (k) + Status Register (SREG): None. Args: rd: Destination register index. - addr_reg: Register index containing the RAM address to load from. + addr: Memory address (0..ram_size-1). """ - addr = self.read_reg(addr_reg) - val = self.read_ram(addr) - self.write_reg(rd, val) + self.write_reg(rd, self.read_ram(addr)) - def op_st(self, addr_reg: int, rr: int): - """Store register ``rr`` into RAM at address contained in register - ``addr_reg``. + def op_sts(self, addr: int, rr: int) -> None: + """Store Direct to Data Space (STS). + + Per AVR Instruction Set Manual DS40002198A: + Operation: (k) <- Rr + Status Register (SREG): None. Args: - addr_reg: Register index containing the RAM address to write to. - rr: Source register index to store. + addr: Memory address (0..ram_size-1). + rr: Source register index. """ - addr = self.read_reg(addr_reg) - val = self.read_reg(rr) - self.write_ram(addr, val) + self.write_ram(addr, self.read_reg(rr)) + + def op_brne(self, label: str | int) -> None: + """Branch if Not Equal (BRNE). - def op_brne(self, label: str): - """Branch to label if Zero flag is not set (BRNE). + Per AVR Instruction Set Manual DS40002198A: + Operation: If Z = 0 then PC <- PC + k + 1 + Status Register (SREG): None. Args: - label: Destination label to jump to if Z flag is not set. + label: Jump target label or numeric address. """ - z = self.get_flag(SREG_Z) - if not z: + if not (self.sreg & 0x02): self.op_jmp(label) - def op_breq(self, label: str): - """Branch to label if Zero flag is set (BREQ). + def op_breq(self, label: str | int) -> None: + """Branch if Equal (BREQ). + + Per AVR Instruction Set Manual DS40002198A: + Operation: If Z = 1 then PC <- PC + k + 1 + Status Register (SREG): None. Args: - label: Destination label to jump to if Z flag is set. + label: Jump target label or numeric address. """ - z = self.get_flag(SREG_Z) - if z: + if self.sreg & 0x02: self.op_jmp(label) - def op_brcs(self, label: str): - """Branch to a label if the carry flag is set. + def op_brcs(self, label: str | int) -> None: + """Branch if Carry Set (BRCS). + + Per AVR Instruction Set Manual DS40002198A: + Operation: If C = 1 then PC <- PC + k + 1 + Status Register (SREG): None. Args: - label (str): Destination label to jump to if the carry flag is set. + label: Jump target label or numeric address. """ - c = self.get_flag(SREG_C) - if c: + if self.sreg & 0x01: self.op_jmp(label) - def op_brcc(self, label: str): - """Branch to a label if the carry flag is clear. + def op_brcc(self, label: str | int) -> None: + """Branch if Carry Cleared (BRCC). + + Per AVR Instruction Set Manual DS40002198A: + Operation: If C = 0 then PC <- PC + k + 1 + Status Register (SREG): None. Args: - label (str): Destination label to jump to if the carry flag is clear. + label: Jump target label or numeric address. """ - c = self.get_flag(SREG_C) - if not c: + if not (self.sreg & 0x01): self.op_jmp(label) - def op_brge(self, label: str | int): - """Branch if Greater or Equal (Signed). + def op_brlo(self, label: str | int) -> None: + """Branch if Lower Unsigned (BRLO). + + Per AVR Instruction Set Manual DS40002198A: + Operation: If C = 1 then PC <- PC + k + 1 (alias for BRCS) + Status Register (SREG): None. Args: - label: Destination label or address to jump to if the condition is met. + label: Jump target label or numeric address. """ - s = self.get_flag(SREG_S) - if not s: + if self.sreg & 0x01: self.op_jmp(label) - def op_brlt(self, label: str | int): - """Branch if Less Than (Signed). + def op_brsh(self, label: str | int) -> None: + """Branch if Same or Higher Unsigned (BRSH). + + Per AVR Instruction Set Manual DS40002198A: + Operation: If C = 0 then PC <- PC + k + 1 (alias for BRCC) + Status Register (SREG): None. Args: - label: Destination label or address to jump to if the condition is met. + label: Jump target label or numeric address. """ - s = self.get_flag(SREG_S) - if s: + if not (self.sreg & 0x01): self.op_jmp(label) - def op_brmi(self, label: str | int): - """Branch if Minus (Negative flag set). + def op_brge(self, label: str | int) -> None: + """Branch if Greater or Equal Signed (BRGE). + + Per AVR Instruction Set Manual DS40002198A: + Operation: If S = 0 (N ⊕ V = 0) then PC <- PC + k + 1 + Status Register (SREG): None. Args: - label: Destination label or address to jump to if the condition is met. + label: Jump target label or numeric address. """ - n = self.get_flag(SREG_N) - if n: + if not (self.sreg & 0x10): self.op_jmp(label) - def op_brpl(self, label: str | int): - """Branch if Plus (Negative flag clear). + def op_brlt(self, label: str | int) -> None: + """Branch if Less Than Signed (BRLT). + + Per AVR Instruction Set Manual DS40002198A: + Operation: If S = 1 (N ⊕ V = 1) then PC <- PC + k + 1 + Status Register (SREG): None. Args: - label: Destination label or address to jump to if the condition is met. + label: Jump target label or numeric address. """ - n = self.get_flag(SREG_N) - if not n: + if self.sreg & 0x10: self.op_jmp(label) - def op_push(self, rr: int): - """Push a register value onto the stack. + def op_brmi(self, label: str | int) -> None: + """Branch if Minus (BRMI). + + Per AVR Instruction Set Manual DS40002198A: + Operation: If N = 1 then PC <- PC + k + 1 + Status Register (SREG): None. Args: - rr: Source register index to push. + label: Jump target label or numeric address. + """ + if self.sreg & 0x04: + self.op_jmp(label) - Note: - The value of register ``rr`` is written to RAM at the current - stack pointer, and the stack pointer is then decremented. + def op_brpl(self, label: str | int) -> None: + """Branch if Plus (BRPL). + + Per AVR Instruction Set Manual DS40002198A: + Operation: If N = 0 then PC <- PC + k + 1 + Status Register (SREG): None. + + Args: + label: Jump target label or numeric address. """ - val = self.read_reg(rr) - self.write_ram(self.sp, val) - self.sp -= 1 + if not (self.sreg & 0x04): + self.op_jmp(label) + + def op_push(self, rr: int) -> None: + """Push Register on Stack (PUSH). - def op_pop(self, rd: int): - """Pop a value from the stack into a register. + Per AVR Instruction Set Manual DS40002198A: + Operation: STACK <- Rr, SP <- SP - 1 + Status Register (SREG): None. Args: - rd: Destination register index to receive the popped value. + rr: Source register index. + """ + self.write_ram(self.sp, self.read_reg(rr)) + self.sp -= 1 - Note: - The stack pointer is incremented, the byte at the new stack pointer - is read from RAM, and the value is written into register ``rd``. + def op_pop(self, rd: int) -> None: + """Pop Register from Stack (POP). + + Per AVR Instruction Set Manual DS40002198A: + Operation: SP <- SP + 1, Rd <- STACK + Status Register (SREG): None. + + Args: + rd: Destination register index. """ self.sp += 1 - val = self.read_ram(self.sp) - self.write_reg(rd, val) + self.write_reg(rd, self.read_ram(self.sp)) - def op_call(self, label: str): - """Call a subroutine by pushing the return address and jumping to label. + def op_call(self, label: str | int) -> None: + """Long Call to Subroutine (CALL). - Args: - label: Label to call. + Per AVR Instruction Set Manual DS40002198A: + Operation: STACK <- PC + 1, PC <- k + Status Register (SREG): None. - Note: - The return address (pc+1) is pushed as two bytes (high then low) onto - the stack, decrementing the stack pointer after each write. + Args: + label: Subroutine target label or address. """ ret = self.pc + 1 self.write_ram(self.sp, (ret >> 8) & 0xFF) @@ -1205,40 +1492,38 @@ def op_call(self, label: str): self.sp -= 1 self.op_jmp(label) - def op_ret(self): - """Return from subroutine by popping the return address and setting PC. + def op_ret(self) -> None: + """Return from Subroutine (RET). - Note: - Two bytes are popped from the stack (low then high) to reconstruct the - return address, which is then loaded into the program counter. + Per AVR Instruction Set Manual DS40002198A: + Operation: PC <- STACK + Status Register (SREG): None. """ self.sp += 1 low = self.read_ram(self.sp) self.sp += 1 high = self.read_ram(self.sp) - ret = (high << 8) | low - self.pc = ret - 1 + self.pc = ((high << 8) | low) - 1 - def op_reti(self): - """Return from interrupt: pop return address and set I flag.""" + def op_reti(self) -> None: + """Return from Interrupt (RETI). + + Per AVR Instruction Set Manual DS40002198A: + Operation: PC <- STACK, I <- 1 + Status Register (SREG): I + """ self.sp += 1 low = self.read_ram(self.sp) self.sp += 1 high = self.read_ram(self.sp) - ret = (high << 8) | low self.set_flag(SREG_I, True) - self.pc = ret - 1 + self.pc = ((high << 8) | low) - 1 - def trigger_interrupt(self, vector_addr: int): - """Trigger an interrupt vector if it is enabled. + def trigger_interrupt(self, vector_addr: int) -> None: + """Trigger an interrupt if enabled in vector configuration. Args: - vector_addr: Interrupt vector address to jump to. - - Note: - If the interrupt vector is enabled in ``self.interrupts``, the current - PC+1 is pushed onto the stack (high then low byte) and control jumps - to ``vector_addr``. + vector_addr: Vector entry address in RAM. """ if not self.interrupts.get(vector_addr, False): return diff --git a/src/tiny8/memory.py b/src/tiny8/memory.py index a4b8ecc..69de4fa 100644 --- a/src/tiny8/memory.py +++ b/src/tiny8/memory.py @@ -1,140 +1,161 @@ -"""Memory model for tiny8 - simple RAM and ROM with change tracking.""" +"""Memory model for tiny8 providing RAM and ROM with change tracking.""" + +from collections import deque class Memory: - """Memory class for a simple byte-addressable RAM/ROM model. + """Byte-addressable RAM/ROM memory subsystem. - This class provides a minimal memory subsystem with separate RAM and ROM - regions, change-logging for writes/loads, and convenience snapshot - methods. All stored values are maintained as 8-bit unsigned bytes (0-255). + Maintains separate RAM and ROM buffers as 8-bit unsigned bytes (0-255) + and logs modifications for tracing and visualization. Args: - ram_size: Number of bytes in RAM (default 2048). - rom_size: Number of bytes in ROM (default 2048). + ram_size: Number of bytes in RAM. Defaults to 65536. + rom_size: Number of bytes in ROM. Defaults to 2048. Attributes: - ram: Mutable list representing RAM contents (each element 0-255). - rom: Mutable list representing ROM contents (each element 0-255). - ram_changes: Change log for RAM writes. Each entry is a tuple - (addr, old_value, new_value, step) appended only when a write - changes the stored byte. - rom_changes: Change log for ROM loads. Each entry is a tuple - (addr, old_value, new_value, step) appended when load_rom - changes bytes. - - Note: - - All write/load operations mask values with 0xFF so stored values are - always in the range 0..255. - - ram_changes and rom_changes record only actual changes (old != new). - - read_* methods perform bounds checking and raise IndexError for - invalid addresses. - - load_rom accepts an iterable of integers and raises ValueError if - the input is larger than the configured ROM size. + ram_size: Size of RAM in bytes. + rom_size: Size of ROM in bytes. + ram: Bytearray representing RAM contents. + rom: Bytearray representing ROM contents. + ram_changes: Log of RAM writes as (addr, old_val, new_val, step) tuples. + rom_changes: Log of ROM writes as (addr, old_val, new_val, step) tuples. """ - def __init__(self, ram_size: int = 2048, rom_size: int = 2048): + __slots__ = ( + "ram_size", + "rom_size", + "max_changes", + "track_changes", + "ram", + "rom", + "nonzero_ram", + "ram_changes", + "rom_changes", + ) + + def __init__( + self, + ram_size: int = 65536, + rom_size: int = 2048, + max_changes: int = 2000, + track_changes: bool = True, + ) -> None: self.ram_size = ram_size self.rom_size = rom_size - self.ram = [0] * ram_size - self.rom = [0] * rom_size - self.ram_changes: list[tuple[int, int, int, int]] = [] - self.rom_changes: list[tuple[int, int, int, int]] = [] + self.max_changes = max_changes + self.track_changes = track_changes + self.ram = bytearray(ram_size) + self.rom = bytearray(rom_size) + self.nonzero_ram: dict[int, int] = {} + self.ram_changes: deque[tuple[int, int, int, int]] = deque(maxlen=max_changes) + self.rom_changes: deque[tuple[int, int, int, int]] = deque(maxlen=max_changes) def read_ram(self, addr: int) -> int: - """Read and return the value stored in RAM at the specified address. + """Read a byte from RAM. Args: - addr: The RAM address to read. Must be within the valid range - 0 <= addr < self.ram_size. + addr: Memory address in the range [0, ram_size). Returns: - The integer value stored at the given RAM address. + The byte value stored at the address. Raises: - IndexError: If the provided addr is negative or not less than - self.ram_size. + IndexError: If addr is outside valid RAM boundaries. """ - if addr < 0 or addr >= self.ram_size: + if not (0 <= addr < self.ram_size): raise IndexError("RAM address out of range") return self.ram[addr] def write_ram(self, addr: int, value: int, step: int = 0) -> None: - """Write a byte to RAM at the specified address. + """Write a byte to RAM and log change if value changed. Args: - addr: Target RAM address. Must be in the range [0, self.ram_size). - value: Value to write; only the low 8 bits are stored (value & 0xFF). - step: Optional step/timestamp associated with this write; defaults to 0. + addr: Memory address in the range [0, ram_size). + value: Byte value to store (masked to 8-bit). + step: Step index associated with this write. Defaults to 0. Raises: - IndexError: If addr is out of the valid RAM range. - - Note: - The provided value is masked to a single byte before storing. If - the stored byte changes, a record (addr, old_value, new_value, - step) is appended to self.ram_changes to track the modification. + IndexError: If addr is outside valid RAM boundaries. """ - if addr < 0 or addr >= self.ram_size: + if not (0 <= addr < self.ram_size): raise IndexError("RAM address out of range") old = self.ram[addr] - self.ram[addr] = value & 0xFF - if old != self.ram[addr]: - self.ram_changes.append((addr, old, self.ram[addr], step)) + new = value & 0xFF + if old == new: + return + self.ram[addr] = new + if self.track_changes: + self.ram_changes.append((addr, old, new, step)) + if new != 0: + self.nonzero_ram[addr] = new + elif old != 0: + self.nonzero_ram.pop(addr, None) + + def clear_changes(self) -> None: + """Clear memory modification history to free memory.""" + self.ram_changes.clear() + self.rom_changes.clear() + + def restore_ram(self, mem_dict: dict[int, int]) -> None: + """Restore RAM contents from a dictionary of non-zero entries.""" + for addr in list(self.nonzero_ram.keys()): + self.ram[addr] = 0 + self.nonzero_ram = dict(mem_dict) + for addr, val in self.nonzero_ram.items(): + if 0 <= addr < self.ram_size: + self.ram[addr] = val & 0xFF def load_rom(self, data: list[int]) -> None: - """Load a ROM image into the emulator's ROM buffer. + """Load byte sequence into ROM and record changes. Args: - data: Sequence of integer byte values (expected 0-255) comprising - the ROM image. Values outside 0-255 will be truncated to 8 bits. + data: Sequence of byte values to load into ROM. Raises: - ValueError: If len(data) is greater than self.rom_size. - - Note: - Overwrites self.rom[i] for i in range(len(data)) with - (data[i] & 0xFF). Appends (index, old_value, new_value, 0) to - self.rom_changes for each address where the value actually changed. + ValueError: If data length exceeds configured rom_size. """ if len(data) > self.rom_size: raise ValueError("ROM image too large") - for i, v in enumerate(data): - old = self.rom[i] - self.rom[i] = v & 0xFF - if old != self.rom[i]: - self.rom_changes.append((i, old, self.rom[i], 0)) + if self.track_changes: + for i, v in enumerate(data): + old = self.rom[i] + new = v & 0xFF + self.rom[i] = new + if old != new: + self.rom_changes.append((i, old, new, 0)) + else: + for i, v in enumerate(data): + self.rom[i] = v & 0xFF def read_rom(self, addr: int) -> int: - """Read a value from the ROM at the specified address. + """Read a byte from ROM. Args: - addr: Zero-based address within ROM to read. + addr: ROM address in the range [0, rom_size). Returns: - The value stored at the given ROM address. + The byte value stored at the address. Raises: - IndexError: If ``addr`` is negative or greater than or equal to - ``self.rom_size``. + IndexError: If addr is outside valid ROM boundaries. """ - if addr < 0 or addr >= self.rom_size: + if not (0 <= addr < self.rom_size): raise IndexError("ROM address out of range") return self.rom[addr] def snapshot_ram(self) -> list[int]: - """Return a snapshot copy of the emulator's RAM. + """Return a shallow copy of RAM contents. Returns: - A new list containing the current contents of RAM. Each element - represents a byte (0-255). Modifying the returned list will not - affect the emulator's internal state. + List of integers representing current RAM. """ return list(self.ram) def snapshot_rom(self) -> list[int]: - """Return a snapshot copy of the ROM contents. + """Return a shallow copy of ROM contents. Returns: - A list of integers representing the ROM data at the time of the call. + List of integers representing current ROM. """ return list(self.rom) diff --git a/tests/test_arithmetic.py b/tests/test_arithmetic.py index 6074a69..c9534a8 100644 --- a/tests/test_arithmetic.py +++ b/tests/test_arithmetic.py @@ -1,6 +1,6 @@ """Test suite for arithmetic instructions. -Covers: ADD, ADC, SUB, SUBI, SBC, SBCI, INC, DEC, MUL, DIV, NEG, ADIW, SBIW +Covers: ADD, ADC, SUB, SUBI, SBC, SBCI, INC, DEC, MUL, NEG, ADIW, SBIW """ import pytest @@ -291,9 +291,9 @@ def test_mul_with_overflow(self, cpu_with_program, helper): ldi r3, 100 mul r2, r3 """) - # 200 * 100 = 20000 = 0x4E20 - helper.assert_register(cpu, 2, 0x20) # Low byte - helper.assert_register(cpu, 3, 0x4E) # High byte + # 200 * 100 = 20000 = 0x4E20 -> R1:R0 = 0x4E:0x20 per AVR DS40002198A + helper.assert_register(cpu, 0, 0x20) # R0: Low byte + helper.assert_register(cpu, 1, 0x4E) # R1: High byte def test_mul_zero(self, cpu_with_program, helper): """Test multiplication by zero.""" @@ -302,8 +302,8 @@ def test_mul_zero(self, cpu_with_program, helper): ldi r5, 0 mul r4, r5 """) - helper.assert_register(cpu, 4, 0) - helper.assert_register(cpu, 5, 0) + helper.assert_register(cpu, 0, 0) + helper.assert_register(cpu, 1, 0) helper.assert_flag(cpu, SREG_Z, True, "Z") @pytest.mark.parametrize( @@ -319,70 +319,14 @@ def test_mul_zero(self, cpu_with_program, helper): def test_mul_parametrized(self, cpu_with_program, helper, a, b, low, high): """Test MUL with various input combinations.""" cpu = cpu_with_program(f""" - ldi r0, {a} - ldi r1, {b} - mul r0, r1 + ldi r2, {a} + ldi r3, {b} + mul r2, r3 """) helper.assert_register(cpu, 0, low) helper.assert_register(cpu, 1, high) -class TestDIV: - """Test DIV (Division) instruction.""" - - def test_div_basic(self, cpu_with_program, helper): - """Test basic division.""" - cpu = cpu_with_program(""" - ldi r0, 42 - ldi r1, 7 - div r0, r1 - """) - helper.assert_register(cpu, 0, 6) # Quotient - helper.assert_register(cpu, 1, 0) # Remainder - - def test_div_with_remainder(self, cpu_with_program, helper): - """Test division with remainder.""" - cpu = cpu_with_program(""" - ldi r2, 100 - ldi r3, 7 - div r2, r3 - """) - helper.assert_register(cpu, 2, 14) # 100 // 7 = 14 - helper.assert_register(cpu, 3, 2) # 100 % 7 = 2 - - def test_div_by_one(self, cpu_with_program, helper): - """Test division by 1.""" - cpu = cpu_with_program(""" - ldi r4, 123 - ldi r5, 1 - div r4, r5 - """) - helper.assert_register(cpu, 4, 123) - helper.assert_register(cpu, 5, 0) - - @pytest.mark.parametrize( - "dividend,divisor,quotient,remainder", - [ - (10, 3, 3, 1), - (20, 4, 5, 0), - (255, 16, 15, 15), - (100, 100, 1, 0), - (7, 10, 0, 7), - ], - ) - def test_div_parametrized( - self, cpu_with_program, helper, dividend, divisor, quotient, remainder - ): - """Test DIV with various input combinations.""" - cpu = cpu_with_program(f""" - ldi r0, {dividend} - ldi r1, {divisor} - div r0, r1 - """) - helper.assert_register(cpu, 0, quotient) - helper.assert_register(cpu, 1, remainder) - - class TestNEG: """Test NEG (Two's complement negation) instruction.""" @@ -436,3 +380,33 @@ def test_sbiw_basic(self, cpu_with_program, helper): """) helper.assert_register(cpu, 24, 0x02) helper.assert_register(cpu, 25, 0x00) + + def test_adiw_preserves_h_flag(self, cpu_with_program, helper): + """Test ADIW preserves H flag per AVR DS40002198A.""" + cpu = cpu_with_program(""" + ldi r24, 0xFF + ldi r25, 0x00 + adiw r24, 1 + """) + cpu.set_flag(SREG_H, True) + cpu.step() # ldi + cpu.step() # ldi + cpu.step() # adiw + helper.assert_flag(cpu, SREG_H, True, "H") + helper.assert_register(cpu, 24, 0x00) + helper.assert_register(cpu, 25, 0x01) + helper.assert_flag(cpu, SREG_Z, False, "Z") + + def test_sbiw_preserves_h_flag(self, cpu_with_program, helper): + """Test SBIW preserves H flag per AVR DS40002198A.""" + cpu = cpu_with_program(""" + ldi r24, 0x01 + ldi r25, 0x00 + sbiw r24, 1 + """) + cpu.set_flag(SREG_H, True) + cpu.step() # ldi + cpu.step() # ldi + cpu.step() # sbiw + helper.assert_flag(cpu, SREG_H, True, "H") + helper.assert_flag(cpu, SREG_Z, True, "Z") diff --git a/tests/test_assembler.py b/tests/test_assembler.py index 4ebf3d5..20380db 100644 --- a/tests/test_assembler.py +++ b/tests/test_assembler.py @@ -91,3 +91,12 @@ def test_assemble_file_with_actual_file(self, tmp_path): result = assemble_file(str(asm_file)) assert len(result.program) == 2 + + def test_parse_number_empty_token(self): + """Test parsing empty token or hash only raises ValueError.""" + from tiny8.assembler import _parse_number + + with pytest.raises(ValueError, match="Unable to parse numeric token"): + _parse_number("") + with pytest.raises(ValueError, match="Unable to parse numeric token"): + _parse_number("#") diff --git a/tests/test_control_flow.py b/tests/test_control_flow.py index f4568c3..b2f96b8 100644 --- a/tests/test_control_flow.py +++ b/tests/test_control_flow.py @@ -5,7 +5,7 @@ import pytest -from tiny8.cpu import SREG_C, SREG_Z +from tiny8.cpu import SREG_C, SREG_I, SREG_Z class TestJMP: @@ -166,6 +166,51 @@ def test_brlt_less_than(self, cpu_with_program, helper): """) helper.assert_register(cpu, 2, 1) + def test_brmi_and_brpl_branches(self, cpu_with_program, helper): + """Test BRMI (minus) and BRPL (plus) branch behaviors.""" + # 10 - 20 is negative -> BRMI taken + cpu1 = cpu_with_program(""" + ldi r0, 10 + ldi r1, 20 + sub r0, r1 + brmi minus_taken + ldi r2, 0 + jmp done1 + minus_taken: + ldi r2, 1 + done1: + nop + """) + helper.assert_register(cpu1, 2, 1) + + # 20 - 10 is positive -> BRPL taken + cpu2 = cpu_with_program(""" + ldi r0, 20 + ldi r1, 10 + sub r0, r1 + brpl plus_taken + ldi r2, 0 + jmp done2 + plus_taken: + ldi r2, 1 + done2: + nop + """) + helper.assert_register(cpu2, 2, 1) + + def test_branch_with_numeric_target(self, cpu_with_program, helper): + """Test branching using explicit numeric instruction address.""" + cpu = cpu_with_program(""" + ldi r0, 100 + ldi r1, 200 + sub r0, r1 + """) + # Append BRMI jumping directly to instruction index 0 + cpu.program.append(("BRMI", (0,))) + cpu.pc = len(cpu.program) - 1 + cpu.step() + assert cpu.pc == 0 + @pytest.mark.parametrize( "a,b,branch,expected", [ @@ -173,6 +218,8 @@ def test_brlt_less_than(self, cpu_with_program, helper): (10, 5, "brne", 1), # Not equal (5, 10, "brcs", 1), # Carry set (a < b) (10, 5, "brcc", 1), # Carry clear (a >= b) + (5, 10, "brlo", 1), # Lower unsigned + (10, 5, "brsh", 1), # Same or higher unsigned ], ) def test_branches_parametrized( @@ -262,6 +309,21 @@ def test_call_preserves_registers(self, cpu_with_program, helper): # 100 + 50 = 150, r1 should be preserved as 200 helper.assert_registers(cpu, {0: 150, 1: 200}) + def test_reti_restores_interrupt_flag(self, cpu_with_program, helper): + """Test RETI restores PC and sets Global Interrupt flag (I=1).""" + cpu = cpu_with_program(""" + cli + call isr + jmp done + isr: + ldi r0, 77 + reti + done: + nop + """) + helper.assert_register(cpu, 0, 77) + helper.assert_flag(cpu, SREG_I, True, "I") + class TestCompare: """Test CP, CPC, CPI (Compare) instructions.""" diff --git a/tests/test_cpu_branches.py b/tests/test_cpu_branches.py deleted file mode 100644 index df9c9dc..0000000 --- a/tests/test_cpu_branches.py +++ /dev/null @@ -1,347 +0,0 @@ -"""CPU branch instruction tests. - -Tests for conditional branches, stack operations, calls, and returns. -""" - -from tiny8 import CPU, assemble - - -class TestConditionalBranches: - """Test conditional branch instructions.""" - - def test_brmi_not_taken(self): - """Test branch if minus when not taken.""" - cpu = CPU() - asm = assemble(""" - ldi r16, 100 - ldi r17, 50 - sub r16, r17 - brmi skip - ldi r18, 1 - skip: - ldi r19, 2 - """) - cpu.load_program(asm) - cpu.run() - assert cpu.read_reg(18) == 1 # Not skipped - - def test_brmi_jump_taken(self): - """Test BRMI when negative flag is set and jump is taken.""" - cpu = CPU() - asm = assemble(""" - ldi r16, 100 - ldi r17, 200 - sub r16, r17 - brmi target - ldi r18, 1 - target: - ldi r19, 2 - """) - cpu.load_program(asm) - cpu.run() - # The jump should be taken because 100 - 200 is negative - assert cpu.regs[19] == 2 - assert cpu.regs[18] == 0 # This should not execute - - def test_brpl_not_taken(self): - """Test branch if plus when not taken.""" - cpu = CPU() - asm = assemble(""" - ldi r16, 50 - ldi r17, 100 - sub r16, r17 - brpl skip - ldi r18, 1 - skip: - ldi r19, 2 - """) - cpu.load_program(asm) - cpu.run() - assert cpu.read_reg(18) == 1 # Not skipped - - def test_brpl_jump_taken(self): - """Test BRPL when negative flag is clear and jump is taken.""" - cpu = CPU() - asm = assemble(""" - ldi r16, 200 - ldi r17, 100 - sub r16, r17 - brpl target - ldi r18, 1 - target: - ldi r19, 2 - """) - cpu.load_program(asm) - cpu.run() - # The jump should be taken because 200 - 100 is positive - assert cpu.regs[19] == 2 - assert cpu.regs[18] == 0 # This should not execute - - def test_brge_taken(self): - """Test branch if greater or equal when taken.""" - cpu = CPU() - asm = assemble(""" - ldi r16, 100 - ldi r17, 50 - cp r16, r17 - brge skip - ldi r18, 99 - skip: - ldi r19, 1 - """) - cpu.load_program(asm) - cpu.run() - assert cpu.read_reg(18) == 0 # Skipped - assert cpu.read_reg(19) == 1 - - def test_brge_not_taken(self): - """Test branch if greater or equal when not taken.""" - cpu = CPU() - asm = assemble(""" - ldi r16, 50 - ldi r17, 100 - cp r16, r17 - brge skip - ldi r18, 1 - skip: - ldi r19, 2 - """) - cpu.load_program(asm) - cpu.run() - assert cpu.read_reg(18) == 1 # Not skipped - - def test_brlt_taken(self): - """Test branch if less than when taken.""" - cpu = CPU() - asm = assemble(""" - ldi r16, 50 - ldi r17, 100 - cp r16, r17 - brlt skip - ldi r18, 99 - skip: - ldi r19, 1 - """) - cpu.load_program(asm) - cpu.run() - assert cpu.read_reg(18) == 0 # Skipped - assert cpu.read_reg(19) == 1 - - def test_brlt_not_taken(self): - """Test branch if less than when not taken.""" - cpu = CPU() - asm = assemble(""" - ldi r16, 100 - ldi r17, 50 - cp r16, r17 - brlt skip - ldi r18, 1 - skip: - ldi r19, 2 - """) - cpu.load_program(asm) - cpu.run() - assert cpu.read_reg(18) == 1 # Not skipped - - def test_brpl_jump_case(self): - """Test BRPL actually jumping.""" - cpu = CPU() - asm = assemble(""" - ldi r16, 100 - ldi r17, 50 - sub r16, r17 - ldi r18, 0 - brpl target - ldi r18, 99 - target: - ldi r19, 1 - """) - cpu.load_program(asm) - cpu.run() - # With positive result, should jump and skip r18 assignment - assert cpu.read_reg(18) == 0 - - def test_brmi_with_integer_label(self): - """Test BRMI with integer label.""" - cpu = CPU() - asm = assemble(""" - ldi r16, 200 - ldi r17, 100 - sub r16, r17 - """) - cpu.load_program(asm) - cpu.run() - # Now manually test brmi with integer - cpu.program.append(("BRMI", (0,))) - cpu.pc = len(cpu.program) - 1 - cpu.step() - - def test_brpl_with_integer_label(self): - """Test BRPL with integer label.""" - cpu = CPU() - asm = assemble(""" - ldi r16, 100 - ldi r17, 50 - sub r16, r17 - """) - cpu.load_program(asm) - cpu.run() - # Now manually test brpl with integer - cpu.program.append(("BRPL", (0,))) - cpu.pc = len(cpu.program) - 1 - cpu.step() - - -class TestStackAndCall: - """Test stack operations, calls, and returns.""" - - def test_push_pop(self): - """Test push and pop instructions.""" - cpu = CPU() - asm = assemble(""" - ldi r16, 42 - push r16 - ldi r16, 0 - pop r17 - """) - cpu.load_program(asm) - cpu.run() - assert cpu.read_reg(17) == 42 - - def test_multiple_push_pop(self): - """Test multiple push and pop operations.""" - cpu = CPU() - asm = assemble(""" - ldi r16, 10 - ldi r17, 20 - ldi r18, 30 - push r16 - push r17 - push r18 - pop r19 - pop r20 - pop r21 - """) - cpu.load_program(asm) - cpu.run() - assert cpu.read_reg(19) == 30 - assert cpu.read_reg(20) == 20 - assert cpu.read_reg(21) == 10 - - def test_call_ret(self): - """Test call and ret instructions.""" - cpu = CPU() - asm = assemble(""" - call func - ldi r16, 1 - jmp done - func: - ldi r17, 2 - ret - done: - nop - """) - cpu.load_program(asm) - cpu.run() - assert cpu.read_reg(16) == 1 - assert cpu.read_reg(17) == 2 - - def test_nested_calls(self): - """Test nested function calls.""" - cpu = CPU() - asm = assemble(""" - call func1 - ldi r16, 1 - jmp done - func1: - call func2 - ldi r17, 2 - ret - func2: - ldi r18, 3 - ret - done: - nop - """) - cpu.load_program(asm) - cpu.run() - assert cpu.read_reg(16) == 1 - assert cpu.read_reg(17) == 2 - assert cpu.read_reg(18) == 3 - - def test_reti(self): - """Test return from interrupt.""" - cpu = CPU() - asm = assemble(""" - call isr - ldi r16, 1 - jmp done - isr: - ldi r17, 2 - reti - done: - nop - """) - cpu.load_program(asm) - cpu.run() - assert cpu.read_reg(16) == 1 - assert cpu.read_reg(17) == 2 - - -class TestSetClearInstructions: - """Test SET and CLEAR type instructions.""" - - def test_ser_clr(self): - """Test SER and CLR instructions.""" - cpu = CPU() - asm = assemble(""" - ser r16 - clr r17 - """) - cpu.load_program(asm) - cpu.run() - assert cpu.read_reg(16) == 0xFF - assert cpu.read_reg(17) == 0 - - def test_adc_with_carry(self): - """Test ADC with carry flag set.""" - cpu = CPU() - asm = assemble(""" - ldi r16, 5 - ldi r17, 10 - add r16, r17 - ldi r18, 20 - ldi r19, 30 - adc r18, r19 - """) - cpu.load_program(asm) - cpu.run() - # r18 should be 20 + 30 + carry (if any) - - -class TestLogicalOperations: - """Test logical operations for branch coverage.""" - - def test_or_operation_edge_case(self): - """Test OR operation with different values.""" - cpu = CPU() - asm = assemble(""" - ldi r16, 0x0F - ldi r17, 0xF0 - or r16, r17 - """) - cpu.load_program(asm) - cpu.run() - assert cpu.read_reg(16) == 0xFF - - def test_eor_operation_edge_case(self): - """Test EOR operation with same values.""" - cpu = CPU() - asm = assemble(""" - ldi r16, 0xFF - ldi r17, 0xFF - eor r16, r17 - """) - cpu.load_program(asm) - cpu.run() - assert cpu.read_reg(16) == 0 diff --git a/tests/test_cpu_core.py b/tests/test_cpu_core.py index 1300b17..6ca87a9 100644 --- a/tests/test_cpu_core.py +++ b/tests/test_cpu_core.py @@ -113,19 +113,48 @@ def test_ori_instruction(self): cpu.run() assert cpu.read_reg(16) == 0b11111111 - def test_eori_instruction(self): - """Test XOR with immediate.""" + def test_asr_instruction(self): + """Test arithmetic shift right preserves sign bit and updates flags.""" + from tiny8.cpu import SREG_C, SREG_N + cpu = CPU() asm = assemble(""" - ldi r16, 0b11110000 - eori r16, 0b11111111 + ldi r16, 0b10000001 + asr r16 """) cpu.load_program(asm) cpu.run() - assert cpu.read_reg(16) == 0b00001111 + assert cpu.read_reg(16) == 0b11000000 + assert cpu.get_flag(SREG_C) is True + assert cpu.get_flag(SREG_N) is True + + def test_muls_and_mulsu_instructions(self): + """Test signed and signed-with-unsigned multiplications.""" + cpu = CPU() + asm = assemble(""" + ldi r16, 0xFE + ldi r17, 3 + muls r16, r17 + ldi r18, 0xFE + ldi r19, 100 + mulsu r18, r19 + """) + cpu.load_program(asm) + cpu.step() + cpu.step() + cpu.step() + assert cpu.read_reg(0) == 0xFA + assert cpu.read_reg(1) == 0xFF + cpu.step() + cpu.step() + cpu.step() + assert cpu.read_reg(0) == 0x38 + assert cpu.read_reg(1) == 0xFF def test_tst_instruction(self): """Test for zero or negative.""" + from tiny8.cpu import SREG_N, SREG_Z + cpu = CPU() asm = assemble(""" ldi r16, 0 @@ -133,6 +162,21 @@ def test_tst_instruction(self): """) cpu.load_program(asm) cpu.run() + assert cpu.read_reg(16) == 0 + assert cpu.get_flag(SREG_Z) is True + assert cpu.get_flag(SREG_N) is False + + # Also test TST with negative value + cpu2 = CPU() + asm2 = assemble(""" + ldi r16, 0x80 + tst r16 + """) + cpu2.load_program(asm2) + cpu2.run() + assert cpu2.read_reg(16) == 0x80 + assert cpu2.get_flag(SREG_N) is True + assert cpu2.get_flag(SREG_Z) is False class TestImmediateArithmetic: @@ -158,6 +202,7 @@ def test_sbci_instruction(self): """) cpu.load_program(asm) cpu.run() + assert cpu.read_reg(16) == 15 def test_sbc_instruction(self): """Test subtract with carry.""" @@ -169,6 +214,7 @@ def test_sbc_instruction(self): """) cpu.load_program(asm) cpu.run() + assert cpu.read_reg(16) == 5 class TestSkipInstructions: @@ -375,18 +421,6 @@ def test_sei_cli_instructions(self): class TestArithmeticEdgeCases: """Test arithmetic edge cases.""" - def test_div_by_zero(self): - """Test division by zero.""" - cpu = CPU() - asm = assemble(""" - ldi r16, 10 - ldi r17, 0 - div r16, r17 - """) - cpu.load_program(asm) - cpu.run() - # Division by zero sets result to 0xFF and 0 - def test_mul_overflow(self): """Test multiplication overflow.""" cpu = CPU() @@ -397,4 +431,6 @@ def test_mul_overflow(self): """) cpu.load_program(asm) cpu.run() - # Result is 16-bit in r0:r1 + # 255 * 2 = 510 = 0x01FE -> r0=0xFE, r1=0x01 + assert cpu.read_reg(0) == 0xFE + assert cpu.read_reg(1) == 0x01 diff --git a/tests/test_cpu_edge_cases.py b/tests/test_cpu_edge_cases.py index a3496ae..8937a07 100644 --- a/tests/test_cpu_edge_cases.py +++ b/tests/test_cpu_edge_cases.py @@ -20,17 +20,15 @@ def test_invalid_instruction(self): ): cpu.step() - def test_operand_formatting_exception(self): - """Test that operand formatting exceptions are handled.""" + def test_operand_formatting_none_operand(self): + """Test formatting instruction when operand list contains None.""" cpu = CPU() - cpu.program = [("LDI", (None,))] - try: - cpu.step() - except Exception: - pass + cpu.program = [("NOP", ())] + cpu._build_predecoded() + assert cpu._predecoded[0][2] == "NOP" def test_operand_formatting_with_exception(self): - """Test operand formatting exception handling in step function.""" + """Test operand formatting fallback when operand str() raises an exception.""" cpu = CPU() class BadOperand: @@ -41,11 +39,9 @@ def __repr__(self): raise ValueError("Cannot format") cpu.program = [("LDI", (("reg", 16), BadOperand()))] - cpu.pc = 0 - try: - cpu.step() - except Exception: - pass + cpu._build_predecoded() + # Fallback catches the exception and falls back to base instruction mnemonic + assert cpu._predecoded[0][2] == "LDI" def test_jmp_with_invalid_label(self): """Test JMP with invalid label raises KeyError.""" @@ -157,6 +153,8 @@ class TestCPUFlagOperations: def test_set_flags_logical_negative(self): """Test _set_flags_logical with negative result.""" + from tiny8.cpu import SREG_N, SREG_S, SREG_V, SREG_Z + cpu = CPU() asm = assemble(""" ldi r16, 0x80 @@ -165,10 +163,16 @@ def test_set_flags_logical_negative(self): """) cpu.load_program(asm) cpu.run() - # Result should be 0x80 which has bit 7 set (negative) + assert cpu.read_reg(16) == 0x80 + assert cpu.get_flag(SREG_N) is True + assert cpu.get_flag(SREG_Z) is False + assert cpu.get_flag(SREG_V) is False + assert cpu.get_flag(SREG_S) is True def test_set_flags_logical_zero(self): """Test _set_flags_logical with zero result.""" + from tiny8.cpu import SREG_N, SREG_S, SREG_V, SREG_Z + cpu = CPU() asm = assemble(""" ldi r16, 0xFF @@ -177,10 +181,14 @@ def test_set_flags_logical_zero(self): """) cpu.load_program(asm) cpu.run() - # Result should be 0 + assert cpu.read_reg(16) == 0x00 + assert cpu.get_flag(SREG_Z) is True + assert cpu.get_flag(SREG_N) is False + assert cpu.get_flag(SREG_V) is False + assert cpu.get_flag(SREG_S) is False def test_mul_result_storage(self): - """Test MUL stores result in rd:rd+1.""" + """Test MUL stores result in R1:R0 per AVR DS40002198A.""" cpu = CPU() asm = assemble(""" ldi r16, 10 @@ -189,20 +197,205 @@ def test_mul_result_storage(self): """) cpu.load_program(asm) cpu.run() - # Result 200 = 0x00C8, so r16=200, r17=0 - assert cpu.read_reg(16) == 200 # low byte - assert cpu.read_reg(17) == 0 # high byte + # Result 200 = 0x00C8, so r0=200, r1=0 + assert cpu.read_reg(0) == 200 # low byte in R0 + assert cpu.read_reg(1) == 0 # high byte in R1 + + +class TestSREGCompliance: + """Comprehensive tests for AVR DS40002198A Status Register (SREG) compliance.""" + + def test_sreg_bit_positions(self): + """Verify exact SREG bit indices: I=7, T=6, H=5, S=4, V=3, N=2, Z=1, C=0.""" + from tiny8.cpu import ( + SREG_C, + SREG_H, + SREG_I, + SREG_N, + SREG_S, + SREG_T, + SREG_V, + SREG_Z, + ) + + cpu = CPU() + flags = [ + (SREG_C, 0, 0x01), + (SREG_Z, 1, 0x02), + (SREG_N, 2, 0x04), + (SREG_V, 3, 0x08), + (SREG_S, 4, 0x10), + (SREG_H, 5, 0x20), + (SREG_T, 6, 0x40), + (SREG_I, 7, 0x80), + ] + for flag_const, bit_idx, bit_mask in flags: + assert flag_const == bit_idx + cpu.sreg = 0 + cpu.set_flag(flag_const, True) + assert cpu.sreg == bit_mask + assert cpu.get_flag(flag_const) is True + cpu.set_flag(flag_const, False) + assert cpu.sreg == 0 + assert cpu.get_flag(flag_const) is False + + def test_sreg_flags_add_half_carry_and_overflow(self): + """Test ADD calculates H, V, S, N, Z, C and preserves I, T.""" + from tiny8.cpu import ( + SREG_C, + SREG_H, + SREG_I, + SREG_N, + SREG_S, + SREG_T, + SREG_V, + SREG_Z, + ) + + cpu = CPU() + cpu.set_flag(SREG_I, True) + cpu.set_flag(SREG_T, True) + # 0x0F + 0x01 = 0x10 (Half Carry set, no overflow, no carry, not zero) + cpu._set_flags_add(0x0F, 0x01, 0, 0x10) + assert cpu.get_flag(SREG_I) is True + assert cpu.get_flag(SREG_T) is True + assert cpu.get_flag(SREG_H) is True + assert cpu.get_flag(SREG_C) is False + assert cpu.get_flag(SREG_Z) is False + assert cpu.get_flag(SREG_N) is False + assert cpu.get_flag(SREG_V) is False + assert cpu.get_flag(SREG_S) is False + + # Signed overflow: 127 + 1 = 128 (0x7F + 0x01 = 0x80) + # Pos + Pos = Neg -> V=1, N=1, S = N ^ V = 0 + cpu._set_flags_add(0x7F, 0x01, 0, 0x80) + assert cpu.get_flag(SREG_V) is True + assert cpu.get_flag(SREG_N) is True + assert cpu.get_flag(SREG_S) is False + assert cpu.get_flag(SREG_H) is True + assert cpu.get_flag(SREG_C) is False + + def test_sreg_flags_sub_half_borrow_and_overflow(self): + """Test SUB calculates H, V, S, N, Z, C and preserves I, T.""" + from tiny8.cpu import ( + SREG_C, + SREG_H, + SREG_N, + SREG_S, + SREG_V, + SREG_Z, + ) + + cpu = CPU() + # 0x10 - 0x01 = 0x0F (Half borrow: (0 - 1) < 0 -> H=1, no unsigned borrow -> C=0) + cpu._set_flags_sub(0x10, 0x01, 0, 0x0F) + assert cpu.get_flag(SREG_H) is True + assert cpu.get_flag(SREG_C) is False + assert cpu.get_flag(SREG_Z) is False + assert cpu.get_flag(SREG_N) is False + assert cpu.get_flag(SREG_V) is False + assert cpu.get_flag(SREG_S) is False + + # Signed underflow: -128 - 1 = +127 (0x80 - 0x01 = 0x7F) + # Neg - Pos = Pos -> V=1, N=0, S = N ^ V = 1 + cpu._set_flags_sub(0x80, 0x01, 0, 0x7F) + assert cpu.get_flag(SREG_V) is True + assert cpu.get_flag(SREG_N) is False + assert cpu.get_flag(SREG_S) is True + assert cpu.get_flag(SREG_C) is False + + def test_sreg_logical_preserves_c_h_t_i(self): + """Test logical ops (AND, OR, EOR) clear V, set S=N, and preserve C, H, T, I.""" + from tiny8.cpu import ( + SREG_C, + SREG_H, + SREG_I, + SREG_N, + SREG_S, + SREG_T, + SREG_V, + SREG_Z, + ) - def test_div_quotient_remainder(self): - """Test DIV stores quotient and remainder correctly.""" cpu = CPU() + cpu.set_flag(SREG_C, True) + cpu.set_flag(SREG_H, True) + cpu.set_flag(SREG_T, True) + cpu.set_flag(SREG_I, True) + cpu.set_flag(SREG_V, True) + + # Result is 0x80 (Negative, non-zero) + cpu._set_flags_logical(0x80) + assert cpu.get_flag(SREG_C) is True # Preserved + assert cpu.get_flag(SREG_H) is True # Preserved + assert cpu.get_flag(SREG_T) is True # Preserved + assert cpu.get_flag(SREG_I) is True # Preserved + assert cpu.get_flag(SREG_V) is False # Cleared + assert cpu.get_flag(SREG_N) is True # Set + assert cpu.get_flag(SREG_S) is True # S = N ^ V = 1 ^ 0 = 1 + assert cpu.get_flag(SREG_Z) is False # Non-zero + + def test_sreg_inc_dec_preserves_c_h_t_i(self): + """Test INC and DEC preserve C, H, T, I while updating S, V, N, Z.""" + from tiny8.cpu import ( + SREG_C, + SREG_H, + SREG_I, + SREG_N, + SREG_S, + SREG_T, + SREG_V, + SREG_Z, + ) + + cpu = CPU() + cpu.set_flag(SREG_C, True) + cpu.set_flag(SREG_H, True) + cpu.set_flag(SREG_T, True) + cpu.set_flag(SREG_I, True) + + # INC from 0x7F to 0x80 -> Signed overflow V=1, N=1, S=0 + cpu._set_flags_inc(0x7F, 0x80) + assert cpu.get_flag(SREG_C) is True # Preserved + assert cpu.get_flag(SREG_H) is True # Preserved + assert cpu.get_flag(SREG_T) is True # Preserved + assert cpu.get_flag(SREG_I) is True # Preserved + assert cpu.get_flag(SREG_V) is True + assert cpu.get_flag(SREG_N) is True + assert cpu.get_flag(SREG_S) is False + assert cpu.get_flag(SREG_Z) is False + + # DEC from 0x80 to 0x7F -> Signed overflow V=1, N=0, S=1 + cpu._set_flags_dec(0x80, 0x7F) + assert cpu.get_flag(SREG_C) is True # Preserved + assert cpu.get_flag(SREG_H) is True # Preserved + assert cpu.get_flag(SREG_V) is True + assert cpu.get_flag(SREG_N) is False + assert cpu.get_flag(SREG_S) is True + assert cpu.get_flag(SREG_Z) is False + + def test_cpu_fast_path_run_and_invoker(self): + """Test CPU fast-path execution loop without progress or tracing.""" + from tiny8.cpu import _make_invoker + + cpu = CPU() + cpu.trace_enabled = False asm = assemble(""" - ldi r16, 17 - ldi r17, 5 - div r16, r17 + ldi r16, 1 + ldi r17, 2 + add r16, r17 """) cpu.load_program(asm) - cpu.run() - # 17 / 5 = 3 remainder 2 - assert cpu.read_reg(16) == 3 # quotient - assert cpu.read_reg(17) == 2 # remainder + cpu.run(show_progress=False) + assert cpu.read_reg(16) == 3 + + # Test _make_invoker with 3+ operands fallback + called_args = [] + + def dummy_handler(a, b, c): + called_args.extend([a, b, c]) + + inv_n = _make_invoker(dummy_handler, (1, 2, 3)) + assert inv_n is not None + inv_n() + assert called_args == [1, 2, 3] diff --git a/tests/test_data_transfer.py b/tests/test_data_transfer.py index 4f745cb..b846cd4 100644 --- a/tests/test_data_transfer.py +++ b/tests/test_data_transfer.py @@ -279,3 +279,30 @@ def test_swap_registers_via_stack(self, cpu_with_program, helper): pop r17 """) helper.assert_registers(cpu, {16: 99, 17: 42}) + + +class TestLDSSTS: + """Test LDS (Load Direct from SRAM) and STS (Store Direct to SRAM).""" + + def test_lds_sts_basic(self, cpu_with_program, helper): + """Test direct 16-bit address load and store.""" + cpu = cpu_with_program(""" + ldi r16, 0x42 + sts 0x0250, r16 + lds r17, 0x0250 + """) + helper.assert_register(cpu, 17, 0x42) + helper.assert_memory(cpu, 0x0250, 0x42) + + def test_16bit_pointer_pair(self, cpu_with_program, helper): + """Test 16-bit pointer pairs (Z=R31:R30) with LD and ST.""" + cpu = cpu_with_program(""" + ldi r30, 0x10 + ldi r31, 0x02 + ldi r16, 99 + st r30, r16 + ldi r17, 0 + ld r17, r30 + """) + helper.assert_register(cpu, 17, 99) + helper.assert_memory(cpu, 0x0210, 99) diff --git a/tests/test_memory.py b/tests/test_memory.py index fffc4fb..f444143 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -3,9 +3,12 @@ Tests for RAM, ROM operations, memory snapshots, and edge cases. """ +from pathlib import Path import pytest +from tiny8 import CPU, assemble, compile_c_file_with_symbols from tiny8.memory import Memory +from tiny8.tui import Tiny8App class TestMemoryBasicOperations: @@ -97,3 +100,110 @@ def test_snapshot_rom(self): assert len(snapshot) == 10 snapshot[0] = 99 assert mem.read_rom(0) == 1 + + def test_restore_ram_and_untracked_rom(self): + """Test restore_ram and load_rom when track_changes is False.""" + mem = Memory(ram_size=20, rom_size=20, track_changes=False) + mem.load_rom([10, 20, 30]) + assert mem.read_rom(0) == 10 + assert mem.read_rom(1) == 20 + assert len(mem.rom_changes) == 0 + + # Write initial non-zero + mem.write_ram(3, 100) + assert mem.read_ram(3) == 100 + + # Restore from dict with different keys and in-range values + mem.restore_ram({5: 250, 7: 15}) + assert mem.read_ram(3) == 0 + assert mem.read_ram(5) == 250 + assert mem.read_ram(7) == 15 + + +class TestMemoryOptimizationAndTraceBounds: + """Test memory optimization, bounded trace history, and trace lifecycle.""" + + def test_cpu_trace_bounded_during_long_run(self): + """Verify that step_trace, reg_trace, and mem_trace do not grow unboundedly.""" + asm_src = """ + loop: + ldi r16, 1 + ldi r17, 2 + add r16, r17 + rjmp loop + """ + asm = assemble(asm_src) + cpu = CPU(max_trace_steps=50, max_trace_history=100) + cpu.load_program(asm) + cpu.run(max_steps=5000, show_progress=False) + + assert cpu.step_count == 5000 + assert len(cpu.step_trace) <= 50 + assert len(cpu.reg_trace) <= 100 + assert len(cpu.mem_trace) <= 100 + assert len(cpu.mem.ram_changes) <= cpu.mem.max_changes + + def test_cpu_tracing_disabled_skips_trace_allocations(self): + """Verify that disabling tracing produces no trace entries.""" + asm_src = """ + loop: + ldi r16, 42 + sts 0x0100, r16 + rjmp loop + """ + asm = assemble(asm_src) + cpu = CPU() + cpu.load_program(asm) + cpu.trace_enabled = False + cpu.mem.track_changes = False + + for _ in range(500): + cpu.step(record_trace=False) + + assert cpu.step_count == 500 + assert len(cpu.step_trace) == 0 + assert len(cpu.reg_trace) == 0 + assert len(cpu.mem_trace) == 0 + assert len(cpu.mem.ram_changes) == 0 + + def test_simulation_memory_bounded(self): + """Simulate execution and verify memory and traces remain strictly bounded.""" + factorial_c = Path(__file__).parent.parent / "examples" / "factorial.c" + asm_code, symbols = compile_c_file_with_symbols(str(factorial_c)) + asm = assemble(asm_code) + + cpu = CPU() + cpu.load_program(asm) + cpu.source_lines = factorial_c.read_text(encoding="utf-8").splitlines() + cpu.pc_to_line = asm.pc_to_line + cpu.run(max_steps=1000, show_progress=False) + + app = Tiny8App( + cpu=cpu, file_path=str(factorial_c), max_steps=1000, var_symbols=symbols + ) + + for _ in range(20): + cpu.run(max_steps=3000, show_progress=False) + + assert len(app.traces) <= 1000 + assert len(cpu.step_trace) <= cpu.max_trace_steps + assert len(cpu.reg_trace) <= cpu.max_trace_history + assert len(cpu.mem_trace) <= cpu.max_trace_history + + def test_clear_traces(self): + """Verify clear_traces empties all trace lists and change logs.""" + cpu = CPU() + cpu.write_reg(16, 42) + cpu.write_ram(0x50, 99) + cpu.step_trace.append({"step": 1}) + + assert len(cpu.reg_trace) > 0 + assert len(cpu.mem_trace) > 0 + assert len(cpu.mem.ram_changes) > 0 + assert len(cpu.step_trace) > 0 + + cpu.clear_traces() + assert len(cpu.reg_trace) == 0 + assert len(cpu.mem_trace) == 0 + assert len(cpu.mem.ram_changes) == 0 + assert len(cpu.step_trace) == 0 From da84f087d867e417f23e6f95bb6c116117df4dc7 Mon Sep 17 00:00:00 2001 From: sql-hkr Date: Sat, 14 Mar 2026 22:38:15 +0900 Subject: [PATCH 2/5] feat: add built-in C compiler --- examples/factorial.c | 13 + src/tiny8/compiler.py | 1341 ++++++++++++++++++++++++++++++++++++++++ tests/test_compiler.py | 300 +++++++++ 3 files changed, 1654 insertions(+) create mode 100644 examples/factorial.c create mode 100644 src/tiny8/compiler.py create mode 100644 tests/test_compiler.py diff --git a/examples/factorial.c b/examples/factorial.c new file mode 100644 index 0000000..cf2611f --- /dev/null +++ b/examples/factorial.c @@ -0,0 +1,13 @@ +// Compute factorial in Tiny C +int factorial(int n) { + if (n <= 1) { + return 1; + } + return n * factorial(n - 1); +} + +int main() { + int res = factorial(5); + poke(0x80, res); // 5! = 120 + return 0; +} diff --git a/src/tiny8/compiler.py b/src/tiny8/compiler.py new file mode 100644 index 0000000..da4656b --- /dev/null +++ b/src/tiny8/compiler.py @@ -0,0 +1,1341 @@ +"""Simple C compiler for Tiny8 converting a subset of C into Tiny8 assembly. + +Features: +- Variable declarations and assignments (int/char) +- Arithmetic (+, -, *, /, %), bitwise (&, |, ^, ~), logical (!), and relational (<, <=, >, >=, ==, !=) +- Control structures: if / else, while, return +- Functions with register-based parameter passing (R16..R19) and return value in R16 +- Hardware builtins: peek(addr), poke(addr, val), in(port), out(port, val) +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import Any + + +@dataclass +class Token: + """Lexical token.""" + + type: str + value: str + line: int + + +class Lexer: + """Tokenize C source code.""" + + TOKEN_SPEC = [ + ("COMMENT_LINE", r"//.*"), + ("COMMENT_BLOCK", r"/\*[\s\S]*?\*/"), + ("NUMBER", r"0x[0-9a-fA-F]+|\d+"), + ("CHAR", r"'(\\.|[^\\'])'"), + ("STRING", r'"([^"\\]|\\.)*"'), + ( + "KEYWORD", + r"\b(int|char|void|if|else|while|for|break|return|peek|poke|in|out|vram_set|vram_get|vram_clear|vram_puts)\b", + ), + ("ID", r"[a-zA-Z_]\w*"), + ("OP", r"==|!=|<=|>=|&&|\|\||[+\-*/%&|^=!<>]"), + ("PUNCT", r"[;,\(\)\{\}\[\]]"), + ("WS", r"\s+"), + ] + + def __init__(self, source: str) -> None: + self.source = source + self.regex = re.compile( + "|".join(f"(?P<{name}>{pattern})" for name, pattern in self.TOKEN_SPEC) + ) + + def tokenize(self) -> list[Token]: + """Convert input source into tokens, ignoring whitespace and comments.""" + tokens: list[Token] = [] + line_num = 1 + for match in self.regex.finditer(self.source): + kind = match.lastgroup + val = match.group() + if kind in ("WS", "COMMENT_LINE", "COMMENT_BLOCK"): + line_num += val.count("\n") + continue + tokens.append(Token(kind or "UNKNOWN", val, line_num)) + return tokens + + +class ASTNode: + """Base AST node.""" + + +@dataclass +class Program(ASTNode): + """Program root containing functions and global declarations.""" + + functions: list[FunctionDef] + globals: list[ASTNode] + + +@dataclass +class VarDecl(ASTNode): + """Variable declaration.""" + + var_type: str + name: str + init_expr: ASTNode | None = None + + +@dataclass +class ArrayDecl(ASTNode): + """Array variable declaration.""" + + var_type: str + name: str + size: int + init_values: list[ASTNode] | None = None + + +@dataclass +class ArrayAccess(ASTNode): + """Array indexing expression: name[index].""" + + name: str + index: ASTNode + + +@dataclass +class ArrayAssign(ASTNode): + """Array element assignment: name[index] = expr.""" + + name: str + index: ASTNode + expr: ASTNode + + +@dataclass +class FunctionDef(ASTNode): + """Function definition.""" + + return_type: str + name: str + params: list[tuple[str, str]] + body: Block + + +@dataclass +class Block(ASTNode): + """Block of statements.""" + + statements: list[ASTNode] + + +@dataclass +class Assign(ASTNode): + """Assignment statement.""" + + name: str + expr: ASTNode + + +@dataclass +class IfStmt(ASTNode): + """If-else statement.""" + + condition: ASTNode + then_branch: Block + else_branch: Block | None = None + + +@dataclass +class WhileStmt(ASTNode): + """While loop.""" + + condition: ASTNode + body: Block + + +@dataclass +class ForStmt(ASTNode): + """For loop.""" + + init: ASTNode | None + condition: ASTNode + post: ASTNode | None + body: Block + + +@dataclass +class BreakStmt(ASTNode): + """Break statement.""" + + +@dataclass +class ReturnStmt(ASTNode): + """Return statement.""" + + expr: ASTNode | None = None + + +@dataclass +class ExprStmt(ASTNode): + """Expression statement.""" + + expr: ASTNode + + +@dataclass +class BinaryOp(ASTNode): + """Binary expression.""" + + op: str + left: ASTNode + right: ASTNode + + +@dataclass +class UnaryOp(ASTNode): + """Unary expression.""" + + op: str + operand: ASTNode + + +@dataclass +class Literal(ASTNode): + """Literal integer value.""" + + value: int + + +@dataclass +class StringLiteral(ASTNode): + """String literal value.""" + + value: str + + +@dataclass +class Var(ASTNode): + """Variable reference.""" + + name: str + + +@dataclass +class Call(ASTNode): + """Function call expression.""" + + name: str + args: list[ASTNode] + + +def _fold_binary(op: str, left: ASTNode, right: ASTNode) -> ASTNode: + """Fold binary operations on literals at compile time.""" + if isinstance(left, Literal) and isinstance(right, Literal): + lv = left.value + rv = right.value + if op == "+": + return Literal((lv + rv) & 0xFF) + if op == "-": + return Literal((lv - rv) & 0xFF) + if op == "*": + return Literal((lv * rv) & 0xFF) + if op == "/" and rv != 0: + return Literal((lv // rv) & 0xFF) + if op == "%" and rv != 0: + return Literal((lv % rv) & 0xFF) + if op == "&": + return Literal((lv & rv) & 0xFF) + if op == "|": + return Literal((lv | rv) & 0xFF) + if op == "^": + return Literal((lv ^ rv) & 0xFF) + if op == "==": + return Literal(1 if lv == rv else 0) + if op == "!=": + return Literal(1 if lv != rv else 0) + if op == "<": + return Literal(1 if lv < rv else 0) + if op == "<=": + return Literal(1 if lv <= rv else 0) + if op == ">": + return Literal(1 if lv > rv else 0) + if op == ">=": + return Literal(1 if lv >= rv else 0) + if op == "&&": + return Literal(1 if (lv != 0 and rv != 0) else 0) + if op == "||": + return Literal(1 if (lv != 0 or rv != 0) else 0) + return BinaryOp(op, left, right) + + +def _fold_unary(op: str, operand: ASTNode) -> ASTNode: + """Fold unary operations on literals at compile time.""" + if isinstance(operand, Literal): + val = operand.value + if op == "-": + return Literal((-val) & 0xFF) + if op == "~": + return Literal((~val) & 0xFF) + if op == "!": + return Literal(1 if val == 0 else 0) + return UnaryOp(op, operand) + + +class Parser: + """Recursive descent parser.""" + + def __init__(self, tokens: list[Token]) -> None: + self.tokens = tokens + self.pos = 0 + + def peek(self) -> Token | None: + return self.tokens[self.pos] if self.pos < len(self.tokens) else None + + def consume(self, expected_val: str | None = None) -> Token: + tok = self.peek() + if tok is None: + raise SyntaxError("Unexpected end of input") + if expected_val and tok.value != expected_val: + raise SyntaxError( + f"Line {tok.line}: Expected '{expected_val}', got '{tok.value}'" + ) + self.pos += 1 + return tok + + def match(self, val: str) -> bool: + tok = self.peek() + if tok and tok.value == val: + self.pos += 1 + return True + return False + + def parse(self) -> Program: + functions: list[FunctionDef] = [] + globals_list: list[ASTNode] = [] + + while self.peek(): + type_tok = self.consume() + name_tok = self.consume() + + if self.match("("): + params: list[tuple[str, str]] = [] + if not self.match(")"): + while True: + ptype = self.consume().value + pname = self.consume().value + params.append((ptype, pname)) + if not self.match(","): + self.consume(")") + break + body = self.parse_block() + functions.append( + FunctionDef(type_tok.value, name_tok.value, params, body) + ) + elif self.match("["): + size = None + if self.peek() and self.peek().value != "]": + size_tok = self.consume() + size = ( + int(size_tok.value, 16) + if size_tok.value.startswith("0x") + else int(size_tok.value) + ) + self.consume("]") + init_values = None + if self.match("="): + self.consume("{") + init_values = [] + while self.peek() and self.peek().value != "}": + init_values.append(self.parse_expr()) + if self.match(","): + continue + break + self.consume("}") + self.consume(";") + if size is None: + if init_values is not None: + size = len(init_values) + else: + raise SyntaxError("Array size missing") + globals_list.append( + ArrayDecl(type_tok.value, name_tok.value, size, init_values) + ) + else: + init = None + if self.match("="): + init = self.parse_expr() + self.consume(";") + globals_list.append(VarDecl(type_tok.value, name_tok.value, init)) + + return Program(functions, globals_list) + + def parse_block(self) -> Block: + self.consume("{") + stmts: list[ASTNode] = [] + while self.peek() and self.peek().value != "}": + stmts.append(self.parse_stmt()) + self.consume("}") + return Block(stmts) + + def parse_inline_stmt(self) -> ASTNode: + tok = self.peek() + if tok and tok.value in ("int", "char"): + vtype = self.consume().value + vname = self.consume().value + init = None + if self.match("="): + init = self.parse_expr() + self.consume(";") + return VarDecl(vtype, vname, init) + if tok and tok.type == "ID": + next_tok = ( + self.tokens[self.pos + 1] if self.pos + 1 < len(self.tokens) else None + ) + if next_tok and next_tok.value == "=": + name = self.consume().value + self.consume("=") + expr = self.parse_expr() + self.consume(";") + return Assign(name, expr) + expr = self.parse_expr() + self.consume(";") + return ExprStmt(expr) + + def parse_inline_post(self) -> ASTNode: + tok = self.peek() + if tok and tok.type == "ID": + next_tok = ( + self.tokens[self.pos + 1] if self.pos + 1 < len(self.tokens) else None + ) + if next_tok and next_tok.value == "=": + name = self.consume().value + self.consume("=") + expr = self.parse_expr() + return Assign(name, expr) + return self.parse_expr() + + def parse_stmt(self) -> ASTNode: + tok = self.peek() + if not tok: + raise SyntaxError("Unexpected end of statement") + + if tok.value in ("int", "char"): + vtype = self.consume().value + vname = self.consume().value + if self.match("["): + size = None + if self.peek() and self.peek().value != "]": + size_tok = self.consume() + size = ( + int(size_tok.value, 16) + if size_tok.value.startswith("0x") + else int(size_tok.value) + ) + self.consume("]") + init_values = None + if self.match("="): + self.consume("{") + init_values = [] + while self.peek() and self.peek().value != "}": + init_values.append(self.parse_expr()) + if self.match(","): + continue + break + self.consume("}") + self.consume(";") + if size is None: + if init_values is not None: + size = len(init_values) + else: + raise SyntaxError("Array size missing") + return ArrayDecl(vtype, vname, size, init_values) + init = None + if self.match("="): + init = self.parse_expr() + self.consume(";") + return VarDecl(vtype, vname, init) + + if tok.value == "break": + self.consume("break") + self.consume(";") + return BreakStmt() + + if tok.value == "for": + self.consume("for") + self.consume("(") + init = None + if self.peek() and self.peek().value != ";": + init = self.parse_inline_stmt() + else: + self.consume(";") + cond = ( + self.parse_expr() + if self.peek() and self.peek().value != ";" + else Literal(1) + ) + self.consume(";") + post = None + if self.peek() and self.peek().value != ")": + post = self.parse_inline_post() + self.consume(")") + body = ( + self.parse_block() + if self.peek() and self.peek().value == "{" + else Block([self.parse_stmt()]) + ) + return ForStmt(init, cond, post, body) + + if tok.value == "if": + self.consume("if") + self.consume("(") + cond = self.parse_expr() + self.consume(")") + then_b = ( + self.parse_block() + if self.peek() and self.peek().value == "{" + else Block([self.parse_stmt()]) + ) + else_b = None + if self.match("else"): + else_b = ( + self.parse_block() + if self.peek() and self.peek().value == "{" + else Block([self.parse_stmt()]) + ) + return IfStmt(cond, then_b, else_b) + + if tok.value == "while": + self.consume("while") + self.consume("(") + cond = self.parse_expr() + self.consume(")") + body = ( + self.parse_block() + if self.peek() and self.peek().value == "{" + else Block([self.parse_stmt()]) + ) + return WhileStmt(cond, body) + + if tok.value == "return": + self.consume("return") + expr = None + if self.peek() and self.peek().value != ";": + expr = self.parse_expr() + self.consume(";") + return ReturnStmt(expr) + + if tok.type == "ID": + next_tok = ( + self.tokens[self.pos + 1] if self.pos + 1 < len(self.tokens) else None + ) + if next_tok and next_tok.value == "[": + name = self.consume().value + self.consume("[") + idx_expr = self.parse_expr() + self.consume("]") + self.consume("=") + expr = self.parse_expr() + self.consume(";") + return ArrayAssign(name, idx_expr, expr) + if next_tok and next_tok.value == "=": + name = self.consume().value + self.consume("=") + expr = self.parse_expr() + self.consume(";") + return Assign(name, expr) + + expr = self.parse_expr() + self.consume(";") + return ExprStmt(expr) + + def parse_expr(self) -> ASTNode: + return self.parse_logic_or() + + def parse_logic_or(self) -> ASTNode: + node = self.parse_logic_and() + while self.match("||"): + node = _fold_binary("||", node, self.parse_logic_and()) + return node + + def parse_logic_and(self) -> ASTNode: + node = self.parse_bitwise() + while self.match("&&"): + node = _fold_binary("&&", node, self.parse_bitwise()) + return node + + def parse_bitwise(self) -> ASTNode: + node = self.parse_equality() + while True: + if self.match("|"): + node = _fold_binary("|", node, self.parse_equality()) + elif self.match("^"): + node = _fold_binary("^", node, self.parse_equality()) + elif self.match("&"): + node = _fold_binary("&", node, self.parse_equality()) + else: + break + return node + + def parse_equality(self) -> ASTNode: + node = self.parse_relational() + while True: + if self.match("=="): + node = _fold_binary("==", node, self.parse_relational()) + elif self.match("!="): + node = _fold_binary("!=", node, self.parse_relational()) + else: + break + return node + + def parse_relational(self) -> ASTNode: + node = self.parse_additive() + while True: + if self.match("<"): + node = _fold_binary("<", node, self.parse_additive()) + elif self.match("<="): + node = _fold_binary("<=", node, self.parse_additive()) + elif self.match(">"): + node = _fold_binary(">", node, self.parse_additive()) + elif self.match(">="): + node = _fold_binary(">=", node, self.parse_additive()) + else: + break + return node + + def parse_additive(self) -> ASTNode: + node = self.parse_multiplicative() + while True: + if self.match("+"): + node = _fold_binary("+", node, self.parse_multiplicative()) + elif self.match("-"): + node = _fold_binary("-", node, self.parse_multiplicative()) + else: + break + return node + + def parse_multiplicative(self) -> ASTNode: + node = self.parse_unary() + while True: + if self.match("*"): + node = _fold_binary("*", node, self.parse_unary()) + elif self.match("/"): + node = _fold_binary("/", node, self.parse_unary()) + elif self.match("%"): + node = _fold_binary("%", node, self.parse_unary()) + else: + break + return node + + def parse_unary(self) -> ASTNode: + tok = self.peek() + if tok and tok.value in ("-", "!", "~"): + op = self.consume().value + return _fold_unary(op, self.parse_unary()) + return self.parse_primary() + + def parse_primary(self) -> ASTNode: + tok = self.consume() + if tok.type == "NUMBER": + val = int(tok.value, 16) if tok.value.startswith("0x") else int(tok.value) + return Literal(val & 0xFF) + if tok.type == "CHAR": + raw = tok.value[1:-1] + if raw == r"\n": + val = 10 + elif raw == r"\t": + val = 9 + elif raw == r"\0": + val = 0 + elif raw == r"\\": + val = ord("\\") + elif raw == r"\'": + val = ord("'") + elif len(raw) == 1: + val = ord(raw) + else: + val = ord(raw[0]) + return Literal(val & 0xFF) + if tok.type == "STRING": + raw = tok.value[1:-1] + return StringLiteral(raw) + if tok.type in ("ID", "KEYWORD"): + if self.match("["): + idx_expr = self.parse_expr() + self.consume("]") + return ArrayAccess(tok.value, idx_expr) + if self.match("("): + args: list[ASTNode] = [] + if not self.match(")"): + while True: + args.append(self.parse_expr()) + if not self.match(","): + self.consume(")") + break + return Call(tok.value, args) + return Var(tok.value) + if tok.value == "(": + expr = self.parse_expr() + self.consume(")") + return expr + raise SyntaxError(f"Line {tok.line}: Unexpected token '{tok.value}'") + + +def optimize_assembly(lines: list[str]) -> list[str]: + """Perform peephole optimizations on generated assembly lines. + + Optimizations include: + - Removing redundant self register moves (MOV Rx, Rx) + - Simplifying PUSH Rx followed immediately by POP Ry into MOV Ry, Rx (or eliminating if Rx == Ry) + - Removing redundant LD R16, R30 immediately after ST R30, R16 + - Removing JMP