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