ecmwf-ifs / loki

Freely programmable source-to-source translation for Fortran

Home Page:https://sites.ecmwf.int/docs/loki/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Hacky or... bad design for ExpressionFinder ?

meteospike opened this issue · comments

The class ExpressionFinder inherits from Visitor in module loki.expression.expr_visitors.

The initialization method is doing something a bit weird, qualified in a comment as "pretty hacky" : setting a static method (retrieval_function) provided by a dummy argument. Moreover, this argument is in fact built on the fly in the initialization methods of the derived classes such as FindTypedSymbols, FindVariables, etc., then calling super.__init__ with this argument, which will be attached as a static method to the parent class !

One may consider that everything is fine as long as we proceed like this :

FindTypedSymbols().visit(item.ir)
FindVariables(unique=False).visit(item.ir)

But as soon as you want to "freeze" your finder (for example to delegate work to an other entity or reuse it several times), things are going wrong:

find_ts = FindTypedSymbols()
find_vars = FindVariables(unique=False)
find_ts.visit(item.ir)

The find_ts will in fact use the retrieval function defined by the initialization method of FindVariables.
At least, even if we want to keep this kind of design for the initialization of derived classes, the retrieval function should be attached to the instance and then called by the retrieve method. Do I miss something ?

Thanks for reporting this. I agree that they way the retrieval function is stored seems odd and certainly mandates an overhaul. However, I haven't been able to reproduce the behaviour you described, the following test works correctly for me:

@pytest.mark.parametrize('frontend', available_frontends())
def test_expression_finder_retrieval_function(frontend):
    """
    Verify that expression finder visitors work as intended and remain
    functional if re-used
    """
    fcode = """
module some_mod
    implicit none
contains
    function some_func() result(ret)
        integer, intent(out) :: ret
        ret = 1
    end function some_func

    subroutine other_routine
        integer :: var, tmp
        var = 5 + some_func()
    end subroutine other_routine
end module some_mod
    """.strip()

    source = Sourcefile.from_source(fcode, frontend=frontend)

    expected_ts = {'var', 'some_func'}
    expected_vars = ('var',)

    # Instantiate the first expression finder and make sure it works as expected
    find_ts = FindTypedSymbols()
    assert find_ts.visit(source['other_routine'].body) == expected_ts

    # Verify that it works also on a repeated invocation
    assert find_ts.visit(source['other_routine'].body) == expected_ts

    # Instantiate the second expression finder and make sure it works as expected
    find_vars = FindVariables(unique=False)
    assert find_vars.visit(source['other_routine'].body) == expected_vars

    # Make sure the first expression finder still works
    assert find_ts.visit(source['other_routine'].body) == expected_ts

Would you be able to provide a complete MFE?

Sorry, I realized too late that the previous case is ok because the retrieval function is attached to the actual class and note the parent class. Nevertheless, the trouble could occur if actual arguments to a same finder are changed from one call to another, such as in the example below:

@pytest.mark.parametrize('frontend', available_frontends())
def test_expression_finder_retrieval_function(frontend):
    """
    Verify that expression finder visitors work as intended and remain
    functional if re-used
    """
    fcode = """
module some_mod
    implicit none
    type some_type
        integer :: ifoo
        real :: zfoo
    end type some_type
contains
    function some_func(ptmp) result(ret)
        type(some_type), intent(in) :: ptmp
        integer, intent(out) :: ret
        ret = tmp%ifoo * 3
    end function some_func

    subroutine other_routine
        integer :: var
        type(some_type) :: tmp
        tmp%ifoo = 2
        var = 5 + some_func(tmp)
    end subroutine other_routine
end module some_mod
    """.strip()

    source = Sourcefile.from_source(fcode, frontend=frontend)

    expected_v1 = ('tmp', 'tmp%ifoo', 'var', 'tmp')
    expected_v2 = ('tmp%ifoo', 'var', 'tmp')

    # Instantiate the first expression finder and make sure it works as expected
    find_v1 = FindVariables(unique=False)
    assert find_v1.visit(source['other_routine'].body) == expected_v1

    # Instantiate the second expression finder and make sure it works as expected
    find_v2 = FindVariables(unique=False, recurse_to_parent=False)
    assert find_v2.visit(source['other_routine'].body) == expected_v2

    # Make sure the first expression finder still works
    assert find_v1.visit(source['other_routine'].body) == expected_v1

Thanks, you're right, I should have figured that out myself!

I can confirm that I can reproduce the behaviour now.