RPNXCortado
C++23 JVM class-file, bytecode, validation, and JAR toolkit
Loading...
Searching...
No Matches
Symbolic control flow and exception regions

code_builder represents destinations with owned labels rather than numeric byte offsets. This lets finalization change instruction widths and switch padding without invalidating branches, exception ranges, or debug tables.

Labels and loops

Create every label through the builder that will use it. A label may be referenced before it is bound, but it must be bound exactly once by the time the method is finalized. Labels from another builder lineage are rejected immediately.

The complete sumTo(int) method initializes two locals, loops while the index is at most the argument, and returns the accumulated sum:

rpnx::cortado::label const sum_entry = sum_to.new_label();
rpnx::cortado::label const sum_loop = sum_to.new_label();
rpnx::cortado::label const sum_done = sum_to.new_label();
rpnx::cortado::label const sum_end = sum_to.new_label();
sum_to.bind(sum_entry)
.append< rpnx::cortado::opcode::iconst_0 >()
.istore({1})
.append< rpnx::cortado::opcode::iconst_1 >()
.istore({2})
.bind(sum_loop)
.iload({2})
.iload({0})
.branch< rpnx::cortado::opcode::if_icmpgt >(sum_done)
.iload({1})
.iload({2})
.append< rpnx::cortado::opcode::iadd >()
.istore({1})
.iinc({2}, 1)
.branch< rpnx::cortado::opcode::goto_ >(sum_loop)
.bind(sum_done)
.iload({1})
.append< rpnx::cortado::opcode::ireturn >()
.bind(sum_end);
sum_to.add_line_number(sum_entry, 10)
.add_line_number(sum_loop, 12)
.add_line_number(sum_done, 16)
.add_local_variable(sum_entry, sum_end, "limit", "I", {0})
.add_local_variable(sum_entry, sum_end, "sum", "I", {1})
.add_local_variable(sum_entry, sum_end, "index", "I", {2});
static_cast< void >(builder.add_method(
"sumTo",
"(I)I",
rpnx::cortado::method_access_flags::is_public |
rpnx::cortado::method_access_flags::is_static,
sum_to));

The branch template accepts only the supported 16-bit symbolic branch opcodes. If a goto or jsr target is too distant, Cortado selects its wide form. If a conditional target is too distant, Cortado inverts the condition and inserts a goto_w. Client code continues to refer to the original label.

Compact local forms (iload_0 through iload_3), one-byte local operands, and wide local operands are similarly selected during construction. iinc selects the compact or wide immediate form from both its index and increment.

Dense and sparse switches

Use tableswitch when keys form a dense consecutive interval:

code.iload({0}).tableswitch(
default_case,
10,
{case_10, case_11, case_12});

The low value is the key for the first target; subsequent targets increment the key by one. The target vector must not be empty.

Use lookupswitch for sparse keys. Pairs must be strictly increasing:

rpnx::cortado::label const zero_case = classify.new_label();
rpnx::cortado::label const one_case = classify.new_label();
rpnx::cortado::label const other_case = classify.new_label();
classify.iload({0}).lookupswitch(
other_case,
{{0, zero_case}, {1, one_case}});
classify.bind(zero_case)
.ldc_string("zero")
.append< rpnx::cortado::opcode::areturn >();
classify.bind(one_case)
.ldc_string("one")
.append< rpnx::cortado::opcode::areturn >();
classify.bind(other_case)
.ldc_string("many")
.append< rpnx::cortado::opcode::areturn >();
static_cast< void >(builder.add_method(
"classify",
"(I)Ljava/lang/String;",
rpnx::cortado::method_access_flags::is_public |
rpnx::cortado::method_access_flags::is_static,
classify));

Cortado recalculates switch padding while branch widths stabilize. This matters because widening an earlier instruction can move the switch across a four-byte alignment boundary, which changes its own encoded size.

Exception handlers

Exception regions also use labels. Their protected interval is [start, end), and the handler label must identify an instruction. Omit the catch type to make a catch-all handler.

rpnx::cortado::label const protected_start = safe_divide.new_label();
rpnx::cortado::label const protected_end = safe_divide.new_label();
rpnx::cortado::label const arithmetic_handler = safe_divide.new_label();
safe_divide.bind(protected_start)
.iload({0})
.iload({1})
.append< rpnx::cortado::opcode::idiv >()
.append< rpnx::cortado::opcode::ireturn >()
.bind(protected_end)
.bind(arithmetic_handler)
.astore({2})
.append< rpnx::cortado::opcode::iconst_0 >()
.append< rpnx::cortado::opcode::ireturn >();
safe_divide.add_exception_handler(
protected_start,
protected_end,
arithmetic_handler,
"java/lang/ArithmeticException");
static_cast< void >(builder.add_method(
"safeDivide",
"(II)I",
rpnx::cortado::method_access_flags::is_public |
rpnx::cortado::method_access_flags::is_static,
safe_divide));

The JVM enters a handler with the thrown reference as the only operand-stack value. The example stores it in local slot two before returning a fallback. Automatic analysis includes handler entry states when computing method limits and stack-map frames.

Control-flow rules checked during finalization

Finalization rejects, among other errors:

  • foreign, unbound, or multiply bound labels;
  • empty tableswitch targets or unsorted lookupswitch pairs;
  • branches that cannot be represented even after widening;
  • exception intervals with empty or out-of-range protected regions;
  • operand-stack underflow;
  • merges with inconsistent operand-stack heights; and
  • reachable fallthrough beyond the end of the code array.

These checks cover structural assembly and stack height. JVM type-state merges and frame generation are described next in Verification, stack maps, and debug metadata.