1 /*
   2  * Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 import jdk.incubator.code.*;
  25 import jdk.incubator.code.dialect.core.*;
  26 import jdk.incubator.code.dialect.java.*;
  27 
  28 import java.lang.classfile.Attributes;
  29 import java.lang.classfile.ClassFile;
  30 import java.lang.classfile.ClassModel;
  31 import java.lang.classfile.CodeElement;
  32 import java.lang.classfile.CodeModel;
  33 import java.lang.classfile.Instruction;
  34 import java.lang.classfile.Label;
  35 import java.lang.classfile.MethodModel;
  36 import java.lang.classfile.Opcode;
  37 import java.lang.classfile.PseudoInstruction;
  38 import java.lang.classfile.TypeKind;
  39 import java.lang.classfile.attribute.CodeAttribute;
  40 import java.lang.classfile.attribute.StackMapFrameInfo;
  41 import java.lang.classfile.instruction.*;
  42 import java.lang.constant.ClassDesc;
  43 import java.lang.constant.ConstantDesc;
  44 import java.lang.constant.ConstantDescs;
  45 import java.lang.constant.DirectMethodHandleDesc;
  46 import java.lang.constant.DynamicConstantDesc;
  47 import java.lang.constant.MethodTypeDesc;
  48 import java.lang.invoke.CallSite;
  49 import java.lang.invoke.MethodHandle;
  50 import java.lang.reflect.AccessFlag;
  51 import java.util.ArrayDeque;
  52 import java.util.ArrayList;
  53 import java.util.Arrays;
  54 import java.util.Collections;
  55 import java.util.Deque;
  56 import java.util.function.ToIntFunction;
  57 import java.util.HashMap;
  58 import java.util.IdentityHashMap;
  59 import java.util.LinkedHashMap;
  60 import java.util.List;
  61 import java.util.Map;
  62 import java.util.stream.Collectors;
  63 import java.util.stream.IntStream;
  64 import java.util.stream.Stream;
  65 
  66 import static java.lang.classfile.attribute.StackMapFrameInfo.SimpleVerificationTypeInfo.*;
  67 import static jdk.incubator.code.dialect.core.CoreOp.constant;
  68 import static jdk.incubator.code.dialect.java.JavaType.type;
  69 
  70 public final class BytecodeLift {
  71 
  72     private static final ClassDesc CD_LambdaMetafactory = ClassDesc.ofDescriptor("Ljava/lang/invoke/LambdaMetafactory;");
  73     private static final ClassDesc CD_StringConcatFactory = ClassDesc.ofDescriptor("Ljava/lang/invoke/StringConcatFactory;");
  74     private static final JavaType MHS_LOOKUP = JavaType.type(ConstantDescs.CD_MethodHandles_Lookup);
  75     private static final JavaType MH = JavaType.type(ConstantDescs.CD_MethodHandle);
  76     private static final JavaType MT = JavaType.type(ConstantDescs.CD_MethodType);
  77     private static final JavaType CLASS_ARRAY = JavaType.array(JavaType.J_L_CLASS);
  78     private static final MethodRef LOOKUP = MethodRef.method(JavaType.type(ConstantDescs.CD_MethodHandles), "lookup", MHS_LOOKUP);
  79     private static final MethodRef FIND_STATIC = MethodRef.method(MHS_LOOKUP, "findStatic", MH, JavaType.J_L_CLASS, JavaType.J_L_STRING, MT);
  80     private static final MethodRef FIND_VIRTUAL = MethodRef.method(MHS_LOOKUP, "findVirtual", MH, JavaType.J_L_CLASS, JavaType.J_L_STRING, MT);
  81     private static final MethodRef FIND_CONSTRUCTOR = MethodRef.method(MHS_LOOKUP, "findConstructor", MH, JavaType.J_L_CLASS, MT);
  82     private static final MethodRef FIND_GETTER = MethodRef.method(MHS_LOOKUP, "findGetter", MH, JavaType.J_L_CLASS, JavaType.J_L_STRING, JavaType.J_L_CLASS);
  83     private static final MethodRef FIND_STATIC_GETTER = MethodRef.method(MHS_LOOKUP, "findStaticGetter", MH, JavaType.J_L_CLASS, JavaType.J_L_STRING, JavaType.J_L_CLASS);
  84     private static final MethodRef FIND_SETTER = MethodRef.method(MHS_LOOKUP, "findSetter", MH, JavaType.J_L_CLASS, JavaType.J_L_STRING, JavaType.J_L_CLASS);
  85     private static final MethodRef FIND_STATIC_SETTER = MethodRef.method(MHS_LOOKUP, "findStaticSetter", MH, JavaType.J_L_CLASS, JavaType.J_L_STRING, JavaType.J_L_CLASS);
  86     private static final MethodRef METHOD_TYPE_0 = MethodRef.method(MT, "methodType", MT, JavaType.J_L_CLASS);
  87     private static final MethodRef METHOD_TYPE_1 = MethodRef.method(MT, "methodType", MT, JavaType.J_L_CLASS, JavaType.J_L_CLASS);
  88     private static final MethodRef METHOD_TYPE_L = MethodRef.method(MT, "methodType", MT, JavaType.J_L_CLASS, CLASS_ARRAY);
  89     private static final MethodRef COMPARE_LONG = MethodRef.method(Long.class, "compare", int.class, long.class, long.class);
  90     private static final MethodRef COMPARE_FLOAT = MethodRef.method(Float.class, "compare", int.class, float.class, float.class);
  91     private static final MethodRef COMPARE_DOUBLE = MethodRef.method(Double.class, "compare", int.class, double.class, double.class);
  92 
  93     private final Block.Builder entryBlock;
  94     private final List<Value> initialValues;
  95     private final ClassModel classModel;
  96     // Cached blocks that enter a handler from a region stack
  97     private final Map<CatchTargetKey, Block.Builder> exceptionHandlerBlocks;
  98     // Cached blocks that enter regions at a bytecode index
  99     private final Map<Integer, Block.Builder> labelEntryBlocks;
 100     // Active region stacks at bytecode positions where they change
 101     private final Map<Integer, List<ExceptionRegion>> exceptionRegionsMap;
 102     // Entered region results recorded for each block
 103     private final Map<Block.Builder, List<Op.Result>> enteredRegionStacks;
 104     // Region owned by each enter op result
 105     private final Map<Op.Result, ExceptionRegion> enteredRegionMap;
 106     // Stack map blocks keyed by bytecode index
 107     private final Map<Integer, Block.Builder> blockMap;
 108     private final List<CodeElement> elements;
 109     private final Deque<Value> stack;
 110     private final Deque<ClassDesc> newStack;
 111     // Bytecode index for each exception table handler
 112     private final List<Integer> handlerBcis;
 113     private final List<CodeType> catchTypes;
 114     // Converts classfile labels to bytecode indexes
 115     private final ToIntFunction<Label> label2Bci;
 116     // Current entered region result stack
 117     private List<Op.Result> actualEreStack;
 118     private Block.Builder currentBlock;
 119 
 120     private BytecodeLift(Block.Builder entryBlock, ClassModel classModel, CodeModel codeModel, Value... capturedValues) {
 121         this.entryBlock = entryBlock;
 122         this.initialValues = Stream.concat(Stream.of(capturedValues), entryBlock.parameters().stream()).toList();
 123         this.currentBlock = entryBlock;
 124         this.classModel = classModel;
 125         this.exceptionHandlerBlocks = new HashMap<>();
 126         this.labelEntryBlocks = new HashMap<>();
 127         this.enteredRegionStacks = new IdentityHashMap<>();
 128         this.enteredRegionMap = new IdentityHashMap<>();
 129         this.actualEreStack = List.of();
 130         this.newStack = new ArrayDeque<>();
 131         this.label2Bci = ((CodeAttribute)codeModel)::labelToBci;
 132         this.stack = new ArrayDeque<>();
 133         this.blockMap = codeModel.findAttribute(Attributes.stackMapTable()).map(sma ->
 134                 sma.entries().stream().collect(Collectors.toUnmodifiableMap(
 135                         smfi -> label2Bci.applyAsInt(smfi.target()),
 136                         smfi -> entryBlock.block(toBlockParams(smfi.stack()))))).orElseGet(Map::of);
 137         this.handlerBcis = new ArrayList<>();
 138         this.catchTypes = new ArrayList<>();
 139         record RegionKey(int start, int end) {}
 140         Map<RegionKey, List<Integer>> grouped = new LinkedHashMap<>();
 141         for (ExceptionCatch ec : codeModel.exceptionHandlers()) {
 142             int handler = handlerBcis.size();
 143             handlerBcis.add(label2Bci.applyAsInt(ec.handler()));
 144             catchTypes.add(ec.catchType().map(ct -> JavaType.type(ct.asSymbol())).orElse(JavaType.VOID));
 145             grouped.computeIfAbsent(new RegionKey(label2Bci.applyAsInt(ec.tryStart()), label2Bci.applyAsInt(ec.tryEnd())), _ -> new ArrayList<>())
 146                     .add(handler);
 147         }
 148         List<ExceptionRegion> regions = new ArrayList<>();
 149         for (var c : grouped.entrySet()) {
 150             regions.add(new ExceptionRegion(regions.size(), c.getKey().start(), c.getKey().end(), c.getValue()));
 151         }
 152         this.exceptionRegionsMap = new HashMap<>();
 153         List<ExceptionRegion> previous = List.of();
 154         List<CodeElement> elements = codeModel.elementList();
 155         for (CodeElement e : elements) {
 156             if (e instanceof LabelTarget lt) {
 157                 int bci = label2Bci.applyAsInt(lt.label());
 158                 List<ExceptionRegion> next = regions.stream()
 159                         .filter(er -> er.start() <= bci && bci < er.end())
 160                         .sorted()
 161                         .toList();
 162                 if (!next.equals(previous) || blockMap.containsKey(bci)) {
 163                     exceptionRegionsMap.put(bci, next);
 164                 }
 165                 previous = next;
 166             }
 167         }
 168         // filter out pseudo instructions that may interfere with expected instructions sequence
 169         this.elements = elements.stream().filter(e ->
 170                 e instanceof Instruction || e instanceof LabelTarget lt
 171                         && exceptionRegionsMap.containsKey(label2Bci.applyAsInt(lt.label()))).toList();
 172     }
 173 
 174     // One bytecode try range and its exception table handler indexes
 175     record ExceptionRegion(int index, int start, int end, List<Integer> handlers) implements Comparable<ExceptionRegion> {
 176 
 177         // Sort outer regions before inner regions
 178         @Override
 179         public int compareTo(ExceptionRegion o) {
 180             int c = Integer.compare(start, o.start);
 181             if (c != 0) return c;
 182             c = Integer.compare(o.end, end);
 183             return c != 0 ? c : Integer.compare(index, o.index);
 184         }
 185     }
 186 
 187     // Cache key for a handler reached from a concrete region stack
 188     record CatchTargetKey(int handler, List<Op.Result> enteredRegions, List<Map.Entry<Integer, CodeType>> handlers) {}
 189 
 190     // Handlers block references with related catch types
 191     record CatchEntries(List<CodeType> types, List<Block.Reference> references) {}
 192 
 193     private List<CodeType> toBlockParams(List<StackMapFrameInfo.VerificationTypeInfo> vtis) {
 194         ArrayList<CodeType> params = new ArrayList<>(vtis.size());
 195         for (int i = vtis.size() - 1; i >= 0; i--) {
 196             var vti = vtis.get(i);
 197             switch (vti) {
 198                 case INTEGER -> params.add(UnresolvedType.unresolvedInt());
 199                 case FLOAT -> params.add(JavaType.FLOAT);
 200                 case DOUBLE -> params.add(JavaType.DOUBLE);
 201                 case LONG -> params.add(JavaType.LONG);
 202                 case NULL -> params.add(UnresolvedType.unresolvedRef());
 203                 case UNINITIALIZED_THIS ->
 204                     params.add(JavaType.type(classModel.thisClass().asSymbol()));
 205                 case StackMapFrameInfo.ObjectVerificationTypeInfo ovti ->
 206                     params.add(JavaType.type(ovti.classSymbol()));
 207 
 208                     // Unitialized entry (a new object before its constructor is called)
 209                     // must be skipped from block parameters because they do not exist in code reflection model
 210                 case StackMapFrameInfo.UninitializedVerificationTypeInfo _ -> {}
 211                 default ->
 212                     throw new IllegalArgumentException("Unexpected VTI: " + vti);
 213             }
 214         }
 215         return params;
 216     }
 217 
 218     private Op.Result op(Op op) {
 219         return currentBlock.add(op);
 220     }
 221 
 222     // Lift to core dialect
 223     public static CoreOp.FuncOp lift(byte[] classdata, String methodName) {
 224         return lift(classdata, methodName, null);
 225     }
 226 
 227     public static CoreOp.FuncOp lift(byte[] classdata, String methodName, MethodTypeDesc methodType) {
 228         return lift(ClassFile.of(
 229                 ClassFile.DebugElementsOption.DROP_DEBUG,
 230                 ClassFile.LineNumbersOption.DROP_LINE_NUMBERS).parse(classdata).methods().stream()
 231                         .filter(mm -> mm.methodName().equalsString(methodName) && (methodType == null || mm.methodTypeSymbol().equals(methodType)))
 232                         .findFirst().orElseThrow(() -> new IllegalArgumentException("Unknown method: " + methodName)));
 233     }
 234 
 235     public static CoreOp.FuncOp lift(MethodModel methodModel) {
 236         ClassModel classModel = methodModel.parent().orElseThrow();
 237         MethodTypeDesc mDesc = methodModel.methodTypeSymbol();
 238         if (!methodModel.flags().has(AccessFlag.STATIC)) {
 239             mDesc = mDesc.insertParameterTypes(0, classModel.thisClass().asSymbol());
 240         }
 241         return NormalizeExceptionRegionsTransformer.transform(
 242                 NormalizeBlocksTransformer.transform(
 243                     UnresolvedTypesTransformer.transform(
 244                         SlotToVarTransformer.transform(
 245                             CoreOp.func(methodModel.methodName().stringValue(),
 246                                         MethodRef.ofNominalDescriptor(mDesc)).body(entryBlock ->
 247                                                 new BytecodeLift(entryBlock,
 248                                                                  classModel,
 249                                                                  methodModel.code().orElseThrow()).liftBody())))));
 250     }
 251 
 252     private void liftBody() {
 253         // store entry block
 254         int slot = 0;
 255         for (var ep : initialValues) {
 256             op(SlotOp.store(slot, ep));
 257             slot += ep.type().equals(JavaType.LONG) || ep.type().equals(JavaType.DOUBLE) ? 2 : 1;
 258         }
 259 
 260         for (int i = 0; i < elements.size(); i++) {
 261             CodeElement element = elements.get(i);
 262             Opcode numericCompare = null;
 263             if (element instanceof OperatorInstruction inst
 264                     && switch (inst.opcode()) {
 265                         case LCMP, FCMPL, FCMPG, DCMPL, DCMPG -> true;
 266                         default -> false;}
 267                     && i + 1 < elements.size()
 268                     && (elements.get(i + 1) instanceof BranchInstruction branch)
 269                     && switch (branch.opcode()) {
 270                         case IFEQ, IFNE, IFLT, IFLE, IFGT, IFGE -> true;
 271                         default -> false;
 272                     }) {
 273                 // pre-scan if a numeric compare preceeds conditional branch
 274                 numericCompare = inst.opcode();
 275                 element = elements.get(++i);
 276             }
 277             switch (element) {
 278                 case ExceptionCatch _ -> {
 279                     // Exception blocks are inserted by label target (below)
 280                 }
 281                 case LabelTarget lt -> {
 282                     int bci = label2Bci.applyAsInt(lt.label());
 283                     List<ExceptionRegion> newEreStack = exceptionRegionsMap.get(bci);
 284                     if (newEreStack != null) {
 285                         Block.Builder target = blockMap.get(bci);
 286                         if (target != null) {
 287                             if (currentBlock != null) {
 288                                 // Transition to a branch target or a handler
 289                                 exitRegions(actualEreStack, currentBlock, targetEntryBlock(bci), stackValues(target));
 290                             }
 291                             actualEreStack = enteredRegionStacks.getOrDefault(target, List.of());
 292                             currentBlock = target;
 293                             stack.clear();
 294                             stack.addAll(target.parameters());
 295                         } else if (currentBlock != null && !actualEreStack.stream().map(enteredRegionMap::get).toList().equals(newEreStack)) {
 296                             // Transition to a block with a different ERE stack
 297                             Block.Builder next = entryBlock.block();
 298                             actualEreStack = ereTransit(newEreStack, next);
 299                             currentBlock = next;
 300                         }
 301                     }
 302                 }
 303                 case BranchInstruction inst when isUnconditionalBranch(inst.opcode()) -> {
 304                     int targetBci = label2Bci.applyAsInt(inst.target());
 305                     Block.Builder target = blockMap.get(targetBci);
 306                     exitRegions(actualEreStack, currentBlock, targetEntryBlock(targetBci), stackValues(target));
 307                     endOfFlow();
 308                 }
 309                 case BranchInstruction inst -> {
 310                     // Conditional branch
 311                     Value right = stack.pop();
 312                     Value left;
 313                     switch (inst.opcode()) {
 314                         case IFNULL, IFNONNULL -> {
 315                             left = right;
 316                             right = liftConstant(null);
 317                         }
 318                         case IFEQ, IFNE, IFLT, IFLE, IFGT, IFGE -> {
 319                             if (numericCompare == null) {
 320                                 left = right;
 321                                 right = liftConstant(0);
 322                             } else {
 323                                 left = stack.pop();
 324                             }
 325                         }
 326                         default -> left = stack.pop();
 327                     }
 328                     boolean branchOnTrue = false;
 329                     Op cop = switch (inst.opcode()) {
 330                         case IFNE, IF_ICMPNE, IF_ACMPNE, IFNONNULL -> JavaOp.eq(left, right);
 331                         case IFEQ, IF_ICMPEQ, IF_ACMPEQ, IFNULL -> JavaOp.neq(left, right);
 332                         case IFGE, IF_ICMPGE -> {
 333                             if (numericCompare == Opcode.FCMPL || numericCompare == Opcode.DCMPL) {
 334                                 branchOnTrue = true;
 335                                 yield JavaOp.ge(left, right);
 336                             }
 337                             yield JavaOp.lt(left, right);
 338                         }
 339                         case IFLE, IF_ICMPLE -> {
 340                             if (numericCompare == Opcode.FCMPG || numericCompare == Opcode.DCMPG) {
 341                                 branchOnTrue = true;
 342                                 yield JavaOp.le(left, right);
 343                             }
 344                             yield JavaOp.gt(left, right);
 345                         }
 346                         case IFGT, IF_ICMPGT -> {
 347                             if (numericCompare == Opcode.FCMPL || numericCompare == Opcode.DCMPL) {
 348                                 branchOnTrue = true;
 349                                 yield JavaOp.gt(left, right);
 350                             }
 351                             yield JavaOp.le(left, right);
 352                         }
 353                         case IFLT, IF_ICMPLT -> {
 354                             if (numericCompare == Opcode.FCMPG || numericCompare == Opcode.DCMPG) {
 355                                 branchOnTrue = true;
 356                                 yield JavaOp.lt(left, right);
 357                             }
 358                             yield JavaOp.ge(left, right);
 359                         }
 360                         default -> throw new UnsupportedOperationException("Unsupported branch instruction: " + inst);
 361                     };
 362                     Block.Builder branch = transitionBlockForTarget(actualEreStack, label2Bci.applyAsInt(inst.target()));
 363                     Block.Builder next = entryBlock.block();
 364                     Block.Reference taken = successorWithStack(branch);
 365                     Block.Reference fallthrough = next.reference();
 366                     op(CoreOp.conditionalBranch(op(cop),
 367                             branchOnTrue ? taken : fallthrough,
 368                             branchOnTrue ? fallthrough : taken));
 369                     currentBlock = next;
 370                 }
 371                 case LookupSwitchInstruction si -> {
 372                     liftSwitch(si.defaultTarget(), si.cases());
 373                 }
 374                 case TableSwitchInstruction si -> {
 375                     liftSwitch(si.defaultTarget(), si.cases());
 376                 }
 377                 case ReturnInstruction inst when inst.typeKind() == TypeKind.VOID -> {
 378                     op(CoreOp.return_());
 379                     endOfFlow();
 380                 }
 381                 case ReturnInstruction _ -> {
 382                     op(CoreOp.return_(stack.pop()));
 383                     endOfFlow();
 384                 }
 385                 case ThrowInstruction _ -> {
 386                     op(JavaOp.throw_(stack.pop()));
 387                     endOfFlow();
 388                 }
 389                 case LoadInstruction inst -> {
 390                     stack.push(op(SlotOp.load(inst.slot(), inst.typeKind())));
 391                 }
 392                 case StoreInstruction inst -> {
 393                     op(SlotOp.store(inst.slot(), stack.pop()));
 394                 }
 395                 case IncrementInstruction inst -> {
 396                     op(SlotOp.store(inst.slot(), op(JavaOp.add(op(SlotOp.load(inst.slot(), TypeKind.INT)), liftConstant(inst.constant())))));
 397                 }
 398                 case ConstantInstruction inst -> {
 399                     stack.push(liftConstant(inst.constantValue()));
 400                 }
 401                 case ConvertInstruction inst -> {
 402                     stack.push(op(JavaOp.conv(switch (inst.toType()) {
 403                         case BYTE -> JavaType.BYTE;
 404                         case SHORT -> JavaType.SHORT;
 405                         case INT -> JavaType.INT;
 406                         case FLOAT -> JavaType.FLOAT;
 407                         case LONG -> JavaType.LONG;
 408                         case DOUBLE -> JavaType.DOUBLE;
 409                         case CHAR -> JavaType.CHAR;
 410                         case BOOLEAN -> JavaType.BOOLEAN;
 411                         default ->
 412                             throw new IllegalArgumentException("Unsupported conversion target: " + inst.toType());
 413                     }, stack.pop())));
 414                 }
 415                 case OperatorInstruction inst -> {
 416                     Value operand = stack.pop();
 417                     stack.push(op(switch (inst.opcode()) {
 418                         case IADD, LADD, FADD, DADD ->
 419                                 JavaOp.add(stack.pop(), operand);
 420                         case ISUB, LSUB, FSUB, DSUB ->
 421                                 JavaOp.sub(stack.pop(), operand);
 422                         case IMUL, LMUL, FMUL, DMUL ->
 423                                 JavaOp.mul(stack.pop(), operand);
 424                         case IDIV, LDIV, FDIV, DDIV ->
 425                                 JavaOp.div(stack.pop(), operand);
 426                         case IREM, LREM, FREM, DREM ->
 427                                 JavaOp.mod(stack.pop(), operand);
 428                         case INEG, LNEG, FNEG, DNEG ->
 429                                 JavaOp.neg(operand);
 430                         case ARRAYLENGTH ->
 431                                 JavaOp.arrayLength(operand);
 432                         case IAND, LAND ->
 433                                 JavaOp.and(stack.pop(), operand);
 434                         case IOR, LOR ->
 435                                 JavaOp.or(stack.pop(), operand);
 436                         case IXOR, LXOR ->
 437                                 JavaOp.xor(stack.pop(), operand);
 438                         case ISHL, LSHL ->
 439                                 JavaOp.lshl(stack.pop(), operand);
 440                         case ISHR, LSHR ->
 441                                 JavaOp.ashr(stack.pop(), operand);
 442                         case IUSHR, LUSHR ->
 443                                 JavaOp.lshr(stack.pop(), operand);
 444                         // standalone numeric compares are not reguraly produced by javac
 445                         case LCMP ->
 446                                 // LCMP is equivalent to Long::compare
 447                                 JavaOp.invoke(COMPARE_LONG, stack.pop(), operand);
 448                         case FCMPL, FCMPG ->
 449                                 // @@@ no suitable operator or API equivalent, NaN operands produce different results
 450                                 // comparison of negative zero is compensated
 451                                 JavaOp.invoke(COMPARE_FLOAT, op(JavaOp.add(stack.pop(), liftConstant(0f))),
 452                                                              op(JavaOp.add(operand, liftConstant(0f))));
 453                         case DCMPL, DCMPG ->
 454                                 // @@@ no suitable operator or API equivalent, NaN operands produce different results
 455                                 // comparison of negative zero is compensated
 456                                 JavaOp.invoke(COMPARE_DOUBLE, op(JavaOp.add(stack.pop(), liftConstant(0d))),
 457                                                               op(JavaOp.add(operand, liftConstant(0d))));
 458                         default ->
 459                             throw new IllegalArgumentException("Unsupported operator opcode: " + inst.opcode());
 460                     }));
 461                 }
 462                 case FieldInstruction inst -> {
 463                         FieldRef fd = FieldRef.field(
 464                                 JavaType.type(inst.owner().asSymbol()),
 465                                 inst.name().stringValue(),
 466                                 JavaType.type(inst.typeSymbol()));
 467                         switch (inst.opcode()) {
 468                             case GETFIELD ->
 469                                 stack.push(op(JavaOp.fieldLoad(fd, stack.pop())));
 470                             case GETSTATIC ->
 471                                 stack.push(op(JavaOp.fieldLoad(fd)));
 472                             case PUTFIELD -> {
 473                                 Value value = stack.pop();
 474                                 op(JavaOp.fieldStore(fd, stack.pop(), value));
 475                             }
 476                             case PUTSTATIC ->
 477                                 op(JavaOp.fieldStore(fd, stack.pop()));
 478                             default ->
 479                                 throw new IllegalArgumentException("Unsupported field opcode: " + inst.opcode());
 480                         }
 481                 }
 482                 case ArrayStoreInstruction _ -> {
 483                     Value value = stack.pop();
 484                     Value index = stack.pop();
 485                     op(JavaOp.arrayStoreOp(stack.pop(), index, value));
 486                 }
 487                 case ArrayLoadInstruction ali -> {
 488                     Value index = stack.pop();
 489                     Value array = stack.pop();
 490                     if (array.type() instanceof UnresolvedType) {
 491                         stack.push(op(JavaOp.arrayLoadOp(array, index, switch (ali.typeKind()) {
 492                             case BYTE -> UnresolvedType.unresolvedInt(); // @@@ Create UnresolvedType.unresolvedByteOrBoolean();
 493                             case CHAR -> JavaType.CHAR;
 494                             case DOUBLE -> JavaType.DOUBLE;
 495                             case FLOAT -> JavaType.FLOAT;
 496                             case INT -> JavaType.INT;
 497                             case LONG -> JavaType.LONG;
 498                             case SHORT -> JavaType.SHORT;
 499                             case REFERENCE ->  UnresolvedType.unresolvedRef();
 500                             case BOOLEAN, VOID -> throw new IllegalArgumentException("Unexpected array load instruction type");
 501                         })));
 502                     } else {
 503                         stack.push(op(JavaOp.arrayLoadOp(array, index)));
 504                     }
 505                 }
 506                 case InvokeInstruction inst -> {
 507                     FunctionType mType = MethodRef.ofNominalDescriptor(inst.typeSymbol());
 508                     List<Value> operands = new ArrayList<>();
 509                     for (var _ : mType.parameterTypes()) {
 510                         operands.add(stack.pop());
 511                     }
 512                     MethodRef mDesc = MethodRef.method(
 513                             JavaType.type(inst.owner().asSymbol()),
 514                             inst.name().stringValue(),
 515                             mType);
 516                     Op.Result result = switch (inst.opcode()) {
 517                         case INVOKEVIRTUAL, INVOKEINTERFACE -> {
 518                             operands.add(stack.pop());
 519                             yield op(JavaOp.invoke(JavaOp.InvokeOp.InvokeKind.INSTANCE, false,
 520                                     mDesc.signature().returnType(), mDesc, operands.reversed()));
 521                         }
 522                         case INVOKESTATIC ->
 523                                 op(JavaOp.invoke(JavaOp.InvokeOp.InvokeKind.STATIC, false,
 524                                         mDesc.signature().returnType(), mDesc, operands.reversed()));
 525                         case INVOKESPECIAL -> {
 526                             if (inst.owner().asSymbol().equals(newStack.peek()) && inst.name().equalsString(ConstantDescs.INIT_NAME)) {
 527                                 newStack.pop();
 528                                 yield op(JavaOp.new_(
 529                                         MethodRef.constructor(
 530                                                 mDesc.refType(),
 531                                                 mType.parameterTypes()),
 532                                         operands.reversed()));
 533                             } else {
 534                                 operands.add(stack.pop());
 535                                 yield op(JavaOp.invoke(JavaOp.InvokeOp.InvokeKind.SUPER, false,
 536                                         mDesc.signature().returnType(), mDesc, operands.reversed()));
 537                             }
 538                         }
 539                         default ->
 540                             throw new IllegalArgumentException("Unsupported invocation opcode: " + inst.opcode());
 541                     };
 542                     if (!result.type().equals(JavaType.VOID)) {
 543                         stack.push(result);
 544                     }
 545                 }
 546                 case InvokeDynamicInstruction inst when inst.bootstrapMethod().kind() == DirectMethodHandleDesc.Kind.STATIC -> {
 547                     DirectMethodHandleDesc bsm = inst.bootstrapMethod();
 548                     ClassDesc bsmOwner = bsm.owner();
 549                     if (bsmOwner.equals(CD_LambdaMetafactory)
 550                         && inst.bootstrapArgs().get(0) instanceof MethodTypeDesc mtd
 551                         && inst.bootstrapArgs().get(1) instanceof DirectMethodHandleDesc dmhd) {
 552 
 553                         var capturedValues = new Value[dmhd.invocationType().parameterCount() - mtd.parameterCount()];
 554                         for (int ci = capturedValues.length - 1; ci >= 0; ci--) {
 555                             capturedValues[ci] = stack.pop();
 556                         }
 557                         for (int ci = capturedValues.length; ci < inst.typeSymbol().parameterCount(); ci++) {
 558                             stack.pop();
 559                         }
 560                         MethodTypeDesc mt = dmhd.invocationType();
 561                         if (capturedValues.length > 0) {
 562                             mt = mt.dropParameterTypes(0, capturedValues.length);
 563                         }
 564                         FunctionType lambdaFunc = CoreType.functionType(JavaType.type(mt.returnType()),
 565                                                                             mt.parameterList().stream().map(JavaType::type).toList());
 566                         JavaOp.LambdaOp.Builder lambda = JavaOp.lambda(currentBlock.parentBody(),
 567                                                                        lambdaFunc,
 568                                                                        JavaType.type(inst.typeSymbol().returnType()));
 569                         // if ReflectableLambdaMetafactory is used, the lambda is reflectable
 570                         if (bsm.owner().displayName().equals("jdk.incubator.code.runtime.ReflectableLambdaMetafactory")) {
 571                             lambda = lambda.reflectable();
 572                         }
 573 
 574                         if (dmhd.methodName().startsWith("lambda$") && dmhd.owner().equals(classModel.thisClass().asSymbol())) {
 575                             // inline lambda impl method
 576                             MethodModel implMethod = classModel.methods().stream().filter(m -> m.methodName().equalsString(dmhd.methodName())).findFirst().orElseThrow();
 577                             stack.push(op(lambda.body(eb -> new BytecodeLift(eb,
 578                                                            classModel,
 579                                                            implMethod.code().orElseThrow(),
 580                                                            capturedValues).liftBody())));
 581                         } else {
 582                             // lambda call to a MH
 583                             stack.push(op(lambda.body(eb -> {
 584                                 Op.Result ret = eb.add(JavaOp.invoke(
 585                                         MethodRef.method(JavaType.type(dmhd.owner()),
 586                                                          dmhd.methodName(),
 587                                                          lambdaFunc.returnType(),
 588                                                          lambdaFunc.parameterTypes()),
 589                                         Stream.concat(Arrays.stream(capturedValues), eb.parameters().stream()).toArray(Value[]::new)));
 590                                 eb.add(ret.type().equals(JavaType.VOID) ? CoreOp.return_() : CoreOp.return_(ret));
 591                             })));
 592                         }
 593                     } else if (bsmOwner.equals(CD_StringConcatFactory)) {
 594                         int argsCount = inst.typeSymbol().parameterCount();
 595                         Deque<Value> args = new ArrayDeque<>(argsCount);
 596                         for (int ai = 0; ai < argsCount; ai++) {
 597                             args.push(stack.pop());
 598                         }
 599                         Value res = null;
 600                         if (bsm.methodName().equals("makeConcat")) {
 601                             for (Value argVal : args) {
 602                                 res = res == null ? argVal : op(JavaOp.concat(res, argVal));
 603                             }
 604                         } else {
 605                             assert bsm.methodName().equals("makeConcatWithConstants");
 606                             var bsmArgs = inst.bootstrapArgs();
 607                             String recipe = (String)(bsmArgs.getFirst());
 608                             int bsmArg = 1;
 609                             for (int ri = 0; ri < recipe.length(); ri++) {
 610                                 Value argVal = switch (recipe.charAt(ri)) {
 611                                     case '\u0001' -> args.pop();
 612                                     case '\u0002' -> liftConstant(bsmArgs.get(bsmArg++));
 613                                     default -> {
 614                                         char c;
 615                                         int start = ri;
 616                                         while (ri < recipe.length() && (c = recipe.charAt(ri)) != '\u0001' && c != '\u0002') ri++;
 617                                         yield liftConstant(recipe.substring(start, ri--));
 618                                     }
 619                                 };
 620                                 res = res == null ? argVal : op(JavaOp.concat(res, argVal));
 621                             }
 622                         }
 623                         if (res != null) stack.push(res);
 624                     } else {
 625                         MethodTypeDesc mtd = inst.typeSymbol();
 626 
 627                         //bootstrap
 628                         MethodTypeDesc bsmDesc = bsm.invocationType();
 629                         MethodRef bsmRef = MethodRef.method(JavaType.type(bsmOwner),
 630                                                             bsm.methodName(),
 631                                                             JavaType.type(bsmDesc.returnType()),
 632                                                             bsmDesc.parameterList().stream().map(JavaType::type).toArray(CodeType[]::new));
 633 
 634                         Value[] bootstrapArgs = liftBootstrapArgs(bsmDesc, inst.name().toString(), mtd, inst.bootstrapArgs());
 635                         Value methodHandle = op(JavaOp.invoke(MethodRef.method(CallSite.class, "dynamicInvoker", MethodHandle.class),
 636                                                     op(JavaOp.invoke(JavaType.type(ConstantDescs.CD_CallSite), bsmRef, bootstrapArgs))));
 637 
 638                         //invocation
 639                         List<Value> operands = new ArrayList<>();
 640                         for (int c = 0; c < mtd.parameterCount(); c++) {
 641                             operands.add(stack.pop());
 642                         }
 643                         operands.add(methodHandle);
 644                         MethodRef mDesc = MethodRef.method(JavaType.type(ConstantDescs.CD_MethodHandle),
 645                                                            "invokeExact",
 646                                                            MethodRef.ofNominalDescriptor(mtd));
 647                         Op.Result result = op(JavaOp.invoke(mDesc, operands.reversed()));
 648                         if (!result.type().equals(JavaType.VOID)) {
 649                             stack.push(result);
 650                         }
 651                     }
 652                 }
 653                 case NewObjectInstruction inst -> {
 654                     // Skip over this and the dup to process the invoke special
 655                     if (i + 2 < elements.size() - 1
 656                             && elements.get(i + 1) instanceof StackInstruction dup
 657                             && dup.opcode() == Opcode.DUP) {
 658                         i++;
 659                         newStack.push(inst.className().asSymbol());
 660                     } else {
 661                         throw new UnsupportedOperationException("New must be followed by dup");
 662                     }
 663                 }
 664                 case NewPrimitiveArrayInstruction inst -> {
 665                     stack.push(op(JavaOp.newArray(
 666                             switch (inst.typeKind()) {
 667                                 case BOOLEAN -> JavaType.BOOLEAN_ARRAY;
 668                                 case BYTE -> JavaType.BYTE_ARRAY;
 669                                 case CHAR -> JavaType.CHAR_ARRAY;
 670                                 case DOUBLE -> JavaType.DOUBLE_ARRAY;
 671                                 case FLOAT -> JavaType.FLOAT_ARRAY;
 672                                 case INT -> JavaType.INT_ARRAY;
 673                                 case LONG -> JavaType.LONG_ARRAY;
 674                                 case SHORT -> JavaType.SHORT_ARRAY;
 675                                 default ->
 676                                         throw new UnsupportedOperationException("Unsupported new primitive array type: " + inst.typeKind());
 677                             },
 678                             stack.pop())));
 679                 }
 680                 case NewReferenceArrayInstruction inst -> {
 681                     stack.push(op(JavaOp.newArray(
 682                             JavaType.type(inst.componentType().asSymbol().arrayType()),
 683                             stack.pop())));
 684                 }
 685                 case NewMultiArrayInstruction inst -> {
 686                     stack.push(op(JavaOp.new_(
 687                             MethodRef.constructor(
 688                                     JavaType.type(inst.arrayType().asSymbol()),
 689                                     Collections.nCopies(inst.dimensions(), JavaType.INT)),
 690                             IntStream.range(0, inst.dimensions()).mapToObj(_ -> stack.pop()).toList().reversed())));
 691                 }
 692                 case TypeCheckInstruction inst when inst.opcode() == Opcode.CHECKCAST -> {
 693                     stack.push(op(JavaOp.cast(JavaType.type(inst.type().asSymbol()), stack.pop())));
 694                 }
 695                 case TypeCheckInstruction inst -> {
 696                     stack.push(op(JavaOp.instanceOf(JavaType.type(inst.type().asSymbol()), stack.pop())));
 697                 }
 698                 case StackInstruction inst -> {
 699                     switch (inst.opcode()) {
 700                         case POP -> {
 701                             stack.pop();
 702                         }
 703                         case POP2 -> {
 704                             if (isCategory1(stack.pop())) {
 705                                 stack.pop();
 706                             }
 707                         }
 708                         case DUP -> {
 709                             stack.push(stack.peek());
 710                         }
 711                         case DUP_X1 -> {
 712                             var value1 = stack.pop();
 713                             var value2 = stack.pop();
 714                             stack.push(value1);
 715                             stack.push(value2);
 716                             stack.push(value1);
 717                         }
 718                         case DUP_X2 -> {
 719                             var value1 = stack.pop();
 720                             var value2 = stack.pop();
 721                             if (isCategory1(value2)) {
 722                                 var value3 = stack.pop();
 723                                 stack.push(value1);
 724                                 stack.push(value3);
 725                             } else {
 726                                 stack.push(value1);
 727                             }
 728                             stack.push(value2);
 729                             stack.push(value1);
 730                         }
 731                         case DUP2 -> {
 732                             var value1 = stack.peek();
 733                             if (isCategory1(value1)) {
 734                                 stack.pop();
 735                                 var value2 = stack.peek();
 736                                 stack.push(value1);
 737                                 stack.push(value2);
 738                             }
 739                             stack.push(value1);
 740                         }
 741                         case DUP2_X1 -> {
 742                             var value1 = stack.pop();
 743                             var value2 = stack.pop();
 744                             if (isCategory1(value1)) {
 745                                 var value3 = stack.pop();
 746                                 stack.push(value2);
 747                                 stack.push(value1);
 748                                 stack.push(value3);
 749                             } else {
 750                                 stack.push(value1);
 751                             }
 752                             stack.push(value2);
 753                             stack.push(value1);
 754                         }
 755                         case DUP2_X2 -> {
 756                             var value1 = stack.pop();
 757                             var value2 = stack.pop();
 758                             if (isCategory1(value1)) {
 759                                 var value3 = stack.pop();
 760                                 if (isCategory1(value3)) {
 761                                     var value4 = stack.pop();
 762                                     stack.push(value2);
 763                                     stack.push(value1);
 764                                     stack.push(value4);
 765                                 } else {
 766                                     stack.push(value2);
 767                                     stack.push(value1);
 768                                 }
 769                                 stack.push(value3);
 770                             } else {
 771                                 if (isCategory1(value2)) {
 772                                     var value3 = stack.pop();
 773                                     stack.push(value1);
 774                                     stack.push(value3);
 775                                 } else {
 776                                     stack.push(value1);
 777                                 }
 778                             }
 779                             stack.push(value2);
 780                             stack.push(value1);
 781                         }
 782                         case SWAP -> {
 783                             var value1 = stack.pop();
 784                             var value2 = stack.pop();
 785                             stack.push(value1);
 786                             stack.push(value2);
 787                         }
 788                         default ->
 789                             throw new UnsupportedOperationException("Unsupported stack instruction: " + inst);
 790                     }
 791                 }
 792                 case MonitorInstruction inst -> {
 793                     var monitor = stack.pop();
 794                     switch (inst.opcode()) {
 795                         case MONITORENTER -> op(JavaOp.monitorEnter(monitor));
 796                         case MONITOREXIT -> op(JavaOp.monitorExit(monitor));
 797                         default ->
 798                                 throw new UnsupportedOperationException("Unsupported stack instruction: " + inst);
 799                     }
 800                 }
 801                 case NopInstruction _ -> {}
 802                 case PseudoInstruction _ -> {}
 803                 case Instruction inst ->
 804                     throw new UnsupportedOperationException("Unsupported instruction: " + inst.opcode().name());
 805                 default ->
 806                     throw new UnsupportedOperationException("Unsupported code element: " + elements.get(i));
 807             }
 808         }
 809         assert newStack.isEmpty();
 810     }
 811 
 812     private Op.Result liftConstantsIntoArray(CodeType arrayType, Object... constants) {
 813         Op.Result array = op(JavaOp.newArray(arrayType, liftConstant(constants.length)));
 814         for (int i = 0; i < constants.length; i++) {
 815             op(JavaOp.arrayStoreOp(array, liftConstant(i), liftConstant(constants[i])));
 816         }
 817         return array;
 818     }
 819 
 820     private Op.Result liftConstant(Object c) {
 821         return switch (c) {
 822             case null -> op(CoreOp.constant(UnresolvedType.unresolvedRef(), null));
 823             case ClassDesc cd -> op(CoreOp.constant(JavaType.J_L_CLASS, JavaType.type(cd)));
 824             case Double d -> op(CoreOp.constant(JavaType.DOUBLE, d));
 825             case Float f -> op(CoreOp.constant(JavaType.FLOAT, f));
 826             case Integer ii -> op(CoreOp.constant(UnresolvedType.unresolvedInt(), ii));
 827             case Long l -> op(CoreOp.constant(JavaType.LONG, l));
 828             case String s -> op(CoreOp.constant(JavaType.J_L_STRING, s));
 829             case DirectMethodHandleDesc dmh -> {
 830                 Op.Result lookup = op(JavaOp.invoke(LOOKUP));
 831                 Op.Result owner = liftConstant(dmh.owner());
 832                 Op.Result name = liftConstant(dmh.methodName());
 833                 MethodTypeDesc invDesc = dmh.invocationType();
 834                 yield op(switch (dmh.kind()) {
 835                     case STATIC, INTERFACE_STATIC  ->
 836                         JavaOp.invoke(FIND_STATIC, lookup, owner, name, liftConstant(invDesc));
 837                     case VIRTUAL, INTERFACE_VIRTUAL ->
 838                         JavaOp.invoke(FIND_VIRTUAL, lookup, owner, name, liftConstant(invDesc.dropParameterTypes(0, 1)));
 839                     case SPECIAL, INTERFACE_SPECIAL ->
 840                         //CoreOp.invoke(MethodRef.method(e), "findSpecial", owner, name, liftConstant(invDesc.dropParameterTypes(0, 1)), lookup.lookupClass());
 841                         throw new UnsupportedOperationException(dmh.toString());
 842                     case CONSTRUCTOR       ->
 843                         JavaOp.invoke(FIND_CONSTRUCTOR, lookup, owner, liftConstant(invDesc.changeReturnType(ConstantDescs.CD_Void)));
 844                     case GETTER            ->
 845                         JavaOp.invoke(FIND_GETTER, lookup, owner, name, liftConstant(invDesc.returnType()));
 846                     case STATIC_GETTER     ->
 847                         JavaOp.invoke(FIND_STATIC_GETTER, lookup, owner, name, liftConstant(invDesc.returnType()));
 848                     case SETTER            ->
 849                         JavaOp.invoke(FIND_SETTER, lookup, owner, name, liftConstant(invDesc.parameterType(1)));
 850                     case STATIC_SETTER     ->
 851                         JavaOp.invoke(FIND_STATIC_SETTER, lookup, owner, name, liftConstant(invDesc.parameterType(0)));
 852                 });
 853             }
 854             case MethodTypeDesc mt -> op(switch (mt.parameterCount()) {
 855                 case 0 -> JavaOp.invoke(METHOD_TYPE_0, liftConstant(mt.returnType()));
 856                 case 1 -> JavaOp.invoke(METHOD_TYPE_1, liftConstant(mt.returnType()), liftConstant(mt.parameterType(0)));
 857                 default -> JavaOp.invoke(METHOD_TYPE_L, liftConstant(mt.returnType()), liftConstantsIntoArray(CLASS_ARRAY, (Object[])mt.parameterArray()));
 858             });
 859             case DynamicConstantDesc<?> v when v.bootstrapMethod().owner().equals(ConstantDescs.CD_ConstantBootstraps)
 860                                          && v.bootstrapMethod().methodName().equals("nullConstant")
 861                     -> {
 862                 c = null;
 863                 yield liftConstant(null);
 864             }
 865             case DynamicConstantDesc<?> dcd when dcd.bootstrapMethod().equals(ConstantDescs.BSM_INVOKE)
 866                     && dcd.constantType().equals(ConstantDescs.CD_MethodHandle)
 867                     && !dcd.bootstrapArgsList().isEmpty()
 868                     && dcd.bootstrapArgsList().getFirst() instanceof DirectMethodHandleDesc finder
 869                     && finder.owner().equals(ConstantDescs.CD_MethodHandles_Lookup)
 870                     && Arrays.asList("findSpecial", "findVirtual", "findStatic",
 871                             "findGetter", "findSetter", "findStaticGetter", "findStaticSetter")
 872                             .contains(finder.methodName())
 873                     -> op(CoreOp.constant(MH, dcd));
 874             case DynamicConstantDesc<?> dcd -> {
 875                 DirectMethodHandleDesc bsm = dcd.bootstrapMethod();
 876                 MethodTypeDesc bsmDesc = bsm.invocationType();
 877                 Value[] bootstrapArgs = liftBootstrapArgs(bsmDesc, dcd.constantName(), dcd.constantType(), dcd.bootstrapArgsList());
 878                 MethodRef bsmRef = MethodRef.method(JavaType.type(bsm.owner()),
 879                                                     bsm.methodName(),
 880                                                     JavaType.type(bsmDesc.returnType()),
 881                                                     bsmDesc.parameterList().stream().map(JavaType::type).toArray(CodeType[]::new));
 882                 Op.Result call = op(JavaOp.invoke(bsmRef, bootstrapArgs));
 883                 JavaType target = JavaType.type(dcd.constantType());
 884                 yield target.equals(call.type()) || dcd.constantType().isPrimitive()
 885                         ? call : op(JavaOp.cast(target, call));
 886             }
 887             case Boolean b -> op(CoreOp.constant(JavaType.BOOLEAN, b));
 888             case Byte b -> op(CoreOp.constant(JavaType.BYTE, b));
 889             case Short s -> op(CoreOp.constant(JavaType.SHORT, s));
 890             case Character ch -> op(CoreOp.constant(JavaType.CHAR, ch));
 891             default -> throw new UnsupportedOperationException(c.getClass().toString());
 892         };
 893     }
 894 
 895     private Value[] liftBootstrapArgs(MethodTypeDesc bsmDesc, String name, ConstantDesc desc, List<ConstantDesc> bsmArgs) {
 896         Value[] bootstrapArgs = new Value[bsmDesc.parameterCount()];
 897         bootstrapArgs[0] = op(JavaOp.invoke(LOOKUP));
 898         bootstrapArgs[1] = liftConstant(name);
 899         bootstrapArgs[2] = liftConstant(desc);
 900         ClassDesc lastArgType = bsmDesc.parameterType(bsmDesc.parameterCount() - 1);
 901         if (lastArgType.isArray()) {
 902             for (int ai = 0; ai < bootstrapArgs.length - 4; ai++) {
 903                 bootstrapArgs[ai + 3] = liftConstant(bsmArgs.get(ai));
 904             }
 905             // Vararg tail of the bootstrap method parameters
 906             bootstrapArgs[bootstrapArgs.length - 1] =
 907                     liftConstantsIntoArray(JavaType.type(lastArgType),
 908                                            bsmArgs.subList(bootstrapArgs.length - 4, bsmArgs.size()).toArray());
 909         } else {
 910             for (int ai = 0; ai < bootstrapArgs.length - 3; ai++) {
 911                 bootstrapArgs[ai + 3] = liftConstant(bsmArgs.get(ai));
 912             }
 913         }
 914         return bootstrapArgs;
 915     }
 916 
 917     private void liftSwitch(Label defaultTarget, List<SwitchCase> cases) {
 918         Value v = stack.pop();
 919         if (!valueType(v).equals(PrimitiveType.INT)) {
 920             v = op(JavaOp.conv(PrimitiveType.INT, v));
 921         }
 922         SwitchCase last = cases.getLast();
 923         Block.Builder def = transitionBlockForTarget(actualEreStack, label2Bci.applyAsInt(defaultTarget));
 924         for (SwitchCase sc : cases) {
 925             if (sc == last) {
 926                 op(CoreOp.conditionalBranch(
 927                         op(JavaOp.eq(v, liftConstant(sc.caseValue()))),
 928                         successorWithStack(transitionBlockForTarget(actualEreStack, label2Bci.applyAsInt(sc.target()))),
 929                         successorWithStack(def)));
 930             } else {
 931                 Block.Builder next = entryBlock.block();
 932                 op(CoreOp.conditionalBranch(
 933                         op(JavaOp.eq(v, liftConstant(sc.caseValue()))),
 934                         successorWithStack(transitionBlockForTarget(actualEreStack, label2Bci.applyAsInt(sc.target()))),
 935                         next.reference()));
 936                 currentBlock = next;
 937             }
 938         }
 939         endOfFlow();
 940     }
 941 
 942     private Block.Builder newBlock(List<Block.Parameter> otherBlockParams) {
 943         return entryBlock.block(otherBlockParams.stream().map(Block.Parameter::type).toList());
 944     }
 945 
 946     private void endOfFlow() {
 947         currentBlock = null;
 948         // Flow discontinued, stack cleared to be ready for the next label target
 949         stack.clear();
 950         actualEreStack = List.of();
 951     }
 952 
 953     // Create a block that exits regions before it reaches the target
 954     private Block.Builder transitionBlockForTarget(List<Op.Result> initialEreStack, int targetBci) {
 955         Block.Builder targetBlock = blockMap.get(targetBci);
 956         if (targetBlock == null) return null;
 957         Block.Builder transitionBlock = newBlock(targetBlock.parameters());
 958         exitRegions(initialEreStack, transitionBlock, targetEntryBlock(targetBci), transitionBlock.parameters());
 959         return transitionBlock;
 960     }
 961 
 962     // Return a block that enters regions needed by the target
 963     private Block.Builder targetEntryBlock(int bci) {
 964         Block.Builder targetBlock = blockMap.get(bci);
 965         List<ExceptionRegion> targetEreStack = exceptionRegionsMap.getOrDefault(bci, List.of());
 966         if (targetEreStack.isEmpty()) {
 967             enteredRegionStacks.putIfAbsent(targetBlock, List.of());
 968             return targetBlock;
 969         }
 970         Block.Builder enterBlock = labelEntryBlocks.get(bci);
 971         if (enterBlock == null) {  // Avoid ConcurrentModificationException
 972             enterBlock = newBlock(targetBlock.parameters());
 973             labelEntryBlocks.put(bci, enterBlock);
 974             List<Op.Result> ereStack = new ArrayList<>();
 975             Block.Builder currentBlock = enterBlock;
 976             for (int i = 0; i < targetEreStack.size(); i++) {
 977                 boolean last = i == targetEreStack.size() - 1;
 978                 Block.Builder nextBlock = last ? targetBlock : newBlock(targetBlock.parameters());
 979                 Block.Reference nextReference = nextBlock.reference(currentBlock.parameters());
 980                 ExceptionRegion entered = targetEreStack.get(i);
 981                 CatchEntries catches = catchReferences(ereStack, entered, currentBlock);
 982                 Op.Result enter = currentBlock.add(JavaOp.exceptionRegionEnter(catches.types(), nextReference,
 983                         catches.references()));
 984                 enteredRegionMap.put(enter, entered);
 985                 ereStack.add(enter);
 986                 currentBlock = nextBlock;
 987             }
 988             enteredRegionStacks.putIfAbsent(targetBlock, List.copyOf(ereStack));
 989         }
 990         return enterBlock;
 991     }
 992 
 993     // Emit exits from innermost region to outermost region
 994     private void exitRegions(List<Op.Result> initialEreStack, Block.Builder initialBlock, Block.Builder targetBlock,
 995                              List<? extends Value> values) {
 996         if (initialEreStack.isEmpty()) {
 997             initialBlock.add(CoreOp.branch(targetBlock.reference(values)));
 998         } else {
 999             Block.Builder currentBlock = initialBlock;
1000             for (int i = initialEreStack.size() - 1; i >= 0; i--) {
1001                 boolean last = i == 0;
1002                 Block.Builder nextBlock = last ? targetBlock : entryBlock.block();
1003                 Block.Reference nextReference = last ? nextBlock.reference(values) : nextBlock.reference();
1004                 currentBlock.add(JavaOp.exceptionRegionExit(initialEreStack.get(i), nextReference));
1005                 currentBlock = nextBlock;
1006             }
1007         }
1008     }
1009 
1010     // Move from one region stack to another
1011     private List<Op.Result> ereTransit(List<ExceptionRegion> targetEreStack, Block.Builder targetBlock) {
1012         int common = 0;
1013         int limit = Math.min(actualEreStack.size(), targetEreStack.size());
1014         while (common < limit && enteredRegionMap.get(actualEreStack.get(common)) == targetEreStack.get(common)) {
1015             common++;
1016         }
1017         int exits = actualEreStack.size() - common;
1018         int enters = targetEreStack.size() - common;
1019         if (exits == 0 && enters == 0) {
1020             // Join with branch
1021             currentBlock.add(CoreOp.branch(targetBlock.reference()));
1022             enteredRegionStacks.putIfAbsent(targetBlock, actualEreStack);
1023             return actualEreStack;
1024         }
1025         List<Op.Result> ereStack = new ArrayList<>(actualEreStack);
1026         Block.Builder block = currentBlock;
1027         int transitionCount = exits + enters;
1028         for (int t = 0; t < transitionCount; t++) {
1029             boolean last = t == transitionCount - 1;
1030             Block.Builder nextBlock = last ? targetBlock : entryBlock.block();
1031             Block.Reference nextReference = nextBlock.reference();
1032             if (t < exits) {
1033                 block.add(JavaOp.exceptionRegionExit(ereStack.removeLast(), nextReference));
1034             } else {
1035                 ExceptionRegion entered = targetEreStack.get(common + t - exits);
1036                 CatchEntries catches = catchReferences(ereStack, entered, block);
1037                 Op.Result enter = block.add(JavaOp.exceptionRegionEnter(catches.types(), nextReference, catches.references()));
1038                 enteredRegionMap.put(enter, entered);
1039                 ereStack.add(enter);
1040             }
1041             block = nextBlock;
1042         }
1043         enteredRegionStacks.putIfAbsent(targetBlock, ereStack);
1044         return ereStack;
1045     }
1046 
1047     // Build catch targets for one enter op
1048     private CatchEntries catchReferences(List<Op.Result> initialEreStack, ExceptionRegion enteredRegion,
1049                                          Block.Builder b) {
1050         record Group(int firstHandler, int handlerBci, List<CodeType> catchTypes) {}
1051 
1052         List<Group> groups = new ArrayList<>();
1053         for (int handler : enteredRegion.handlers()) {
1054             int handlerBci = handlerBcis.get(handler);
1055             CodeType catchType = catchTypes.get(handler);
1056             Group last = groups.isEmpty() ? null : groups.getLast();
1057             if (last != null && last.handlerBci() == handlerBci) {
1058                 last.catchTypes().add(catchType);
1059             } else {
1060                 groups.add(new Group(handler, handlerBci, new ArrayList<>(List.of(catchType))));
1061             }
1062         }
1063 
1064         List<Block.Reference> references = new ArrayList<>();
1065         List<CodeType> types = new ArrayList<>();
1066         List<Op.Result> enteredRegions = List.copyOf(initialEreStack);
1067         for (Group group : groups.reversed()) {
1068             CatchTargetKey key = new CatchTargetKey(group.handlerBci(), enteredRegions,
1069                     enteredRegion.handlers().stream().map(h -> Map.entry(handlerBcis.get(h), catchTypes.get(h))).toList());
1070             Block.Builder target = exceptionHandlerBlocks.get(key);
1071             if (target == null) { // Avoid ConcurrentModificationException
1072                 target = transitionBlockForTarget(enteredRegions, group.handlerBci());
1073                 exceptionHandlerBlocks.put(key, target);
1074             }
1075             Op.Result arg = b.add(constant(target.parameters().getFirst().type(), null));
1076             references.add(target.reference(arg));
1077             types.add(group.catchTypes().size() == 1
1078                     ? group.catchTypes().getFirst()
1079                     : CoreType.tupleType(group.catchTypes()));
1080         }
1081         return new CatchEntries(types, references);
1082     }
1083 
1084     Block.Reference successorWithStack(Block.Builder next) {
1085         return next.reference(stackValues(next));
1086     }
1087 
1088     private List<Value> stackValues(Block.Builder limit) {
1089         return stack.stream().limit(limit.parameters().size()).toList();
1090     }
1091 
1092     private static CodeType valueType(Value v) {
1093         var t = v.type();
1094         while (t instanceof VarType vt) t = vt.valueType();
1095         return t;
1096     }
1097 
1098     private static boolean isCategory1(Value v) {
1099         CodeType t = v.type();
1100         return !t.equals(JavaType.LONG) && !t.equals(JavaType.DOUBLE);
1101     }
1102 
1103     private static boolean isUnconditionalBranch(Opcode opcode) {
1104         return switch (opcode) {
1105             case GOTO, ATHROW, GOTO_W, LOOKUPSWITCH, TABLESWITCH -> true;
1106             default -> opcode.kind() == Opcode.Kind.RETURN;
1107         };
1108     }
1109 }