# Copyright (c) 2020-2022 Rocky Bernstein """ If/else statement reduction check for Python 2.6 (and older?) """ IFELSE_STMT_RULES = frozenset( [ ( "ifelsestmt", ( "testexpr_then", "pass", "filler", "else_suitel", "COME_FROM", "POP_TOP", ), ), ( "ifelsestmt", ( "testexpr_then", "c_stmts_opt", "\\e_filler", "else_suitel", "come_froms", "POP_TOP", ), ), ( "ifelsestmt", ( "testexpr_then", "\\e_c_stmts_opt", "\\e_filler", "else_suitel", "come_froms", "POP_TOP", ), ), # We may do something like add these in the future: ] ) def ifelsestmt2(self, lhs, n, rule, tree, tokens, first, last): if (last + 1) < n and tokens[last + 1] == "COME_FROM_LOOP" and lhs != "ifelsestmtc": # ifelsestmt jumped outside of loop. No good. return True # print("XXX", first, last) # for t in range(first, last): # print(tokens[t]) # print("=" * 30) if rule not in IFELSE_STMT_RULES: # print("XXX", rule) return False # Avoid if/else where the "then" is a "raise_stmt1" for an # assert statement. Parse this as an "assert" instead. stmts = tree[1] if stmts in ("c_stmts",) and len(stmts) == 1: raise_stmt1 = stmts[0] if raise_stmt1 == "raise_stmt1" and raise_stmt1[0] in ("LOAD_ASSERT",): return True # Make sure all of the "come_froms" offset at the # end of the "if" come from somewhere inside the "if". # Since the come_froms are ordered so that lowest # offset COME_FROM is last, it is sufficient to test # just the last one. if len(tree) == 6 and tree[-1] == "POP_TOP": # FIXME: There is weirdness in the grammar we need to work around. # we need to clean up the grammar. last_token = tree[-2] if last_token == "COME_FROM" and tokens[first].offset > last_token.attr: if ( self.insts[self.offset2inst_index[last_token.attr]].opname != "SETUP_LOOP" ): return True testexpr = tree[0] # Check that the condition portion of the "if" # jumps to the "else" part. if testexpr[0] in ("testtrue", "testfalse", "testfalse_then"): if_condition = testexpr[0] else_suite = tree[3] assert else_suite.kind.startswith("else_suite") if len(if_condition) > 1 and if_condition[1].kind.startswith("jmp_"): if last == n: last -= 1 jmp = if_condition[1] jmp_target = int(jmp[0].pattr) # Below we check that jmp_target is jumping to a feasible # location. It should be to the transition after the "then" # block and to the beginning of the "else" block. # However the "if/else" is inside a loop the false test can be # back to the loop. # FIXME: the below logic for jf_cfs could probably be # simplified. if tree[2] == "filler": jump_else_end = tree[3] else: jump_else_end = tree[2] if jump_else_end == "jf_cfs": jump_else_end = jump_else_end[0] if jump_else_end == "JUMP_FORWARD": endif_target = int(jump_else_end.pattr) last_offset = tokens[last].off2int() if endif_target != last_offset: return True last_offset = tokens[last].off2int(prefer_last=False) if jmp_target <= last_offset: # jmp_target should be jumping to the end of the if/then/else # but is it jumping to the beginning of the "else" or before return True if ( jump_else_end in ("jf_cfs", "jump_forward_else") and jump_else_end[0] == "JUMP_FORWARD" ): # If the "else" jump jumps before the end of the the "if .. else end", then this # is not this kind of "ifelsestmt". jump_else_forward = jump_else_end[0] jump_else_forward_target = jump_else_forward.attr if jump_else_forward_target < last_offset: return True pass if ( jump_else_end in ("jb_elsec", "jb_elsel", "jf_cfs", "jb_cfs") and jump_else_end[-1] == "COME_FROM" ): if jump_else_end[-1].off2int() != jmp_target: return True if tokens[first].off2int() > jmp_target: return True return (jmp_target > last_offset) and tokens[last] != "JUMP_FORWARD" return False