def test_slice(self): code = Bytecode() code.first_lineno = 3 code.extend([ Instr("LOAD_CONST", 7), Instr("STORE_NAME", "x"), SetLineno(4), Instr("LOAD_CONST", 8), Instr("STORE_NAME", "y"), SetLineno(5), Instr("LOAD_CONST", 9), Instr("STORE_NAME", "z"), ]) sliced_code = code[:] self.assertEqual(code, sliced_code) for name in ( "argcount", "posonlyargcount", "kwonlyargcount", "first_lineno", "name", "filename", "docstring", "cellvars", "freevars", "argnames", ): self.assertEqual(getattr(code, name, None), getattr(sliced_code, name, None))
def test_setlineno(self): # x = 7 # y = 8 # z = 9 code = Bytecode() code.first_lineno = 3 code.extend([ Instr("LOAD_CONST", 7), Instr("STORE_NAME", "x"), SetLineno(4), Instr("LOAD_CONST", 8), Instr("STORE_NAME", "y"), SetLineno(5), Instr("LOAD_CONST", 9), Instr("STORE_NAME", "z"), ]) concrete = code.to_concrete_bytecode() self.assertEqual(concrete.consts, [7, 8, 9]) self.assertEqual(concrete.names, ["x", "y", "z"]) self.assertListEqual( list(concrete), [ ConcreteInstr("LOAD_CONST", 0, lineno=3), ConcreteInstr("STORE_NAME", 0, lineno=3), ConcreteInstr("LOAD_CONST", 1, lineno=4), ConcreteInstr("STORE_NAME", 1, lineno=4), ConcreteInstr("LOAD_CONST", 2, lineno=5), ConcreteInstr("STORE_NAME", 2, lineno=5), ], )
def test_build_tuple(self): # x = (1, 2, 3) code = Bytecode([ Instr("LOAD_CONST", 1), Instr("LOAD_CONST", 2), Instr("LOAD_CONST", 3), Instr("BUILD_TUPLE", 3), Instr("STORE_NAME", "x"), ]) self.check(code, Instr("LOAD_CONST", (1, 2, 3)), Instr("STORE_NAME", "x"))
def build_tuple_unpack_seq(self, instr): next_instr = self.get_next_instr("UNPACK_SEQUENCE") if next_instr is None or next_instr.arg != instr.arg: return if instr.arg < 1: return if self.const_stack and instr.arg <= len(self.const_stack): nconst = instr.arg start = self.index - 1 # Rewrite LOAD_CONST instructions in the reverse order load_consts = self.block[start - nconst:start] self.block[start - nconst:start] = reversed(load_consts) # Remove BUILD_TUPLE+UNPACK_SEQUENCE self.block[start:start + 2] = () self.index -= 2 self.const_stack.clear() return if instr.arg == 1: # Replace BUILD_TUPLE 1 + UNPACK_SEQUENCE 1 with NOP del self.block[self.index - 1:self.index + 1] elif instr.arg == 2: # Replace BUILD_TUPLE 2 + UNPACK_SEQUENCE 2 with ROT_TWO rot2 = Instr("ROT_TWO", lineno=instr.lineno) self.block[self.index - 1:self.index + 1] = (rot2, ) self.index -= 1 self.const_stack.clear() elif instr.arg == 3: # Replace BUILD_TUPLE 3 + UNPACK_SEQUENCE 3 # with ROT_THREE + ROT_TWO rot3 = Instr("ROT_THREE", lineno=instr.lineno) rot2 = Instr("ROT_TWO", lineno=instr.lineno) self.block[self.index - 1:self.index + 1] = (rot3, rot2) self.index -= 1 self.const_stack.clear()
def test_blocks_broken_jump(self): block = BasicBlock() code = ControlFlowGraph() code[0].append(Instr("JUMP_ABSOLUTE", block)) expected = textwrap.dedent( """ block1: JUMP_ABSOLUTE <error: unknown block> """ ).lstrip("\n") self.check_dump_bytecode(code, expected)
def test_split_block_end(self): code = self.sample_code() # split at the end of the last block requires to add a new empty block label = code.split_block(code[0], 2) self.assertIs(label, code[1]) self.assertBlocksEqual( code, [Instr("LOAD_CONST", 1, lineno=1), Instr("STORE_NAME", "x", lineno=1)], [], ) self.check_getitem(code) # split at the end of a block which is not the end doesn't require to # add a new block label = code.split_block(code[0], 2) self.assertIs(label, code[1]) self.assertBlocksEqual( code, [Instr("LOAD_CONST", 1, lineno=1), Instr("STORE_NAME", "x", lineno=1)], [], )
def test_load_classderef(self): concrete = ConcreteBytecode() concrete.cellvars = ["__class__"] concrete.freevars = ["__class__"] concrete.extend([ ConcreteInstr("LOAD_CLASSDEREF", 1), ConcreteInstr("STORE_DEREF", 1) ]) bytecode = concrete.to_bytecode() self.assertEqual(bytecode.freevars, ["__class__"]) self.assertEqual(bytecode.cellvars, ["__class__"]) self.assertEqual( list(bytecode), [ Instr("LOAD_CLASSDEREF", FreeVar("__class__"), lineno=1), Instr("STORE_DEREF", FreeVar("__class__"), lineno=1), ], ) concrete = bytecode.to_concrete_bytecode() self.assertEqual(concrete.freevars, ["__class__"]) self.assertEqual(concrete.cellvars, ["__class__"]) self.assertEqual( list(concrete), [ ConcreteInstr("LOAD_CLASSDEREF", 1, lineno=1), ConcreteInstr("STORE_DEREF", 1, lineno=1), ], ) code = concrete.to_code() self.assertEqual(code.co_freevars, ("__class__", )) self.assertEqual(code.co_cellvars, ("__class__", )) self.assertEqual( code.co_code, b"\x94\x01\x89\x01", )
def test_negative_size_unary_with_disable_check_of_pre_and_post(self): opnames = ( "UNARY_POSITIVE", "UNARY_NEGATIVE", "UNARY_NOT", "UNARY_INVERT", ) for opname in opnames: with self.subTest(): code = Bytecode() code.first_lineno = 1 code.extend([Instr(opname)]) co = code.to_code(check_pre_and_post=False) self.assertEqual(co.co_stacksize, 0)
def test_negative_size_unary(self): opnames = ( "UNARY_POSITIVE", "UNARY_NEGATIVE", "UNARY_NOT", "UNARY_INVERT", ) for opname in opnames: with self.subTest(): code = Bytecode() code.first_lineno = 1 code.extend([Instr(opname)]) with self.assertRaises(RuntimeError): code.compute_stacksize()
def test_freevar(self): concrete = ConcreteBytecode() concrete.freevars = ["x"] concrete.append(ConcreteInstr("LOAD_DEREF", 0)) code = concrete.to_code() concrete = ConcreteBytecode.from_code(code) self.assertEqual(concrete.cellvars, []) self.assertEqual(concrete.freevars, ["x"]) self.assertEqual(list(concrete), [ConcreteInstr("LOAD_DEREF", 0, lineno=1)]) bytecode = concrete.to_bytecode() self.assertEqual(bytecode.cellvars, []) self.assertEqual(list(bytecode), [Instr("LOAD_DEREF", FreeVar("x"), lineno=1)])
def test_label_at_the_end(self): 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, ]) cfg = ControlFlowGraph.from_bytecode(code) self.assertBlocksEqual( cfg, [ Instr("LOAD_NAME", "x"), Instr("UNARY_NOT"), Instr("POP_JUMP_IF_FALSE", cfg[2]), ], [Instr("LOAD_CONST", 9), Instr("STORE_NAME", "y")], [], )
def test_jumps(self): # if test: # x = 12 # else: # x = 37 code = Bytecode() label_else = Label() label_return = Label() code.extend([ Instr("LOAD_NAME", "test", lineno=1), Instr("POP_JUMP_IF_FALSE", label_else), Instr("LOAD_CONST", 12, lineno=2), Instr("STORE_NAME", "x"), Instr("JUMP_FORWARD", label_return), label_else, Instr("LOAD_CONST", 37, lineno=4), Instr("STORE_NAME", "x"), label_return, Instr("LOAD_CONST", None, lineno=4), Instr("RETURN_VALUE"), ]) code = code.to_concrete_bytecode() expected = [ ConcreteInstr("LOAD_NAME", 0, lineno=1), ConcreteInstr("POP_JUMP_IF_FALSE", 5 if OFFSET_AS_INSTRUCTION else 10, lineno=1), ConcreteInstr("LOAD_CONST", 0, lineno=2), ConcreteInstr("STORE_NAME", 1, lineno=2), ConcreteInstr("JUMP_FORWARD", 2 if OFFSET_AS_INSTRUCTION else 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(code), expected) self.assertListEqual(code.consts, [12, 37, None]) self.assertListEqual(code.names, ["test", "x"]) self.assertListEqual(code.varnames, [])
def check_bin_op(left, op, right, result): code = Bytecode([ Instr("LOAD_CONST", left), Instr("LOAD_CONST", right), Instr(op), Instr("STORE_NAME", "x"), ]) self.check(code, Instr("LOAD_CONST", result), Instr("STORE_NAME", "x"))
def test_dead_code_jump(self): label = Label() code = Bytecode([ Instr("LOAD_NAME", "x"), Instr("JUMP_ABSOLUTE", label), # dead code Instr("LOAD_NAME", "y"), Instr("STORE_NAME", "test"), label, Instr("STORE_NAME", "test"), ]) self.check(code, Instr("LOAD_NAME", "x"), Instr("STORE_NAME", "test"))
def test_negative_size_binary(self): opnames = ( "BINARY_POWER", "BINARY_MULTIPLY", "BINARY_MATRIX_MULTIPLY", "BINARY_FLOOR_DIVIDE", "BINARY_TRUE_DIVIDE", "BINARY_MODULO", "BINARY_ADD", "BINARY_SUBTRACT", "BINARY_SUBSCR", "BINARY_LSHIFT", "BINARY_RSHIFT", "BINARY_AND", "BINARY_XOR", "BINARY_OR", ) for opname in opnames: with self.subTest(): code = Bytecode() code.first_lineno = 1 code.extend([Instr("LOAD_CONST", 1), Instr(opname)]) with self.assertRaises(RuntimeError): code.compute_stacksize()
def test_negative_size_binary_with_disable_check_of_pre_and_post(self): opnames = ( "BINARY_POWER", "BINARY_MULTIPLY", "BINARY_MATRIX_MULTIPLY", "BINARY_FLOOR_DIVIDE", "BINARY_TRUE_DIVIDE", "BINARY_MODULO", "BINARY_ADD", "BINARY_SUBTRACT", "BINARY_SUBSCR", "BINARY_LSHIFT", "BINARY_RSHIFT", "BINARY_AND", "BINARY_XOR", "BINARY_OR", ) for opname in opnames: with self.subTest(): code = Bytecode() code.first_lineno = 1 code.extend([Instr("LOAD_CONST", 1), Instr(opname)]) co = code.to_code(check_pre_and_post=False) self.assertEqual(co.co_stacksize, 1)
def replace_load_const(self, nconst, instr, result): # FIXME: remove temporary computed constants? # FIXME: or at least reuse existing constants? self.in_consts = True load_const = Instr("LOAD_CONST", result, lineno=instr.lineno) start = self.index - nconst - 1 self.block[start:self.index] = (load_const, ) self.index -= nconst if nconst: del self.const_stack[-nconst:] self.const_stack.append(result) self.in_consts = True
def test_negative_size_build(self): opnames = ( "BUILD_TUPLE", "BUILD_LIST", "BUILD_SET", ) if sys.version_info >= (3, 6): opnames = (*opnames, "BUILD_STRING") for opname in opnames: with self.subTest(): code = Bytecode() code.first_lineno = 1 code.extend([Instr(opname, 1)]) with self.assertRaises(RuntimeError): code.compute_stacksize()
def test_compare(self): instr = Instr("LOAD_CONST", 3, lineno=7) self.assertEqual(instr, Instr("LOAD_CONST", 3, lineno=7)) self.assertNotEqual(instr, 1) # different lineno self.assertNotEqual(instr, Instr("LOAD_CONST", 3)) self.assertNotEqual(instr, Instr("LOAD_CONST", 3, lineno=6)) # different op self.assertNotEqual(instr, Instr("LOAD_FAST", "x", lineno=7)) # different arg self.assertNotEqual(instr, Instr("LOAD_CONST", 4, lineno=7))
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", 7 if OFFSET_AS_INSTRUCTION else 14, lineno=1), ConcreteInstr("LOAD_CONST", 0, lineno=2), ConcreteInstr("STORE_NAME", 1, lineno=2), ConcreteInstr("JUMP_FORWARD", 2 if OFFSET_AS_INSTRUCTION else 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_handling_of_set_lineno(self): code = Bytecode() code.first_lineno = 3 code.extend([ Instr("LOAD_CONST", 7), Instr("STORE_NAME", "x"), SetLineno(4), Instr("LOAD_CONST", 8), Instr("STORE_NAME", "y"), SetLineno(5), Instr("LOAD_CONST", 9), Instr("STORE_NAME", "z"), ]) self.assertEqual(code.compute_stacksize(), 1)
def test_stack_effects(self): # Verify all opcodes are handled and that "jump=None" really returns # the max of the other cases. from _pydevd_frame_eval.vendored.bytecode.concrete import ConcreteInstr def check(instr): jump = instr.stack_effect(jump=True) no_jump = instr.stack_effect(jump=False) max_effect = instr.stack_effect(jump=None) self.assertEqual(instr.stack_effect(), max_effect) self.assertEqual(max_effect, max(jump, no_jump)) if not instr.has_jump(): self.assertEqual(jump, no_jump) for name, op in opcode.opmap.items(): with self.subTest(name): # Use ConcreteInstr instead of Instr because it doesn't care # what kind of argument it is constructed with. if op < opcode.HAVE_ARGUMENT: check(ConcreteInstr(name)) else: for arg in range(256): check(ConcreteInstr(name, arg)) # LOAD_CONST uses a concrete python object as its oparg, however, in # dis.stack_effect(opcode.opmap['LOAD_CONST'], oparg), # oparg should be the index of that python object in the constants. # # Fortunately, for an instruction whose oparg isn't equivalent to its # form in binary files(pyc format), the stack effect is a # constant which does not depend on its oparg. # # The second argument of dis.stack_effect cannot be # more than 2**31 - 1. If stack effect of an instruction is # independent of its oparg, we pass 0 as the second argument # of dis.stack_effect. # (As a result we can calculate stack_effect for # any LOAD_CONST instructions, even for large integers) for arg in 2 ** 31, 2 ** 32, 2 ** 63, 2 ** 64, -1: self.assertEqual(Instr("LOAD_CONST", arg).stack_effect(), 1)
def mk_if_then_else(depth): instructions = [] for i in range(depth): label_else = Label() instructions.extend([ Instr("LOAD_FAST", "x"), Instr("POP_JUMP_IF_FALSE", label_else), Instr("LOAD_GLOBAL", "f{}".format(i)), Instr("RETURN_VALUE"), label_else, ]) instructions.extend( [Instr("LOAD_CONST", None), Instr("RETURN_VALUE")]) return instructions
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 = "NOP" 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_to_code(self): code = Bytecode() code.first_lineno = 50 code.extend([ Instr("LOAD_NAME", "print"), Instr("LOAD_CONST", "%s"), Instr("LOAD_GLOBAL", "a"), Instr("BINARY_MODULO"), Instr("CALL_FUNCTION", 1), Instr("RETURN_VALUE"), ]) co = code.to_code() # hopefully this is obvious from inspection? :-) self.assertEqual(co.co_stacksize, 3) co = code.to_code(stacksize=42) self.assertEqual(co.co_stacksize, 42)
def test_from_code_load_fast(self): code = get_code( """ def func(): x = 33 y = x """, function=True, ) code = Bytecode.from_code(code) self.assertEqual( code, [ Instr("LOAD_CONST", 33, lineno=2), Instr("STORE_FAST", "x", lineno=2), Instr("LOAD_FAST", "x", lineno=3), Instr("STORE_FAST", "y", lineno=3), Instr("LOAD_CONST", None, lineno=3), Instr("RETURN_VALUE", lineno=3), ], )
def test_attr(self): instr = Instr("LOAD_CONST", 3, lineno=5) self.assertEqual(instr.name, "LOAD_CONST") self.assertEqual(instr.opcode, 100) self.assertEqual(instr.arg, 3) self.assertEqual(instr.lineno, 5) # invalid values/types self.assertRaises(ValueError, setattr, instr, "lineno", 0) self.assertRaises(TypeError, setattr, instr, "lineno", 1.0) self.assertRaises(TypeError, setattr, instr, "name", 5) self.assertRaises(TypeError, setattr, instr, "opcode", 1.0) self.assertRaises(ValueError, setattr, instr, "opcode", -1) self.assertRaises(ValueError, setattr, instr, "opcode", 255) # arg can take any attribute but cannot be deleted instr.arg = -8 instr.arg = object() self.assertRaises(AttributeError, delattr, instr, "arg") # no argument instr = Instr("ROT_TWO") self.assertIs(instr.arg, UNSET)
def test_invalid_arg(self): label = Label() block = BasicBlock() # EXTENDED_ARG self.assertRaises(ValueError, Instr, "EXTENDED_ARG", 0) # has_jump() self.assertRaises(TypeError, Instr, "JUMP_ABSOLUTE", 1) self.assertRaises(TypeError, Instr, "JUMP_ABSOLUTE", 1.0) Instr("JUMP_ABSOLUTE", label) Instr("JUMP_ABSOLUTE", block) # hasfree self.assertRaises(TypeError, Instr, "LOAD_DEREF", "x") Instr("LOAD_DEREF", CellVar("x")) Instr("LOAD_DEREF", FreeVar("x")) # haslocal self.assertRaises(TypeError, Instr, "LOAD_FAST", 1) Instr("LOAD_FAST", "x") # hasname self.assertRaises(TypeError, Instr, "LOAD_NAME", 1) Instr("LOAD_NAME", "x") # hasconst self.assertRaises(ValueError, Instr, "LOAD_CONST") # UNSET self.assertRaises(ValueError, Instr, "LOAD_CONST", label) self.assertRaises(ValueError, Instr, "LOAD_CONST", block) Instr("LOAD_CONST", 1.0) Instr("LOAD_CONST", object()) # hascompare self.assertRaises(TypeError, Instr, "COMPARE_OP", 1) Instr("COMPARE_OP", Compare.EQ) # HAVE_ARGUMENT self.assertRaises(ValueError, Instr, "CALL_FUNCTION", -1) self.assertRaises(TypeError, Instr, "CALL_FUNCTION", 3.0) Instr("CALL_FUNCTION", 3) # test maximum argument self.assertRaises(ValueError, Instr, "CALL_FUNCTION", 2147483647 + 1) instr = Instr("CALL_FUNCTION", 2147483647) self.assertEqual(instr.arg, 2147483647) # not HAVE_ARGUMENT self.assertRaises(ValueError, Instr, "NOP", 0) Instr("NOP")
def test_const_key_equal(self): neg_zero = -0.0 pos_zero = +0.0 # int and float: 0 == 0.0 self.assertNotEqual(Instr("LOAD_CONST", 0), Instr("LOAD_CONST", 0.0)) # float: -0.0 == +0.0 self.assertNotEqual( Instr("LOAD_CONST", neg_zero), Instr("LOAD_CONST", pos_zero) ) # complex self.assertNotEqual( Instr("LOAD_CONST", complex(neg_zero, 1.0)), Instr("LOAD_CONST", complex(pos_zero, 1.0)), ) self.assertNotEqual( Instr("LOAD_CONST", complex(1.0, neg_zero)), Instr("LOAD_CONST", complex(1.0, pos_zero)), ) # tuple self.assertNotEqual(Instr("LOAD_CONST", (0,)), Instr("LOAD_CONST", (0.0,))) nested_tuple1 = (0,) nested_tuple1 = (nested_tuple1,) nested_tuple2 = (0.0,) nested_tuple2 = (nested_tuple2,) self.assertNotEqual( Instr("LOAD_CONST", nested_tuple1), Instr("LOAD_CONST", nested_tuple2) ) # frozenset self.assertNotEqual( Instr("LOAD_CONST", frozenset({0})), Instr("LOAD_CONST", frozenset({0.0})) )