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.