-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Sanitise: Resolve free range indices when resolving associates #455
Conversation
88e014b
to
6874999
Compare
Documentation for this branch can be viewed at https://sites.ecmwf.int/docs/loki/455/index.html |
This is subtle, but to avoid false positives on the index-range matching, we need to terminate early if the symbol scope is not an association. However, to get this right, we still need to recurse over prior expression dimensions and type symbols.
Also slightly adjust the simply associate resolver test case to cover this basic use case.
6874999
to
c055d93
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #455 +/- ##
=======================================
Coverage 93.28% 93.29%
=======================================
Files 220 220
Lines 41224 41269 +45
=======================================
+ Hits 38457 38502 +45
Misses 2767 2767
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this! I have some questions about the recursion and potentially a missing test but not sure on either, so marking this as a comment only for now
expr_dims = self.rec(expr.dimensions, *args, **kwargs) | ||
|
||
# Recurse over the type's shape | ||
_type = expr.type | ||
if expr.type.shape: | ||
new_shape = self.rec(expr.type.shape, *args, **kwargs) | ||
_type = expr.type.clone(shape=new_shape) | ||
|
||
# Stop if scope is not an associate | ||
if not isinstance(expr.scope, ir.Associate): | ||
return expr.clone(dimensions=expr_dims, type=_type) | ||
|
||
new = self.map_scalar(expr, *args, **kwargs) | ||
|
||
# Recurse over array dimensions | ||
new_dims = self.rec(expr.dimensions, *args, **kwargs) | ||
if isinstance(new, sym.Array) and new.dimensions: | ||
# Resolve unbound range symbols form existing indices | ||
new_dims = self.rec(new.dimensions, *args, **kwargs) | ||
new_dims = self._match_range_indices(new_dims, expr_dims) | ||
else: | ||
new_dims = expr_dims | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I'm following the logic here. The first recursion to expr_dims seems to only be used if the array's scope isn't an associate - could we not move it into the conditional just before the return statement?
Or alternatively, in which situation do we expect the return value of the recursion on new.dimensions
to differ from the first recursion? Could we maybe use the previous recursion value here?
I suspect the second wouldn't work but the first should allow us to save some recursion maybe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yes, this was quite fiddly. The initial recursion is required for resolving associations in the dimensions of arrays that are not actually scope to the Associate
; that's also why it has to happen before the short-circuiting. For example, consider
associate(a => b)
some%obj(i, :, a) = ...
end associate
The second recursion is then done on the potentially replaced symbol (new
), which now might have entirely different .dimensions
. For example consider
associate(a => b(:, : i))
a = 42.0
! or even
a(j, k) = 66.6
end associate
In the first line of this example expr == 'a'
and expr.dimensions == None
and new.dimensions == (:, :, i)
, the latter of which we have not recursed into yet. In the second example line new.dimensions
then also needs to be matched against (j, k)
to yield b(j, k, i)
. Does that make sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EDIT: Scratch this, I'm seeing my error 🤦
Thanks, I suspected something along these lines.
My question was mostly on the grounds of avoiding redundant recursion, purely judged from the dependencies within this method. Something like this:
# Recurse over the type's shape
_type = expr.type
if _type.shape:
new_shape = self.rec(_type.shape, *args, **kwargs)
_type = expr.type.clone(shape=new_shape)
# Stop if scope is not an associate, but still recurse on the dimensions
# in case an associated symbol is in there
if not isinstance(expr.scope, ir.Associate):
expr_dims = self.rec(expr.dimensions, *args, **kwargs)
return expr.clone(dimensions=expr_dims, type=_type)
# For arrays that represent an associate symbol, re-use the
# scalar implementation to resolve the symbol
new = self.map_scalar(expr, *args, **kwargs)
# Recurse over array dimensions
if isinstance(new, sym.Array) and new.dimensions:
# Resolve unbound range symbols form existing indices
new_dims = self.rec(new.dimensions, *args, **kwargs)
new_dims = self._match_range_indices(new_dims, expr_dims)
else:
new_dims = expr_dims
return new.clone(dimensions=new_dims, type=_type)
This should save the need to do the recursion on the dimensions for symbols from the associate since you're recursing on the new_dims
anyway
associate (a => arr2d(:, 1), b=>arr2d(:, idx_a) ) | ||
b(:) = 42.0 | ||
do i=1, 5 | ||
a(i) = b(i+2) | ||
call another_routine(i, a(2:4), b) | ||
end do | ||
end associate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we also have a test that covers the reverse situation, where a dimension expression contains an associated symbol but the array is defined outside?
Something like:
real, intent(inout) :: arr2d(:,:)
associate(var_idx=>some%type%idx)
do i=1,5
arr2d(i, var_idx) = 1.0
end do
end associate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, agreed, good point! I've extended the array slicing test to cover this example.
When using dict-mapping to match symbols, the range keys might be `:`, which alias and mean we'd miss susequent `:` matches. The test has been updated accordingly.
Hi @reuterbal, thanks for the review and apologies for the delay. In addition to the things you flagged, I've independently found another bug in this, which I've now fixed and extended the slice array test for. This pertains to the matching of ':' when matching the free range symbols in the utility method of the mapper class. Please have another look. 🙏 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Many thanks for the additional testing and explanations. I was a bit slow but finally got the double recursion... (scratch my comment)
The current
Associate
resolution does not account for array index ranges if they are specific in the associate clauses (index remapping). This PR adds a partial resolution step toResolveAssociates
, which resolves free index ranges (:
) in association clause against explicit indices from the original expression, should should both exist. The respective changes in the expression mapper require some care with respect to early termination and recursion into.dimension
and.type
properties of the given array expression.Note: It does not resolve index shifts, where offsets are introduced by using bounded ranges (eg.
3:8
). Instead it warns about such (ab)use.