def ascode(self, func_name: str, argcount: int) -> CodeType: code = bytes(self) stacksize = max( accumulate( stack_effect(op, arg ) if op >= HAVE_ARGUMENT else stack_effect(op) for op, arg in zip(code[::2], code[1::2]))) return CodeType( argcount, 0, # posonlyargcount 0, # kwonlyargcount len(self.varnames), stacksize, # stacksize (CompilerFlags.OPTIMIZED | CompilerFlags.NEWLOCALS), code, tuple(self.consts), # insertion order tuple(self.names), tuple(self.varnames), "", func_name, 0, bytes(), tuple(self.freevars), tuple(self.cellvars), )
def resolve_stacksize(insts): max_stacksize = 0 pending = [(0, 0)] while pending: (i, stacksize), *pending = pending inst = insts[i] if isinstance(inst, Label): if inst.stacksize is None: inst.stacksize = stacksize else: assert stacksize == inst.stacksize continue i += 1 else: assert i == 0 while True: inst = insts[i] if isinstance(inst, Label): if inst.stacksize is None: inst.stacksize = stacksize assert inst.stacksize == stacksize elif isinstance(inst, Instruction): if inst.opcode == opmap["RETURN_VALUE"]: assert stacksize == 1 break if inst.opcode == opmap["JUMP_ABSOLUTE"]: pending.append((insts.index(inst.arg), stacksize)) break if inst.opcode in hasconst: stacksize += stack_effect(inst.opcode, inst.slot) elif inst.opcode < HAVE_ARGUMENT: stacksize += stack_effect(inst.opcode) elif inst.opcode not in hasjrel and inst.opcode not in hasjabs: stacksize += stack_effect(inst.opcode, inst.arg) else: notjump, jump = _stack_effect[opname[inst.opcode]] jump += stacksize pending.append((insts.index(inst.arg), jump)) max_stacksize = max(jump, max_stacksize) stacksize += notjump i += 1 max_stacksize = max(stacksize, max_stacksize) return max_stacksize
def get_stack_effect(self) -> int: # dis.stack_effect does not work for EXTENDED_ARG and NOP if self.mnemonic in ["EXTENDED_ARG", "NOP"]: return 0 return dis.stack_effect(self.opcode, (self.arg if self.has_argument() else None))
def stack_effect(self): """ The net effect of executing this instruction on the interpreter stack. Instructions that pop values off the stack have negative stack effect equal to the number of popped values. Instructions that push values onto the stack have positive stack effect equal to the number of popped values. Examples -------- - LOAD_{FAST,NAME,GLOBAL,DEREF} push one value onto the stack. They have a stack_effect of 1. - POP_JUMP_IF_{TRUE,FALSE} always pop one value off the stack. They have a stack effect of -1. - BINARY_* instructions pop two instructions off the stack, apply a binary operator, and push the resulting value onto the stack. They have a stack effect of -1 (-2 values consumed + 1 value pushed). """ if self.opcode == NOP.opcode: # noqa # dis.stack_effect is broken here return 0 return stack_effect( self.opcode, *((self.arg if isinstance(self.arg, int) else 0, ) if self.have_arg else ()))
def stack_effect(opcode, oparg=None): "Return the stack effect as seen by the following instruction." if opcode in or_pop_instructions: # N.B. these are 0 in the dis version; -1 if jump not taken return -1 else: return dis.stack_effect(opcode, oparg if isinstance(oparg, (int, type(None))) else 0)
def stack_effect(self): """ The net effect of executing this instruction on the interpreter stack. Instructions that pop values off the stack have negative stack effect equal to the number of popped values. Instructions that push values onto the stack have positive stack effect equal to the number of popped values. Examples -------- - LOAD_{FAST,NAME,GLOBAL,DEREF} push one value onto the stack. They have a stack_effect of 1. - POP_JUMP_IF_{TRUE,FALSE} always pop one value off the stack. They have a stack effect of -1. - BINARY_* instructions pop two instructions off the stack, apply a binary operator, and push the resulting value onto the stack. They have a stack effect of -1 (-2 values consumed + 1 value pushed). """ return stack_effect( self.opcode, *((self.arg if isinstance(self.arg, int) else 0,) if self.have_arg else ()) )
def stack_effect(self, jump=None): if self._opcode < _opcode.HAVE_ARGUMENT: arg = None elif not isinstance(self._arg, int) or self._opcode in _opcode.hasconst: # Argument is either a non-integer or an integer constant, # not oparg. arg = 0 else: arg = self._arg if sys.version_info < (3, 8): effect = _stack_effects.get(self._opcode, None) if effect is not None: return max(effect) if jump is None else effect[jump] return dis.stack_effect(self._opcode, arg) else: return dis.stack_effect(self._opcode, arg, jump=jump)
def stack_effect(self): '''not exact. see https://github.com/python/cpython/blob/master/Python/compile.c#L860''' if self.opname in ('SETUP_EXCEPT', 'SETUP_FINALLY', 'POP_EXCEPT', 'END_FINALLY'): assert False, 'for all we know. we assume no exceptions' if self.is_raise: # if we wish to analyze exception path, we should break to except: and push 3, or somthing. return -1 if self.opname == 'BREAK_LOOP' and self.argrepr.startswith('FOR'): return -1 return dis.stack_effect(self.opcode, self.arg)
def check_stack_effect(): import dis from xdis import IS_PYPY from xdis.op_imports import get_opcode_module if IS_PYPY: variant = "pypy" else: variant = "" opc = get_opcode_module(None, variant) for ( opname, opcode, ) in opc.opmap.items(): if opname in ("EXTENDED_ARG", "NOP"): continue xdis_args = [opcode, opc] dis_args = [opcode] if op_has_argument(opcode, opc): xdis_args.append(0) dis_args.append(0) if ( PYTHON_VERSION_TRIPLE > (3, 7) and opcode in opc.CONDITION_OPS and opname not in ( "JUMP_IF_FALSE_OR_POP", "JUMP_IF_TRUE_OR_POP", "POP_JUMP_IF_FALSE", "POP_JUMP_IF_TRUE", "SETUP_FINALLY", ) ): xdis_args.append(0) dis_args.append(0) effect = xstack_effect(*xdis_args) check_effect = dis.stack_effect(*dis_args) if effect == -100: print( "%d (%s) needs adjusting; should be: should have effect %d" % (opcode, opname, check_effect) ) elif check_effect == effect: pass # print("%d (%s) is good: effect %d" % (opcode, opname, effect)) else: print( "%d (%s) not okay; effect %d vs %d" % (opcode, opname, effect, check_effect) ) pass pass return
def _compute_stacksize(self): code = self.code label_pos = {op[0]:pos for pos,op in enumerate(code) if isinstance(op[0],Label)} # sf_targets are the targets of SETUP_FINALLY opcodes. They are recorded # because they have special stack behaviour. If an exception was raised # in the block pushed by a SETUP_FINALLY opcode, the block is popped # and 3 objects are pushed. On return or continue, the block is popped # and 2 objects are pushed. If nothing happened, the block is popped by # a POP_BLOCK opcode and 1 object is pushed by a (LOAD_CONST, None) # operation # Our solution is to record the stack state of SETUP_FINALLY targets # as having 3 objects pushed, which is the maximum. However, to make # stack recording consistent, the get_next_stacks function will always # yield the stack state of the target as if 1 object was pushed, but # this will be corrected in the actual stack recording sf_targets={label_pos[arg] for op,arg in code if op==SETUP_FINALLY} stacks=[None]*len(code) maxsize=0 op=[(0,(0,))] def newstack(n): if curstack[-1]<-n:raise ValueError("Popped a non-existing element at %s %s"%(pos,code[pos-3:pos+2])) return curstack[:-1]+(curstack[-1]+n,) while op: pos,curstack=op.pop() o=sum(curstack) if o>maxsize:maxsize=o o,arg=code[pos] if isinstance(o,Label): if pos in sf_targets:curstack=curstack[:-1]+(curstack[-1]+2,) if stacks[pos] is None: stacks[pos]=curstack if o not in (BREAK_LOOP,RETURN_VALUE,RAISE_VARARGS,STOP_CODE): pos+=1 if not isopcode(o):op+=(pos,curstack), elif o not in hasflow:op+=(pos,newstack(stack_effect(o,arg))), elif o == FOR_ITER:op+=(label_pos[arg],newstack(-1)),(pos,newstack(1)) elif o in (JUMP_FORWARD,JUMP_ABSOLUTE):op+=(label_pos[arg],curstack), elif o in (POP_JUMP_IF_FALSE,POP_JUMP_IF_TRUE): curstack=newstack(-1) op+=(label_pos[arg],curstack),(pos,curstack) elif o in (JUMP_IF_FALSE_OR_POP,JUMP_IF_TRUE_OR_POP):op+=(label_pos[arg],curstack),(pos,newstack(-1)) elif o == CONTINUE_LOOP:op+=(label_pos[arg],curstack[:-1]), elif o == SETUP_LOOP:op+=(pos,curstack+(0,)),(label_pos[arg],curstack) elif o == SETUP_EXCEPT:op+=(pos,curstack+(0,)),(label_pos[arg],newstack(3)) elif o == SETUP_FINALLY:op+=(pos,curstack+(0,)),(label_pos[arg],newstack(1)) elif o == POP_BLOCK:op+=(pos,curstack[:-1]), elif o == END_FINALLY:op+=(pos,newstack(-3)), elif o == WITH_CLEANUP:op+=(pos,newstack(-1)), else:raise ValueError("Unhandled opcode %s"%op) elif stacks[pos]!=curstack: op=pos+1 while code[op][0] not in hasflow:op+=1 if code[op][0] not in (RETURN_VALUE,RAISE_VARARGS,STOP_CODE):raise ValueError("Inconsistent code at %s %s %s\n%s"%(pos,curstack,stacks[pos],code[pos-5:pos+4])) return maxsize
def expr_that_added_elem_to_stack(instructions: List[Instruction], start_index: int, stack_pos: int): """Backwards traverse instructions Backwards traverse the instructions starting at `start_index` until we find the instruction that added the element at stack position `stack_pos` (where 0 means top of stack). For example, if the instructions are: ``` 0: LOAD_GLOBAL 0 (func) 1: LOAD_CONST 1 (42) 2: CALL_FUNCTION 1 ``` We can look for the function that is called by invoking this function with `start_index = 1` and `stack_pos = 1`. It will see that `LOAD_CONST` added the top element to the stack, and find that `LOAD_GLOBAL` was the instruction to add element in stack position 1 to the stack -- so `expr_from_instruction(instructions, 0)` is returned. It is assumed that if `stack_pos == 0` then the instruction you are looking for is the one at `instructions[start_index]`. This might not hold, in case of using `NOP` instructions. If any jump instruction is found, `SomethingInvolvingScaryBytecodeJump` is returned immediately. (since correctly process the bytecode when faced with jumps is not as straight forward). """ if DEBUG: LOGGER.debug( f"find_inst_that_added_elem_to_stack start_index={start_index} stack_pos={stack_pos}" ) assert stack_pos >= 0 for inst in reversed(instructions[:start_index + 1]): # Return immediately if faced with a jump if inst.opcode in dis.hasjabs or inst.opcode in dis.hasjrel: return SomethingInvolvingScaryBytecodeJump(inst.opname) if stack_pos == 0: if DEBUG: LOGGER.debug(f"Found it: {inst}") found_index = instructions.index(inst) break old = stack_pos stack_pos -= dis.stack_effect(inst.opcode, inst.arg) new = stack_pos if DEBUG: LOGGER.debug(f"Skipping ({old} -> {new}) {inst}") else: raise Exception("inst_index_for_stack_diff failed") return expr_from_instruction(instructions, found_index)
def stack_effect(self, jump=None): effect = _stack_effects.get(self._opcode, None) if effect is not None: return max(effect) if jump is None else effect[jump] # TODO: if dis.stack_effect ever expands to take the 'jump' parameter # then we should pass that through, and perhaps remove some of the # overrides that are set up in _init_stack_effects() # All opcodes whose arguments are not represented by integers have # a stack_effect indepent of their argument. arg = (self._arg if isinstance(self._arg, int) else 0 if self._opcode >= _opcode.HAVE_ARGUMENT else None) return dis.stack_effect(self._opcode, arg)
def test_one(xdis_args, dis_args, has_arg): effect = xstack_effect(*xdis_args) check_effect = dis.stack_effect(*dis_args) assert effect != -100, ( "%d (%s) needs adjusting; should be: should have effect %d" % (opcode, opname, check_effect)) if has_arg: op_val = "with operand %d" % dis_args[1] else: op_val = "" assert check_effect == effect, ( "%d (%s) %s not okay; effect %d vs %d" % (opcode, opname, op_val, effect, check_effect)) print("%d (%s) is good: effect %d" % (opcode, opname, effect))
def compute_stack_effect(decomp, jmp_tbl, end, start): SE = 0 idx = 0 segment = decomp[start + 1: end] while idx < len(segment): (op, arg) = lst = segment[idx] isjump = 'JUMP' in op SE += dis.stack_effect( dis.opmap[op], arg if dis.opmap[op] > dis.HAVE_ARGUMENT else None, jump = isjump ) if isjump: idx = find(segment, jmp_tbl[id(lst)]) else: idx += 1 return SE
def stack_effect(self, jump=None): effect = _stack_effects.get(self.opcode, None) if effect is not None: return max(effect) if jump is None else effect[jump] # TODO: if dis.stack_effect ever expands to take the 'jump' parameter # then we should pass that through, and perhaps remove some of the # overrides that are set up in _init_stack_effects() # Each of following opcodes has a stack_effect indepent of its # argument: # 1. Whose argument is not represented by an integer. # 2. Whose stack effect can be calculated without using oparg # from this link: # https://github.com/python/cpython/blob/master/Python/compile.c#L859 use_oparg = self.opcode in _stack_effects_use_opargs arg = (self._arg if use_oparg and isinstance(self._arg, int) else 0 if self.opcode >= opcode.HAVE_ARGUMENT else None) return dis.stack_effect(self.opcode, arg)
def stack_effect(self, arg=None): return dis.stack_effect(self.value, arg)
def _compute_stacksize(self, logging=False): code = self.code label_pos = {op[0]: pos for pos, op in enumerate(code) if isinstance(op[0], Label)} # sf_targets are the targets of SETUP_FINALLY opcodes. They are recorded # because they have special stack behaviour. If an exception was raised # in the block pushed by a SETUP_FINALLY opcode, the block is popped # and 3 objects are pushed. On return or continue, the block is popped # and 2 objects are pushed. If nothing happened, the block is popped by # a POP_BLOCK opcode and 1 object is pushed by a (LOAD_CONST, None) # operation # Our solution is to record the stack state of SETUP_FINALLY targets # as having 3 objects pushed, which is the maximum. However, to make # stack recording consistent, the get_next_stacks function will always # yield the stack state of the target as if 1 object was pushed, but # this will be corrected in the actual stack recording if version_info < (3, 5): sf_targets = {label_pos[arg] for op, arg in code if (op == SETUP_FINALLY or op == SETUP_WITH)} else: sf_targets = {label_pos[arg] for op, arg in code if (op == SETUP_FINALLY or op == SETUP_WITH or op == SETUP_ASYNC_WITH)} states = [None] * len(code) maxsize = 0 class BlockType(Enum): DEFAULT = 0, TRY_FINALLY = 1, TRY_EXCEPT = 2, LOOP_BODY = 3, WITH_BLOCK = 4, EXCEPTION = 5, SILENCED_EXCEPTION_BLOCK = 6, class State: def __init__(self, pos=0, stack=(0,), block_stack=(BlockType.DEFAULT,), log=[]): self._pos = pos self._stack = stack self._block_stack = block_stack self._log = log @property def pos(self): return self._pos @property def stack(self): return self._stack @stack.setter def stack(self, val): self._stack = val def newstack(self, n): if self._stack[-1] < -n: raise ValueError("Popped a non-existing element at %s %s" % (self._pos, code[self._pos - 4: self._pos + 3])) return self._stack[:-1] + (self._stack[-1] + n,) @property def block_stack(self): return self._block_stack @property def log(self): return self._log def newlog(self, msg): if not logging: return None log_msg = str(self._pos) + ": " + msg if self._stack: log_msg += " (on stack: " log_depth = 2 log_depth = min(log_depth, len(self._stack)) for pos in range(-1, -log_depth, -1): log_msg += str(self._stack[pos]) + ", " log_msg += str(self._stack[-log_depth]) log_msg += ")" else: log_msg += " (empty stack)" return [log_msg] + self._log op = [State()] while op: cur_state = op.pop() o = sum(cur_state.stack) if o > maxsize: maxsize = o o, arg = code[cur_state.pos] if isinstance(o, Label): if cur_state.pos in sf_targets: cur_state.stack = cur_state.newstack(5) if states[cur_state.pos] is None: states[cur_state.pos] = cur_state elif states[cur_state.pos].stack != cur_state.stack: check_pos = cur_state.pos + 1 while code[check_pos][0] not in hasflow: check_pos += 1 if code[check_pos][0] not in (RETURN_VALUE, RAISE_VARARGS, STOP_CODE): if cur_state.pos not in sf_targets: raise ValueError("Inconsistent code at %s %s %s\n%s" % (cur_state.pos, cur_state.stack, states[cur_state.pos].stack, code[cur_state.pos - 5:cur_state.pos + 4])) else: # SETUP_FINALLY target inconsistent code! # # Since Python 3.2 assigned exception is cleared at the end of # the except clause (named exception handler). # To perform this CPython (checked in version 3.4.3) adds special # bytecode in exception handler which currently breaks 'regularity' of bytecode. # Exception handler is wrapped in try/finally block and POP_EXCEPT opcode # is inserted before END_FINALLY, as a result cleanup-finally block is executed outside # except handler. It's not a bug, as it doesn't cause any problems during execution, but # it breaks 'regularity' and we can't check inconsistency here. Maybe issue should be # posted to Python bug tracker. pass continue else: continue if o not in (BREAK_LOOP, RETURN_VALUE, RAISE_VARARGS, STOP_CODE): next_pos = cur_state.pos + 1 if not isopcode(o): op += State(next_pos, cur_state.stack, cur_state.block_stack, cur_state.log), elif o not in hasflow: if o in (LOAD_GLOBAL, LOAD_CONST, LOAD_NAME, LOAD_FAST, LOAD_ATTR, LOAD_DEREF, LOAD_CLASSDEREF, LOAD_CLOSURE, STORE_GLOBAL, STORE_NAME, STORE_FAST, STORE_ATTR, STORE_DEREF, DELETE_GLOBAL, DELETE_NAME, DELETE_FAST, DELETE_ATTR, DELETE_DEREF, IMPORT_NAME, IMPORT_FROM, COMPARE_OP): se = stack_effect(o, 0) else: se = stack_effect(o, arg) log = cur_state.newlog("non-flow command (" + str(o) + ", se = " + str(se) + ")") op += State(next_pos, cur_state.newstack(se), cur_state.block_stack, log), elif o == FOR_ITER: inside_for_log = cur_state.newlog("FOR_ITER (+1)") op += State(label_pos[arg], cur_state.newstack(-1), cur_state.block_stack, cur_state.log),\ State(next_pos, cur_state.newstack(1), cur_state.block_stack, inside_for_log) elif o in (JUMP_FORWARD, JUMP_ABSOLUTE): after_jump_log = cur_state.newlog(str(o)) op += State(label_pos[arg], cur_state.stack, cur_state.block_stack, after_jump_log), elif o in (JUMP_IF_FALSE_OR_POP, JUMP_IF_TRUE_OR_POP): after_jump_log = cur_state.newlog(str(o) + ", jumped") log = cur_state.newlog(str(o) + ", not jumped (-1)") op += State(label_pos[arg], cur_state.stack, cur_state.block_stack, after_jump_log),\ State(next_pos, cur_state.newstack(-1), cur_state.block_stack, log) elif o in {POP_JUMP_IF_TRUE, POP_JUMP_IF_FALSE}: after_jump_log = cur_state.newlog(str(o) + ", jumped (-1)") log = cur_state.newlog(str(o) + ", not jumped (-1)") op += State(label_pos[arg], cur_state.newstack(-1), cur_state.block_stack, after_jump_log),\ State(next_pos, cur_state.newstack(-1), cur_state.block_stack, log) elif o == CONTINUE_LOOP: next_stack, next_block_stack = cur_state.stack, cur_state.block_stack last_popped_block = None while next_block_stack[-1] != BlockType.LOOP_BODY: last_popped_block = next_block_stack[-1] next_stack, next_block_stack = next_stack[:-1], next_block_stack[:-1] if next_stack != cur_state.stack: log = cur_state.newlog("CONTINUE_LOOP, from non-loop block") else: log = cur_state.newlog("CONTINUE_LOOP") jump_to_pos = label_pos[arg] if last_popped_block == BlockType.WITH_BLOCK: next_stack = next_stack[:-1] + (next_stack[-1] - 1,) op += State(jump_to_pos, next_stack, next_block_stack, log), elif o == SETUP_LOOP: inside_loop_log = cur_state.newlog("SETUP_LOOP (+block)") op += State(label_pos[arg], cur_state.stack, cur_state.block_stack, cur_state.log),\ State(next_pos, cur_state.stack + (0,), cur_state.block_stack + (BlockType.LOOP_BODY,), inside_loop_log) elif o == SETUP_EXCEPT: inside_except_log = cur_state.newlog("SETUP_EXCEPT, exception (+6, +block)") inside_try_log = cur_state.newlog("SETUP_EXCEPT, try-block (+block)") op += State(label_pos[arg], cur_state.stack + (6,), cur_state.block_stack + (BlockType.EXCEPTION,), inside_except_log),\ State(next_pos, cur_state.stack + (0,), cur_state.block_stack + (BlockType.TRY_EXCEPT,), inside_try_log) elif o == SETUP_FINALLY: inside_finally_block = cur_state.newlog("SETUP_FINALLY (+1)") inside_try_log = cur_state.newlog("SETUP_FINALLY try-block (+block)") op += State(label_pos[arg], cur_state.newstack(1), cur_state.block_stack, inside_finally_block),\ State(next_pos, cur_state.stack + (0,), cur_state.block_stack + (BlockType.TRY_FINALLY,), inside_try_log) elif o == POP_BLOCK: log = cur_state.newlog("POP_BLOCK (-block)") op += State(next_pos, cur_state.stack[:-1], cur_state.block_stack[:-1], log), elif o == POP_EXCEPT: log = cur_state.newlog("POP_EXCEPT (-block)") op += State(next_pos, cur_state.stack[:-1], cur_state.block_stack[:-1], log), elif o == END_FINALLY: if cur_state.block_stack[-1] == BlockType.SILENCED_EXCEPTION_BLOCK: log = cur_state.newlog("END_FINALLY pop silenced exception block (-block)") op += State(next_pos, cur_state.stack[:-1], cur_state.block_stack[:-1], log), elif cur_state.block_stack[-1] == BlockType.EXCEPTION: # Reraise exception pass else: log = cur_state.newlog("END_FINALLY (-6)") op += State(next_pos, cur_state.newstack(-6), cur_state.block_stack, log), elif o == SETUP_WITH or (version_info >= (3, 5,) and o == SETUP_ASYNC_WITH): inside_with_block = cur_state.newlog("SETUP_WITH, with-block (+1, +block)") inside_finally_block = cur_state.newlog("SETUP_WITH, finally (+1)") op += State(label_pos[arg], cur_state.newstack(1), cur_state.block_stack, inside_finally_block),\ State(next_pos, cur_state.stack + (1,), cur_state.block_stack + (BlockType.WITH_BLOCK,), inside_with_block) elif version_info < (3, 5) and o == WITH_CLEANUP: # There is special case when 'with' __exit__ function returns True, # that's the signal to silence exception, in this case additional element is pushed # and next END_FINALLY command won't reraise exception. log = cur_state.newlog("WITH_CLEANUP (-1)") silenced_exception_log = cur_state.newlog("WITH_CLEANUP silenced_exception (+1, +block)") op += State(next_pos, cur_state.newstack(-1), cur_state.block_stack, log),\ State(next_pos, cur_state.newstack(-7) + (8,), cur_state.block_stack + (BlockType.SILENCED_EXCEPTION_BLOCK,), silenced_exception_log) elif version_info >= (3, 5,) and o == WITH_CLEANUP_START: # There is special case when 'with' __exit__ function returns True, # that's the signal to silence exception, in this case additional element is pushed # and next END_FINALLY command won't reraise exception. # Emulate this situation on WITH_CLEANUP_START with creating special block which will be # handled differently by WITH_CLEANUP_FINISH and will cause END_FINALLY not to reraise exception. log = cur_state.newlog("WITH_CLEANUP_START (+1)") silenced_exception_log = cur_state.newlog("WITH_CLEANUP_START silenced_exception (+block)") op += State(next_pos, cur_state.newstack(1), cur_state.block_stack, log),\ State(next_pos, cur_state.newstack(-7) + (9,), cur_state.block_stack + (BlockType.SILENCED_EXCEPTION_BLOCK,), silenced_exception_log) elif version_info >= (3, 5,) and o == WITH_CLEANUP_FINISH: if cur_state.block_stack[-1] == BlockType.SILENCED_EXCEPTION_BLOCK: # See comment in WITH_CLEANUP_START handler log = cur_state.newlog("WITH_CLEANUP_FINISH silenced_exception (-1)") op += State(next_pos, cur_state.newstack(-1), cur_state.block_stack, log), else: log = cur_state.newlog("WITH_CLEANUP_FINISH (-2)") op += State(next_pos, cur_state.newstack(-2), cur_state.block_stack, log), else: raise ValueError("Unhandled opcode %s" % o) return maxsize + 6 # for exception raise in deepest place
def stack_effect(self): return stack_effect( self.opcode, *((self.arg,) if self.have_arg else ()) )
def plumb(self, depths): arg = 0 if isinstance(self.arg, Label) else self.arg depths.append(depths[-1] + dis.stack_effect(self.opcode, arg))
def update_event(self, inp=-1): self.set_output_val(0, dis.stack_effect(self.input(0), self.input(1)))
def _compute_stacksize(self, logging=False): code = self.code label_pos = { op[0]: pos for pos, op in enumerate(code) if isinstance(op[0], Label) } # sf_targets are the targets of SETUP_FINALLY opcodes. They are recorded # because they have special stack behaviour. If an exception was raised # in the block pushed by a SETUP_FINALLY opcode, the block is popped # and 3 objects are pushed. On return or continue, the block is popped # and 2 objects are pushed. If nothing happened, the block is popped by # a POP_BLOCK opcode and 1 object is pushed by a (LOAD_CONST, None) # operation # Our solution is to record the stack state of SETUP_FINALLY targets # as having 3 objects pushed, which is the maximum. However, to make # stack recording consistent, the get_next_stacks function will always # yield the stack state of the target as if 1 object was pushed, but # this will be corrected in the actual stack recording if version_info < (3, 5): sf_targets = { label_pos[arg] for op, arg in code if (op == SETUP_FINALLY or op == SETUP_WITH) } else: sf_targets = { label_pos[arg] for op, arg in code if (op == SETUP_FINALLY or op == SETUP_WITH or op == SETUP_ASYNC_WITH) } states = [None] * len(code) maxsize = 0 class BlockType(Enum): DEFAULT = 0, TRY_FINALLY = 1, TRY_EXCEPT = 2, LOOP_BODY = 3, WITH_BLOCK = 4, EXCEPTION = 5, SILENCED_EXCEPTION_BLOCK = 6, class State: def __init__(self, pos=0, stack=(0, ), block_stack=(BlockType.DEFAULT, ), log=[]): self._pos = pos self._stack = stack self._block_stack = block_stack self._log = log @property def pos(self): return self._pos @property def stack(self): return self._stack @stack.setter def stack(self, val): self._stack = val def newstack(self, n): if self._stack[-1] < -n: raise ValueError( "Popped a non-existing element at %s %s" % (self._pos, code[self._pos - 4:self._pos + 3])) return self._stack[:-1] + (self._stack[-1] + n, ) @property def block_stack(self): return self._block_stack @property def log(self): return self._log def newlog(self, msg): if not logging: return None log_msg = str(self._pos) + ": " + msg if self._stack: log_msg += " (on stack: " log_depth = 2 log_depth = min(log_depth, len(self._stack)) for pos in range(-1, -log_depth, -1): log_msg += str(self._stack[pos]) + ", " log_msg += str(self._stack[-log_depth]) log_msg += ")" else: log_msg += " (empty stack)" return [log_msg] + self._log op = [State()] while op: cur_state = op.pop() o = sum(cur_state.stack) if o > maxsize: maxsize = o o, arg = code[cur_state.pos] if isinstance(o, Label): if cur_state.pos in sf_targets: cur_state.stack = cur_state.newstack(5) if states[cur_state.pos] is None: states[cur_state.pos] = cur_state elif states[cur_state.pos].stack != cur_state.stack: check_pos = cur_state.pos + 1 while code[check_pos][0] not in hasflow: check_pos += 1 if code[check_pos][0] not in (RETURN_VALUE, RAISE_VARARGS, STOP_CODE): if cur_state.pos not in sf_targets: raise ValueError( "Inconsistent code at %s %s %s\n%s" % (cur_state.pos, cur_state.stack, states[cur_state.pos].stack, code[cur_state.pos - 5:cur_state.pos + 4])) else: # SETUP_FINALLY target inconsistent code! # # Since Python 3.2 assigned exception is cleared at the end of # the except clause (named exception handler). # To perform this CPython (checked in version 3.4.3) adds special # bytecode in exception handler which currently breaks 'regularity' of bytecode. # Exception handler is wrapped in try/finally block and POP_EXCEPT opcode # is inserted before END_FINALLY, as a result cleanup-finally block is executed outside # except handler. It's not a bug, as it doesn't cause any problems during execution, but # it breaks 'regularity' and we can't check inconsistency here. Maybe issue should be # posted to Python bug tracker. pass continue else: continue if o not in (BREAK_LOOP, RETURN_VALUE, RAISE_VARARGS, STOP_CODE): next_pos = cur_state.pos + 1 if not isopcode(o): op += State(next_pos, cur_state.stack, cur_state.block_stack, cur_state.log), elif o not in hasflow: if o in (LOAD_GLOBAL, LOAD_CONST, LOAD_NAME, LOAD_FAST, LOAD_ATTR, LOAD_DEREF, LOAD_CLASSDEREF, LOAD_CLOSURE, STORE_GLOBAL, STORE_NAME, STORE_FAST, STORE_ATTR, STORE_DEREF, DELETE_GLOBAL, DELETE_NAME, DELETE_FAST, DELETE_ATTR, DELETE_DEREF, IMPORT_NAME, IMPORT_FROM, COMPARE_OP): se = stack_effect(o, 0) else: se = stack_effect(o, arg) log = cur_state.newlog("non-flow command (" + str(o) + ", se = " + str(se) + ")") op += State(next_pos, cur_state.newstack(se), cur_state.block_stack, log), elif o == FOR_ITER: inside_for_log = cur_state.newlog("FOR_ITER (+1)") op += State(label_pos[arg], cur_state.newstack(-1), cur_state.block_stack, cur_state.log),\ State(next_pos, cur_state.newstack(1), cur_state.block_stack, inside_for_log) elif o in (JUMP_FORWARD, JUMP_ABSOLUTE): after_jump_log = cur_state.newlog(str(o)) op += State(label_pos[arg], cur_state.stack, cur_state.block_stack, after_jump_log), elif o in (JUMP_IF_FALSE_OR_POP, JUMP_IF_TRUE_OR_POP): after_jump_log = cur_state.newlog(str(o) + ", jumped") log = cur_state.newlog(str(o) + ", not jumped (-1)") op += State(label_pos[arg], cur_state.stack, cur_state.block_stack, after_jump_log),\ State(next_pos, cur_state.newstack(-1), cur_state.block_stack, log) elif o in {POP_JUMP_IF_TRUE, POP_JUMP_IF_FALSE}: after_jump_log = cur_state.newlog(str(o) + ", jumped (-1)") log = cur_state.newlog(str(o) + ", not jumped (-1)") op += State(label_pos[arg], cur_state.newstack(-1), cur_state.block_stack, after_jump_log),\ State(next_pos, cur_state.newstack(-1), cur_state.block_stack, log) elif o == CONTINUE_LOOP: next_stack, next_block_stack = cur_state.stack, cur_state.block_stack last_popped_block = None while next_block_stack[-1] != BlockType.LOOP_BODY: last_popped_block = next_block_stack[-1] next_stack, next_block_stack = next_stack[: -1], next_block_stack[: -1] if next_stack != cur_state.stack: log = cur_state.newlog( "CONTINUE_LOOP, from non-loop block") else: log = cur_state.newlog("CONTINUE_LOOP") jump_to_pos = label_pos[arg] if last_popped_block == BlockType.WITH_BLOCK: next_stack = next_stack[:-1] + (next_stack[-1] - 1, ) op += State(jump_to_pos, next_stack, next_block_stack, log), elif o == SETUP_LOOP: inside_loop_log = cur_state.newlog("SETUP_LOOP (+block)") op += State(label_pos[arg], cur_state.stack, cur_state.block_stack, cur_state.log),\ State(next_pos, cur_state.stack + (0,), cur_state.block_stack + (BlockType.LOOP_BODY,), inside_loop_log) elif o == SETUP_EXCEPT: inside_except_log = cur_state.newlog( "SETUP_EXCEPT, exception (+6, +block)") inside_try_log = cur_state.newlog( "SETUP_EXCEPT, try-block (+block)") op += State(label_pos[arg], cur_state.stack + (6,), cur_state.block_stack + (BlockType.EXCEPTION,), inside_except_log),\ State(next_pos, cur_state.stack + (0,), cur_state.block_stack + (BlockType.TRY_EXCEPT,), inside_try_log) elif o == SETUP_FINALLY: inside_finally_block = cur_state.newlog( "SETUP_FINALLY (+1)") inside_try_log = cur_state.newlog( "SETUP_FINALLY try-block (+block)") op += State(label_pos[arg], cur_state.newstack(1), cur_state.block_stack, inside_finally_block),\ State(next_pos, cur_state.stack + (0,), cur_state.block_stack + (BlockType.TRY_FINALLY,), inside_try_log) elif o == POP_BLOCK: log = cur_state.newlog("POP_BLOCK (-block)") op += State(next_pos, cur_state.stack[:-1], cur_state.block_stack[:-1], log), elif o == POP_EXCEPT: log = cur_state.newlog("POP_EXCEPT (-block)") op += State(next_pos, cur_state.stack[:-1], cur_state.block_stack[:-1], log), elif o == END_FINALLY: if cur_state.block_stack[ -1] == BlockType.SILENCED_EXCEPTION_BLOCK: log = cur_state.newlog( "END_FINALLY pop silenced exception block (-block)" ) op += State(next_pos, cur_state.stack[:-1], cur_state.block_stack[:-1], log), elif cur_state.block_stack[-1] == BlockType.EXCEPTION: # Reraise exception pass else: log = cur_state.newlog("END_FINALLY (-6)") op += State(next_pos, cur_state.newstack(-6), cur_state.block_stack, log), elif o == SETUP_WITH or (version_info >= ( 3, 5, ) and o == SETUP_ASYNC_WITH): inside_with_block = cur_state.newlog( "SETUP_WITH, with-block (+1, +block)") inside_finally_block = cur_state.newlog( "SETUP_WITH, finally (+1)") op += State(label_pos[arg], cur_state.newstack(1), cur_state.block_stack, inside_finally_block),\ State(next_pos, cur_state.stack + (1,), cur_state.block_stack + (BlockType.WITH_BLOCK,), inside_with_block) elif version_info < (3, 5) and o == WITH_CLEANUP: # There is special case when 'with' __exit__ function returns True, # that's the signal to silence exception, in this case additional element is pushed # and next END_FINALLY command won't reraise exception. log = cur_state.newlog("WITH_CLEANUP (-1)") silenced_exception_log = cur_state.newlog( "WITH_CLEANUP silenced_exception (+1, +block)") op += State(next_pos, cur_state.newstack(-1), cur_state.block_stack, log),\ State(next_pos, cur_state.newstack(-7) + (8,), cur_state.block_stack + (BlockType.SILENCED_EXCEPTION_BLOCK,), silenced_exception_log) elif version_info >= ( 3, 5, ) and o == WITH_CLEANUP_START: # There is special case when 'with' __exit__ function returns True, # that's the signal to silence exception, in this case additional element is pushed # and next END_FINALLY command won't reraise exception. # Emulate this situation on WITH_CLEANUP_START with creating special block which will be # handled differently by WITH_CLEANUP_FINISH and will cause END_FINALLY not to reraise exception. log = cur_state.newlog("WITH_CLEANUP_START (+1)") silenced_exception_log = cur_state.newlog( "WITH_CLEANUP_START silenced_exception (+block)") op += State(next_pos, cur_state.newstack(1), cur_state.block_stack, log),\ State(next_pos, cur_state.newstack(-7) + (9,), cur_state.block_stack + (BlockType.SILENCED_EXCEPTION_BLOCK,), silenced_exception_log) elif version_info >= ( 3, 5, ) and o == WITH_CLEANUP_FINISH: if cur_state.block_stack[ -1] == BlockType.SILENCED_EXCEPTION_BLOCK: # See comment in WITH_CLEANUP_START handler log = cur_state.newlog( "WITH_CLEANUP_FINISH silenced_exception (-1)") op += State(next_pos, cur_state.newstack(-1), cur_state.block_stack, log), else: log = cur_state.newlog("WITH_CLEANUP_FINISH (-2)") op += State(next_pos, cur_state.newstack(-2), cur_state.block_stack, log), else: raise ValueError("Unhandled opcode %s" % o) return maxsize + 6 # for exception raise in deepest place
def _hax(bytecode: CodeType) -> CodeType: ops = instructions_with_lines(bytecode) used = False code: List[int] = [] last_line = bytecode.co_firstlineno lnotab: List[int] = [] consts: List[object] = [bytecode.co_consts[0]] names: Dict[str, int] = {} stacksize = 0 jumps: Dict[int, List[Dict[str, Any]]] = {} deferred: Dict[int, int] = {} varnames: Dict[str, int] = { name: index for index, name in enumerate( bytecode.co_varnames[ : bytecode.co_argcount + bytecode.co_kwonlyargcount + getattr(bytecode, "co_posonlyargcount", 0) ] ) } flags = bytecode.co_flags labels: Dict[Hashable, int] = {} deferred_labels: Dict[Hashable, List[Dict[str, Any]]] = {} while True: extended: List[int] = [] for op, line in ops: if op.is_jump_target: deferred[op.offset] = len(code) // OFFSET_SCALE offset = len(code) // OFFSET_SCALE for info in jumps.get(op.offset, ()): info["arg"] = offset - info["arg"] code[info["start"] : info["start"] + info["min_size"]] = backfill( **info ) # assert len(code) == offset, "Code changed size!" # Look into this! if op.opcode < HAVE_ARGUMENT: if op.opcode != NOP: # Just skip these? stacksize += max(0, stack_effect(op.opcode)) lnotab += 1, line - last_line code += op.opcode, 0 last_line = line continue if op.opcode != EXTENDED_ARG: break assert isinstance(op.arg, int), "Non-integer argument!" extended += EXTENDED_ARG, op.arg else: break if op.argval not in opmap and op.argval not in {"HAX_LABEL", "LABEL"}: info = dict( arg=op.arg, start=len(code), line=line, following=op, min_size=len(extended) + 2, new_op=op.opcode, filename=bytecode.co_filename, ) if op.opcode in HASLOCAL: info["arg"] = varnames.setdefault(op.argval, len(varnames)) elif op.opcode in HASNAME: info["arg"] = names.setdefault(op.argval, len(names)) elif op.opcode in HASCONST: try: info["arg"] = consts.index(op.argval) except ValueError: consts.append(op.argval) info["arg"] = len(consts) - 1 elif op.opcode in HASJABS: if op.argval <= op.offset: info["arg"] = deferred[op.argval] else: info["arg"] = 0 jumps.setdefault(op.argval, []).append(info) elif op.opcode in HASJREL: info["arg"] = (len(code) + len(extended) + 2) // OFFSET_SCALE jumps.setdefault(op.argval, []).append(info) assert op.opcode != EXTENDED_ARG stacksize += max( 0, stack_effect( op.opcode, info["arg"] if HAVE_ARGUMENT <= op.opcode else None ), ) new_code = [*backfill(**info)] lnotab += 1, line - last_line, len(new_code) - 1, 0 code += new_code last_line = line continue used = True if op.opname not in {"LOAD_FAST", "LOAD_GLOBAL", "LOAD_NAME"}: raise HaxCompileError( "Ops must consist of a simple call.", (bytecode.co_filename, line, None, None), ) args = 0 arg = 0 for following, _ in ops: if following.opcode == EXTENDED_ARG: continue if following.opcode == LOAD_CONST: arg = following.argval args += 1 continue break else: # pragma: no cover assert False if following.opcode != CALL_FUNCTION: raise HaxCompileError( "Ops must consist of a simple call.", (bytecode.co_filename, line, None, None), ) following, _ = next(ops) if following.opcode != POP_TOP: raise HaxCompileError( "Ops must be standalone statements.", (bytecode.co_filename, line, None, None), ) line = following.starts_line or line if op.argval in {"HAX_LABEL", "LABEL"}: if op.argval == "LABEL": warn( DeprecationWarning("LABEL is deprecated (use HAX_LABEL instead)"), stacklevel=3, ) if arg in labels: raise HaxCompileError( f"Label {arg!r} already exists!", (bytecode.co_filename, line, None, None), ) offset = len(code) // OFFSET_SCALE labels[arg] = offset for info in deferred_labels.pop(arg, ()): info["arg"] = offset - info["arg"] code[info["start"] : info["start"] + info["min_size"]] = backfill( **info ) assert len(code) == offset * OFFSET_SCALE, "Code changed size!" last_line = line continue if op.argval in {"YIELD_FROM", "YIELD_VALUE"}: if flags & CO_COROUTINE: flags ^= CO_COROUTINE flags |= CO_ASYNC_GENERATOR assert flags & CO_ASYNC_GENERATOR assert not flags & CO_GENERATOR elif not flags & CO_ASYNC_GENERATOR: flags |= CO_GENERATOR assert not flags & CO_ASYNC_GENERATOR assert flags & CO_GENERATOR assert not flags & CO_COROUTINE new_op = opmap[op.argval] has_arg = HAVE_ARGUMENT <= new_op if args != has_arg: raise HaxCompileError( f"Number of arguments is wrong (expected {int(has_arg)}, got {args}).", (bytecode.co_filename, line, None, None), ) info = dict( arg=arg, start=0, line=line, following=following, min_size=2, new_op=new_op, filename=bytecode.co_filename, ) if new_op in HASLOCAL: if not isinstance(arg, str): raise HaxCompileError( f"Expected a string (got {arg!r}).", (bytecode.co_filename, line, None, None), ) info["arg"] = varnames.setdefault(arg, len(varnames)) elif new_op in HASNAME: if not isinstance(arg, str): raise HaxCompileError( f"Expected a string (got {arg!r}).", (bytecode.co_filename, line, None, None), ) info["arg"] = names.setdefault(arg, len(names)) elif new_op in HASCONST: try: info["arg"] = consts.index(arg) except ValueError: consts.append(arg) info["arg"] = len(consts) - 1 elif new_op in HASCOMPARE: if not isinstance(arg, str): raise HaxCompileError( f"Expected a string (got {arg!r}).", (bytecode.co_filename, line, None, None), ) try: info["arg"] = cmp_op.index(arg) except ValueError: raise HaxCompileError( f"Bad comparision operator {arg!r}; expected one of {' / '.join(map(repr, cmp_op))}!", (bytecode.co_filename, line, None, None), ) from None elif new_op in HASFREE: if not isinstance(arg, str): raise HaxCompileError( f"Expected a string (got {arg!r}).", (bytecode.co_filename, line, None, None), ) try: info["arg"] = (bytecode.co_cellvars + bytecode.co_freevars).index(arg) except ValueError: raise HaxCompileError( # Just do this for them? f'No free/cell variable {arg!r}; maybe use "nonlocal" in the inner scope to compile correctly?', (bytecode.co_filename, line, None, None), ) from None elif new_op in HASJUMP: if arg in labels: if new_op in HASJREL: raise HaxCompileError( "Relative jumps must be forwards, not backwards!", (bytecode.co_filename, line, None, None), ) info["arg"] = labels[arg] else: max_jump = ( len(bytecode.co_code) - 1 - ((len(code) + 2) if new_op in HASJREL else 0) ) if 1 << 24 <= max_jump: padding = 6 elif 1 << 16 <= max_jump: padding = 4 elif 1 << 8 <= max_jump: padding = 2 else: padding = 0 info["arg"] = ( ((len(code) + padding + 2) // OFFSET_SCALE) if new_op in HASJREL else 0 ) info["start"] = len(code) info["min_size"] = padding + 2 deferred_labels.setdefault(arg, []).append(info) elif not isinstance(arg, int): raise HaxCompileError( f"Expected integer argument, got {arg!r}.", (bytecode.co_filename, line, None, None), ) if new_op not in {EXTENDED_ARG, NOP}: stacksize += max(0, stack_effect(new_op, info["arg"] if has_arg else None)) new_code = [*backfill(**info)] lnotab += 1, line - last_line, len(new_code) - 1, 0 code += new_code last_line = line if not used: return bytecode if deferred_labels: # Warn unused labels, too? raise HaxCompileError( f"The following labels don't exist: {', '.join(map(repr, deferred_labels))}" ) if sys.version_info < (3, 8): # pragma: no cover maybe_posonlyargcount = () else: # pragma: no cover maybe_posonlyargcount = (bytecode.co_posonlyargcount,) return CodeType( bytecode.co_argcount, *maybe_posonlyargcount, bytecode.co_kwonlyargcount, len(varnames), stacksize, flags, bytes(code), tuple(consts), tuple(names), tuple(varnames), bytecode.co_filename, bytecode.co_name, bytecode.co_firstlineno, bytes(lnotab), bytecode.co_freevars, # Need to update? CO_NOFREE? bytecode.co_cellvars, # Need to update? CO_NOFREE? )
However this can only used in Python 3.4 and above which has dis.stack_effect(). """ from xdis import PYTHON_VERSION import dis NOTFIXED = -100 from xdis.cross_dis import op_has_argument from xdis import get_opcode print("# Python %s Stack effects\n" % PYTHON_VERSION) assert PYTHON_VERSION >= 3.4, "This only works for Python version 3.4 and above; you have version %s." % PYTHON_VERSION print("[") opc = get_opcode(PYTHON_VERSION, False) for i in range(256): try: if op_has_argument(i, opc): effect = dis.stack_effect(i, 0) opargs_to_try = [ -1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 256, 1000, 0xffff, 0 ] for operand in opargs_to_try: with_oparg = dis.stack_effect(i, operand) if effect != with_oparg: effect = NOTFIXED break pass else: effect = dis.stack_effect(i) pass pass except: print(" %d, # %d" % (NOTFIXED, i))
builder.store(builder.load(builder.gep(args,(int32(de),))),stack[stack_ptr]) stack_ptr+=1 builder.call(stackrestore,[savestack]) elif ins.opname=='BUILD_SLICE': stack_ptr = pop_and_call(builder,func,stack_ptr,builtin_slice, range(ins.arg), []) else: assert(False) if 42 in branch_stack and branch_stack[42] != oldstuff: print("br stack: " + str(branch_stack[42]) + ", " + str(oldstuff)) print(block.name) if ( ins.opname!='SETUP_FINALLY' and ins.opname!='SETUP_EXCEPT' and ins.opname!='POP_EXCEPT' and ins.opname!='END_FINALLY' and ins.opname!='EXTENDED_ARG' and ins.opname!='SETUP_LOOP' and not dis.stack_effect(ins.opcode,ins.arg) == stack_ptr - save_stack_ptr): print(dis.stack_effect(ins.opcode,ins.arg), stack_ptr - save_stack_ptr) assert(False) if did_jmp == False and block_idx+1 < len(blocks) and ins_idx+1==blocks[block_idx+1][0]: builder.branch(blocks[block_idx+1][2]) ins_idx+=1 i+=1 open("foo.ll", "w+").write(str(module))
def stack_effect(o, arg): return (dis.stack_effect(o, arg) if o != CALL_FUNCTION_EX else -2 if arg else -1)
def stack_effect(self): return stack_effect( self.opcode, *((self.arg if isinstance(self.arg, int) else 0,) if self.have_arg else ()) )
def stack_effect(self): return stack_effect(self.opcode, *((self.arg, ) if self.have_arg else ()))
def stack_effect(self): return stack_effect( self.opcode, *((self.arg if isinstance(self.arg, int) else 0, ) if self.have_arg else ()))
def _compute_stacksize(self): code = self.code label_pos = { op[0]: pos for pos, op in enumerate(code) if isinstance(op[0], Label) } # sf_targets are the targets of SETUP_FINALLY opcodes. They are recorded # because they have special stack behaviour. If an exception was raised # in the block pushed by a SETUP_FINALLY opcode, the block is popped # and 3 objects are pushed. On return or continue, the block is popped # and 2 objects are pushed. If nothing happened, the block is popped by # a POP_BLOCK opcode and 1 object is pushed by a (LOAD_CONST, None) # operation # Our solution is to record the stack state of SETUP_FINALLY targets # as having 3 objects pushed, which is the maximum. However, to make # stack recording consistent, the get_next_stacks function will always # yield the stack state of the target as if 1 object was pushed, but # this will be corrected in the actual stack recording sf_targets = { label_pos[arg] for op, arg in code if op == SETUP_FINALLY } stacks = [None] * len(code) maxsize = 0 op = [(0, (0, ))] def newstack(n): if curstack[-1] < -n: raise ValueError("Popped a non-existing element at %s %s" % (pos, code[pos - 3:pos + 2])) return curstack[:-1] + (curstack[-1] + n, ) while op: pos, curstack = op.pop() o = sum(curstack) if o > maxsize: maxsize = o o, arg = code[pos] if isinstance(o, Label): if pos in sf_targets: curstack = curstack[:-1] + (curstack[-1] + 2, ) if stacks[pos] is None: stacks[pos] = curstack if o not in (BREAK_LOOP, RETURN_VALUE, RAISE_VARARGS, STOP_CODE): pos += 1 if not isopcode(o): op += (pos, curstack), elif o not in hasflow: op += (pos, newstack(stack_effect(o, arg))), elif o == FOR_ITER: op += (label_pos[arg], newstack(-1)), (pos, newstack(1)) elif o in (JUMP_FORWARD, JUMP_ABSOLUTE): op += (label_pos[arg], curstack), elif o in (POP_JUMP_IF_FALSE, POP_JUMP_IF_TRUE): curstack = newstack(-1) op += (label_pos[arg], curstack), (pos, curstack) elif o in (JUMP_IF_FALSE_OR_POP, JUMP_IF_TRUE_OR_POP): op += (label_pos[arg], curstack), (pos, newstack(-1)) elif o == CONTINUE_LOOP: op += (label_pos[arg], curstack[:-1]), elif o == SETUP_LOOP: op += (pos, curstack + (0, )), (label_pos[arg], curstack) elif o == SETUP_EXCEPT: op += (pos, curstack + (0, )), (label_pos[arg], newstack(3)) elif o == SETUP_FINALLY: op += (pos, curstack + (0, )), (label_pos[arg], newstack(1)) elif o == POP_BLOCK: op += (pos, curstack[:-1]), elif o == END_FINALLY: op += (pos, newstack(-3)), elif o == WITH_CLEANUP: op += (pos, newstack(-1)), else: raise ValueError("Unhandled opcode %s" % op) elif stacks[pos] != curstack: op = pos + 1 while code[op][0] not in hasflow: op += 1 if code[op][0] not in (RETURN_VALUE, RAISE_VARARGS, STOP_CODE): raise ValueError("Inconsistent code at %s %s %s\n%s" % (pos, curstack, stacks[pos], code[pos - 5:pos + 4])) return maxsize
def stack_effect(self): # All opcodes whose arguments are not represented by integers have # a stack_effect indepent of their argument. arg = (self._arg if isinstance(self._arg, int) else 0 if self._opcode >= _opcode.HAVE_ARGUMENT else None) return dis.stack_effect(self._opcode, arg)
""" Based on a question in Freenode #python on March 6th, 2019 about computing the effect of code on the size of the stack """ import dis from pprint import pprint from codetransformer import Code def example(arg): try: arg.x except: return print(dis.code_info(example)) instr = list(dis.get_instructions(example)) sfx = [dis.stack_effect(op.opcode, op.arg) for op in instr] for i, s in zip(instr, sfx): opline = '\t'.join([ f"{thing:<15}" for thing in ('>>' if i.is_jump_target else '', i.offset, i.opname, i.argrepr, f'{s:>5d}') ]) print(opline) c = Code.from_pyfunc(example)