"""Tests for asm.opcodes module — mnemonic mapping and arity classification.""" import pytest from cm_inst import ArithOp, LogicOp, MemOp, RoutingOp from asm.opcodes import ( MNEMONIC_TO_OP, OP_TO_MNEMONIC, MONADIC_OPS, is_monadic, is_dyadic, ) class TestMnemonicToOpMapping: """Verify all ALU opcodes map to correct enum values.""" def test_arithmetic_opcodes(self): """or1-asm.AC1.1: Arithmetic opcodes map correctly.""" assert MNEMONIC_TO_OP["add"] == ArithOp.ADD assert MNEMONIC_TO_OP["sub"] == ArithOp.SUB assert MNEMONIC_TO_OP["inc"] == ArithOp.INC assert MNEMONIC_TO_OP["dec"] == ArithOp.DEC assert MNEMONIC_TO_OP["shl"] == ArithOp.SHL assert MNEMONIC_TO_OP["shr"] == ArithOp.SHR assert MNEMONIC_TO_OP["asr"] == ArithOp.ASR def test_logic_opcodes(self): """or1-asm.AC1.1: Logic opcodes map correctly.""" assert MNEMONIC_TO_OP["and"] == LogicOp.AND assert MNEMONIC_TO_OP["or"] == LogicOp.OR assert MNEMONIC_TO_OP["xor"] == LogicOp.XOR assert MNEMONIC_TO_OP["not"] == LogicOp.NOT assert MNEMONIC_TO_OP["eq"] == LogicOp.EQ assert MNEMONIC_TO_OP["lt"] == LogicOp.LT assert MNEMONIC_TO_OP["lte"] == LogicOp.LTE assert MNEMONIC_TO_OP["gt"] == LogicOp.GT assert MNEMONIC_TO_OP["gte"] == LogicOp.GTE def test_branch_opcodes(self): """or1-asm.AC1.1: Branch opcodes map correctly.""" assert MNEMONIC_TO_OP["breq"] == RoutingOp.BREQ assert MNEMONIC_TO_OP["brgt"] == RoutingOp.BRGT assert MNEMONIC_TO_OP["brge"] == RoutingOp.BRGE assert MNEMONIC_TO_OP["brof"] == RoutingOp.BROF def test_switch_opcodes(self): """or1-asm.AC1.1: Switch opcodes map correctly.""" assert MNEMONIC_TO_OP["sweq"] == RoutingOp.SWEQ assert MNEMONIC_TO_OP["swgt"] == RoutingOp.SWGT assert MNEMONIC_TO_OP["swge"] == RoutingOp.SWGE assert MNEMONIC_TO_OP["swof"] == RoutingOp.SWOF def test_control_opcodes(self): """or1-asm.AC1.1: Control/routing opcodes map correctly.""" assert MNEMONIC_TO_OP["gate"] == RoutingOp.GATE assert MNEMONIC_TO_OP["sel"] == RoutingOp.SEL assert MNEMONIC_TO_OP["merge"] == RoutingOp.MRGE assert MNEMONIC_TO_OP["pass"] == RoutingOp.PASS assert MNEMONIC_TO_OP["const"] == RoutingOp.CONST assert MNEMONIC_TO_OP["free_frame"] == RoutingOp.FREE_FRAME def test_memory_opcodes(self): """or1-asm.AC1.2: Memory opcodes map correctly.""" assert MNEMONIC_TO_OP["read"] == MemOp.READ assert MNEMONIC_TO_OP["write"] == MemOp.WRITE assert MNEMONIC_TO_OP["clear"] == MemOp.CLEAR assert MNEMONIC_TO_OP["alloc"] == MemOp.ALLOC assert MNEMONIC_TO_OP["free"] == MemOp.FREE assert MNEMONIC_TO_OP["rd_inc"] == MemOp.RD_INC assert MNEMONIC_TO_OP["rd_dec"] == MemOp.RD_DEC assert MNEMONIC_TO_OP["cmp_sw"] == MemOp.CMP_SW def test_mnemonic_to_op_count(self): """Verify all expected mnemonics are present.""" # Total: 7 arithmetic + 9 logic + 4 branch + 4 switch + 8 control + 13 memory = 45 # (CfgOp removed: -2 load_inst, route_set; new MemOps added: +5 exec, raw_read, set_page, write_imm, ext) # (new RoutingOps added: +2 extract_tag, alloc_remote) expected_count = 45 assert len(MNEMONIC_TO_OP) == expected_count class TestOpcodeRoundTrip: """Verify round-trip mapping: mnemonic -> op -> mnemonic.""" @pytest.mark.parametrize( "mnemonic", [ # Arithmetic "add", "sub", "inc", "dec", "shl", "shr", "asr", # Logic "and", "or", "xor", "not", "eq", "lt", "lte", "gt", "gte", # Branch "breq", "brgt", "brge", "brof", # Switch "sweq", "swgt", "swge", "swof", # Control "gate", "sel", "merge", "pass", "const", "free_frame", "extract_tag", "alloc_remote", # Memory "read", "write", "clear", "alloc", "free", "rd_inc", "rd_dec", "cmp_sw", "exec", "raw_read", "set_page", "write_imm", "ext", ] ) def test_round_trip(self, mnemonic): """Every mnemonic should round-trip through the mapping.""" op = MNEMONIC_TO_OP[mnemonic] recovered_mnemonic = OP_TO_MNEMONIC[op] assert recovered_mnemonic == mnemonic @pytest.mark.parametrize( "mnemonic", [ # Arithmetic "add", "sub", "inc", "dec", "shl", "shr", "asr", # Logic "and", "or", "xor", "not", "eq", "lt", "lte", "gt", "gte", # Branch "breq", "brgt", "brge", "brof", # Switch "sweq", "swgt", "swge", "swof", # Control "gate", "sel", "merge", "pass", "const", "free_frame", "extract_tag", "alloc_remote", # Memory "read", "write", "clear", "alloc", "free", "rd_inc", "rd_dec", "cmp_sw", "exec", "raw_read", "set_page", "write_imm", "ext", ] ) def test_round_trip_via_dict(self, mnemonic): """Every mnemonic should round-trip via OP_TO_MNEMONIC dict directly. This test verifies the dict is collision-free. For example: - OP_TO_MNEMONIC[ArithOp.ADD] must return 'add' (not 'read') - OP_TO_MNEMONIC[MemOp.READ] must return 'read' (not overwritten) """ op = MNEMONIC_TO_OP[mnemonic] recovered_mnemonic = OP_TO_MNEMONIC[op] assert recovered_mnemonic == mnemonic class TestMonadicOpsSet: """Verify MONADIC_OPS contains exactly the right opcodes.""" def test_monadic_ops_size(self): """MONADIC_OPS should have exactly 22 opcodes (collision-free). Without collision-free implementation, this would be lower due to IntEnum collisions (e.g., ArithOp.INC colliding with some MemOp value). Count: 5 arithmetic + 1 logic + 5 routing + 5 old memory + 5 new memory + 1 WRITE_IMM = 22 (Added: EXTRACT_TAG, ALLOC_REMOTE, WRITE_IMM, FREE_FRAME as monadic routing ops) """ assert len(MONADIC_OPS) == 22 def test_monadic_opcodes_present(self): """All known monadic opcodes should be in the set.""" monadic_list = [ # Arithmetic (single input + const) ArithOp.INC, ArithOp.DEC, ArithOp.SHL, ArithOp.SHR, ArithOp.ASR, # Logic LogicOp.NOT, # Routing RoutingOp.PASS, RoutingOp.CONST, RoutingOp.FREE_FRAME, RoutingOp.EXTRACT_TAG, RoutingOp.ALLOC_REMOTE, # Memory MemOp.READ, MemOp.ALLOC, MemOp.FREE, MemOp.CLEAR, MemOp.RD_INC, MemOp.RD_DEC, MemOp.WRITE_IMM, ] for op in monadic_list: assert op in MONADIC_OPS, f"{op} should be in MONADIC_OPS" assert is_monadic(op), f"{op} should be monadic" def test_collision_free_membership(self): """MONADIC_OPS membership must be collision-free. Due to IntEnum cross-type equality, ArithOp.ADD (0) equals MemOp.READ (0). Without collision-free implementation, ArithOp.ADD in MONADIC_OPS would return True because MemOp.READ is in the set. """ # ArithOp.ADD and MemOp.READ have the same value (0) so they're equal assert ArithOp.ADD == MemOp.READ # But only MemOp.READ should be in MONADIC_OPS, not ArithOp.ADD assert MemOp.READ in MONADIC_OPS assert ArithOp.ADD not in MONADIC_OPS def test_context_dependent_not_in_monadic(self): """WRITE should not be in MONADIC_OPS (context-dependent).""" assert not is_monadic(MemOp.WRITE, const=None) def test_always_dyadic_not_in_monadic(self): """CMP_SW should not be in MONADIC_OPS (always dyadic).""" assert not is_monadic(MemOp.CMP_SW) def test_dyadic_opcodes_not_in_monadic(self): """All known dyadic opcodes should be dyadic.""" dyadic_list = [ # Arithmetic (two inputs) ArithOp.ADD, ArithOp.SUB, # Logic (two inputs) LogicOp.AND, LogicOp.OR, LogicOp.XOR, LogicOp.EQ, LogicOp.LT, LogicOp.LTE, LogicOp.GT, LogicOp.GTE, # Branch/switch (two inputs + dest) RoutingOp.BREQ, RoutingOp.BRGT, RoutingOp.BRGE, RoutingOp.BROF, RoutingOp.SWEQ, RoutingOp.SWGT, RoutingOp.SWGE, RoutingOp.SWOF, RoutingOp.GATE, RoutingOp.SEL, RoutingOp.MRGE, # Memory MemOp.WRITE, # Usually dyadic MemOp.CMP_SW, # Always dyadic ] for op in dyadic_list: assert is_dyadic(op), f"{op} should be dyadic" class TestIsMonadicFunction: """Verify is_monadic() function works correctly.""" @pytest.mark.parametrize( "op", [ ArithOp.INC, ArithOp.DEC, ArithOp.SHL, ArithOp.SHR, ArithOp.ASR, LogicOp.NOT, RoutingOp.PASS, RoutingOp.CONST, RoutingOp.FREE_FRAME, RoutingOp.EXTRACT_TAG, RoutingOp.ALLOC_REMOTE, MemOp.READ, MemOp.ALLOC, MemOp.FREE, MemOp.CLEAR, MemOp.RD_INC, MemOp.RD_DEC, MemOp.EXEC, MemOp.RAW_READ, MemOp.SET_PAGE, MemOp.WRITE_IMM, MemOp.EXT, ] ) def test_always_monadic_opcodes(self, op): """Always-monadic opcodes should return True regardless of const.""" assert is_monadic(op) is True assert is_monadic(op, const=None) is True assert is_monadic(op, const=42) is True def test_write_monadic_with_const(self): """WRITE with const should be monadic.""" assert is_monadic(MemOp.WRITE, const=42) is True def test_write_dyadic_without_const(self): """WRITE without const should be dyadic.""" assert is_monadic(MemOp.WRITE, const=None) is False assert is_monadic(MemOp.WRITE) is False def test_cmp_sw_always_dyadic(self): """CMP_SW should always be dyadic.""" assert is_monadic(MemOp.CMP_SW) is False assert is_monadic(MemOp.CMP_SW, const=None) is False assert is_monadic(MemOp.CMP_SW, const=42) is False @pytest.mark.parametrize( "op", [ ArithOp.ADD, ArithOp.SUB, LogicOp.AND, LogicOp.OR, LogicOp.XOR, LogicOp.EQ, LogicOp.LT, LogicOp.LTE, LogicOp.GT, LogicOp.GTE, RoutingOp.BREQ, RoutingOp.BRGT, RoutingOp.BRGE, RoutingOp.BROF, RoutingOp.SWEQ, RoutingOp.SWGT, RoutingOp.SWGE, RoutingOp.SWOF, RoutingOp.GATE, RoutingOp.SEL, RoutingOp.MRGE, ] ) def test_dyadic_opcodes(self, op): """Dyadic opcodes should return False.""" assert is_monadic(op) is False class TestIsDyadicFunction: """Verify is_dyadic() function is the inverse of is_monadic().""" @pytest.mark.parametrize( "op", [ ArithOp.INC, ArithOp.DEC, ArithOp.SHL, ArithOp.SHR, ArithOp.ASR, LogicOp.NOT, RoutingOp.PASS, RoutingOp.CONST, RoutingOp.FREE_FRAME, RoutingOp.EXTRACT_TAG, RoutingOp.ALLOC_REMOTE, MemOp.READ, MemOp.ALLOC, MemOp.FREE, MemOp.CLEAR, MemOp.RD_INC, MemOp.RD_DEC, MemOp.EXEC, MemOp.RAW_READ, MemOp.SET_PAGE, MemOp.WRITE_IMM, MemOp.EXT, ] ) def test_always_monadic_is_not_dyadic(self, op): """Always-monadic should be not dyadic.""" assert is_dyadic(op) is False def test_write_context_dependent(self): """WRITE arity is context-dependent via const.""" assert is_dyadic(MemOp.WRITE, const=42) is False # monadic assert is_dyadic(MemOp.WRITE, const=None) is True # dyadic def test_cmp_sw_always_dyadic(self): """CMP_SW is always dyadic.""" assert is_dyadic(MemOp.CMP_SW) is True assert is_dyadic(MemOp.CMP_SW, const=42) is True @pytest.mark.parametrize( "op", [ ArithOp.ADD, ArithOp.SUB, LogicOp.AND, LogicOp.OR, LogicOp.XOR, LogicOp.EQ, LogicOp.LT, LogicOp.LTE, LogicOp.GT, LogicOp.GTE, RoutingOp.BREQ, RoutingOp.BRGT, RoutingOp.BRGE, RoutingOp.BROF, RoutingOp.SWEQ, RoutingOp.SWGT, RoutingOp.SWGE, RoutingOp.SWOF, RoutingOp.GATE, RoutingOp.SEL, RoutingOp.MRGE, ] ) def test_dyadic_opcodes_is_dyadic(self, op): """Dyadic opcodes should be dyadic.""" assert is_dyadic(op) is True class TestAC3_2TierGrouping: """AC3.2: MemOp integer values preserve tier 1/tier 2 encoding boundaries.""" def test_tier1_ops_have_values_0_through_5(self): """Tier 1 memory operations must have values in range [0, 5].""" tier1 = [MemOp.READ, MemOp.WRITE, MemOp.EXEC, MemOp.ALLOC, MemOp.FREE, MemOp.EXT] for op in tier1: assert op.value <= 5, f"{op.name} has value {op.value}, expected <= 5" def test_tier2_ops_have_values_6_through_12(self): """Tier 2 memory operations must have values in range [6, 12].""" tier2 = [MemOp.CLEAR, MemOp.RD_INC, MemOp.RD_DEC, MemOp.CMP_SW, MemOp.RAW_READ, MemOp.SET_PAGE, MemOp.WRITE_IMM] for op in tier2: assert op.value >= 6 and op.value <= 12, f"{op.name} has value {op.value}, expected in range [6, 12]" class TestFreeDisambiguation: """Verify free_ctx (ALU) and free (SM) are distinct and round-trip correctly.""" def test_free_frame_is_routing_op(self): """The free_frame mnemonic should map to ALU RoutingOp.FREE_FRAME.""" assert MNEMONIC_TO_OP["free_frame"] == RoutingOp.FREE_FRAME def test_free_is_memop(self): """The free mnemonic should map to SM MemOp.FREE.""" assert MNEMONIC_TO_OP["free"] == MemOp.FREE def test_both_free_round_trip(self): """Both free_frame and free should round-trip correctly.""" # free_frame -> RoutingOp.FREE_FRAME -> free_frame assert OP_TO_MNEMONIC[RoutingOp.FREE_FRAME] == "free_frame" assert OP_TO_MNEMONIC[MemOp.FREE] == "free" def test_no_collision(self): """Distinct enum types should not collide in reverse mapping.""" # RoutingOp.FREE_FRAME and MemOp.FREE are different enum values assert RoutingOp.FREE_FRAME != MemOp.FREE