diff --git a/src/tiny8/assembler.py b/src/tiny8/assembler.py index c2c3d52..7652f7f 100644 --- a/src/tiny8/assembler.py +++ b/src/tiny8/assembler.py @@ -1,5 +1,6 @@ """Assembler for tiny8 converting assembly text into program listings.""" +import re from dataclasses import dataclass, field @@ -75,6 +76,7 @@ def parse_asm(text: str) -> AsmResult: if not line: continue + line = re.sub(r"([XYZxyz]|r\d+|R\d+)\s*\+\s*(\w+)", r"\1+\2", line) parts = line.replace(",", " ").split() instr = parts[0].upper() ops = [] @@ -84,6 +86,18 @@ def parse_asm(text: str) -> AsmResult: elif p.upper() in ("X", "Y", "Z"): reg_map = {"X": 26, "Y": 28, "Z": 30} ops.append(("reg", reg_map[p.upper()])) + elif "+" in p: + base, disp = p.split("+", 1) + base_u = base.upper() + reg_map = {"X": 26, "Y": 28, "Z": 30} + if base_u in reg_map: + base_reg = reg_map[base_u] + elif base_u.startswith("R") and base_u[1:].isdigit(): + base_reg = int(base_u[1:]) + else: + base_reg = int(base) + disp_val = _parse_number(disp) + ops.append(("disp", base_reg, disp_val)) else: try: ops.append(_parse_number(p)) diff --git a/src/tiny8/compiler.py b/src/tiny8/compiler.py index cfca9bf..214b6ff 100644 --- a/src/tiny8/compiler.py +++ b/src/tiny8/compiler.py @@ -965,6 +965,24 @@ def optimize_assembly(lines: list[str]) -> list[str]: i += 3 continue + # Rule 4b: STD Y+q, R16 followed by LDD R16, Y+q + if s_curr.upper().startswith("STD ") and s_nxt.upper().startswith( + "LDD " + ): + parts_st = s_curr.replace(",", " ").split() + parts_ld = s_nxt.replace(",", " ").split() + if ( + len(parts_st) == 3 + and len(parts_ld) == 3 + and parts_st[1].upper() == parts_ld[2].upper() + and parts_st[2].upper() == parts_ld[1].upper() + and parts_st[2].upper() == "R16" + ): + new_lines.append(curr) + changed = True + i += 2 + continue + # Rule 5: Consecutive duplicate LDI if s_curr.upper().startswith("LDI ") and s_nxt.upper().startswith( "LDI " @@ -993,6 +1011,47 @@ def optimize_assembly(lines: list[str]) -> list[str]: return current_lines +def _collect_locals(func: FunctionDef) -> list[tuple[str, str]]: + """Collect all function parameters and local variable declarations.""" + locals_list: list[tuple[str, str]] = [] + seen: set[str] = set() + + for ptype, pname in func.params: + if pname not in seen: + locals_list.append((pname, ptype)) + seen.add(pname) + + def visit(node: ASTNode | None) -> None: + if node is None: + return + if isinstance(node, VarDecl): + if node.name not in seen: + locals_list.append((node.name, node.var_type)) + seen.add(node.name) + elif isinstance(node, Block): + for s in node.statements: + visit(s) + elif isinstance(node, IfStmt): + visit(node.then_branch) + if node.else_branch: + visit(node.else_branch) + elif isinstance(node, WhileStmt): + visit(node.body) + elif isinstance(node, DoWhileStmt): + visit(node.body) + elif isinstance(node, ForStmt): + if node.init: + visit(node.init) + visit(node.body) + elif isinstance(node, SwitchStmt): + for c in node.clauses: + for s in c.statements: + visit(s) + + visit(func.body) + return locals_list + + class CodeGenerator: """Generate Tiny8 assembly from AST using register & stack model.""" @@ -1007,6 +1066,8 @@ def __init__(self) -> None: self.current_func = "" self.loop_end_stack: list[str] = [] self.loop_continue_stack: list[str] = [] + self.frame_offsets: dict[str, int] = {} + self.func_epilogue_lbl: str | None = None def new_label(self, prefix: str = "L") -> str: self.label_counter += 1 @@ -1092,15 +1153,38 @@ def gen_function(self, func: FunctionDef) -> None: self.current_func = func.name self.emit_label(func.name) + locals_info = _collect_locals(func) + self.frame_offsets = {} + for idx, (vname, vtype) in enumerate(locals_info, start=1): + self.frame_offsets[vname] = idx + self.symbols[vname] = { + "offset": idx, + "addr": 0, + "type": vtype, + "func": func.name, + } + locals_size = len(locals_info) + + if locals_size > 0: + self.emit("PUSH R28") + self.emit("PUSH R29") + self.emit("IN R28, 0x3D") + self.emit("IN R29, 0x3E") + rem = locals_size + while rem > 0: + chunk = min(rem, 63) + self.emit(f"SBIW R28, {chunk}") + rem -= chunk + self.emit("OUT 0x3D, R28") + self.emit("OUT 0x3E, R29") + for i, (ptype, pname) in enumerate(func.params): - addr = self.allocate_var(pname, ptype) - reg = self.PARAM_REGS[i] - self.emit(f"LDI R30, {addr & 0xFF}") - if addr >= 256: - self.emit(f"LDI R31, {addr >> 8}") - self.emit(f"ST R30, R{reg}") - if addr >= 256: - self.emit("LDI R31, 0") + if i < len(self.PARAM_REGS): + reg = self.PARAM_REGS[i] + offset = self.frame_offsets[pname] + self.emit(f"STD Y+{offset}, R{reg}") + + self.func_epilogue_lbl = self.new_label(f"{func.name}_epilogue") for stmt in func.body.statements: self.gen_stmt(stmt) @@ -1108,7 +1192,19 @@ def gen_function(self, func: FunctionDef) -> None: if func.name == "main": self.emit("halt: JMP halt") else: + self.emit_label(self.func_epilogue_lbl) + if locals_size > 0: + rem = locals_size + while rem > 0: + chunk = min(rem, 63) + self.emit(f"ADIW R28, {chunk}") + rem -= chunk + self.emit("OUT 0x3D, R28") + self.emit("OUT 0x3E, R29") + self.emit("POP R29") + self.emit("POP R28") self.emit("RET") + self.frame_offsets = {} def gen_stmt(self, stmt: ASTNode) -> None: if isinstance(stmt, ArrayDecl): @@ -1125,15 +1221,21 @@ def gen_stmt(self, stmt: ASTNode) -> None: self.emit("LDI R31, 0") elif isinstance(stmt, VarDecl): - addr = self.allocate_var(stmt.name, stmt.var_type) - if stmt.init_expr: - self.gen_expr(stmt.init_expr) - self.emit(f"LDI R30, {addr & 0xFF}") - if addr >= 256: - self.emit(f"LDI R31, {addr >> 8}") - self.emit("ST R30, R16") - if addr >= 256: - self.emit("LDI R31, 0") + if self.frame_offsets and stmt.name in self.frame_offsets: + offset = self.frame_offsets[stmt.name] + if stmt.init_expr: + self.gen_expr(stmt.init_expr) + self.emit(f"STD Y+{offset}, R16") + else: + addr = self.allocate_var(stmt.name, stmt.var_type) + if stmt.init_expr: + self.gen_expr(stmt.init_expr) + self.emit(f"LDI R30, {addr & 0xFF}") + if addr >= 256: + self.emit(f"LDI R31, {addr >> 8}") + self.emit("ST R30, R16") + if addr >= 256: + self.emit("LDI R31, 0") elif isinstance(stmt, ArrayAssign): base_addr = self.allocate_var(stmt.name) @@ -1154,12 +1256,17 @@ def gen_stmt(self, stmt: ASTNode) -> None: self.emit("LDI R31, 0") elif isinstance(stmt, Assign): - addr = self.allocate_var(stmt.name) - self.gen_expr(stmt.expr) - self.emit(f"LDI R30, {addr & 0xFF}") - if addr >= 256: - self.emit(f"LDI R31, {addr >> 8}") - self.emit("ST R30, R16") + if self.frame_offsets and stmt.name in self.frame_offsets: + offset = self.frame_offsets[stmt.name] + self.gen_expr(stmt.expr) + self.emit(f"STD Y+{offset}, R16") + else: + addr = self.allocate_var(stmt.name) + self.gen_expr(stmt.expr) + self.emit(f"LDI R30, {addr & 0xFF}") + if addr >= 256: + self.emit(f"LDI R31, {addr >> 8}") + self.emit("ST R30, R16") elif isinstance(stmt, CompoundAssign): self.gen_stmt( @@ -1320,7 +1427,7 @@ def gen_stmt(self, stmt: ASTNode) -> None: if self.current_func == "main": self.emit("halt_ret: JMP halt_ret") else: - self.emit("RET") + self.emit(f"JMP {self.func_epilogue_lbl}") elif isinstance(stmt, ExprStmt): self.gen_expr(stmt.expr) @@ -1350,13 +1457,17 @@ def gen_expr(self, expr: ASTNode) -> None: self.emit("LDI R31, 0") elif isinstance(expr, Var): - addr = self.allocate_var(expr.name) - self.emit(f"LDI R30, {addr & 0xFF}") - if addr >= 256: - self.emit(f"LDI R31, {addr >> 8}") - self.emit("LD R16, R30") - if addr >= 256: - self.emit("LDI R31, 0") + if self.frame_offsets and expr.name in self.frame_offsets: + offset = self.frame_offsets[expr.name] + self.emit(f"LDD R16, Y+{offset}") + else: + addr = self.allocate_var(expr.name) + self.emit(f"LDI R30, {addr & 0xFF}") + if addr >= 256: + self.emit(f"LDI R31, {addr >> 8}") + self.emit("LD R16, R30") + if addr >= 256: + self.emit("LDI R31, 0") elif isinstance(expr, UnaryOp): self.gen_expr(expr.operand) @@ -1378,20 +1489,31 @@ def gen_expr(self, expr: ASTNode) -> None: elif isinstance(expr, IncDec): instr = "INC" if expr.is_inc else "DEC" if isinstance(expr.expr, Var): - addr = self.allocate_var(expr.expr.name) - self.emit(f"LDI R30, {addr & 0xFF}") - if addr >= 256: - self.emit(f"LDI R31, {addr >> 8}") - self.emit("LD R16, R30") - if expr.is_prefix: - self.emit(f"{instr} R16") - self.emit("ST R30, R16") + if self.frame_offsets and expr.expr.name in self.frame_offsets: + offset = self.frame_offsets[expr.expr.name] + self.emit(f"LDD R16, Y+{offset}") + if expr.is_prefix: + self.emit(f"{instr} R16") + self.emit(f"STD Y+{offset}, R16") + else: + self.emit("MOV R17, R16") + self.emit(f"{instr} R17") + self.emit(f"STD Y+{offset}, R17") else: - self.emit("MOV R17, R16") - self.emit(f"{instr} R17") - self.emit("ST R30, R17") - if addr >= 256: - self.emit("LDI R31, 0") + addr = self.allocate_var(expr.expr.name) + self.emit(f"LDI R30, {addr & 0xFF}") + if addr >= 256: + self.emit(f"LDI R31, {addr >> 8}") + self.emit("LD R16, R30") + if expr.is_prefix: + self.emit(f"{instr} R16") + self.emit("ST R30, R16") + else: + self.emit("MOV R17, R16") + self.emit(f"{instr} R17") + self.emit("ST R30, R17") + if addr >= 256: + self.emit("LDI R31, 0") elif isinstance(expr.expr, ArrayAccess): base_addr = self.allocate_var(expr.expr.name) diff --git a/src/tiny8/cpu.py b/src/tiny8/cpu.py index bc876da..54ab5ea 100644 --- a/src/tiny8/cpu.py +++ b/src/tiny8/cpu.py @@ -387,12 +387,17 @@ def _build_predecoded(self) -> None: for o in operands: if isinstance(o, tuple) and len(o) == 2 and o[0] == "reg": decoded_ops.append(int(o[1])) + elif isinstance(o, tuple) and len(o) == 3 and o[0] == "disp": + decoded_ops.extend([int(o[1]), int(o[2])]) 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]}" + if isinstance(o, tuple) and len(o) == 3 and o[0] == "disp": + reg_name = {26: "X", 28: "Y", 30: "Z"}.get(o[1], f"R{o[1]}") + return f"{reg_name}+{o[2]}" return str(o) try: @@ -766,7 +771,12 @@ def op_in(self, rd: int, port: int) -> None: rd: Destination register index. port: Port/memory address to read from. """ - self.write_reg(rd, self.read_ram(port)) + if port == 0x3D: + self.write_reg(rd, self.sp & 0xFF) + elif port == 0x3E: + self.write_reg(rd, (self.sp >> 8) & 0xFF) + else: + self.write_reg(rd, self.read_ram(port)) def op_out(self, port: int, rr: int) -> None: """Store Register to an I/O Location (OUT). @@ -779,7 +789,15 @@ def op_out(self, port: int, rr: int) -> None: port: Port/memory address to write to. rr: Source register index. """ - self.write_ram(port, self.read_reg(rr)) + val = self.read_reg(rr) + if port == 0x3D: + self.sp = (self.sp & 0xFF00) | val + self.write_ram(port, val) + elif port == 0x3E: + self.sp = (self.sp & 0x00FF) | (val << 8) + self.write_ram(port, val) + else: + self.write_ram(port, val) def op_jmp(self, label: str | int) -> None: """Jump (JMP). @@ -1293,6 +1311,40 @@ def op_st(self, addr_reg: int, rr: int) -> None: addr = (high << 8) | low if high > 0 else low self.write_ram(addr, self.read_reg(rr)) + def op_ldd(self, rd: int, addr_reg: int, q: int) -> None: + """Load Indirect with Displacement (LDD). + + Per AVR Instruction Set Manual DS40002198A: + Operation: Rd <- (Y + q) or (Z + q) + Status Register (SREG): None. + + Args: + rd: Destination register index. + addr_reg: Pointer register index (e.g. 28 for Y, 30 for Z). + q: Displacement offset (0..63). + """ + 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) + q + self.write_reg(rd, self.read_ram(addr)) + + def op_std(self, addr_reg: int, q: int, rr: int) -> None: + """Store Indirect with Displacement (STD). + + Per AVR Instruction Set Manual DS40002198A: + Operation: (Y + q) <- Rr or (Z + q) <- Rr + Status Register (SREG): None. + + Args: + addr_reg: Pointer register index (e.g. 28 for Y, 30 for Z). + q: Displacement offset (0..63). + rr: Source register index. + """ + 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) + q + self.write_ram(addr, self.read_reg(rr)) + def op_lds(self, rd: int, addr: int) -> None: """Load Direct from Data Space (LDS). diff --git a/src/tiny8/tui.py b/src/tiny8/tui.py index a116251..0961f26 100644 --- a/src/tiny8/tui.py +++ b/src/tiny8/tui.py @@ -520,6 +520,18 @@ def precompute( "desc": "Stores one byte indirect from register Rr to data space using pointer register.", "flags": "None", }, + "LDD": { + "name": "Load Indirect with Displacement", + "syntax": "LDD Rd, Y+q / Z+q", + "desc": "Loads one byte from data space to register Rd using pointer register with displacement.", + "flags": "None", + }, + "STD": { + "name": "Store Indirect with Displacement", + "syntax": "STD Y+q / Z+q, Rr", + "desc": "Stores one byte from register Rr to data space using pointer register with displacement.", + "flags": "None", + }, "LDS": { "name": "Load Direct from Data Space", "syntax": "LDS Rd, k", diff --git a/tests/test_assembler.py b/tests/test_assembler.py index 20380db..b747bb0 100644 --- a/tests/test_assembler.py +++ b/tests/test_assembler.py @@ -100,3 +100,27 @@ def test_parse_number_empty_token(self): _parse_number("") with pytest.raises(ValueError, match="Unable to parse numeric token"): _parse_number("#") + + def test_parse_displacement_operands(self): + """Test parsing instructions with displacement addressing.""" + from tiny8.assembler import assemble + + asm_text = """ + ldd r16, Y+2 + std Y+3, r17 + ldd r18, Y + 4 + std Z + 5, r19 + ldd r20, r28+6 + ldd r23, 28+7 + ld r21, X + st Y, r22 + """ + res = assemble(asm_text) + assert res.program[0] == ("LDD", (("reg", 16), ("disp", 28, 2))) + assert res.program[1] == ("STD", (("disp", 28, 3), ("reg", 17))) + assert res.program[2] == ("LDD", (("reg", 18), ("disp", 28, 4))) + assert res.program[3] == ("STD", (("disp", 30, 5), ("reg", 19))) + assert res.program[4] == ("LDD", (("reg", 20), ("disp", 28, 6))) + assert res.program[5] == ("LDD", (("reg", 23), ("disp", 28, 7))) + assert res.program[6] == ("LD", (("reg", 21), ("reg", 26))) + assert res.program[7] == ("ST", (("reg", 28), ("reg", 22))) diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 231c312..1cfd513 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -4,19 +4,29 @@ from tiny8 import CPU, assemble from tiny8.compiler import ( + Assign, BinaryOp, + Block, Call, CaseClause, CodeGenerator, ContinueStmt, DefaultClause, DoWhileStmt, + ForStmt, + FunctionDef, + IfStmt, + IncDec, Lexer, Literal, Parser, SwitchStmt, Token, UnaryOp, + Var, + VarDecl, + WhileStmt, + _collect_locals, _fold_binary, _fold_unary, compile_c, @@ -1583,3 +1593,210 @@ def test_compiler_switch_ast_and_lexer(): sw3 = SwitchStmt(Literal(1), cases=[], default=None, clauses=[case_c]) assert sw3.cases == [case_c] assert sw3.default is None + + +def test_compiler_recursive_fibonacci(): + """Verify recursive Fibonacci correctly preserves local variables and parameters.""" + c_code = """ + int fib(int n) { + if (n <= 1) { + return n; + } + int a = fib(n - 1); + int b = fib(n - 2); + return a + b; + } + int main() { + poke(0x80, fib(7)); // fib(7) = 13 + return 0; + } + """ + asm = compile_c(c_code) + asm_res = assemble(asm) + cpu = CPU() + cpu.load_program(asm_res) + cpu.run(max_steps=50000, show_progress=False) + + assert cpu.read_ram(0x80) == 13 + + +def test_compiler_recursive_factorial_with_locals(): + """Verify recursive factorial with local variable stores.""" + c_code = """ + int factorial(int n) { + if (n <= 1) { + return 1; + } + int next_n = n - 1; + int sub = factorial(next_n); + return n * sub; + } + int main() { + poke(0x80, factorial(5)); // 5! = 120 + return 0; + } + """ + asm = compile_c(c_code) + asm_res = assemble(asm) + cpu = CPU() + cpu.load_program(asm_res) + cpu.run(max_steps=10000, show_progress=False) + + assert cpu.read_ram(0x80) == 120 + + +def test_compiler_nested_functions_stack_frames(): + """Verify nested calls preserve separate stack frames and caller variables.""" + c_code = """ + int add_three(int a, int b, int c) { + int temp = a + b; + return temp + c; + } + int compute(int x) { + int y = x * 2; + int z = add_three(x, y, 5); + return y + z; + } + int main() { + poke(0x80, compute(3)); // y = 6, z = 3+6+5=14, return 6+14=20 + return 0; + } + """ + asm = compile_c(c_code) + asm_res = assemble(asm) + cpu = CPU() + cpu.load_program(asm_res) + cpu.run(max_steps=5000, show_progress=False) + + assert cpu.read_ram(0x80) == 20 + + +def test_compiler_global_variable_read_and_write_inside_function(): + """Verify functions can read, write, and inc/dec global variables.""" + c_code = """ + int g_counter = 10; + void modify_globals() { + g_counter = 20; + g_counter++; + ++g_counter; + } + int main() { + modify_globals(); + poke(0x80, g_counter); + return 0; + } + """ + asm = compile_c(c_code) + asm_res = assemble(asm) + cpu = CPU() + cpu.load_program(asm_res) + cpu.run(max_steps=2000, show_progress=False) + + assert cpu.read_ram(0x80) == 22 + + +def test_compiler_function_without_locals(): + """Verify function with no params or locals has no stack frame overhead.""" + c_code = """ + int answer() { + return 42; + } + int main() { + poke(0x80, answer()); + return 0; + } + """ + asm = compile_c(c_code) + # answer function should not have SBIW R28 + assert "SBIW R28" not in asm.split("main:")[0] + asm_res = assemble(asm) + cpu = CPU() + cpu.load_program(asm_res) + cpu.run(max_steps=1000, show_progress=False) + + assert cpu.read_ram(0x80) == 42 + + +def test_compiler_large_locals_chunking(): + """Verify functions with > 63 locals generate chunked SBIW / ADIW.""" + # Declare 65 local variables + var_decls = "\n".join(f"int v{i} = {i};" for i in range(65)) + c_code = f""" + int big_func() {{ + {var_decls} + return v0 + v64; + }} + int main() {{ + poke(0x80, big_func()); + return 0; + }} + """ + asm = compile_c(c_code) + # Should contain SBIW with 63 and remaining chunk + assert "SBIW R28, 63" in asm + assert "ADIW R28, 63" in asm + asm_res = assemble(asm) + cpu = CPU() + cpu.load_program(asm_res) + cpu.run(max_steps=5000, show_progress=False) + + assert cpu.read_ram(0x80) == 64 + + +def test_compiler_collect_locals_edge_cases(): + """Verify _collect_locals handles AST nodes, blocks, and duplicate declarations.""" + func = FunctionDef( + name="test_func", + return_type="int", + params=[("int", "a"), ("int", "a")], # duplicate param + body=Block( + [ + None, # test node is None + VarDecl("int", "x", None), + VarDecl("int", "x", None), # duplicate local + IfStmt( + Literal(1), + Block([VarDecl("int", "y", None)]), + Block([VarDecl("int", "z", None)]), + ), + WhileStmt(Literal(1), Block([VarDecl("int", "w", None)])), + DoWhileStmt(Block([VarDecl("int", "d", None)]), Literal(1)), + ForStmt( + VarDecl("int", "i", None), + Literal(1), + None, + Block([VarDecl("int", "fb", None)]), + ), + SwitchStmt( + Literal(1), + cases=[CaseClause(Literal(1), [VarDecl("int", "sc", None)])], + ), + ] + ), + ) + locals_collected = _collect_locals(func) + names = [name for name, _ in locals_collected] + assert names == ["a", "x", "y", "z", "w", "d", "i", "fb", "sc"] + + +def test_compiler_peephole_std_ldd_optimization(): + """Verify peephole optimizer removes redundant LDD after STD.""" + lines = [ + " STD Y+1, R16", + " LDD R16, Y+1", + ] + opt = optimize_assembly(lines) + assert opt == [" STD Y+1, R16"] + + +def test_compiler_standalone_vardecl_and_assign_without_frame(): + """Verify CodeGenerator handles standalone VarDecl, Assign, and IncDec when frame_offsets is empty.""" + gen = CodeGenerator() + gen.frame_offsets = {} + gen.next_ram = 0x0600 + gen.gen_stmt(VarDecl("int", "big_v", Literal(123))) + gen.gen_stmt(Assign("big_v", Literal(456))) + gen.gen_expr(IncDec(Var("big_v"), is_inc=True, is_prefix=True)) + asm_text = "\n".join(gen.asm) + assert "ST R30, R16" in asm_text + assert "LDI R31, 6" in asm_text diff --git a/tests/test_data_transfer.py b/tests/test_data_transfer.py index 40313b6..c50c180 100644 --- a/tests/test_data_transfer.py +++ b/tests/test_data_transfer.py @@ -326,3 +326,47 @@ def test_ser_direct_call(self): cpu = CPU() cpu.op_ser(17) assert cpu.read_reg(17) == 0xFF + + +class TestLDDSTD: + """Test LDD and STD (Load/Store with displacement) and SP (0x3D/0x3E) handling.""" + + def test_ldd_std_basic(self, cpu_with_program, helper): + """Test basic displacement addressing with Y register pair.""" + cpu = cpu_with_program(""" + ldi r28, 0x50 + ldi r29, 0x01 + ldi r16, 42 + std Y+1, r16 + std Y+2, r16 + ldd r17, Y+1 + ldd r18, Y+2 + """) + helper.assert_registers(cpu, {17: 42, 18: 42}) + helper.assert_memory(cpu, 0x0151, 42) + helper.assert_memory(cpu, 0x0152, 42) + + def test_ldd_std_with_z_pair(self, cpu_with_program, helper): + """Test displacement addressing with Z register pair.""" + cpu = cpu_with_program(""" + ldi r30, 0x10 + ldi r31, 0x02 + ldi r16, 77 + std Z+5, r16 + ldd r19, Z+5 + """) + helper.assert_register(cpu, 19, 77) + helper.assert_memory(cpu, 0x0215, 77) + + def test_in_out_spl_sph(self, cpu_with_program, helper): + """Test reading and writing Stack Pointer via I/O ports 0x3D (SPL) and 0x3E (SPH).""" + cpu = cpu_with_program(""" + ldi r16, 0x34 + out 0x3D, r16 + ldi r17, 0x12 + out 0x3E, r17 + in r18, 0x3D + in r19, 0x3E + """) + helper.assert_registers(cpu, {18: 0x34, 19: 0x12}) + assert cpu.sp == 0x1234