def test_unconditional_jumps(self): # def func(): # if x: # if y: # func() label_instr7 = Label() code = Bytecode([ Instr("LOAD_GLOBAL", "x", lineno=2), Instr("POP_JUMP_IF_FALSE", label_instr7, lineno=2), Instr("LOAD_GLOBAL", "y", lineno=3), Instr("POP_JUMP_IF_FALSE", label_instr7, lineno=3), Instr("LOAD_GLOBAL", "func", lineno=4), Instr("CALL_FUNCTION", 0, lineno=4), Instr("POP_TOP", lineno=4), label_instr7, Instr("LOAD_CONST", None, lineno=4), Instr("RETURN_VALUE", lineno=4), ]) label_return = Label() self.check( code, Instr("LOAD_GLOBAL", "x", lineno=2), Instr("POP_JUMP_IF_FALSE", label_return, lineno=2), Instr("LOAD_GLOBAL", "y", lineno=3), Instr("POP_JUMP_IF_FALSE", label_return, lineno=3), Instr("LOAD_GLOBAL", "func", lineno=4), Instr("CALL_FUNCTION", 0, lineno=4), Instr("POP_TOP", lineno=4), label_return, Instr("LOAD_CONST", None, lineno=4), Instr("RETURN_VALUE", lineno=4), )
def generate_bytecodes(bytecode): # can we do this, ref the const to our own function? return_finally = Label() calculate = Label() yield Instr("LOAD_CONST", prefix) yield Instr('CALL_FUNCTION', 0) yield Instr('DUP_TOP') yield Instr('LOAD_CONST', Ellipsis) # or some noncached sinlgeton yield Instr('COMPARE_OP', Compare.IS) yield Instr('POP_JUMP_IF_TRUE', calculate) yield Instr('RETURN_VALUE') yield Instr('POP_TOP') # need to check the reurn value of the call, and return it directly before the finally block is setup yield calculate yield Instr('SETUP_FINALLY', return_finally) for instr in bytecode: yield instr # return value is now on the top of the stack, so we can cache it in postfix? yield return_finally yield Instr('DUP_TOP') # copy the return value to the top yield Instr('LOAD_CONST', postfix) # load a return function yield Instr('ROT_TWO') # swap so it's a call yield Instr('CALL_FUNCTION', 1) # invoke with the return value yield Instr('POP_TOP') yield Instr('END_FINALLY')
def test_iter_invalid_types(self): # Labels are not allowed in basic blocks block = BasicBlock() block.append(Label()) with self.assertRaises(ValueError): list(block) with self.assertRaises(ValueError): block.legalize(1) # Only one jump allowed and only at the end block = BasicBlock() block2 = BasicBlock() block.extend([Instr("JUMP_ABSOLUTE", block2), Instr("NOP")]) with self.assertRaises(ValueError): list(block) with self.assertRaises(ValueError): block.legalize(1) # jump target must be a BasicBlock block = BasicBlock() label = Label() block.extend([Instr("JUMP_ABSOLUTE", label)]) with self.assertRaises(ValueError): list(block) with self.assertRaises(ValueError): block.legalize(1)
def test_not_jump_if_false(self): # Replace UNARY_NOT+POP_JUMP_IF_FALSE with POP_JUMP_IF_TRUE # # if not x: # y = 9 label = Label() code = Bytecode([ Instr("LOAD_NAME", "x"), Instr("UNARY_NOT"), Instr("POP_JUMP_IF_FALSE", label), Instr("LOAD_CONST", 9), Instr("STORE_NAME", "y"), label, ]) code = self.optimize_blocks(code) label = Label() self.check( code, Instr("LOAD_NAME", "x"), Instr("POP_JUMP_IF_TRUE", label), Instr("LOAD_CONST", 9), Instr("STORE_NAME", "y"), label, )
def test_jump_if_true_to_jump_if_false(self): # Replace JUMP_IF_TRUE_OR_POP jumping to POP_JUMP_IF_FALSE <target> # with POP_JUMP_IF_TRUE <offset after the second POP_JUMP_IF_FALSE> # # if x or y: # z = 1 label_instr3 = Label() label_instr7 = Label() code = Bytecode([ Instr('LOAD_NAME', 'x'), Instr('JUMP_IF_TRUE_OR_POP', label_instr3), Instr('LOAD_NAME', 'y'), label_instr3, Instr('POP_JUMP_IF_FALSE', label_instr7), Instr('LOAD_CONST', 1), Instr('STORE_NAME', 'z'), label_instr7, Instr('LOAD_CONST', None), Instr('RETURN_VALUE') ]) label_instr4 = Label() label_instr7 = Label() self.check(code, Instr('LOAD_NAME', 'x'), Instr('POP_JUMP_IF_TRUE', label_instr4), Instr('LOAD_NAME', 'y'), Instr('POP_JUMP_IF_FALSE', label_instr7), label_instr4, Instr('LOAD_CONST', 1), Instr('STORE_NAME', 'z'), label_instr7, Instr('LOAD_CONST', None), Instr('RETURN_VALUE'))
def test_unconditional_jumps(self): # def func(): # if x: # if y: # func() label_instr7 = Label() code = Bytecode([ Instr('LOAD_GLOBAL', 'x', lineno=2), Instr('POP_JUMP_IF_FALSE', label_instr7, lineno=2), Instr('LOAD_GLOBAL', 'y', lineno=3), Instr('POP_JUMP_IF_FALSE', label_instr7, lineno=3), Instr('LOAD_GLOBAL', 'func', lineno=4), Instr('CALL_FUNCTION', 0, lineno=4), Instr('POP_TOP', lineno=4), label_instr7, Instr('LOAD_CONST', None, lineno=4), Instr('RETURN_VALUE', lineno=4) ]) label_return = Label() self.check(code, Instr('LOAD_GLOBAL', 'x', lineno=2), Instr('POP_JUMP_IF_FALSE', label_return, lineno=2), Instr('LOAD_GLOBAL', 'y', lineno=3), Instr('POP_JUMP_IF_FALSE', label_return, lineno=3), Instr('LOAD_GLOBAL', 'func', lineno=4), Instr('CALL_FUNCTION', 0, lineno=4), Instr('POP_TOP', lineno=4), label_return, Instr('LOAD_CONST', None, lineno=4), Instr('RETURN_VALUE', lineno=4))
def test_legalize(self): code = Bytecode() code.first_lineno = 3 code.extend( [ Instr("LOAD_CONST", 7), Instr("STORE_NAME", "x"), Instr("LOAD_CONST", 8, lineno=4), Instr("STORE_NAME", "y"), Label(), SetLineno(5), Instr("LOAD_CONST", 9, lineno=6), Instr("STORE_NAME", "z"), ] ) code.legalize() self.assertListEqual( code, [ Instr("LOAD_CONST", 7, lineno=3), Instr("STORE_NAME", "x", lineno=3), Instr("LOAD_CONST", 8, lineno=4), Instr("STORE_NAME", "y", lineno=4), Label(), Instr("LOAD_CONST", 9, lineno=5), Instr("STORE_NAME", "z", lineno=5), ], )
def test_from_code(self): code = get_code( """ if test: x = 1 else: x = 2 """ ) bytecode = Bytecode.from_code(code) label_else = Label() label_exit = Label() self.assertEqual( bytecode, [ Instr("LOAD_NAME", "test", lineno=1), Instr("POP_JUMP_IF_FALSE", label_else, lineno=1), Instr("LOAD_CONST", 1, lineno=2), Instr("STORE_NAME", "x", lineno=2), Instr("JUMP_FORWARD", label_exit, lineno=2), label_else, Instr("LOAD_CONST", 2, lineno=4), Instr("STORE_NAME", "x", lineno=4), label_exit, Instr("LOAD_CONST", None, lineno=4), Instr("RETURN_VALUE", lineno=4), ], )
def test_return_value(self): # return+return: remove second return # # def func(): # return 4 # return 5 code = Bytecode([ Instr('LOAD_CONST', 4, lineno=2), Instr('RETURN_VALUE', lineno=2), Instr('LOAD_CONST', 5, lineno=3), Instr('RETURN_VALUE', lineno=3) ]) code = ControlFlowGraph.from_bytecode(code) self.check(code, Instr('LOAD_CONST', 4, lineno=2), Instr('RETURN_VALUE', lineno=2)) # return+return + return+return: remove second and fourth return # # def func(): # return 4 # return 5 # return 6 # return 7 code = Bytecode([ Instr('LOAD_CONST', 4, lineno=2), Instr('RETURN_VALUE', lineno=2), Instr('LOAD_CONST', 5, lineno=3), Instr('RETURN_VALUE', lineno=3), Instr('LOAD_CONST', 6, lineno=4), Instr('RETURN_VALUE', lineno=4), Instr('LOAD_CONST', 7, lineno=5), Instr('RETURN_VALUE', lineno=5) ]) code = ControlFlowGraph.from_bytecode(code) self.check(code, Instr('LOAD_CONST', 4, lineno=2), Instr('RETURN_VALUE', lineno=2)) # return + JUMP_ABSOLUTE: remove JUMP_ABSOLUTE # while 1: # return 7 setup_loop = Label() return_label = Label() code = Bytecode([ setup_loop, Instr('SETUP_LOOP', return_label, lineno=2), Instr('LOAD_CONST', 7, lineno=3), Instr('RETURN_VALUE', lineno=3), Instr('JUMP_ABSOLUTE', setup_loop, lineno=3), Instr('POP_BLOCK', lineno=3), return_label, Instr('LOAD_CONST', None, lineno=3), Instr('RETURN_VALUE', lineno=3) ]) code = ControlFlowGraph.from_bytecode(code) end_loop = Label() self.check(code, Instr('SETUP_LOOP', end_loop, lineno=2), Instr('LOAD_CONST', 7, lineno=3), Instr('RETURN_VALUE', lineno=3), end_loop, Instr('LOAD_CONST', None, lineno=3), Instr('RETURN_VALUE', lineno=3))
def test_invalid_types(self): code = ConcreteBytecode() code.append(Label()) with self.assertRaises(ValueError): list(code) with self.assertRaises(ValueError): ConcreteBytecode([Label()])
def test_from_bytecode(self): bytecode = Bytecode() label = Label() bytecode.extend([Instr('LOAD_NAME', 'test', lineno=1), Instr('POP_JUMP_IF_FALSE', label, lineno=1), Instr('LOAD_CONST', 5, lineno=2), Instr('STORE_NAME', 'x', lineno=2), Instr('JUMP_FORWARD', label, lineno=2), # dead code! Instr('LOAD_CONST', 7, lineno=4), Instr('STORE_NAME', 'x', lineno=4), Label(), # unused label label, Label(), # unused label Instr('LOAD_CONST', None, lineno=4), Instr('RETURN_VALUE', lineno=4)]) blocks = ControlFlowGraph.from_bytecode(bytecode) label2 = blocks[3] self.assertBlocksEqual(blocks, [Instr('LOAD_NAME', 'test', lineno=1), Instr('POP_JUMP_IF_FALSE', label2, lineno=1)], [Instr('LOAD_CONST', 5, lineno=2), Instr('STORE_NAME', 'x', lineno=2), Instr('JUMP_FORWARD', label2, lineno=2)], [Instr('LOAD_CONST', 7, lineno=4), Instr('STORE_NAME', 'x', lineno=4)], [Instr('LOAD_CONST', None, lineno=4), Instr('RETURN_VALUE', lineno=4)])
def test_jump_to_return(self): # def func(condition): # return 'yes' if condition else 'no' label_instr4 = Label() label_instr6 = Label() code = Bytecode([ Instr("LOAD_FAST", "condition"), Instr("POP_JUMP_IF_FALSE", label_instr4), Instr("LOAD_CONST", "yes"), Instr("JUMP_FORWARD", label_instr6), label_instr4, Instr("LOAD_CONST", "no"), label_instr6, Instr("RETURN_VALUE"), ]) label = Label() self.check( code, Instr("LOAD_FAST", "condition"), Instr("POP_JUMP_IF_FALSE", label), Instr("LOAD_CONST", "yes"), Instr("RETURN_VALUE"), label, Instr("LOAD_CONST", "no"), Instr("RETURN_VALUE"), )
def test_from_bytecode_loop(self): # for x in (1, 2, 3): # if x == 2: # break # continue label_loop_start = Label() label_loop_exit = Label() label_loop_end = Label() code = Bytecode() code.extend(( Instr("SETUP_LOOP", label_loop_end, lineno=1), Instr("LOAD_CONST", (1, 2, 3), lineno=1), Instr("GET_ITER", lineno=1), label_loop_start, Instr("FOR_ITER", label_loop_exit, lineno=1), Instr("STORE_NAME", "x", lineno=1), Instr("LOAD_NAME", "x", lineno=2), Instr("LOAD_CONST", 2, lineno=2), Instr("COMPARE_OP", Compare.EQ, lineno=2), Instr("POP_JUMP_IF_FALSE", label_loop_start, lineno=2), Instr("BREAK_LOOP", lineno=3), Instr("JUMP_ABSOLUTE", label_loop_start, lineno=4), Instr("JUMP_ABSOLUTE", label_loop_start, lineno=4), label_loop_exit, Instr("POP_BLOCK", lineno=4), label_loop_end, Instr("LOAD_CONST", None, lineno=4), Instr("RETURN_VALUE", lineno=4), )) blocks = ControlFlowGraph.from_bytecode(code) expected = [ [Instr("SETUP_LOOP", blocks[8], lineno=1)], [ Instr("LOAD_CONST", (1, 2, 3), lineno=1), Instr("GET_ITER", lineno=1) ], [Instr("FOR_ITER", blocks[7], lineno=1)], [ Instr("STORE_NAME", "x", lineno=1), Instr("LOAD_NAME", "x", lineno=2), Instr("LOAD_CONST", 2, lineno=2), Instr("COMPARE_OP", Compare.EQ, lineno=2), Instr("POP_JUMP_IF_FALSE", blocks[2], lineno=2), ], [Instr("BREAK_LOOP", lineno=3)], [Instr("JUMP_ABSOLUTE", blocks[2], lineno=4)], [Instr("JUMP_ABSOLUTE", blocks[2], lineno=4)], [Instr("POP_BLOCK", lineno=4)], [ Instr("LOAD_CONST", None, lineno=4), Instr("RETURN_VALUE", lineno=4) ], ] self.assertBlocksEqual(blocks, *expected)
def _(ast: If, ctx: Ctx): label1 = Label() label2 = Label() ctx.visit(ast.cond) ctx.bc.append( Instr("POP_JUMP_IF_FALSE", arg=label1, lineno=ast.cond.lineno + 1)) ctx.visit(ast.iftrue) ctx.bc.append( Instr('JUMP_FORWARD', arg=label2, lineno=ast.iftrue.lineno + 1)) ctx.bc.append(label1) ctx.visit(ast.iffalse) ctx.bc.append(label2)
def test_from_bytecode_loop(self): # for x in (1, 2, 3): # if x == 2: # break # continue label_loop_start = Label() label_loop_exit = Label() label_loop_end = Label() code = Bytecode() code.extend(( Instr('SETUP_LOOP', label_loop_end, lineno=1), Instr('LOAD_CONST', (1, 2, 3), lineno=1), Instr('GET_ITER', lineno=1), label_loop_start, Instr('FOR_ITER', label_loop_exit, lineno=1), Instr('STORE_NAME', 'x', lineno=1), Instr('LOAD_NAME', 'x', lineno=2), Instr('LOAD_CONST', 2, lineno=2), Instr('COMPARE_OP', Compare.EQ, lineno=2), Instr('POP_JUMP_IF_FALSE', label_loop_start, lineno=2), Instr('BREAK_LOOP', lineno=3), Instr('JUMP_ABSOLUTE', label_loop_start, lineno=4), Instr('JUMP_ABSOLUTE', label_loop_start, lineno=4), label_loop_exit, Instr('POP_BLOCK', lineno=4), label_loop_end, Instr('LOAD_CONST', None, lineno=4), Instr('RETURN_VALUE', lineno=4), )) blocks = ControlFlowGraph.from_bytecode(code) expected = [[Instr('SETUP_LOOP', blocks[8], lineno=1)], [ Instr('LOAD_CONST', (1, 2, 3), lineno=1), Instr('GET_ITER', lineno=1) ], [Instr('FOR_ITER', blocks[7], lineno=1)], [ Instr('STORE_NAME', 'x', lineno=1), Instr('LOAD_NAME', 'x', lineno=2), Instr('LOAD_CONST', 2, lineno=2), Instr('COMPARE_OP', Compare.EQ, lineno=2), Instr('POP_JUMP_IF_FALSE', blocks[2], lineno=2) ], [Instr('BREAK_LOOP', lineno=3)], [Instr('JUMP_ABSOLUTE', blocks[2], lineno=4)], [Instr('JUMP_ABSOLUTE', blocks[2], lineno=4)], [Instr('POP_BLOCK', lineno=4)], [ Instr('LOAD_CONST', None, lineno=4), Instr('RETURN_VALUE', lineno=4) ]] self.assertBlocksEqual(blocks, *expected)
def test_compute_jumps_convergence(self): # Consider the following sequence of instructions: # # JUMP_ABSOLUTE Label1 # JUMP_ABSOLUTE Label2 # ...126 instructions... # Label1: Offset 254 on first pass, 256 second pass # NOP # ... many more instructions ... # Label2: Offset > 256 on first pass # # On first pass of compute_jumps(), Label2 will be at address 254, so # that value encodes into the single byte arg of JUMP_ABSOLUTE. # # On second pass compute_jumps() the instr at Label1 will have offset # of 256 so will also be given an EXTENDED_ARG. # # Thus we need to make an additional pass. This test only verifies # case where 2 passes is insufficient but three is enough. # # On Python > 3.10 we need to double the number since the offset is now # in term of instructions and not bytes. # Create code from comment above. code = Bytecode() label1 = Label() label2 = Label() nop = "NOP" code.append(Instr("JUMP_ABSOLUTE", label1)) code.append(Instr("JUMP_ABSOLUTE", label2)) # Need 254 * 2 + 2 since the arg will change by 1 instruction rather than 2 # bytes. for x in range(4, 510 if OFFSET_AS_INSTRUCTION else 254, 2): code.append(Instr(nop)) code.append(label1) code.append(Instr(nop)) for x in range( 514 if OFFSET_AS_INSTRUCTION else 256, 600 if OFFSET_AS_INSTRUCTION else 300, 2, ): code.append(Instr(nop)) code.append(label2) code.append(Instr(nop)) # This should pass by default. code.to_code() # Try with max of two passes: it should raise with self.assertRaises(RuntimeError): code.to_code(compute_jumps_passes=2)
def test_label2(self): bytecode = Bytecode() label = Label() bytecode.extend( [ Instr("LOAD_NAME", "test", lineno=1), Instr("POP_JUMP_IF_FALSE", label), Instr("LOAD_CONST", 5, lineno=2), Instr("STORE_NAME", "x"), Instr("JUMP_FORWARD", label), Instr("LOAD_CONST", 7, lineno=4), Instr("STORE_NAME", "x"), label, Instr("LOAD_CONST", None), Instr("RETURN_VALUE"), ] ) concrete = bytecode.to_concrete_bytecode() expected = [ ConcreteInstr("LOAD_NAME", 0, lineno=1), ConcreteInstr("POP_JUMP_IF_FALSE", 14, lineno=1), ConcreteInstr("LOAD_CONST", 0, lineno=2), ConcreteInstr("STORE_NAME", 1, lineno=2), ConcreteInstr("JUMP_FORWARD", 4, lineno=2), ConcreteInstr("LOAD_CONST", 1, lineno=4), ConcreteInstr("STORE_NAME", 1, lineno=4), ConcreteInstr("LOAD_CONST", 2, lineno=4), ConcreteInstr("RETURN_VALUE", lineno=4), ] self.assertListEqual(list(concrete), expected) self.assertListEqual(concrete.consts, [5, 7, None]) self.assertListEqual(concrete.names, ["test", "x"]) self.assertListEqual(concrete.varnames, [])
def test_extended_jump(self): NOP = bytes((opcode.opmap["NOP"],)) class BigInstr(ConcreteInstr): def __init__(self, size): super().__init__("NOP") self._size = size def copy(self): return self def assemble(self): return NOP * self._size # (invalid) code using jumps > 0xffff to test extended arg label = Label() nb_nop = 2 ** 16 code = Bytecode( [ Instr("JUMP_ABSOLUTE", label), BigInstr(nb_nop), label, Instr("LOAD_CONST", None), Instr("RETURN_VALUE"), ] ) code_obj = code.to_code() expected = b"\x90\x01\x90\x00q\x06" + NOP * nb_nop + b"d\x00S\x00" self.assertEqual(code_obj.co_code, expected)
def test_to_bytecode(self): # if test: # x = 2 # x = 5 blocks = ControlFlowGraph() blocks.add_block() blocks.add_block() blocks[0].extend([Instr('LOAD_NAME', 'test', lineno=1), Instr('POP_JUMP_IF_FALSE', blocks[2], lineno=1)]) blocks[1].extend([Instr('LOAD_CONST', 5, lineno=2), Instr('STORE_NAME', 'x', lineno=2), Instr('JUMP_FORWARD', blocks[2], lineno=2)]) blocks[2].extend([Instr('LOAD_CONST', 7, lineno=3), Instr('STORE_NAME', 'x', lineno=3), Instr('LOAD_CONST', None, lineno=3), Instr('RETURN_VALUE', lineno=3)]) bytecode = blocks.to_bytecode() label = Label() self.assertEqual(bytecode, [Instr('LOAD_NAME', 'test', lineno=1), Instr('POP_JUMP_IF_FALSE', label, lineno=1), Instr('LOAD_CONST', 5, lineno=2), Instr('STORE_NAME', 'x', lineno=2), Instr('JUMP_FORWARD', label, lineno=2), label, Instr('LOAD_CONST', 7, lineno=3), Instr('STORE_NAME', 'x', lineno=3), Instr('LOAD_CONST', None, lineno=3), Instr('RETURN_VALUE', lineno=3)])
def test_extended_jump(self): NOP = bytes((opcode.opmap['NOP'], )) class BigInstr(ConcreteInstr): def __init__(self, size): super().__init__('NOP') self._size = size def copy(self): return self def assemble(self): return NOP * self._size # (invalid) code using jumps > 0xffff to test extended arg label = Label() nb_nop = 2**16 code = Bytecode([ Instr("JUMP_ABSOLUTE", label), BigInstr(nb_nop), label, Instr('LOAD_CONST', None), Instr('RETURN_VALUE') ]) code_obj = code.to_code() if WORDCODE: expected = b'\x90\x01\x90\x00q\x06' + NOP * nb_nop + b'd\x00S\x00' else: expected = b'\x90\x01\x00q\x06\x00' + NOP * nb_nop + b'd\x00\x00S' self.assertEqual(code_obj.co_code, expected)
def test_label2(self): bytecode = Bytecode() label = Label() bytecode.extend([ Instr('LOAD_NAME', 'test', lineno=1), Instr('POP_JUMP_IF_FALSE', label), Instr('LOAD_CONST', 5, lineno=2), Instr('STORE_NAME', 'x'), Instr('JUMP_FORWARD', label), Instr('LOAD_CONST', 7, lineno=4), Instr('STORE_NAME', 'x'), label, Instr('LOAD_CONST', None), Instr('RETURN_VALUE') ]) concrete = bytecode.to_concrete_bytecode() expected = [ ConcreteInstr('LOAD_NAME', 0, lineno=1), ConcreteInstr('POP_JUMP_IF_FALSE', 14 if WORDCODE else 21, lineno=1), ConcreteInstr('LOAD_CONST', 0, lineno=2), ConcreteInstr('STORE_NAME', 1, lineno=2), ConcreteInstr('JUMP_FORWARD', 4 if WORDCODE else 6, lineno=2), ConcreteInstr('LOAD_CONST', 1, lineno=4), ConcreteInstr('STORE_NAME', 1, lineno=4), ConcreteInstr('LOAD_CONST', 2, lineno=4), ConcreteInstr('RETURN_VALUE', lineno=4) ] self.assertListEqual(list(concrete), expected) self.assertListEqual(concrete.consts, [5, 7, None]) self.assertListEqual(concrete.names, ['test', 'x']) self.assertListEqual(concrete.varnames, [])
def test_compare_op_unary_not(self): for op, not_op in ( (Compare.IN, Compare.NOT_IN), # in => not in (Compare.NOT_IN, Compare.IN), # not in => in (Compare.IS, Compare.IS_NOT), # is => is not (Compare.IS_NOT, Compare.IS), # is not => is ): code = Bytecode([ Instr('LOAD_NAME', 'a'), Instr('LOAD_NAME', 'b'), Instr('COMPARE_OP', op), Instr('UNARY_NOT'), Instr('STORE_NAME', 'x') ]) self.check(code, Instr('LOAD_NAME', 'a'), Instr('LOAD_NAME', 'b'), Instr('COMPARE_OP', not_op), Instr('STORE_NAME', 'x')) # don't optimize: # x = not (a and b is True) label_instr5 = Label() code = Bytecode([ Instr('LOAD_NAME', 'a'), Instr('JUMP_IF_FALSE_OR_POP', label_instr5), Instr('LOAD_NAME', 'b'), Instr('LOAD_CONST', True), Instr('COMPARE_OP', Compare.IS), label_instr5, Instr('UNARY_NOT'), Instr('STORE_NAME', 'x'), Instr('LOAD_CONST', None), Instr('RETURN_VALUE') ]) self.check_dont_optimize(code)
def test_is_uncond_jump(self): label = Label() jump = Instr("JUMP_ABSOLUTE", label) self.assertTrue(jump.is_uncond_jump()) instr = Instr("POP_JUMP_IF_TRUE", label) self.assertFalse(instr.is_uncond_jump())
def test_is_cond_jump(self): label = Label() jump = Instr("POP_JUMP_IF_TRUE", label) self.assertTrue(jump.is_cond_jump()) instr = Instr("LOAD_FAST", "x") self.assertFalse(instr.is_cond_jump())
def test_has_jump(self): label = Label() jump = Instr("JUMP_ABSOLUTE", label) self.assertTrue(jump.has_jump()) instr = Instr("LOAD_FAST", "x") self.assertFalse(instr.has_jump())
def _update_stack(code): yield Instr('LOAD_CONST', _local) yield Instr('LOAD_ATTR', ATTR_STACK) yield Instr('LOAD_ATTR', 'append') yield Instr('LOAD_FAST', LOCAL_KEY) yield Instr('BUILD_LIST', 0) yield Instr('DUP_TOP') yield Instr('STORE_FAST', LOCAL_CHILDREN) yield Instr('BUILD_TUPLE', 2) yield Instr('CALL_FUNCTION', 1) yield Instr('POP_TOP') label = Label() yield Instr('SETUP_FINALLY', label) yield from code yield label yield Instr('LOAD_CONST', _local) yield Instr('LOAD_ATTR', ATTR_STACK) yield Instr('LOAD_ATTR', 'pop') yield Instr('CALL_FUNCTION', 0) yield Instr('POP_TOP') yield Instr('END_FINALLY') yield Instr('LOAD_CONST', None) yield Instr('RETURN_VALUE')
def make_bytecode(self, symbol_table): catch_block = Label() start_loop = Label() end_loop = Label() end_for = Label() end_of_function = Label() inner = [ Instr("SETUP_EXCEPT", catch_block), Instr("SETUP_LOOP", end_for), Instr("LOAD_NAME", self.collection), Instr("GET_ITER"), start_loop, Instr("FOR_ITER", end_loop), Instr("STORE_NAME", self.variable) ] for element in self.sequence.elements: inner += element.make_bytecode(symbol_table) inner += [ Instr("JUMP_ABSOLUTE", start_loop), end_loop, Instr("POP_BLOCK"), end_for, Instr("POP_BLOCK"), Instr("JUMP_FORWARD", end_of_function) ] inner += [ catch_block, # In the catch block, we catch everything # unconditionally (this is wrong, we should filter # out just StopException, which is the only thing we # have any business catching here). # # We pop (I think?) the exception and the stack # frame data. Instr("POP_TOP"), Instr("POP_TOP"), Instr("POP_TOP"), # Pop the exception frame from the block stack. Instr("POP_EXCEPT"), Instr("JUMP_FORWARD", end_of_function), Instr("END_FINALLY"), end_of_function ] return inner
def test_compute_jumps_convergence(self): # Consider the following sequence of instructions: # # JUMP_ABSOLUTE Label1 # JUMP_ABSOLUTE Label2 # ...126 instructions... # Label1: Offset 254 on first pass, 256 second pass # NOP # ... many more instructions ... # Label2: Offset > 256 on first pass # # On first pass of compute_jumps(), Label2 will be at address 254, so # that value encodes into the single byte arg of JUMP_ABSOLUTE. # # On second pass compute_jumps() the instr at Label1 will have offset # of 256 so will also be given an EXTENDED_ARG. # # Thus we need to make an additional pass. This test only verifies # case where 2 passes is insufficient but three is enough. if not WORDCODE: # Could be done pre-WORDCODE, but that requires 2**16 bytes of # code. return # Create code from comment above. code = Bytecode() label1 = Label() label2 = Label() nop = 'UNARY_POSITIVE' # don't use NOP, dis.stack_effect will raise code.append(Instr('JUMP_ABSOLUTE', label1)) code.append(Instr('JUMP_ABSOLUTE', label2)) for x in range(4, 254, 2): code.append(Instr(nop)) code.append(label1) code.append(Instr(nop)) for x in range(256, 300, 2): code.append(Instr(nop)) code.append(label2) code.append(Instr(nop)) # This should pass by default. code.to_code() # Try with max of two passes: it should raise with self.assertRaises(RuntimeError): code.to_code(compute_jumps_passes=2)
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 conditional_jump_example_bytecode() -> Bytecode: label_else = Label() label_print = Label() byte_code = Bytecode([ Instr("LOAD_NAME", "print"), Instr("LOAD_NAME", "test"), Instr("POP_JUMP_IF_FALSE", label_else), Instr("LOAD_CONST", "yes"), Instr("JUMP_FORWARD", label_print), label_else, Instr("LOAD_CONST", "no"), label_print, Instr("CALL_FUNCTION", 1), Instr("LOAD_CONST", None), Instr("RETURN_VALUE"), ]) return byte_code