Michael and I were chasing an obscure bug the last two days that produced incorrect results in certain side exit conditions when the guard elimination pass was enabled. We ended up changing the side exit code to always re-execute any conditional branch that causes a side exit. This was already the case for implicit conditional checks that are the result of i.e. an array load instruction. If the null check or range check guards fail the instruction is re-executed by the interpreter. For conditional branches we previously simply side exited to the target of the conditonal branch. Instead we now always re-executed the conditional branch in question just as we do for loads and other instructions that have an implied check (i.e. null check).
This is necessary to ensure proper semantics in case we are side-exiting on an optimized guard instruction, i.e. one where we chose a tighter condition that side exits at a condition that might not be given at this guard yet but would be checked further downstream in the trace. A common example is a loop where the loop condition is checked at the end:
a[i++] = ...;
a[i++] = ...;
if i < condition end;
We transform this code to only contain a single guard that will side exit on the first array exit. The interpreter will then execute the remainder of the iteration and exit the loop at the loop condition check. If the array exit were a conditional branch, we previously would have followed it to its target after the side exit, which obviously is wrong, because it still would evaluate to not taken just as the array access here doesn’t throw an exception. We only left the loop premature at this point. By re-executing the instruction we side exit on this issue is resolved and the proper path will be chosen by the interpreter.
Michael also fixed a related problem in the trace recorder. If we exit on a condition prematurely and try to record a new trace, we must not do so if the condition actually evaluates to the same branch direction as the one we just side-exited from (and already recorded previously). New secondary traces are only added if actually a different path is executed.
This also required an implementation fix in the trace recorder, which uses an exception to abort trace recording. If such an abort condition was recognized while re-executing the instruction an exception was thrown after the stack was manipulated by removing the arguments of the conditional branch from the stack. The exception eventually returned execution to the non-recording interpreter, which tried to re-execute the branch and received the wrong operands (which already had been popped of the stack by the recording interpreter).