Yes absolutely.
Allow me some historical digression. scf.for originated in Linalg and circa July 2019 came the time to refactor because the concept was more generally reusable (e.g. for mapping to GPU, having a common abstraction on the way to unstructured std basic blocks + branches, representing non-affine control-flow and for future control-flow / loop types). We went through the traditional discussion of “should we just have a std.for and std.if” or a new dialect.
A bunch of digressions and renamings later, scf is now well-established, it is indeed time to start looking at more general (but still structured) control-flow.
@aartbik I suspect you are interested in such generalizations in the context of the ongoing sparse work.
One key question for me is: what transformations will be interesting in this context (esp. for vectorization + LICM) and how does the representation help or hinder the transformation?
I’ll just paste content from the original proposal that didn’t make it publicly back then. It is largely unchanged except for replacing the original cf dialect name by scf. All this of course begs to be modernized and tested against real use cases.
Loops
The values for the lower bound, upper bound and step are precomputed when entering the scf.for operation. Alternatively we considered attaching regions to scf.for that would compute the upper bound and the step at each iteration. We do not have a concrete transformation that would warrant the extra complexity at this time. This may evolve in the future.
A generalization of the scf.for loop may resemble
scf.for %i : f32 = %init // an induction variable of any type is declared
condition { // it is available in the condition block
%0 = call @some_func(%i) : (f32) -> i1 // arbitrary computation of the condition
cf.cond %0 : i1 // cf.cond tells cf.for about the value of the condition
}
update { // the induction variable is available in this block
%cst = constant 0.05 : f32 // arbitrary computation of the new value of the
%1 = addf %i, %cst // induction variable
cf.next %1 : f32 // cf.next tells cf.for about the new value
}
body { // the induction variable is available in the body
"use"(%i) : (f32) -> () // but cannot be changed (SSA)
}
Note that such a generalization would also have the benefit of keeping perfectly nested loop ops structure when performing e.g. tiling. To simplify analyses, an attribute could be added to specify whether condition and increment (1) can be computed only once on entry or (2) need to be updated iteratively.
A more general scf.while loop may be created along the following lines
// A set of values are declared in the "while" construct.
// They are assigned some initial value and are available in the regions below (entry block arguments).
scf.while(%v1 = %init1 : index, %v2 = %init2 : memref<?xf32>) {
// The condition block has arbitrary computation and terminates with "scf.cond" that
// consumes a value of i1 type telling whether to continue the loop.
%0 = "condition"(%v1, %v2) : (index, memref<?xf32>) -> i1
scf.cond %0 : i1
} do {
// The body block has arbitrary computation and terminates with "scf.next", the
// operands of which will be used as values of the loop-defined values in the next
// iteration.
store %v2[%v1] : memref<?xf32>
%cst = constant 2 : index
%1 = addi %v1, %cst : index
scf.next %1, %v2 : index, memref<?xf32>
}
Thanks for bringing up this topic!