Skip to content

Regression Tests (Python 3.9) #85

@hazimeh

Description

@hazimeh

I've been running some regression tests against the online version at pylingual.io, and have so far collected the following test cases which fail using the published source code on the dev branch:

# test.py

def if_else(A):
    """
  3           0 LOAD_FAST                0 (A)
              2 POP_JUMP_IF_FALSE        6

  4           4 JUMP_FORWARD             4 (to 10)

  6     >>    6 LOAD_CONST               1 (False)
              8 RETURN_VALUE

  7     >>   10 LOAD_CONST               2 (True)
             12 RETURN_VALUE
    """
    # expect pass
    if A:
        pass
    else:
        return False
    return True

def pop_block_return_value_in_if_else(A):
    """
 11           0 SETUP_FINALLY           16 (to 18)

 12           2 LOAD_FAST                0 (A)
              4 POP_JUMP_IF_FALSE        8

 13           6 JUMP_FORWARD             6 (to 14)

 15     >>    8 POP_BLOCK
             10 LOAD_CONST               1 (False)
             12 RETURN_VALUE
        >>   14 POP_BLOCK
             16 JUMP_FORWARD            12 (to 30)

 16     >>   18 POP_TOP
             20 POP_TOP
             22 POP_TOP

 17          24 POP_EXCEPT
             26 JUMP_FORWARD             2 (to 30)
             28 RERAISE

 18     >>   30 LOAD_CONST               2 (True)
             32 RETURN_VALUE
    """
    # expect fail
    try:
        if A:
            pass
        else:
            # <8> POP_BLOCK
            # BlockTemplate[
            #   <10> LOAD_CONST False
            #   <12> RETURN_VALUE
            # ]
            #
            # Since the BlockTemplate node does not have the same
            # out[EdgeCategory.Exception] as POP_BLOCK, it is not merged.
            # The tail node referenced by if_body and else_body diverges:
            # - if_body.tail: 14 POP_BLOCK
            # - else_body.tail: BlockTemplate[
            #                     <10> LOAD_CONST False
            #                     <12> RETURN_VALUE
            #                   ]
            #
            # Attempted solution: merge POP_BLOCK into BlockTemplate if it
            #   ends with RETURN_VALUE
            return False
    except:
        pass
    return True

def pop_block_return_global_in_if_else(A):
    """
 23           0 SETUP_FINALLY           16 (to 18)

 24           2 LOAD_FAST                0 (A)
              4 POP_JUMP_IF_FALSE        8

 25           6 JUMP_FORWARD             6 (to 14)

 27     >>    8 LOAD_GLOBAL              0 (result)
             10 POP_BLOCK
             12 RETURN_VALUE
        >>   14 POP_BLOCK
             16 JUMP_FORWARD            12 (to 30)

 28     >>   18 POP_TOP
             20 POP_TOP
             22 POP_TOP

 29          24 POP_EXCEPT
             26 JUMP_FORWARD             2 (to 30)
             28 RERAISE

 30     >>   30 LOAD_CONST               1 (True)
             32 RETURN_VALUE
    """
    # expect fail
    global result
    try:
        if A:
            pass
        else:
            # BlockTemplate [
            #   <8> LOAD_GLOBAL
            #   <10> POP_BLOCK
            # ]
            # <12> RETURN_VALUE
            #
            # Similar to pop_block_return_value_in_if_else, but RETURN_VALUE is
            # an InstTemplate.
            #
            # Attempted solution: merge 2 consecutive {Inst,Block}Templates
            #   if the first one ends with POP_BLOCK and the second one with
            #   RETURN_VALUE
            return result
    except:
        pass
    return True

def pop_block_return_value_in_for_loop(A):
    """
 34           0 LOAD_FAST                0 (A)
              2 GET_ITER
        >>    4 FOR_ITER                30 (to 36)
              6 STORE_FAST               1 (x)

 35           8 SETUP_FINALLY            8 (to 18)

 36          10 POP_BLOCK
             12 POP_TOP
             14 LOAD_CONST               0 (None)
             16 RETURN_VALUE

 37     >>   18 POP_TOP
             20 POP_TOP
             22 POP_TOP

 38          24 POP_EXCEPT
             26 JUMP_ABSOLUTE            4
             28 POP_EXCEPT
             30 JUMP_ABSOLUTE            4
             32 RERAISE
             34 JUMP_ABSOLUTE            4
        >>   36 LOAD_CONST               0 (None)
             38 RETURN_VALUE
    """
    # expect pass
    for x in A:
        try:
            return
        except:
            continue
        
def pop_block_jump_in_for_loop(A):
    """
 42           0 LOAD_FAST                0 (A)
              2 GET_ITER
        >>    4 FOR_ITER                32 (to 38)
              6 STORE_FAST               1 (x)

 43           8 SETUP_FINALLY           10 (to 20)

 44          10 POP_BLOCK
             12 POP_TOP
             14 JUMP_ABSOLUTE           38
             16 POP_BLOCK
             18 JUMP_ABSOLUTE            4

 45     >>   20 POP_TOP
             22 POP_TOP
             24 POP_TOP

 46          26 POP_EXCEPT
             28 JUMP_ABSOLUTE            4
             30 POP_EXCEPT
             32 JUMP_ABSOLUTE            4
             34 RERAISE
             36 JUMP_ABSOLUTE            4
        >>   38 LOAD_CONST               0 (None)
             40 RETURN_VALUE
    """
    # expect fail
    for x in A:
        try:
            break
        except:
            continue

Compiled to test.pyc with:

PYENV_VERSION=3.9 pyenv exec python -c 'import py_compile;py_compile.compile("test.py",cfile="test.pyc")'

The online version passes all the tests: https://pylingual.io/view_chimera?identifier=bea4f3d02f7d5e19b525b83b633607e6a28e22598612eb0303089be1f6abbb2b

The open-source version fails for pop_block_return_value_in_if_else, pop_block_return_global_in_if_else, and pop_block_jump_in_for_loop.

I have attempted a naïve fix, based on my very limited understanding of the control flow reconstructor, by registering the following template in Block.py:

@register_template(2, 21)
class ExcBlockTemplate(ControlFlowTemplate):
    @override
    @classmethod
    def try_match(cls, cfg, node) -> ControlFlowTemplate | None:
        out = out_edge_dict(cfg, node)
        if not ending_instructions("POP_BLOCK")(cfg, node):
            return
        if not (next_node := out[EdgeCategory.Natural]) or out[EdgeCategory.Conditional]:
            return
        if not with_top_level_instructions("RETURN_VALUE", "RETURN_CONST", "JUMP_ABSOLUTE")(cfg, next_node):
            return
        match node, next_node:
            case InstTemplate(), InstTemplate():
                members = [node, next_node]
                new_node = BlockTemplate(members)
                in_edges = [(src, new_node, prop) for src, _, prop in cfg.in_edges(node, data=True) if src not in members]
                out_edges = [(new_node, dst, prop) for _, dst, prop in cfg.out_edges(next_node, data=True)]
                out_edges += [(new_node, dst, prop) for _, dst, prop in cfg.out_edges(node, data=True) if prop['kind'] == EdgeKind.Exception]
                cfg.remove_nodes_from(members)
                cfg.add_node(new_node)
                cfg.add_edges_from(chain(in_edges, out_edges))
                return new_node
            case BlockTemplate(), InstTemplate():
                out_edges = [(node, dst, prop) for _, dst, prop in cfg.out_edges(next_node, data=True)]
                cfg.remove_node(next_node)
                cfg.add_edges_from(out_edges)
                node.members.append(next_node)
                return node
            case BlockTemplate(), BlockTemplate():
                in_edges = [(src, next_node, prop) for src, _, prop in cfg.in_edges(node, data=True) if src not in next_node.members]
                out_edges = [(next_node, dst, prop) for _, dst, prop in cfg.out_edges(node, data=True) if prop['kind'] == EdgeKind.Exception]
                cfg.remove_node(node)
                cfg.add_edges_from(chain(in_edges, out_edges))
                next_node.members[:] = [*node.members, *next_node.members]
                return next_node
            case InstTemplate(), BlockTemplate():
                in_edges = [(src, next_node, prop) for src, _, prop in cfg.in_edges(node, data=True) if src not in next_node.members]
                out_edges = [(next_node, dst, prop) for _, dst, prop in cfg.out_edges(node, data=True) if prop['kind'] == EdgeKind.Exception]
                cfg.remove_node(node)
                cfg.add_edges_from(chain(in_edges, out_edges))
                next_node.members.insert(0, node)
                return next_node

Essentially, I violate the assumption made by BlockTemplate.try_match, that out[EdgeCategory.Exception] == exc for all members in the block, but only in the case where POP_BLOCK is followed by a RETURN_* or a JUMP_ABSOLUTE.

This results in passing the tests pop_block_return_value_in_if_else and pop_block_return_global_in_if_else, and it yields a different but incorrect output for pop_block_jump_in_for_loop.

The fix is far from correct or well-founded, but I hope it helps identify the root cause for these regressions.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions