pcode-graph 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,3 @@
1
+ from pcode_graph.cli import main
2
+
3
+ main()
@@ -0,0 +1,395 @@
1
+ from dataclasses import dataclass, field
2
+ from enum import Enum
3
+ from typing import Iterator, override
4
+ from pypcode import OpCode, PcodeOp, Varnode
5
+ from pcode_graph.arch import Arch
6
+ from pcode_graph.pcode import OpCodes, dump_operation, from_native_opcode
7
+ from pcode_graph.visitor import PcodeVisitor
8
+
9
+ OpIndex = int
10
+ EntryIndex = -1
11
+ UnknownCodeIndex = -2
12
+ ExitIndex = -3
13
+
14
+ Address = int
15
+
16
+
17
+ def dump_index(i: OpIndex) -> str:
18
+ match i:
19
+ case -1:
20
+ return "entry"
21
+ case -2:
22
+ return "elsewhere"
23
+ case -3:
24
+ return "exit"
25
+ case _:
26
+ assert i >= 0
27
+ return str(i)
28
+
29
+
30
+ class VarKinds(Enum):
31
+ # Register or named P-Code temporary
32
+ Register = 0
33
+ # P-Code temporary
34
+ Unique = 1
35
+ # Numerical constant
36
+ Constant = 2
37
+ # PC-relative address value
38
+ Ram = 3
39
+ # Dummy access to the memory
40
+ IndirectAccess = 4
41
+
42
+
43
+ @dataclass(frozen=True, order=True)
44
+ class Var:
45
+ """Hashable and comparable wrapper for temporaries, registers, constants and memory."""
46
+
47
+ kind: VarKinds
48
+ size: int = field(compare=False)
49
+ value: int
50
+ pretty_name: str = field(compare=False)
51
+
52
+ def __str__(self) -> str:
53
+ return self.pretty_name
54
+
55
+
56
+ def wrap(var: Varnode) -> Var:
57
+ match var.space.name:
58
+ case "register":
59
+ return Var(VarKinds.Register, var.size, var.offset, var.getRegisterName())
60
+ case "unique":
61
+ return Var(VarKinds.Unique, var.size, var.offset, f"${var.offset}")
62
+ case "const":
63
+ return Var(VarKinds.Constant, var.size, var.offset, f"${var.offset}")
64
+ case "ram":
65
+ return Var(VarKinds.Ram, var.size, var.offset, f"[0x{var.offset:x}]")
66
+ case _ as name:
67
+ raise ValueError(f"Unexpected space for input variable: {name}")
68
+
69
+
70
+ # Special variable singleton
71
+ MEMORY = Var(VarKinds.IndirectAccess, 0, 0, "MEMORY")
72
+
73
+
74
+ @dataclass
75
+ class RichPcodeList:
76
+ """A class to run static analysis on a list of P-Code operations."""
77
+
78
+ # Source architecture
79
+ arch: Arch
80
+
81
+ # List of P-Code operations
82
+ operations: list[PcodeOp]
83
+
84
+ # CFG edges: src/dst indexes on the previous list.
85
+ # Order of successors matters for cbranch, call, etc.
86
+ predecessors: dict[OpIndex, set[OpIndex]] = field(default_factory=dict)
87
+ successors: dict[OpIndex, list[OpIndex]] = field(default_factory=dict)
88
+
89
+ # First P-Code operation for each instruction address
90
+ addresses: dict[Address, OpIndex] = field(default_factory=dict)
91
+
92
+ # Reachable operation flags
93
+ reachable: list[bool] = field(default_factory=list)
94
+
95
+ # Reaching definitions. For each operation, by variable, list of:
96
+ # - definitions (= writing operation index) that reach this operation,
97
+ # - or EntryIndex (for initial definitions of inputs).
98
+ reaching_defs: list[dict[Var, set[OpIndex]]] = field(default_factory=list)
99
+
100
+ def __post_init__(self):
101
+ InstructionAddressCollector(self)()
102
+ CFGBuilder(self)()
103
+ UnreachableCodeFinder(self)()
104
+ ReachingDefBuilder(self)()
105
+
106
+ def __str__(self) -> str:
107
+ return self.dump()
108
+
109
+ def dump(self) -> str:
110
+ """Dumps the program and analysis results in a markdown table."""
111
+
112
+ s = "|index|op|preds|succs|input defs|reachable|exit def|\n"
113
+ exit_defs = self.get_exit_defs()
114
+
115
+ s += "|--|--|--|--|--|--|--|\n"
116
+ for i, op in enumerate(self.operations):
117
+ s += f"|{i}|{dump_operation(op)}|"
118
+ s += ", ".join(dump_index(p) for p in self.predecessors.get(i, [])) + "|"
119
+ s += ", ".join(dump_index(p) for p in self.successors.get(i, [])) + "|"
120
+ var_defs = []
121
+ for var, defs in self.iter_input_defs(i):
122
+ if defs:
123
+ var_defs.append(
124
+ str(var) + " from " + " or ".join(dump_index(d) for d in defs)
125
+ )
126
+
127
+ s += ", ".join(var_defs) + "|"
128
+ if self.reachable[i]:
129
+ s += "x"
130
+ s += "|"
131
+ exit_def_here = []
132
+ for var, defs in exit_defs.items():
133
+ for j in defs:
134
+ if i == j:
135
+ exit_def_here.append(str(var))
136
+ if exit_def_here:
137
+ s += ", ".join(exit_def_here)
138
+ s += "|\n"
139
+
140
+ return s
141
+
142
+ def get_defs(self, index: OpIndex, var: Var) -> set[OpIndex]:
143
+ """Returns all the operations defining the value at given operation of the given variable."""
144
+
145
+ return self.reaching_defs[index].get(var, set())
146
+
147
+ def get_exit_defs(self) -> dict[Var, set[OpIndex]]:
148
+ """Computes side effects of the program."""
149
+
150
+ exit_defs: dict[Var, set[OpIndex]] = {}
151
+
152
+ for index, op in enumerate(self.operations):
153
+ if not self.reachable[index]:
154
+ continue
155
+
156
+ if not {UnknownCodeIndex, ExitIndex}.intersection(
157
+ self.successors.get(index, [])
158
+ ):
159
+ # Not an exit
160
+ continue
161
+
162
+ for var, defs in self.reaching_defs[index].items():
163
+ if var.kind == VarKinds.Unique:
164
+ # Ignore P-Code temporary
165
+ continue
166
+ write_defs = defs - {EntryIndex}
167
+ if write_defs:
168
+ # logger.debug(f"Add exit defs for var {var} from op {index}: {defs}")
169
+ exit_defs.setdefault(var, set()).update(write_defs)
170
+ if op.output:
171
+ var = wrap(op.output)
172
+ if var.kind != VarKinds.Unique:
173
+ exit_defs[var] = {index}
174
+ elif op.opcode == OpCode.STORE:
175
+ # Do not kill other memory defs
176
+ exit_defs.setdefault(MEMORY, set()).add(index)
177
+ return exit_defs
178
+
179
+ def iter_inputs(self, index: OpIndex) -> Iterator[Var]:
180
+ """Iterates operation inputs, skipping **special** ones (see P-Code documentation)."""
181
+
182
+ op = self.operations[index]
183
+ if op.opcode == OpCode.LOAD:
184
+ yield MEMORY
185
+
186
+ for i, var in enumerate(op.inputs):
187
+ if i != 0 or op.opcode not in {
188
+ OpCode.IMARK,
189
+ OpCode.CBRANCH,
190
+ OpCode.BRANCH,
191
+ OpCode.CALL,
192
+ OpCode.LOAD,
193
+ OpCode.STORE,
194
+ }:
195
+ yield wrap(var)
196
+
197
+ def iter_input_defs(self, index: OpIndex) -> Iterator[tuple[Var, set[OpIndex]]]:
198
+ """For each operand, returns the written variable and the operation(s) defining it."""
199
+
200
+ for var in self.iter_inputs(index):
201
+ yield var, self.get_defs(index, var)
202
+
203
+
204
+ class PassBase(PcodeVisitor):
205
+
206
+ def __init__(self, program: RichPcodeList) -> None:
207
+ super().__init__()
208
+ self.program = program
209
+
210
+
211
+ class InstructionAddressCollector(PassBase):
212
+ """Collects instruction addresses removing IMARK operations"""
213
+
214
+ def __call__(self):
215
+ for i, op in enumerate(self.program.operations):
216
+ if op.opcode == OpCode.IMARK:
217
+ self.program.addresses[op.inputs[0].offset] = i
218
+
219
+
220
+ class CFGBuilder(PassBase):
221
+ """List the CFG edges"""
222
+
223
+ def __call__(self):
224
+
225
+ # Collected src/dst pairs
226
+ self._edges: list[tuple[OpIndex, OpIndex]] = []
227
+
228
+ for o, op in enumerate(self.program.operations):
229
+ opcode = from_native_opcode(op.opcode)
230
+ self.visit(opcode, o, op)
231
+
232
+ for src, dst in self._edges:
233
+ assert src >= 0 and src < len(self.program.operations)
234
+ self.program.successors.setdefault(src, []).append(dst)
235
+ if dst not in {UnknownCodeIndex, ExitIndex}:
236
+ self.program.predecessors.setdefault(dst, set()).add(src)
237
+
238
+ self.program.predecessors.setdefault(0, set()).add(EntryIndex)
239
+
240
+ @override
241
+ def visit_default(self, index: int, op: PcodeOp):
242
+ # Edge to next operation
243
+ next_index = index + 1
244
+ if next_index == len(self.program.operations):
245
+ next_index = ExitIndex
246
+ self._edges.append((index, next_index))
247
+
248
+ def _get_jump_edge(self, index: int, op: PcodeOp):
249
+ # Edge to branched operation
250
+ address_op = op.inputs[0]
251
+ match address_op.space.name:
252
+ case "ram":
253
+ target_index = self.program.addresses.get(
254
+ address_op.offset, UnknownCodeIndex
255
+ )
256
+ self._edges.append((index, target_index))
257
+ case "const":
258
+ # Detect negative offsets
259
+ bits = address_op.size * 8
260
+ sign_bit = 1 << (bits - 1)
261
+ if (address_op.offset & sign_bit) != 0:
262
+ offset = address_op.offset - (1 << bits)
263
+ else:
264
+ offset = address_op.offset
265
+ self._edges.append((index, index + offset))
266
+ case "register" | "unique":
267
+ self._edges.append((index, UnknownCodeIndex))
268
+ case _ as name:
269
+ raise ValueError(f"Unexpected address space for jump address: {name}")
270
+
271
+ @override
272
+ def visit_branch(self, index: int, op: PcodeOp):
273
+ # Unconditional branching without return
274
+ self._get_jump_edge(index, op)
275
+
276
+ @override
277
+ def visit_branchind(self, index: int, op: PcodeOp):
278
+ # Unconditional branching without return
279
+ self._get_jump_edge(index, op)
280
+
281
+ @override
282
+ def visit_return(self, index: int, op: PcodeOp):
283
+ # Unconditional branching without return
284
+ self._get_jump_edge(index, op)
285
+
286
+ @override
287
+ def visit_call(self, index: int, op: PcodeOp):
288
+ self._get_jump_edge(index, op)
289
+ if self._edges[-1][-1] is UnknownCodeIndex:
290
+ # Calling an unseen function: we suppose it returns here after
291
+ self.visit_default(index, op)
292
+
293
+ @override
294
+ def visit_callind(self, index: int, op: PcodeOp):
295
+ self._get_jump_edge(index, op)
296
+ if self._edges[-1][-1] is UnknownCodeIndex:
297
+ # Calling an unseen function: we suppose it returns here after
298
+ self.visit_default(index, op)
299
+
300
+ @override
301
+ def visit_cbranch(self, index: int, op: PcodeOp):
302
+ # Conditional branching
303
+ self._get_jump_edge(index, op)
304
+ self.visit_default(index, op)
305
+
306
+
307
+ class UnreachableCodeFinder(PassBase):
308
+ def __call__(self):
309
+ self.program.reachable = [False] * len(self.program.operations)
310
+ active: set[int] = {0}
311
+ while active:
312
+ index = active.pop()
313
+ self.program.reachable[index] = True
314
+ for s in self.program.successors.get(index, []):
315
+ if s >= 0 and not self.program.reachable[s]:
316
+ active.add(s)
317
+
318
+
319
+ class ReachingDefBuilder(PassBase):
320
+ def __call__(self):
321
+
322
+ # logger.debug("Initialize defs")
323
+ # Initialize empty defs for each operation
324
+ self.program.reaching_defs = [{} for i in range(len(self.program.operations))]
325
+
326
+ # To detect the end of propagation
327
+ changed: bool = True
328
+
329
+ def add_def(var: Var, to_op: OpIndex, from_op: OpIndex):
330
+ nonlocal changed
331
+ assert to_op >= 0
332
+
333
+ defs = self.program.reaching_defs[to_op].setdefault(var, set())
334
+ if from_op not in defs:
335
+ # logger.debug(f" {var} from {from_op} reaching {to_op}")
336
+ defs.add(from_op)
337
+ changed = True
338
+
339
+ # Initialize with start defs
340
+ for index, op in enumerate(self.program.operations):
341
+
342
+ if not self.program.reachable[index]:
343
+ continue
344
+
345
+ if op.output:
346
+ var = wrap(op.output)
347
+ for succ in self.program.successors.get(index, []):
348
+ if succ >= 0:
349
+ add_def(var, succ, index)
350
+
351
+ elif op.opcode == OpCode.STORE:
352
+ for succ in self.program.successors.get(index, []):
353
+ if succ >= 0:
354
+ add_def(MEMORY, succ, index)
355
+
356
+ for var in self.program.iter_inputs(index):
357
+ if var.kind in {
358
+ VarKinds.IndirectAccess,
359
+ VarKinds.Ram,
360
+ VarKinds.Register,
361
+ }:
362
+ add_def(var, 0, EntryIndex)
363
+
364
+ # Propagate defs to predecessors
365
+ while changed:
366
+ changed = False
367
+
368
+ # logger.debug("Now reaching defs are:")
369
+ # for index, op in enumerate(self.program.operations):
370
+ # for var, defs in self.program.reaching_defs[index].items():
371
+ # logger.debug(
372
+ # f"{index:>5}: {dump_operation(op)}: {var} from {', '.join(dump_index(d) for d in defs)}"
373
+ # )
374
+
375
+ for index, op in enumerate(self.program.operations):
376
+
377
+ if not self.program.reachable[index]:
378
+ continue
379
+
380
+ for p in self.program.predecessors.get(index, set()):
381
+
382
+ if p < 0:
383
+ continue
384
+
385
+ pred_op = self.program.operations[p]
386
+ for var, defs in self.program.reaching_defs[p].items():
387
+
388
+ if pred_op.output and wrap(pred_op.output) == var:
389
+ # Kill previous defs of output variable.
390
+ # As we do not analyze which part of memory is read or written,
391
+ # a write to memory won't kill previous definitions of MEMORY.
392
+ continue
393
+
394
+ for writer_index in defs:
395
+ add_def(var, index, writer_index)
pcode_graph/arch.py ADDED
@@ -0,0 +1,10 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class Arch(StrEnum):
5
+ arm_32 = "arm_32"
6
+ arm_64 = "arm_64"
7
+ x86_64 = "x86_64"
8
+ x86_32 = "x86_32"
9
+ mips_32 = "mips_32" # Big endian
10
+ mips_64 = "mips_64"
pcode_graph/asm.py ADDED
@@ -0,0 +1,60 @@
1
+ import keystone
2
+
3
+ from pcode_graph.arch import Arch
4
+
5
+ KEYSTONE_MODES = {
6
+ Arch.arm_32: (
7
+ keystone.KS_ARCH_ARM,
8
+ keystone.KS_MODE_ARM | keystone.KS_MODE_V8 | keystone.KS_MODE_LITTLE_ENDIAN,
9
+ ),
10
+ Arch.arm_64: (keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN),
11
+ Arch.x86_32: (keystone.KS_ARCH_X86, keystone.KS_MODE_32),
12
+ Arch.x86_64: (keystone.KS_ARCH_X86, keystone.KS_MODE_64),
13
+ Arch.mips_32: (
14
+ keystone.KS_ARCH_MIPS,
15
+ keystone.KS_MODE_MIPS32 | keystone.KS_MODE_BIG_ENDIAN,
16
+ ),
17
+ Arch.mips_64: (
18
+ keystone.KS_ARCH_MIPS,
19
+ keystone.KS_MODE_MIPS64 | keystone.KS_MODE_BIG_ENDIAN,
20
+ ),
21
+ }
22
+
23
+
24
+ # To ease diagnostic when writing assembly by hand...
25
+ def split_instructions(asm: str):
26
+
27
+ asm = asm.replace("\t", " ")
28
+ instructions: list[str] = []
29
+
30
+ for line in asm.splitlines():
31
+ for ins in line.split(";"):
32
+ ins = ins.strip()
33
+ if ins:
34
+ instructions.append(ins)
35
+
36
+ return instructions
37
+
38
+
39
+ class Assembler:
40
+
41
+ def __init__(self, arch: Arch) -> None:
42
+ self.arch = arch
43
+ self._keystone = keystone.Ks(*KEYSTONE_MODES[arch])
44
+
45
+ def assemble(self, instructions: str, base_address: int) -> bytes:
46
+ try:
47
+ data, count = self._keystone.asm(
48
+ instructions, addr=base_address, as_bytes=True
49
+ )
50
+ except keystone.KsError:
51
+ for i, ins in enumerate(split_instructions(instructions)):
52
+ try:
53
+ self._keystone.asm(ins)
54
+ except keystone.KsError as e:
55
+ raise ValueError(f"Error in instruction {i}: {ins}: {e}")
56
+ raise
57
+
58
+ assert count > 0
59
+ assert isinstance(data, bytes)
60
+ return data
pcode_graph/charac.py ADDED
@@ -0,0 +1,97 @@
1
+ from enum import IntFlag, auto
2
+ from typing import NamedTuple
3
+ import networkx as nx
4
+
5
+
6
+ from pcode_graph.nx_exporter import export_to_networkx
7
+ from pcode_graph.graph import CDG, EdgeKinds, NodeKinds
8
+ from pcode_graph.pcode import OpCodes
9
+
10
+
11
+ from collections import deque
12
+ from itertools import chain
13
+
14
+ class Diameters(NamedTuple):
15
+ data_only: int
16
+ control_only: int
17
+ both: int
18
+
19
+
20
+ def get_graph_diameters(cdg: CDG) -> Diameters:
21
+ n = len(cdg.nodes)
22
+
23
+ def build_adjacency(edges):
24
+ adj = [[] for _ in range(n)]
25
+ for e in edges:
26
+ adj[e.source_node].append(e.destination_node)
27
+ return adj
28
+
29
+ data_edges = []
30
+ ctrl_edges = []
31
+ for e in cdg.edges:
32
+ if e.kind == EdgeKinds.Data:
33
+ data_edges.append(e)
34
+ else:
35
+ ctrl_edges.append(e)
36
+
37
+ def diameter(adjacency) -> int:
38
+ # Compute graph diameter with BFS
39
+ d = 0
40
+ for src in range(n):
41
+ dist = [-1] * n
42
+ dist[src] = 0
43
+ front = deque((src,))
44
+ while front:
45
+ current = front.popleft()
46
+ for neigbor in adjacency[current]:
47
+ if dist[neigbor] == -1:
48
+ dist[neigbor] = dist[current] + 1
49
+ front.append(neigbor)
50
+ d = max(d, max(dist))
51
+ return d
52
+
53
+ return Diameters(
54
+ diameter(build_adjacency(data_edges)),
55
+ diameter(build_adjacency(ctrl_edges)),
56
+ diameter(build_adjacency(chain(data_edges, ctrl_edges))),
57
+ )
58
+
59
+ class FunctionCharacteristics(IntFlag):
60
+ """Helps evaluating the level of difficulty of this function."""
61
+
62
+ data = auto()
63
+ condition = auto()
64
+ memory = auto()
65
+ phi = auto()
66
+ call = auto()
67
+ loop = auto()
68
+
69
+
70
+ def get_function_characteristics(graph: CDG) -> FunctionCharacteristics:
71
+
72
+ # Every function contains data-flow easiest level
73
+ function_characteristics = FunctionCharacteristics.data
74
+
75
+ ng = export_to_networkx(graph, False)
76
+ try:
77
+ nx.find_cycle(ng)
78
+ except nx.NetworkXNoCycle:
79
+ pass
80
+ else:
81
+ function_characteristics |= FunctionCharacteristics.loop
82
+
83
+ for n in graph.nodes:
84
+ match n.kind:
85
+ case NodeKinds.Operation:
86
+ if n.opcode in {OpCodes.CALL, OpCodes.CALLIND}:
87
+ function_characteristics |= FunctionCharacteristics.call
88
+ elif n.opcode == OpCodes.CBRANCH:
89
+ function_characteristics |= FunctionCharacteristics.condition
90
+
91
+ case NodeKinds.Phi:
92
+ function_characteristics |= FunctionCharacteristics.phi
93
+
94
+ case NodeKinds.ReadMemory | NodeKinds.WrittenMemory:
95
+ function_characteristics |= FunctionCharacteristics.memory
96
+
97
+ return function_characteristics
pcode_graph/checker.py ADDED
@@ -0,0 +1,120 @@
1
+ from pcode_graph.graph import CDG, EdgeKinds, NodeKinds
2
+
3
+ from pcode_graph.pcode import NUM_OPERANDS, OpCodes
4
+
5
+
6
+ def check_graph(graph: CDG):
7
+ """Checks the coherency of this graph.
8
+
9
+ Can be used to verify that the graph remains valid after a set of transformations
10
+ made by a neural network for instance.
11
+
12
+ Raises AssertionError if invalid.
13
+ """
14
+
15
+ # Check edges are valid
16
+ has_cfg = False
17
+ for edge in graph.edges:
18
+ # assert isinstance(edge.source_node, int)
19
+ # assert isinstance(edge.destination_node, int)
20
+ has_cfg |= edge.kind == EdgeKinds.Control
21
+ assert 0 <= edge.source_node < len(graph.nodes)
22
+ assert 0 <= edge.destination_node < len(graph.nodes)
23
+
24
+ # Check nodes attributes are coherent with node kind
25
+ proxy_dict = graph.compute_edge_proxy()
26
+
27
+ for index, node in enumerate(graph.nodes):
28
+
29
+ if node.value:
30
+ assert node.kind == NodeKinds.Constant
31
+
32
+ assert (not node.register_name) ^ (
33
+ node.kind in {NodeKinds.InputRegister, NodeKinds.OutputRegister}
34
+ )
35
+
36
+ proxy = proxy_dict[index]
37
+ control_succs = proxy.get_control_successors()
38
+ data_preds = proxy.get_data_predecessors()
39
+
40
+ if node.kind != NodeKinds.Operation:
41
+ assert node.opcode is None
42
+ else:
43
+ assert node.opcode is not None
44
+ # Check operands numbering
45
+ assert NUM_OPERANDS[node.opcode] in {-1, len(data_preds)}
46
+
47
+ data_numbers: list[int] = []
48
+ control_numbers: list[int] = []
49
+ for edge in proxy.in_edges:
50
+ if edge.kind == EdgeKinds.Data:
51
+ assert edge.operand_number is not None
52
+ data_numbers.append(edge.operand_number)
53
+ for edge in proxy.out_edges:
54
+ if edge.kind == EdgeKinds.Control:
55
+ assert edge.operand_number is not None
56
+ control_numbers.append(edge.operand_number)
57
+ assert sorted(data_numbers) == list(range(len(data_preds)))
58
+ assert sorted(control_numbers) == list(range(len(control_succs)))
59
+
60
+ if has_cfg:
61
+ # Check CFG coherency
62
+ match len(control_succs):
63
+ case 0:
64
+ assert node.kind in {
65
+ NodeKinds.External,
66
+ NodeKinds.End,
67
+ NodeKinds.InputRegister,
68
+ NodeKinds.OutputRegister,
69
+ NodeKinds.Phi,
70
+ NodeKinds.Constant,
71
+ NodeKinds.ReadMemory,
72
+ NodeKinds.WrittenMemory,
73
+ }, f"Node {index} {node} without successor"
74
+ case 1:
75
+ assert node.kind in {NodeKinds.Begin, NodeKinds.Operation}
76
+ if node.kind == NodeKinds.Operation:
77
+ assert node.opcode not in {
78
+ OpCodes.CBRANCH,
79
+ OpCodes.CALLIND,
80
+ OpCodes.CALL,
81
+ }, f"Node {index} {node} with only 1 successors"
82
+ case 2:
83
+ assert node.kind == NodeKinds.Operation and node.opcode in {
84
+ OpCodes.CBRANCH,
85
+ OpCodes.CALLIND,
86
+ OpCodes.CALL,
87
+ }, f"Node {index} {node} with 2 successors"
88
+ case _:
89
+ raise AssertionError("Unexpected number of successors")
90
+
91
+ # Check dataflow coeherency
92
+ match len(data_preds):
93
+ case 0:
94
+ assert node.kind in {
95
+ NodeKinds.Begin,
96
+ NodeKinds.InputRegister,
97
+ NodeKinds.OutputRegister,
98
+ NodeKinds.Constant,
99
+ NodeKinds.ReadMemory,
100
+ NodeKinds.External,
101
+ NodeKinds.End,
102
+ NodeKinds.Operation, # For example the CALL operation
103
+ }, f"Node {index} {node} without data predecessors"
104
+ case 1 | 2:
105
+ assert node.kind in {
106
+ NodeKinds.Operation,
107
+ NodeKinds.Phi,
108
+ NodeKinds.OutputRegister,
109
+ NodeKinds.WrittenMemory,
110
+ }, f"Node {index} {node} with {len(data_preds)} data predecessors"
111
+ case _:
112
+ if node.kind not in {
113
+ NodeKinds.Phi,
114
+ NodeKinds.OutputRegister,
115
+ NodeKinds.WrittenMemory,
116
+ }:
117
+ assert (
118
+ node.kind == NodeKinds.Operation
119
+ and node.opcode == OpCodes.CALLOTHER
120
+ ), f"Node {index} {node} with {len(data_preds)} data predecessors"