pcode-graph 1.1.2__py3-none-any.whl → 2.0.2__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.
pcode_graph/analysis.py CHANGED
@@ -1,9 +1,12 @@
1
1
  from dataclasses import dataclass, field
2
2
  from enum import Enum
3
+ from functools import cache
4
+ import heapq
3
5
  from typing import Iterator, override
4
6
  from pypcode import OpCode, PcodeOp, Varnode
5
7
  from pcode_graph.arch import Arch
6
8
  from pcode_graph.pcode import dump_operation, from_native_opcode
9
+ from pcode_graph.bit_utils import unsigned_to_signed
7
10
  from pcode_graph.visitor import PcodeVisitor
8
11
 
9
12
  OpIndex = int
@@ -13,6 +16,8 @@ ExitIndex = -3
13
16
 
14
17
  Address = int
15
18
 
19
+ BlockIndex = int
20
+
16
21
 
17
22
  def dump_index(i: OpIndex) -> str:
18
23
  match i:
@@ -45,7 +50,7 @@ class Var:
45
50
  """Hashable and comparable wrapper for temporaries, registers, constants and memory."""
46
51
 
47
52
  kind: VarKinds
48
- size: int = field(compare=False)
53
+ size: int
49
54
  value: int
50
55
  pretty_name: str = field(compare=False)
51
56
 
@@ -71,6 +76,71 @@ def wrap(var: Varnode) -> Var:
71
76
  # Special variable singleton
72
77
  MEMORY = Var(VarKinds.IndirectAccess, 0, 0, "MEMORY")
73
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
+
74
144
 
75
145
  @dataclass
76
146
  class RichPcodeList:
@@ -82,21 +152,18 @@ class RichPcodeList:
82
152
  # List of P-Code operations
83
153
  operations: list[PcodeOp]
84
154
 
85
- # CFG edges: src/dst indexes on the previous list.
86
- # Order of successors matters for cbranch, call, etc.
87
- predecessors: dict[OpIndex, set[OpIndex]] = field(default_factory=dict)
88
- successors: dict[OpIndex, list[OpIndex]] = field(default_factory=dict)
89
-
90
155
  # First P-Code operation for each instruction address
91
156
  addresses: dict[Address, OpIndex] = field(default_factory=dict)
92
157
 
93
- # Reachable operation flags
94
- reachable: list[bool] = field(default_factory=list)
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)
95
161
 
96
162
  # Reaching definitions. For each operation, by variable, list of:
97
163
  # - definitions (= writing operation index) that reach this operation,
98
164
  # - or EntryIndex (for initial definitions of inputs).
99
- reaching_defs: list[dict[Var, set[OpIndex]]] = field(default_factory=list)
165
+ # Sets are shared between operations: do not modify them.
166
+ reaching_defs: list[Definitions] = field(default_factory=list)
100
167
 
101
168
  def __post_init__(self):
102
169
  InstructionAddressCollector(self)()
@@ -107,6 +174,34 @@ class RichPcodeList:
107
174
  def __str__(self) -> str:
108
175
  return self.dump()
109
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
+
110
205
  def dump(self) -> str:
111
206
  """Dumps the program and analysis results in a markdown table."""
112
207
 
@@ -116,8 +211,8 @@ class RichPcodeList:
116
211
  s += "|--|--|--|--|--|--|--|\n"
117
212
  for i, op in enumerate(self.operations):
118
213
  s += f"|{i}|{dump_operation(op)}|"
119
- s += ", ".join(dump_index(p) for p in self.predecessors.get(i, [])) + "|"
120
- s += ", ".join(dump_index(p) for p in self.successors.get(i, [])) + "|"
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)) + "|"
121
216
  var_defs = []
122
217
  for var, defs in self.iter_input_defs(i):
123
218
  if defs:
@@ -126,7 +221,7 @@ class RichPcodeList:
126
221
  )
127
222
 
128
223
  s += ", ".join(var_defs) + "|"
129
- if self.reachable[i]:
224
+ if self.reachable(i):
130
225
  s += "x"
131
226
  s += "|"
132
227
  exit_def_here = []
@@ -143,42 +238,40 @@ class RichPcodeList:
143
238
  def get_defs(self, index: OpIndex, var: Var) -> set[OpIndex]:
144
239
  """Returns all the operations defining the value at given operation of the given variable."""
145
240
 
146
- return self.reaching_defs[index].get(var, set())
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
147
246
 
148
- def get_exit_defs(self) -> dict[Var, set[OpIndex]]:
247
+ def get_exit_defs(self) -> Definitions:
149
248
  """Computes side effects of the program."""
150
249
 
151
- exit_defs: dict[Var, set[OpIndex]] = {}
152
-
153
- for index, op in enumerate(self.operations):
154
- if not self.reachable[index]:
155
- continue
156
-
157
- if not {UnknownCodeIndex, ExitIndex}.intersection(
158
- self.successors.get(index, [])
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
159
254
  ):
160
- # Not an exit
161
255
  continue
162
-
163
- for var, defs in self.reaching_defs[index].items():
164
- if var.kind == VarKinds.Unique:
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:
165
262
  # Ignore P-Code temporary
166
263
  continue
167
- write_defs = defs - {EntryIndex}
168
- if write_defs:
169
- # logger.debug(f"Add exit defs for var {var} from op {index}: {defs}")
170
- exit_defs.setdefault(var, set()).update(write_defs)
171
- if op.output:
172
- var = wrap(op.output)
173
- if var.kind != VarKinds.Unique:
174
- exit_defs[var] = {index}
175
- elif op.opcode == OpCode.STORE:
176
- # Do not kill other memory defs
177
- exit_defs.setdefault(MEMORY, set()).add(index)
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)
178
269
  return exit_defs
179
270
 
180
271
  def iter_inputs(self, index: OpIndex) -> Iterator[Var]:
181
- """Iterates operation inputs, skipping **special** ones (see P-Code documentation)."""
272
+ """Iterates operation inputs, skipping **special** ones
273
+ that have no semantic interest to put in the graph (see P-Code documentation).
274
+ """
182
275
 
183
276
  op = self.operations[index]
184
277
  if op.opcode == OpCode.LOAD:
@@ -192,6 +285,7 @@ class RichPcodeList:
192
285
  OpCode.CALL,
193
286
  OpCode.LOAD,
194
287
  OpCode.STORE,
288
+ OpCode.CALLOTHER,
195
289
  }:
196
290
  yield wrap(var)
197
291
 
@@ -219,178 +313,216 @@ class InstructionAddressCollector(PassBase):
219
313
 
220
314
 
221
315
  class CFGBuilder(PassBase):
222
- """List the CFG edges"""
223
316
 
224
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)
225
346
 
226
- # Collected src/dst pairs
227
- self._edges: list[tuple[OpIndex, OpIndex]] = []
228
-
229
- for o, op in enumerate(self.program.operations):
230
- opcode = from_native_opcode(op.opcode)
231
- self.visit(opcode, o, op)
232
-
233
- for src, dst in self._edges:
234
- assert src >= 0 and src < len(self.program.operations)
235
- self.program.successors.setdefault(src, []).append(dst)
236
- if dst not in {UnknownCodeIndex, ExitIndex}:
237
- self.program.predecessors.setdefault(dst, set()).add(src)
347
+ if self.program.basic_blocks:
348
+ self.program.basic_blocks[0].pred_blocks.add(EntryIndex)
238
349
 
239
- self.program.predecessors.setdefault(0, set()).add(EntryIndex)
350
+ def _next_index(self, index: OpIndex) -> OpIndex:
351
+ return index + 1 if index + 1 < len(self.program.operations) else ExitIndex
240
352
 
241
- @override
242
- def visit_default(self, index: int, op: PcodeOp):
243
- # Edge to next operation
244
- next_index = index + 1
245
- if next_index == len(self.program.operations):
246
- next_index = ExitIndex
247
- self._edges.append((index, next_index))
248
-
249
- def _get_jump_edge(self, index: int, op: PcodeOp):
250
- # Edge to branched operation
251
- address_op = op.inputs[0]
252
- match address_op.space.name:
353
+ def _get_jump_target(self, index: OpIndex, op: PcodeOp) -> OpIndex:
354
+ address = op.inputs[0]
355
+ match address.space.name:
253
356
  case "ram":
254
- target_index = self.program.addresses.get(
255
- address_op.offset, UnknownCodeIndex
256
- )
257
- self._edges.append((index, target_index))
357
+ return self.program.addresses.get(address.offset, UnknownCodeIndex)
258
358
  case "const":
259
- # Detect negative offsets
260
- bits = address_op.size * 8
261
- sign_bit = 1 << (bits - 1)
262
- if (address_op.offset & sign_bit) != 0:
263
- offset = address_op.offset - (1 << bits)
264
- else:
265
- offset = address_op.offset
266
- self._edges.append((index, index + offset))
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
267
363
  case "register" | "unique":
268
- self._edges.append((index, UnknownCodeIndex))
364
+ return UnknownCodeIndex
269
365
  case _ as name:
270
366
  raise ValueError(f"Unexpected address space for jump address: {name}")
271
367
 
272
368
  @override
273
- def visit_branch(self, index: int, op: PcodeOp):
274
- # Unconditional branching without return
275
- self._get_jump_edge(index, op)
369
+ def visit_default(self, index: OpIndex, op: PcodeOp) -> None:
370
+ pass
276
371
 
277
372
  @override
278
- def visit_branchind(self, index: int, op: PcodeOp):
279
- # Unconditional branching without return
280
- self._get_jump_edge(index, op)
373
+ def visit_branch(self, index: OpIndex, op: PcodeOp) -> None:
374
+ self._jumps[index] = [self._get_jump_target(index, op)]
281
375
 
282
376
  @override
283
- def visit_return(self, index: int, op: PcodeOp):
284
- # Unconditional branching without return
285
- self._get_jump_edge(index, op)
377
+ def visit_branchind(self, index: OpIndex, op: PcodeOp) -> None:
378
+ self._jumps[index] = [self._get_jump_target(index, op)]
286
379
 
287
380
  @override
288
- def visit_call(self, index: int, op: PcodeOp):
289
- self._get_jump_edge(index, op)
290
- if self._edges[-1][-1] is UnknownCodeIndex:
291
- # Calling an unseen function: we suppose it returns here after
292
- self.visit_default(index, op)
381
+ def visit_return(self, index: OpIndex, op: PcodeOp) -> None:
382
+ self._jumps[index] = [self._get_jump_target(index, op)]
293
383
 
294
384
  @override
295
- def visit_callind(self, index: int, op: PcodeOp):
296
- self._get_jump_edge(index, op)
297
- if self._edges[-1][-1] is UnknownCodeIndex:
385
+ def visit_call(self, index: OpIndex, op: PcodeOp) -> None:
386
+ target = self._get_jump_target(index, op)
387
+ if target == UnknownCodeIndex:
298
388
  # Calling an unseen function: we suppose it returns here after
299
- self.visit_default(index, op)
389
+ self._jumps[index] = [target, self._next_index(index)]
390
+ else:
391
+ self._jumps[index] = [target]
300
392
 
301
393
  @override
302
- def visit_cbranch(self, index: int, op: PcodeOp):
303
- # Conditional branching
304
- self._get_jump_edge(index, op)
305
- self.visit_default(index, op)
394
+ def visit_callind(self, index: OpIndex, op: PcodeOp) -> None:
395
+ self.visit_call(index, op)
306
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)]
307
400
 
308
- class UnreachableCodeFinder(PassBase):
309
- def __call__(self):
310
- self.program.reachable = [False] * len(self.program.operations)
311
- active: set[int] = {0}
312
- while active:
313
- index = active.pop()
314
- self.program.reachable[index] = True
315
- for s in self.program.successors.get(index, []):
316
- if s >= 0 and not self.program.reachable[s]:
317
- active.add(s)
401
+ @override
402
+ def visit_callother(self, index: OpIndex, op: PcodeOp) -> None:
403
+ self._jumps[index] = [UnknownCodeIndex, self._next_index(index)]
318
404
 
319
405
 
320
- class ReachingDefBuilder(PassBase):
406
+ class UnreachableCodeFinder(PassBase):
407
+
321
408
  def __call__(self):
322
409
 
323
- # logger.debug("Initialize defs")
324
- # Initialize empty defs for each operation
325
- self.program.reaching_defs = [{} for i in range(len(self.program.operations))]
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
+ )
326
419
 
327
- # To detect the end of propagation
328
- changed: bool = True
329
420
 
330
- def add_def(var: Var, to_op: OpIndex, from_op: OpIndex):
331
- nonlocal changed
332
- assert to_op >= 0
421
+ class ReachingDefBuilder(PassBase):
333
422
 
334
- defs = self.program.reaching_defs[to_op].setdefault(var, set())
335
- if from_op not in defs:
336
- # logger.debug(f" {var} from {from_op} reaching {to_op}")
337
- defs.add(from_op)
338
- changed = True
423
+ def __call__(self):
424
+ self.program.reaching_defs = [{} for _ in self.program.operations]
425
+ if not self.program.basic_blocks:
426
+ return
339
427
 
340
- # Initialize with start defs
341
- for index, op in enumerate(self.program.operations):
428
+ block_inputs: list[Definitions] = [{} for _ in self.program.basic_blocks]
342
429
 
343
- if not self.program.reachable[index]:
430
+ # Initialize
431
+ for index in range(len(self.program.operations)):
432
+ if not self.program.reachable(index):
344
433
  continue
345
-
346
- if op.output:
347
- var = wrap(op.output)
348
- for succ in self.program.successors.get(index, []):
349
- if succ >= 0:
350
- add_def(var, succ, index)
351
-
352
- elif op.opcode == OpCode.STORE:
353
- for succ in self.program.successors.get(index, []):
354
- if succ >= 0:
355
- add_def(MEMORY, succ, index)
356
-
357
434
  for var in self.program.iter_inputs(index):
358
435
  if var.kind in {
359
436
  VarKinds.IndirectAccess,
360
437
  VarKinds.Ram,
361
438
  VarKinds.Register,
362
439
  }:
363
- add_def(var, 0, EntryIndex)
364
-
365
- # Propagate defs to predecessors
366
- while changed:
367
- changed = False
368
-
369
- # logger.debug("Now reaching defs are:")
370
- # for index, op in enumerate(self.program.operations):
371
- # for var, defs in self.program.reaching_defs[index].items():
372
- # logger.debug(
373
- # f"{index:>5}: {dump_operation(op)}: {var} from {', '.join(dump_index(d) for d in defs)}"
374
- # )
375
-
376
- for index, op in enumerate(self.program.operations):
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)
377
466
 
378
- if not self.program.reachable[index]:
467
+ for successor in self.program.basic_blocks[block_index].succ_blocks:
468
+ if successor < 0:
379
469
  continue
380
-
381
- for p in self.program.predecessors.get(index, set()):
382
-
383
- if p < 0:
384
- continue
385
-
386
- pred_op = self.program.operations[p]
387
- for var, defs in self.program.reaching_defs[p].items():
388
-
389
- if pred_op.output and wrap(pred_op.output) == var:
390
- # Kill previous defs of output variable.
391
- # As we do not analyze which part of memory is read or written,
392
- # a write to memory won't kill previous definitions of MEMORY.
393
- continue
394
-
395
- for writer_index in defs:
396
- add_def(var, index, writer_index)
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