ExceptionRegion modeling issues and proposed improvements
Paul Sandoz
paul.sandoz at oracle.com
Tue Oct 8 21:21:56 UTC 2024
Hi Adam,
Thanks for looking at this complicated topic. Lifting up from bytecode makes this more challenging and was not originally factored into the design.
To better focus I think it would be helpful for now to place aside multi-catch. We chose to model for now using the LUB, and we can revisit later.
I am guessing you explicitly generated the bytecode example? although it is indicative of similar issues for bytecode generated from source?
- detach exception table entry declaration from the region entry
It’s not clear to me from this example why we need to detach the referencing of catch blocks from the exception region entry op.
Note that the exception.region op can already reference multiple catch blocks. That would likely significantly collapse the horrific number of blocks.
Will it always be the case that an exception.region.enter will dominate a corresponding exception.region.exit? It seems so, and if so an exit can reference an enter.
- allow exception.region.enter and exception.region.exit to enter resp. exit one or more exception table entries
Is it related to region exit and re-entry along some particular path of control where eventually all paths join to the region exit?
- behavioral change: an exception thrown causes to leave all levels exception regions (transition to a handler clears the actual exception stack). The change requires each exception handler to explicitly declare relevant exception regions re-entries.
Ok. I don’t yet fully understand this and the implications.
AFAICT another change in behavior is that a return from a method requires no preceding region exits (similar to throw).
Also with your proposal I don’t yet know the implications when lowering try/catch/finally blocks.
What about the following? (I am unsure of the cbranch in block_4)
func @"tryMethod" ()void -> {
// Reference multiple catch blocks
%r1 = exception.region.enter ^block_1 ^block_8 ^block_9 ^block_10;
^block_1:
%3 : int = constant @"0";
%4 : boolean = neq %3 %3;
cbranch %4 ^block_2 ^block_5;
^block_2:
// %r1 dominates this operation, so we can refer to it for the catch blocks
exception.region.exit %r1 ^block_3;
^block_3:
// Reenter region %r1, could be modeled as a distinct operation
// %r1 dominates this operation, so we can refer to it
// %r2 is unused, or should never be used because we defer to %r1
// Could be a no-op if region is already entered
%r2 = exception.region.enter %r1 ^block_4;
^block_4:
%5 : boolean = neq %3 %3;
// Did you intend that block_5 is referenced twice?
cbranch %5 ^block_5 ^block_5;
^block_5:
// This is the join point where %r1 is definitively exited
// %r1 dominates this operation, so we can refer to it for the catch blocks
// Error if at this point we are not in region %r1
exception.region.exit %r1 ^block_6;
^block_6:
// Re-reference multiple catch blocks
// Same order as %r1 but it is a distinct region w.r.t control flow
%r3 = exception.region.enter ^block_7 ^block_8 ^block_9 ^block_10;
^block_7:
// Implicit region exit for %r3
return;
^block_8(%6 : java.lang.NullPointerException):
// Implicit exception region exit for %r1 and %r3
throw %6;
^block_9(%7 : java.lang.RuntimeException):
// Implicit exception region exit for %r1 and %r3
throw %7;
^block_10(%8 : java.lang.Throwable):
// Implicit exception region exit for %r1 and %r3
throw %8;
};
be enter %r1
|
b1
/ \
| \
| b2 exit %r1
| |
| b3 enter %r1
| |
| b4
\ /
b5 exit %r1
|
b6 enter %r2
|
b7 exit %r2
Paul.
On Oct 8, 2024, at 5:28 AM, Adam Sotona <adam.sotona at oracle.com> wrote:
Hi,
Current exception regions model is weak in modeling nested try blocks, shared catch handlers and transitions between try blocks. Each of this complication multiplies complexity of the code model.
Following example consists of 9 bytecode instructions, wrapped in 3-level try catch, with two gaps and transitions between the try blocks:
- method name: tryMethod
flags: [STATIC]
method type: ()V
attributes: [Code]
code:
max stack: 1
max locals: 0
0: {opcode: ICONST_0, constant value: 0}
1: {opcode: IFEQ, target: 8}
4: {opcode: ICONST_0, constant value: 0}
5: {opcode: IFEQ, target: 9}
8: {opcode: NOP}
9: {opcode: RETURN}
10: {opcode: ATHROW}
11: {opcode: ATHROW}
12: {opcode: ATHROW}
exception handlers:
handler 1: {start: 0, end: 4, handler: 10, type: java/lang/NullPointerException}
handler 2: {start: 4, end: 8, handler: 10, type: java/lang/NullPointerException}
handler 3: {start: 8, end: 10, handler: 10, type: java/lang/NullPointerException}
handler 4: {start: 0, end: 4, handler: 11, type: java/lang/RuntimeException}
handler 5: {start: 4, end: 8, handler: 11, type: java/lang/RuntimeException}
handler 6: {start: 8, end: 10, handler: 11, type: java/lang/RuntimeException}
handler 7: {start: 0, end: 4, handler: 12, type: java/lang/Throwable}
handler 8: {start: 4, end: 8, handler: 12, type: java/lang/Throwable}
handler 9: {start: 8, end: 10, handler: 12, type: java/lang/Throwable}
When we try to lift the above bytecode, we get following 47 blocks of model:
func @"tryMethod" ()void -> {
%0 : java.lang.reflect.code.op.CoreOp$ExceptionRegion = exception.region.enter ^block_1 ^block_46;
^block_1:
%1 : java.lang.reflect.code.op.CoreOp$ExceptionRegion = exception.region.enter ^block_2 ^block_44;
^block_2:
%2 : java.lang.reflect.code.op.CoreOp$ExceptionRegion = exception.region.enter ^block_3 ^block_41;
^block_3:
%3 : int = constant @"0";
%4 : boolean = neq %3 %3;
cbranch %4 ^block_4 ^block_21;
^block_4:
exception.region.exit %2 ^block_5;
^block_5:
exception.region.exit %1 ^block_6;
^block_6:
exception.region.exit %0 ^block_7;
^block_7:
%5 : java.lang.reflect.code.op.CoreOp$ExceptionRegion = exception.region.enter ^block_8 ^block_46;
.
. (skipped due to mailing list size limit)
.
^block_41(%24 : java.lang.NullPointerException):
exception.region.exit %1 ^block_42(%24);
^block_42(%25 : java.lang.NullPointerException):
exception.region.exit %0 ^block_43(%25);
^block_43(%26 : java.lang.NullPointerException):
throw %26;
^block_44(%27 : java.lang.RuntimeException):
exception.region.exit %0 ^block_45(%27);
^block_45(%28 : java.lang.RuntimeException):
throw %28;
^block_46(%29 : java.lang.Throwable):
throw %29;
};
My first proposal consists of:
- detach exception table entry declaration from the region entry
- allow exception.region.enter and exception.region.exit to enter resp. exit one or more exception table entries
- behavioral change: an exception thrown causes to leave all levels exception regions (transition to a handler clears the actual exception stack). The change requires each exception handler to explicitly declare relevant exception regions re-entries.
The above example model will significantly simplify and may be easily optimized even more:
func @"tryMethod" ()void -> {
%0 : java.lang.reflect.code.op.CoreOp$ExceptionRegion = exception.region ^block_8;
%1 : java.lang.reflect.code.op.CoreOp$ExceptionRegion = exception.region ^block_9;
%2 : java.lang.reflect.code.op.CoreOp$ExceptionRegion = exception.region ^block_10;
exception.region.enter %0 %1 %2 ^block_1;
^block_1:
%3 : int = constant @"0";
%4 : boolean = neq %3 %3;
cbranch %4 ^block_2 ^block_5;
^block_2:
exception.region.exit %0 %1 %2 ^block_3;
^block_3:
exception.region.enter %0 %1 %2 ^block_4;
^block_4:
%5 : boolean = neq %3 %3;
cbranch %5 ^block_5 ^block_5;
^block_5:
exception.region.exit %0 %1 %2 ^block_6;
^block_6:
exception.region.enter %0 %1 %2 ^block_7;
^block_7:
return;
^block_8(%6 : java.lang.NullPointerException):
throw %6;
^block_9(%7 : java.lang.RuntimeException):
throw %7;
^block_10(%8 : java.lang.Throwable):
throw %8;
};
My second concern and proposal related to inability to model multi-catch try blocks. Following code:
@CodeReflection
static void multicatch() {
try {
System.out.println("do something");
} catch (NullPointerException | IllegalArgumentException e) {
throw e;
} catch (RuntimeException e) {
return;
}
}
is lowered to:
func @"multicatch" ()void -> {
%0 : java.lang.reflect.code.op.CoreOp$ExceptionRegion = exception.region.enter ^block_1 ^block_4 ^block_5;
^block_1:
%1 : java.io.PrintStream = field.load @"java.lang.System::out()java.io.PrintStream";
%2 : java.lang.String = constant @"do something";
invoke %1 %2 @"java.io.PrintStream::println(java.lang.String)void";
branch ^block_2;
^block_2:
exception.region.exit %0 ^block_3;
^block_3:
return;
^block_4(%3 : java.lang.RuntimeException):
%4 : Var<java.lang.RuntimeException> = var %3 @"e";
%5 : java.lang.RuntimeException = var.load %4;
throw %5;
^block_5(%6 : java.lang.RuntimeException):
%7 : Var<java.lang.RuntimeException> = var %6 @"e";
return;
};
Which is not correct and information about exact catch type is lost.
I propose to add specific catch type to the exception table entry declaration, so the model will change to:
func @"multicatch" ()void -> {
%0 : java.lang.reflect.code.op.CoreOp$ExceptionRegion<java.lang.NullPointerException> = exception.region ^block_4;
%1 : java.lang.reflect.code.op.CoreOp$ExceptionRegion<java.lang.IllegalArgumentException> = exception.region ^block_4;
%2 : java.lang.reflect.code.op.CoreOp$ExceptionRegion<java.lang.RuntimeException> = exception.region ^block_5;
exception.region.enter %0 %1 %2 ^block_1;
^block_1:
%3 : java.io.PrintStream = field.load @"java.lang.System::out()java.io.PrintStream";
%4 : java.lang.String = constant @"do something";
invoke %3 %4 @"java.io.PrintStream::println(java.lang.String)void";
branch ^block_2;
^block_2:
exception.region.exit %0 %1 %2 ^block_3;
^block_3:
return;
^block_4(%3 : java.lang.RuntimeException):
%4 : Var<java.lang.RuntimeException> = var %3 @"e";
%5 : java.lang.RuntimeException = var.load %4;
throw %5;
^block_5(%6 : java.lang.RuntimeException):
%7 : Var<java.lang.RuntimeException> = var %6 @"e";
return;
};
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/babylon-dev/attachments/20241008/79d64532/attachment-0001.htm>
More information about the babylon-dev
mailing list