OR-1 dataflow CPU sketch

feat: add opcode-to-category mapping for dfgraph

Orual bf838867 f0212962

+250
+74
dfgraph/categories.py
··· 1 + """Opcode-to-category mapping for visual graph rendering. 2 + 3 + Maps each ALUOp/MemOp/CfgOp to a visual category and colour 4 + for the dataflow graph renderer. 5 + """ 6 + 7 + from __future__ import annotations 8 + 9 + from enum import Enum 10 + from typing import Union 11 + 12 + from cm_inst import ArithOp, CfgOp, LogicOp, MemOp, RoutingOp 13 + 14 + 15 + class OpcodeCategory(Enum): 16 + ARITHMETIC = "arithmetic" 17 + LOGIC = "logic" 18 + COMPARISON = "comparison" 19 + ROUTING = "routing" 20 + MEMORY = "memory" 21 + IO = "io" # reserved for future I/O ops (ior, iow, iorw) — not yet in asm/opcodes.py 22 + CONFIG = "config" 23 + 24 + 25 + CATEGORY_COLOURS: dict[OpcodeCategory, str] = { 26 + OpcodeCategory.ARITHMETIC: "#4a90d9", 27 + OpcodeCategory.LOGIC: "#4caf50", 28 + OpcodeCategory.COMPARISON: "#ff9800", 29 + OpcodeCategory.ROUTING: "#9c27b0", 30 + OpcodeCategory.MEMORY: "#ff5722", 31 + OpcodeCategory.IO: "#009688", 32 + OpcodeCategory.CONFIG: "#9e9e9e", 33 + } 34 + 35 + 36 + _COMPARISON_OPS: frozenset[LogicOp] = frozenset({ 37 + LogicOp.EQ, LogicOp.LT, LogicOp.LTE, LogicOp.GT, LogicOp.GTE, 38 + }) 39 + 40 + _CONFIG_ROUTING_OPS: frozenset[RoutingOp] = frozenset({ 41 + RoutingOp.CONST, RoutingOp.FREE_CTX, 42 + }) 43 + 44 + 45 + def categorise(op: Union[ArithOp, LogicOp, RoutingOp, MemOp, CfgOp]) -> OpcodeCategory: 46 + """Categorise an opcode for visual rendering. 47 + 48 + Maps each opcode to a visual category used by the graph renderer. 49 + Handles special cases like LogicOp comparison ops and RoutingOp config ops. 50 + 51 + Args: 52 + op: An opcode enum value (ArithOp, LogicOp, RoutingOp, MemOp, or CfgOp) 53 + 54 + Returns: 55 + The OpcodeCategory for this opcode 56 + 57 + Raises: 58 + ValueError: If the opcode type is unknown 59 + """ 60 + if isinstance(op, ArithOp): 61 + return OpcodeCategory.ARITHMETIC 62 + if isinstance(op, LogicOp): 63 + if op in _COMPARISON_OPS: 64 + return OpcodeCategory.COMPARISON 65 + return OpcodeCategory.LOGIC 66 + if isinstance(op, RoutingOp): 67 + if op in _CONFIG_ROUTING_OPS: 68 + return OpcodeCategory.CONFIG 69 + return OpcodeCategory.ROUTING 70 + if isinstance(op, MemOp): 71 + return OpcodeCategory.MEMORY 72 + if isinstance(op, CfgOp): 73 + return OpcodeCategory.CONFIG 74 + raise ValueError(f"Unknown opcode type: {type(op).__name__}")
+176
tests/test_dfgraph_categories.py
··· 1 + """Tests for dfgraph/categories.py — opcode-to-category mapping. 2 + 3 + Tests verify: 4 + - Every opcode in MNEMONIC_TO_OP has a valid category via categorise() 5 + - ArithOp members map to ARITHMETIC 6 + - LogicOp logic ops map to LOGIC 7 + - LogicOp comparison ops map to COMPARISON 8 + - RoutingOp members (except CONST/FREE_CTX) map to ROUTING 9 + - RoutingOp.CONST and FREE_CTX map to CONFIG 10 + - MemOp members map to MEMORY 11 + - CfgOp members map to CONFIG 12 + - Every OpcodeCategory has a colour in CATEGORY_COLOURS 13 + """ 14 + 15 + import pytest 16 + 17 + from cm_inst import ArithOp, CfgOp, LogicOp, MemOp, RoutingOp 18 + from asm.opcodes import MNEMONIC_TO_OP 19 + from dfgraph.categories import categorise, OpcodeCategory, CATEGORY_COLOURS 20 + 21 + 22 + class TestCategoriseArithOp: 23 + """Tests for ArithOp -> ARITHMETIC category mapping.""" 24 + 25 + @pytest.mark.parametrize("op", [ 26 + ArithOp.ADD, 27 + ArithOp.SUB, 28 + ArithOp.INC, 29 + ArithOp.DEC, 30 + ArithOp.SHIFT_L, 31 + ArithOp.SHIFT_R, 32 + ArithOp.ASHFT_R, 33 + ]) 34 + def test_arith_ops_map_to_arithmetic(self, op): 35 + """All ArithOp members map to ARITHMETIC category.""" 36 + assert categorise(op) == OpcodeCategory.ARITHMETIC 37 + 38 + 39 + class TestCategoriseLogicOp: 40 + """Tests for LogicOp -> LOGIC or COMPARISON category mapping.""" 41 + 42 + @pytest.mark.parametrize("op", [ 43 + LogicOp.AND, 44 + LogicOp.OR, 45 + LogicOp.XOR, 46 + LogicOp.NOT, 47 + ]) 48 + def test_logic_ops_map_to_logic(self, op): 49 + """Pure logic opcodes (AND, OR, XOR, NOT) map to LOGIC category.""" 50 + assert categorise(op) == OpcodeCategory.LOGIC 51 + 52 + @pytest.mark.parametrize("op", [ 53 + LogicOp.EQ, 54 + LogicOp.LT, 55 + LogicOp.LTE, 56 + LogicOp.GT, 57 + LogicOp.GTE, 58 + ]) 59 + def test_comparison_ops_map_to_comparison(self, op): 60 + """Comparison opcodes (EQ, LT, LTE, GT, GTE) map to COMPARISON category.""" 61 + assert categorise(op) == OpcodeCategory.COMPARISON 62 + 63 + 64 + class TestCategoriseRoutingOp: 65 + """Tests for RoutingOp -> ROUTING or CONFIG category mapping.""" 66 + 67 + @pytest.mark.parametrize("op", [ 68 + RoutingOp.BREQ, 69 + RoutingOp.BRGT, 70 + RoutingOp.BRGE, 71 + RoutingOp.BROF, 72 + RoutingOp.SWEQ, 73 + RoutingOp.SWGT, 74 + RoutingOp.SWGE, 75 + RoutingOp.SWOF, 76 + RoutingOp.GATE, 77 + RoutingOp.PASS, 78 + RoutingOp.SEL, 79 + RoutingOp.MRGE, 80 + ]) 81 + def test_routing_ops_map_to_routing(self, op): 82 + """Routing/branch/switch/control opcodes map to ROUTING category.""" 83 + assert categorise(op) == OpcodeCategory.ROUTING 84 + 85 + @pytest.mark.parametrize("op", [ 86 + RoutingOp.CONST, 87 + RoutingOp.FREE_CTX, 88 + ]) 89 + def test_config_routing_ops_map_to_config(self, op): 90 + """RoutingOp.CONST and RoutingOp.FREE_CTX map to CONFIG category.""" 91 + assert categorise(op) == OpcodeCategory.CONFIG 92 + 93 + 94 + class TestCategoriseMemOp: 95 + """Tests for MemOp -> MEMORY category mapping.""" 96 + 97 + @pytest.mark.parametrize("op", [ 98 + MemOp.READ, 99 + MemOp.WRITE, 100 + MemOp.CLEAR, 101 + MemOp.ALLOC, 102 + MemOp.FREE, 103 + MemOp.RD_INC, 104 + MemOp.RD_DEC, 105 + MemOp.CMP_SW, 106 + ]) 107 + def test_memory_ops_map_to_memory(self, op): 108 + """All MemOp members map to MEMORY category.""" 109 + assert categorise(op) == OpcodeCategory.MEMORY 110 + 111 + 112 + class TestCategoriseCfgOp: 113 + """Tests for CfgOp -> CONFIG category mapping.""" 114 + 115 + @pytest.mark.parametrize("op", [ 116 + CfgOp.LOAD_INST, 117 + CfgOp.ROUTE_SET, 118 + ]) 119 + def test_config_ops_map_to_config(self, op): 120 + """All CfgOp members map to CONFIG category.""" 121 + assert categorise(op) == OpcodeCategory.CONFIG 122 + 123 + 124 + class TestCategoriseMnemonicToOp: 125 + """Tests for all opcodes in MNEMONIC_TO_OP.""" 126 + 127 + @pytest.mark.parametrize("mnemonic, op", MNEMONIC_TO_OP.items()) 128 + def test_all_mnemonics_have_category(self, mnemonic, op): 129 + """Every opcode in MNEMONIC_TO_OP has a valid category via categorise().""" 130 + # Should not raise ValueError 131 + category = categorise(op) 132 + assert isinstance(category, OpcodeCategory) 133 + 134 + 135 + class TestCategoryColours: 136 + """Tests for CATEGORY_COLOURS mapping.""" 137 + 138 + def test_all_categories_have_colour(self): 139 + """Every OpcodeCategory has a colour in CATEGORY_COLOURS.""" 140 + for category in OpcodeCategory: 141 + assert category in CATEGORY_COLOURS 142 + colour = CATEGORY_COLOURS[category] 143 + assert isinstance(colour, str) 144 + assert colour.startswith("#") 145 + 146 + def test_colours_are_valid_hex(self): 147 + """All colours are valid 6-digit hex codes.""" 148 + for category, colour in CATEGORY_COLOURS.items(): 149 + assert len(colour) == 7, f"Colour for {category} is not 7 chars: {colour}" 150 + assert colour[0] == "#", f"Colour for {category} doesn't start with #: {colour}" 151 + try: 152 + int(colour[1:], 16) 153 + except ValueError: 154 + pytest.fail(f"Colour for {category} is not valid hex: {colour}") 155 + 156 + def test_expected_colour_values(self): 157 + """Verify the design-specified colours are present.""" 158 + assert CATEGORY_COLOURS[OpcodeCategory.ARITHMETIC] == "#4a90d9" 159 + assert CATEGORY_COLOURS[OpcodeCategory.LOGIC] == "#4caf50" 160 + assert CATEGORY_COLOURS[OpcodeCategory.COMPARISON] == "#ff9800" 161 + assert CATEGORY_COLOURS[OpcodeCategory.ROUTING] == "#9c27b0" 162 + assert CATEGORY_COLOURS[OpcodeCategory.MEMORY] == "#ff5722" 163 + assert CATEGORY_COLOURS[OpcodeCategory.IO] == "#009688" 164 + assert CATEGORY_COLOURS[OpcodeCategory.CONFIG] == "#9e9e9e" 165 + 166 + 167 + class TestCategoriseErrors: 168 + """Tests for error handling in categorise().""" 169 + 170 + def test_unknown_type_raises_valueerror(self): 171 + """Passing an unknown type to categorise() raises ValueError.""" 172 + class UnknownOp: 173 + pass 174 + 175 + with pytest.raises(ValueError, match="Unknown opcode type"): 176 + categorise(UnknownOp()) # type: ignore