OR-1 dataflow CPU sketch
1"""Tests for asm.opcodes module — mnemonic mapping and arity classification."""
2
3import pytest
4from cm_inst import ArithOp, LogicOp, MemOp, RoutingOp
5from asm.opcodes import (
6 MNEMONIC_TO_OP,
7 OP_TO_MNEMONIC,
8 MONADIC_OPS,
9 is_monadic,
10 is_dyadic,
11)
12
13
14class TestMnemonicToOpMapping:
15 """Verify all ALU opcodes map to correct enum values."""
16
17 def test_arithmetic_opcodes(self):
18 """or1-asm.AC1.1: Arithmetic opcodes map correctly."""
19 assert MNEMONIC_TO_OP["add"] == ArithOp.ADD
20 assert MNEMONIC_TO_OP["sub"] == ArithOp.SUB
21 assert MNEMONIC_TO_OP["inc"] == ArithOp.INC
22 assert MNEMONIC_TO_OP["dec"] == ArithOp.DEC
23 assert MNEMONIC_TO_OP["shl"] == ArithOp.SHL
24 assert MNEMONIC_TO_OP["shr"] == ArithOp.SHR
25 assert MNEMONIC_TO_OP["asr"] == ArithOp.ASR
26
27 def test_logic_opcodes(self):
28 """or1-asm.AC1.1: Logic opcodes map correctly."""
29 assert MNEMONIC_TO_OP["and"] == LogicOp.AND
30 assert MNEMONIC_TO_OP["or"] == LogicOp.OR
31 assert MNEMONIC_TO_OP["xor"] == LogicOp.XOR
32 assert MNEMONIC_TO_OP["not"] == LogicOp.NOT
33 assert MNEMONIC_TO_OP["eq"] == LogicOp.EQ
34 assert MNEMONIC_TO_OP["lt"] == LogicOp.LT
35 assert MNEMONIC_TO_OP["lte"] == LogicOp.LTE
36 assert MNEMONIC_TO_OP["gt"] == LogicOp.GT
37 assert MNEMONIC_TO_OP["gte"] == LogicOp.GTE
38
39 def test_branch_opcodes(self):
40 """or1-asm.AC1.1: Branch opcodes map correctly."""
41 assert MNEMONIC_TO_OP["breq"] == RoutingOp.BREQ
42 assert MNEMONIC_TO_OP["brgt"] == RoutingOp.BRGT
43 assert MNEMONIC_TO_OP["brge"] == RoutingOp.BRGE
44 assert MNEMONIC_TO_OP["brof"] == RoutingOp.BROF
45
46 def test_switch_opcodes(self):
47 """or1-asm.AC1.1: Switch opcodes map correctly."""
48 assert MNEMONIC_TO_OP["sweq"] == RoutingOp.SWEQ
49 assert MNEMONIC_TO_OP["swgt"] == RoutingOp.SWGT
50 assert MNEMONIC_TO_OP["swge"] == RoutingOp.SWGE
51 assert MNEMONIC_TO_OP["swof"] == RoutingOp.SWOF
52
53 def test_control_opcodes(self):
54 """or1-asm.AC1.1: Control/routing opcodes map correctly."""
55 assert MNEMONIC_TO_OP["gate"] == RoutingOp.GATE
56 assert MNEMONIC_TO_OP["sel"] == RoutingOp.SEL
57 assert MNEMONIC_TO_OP["merge"] == RoutingOp.MRGE
58 assert MNEMONIC_TO_OP["pass"] == RoutingOp.PASS
59 assert MNEMONIC_TO_OP["const"] == RoutingOp.CONST
60 assert MNEMONIC_TO_OP["free_frame"] == RoutingOp.FREE_FRAME
61
62 def test_memory_opcodes(self):
63 """or1-asm.AC1.2: Memory opcodes map correctly."""
64 assert MNEMONIC_TO_OP["read"] == MemOp.READ
65 assert MNEMONIC_TO_OP["write"] == MemOp.WRITE
66 assert MNEMONIC_TO_OP["clear"] == MemOp.CLEAR
67 assert MNEMONIC_TO_OP["alloc"] == MemOp.ALLOC
68 assert MNEMONIC_TO_OP["free"] == MemOp.FREE
69 assert MNEMONIC_TO_OP["rd_inc"] == MemOp.RD_INC
70 assert MNEMONIC_TO_OP["rd_dec"] == MemOp.RD_DEC
71 assert MNEMONIC_TO_OP["cmp_sw"] == MemOp.CMP_SW
72
73 def test_mnemonic_to_op_count(self):
74 """Verify all expected mnemonics are present."""
75 # Total: 7 arithmetic + 9 logic + 4 branch + 4 switch + 8 control + 13 memory = 45
76 # (CfgOp removed: -2 load_inst, route_set; new MemOps added: +5 exec, raw_read, set_page, write_imm, ext)
77 # (new RoutingOps added: +2 extract_tag, alloc_remote)
78 expected_count = 45
79 assert len(MNEMONIC_TO_OP) == expected_count
80
81
82class TestOpcodeRoundTrip:
83 """Verify round-trip mapping: mnemonic -> op -> mnemonic."""
84
85 @pytest.mark.parametrize(
86 "mnemonic",
87 [
88 # Arithmetic
89 "add", "sub", "inc", "dec", "shl", "shr", "asr",
90 # Logic
91 "and", "or", "xor", "not", "eq", "lt", "lte", "gt", "gte",
92 # Branch
93 "breq", "brgt", "brge", "brof",
94 # Switch
95 "sweq", "swgt", "swge", "swof",
96 # Control
97 "gate", "sel", "merge", "pass", "const", "free_frame", "extract_tag", "alloc_remote",
98 # Memory
99 "read", "write", "clear", "alloc", "free", "rd_inc", "rd_dec", "cmp_sw",
100 "exec", "raw_read", "set_page", "write_imm", "ext",
101 ]
102 )
103 def test_round_trip(self, mnemonic):
104 """Every mnemonic should round-trip through the mapping."""
105 op = MNEMONIC_TO_OP[mnemonic]
106 recovered_mnemonic = OP_TO_MNEMONIC[op]
107 assert recovered_mnemonic == mnemonic
108
109 @pytest.mark.parametrize(
110 "mnemonic",
111 [
112 # Arithmetic
113 "add", "sub", "inc", "dec", "shl", "shr", "asr",
114 # Logic
115 "and", "or", "xor", "not", "eq", "lt", "lte", "gt", "gte",
116 # Branch
117 "breq", "brgt", "brge", "brof",
118 # Switch
119 "sweq", "swgt", "swge", "swof",
120 # Control
121 "gate", "sel", "merge", "pass", "const", "free_frame", "extract_tag", "alloc_remote",
122 # Memory
123 "read", "write", "clear", "alloc", "free", "rd_inc", "rd_dec", "cmp_sw",
124 "exec", "raw_read", "set_page", "write_imm", "ext",
125 ]
126 )
127 def test_round_trip_via_dict(self, mnemonic):
128 """Every mnemonic should round-trip via OP_TO_MNEMONIC dict directly.
129
130 This test verifies the dict is collision-free. For example:
131 - OP_TO_MNEMONIC[ArithOp.ADD] must return 'add' (not 'read')
132 - OP_TO_MNEMONIC[MemOp.READ] must return 'read' (not overwritten)
133 """
134 op = MNEMONIC_TO_OP[mnemonic]
135 recovered_mnemonic = OP_TO_MNEMONIC[op]
136 assert recovered_mnemonic == mnemonic
137
138
139class TestMonadicOpsSet:
140 """Verify MONADIC_OPS contains exactly the right opcodes."""
141
142 def test_monadic_ops_size(self):
143 """MONADIC_OPS should have exactly 22 opcodes (collision-free).
144
145 Without collision-free implementation, this would be lower due to IntEnum
146 collisions (e.g., ArithOp.INC colliding with some MemOp value).
147 Count: 5 arithmetic + 1 logic + 5 routing + 5 old memory + 5 new memory + 1 WRITE_IMM = 22
148 (Added: EXTRACT_TAG, ALLOC_REMOTE, WRITE_IMM, FREE_FRAME as monadic routing ops)
149 """
150 assert len(MONADIC_OPS) == 22
151
152 def test_monadic_opcodes_present(self):
153 """All known monadic opcodes should be in the set."""
154 monadic_list = [
155 # Arithmetic (single input + const)
156 ArithOp.INC, ArithOp.DEC,
157 ArithOp.SHL, ArithOp.SHR, ArithOp.ASR,
158 # Logic
159 LogicOp.NOT,
160 # Routing
161 RoutingOp.PASS, RoutingOp.CONST, RoutingOp.FREE_FRAME,
162 RoutingOp.EXTRACT_TAG, RoutingOp.ALLOC_REMOTE,
163 # Memory
164 MemOp.READ, MemOp.ALLOC, MemOp.FREE, MemOp.CLEAR,
165 MemOp.RD_INC, MemOp.RD_DEC, MemOp.WRITE_IMM,
166 ]
167 for op in monadic_list:
168 assert op in MONADIC_OPS, f"{op} should be in MONADIC_OPS"
169 assert is_monadic(op), f"{op} should be monadic"
170
171 def test_collision_free_membership(self):
172 """MONADIC_OPS membership must be collision-free.
173
174 Due to IntEnum cross-type equality, ArithOp.ADD (0) equals MemOp.READ (0).
175 Without collision-free implementation, ArithOp.ADD in MONADIC_OPS would
176 return True because MemOp.READ is in the set.
177 """
178 # ArithOp.ADD and MemOp.READ have the same value (0) so they're equal
179 assert ArithOp.ADD == MemOp.READ
180 # But only MemOp.READ should be in MONADIC_OPS, not ArithOp.ADD
181 assert MemOp.READ in MONADIC_OPS
182 assert ArithOp.ADD not in MONADIC_OPS
183
184 def test_context_dependent_not_in_monadic(self):
185 """WRITE should not be in MONADIC_OPS (context-dependent)."""
186 assert not is_monadic(MemOp.WRITE, const=None)
187
188 def test_always_dyadic_not_in_monadic(self):
189 """CMP_SW should not be in MONADIC_OPS (always dyadic)."""
190 assert not is_monadic(MemOp.CMP_SW)
191
192 def test_dyadic_opcodes_not_in_monadic(self):
193 """All known dyadic opcodes should be dyadic."""
194 dyadic_list = [
195 # Arithmetic (two inputs)
196 ArithOp.ADD, ArithOp.SUB,
197 # Logic (two inputs)
198 LogicOp.AND, LogicOp.OR, LogicOp.XOR,
199 LogicOp.EQ, LogicOp.LT, LogicOp.LTE, LogicOp.GT, LogicOp.GTE,
200 # Branch/switch (two inputs + dest)
201 RoutingOp.BREQ, RoutingOp.BRGT, RoutingOp.BRGE, RoutingOp.BROF,
202 RoutingOp.SWEQ, RoutingOp.SWGT, RoutingOp.SWGE, RoutingOp.SWOF,
203 RoutingOp.GATE, RoutingOp.SEL, RoutingOp.MRGE,
204 # Memory
205 MemOp.WRITE, # Usually dyadic
206 MemOp.CMP_SW, # Always dyadic
207 ]
208 for op in dyadic_list:
209 assert is_dyadic(op), f"{op} should be dyadic"
210
211
212class TestIsMonadicFunction:
213 """Verify is_monadic() function works correctly."""
214
215 @pytest.mark.parametrize(
216 "op",
217 [
218 ArithOp.INC, ArithOp.DEC,
219 ArithOp.SHL, ArithOp.SHR, ArithOp.ASR,
220 LogicOp.NOT,
221 RoutingOp.PASS, RoutingOp.CONST, RoutingOp.FREE_FRAME,
222 RoutingOp.EXTRACT_TAG, RoutingOp.ALLOC_REMOTE,
223 MemOp.READ, MemOp.ALLOC, MemOp.FREE, MemOp.CLEAR,
224 MemOp.RD_INC, MemOp.RD_DEC,
225 MemOp.EXEC, MemOp.RAW_READ, MemOp.SET_PAGE, MemOp.WRITE_IMM, MemOp.EXT,
226 ]
227 )
228 def test_always_monadic_opcodes(self, op):
229 """Always-monadic opcodes should return True regardless of const."""
230 assert is_monadic(op) is True
231 assert is_monadic(op, const=None) is True
232 assert is_monadic(op, const=42) is True
233
234 def test_write_monadic_with_const(self):
235 """WRITE with const should be monadic."""
236 assert is_monadic(MemOp.WRITE, const=42) is True
237
238 def test_write_dyadic_without_const(self):
239 """WRITE without const should be dyadic."""
240 assert is_monadic(MemOp.WRITE, const=None) is False
241 assert is_monadic(MemOp.WRITE) is False
242
243 def test_cmp_sw_always_dyadic(self):
244 """CMP_SW should always be dyadic."""
245 assert is_monadic(MemOp.CMP_SW) is False
246 assert is_monadic(MemOp.CMP_SW, const=None) is False
247 assert is_monadic(MemOp.CMP_SW, const=42) is False
248
249 @pytest.mark.parametrize(
250 "op",
251 [
252 ArithOp.ADD, ArithOp.SUB,
253 LogicOp.AND, LogicOp.OR, LogicOp.XOR,
254 LogicOp.EQ, LogicOp.LT, LogicOp.LTE, LogicOp.GT, LogicOp.GTE,
255 RoutingOp.BREQ, RoutingOp.BRGT, RoutingOp.BRGE, RoutingOp.BROF,
256 RoutingOp.SWEQ, RoutingOp.SWGT, RoutingOp.SWGE, RoutingOp.SWOF,
257 RoutingOp.GATE, RoutingOp.SEL, RoutingOp.MRGE,
258 ]
259 )
260 def test_dyadic_opcodes(self, op):
261 """Dyadic opcodes should return False."""
262 assert is_monadic(op) is False
263
264
265class TestIsDyadicFunction:
266 """Verify is_dyadic() function is the inverse of is_monadic()."""
267
268 @pytest.mark.parametrize(
269 "op",
270 [
271 ArithOp.INC, ArithOp.DEC,
272 ArithOp.SHL, ArithOp.SHR, ArithOp.ASR,
273 LogicOp.NOT,
274 RoutingOp.PASS, RoutingOp.CONST, RoutingOp.FREE_FRAME,
275 RoutingOp.EXTRACT_TAG, RoutingOp.ALLOC_REMOTE,
276 MemOp.READ, MemOp.ALLOC, MemOp.FREE, MemOp.CLEAR,
277 MemOp.RD_INC, MemOp.RD_DEC,
278 MemOp.EXEC, MemOp.RAW_READ, MemOp.SET_PAGE, MemOp.WRITE_IMM, MemOp.EXT,
279 ]
280 )
281 def test_always_monadic_is_not_dyadic(self, op):
282 """Always-monadic should be not dyadic."""
283 assert is_dyadic(op) is False
284
285 def test_write_context_dependent(self):
286 """WRITE arity is context-dependent via const."""
287 assert is_dyadic(MemOp.WRITE, const=42) is False # monadic
288 assert is_dyadic(MemOp.WRITE, const=None) is True # dyadic
289
290 def test_cmp_sw_always_dyadic(self):
291 """CMP_SW is always dyadic."""
292 assert is_dyadic(MemOp.CMP_SW) is True
293 assert is_dyadic(MemOp.CMP_SW, const=42) is True
294
295 @pytest.mark.parametrize(
296 "op",
297 [
298 ArithOp.ADD, ArithOp.SUB,
299 LogicOp.AND, LogicOp.OR, LogicOp.XOR,
300 LogicOp.EQ, LogicOp.LT, LogicOp.LTE, LogicOp.GT, LogicOp.GTE,
301 RoutingOp.BREQ, RoutingOp.BRGT, RoutingOp.BRGE, RoutingOp.BROF,
302 RoutingOp.SWEQ, RoutingOp.SWGT, RoutingOp.SWGE, RoutingOp.SWOF,
303 RoutingOp.GATE, RoutingOp.SEL, RoutingOp.MRGE,
304 ]
305 )
306 def test_dyadic_opcodes_is_dyadic(self, op):
307 """Dyadic opcodes should be dyadic."""
308 assert is_dyadic(op) is True
309
310
311class TestAC3_2TierGrouping:
312 """AC3.2: MemOp integer values preserve tier 1/tier 2 encoding boundaries."""
313
314 def test_tier1_ops_have_values_0_through_5(self):
315 """Tier 1 memory operations must have values in range [0, 5]."""
316 tier1 = [MemOp.READ, MemOp.WRITE, MemOp.EXEC, MemOp.ALLOC, MemOp.FREE, MemOp.EXT]
317 for op in tier1:
318 assert op.value <= 5, f"{op.name} has value {op.value}, expected <= 5"
319
320 def test_tier2_ops_have_values_6_through_12(self):
321 """Tier 2 memory operations must have values in range [6, 12]."""
322 tier2 = [MemOp.CLEAR, MemOp.RD_INC, MemOp.RD_DEC, MemOp.CMP_SW, MemOp.RAW_READ, MemOp.SET_PAGE, MemOp.WRITE_IMM]
323 for op in tier2:
324 assert op.value >= 6 and op.value <= 12, f"{op.name} has value {op.value}, expected in range [6, 12]"
325
326
327class TestFreeDisambiguation:
328 """Verify free_ctx (ALU) and free (SM) are distinct and round-trip correctly."""
329
330 def test_free_frame_is_routing_op(self):
331 """The free_frame mnemonic should map to ALU RoutingOp.FREE_FRAME."""
332 assert MNEMONIC_TO_OP["free_frame"] == RoutingOp.FREE_FRAME
333
334 def test_free_is_memop(self):
335 """The free mnemonic should map to SM MemOp.FREE."""
336 assert MNEMONIC_TO_OP["free"] == MemOp.FREE
337
338 def test_both_free_round_trip(self):
339 """Both free_frame and free should round-trip correctly."""
340 # free_frame -> RoutingOp.FREE_FRAME -> free_frame
341 assert OP_TO_MNEMONIC[RoutingOp.FREE_FRAME] == "free_frame"
342 assert OP_TO_MNEMONIC[MemOp.FREE] == "free"
343
344 def test_no_collision(self):
345 """Distinct enum types should not collide in reverse mapping."""
346 # RoutingOp.FREE_FRAME and MemOp.FREE are different enum values
347 assert RoutingOp.FREE_FRAME != MemOp.FREE