9
9
10
10
import json
11
11
import logging
12
+ from itertools import count
12
13
from types import CodeType
13
14
from typing import TYPE_CHECKING
14
15
@@ -54,11 +55,11 @@ class InstrumentationAdapter:
54
55
# TODO(fk) make this more fine grained? e.g. visit_line, visit_compare etc.
55
56
# Or use sub visitors?
56
57
57
- def visit_entry_node (self , block : BasicBlock , code_object_id : int ) -> None :
58
+ def visit_entry_node (self , basic_block : BasicBlock , code_object_id : int ) -> None :
58
59
"""Called when we visit the entry node of a code object.
59
60
60
61
Args:
61
- block : The basic block of the entry node.
62
+ basic_block : The basic block of the entry node.
62
63
code_object_id: The code object id of the containing code object.
63
64
"""
64
65
@@ -238,13 +239,9 @@ class BranchCoverageInstrumentation(InstrumentationAdapter):
238
239
"""Instruments code objects to enable tracking branch distances and thus
239
240
branch coverage."""
240
241
241
- # Conditional jump operations are the last operation within a basic block
242
+ # Jump operations are the last operation within a basic block
242
243
_JUMP_OP_POS = - 1
243
244
244
- # If a conditional jump is based on a comparison, it has to be the second-to-last
245
- # instruction within the basic block.
246
- _COMPARE_OP_POS = - 2
247
-
248
245
_logger = logging .getLogger (__name__ )
249
246
250
247
def __init__ (self , tracer : ExecutionTracer ) -> None :
@@ -270,30 +267,59 @@ def visit_node(
270
267
271
268
assert len (basic_block ) > 0 , "Empty basic block in CFG."
272
269
maybe_jump : Instr = basic_block [self ._JUMP_OP_POS ]
273
- maybe_compare : Instr | None = (
274
- basic_block [ self . _COMPARE_OP_POS ] if len ( basic_block ) > 1 else None
270
+ maybe_compare_idx : int | None = self . _find_index_of_potential_compare_instr (
271
+ basic_block
275
272
)
276
273
if isinstance (maybe_jump , Instr ):
277
274
predicate_id : int | None = None
278
275
if maybe_jump .name == "FOR_ITER" :
279
276
predicate_id = self ._instrument_for_loop (
280
- cfg , node , basic_block , code_object_id
277
+ cfg = cfg ,
278
+ node = node ,
279
+ basic_block = basic_block ,
280
+ code_object_id = code_object_id ,
281
281
)
282
282
elif maybe_jump .is_cond_jump ():
283
283
predicate_id = self ._instrument_cond_jump (
284
- code_object_id ,
285
- maybe_compare ,
286
- maybe_jump ,
287
- basic_block ,
288
- node ,
284
+ code_object_id = code_object_id ,
285
+ maybe_compare_idx = maybe_compare_idx ,
286
+ jump = maybe_jump ,
287
+ block = basic_block ,
288
+ node = node ,
289
289
)
290
290
if predicate_id is not None :
291
291
node .predicate_id = predicate_id
292
292
293
+ @staticmethod
294
+ def _find_index_of_potential_compare_instr (basic_block : BasicBlock ) -> int | None :
295
+ """It may happen that another instrumentation added artificial instructions
296
+ between the conditional jump and the preceding comparison. Find the index of the
297
+ first non-artificial instruction that precedes the jump at the end of a basic
298
+ block.
299
+
300
+ Args:
301
+ basic_block: The block to search
302
+
303
+ Returns:
304
+ The index of the first non-artificial instruction that precedes the jump.
305
+ The index is negative, i.e., it indexes from the end.
306
+ """
307
+ block_without_jump = basic_block [: BranchCoverageInstrumentation ._JUMP_OP_POS ]
308
+ for idx , instr in zip (
309
+ count (BranchCoverageInstrumentation ._JUMP_OP_POS - 1 , - 1 ),
310
+ reversed (block_without_jump ),
311
+ ):
312
+ if isinstance (instr , ArtificialInstr ):
313
+ # Skip over artificial instructions
314
+ continue
315
+ # Return first result
316
+ return idx
317
+ return None
318
+
293
319
def _instrument_cond_jump (
294
320
self ,
295
321
code_object_id : int ,
296
- maybe_compare : Instr | None ,
322
+ maybe_compare_idx : int | None ,
297
323
jump : Instr ,
298
324
block : BasicBlock ,
299
325
node : ProgramGraphNode ,
@@ -307,27 +333,34 @@ def _instrument_cond_jump(
307
333
308
334
Args:
309
335
code_object_id: The id of the containing Code Object.
310
- maybe_compare : The comparison operation, if any.
336
+ maybe_compare_idx : The index of the comparison operation, if any.
311
337
jump: The jump operation.
312
338
block: The containing basic block.
313
339
node: The associated node from the CFG.
314
340
315
341
Returns:
316
342
The id that was assigned to the predicate.
317
343
"""
344
+ maybe_compare = block [maybe_compare_idx ]
318
345
if (
319
346
maybe_compare is not None
320
347
and isinstance (maybe_compare , Instr )
321
348
and maybe_compare .name in ("COMPARE_OP" , "IS_OP" , "CONTAINS_OP" )
322
349
):
350
+ assert maybe_compare_idx is not None
323
351
return self ._instrument_compare_based_conditional_jump (
324
- block , code_object_id , node
352
+ block = block ,
353
+ code_object_id = code_object_id ,
354
+ compare_idx = maybe_compare_idx ,
355
+ node = node ,
325
356
)
326
357
if jump .name == "JUMP_IF_NOT_EXC_MATCH" :
327
358
return self ._instrument_exception_based_conditional_jump (
328
- block , code_object_id , node
359
+ basic_block = block , code_object_id = code_object_id , node = node
329
360
)
330
- return self ._instrument_bool_based_conditional_jump (block , code_object_id , node )
361
+ return self ._instrument_bool_based_conditional_jump (
362
+ block = block , code_object_id = code_object_id , node = node
363
+ )
331
364
332
365
def _instrument_bool_based_conditional_jump (
333
366
self , block : BasicBlock , code_object_id : int , node : ProgramGraphNode
@@ -369,7 +402,11 @@ def _instrument_bool_based_conditional_jump(
369
402
return predicate_id
370
403
371
404
def _instrument_compare_based_conditional_jump (
372
- self , block : BasicBlock , code_object_id : int , node : ProgramGraphNode
405
+ self ,
406
+ block : BasicBlock ,
407
+ compare_idx : int ,
408
+ code_object_id : int ,
409
+ node : ProgramGraphNode ,
373
410
) -> int :
374
411
"""Instrument compare-based conditional jumps.
375
412
@@ -378,6 +415,7 @@ def _instrument_compare_based_conditional_jump(
378
415
379
416
Args:
380
417
block: The containing basic block.
418
+ compare_idx: The index of the comparison index
381
419
code_object_id: The id of the containing Code Object.
382
420
node: The associated node from the CFG.
383
421
@@ -391,7 +429,7 @@ def _instrument_compare_based_conditional_jump(
391
429
predicate_id = self ._tracer .register_predicate (
392
430
PredicateMetaData (line_no = lineno , code_object_id = code_object_id , node = node )
393
431
)
394
- operation = block [self . _COMPARE_OP_POS ]
432
+ operation = block [compare_idx ]
395
433
396
434
match operation .name :
397
435
case "COMPARE_OP" :
@@ -409,7 +447,7 @@ def _instrument_compare_based_conditional_jump(
409
447
# Insert instructions right before the comparison.
410
448
# We duplicate the values on top of the stack and report
411
449
# them to the tracer.
412
- block [self . _COMPARE_OP_POS : self . _COMPARE_OP_POS ] = [
450
+ block [compare_idx : compare_idx ] = [
413
451
ArtificialInstr ("DUP_TOP_TWO" , lineno = lineno ),
414
452
ArtificialInstr ("LOAD_CONST" , self ._tracer , lineno = lineno ),
415
453
ArtificialInstr (
@@ -427,29 +465,29 @@ def _instrument_compare_based_conditional_jump(
427
465
return predicate_id
428
466
429
467
def _instrument_exception_based_conditional_jump (
430
- self , block : BasicBlock , code_object_id : int , node : ProgramGraphNode
468
+ self , basic_block : BasicBlock , code_object_id : int , node : ProgramGraphNode
431
469
) -> int :
432
470
"""Instrument exception-based conditional jumps.
433
471
434
472
We add a call to the tracer which reports the values that will be used
435
473
in the following exception matching case.
436
474
437
475
Args:
438
- block : The containing basic block.
476
+ basic_block : The containing basic block.
439
477
code_object_id: The id of the containing Code Object.
440
478
node: The associated node from the CFG.
441
479
442
480
Returns:
443
481
The id assigned to the predicate.
444
482
"""
445
- lineno = block [self ._JUMP_OP_POS ].lineno
483
+ lineno = basic_block [self ._JUMP_OP_POS ].lineno
446
484
predicate_id = self ._tracer .register_predicate (
447
485
PredicateMetaData (line_no = lineno , code_object_id = code_object_id , node = node )
448
486
)
449
487
# Insert instructions right before the conditional jump.
450
488
# We duplicate the values on top of the stack and report
451
489
# them to the tracer.
452
- block [self ._JUMP_OP_POS : self ._JUMP_OP_POS ] = [
490
+ basic_block [self ._JUMP_OP_POS : self ._JUMP_OP_POS ] = [
453
491
ArtificialInstr ("DUP_TOP_TWO" , lineno = lineno ),
454
492
ArtificialInstr ("LOAD_CONST" , self ._tracer , lineno = lineno ),
455
493
ArtificialInstr (
@@ -465,19 +503,20 @@ def _instrument_exception_based_conditional_jump(
465
503
]
466
504
return predicate_id
467
505
468
- def visit_entry_node (self , block : BasicBlock , code_object_id : int ) -> None :
506
+ def visit_entry_node (self , basic_block : BasicBlock , code_object_id : int ) -> None :
469
507
"""Add instructions at the beginning of the given basic block which inform
470
508
the tracer, that the code object with the given id has been entered.
471
509
472
510
Args:
473
- block: The entry basic block of a code object, i.e. the first basic block.
511
+ basic_block: The entry basic block of a code object, i.e. the first basic
512
+ block.
474
513
code_object_id: The id that the tracer has assigned to the code object
475
514
which contains the given basic block.
476
515
"""
477
516
# Use line number of first instruction
478
- lineno = block [0 ].lineno
517
+ lineno = basic_block [0 ].lineno
479
518
# Insert instructions at the beginning.
480
- block [0 :0 ] = [
519
+ basic_block [0 :0 ] = [
481
520
ArtificialInstr ("LOAD_CONST" , self ._tracer , lineno = lineno ),
482
521
ArtificialInstr (
483
522
"LOAD_METHOD" ,
0 commit comments