pcode-graph 1.1.2__tar.gz → 2.0.0__tar.gz

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.
Files changed (50) hide show
  1. pcode_graph-2.0.0/CHANGELOG +6 -0
  2. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/PKG-INFO +2 -2
  3. pcode_graph-2.0.0/pcode_graph/analysis.py +528 -0
  4. pcode_graph-2.0.0/pcode_graph/basic_blocks.py +196 -0
  5. pcode_graph-1.1.2/pcode_graph/utils.py → pcode_graph-2.0.0/pcode_graph/bit_utils.py +0 -2
  6. pcode_graph-1.1.2/pcode_graph/charac.py → pcode_graph-2.0.0/pcode_graph/characterization.py +5 -5
  7. pcode_graph-2.0.0/pcode_graph/checker.py +235 -0
  8. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/cli.py +7 -3
  9. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/emulator.py +1 -1
  10. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/gnn_exporter.py +19 -17
  11. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/graph.py +20 -10
  12. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/lief_importer.py +3 -5
  13. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/maker.py +30 -13
  14. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/nx_exporter.py +7 -6
  15. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/pcode.py +1 -2
  16. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/registers.py +0 -1
  17. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/simplifier.py +1 -2
  18. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/torch_utils.py +1 -0
  19. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/translator.py +8 -3
  20. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/view.py +21 -11
  21. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/visitor.py +1 -1
  22. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pyproject.toml +2 -2
  23. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/analysis_test.py +48 -12
  24. pcode_graph-2.0.0/tests/basic_block_test.py +281 -0
  25. pcode_graph-2.0.0/tests/callother_test.py +193 -0
  26. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/charac_test.py +4 -1
  27. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/exporter_test.py +2 -2
  28. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/fixtures.py +32 -0
  29. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/maker_test.py +13 -34
  30. pcode_graph-2.0.0/todo.md +2 -0
  31. pcode_graph-1.1.2/pcode_graph/analysis.py +0 -396
  32. pcode_graph-1.1.2/pcode_graph/checker.py +0 -120
  33. pcode_graph-1.1.2/todo.md +0 -3
  34. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/.github/workflows/release.yml +0 -0
  35. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/.github/workflows/test.yml +0 -0
  36. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/.gitignore +0 -0
  37. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/LICENSE +0 -0
  38. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/README.md +0 -0
  39. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/__init__.py +0 -0
  40. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/__main__.py +0 -0
  41. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/arch.py +0 -0
  42. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/asm.py +0 -0
  43. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/pcode_graph/log.py +0 -0
  44. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/requirements.txt +0 -0
  45. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/__init__.py +0 -0
  46. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/conftest.py +0 -0
  47. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/emulator_test.py +0 -0
  48. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/helpers.py +0 -0
  49. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/translator_test.py +0 -0
  50. {pcode_graph-1.1.2 → pcode_graph-2.0.0}/tests/utils_test.py +0 -0
@@ -0,0 +1,6 @@
1
+ # 2.0.0
2
+
3
+ - Add basic blocks node in the graph, to make the graph invariant by instruction reordering, while reducing its diameter. The previous format is still available by setting maker flags. Because of new "membership" edge kind, the number of egde features changed.
4
+ - Change translation of pcode `CALLOTHER` operation as a call to express its possible side effects.
5
+ - Change default number of encoded operands in edge features.
6
+ - Improve graph creation speed, by switching to a basic-blocks based def-use analysis
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pcode_graph
3
- Version: 1.1.2
3
+ Version: 2.0.0
4
4
  Summary: Build, display and vectorize semantic graphs from binary code
5
5
  Project-URL: Homepage, https://github.com/quarkslab/pcode_graph
6
6
  Project-URL: Issues, https://github.com/quarkslab/pcode_graph/issues
@@ -15,7 +15,7 @@ Requires-Dist: docopt
15
15
  Requires-Dist: keystone-engine~=0.9.2
16
16
  Requires-Dist: lief~=1.0.0
17
17
  Requires-Dist: loguru
18
- Requires-Dist: pypcode>=4.0.0
18
+ Requires-Dist: pypcode>=4.0.1
19
19
  Requires-Dist: pyvis
20
20
  Provides-Extra: test
21
21
  Requires-Dist: pytest; extra == 'test'
@@ -0,0 +1,528 @@
1
+ from dataclasses import dataclass, field
2
+ from enum import Enum
3
+ from functools import cache
4
+ import heapq
5
+ from typing import Iterator, override
6
+ from pypcode import OpCode, PcodeOp, Varnode
7
+ from pcode_graph.arch import Arch
8
+ from pcode_graph.pcode import dump_operation, from_native_opcode
9
+ from pcode_graph.bit_utils import unsigned_to_signed
10
+ from pcode_graph.visitor import PcodeVisitor
11
+
12
+ OpIndex = int
13
+ EntryIndex = -1
14
+ UnknownCodeIndex = -2
15
+ ExitIndex = -3
16
+
17
+ Address = int
18
+
19
+ BlockIndex = int
20
+
21
+
22
+ def dump_index(i: OpIndex) -> str:
23
+ match i:
24
+ case -1:
25
+ return "entry"
26
+ case -2:
27
+ return "elsewhere"
28
+ case -3:
29
+ return "exit"
30
+ case _:
31
+ assert i >= 0
32
+ return str(i)
33
+
34
+
35
+ class VarKinds(Enum):
36
+ # Register or named P-Code temporary
37
+ Register = 0
38
+ # P-Code temporary
39
+ Unique = 1
40
+ # Numerical constant
41
+ Constant = 2
42
+ # PC-relative address value
43
+ Ram = 3
44
+ # Dummy access to the memory
45
+ IndirectAccess = 4
46
+
47
+
48
+ @dataclass(frozen=True, order=True)
49
+ class Var:
50
+ """Hashable and comparable wrapper for temporaries, registers, constants and memory."""
51
+
52
+ kind: VarKinds
53
+ size: int
54
+ value: int
55
+ pretty_name: str = field(compare=False)
56
+
57
+ def __str__(self) -> str:
58
+ return self.pretty_name
59
+
60
+
61
+ def wrap(var: Varnode) -> Var:
62
+ match var.space.name:
63
+ case "register":
64
+ name = var.getRegisterName() or "UNKNOWN"
65
+ return Var(VarKinds.Register, var.size, var.offset, name)
66
+ case "unique":
67
+ return Var(VarKinds.Unique, var.size, var.offset, f"${var.offset}")
68
+ case "const":
69
+ return Var(VarKinds.Constant, var.size, var.offset, f"${var.offset}")
70
+ case "ram":
71
+ return Var(VarKinds.Ram, var.size, var.offset, f"[0x{var.offset:x}]")
72
+ case _ as name:
73
+ raise ValueError(f"Unexpected space for input variable: {name}")
74
+
75
+
76
+ # Special variable singleton
77
+ MEMORY = Var(VarKinds.IndirectAccess, 0, 0, "MEMORY")
78
+
79
+ # Writing operations by variable byte (see split), or for MEMORY
80
+ Definitions = dict[Var, set[OpIndex]]
81
+
82
+
83
+ def split(var: Var) -> tuple[Var, ...]:
84
+ """Splits a register into its bytes to track definitions.
85
+ Overlapping registers share bytes (RAX, AX, AH) so that a partial write only kills the definitions of the bytes it writes.
86
+ """
87
+
88
+ if var.kind in {VarKinds.IndirectAccess, VarKinds.Constant}:
89
+ return (var,)
90
+ return _split_bytes(var.kind, var.value, var.size)
91
+
92
+
93
+ @cache
94
+ def _split_bytes(kind: VarKinds, value: int, size: int) -> tuple[Var, ...]:
95
+ return tuple(
96
+ Var(kind, 1, value + offset, f"{kind.name}:{value + offset:#x}")
97
+ for offset in range(size)
98
+ )
99
+
100
+
101
+ @dataclass(slots=True)
102
+ class BasicBlock:
103
+ """Maximal sequence of operations with a single entry and a single exit."""
104
+
105
+ # Operation indexes range: start included, end excluded
106
+ start: OpIndex
107
+ end: OpIndex
108
+
109
+ # Block-level CFG edges, special indexes (EntryIndex, ExitIndex...) are kept as is.
110
+ # Order of successors matters, as for operations.
111
+ pred_blocks: set[BlockIndex] = field(default_factory=set)
112
+ succ_blocks: list[BlockIndex] = field(default_factory=list)
113
+
114
+ reachable: bool = False
115
+
116
+ @property
117
+ def last_op_index(self) -> OpIndex:
118
+ return self.end - 1
119
+
120
+ @property
121
+ def op_indexes(self) -> range:
122
+ return range(self.start, self.end)
123
+
124
+
125
+ def apply_operation(
126
+ definitions: Definitions, index: OpIndex, op: PcodeOp
127
+ ) -> Definitions:
128
+ """Returns the new definitions after the given operation is applied."""
129
+
130
+ if op.output:
131
+ result = dict(definitions)
132
+ written = {index}
133
+ for byte in split(wrap(op.output)):
134
+ result[byte] = written
135
+ return result
136
+ if op.opcode == OpCode.STORE:
137
+ # As we do not analyze which part of memory is written,
138
+ # a write to memory won't kill previous definitions of MEMORY.
139
+ result = dict(definitions)
140
+ result[MEMORY] = definitions.get(MEMORY, set()) | {index}
141
+ return result
142
+ return definitions
143
+
144
+
145
+ @dataclass
146
+ class RichPcodeList:
147
+ """A class to run static analysis on a list of P-Code operations."""
148
+
149
+ # Source architecture
150
+ arch: Arch
151
+
152
+ # List of P-Code operations
153
+ operations: list[PcodeOp]
154
+
155
+ # First P-Code operation for each instruction address
156
+ addresses: dict[Address, OpIndex] = field(default_factory=dict)
157
+
158
+ # Basic blocks sorted by operation index, and block of each operation
159
+ basic_blocks: list[BasicBlock] = field(default_factory=list)
160
+ block_indexes: list[BlockIndex] = field(default_factory=list)
161
+
162
+ # Reaching definitions. For each operation, by variable, list of:
163
+ # - definitions (= writing operation index) that reach this operation,
164
+ # - or EntryIndex (for initial definitions of inputs).
165
+ # Sets are shared between operations: do not modify them.
166
+ reaching_defs: list[Definitions] = field(default_factory=list)
167
+
168
+ def __post_init__(self):
169
+ InstructionAddressCollector(self)()
170
+ CFGBuilder(self)()
171
+ UnreachableCodeFinder(self)()
172
+ ReachingDefBuilder(self)()
173
+
174
+ def __str__(self) -> str:
175
+ return self.dump()
176
+
177
+ def block_of(self, index: OpIndex) -> BasicBlock:
178
+ return self.basic_blocks[self.block_indexes[index]]
179
+
180
+ def reachable(self, index: OpIndex) -> bool:
181
+ return self.block_of(index).reachable
182
+
183
+ def pred_ops(self, index: OpIndex) -> set[OpIndex]:
184
+ """Operation-level CFG predecessors, derived from the block-level CFG."""
185
+
186
+ block = self.block_of(index)
187
+ if index != block.start:
188
+ return {index - 1}
189
+ return {
190
+ self.basic_blocks[pred].last_op_index if pred >= 0 else pred
191
+ for pred in block.pred_blocks
192
+ }
193
+
194
+ def succ_ops(self, index: OpIndex) -> list[OpIndex]:
195
+ """Operation-level CFG successors, derived from the block-level CFG."""
196
+
197
+ block = self.block_of(index)
198
+ if index != block.last_op_index:
199
+ return [index + 1]
200
+ return [
201
+ self.basic_blocks[successor].start if successor >= 0 else successor
202
+ for successor in block.succ_blocks
203
+ ]
204
+
205
+ def dump(self) -> str:
206
+ """Dumps the program and analysis results in a markdown table."""
207
+
208
+ s = "|index|op|preds|succs|input defs|reachable|exit def|\n"
209
+ exit_defs = self.get_exit_defs()
210
+
211
+ s += "|--|--|--|--|--|--|--|\n"
212
+ for i, op in enumerate(self.operations):
213
+ s += f"|{i}|{dump_operation(op)}|"
214
+ s += ", ".join(dump_index(p) for p in self.pred_ops(i)) + "|"
215
+ s += ", ".join(dump_index(p) for p in self.succ_ops(i)) + "|"
216
+ var_defs = []
217
+ for var, defs in self.iter_input_defs(i):
218
+ if defs:
219
+ var_defs.append(
220
+ str(var) + " from " + " or ".join(dump_index(d) for d in defs)
221
+ )
222
+
223
+ s += ", ".join(var_defs) + "|"
224
+ if self.reachable(i):
225
+ s += "x"
226
+ s += "|"
227
+ exit_def_here = []
228
+ for var, defs in exit_defs.items():
229
+ for j in defs:
230
+ if i == j:
231
+ exit_def_here.append(str(var))
232
+ if exit_def_here:
233
+ s += ", ".join(exit_def_here)
234
+ s += "|\n"
235
+
236
+ return s
237
+
238
+ def get_defs(self, index: OpIndex, var: Var) -> set[OpIndex]:
239
+ """Returns all the operations defining the value at given operation of the given variable."""
240
+
241
+ definitions = self.reaching_defs[index]
242
+ defs: set[OpIndex] = set()
243
+ for byte in split(var):
244
+ defs |= definitions.get(byte, set())
245
+ return defs
246
+
247
+ def get_exit_defs(self) -> Definitions:
248
+ """Computes side effects of the program."""
249
+
250
+ exit_defs: Definitions = {}
251
+ for block in self.basic_blocks:
252
+ if not block.reachable or not {UnknownCodeIndex, ExitIndex}.intersection(
253
+ block.succ_blocks
254
+ ):
255
+ continue
256
+ index = block.last_op_index
257
+ outputs = apply_operation(
258
+ self.reaching_defs[index], index, self.operations[index]
259
+ )
260
+ for byte, defs in outputs.items():
261
+ if byte.kind == VarKinds.Unique:
262
+ # Ignore P-Code temporary
263
+ continue
264
+ for writer in defs - {EntryIndex}:
265
+ # Group bytes back by written variable
266
+ output = self.operations[writer].output
267
+ var = wrap(output) if output else MEMORY
268
+ exit_defs.setdefault(var, set()).add(writer)
269
+ return exit_defs
270
+
271
+ def iter_inputs(self, index: OpIndex) -> Iterator[Var]:
272
+ """Iterates operation inputs, skipping **special** ones
273
+ that have no semantic interest to put in the graph (see P-Code documentation).
274
+ """
275
+
276
+ op = self.operations[index]
277
+ if op.opcode == OpCode.LOAD:
278
+ yield MEMORY
279
+
280
+ for i, var in enumerate(op.inputs):
281
+ if i != 0 or op.opcode not in {
282
+ OpCode.IMARK,
283
+ OpCode.CBRANCH,
284
+ OpCode.BRANCH,
285
+ OpCode.CALL,
286
+ OpCode.LOAD,
287
+ OpCode.STORE,
288
+ OpCode.CALLOTHER,
289
+ }:
290
+ yield wrap(var)
291
+
292
+ def iter_input_defs(self, index: OpIndex) -> Iterator[tuple[Var, set[OpIndex]]]:
293
+ """For each operand, returns the written variable and the operation(s) defining it."""
294
+
295
+ for var in self.iter_inputs(index):
296
+ yield var, self.get_defs(index, var)
297
+
298
+
299
+ class PassBase(PcodeVisitor):
300
+
301
+ def __init__(self, program: RichPcodeList) -> None:
302
+ super().__init__()
303
+ self.program = program
304
+
305
+
306
+ class InstructionAddressCollector(PassBase):
307
+ """Collects instruction addresses removing IMARK operations"""
308
+
309
+ def __call__(self):
310
+ for i, op in enumerate(self.program.operations):
311
+ if op.opcode == OpCode.IMARK:
312
+ self.program.addresses[op.inputs[0].offset] = i
313
+
314
+
315
+ class CFGBuilder(PassBase):
316
+
317
+ def __call__(self):
318
+ # Successors of jumping operations
319
+ self._jumps: dict[OpIndex, list[OpIndex]] = {}
320
+ for index, op in enumerate(self.program.operations):
321
+ self.visit(from_native_opcode(op.opcode), index, op)
322
+
323
+ op_count = len(self.program.operations)
324
+ leaders = {0} | {index + 1 for index in self._jumps}
325
+ for targets in self._jumps.values():
326
+ leaders.update(target for target in targets if target >= 0)
327
+ sorted_leaders = sorted(leaders - {op_count}) if op_count else []
328
+
329
+ self.program.basic_blocks = []
330
+ self.program.block_indexes = []
331
+ for start, end in zip(sorted_leaders, sorted_leaders[1:] + [op_count]):
332
+ self.program.block_indexes.extend(
333
+ [len(self.program.basic_blocks)] * (end - start)
334
+ )
335
+ self.program.basic_blocks.append(BasicBlock(start, end))
336
+
337
+ for block_index, block in enumerate(self.program.basic_blocks):
338
+ last_index = block.last_op_index
339
+ for successor in self._jumps.get(
340
+ last_index, [self._next_index(last_index)]
341
+ ):
342
+ if successor >= 0:
343
+ successor = self.program.block_indexes[successor]
344
+ self.program.basic_blocks[successor].pred_blocks.add(block_index)
345
+ block.succ_blocks.append(successor)
346
+
347
+ if self.program.basic_blocks:
348
+ self.program.basic_blocks[0].pred_blocks.add(EntryIndex)
349
+
350
+ def _next_index(self, index: OpIndex) -> OpIndex:
351
+ return index + 1 if index + 1 < len(self.program.operations) else ExitIndex
352
+
353
+ def _get_jump_target(self, index: OpIndex, op: PcodeOp) -> OpIndex:
354
+ address = op.inputs[0]
355
+ match address.space.name:
356
+ case "ram":
357
+ return self.program.addresses.get(address.offset, UnknownCodeIndex)
358
+ case "const":
359
+ # Relative jump inside the current instruction
360
+ target = index + unsigned_to_signed(address.offset, address.size)
361
+ assert 0 <= target < len(self.program.operations)
362
+ return target
363
+ case "register" | "unique":
364
+ return UnknownCodeIndex
365
+ case _ as name:
366
+ raise ValueError(f"Unexpected address space for jump address: {name}")
367
+
368
+ @override
369
+ def visit_default(self, index: OpIndex, op: PcodeOp) -> None:
370
+ pass
371
+
372
+ @override
373
+ def visit_branch(self, index: OpIndex, op: PcodeOp) -> None:
374
+ self._jumps[index] = [self._get_jump_target(index, op)]
375
+
376
+ @override
377
+ def visit_branchind(self, index: OpIndex, op: PcodeOp) -> None:
378
+ self._jumps[index] = [self._get_jump_target(index, op)]
379
+
380
+ @override
381
+ def visit_return(self, index: OpIndex, op: PcodeOp) -> None:
382
+ self._jumps[index] = [self._get_jump_target(index, op)]
383
+
384
+ @override
385
+ def visit_call(self, index: OpIndex, op: PcodeOp) -> None:
386
+ target = self._get_jump_target(index, op)
387
+ if target == UnknownCodeIndex:
388
+ # Calling an unseen function: we suppose it returns here after
389
+ self._jumps[index] = [target, self._next_index(index)]
390
+ else:
391
+ self._jumps[index] = [target]
392
+
393
+ @override
394
+ def visit_callind(self, index: OpIndex, op: PcodeOp) -> None:
395
+ self.visit_call(index, op)
396
+
397
+ @override
398
+ def visit_cbranch(self, index: OpIndex, op: PcodeOp) -> None:
399
+ self._jumps[index] = [self._get_jump_target(index, op), self._next_index(index)]
400
+
401
+ @override
402
+ def visit_callother(self, index: OpIndex, op: PcodeOp) -> None:
403
+ self._jumps[index] = [UnknownCodeIndex, self._next_index(index)]
404
+
405
+
406
+ class UnreachableCodeFinder(PassBase):
407
+
408
+ def __call__(self):
409
+
410
+ pending: list[BlockIndex] = [0] if self.program.basic_blocks else []
411
+ while pending:
412
+ block = self.program.basic_blocks[pending.pop()]
413
+ if block.reachable:
414
+ continue
415
+ block.reachable = True
416
+ pending.extend(
417
+ successor for successor in block.succ_blocks if successor >= 0
418
+ )
419
+
420
+
421
+ class ReachingDefBuilder(PassBase):
422
+
423
+ def __call__(self):
424
+ self.program.reaching_defs = [{} for _ in self.program.operations]
425
+ if not self.program.basic_blocks:
426
+ return
427
+
428
+ block_inputs: list[Definitions] = [{} for _ in self.program.basic_blocks]
429
+
430
+ # Initialize
431
+ for index in range(len(self.program.operations)):
432
+ if not self.program.reachable(index):
433
+ continue
434
+ for var in self.program.iter_inputs(index):
435
+ if var.kind in {
436
+ VarKinds.IndirectAccess,
437
+ VarKinds.Ram,
438
+ VarKinds.Register,
439
+ }:
440
+ for byte in split(var):
441
+ block_inputs[0][byte] = {EntryIndex}
442
+
443
+ # Block effects are computed once, on demand, as (generated defs, killed variables)
444
+ effects: dict[BlockIndex, tuple[Definitions, set[Var]]] = {}
445
+
446
+ # Each reachable block is processed at least once, lowest index first:
447
+ # follows the code layout, close to a reverse post-order.
448
+ # The list is sorted, so it is already a valid heap.
449
+ pending = [
450
+ index
451
+ for index, block in enumerate(self.program.basic_blocks)
452
+ if block.reachable
453
+ ]
454
+ queued = set(pending)
455
+
456
+ while pending:
457
+ block_index = heapq.heappop(pending)
458
+ queued.remove(block_index)
459
+
460
+ if block_index not in effects:
461
+ effects[block_index] = self._compute_effect(
462
+ self.program.basic_blocks[block_index]
463
+ )
464
+ generated, killed = effects[block_index]
465
+ outputs = self._apply_effect(block_inputs[block_index], generated, killed)
466
+
467
+ for successor in self.program.basic_blocks[block_index].succ_blocks:
468
+ if successor < 0:
469
+ continue
470
+ changed = self._merge_definitions(block_inputs[successor], outputs)
471
+ if changed and successor not in queued:
472
+ heapq.heappush(pending, successor)
473
+ queued.add(successor)
474
+
475
+ for block, inputs in zip(self.program.basic_blocks, block_inputs):
476
+ if block.reachable:
477
+ self._expand_block(block, inputs)
478
+
479
+ def _compute_effect(self, block: BasicBlock) -> tuple[Definitions, set[Var]]:
480
+ generated: Definitions = {}
481
+ killed: set[Var] = set()
482
+ for index in block.op_indexes:
483
+ op = self.program.operations[index]
484
+ if op.output:
485
+ written = {index}
486
+ for byte in split(wrap(op.output)):
487
+ generated[byte] = written
488
+ killed.add(byte)
489
+ elif op.opcode == OpCode.STORE:
490
+ # As we do not analyze which part of memory is written,
491
+ # a write to memory won't kill previous definitions of MEMORY.
492
+ generated.setdefault(MEMORY, set()).add(index)
493
+ return generated, killed
494
+
495
+ @staticmethod
496
+ def _apply_effect(
497
+ inputs: Definitions, generated: Definitions, killed: set[Var]
498
+ ) -> Definitions:
499
+ # Sets are never modified in place, so they can be shared
500
+ outputs = dict(generated)
501
+ for var, defs in inputs.items():
502
+ if var in killed:
503
+ continue
504
+ outputs[var] = outputs[var] | defs if var in outputs else defs
505
+ return outputs
506
+
507
+ def _expand_block(self, block: BasicBlock, inputs: Definitions):
508
+ """Computes the definitions reaching each operation of the block."""
509
+
510
+ current = inputs
511
+ for index in block.op_indexes:
512
+ self.program.reaching_defs[index] = current
513
+ current = apply_operation(current, index, self.program.operations[index])
514
+
515
+ @staticmethod
516
+ def _merge_definitions(target: Definitions, source: Definitions) -> bool:
517
+ """Adds source definitions to target ones, returns True if target changed."""
518
+
519
+ changed = False
520
+ for var, defs in source.items():
521
+ target_defs = target.get(var)
522
+ if target_defs is None:
523
+ target[var] = set(defs)
524
+ changed = True
525
+ elif not defs <= target_defs:
526
+ target_defs |= defs
527
+ changed = True
528
+ return changed