I’d like to raise again a topic that we haven’t really solved or addressed which is related to Undefined Behavior in MLIR, and its potential relationship to the notion of “side effects”.
Recap
As a recap, right now MLIR offers a fairly rich interface mechanism to model “side effects” through an extensible concept of “resource”, “effect”, and an OpInterface to describe what kind of “effects” an operation has on a particular “resource”.
This is used in particular to model the memory effects, which is the common thing many people think about when we refer to “side effects”.
This brings me to the notion of “NoSideEffect”, which is current defined as def NoSideEffect : MemoryEffects<[]>;
– basically the absence of any MemoryEffects! This seems (unfortunately?) orthogonal to non-memory side effects.
Before we had interfaces, we were making more uses of “traits” on operation, and NoSideEffect
was a trait.
Depending on the place, this was documented as:
/// This bit is set for operations that have no side effects: that means that
/// they do not read or write memory, or access any hidden state.
/// This trait indicates that an operation never has side effects.
This trait signifies that the operation is pure and has no visible side effects.
The last one in particular refers to “pure” and has been known to bring confusion with respect to how to grasp with Undefined Behavior.
We had a previous (long) discussion about how to handle operations with some preconditions, in particular if an op is considered exhibiting Undefined Behavior when these preconditions aren’t met.
How to move forward?
One possible vision is to considered that “undefined behavior” is a side-effect (in fact Sanjoy suggested in the first answer in the thread above that UB “is every possible side effect”).
On the other hand, this definition is unfortunate: it does not allow to model the side-effects of an operation in a program without undefined behavior.
I’d also bring up that if we go back to first principles, according to Wikipedia, side effect is (emphasis of mine) an “observable effect besides returning a value (the intended effect) to the invoker of the operation”. However by definition Undefined Behavior can’t be “observable”.
One practical issue (that is shared with LLLVM) is how to handle speculation:
int div(int cond, int x, int y)
if (cond)
return x / y;
return 0;
}
In this function, we can’t do
int div(int cond, int x, int y)
// x / y is undefined if y == 0 or x == INT_MIN and y == -1
int div = x / y;
if (cond) return div;
return 0;
}
I suspect we don’t want something like integer division to be considered as “having side-effect” because it might exhibit undefined behavior.
In the previous thread, I referred to LLVM handling of this: there is a specific handling of the ability to speculate an operation with llvm::isSafeToSpeculativelyExecute.
I’d favor to try to look in this direction instead. One simple way would be to bring back a trait (SafeToSpeculate
), but that might be a bit too much all-or-nothing of a flag. So an alternative would be to use an interface with a query that is more contextual, as a strawman it could look like:
// Return true is the current operation can be safely speculated to the given position.
// The provided iterator must dominate the current position for the operation.
bool isSafeToSpeculate(Block::iterator position);
This would allow operation to implement some more complex logic to determine when it is / isn’t safe to speculate.
I’m interested to hear about alternative modeling or other approaches that have been employed to tame UB situations in MLIR?