def test_extreme_compute_jumps_convergence(self): """Test of compute_jumps() requiring absurd number of passes. NOTE: This test also serves to demonstrate that there is no worst case: the number of passes can be unlimited (or, actually, limited by the size of the provided code). This is an extension of test_compute_jumps_convergence. Instead of two jumps, where the earlier gets extended after the latter, we instead generate a series of many jumps. Each pass of compute_jumps() extends one more instruction, which in turn causes the one behind it to be extended on the next pass. """ if not WORDCODE: return # N: the number of unextended instructions that can be squeezed into a # set of bytes adressable by the arg of an unextended instruction. # The answer is "128", but here's how we arrive at it (and it also # hints at how to make this work for pre-WORDCODE). max_unextended_offset = 1 << 8 unextended_branch_instr_size = 2 N = max_unextended_offset // unextended_branch_instr_size nop = 'UNARY_POSITIVE' # don't use NOP, dis.stack_effect will raise # The number of jumps will be equal to the number of labels. The # number of passes of compute_jumps() required will be one greater # than this. labels = [Label() for x in range(0, 3 * N)] code = Bytecode() code.extend( Instr('JUMP_FORWARD', labels[len(labels) - x - 1]) for x in range(0, len(labels))) end_of_jumps = len(code) code.extend(Instr(nop) for x in range(0, N)) # Now insert the labels. The first is N instructions (i.e. 256 # bytes) after the last jump. Then they proceed to earlier positions # 4 bytes at a time. While the targets are in the range of the nop # instructions, 4 bytes is two instructions. When the targets are in # the range of JUMP_FORWARD instructions we have to allow for the fact # that the instructions will have been extended to four bytes each, so # working backwards 4 bytes per label means just one instruction per # label. offset = end_of_jumps + N for l in range(0, len(labels)): code.insert(offset, labels[l]) if offset <= end_of_jumps: offset -= 1 else: offset -= 2 code.insert(0, Instr("LOAD_CONST", 0)) del end_of_jumps code.append(Instr('RETURN_VALUE')) code.to_code(compute_jumps_passes=(len(labels) + 1))
def test_extreme_compute_jumps_convergence(self): """Test of compute_jumps() requiring absurd number of passes. NOTE: This test also serves to demonstrate that there is no worst case: the number of passes can be unlimited (or, actually, limited by the size of the provided code). This is an extension of test_compute_jumps_convergence. Instead of two jumps, where the earlier gets extended after the latter, we instead generate a series of many jumps. Each pass of compute_jumps() extends one more instruction, which in turn causes the one behind it to be extended on the next pass. """ if not WORDCODE: return # N: the number of unextended instructions that can be squeezed into a # set of bytes adressable by the arg of an unextended instruction. # The answer is "128", but here's how we arrive at it (and it also # hints at how to make this work for pre-WORDCODE). max_unextended_offset = 1 << 8 unextended_branch_instr_size = 2 N = max_unextended_offset // unextended_branch_instr_size nop = 'UNARY_POSITIVE' # don't use NOP, dis.stack_effect will raise # The number of jumps will be equal to the number of labels. The # number of passes of compute_jumps() required will be one greater # than this. labels = [Label() for x in range(0, 3 * N)] code = Bytecode() code.extend(Instr('JUMP_FORWARD', labels[len(labels) - x - 1]) for x in range(0, len(labels))) end_of_jumps = len(code) code.extend(Instr(nop) for x in range(0, N)) # Now insert the labels. The first is N instructions (i.e. 256 # bytes) after the last jump. Then they proceed to earlier positions # 4 bytes at a time. While the targets are in the range of the nop # instructions, 4 bytes is two instructions. When the targets are in # the range of JUMP_FORWARD instructions we have to allow for the fact # that the instructions will have been extended to four bytes each, so # working backwards 4 bytes per label means just one instruction per # label. offset = end_of_jumps + N for l in range(0, len(labels)): code.insert(offset, labels[l]) if offset <= end_of_jumps: offset -= 1 else: offset -= 2 code.insert(0, Instr("LOAD_CONST", 0)) del end_of_jumps code.append(Instr('RETURN_VALUE')) code.to_code(compute_jumps_passes=(len(labels) + 1))
def insert_mark(instrs: b.Bytecode, last_i): """Inserts a mark after the call instruction that last_i points to.""" current_offset = 0 for index, instr in enumerate(instrs): # Only increases offset if it's a real instruction. if not isinstance(instr, b.Instr): continue # When calling a function, there are two possible locations that # caller_frame.last_i can point to: # # 1. The CALL_XXX instruction. This is the "correct" behavior defined in # https://docs.python.org/3.7/library/inspect.html#types-and-members. # # 2. The instruction just before CALL_XXX. This is caused by the "PREDICT" # optimization, described in # https://github.com/python/cpython/blob/v3.7.5/Python/ceval.c#L888-L900 # Note that in this case, The instruction last_i points to must not be # another call. # # We want to find the CALL_XXX instruction that triggers the 'call' event, and # that is either at offset last_i or last_i + 2. if current_offset < last_i: current_offset += 2 # Since Cyberbrain is Python 3.6+ only, it's always 2. continue if current_offset == last_i and utils.is_call_instruction(instr): break if current_offset == last_i + 2 and utils.is_call_instruction(instr): break if current_offset > last_i + 2: raise RuntimeError("No way the program reaches here.") # Inserts __MARK__ after CALL_XXX. instrs.insert(index + 1, b.Instr("LOAD_ATTR", MARK))