/*
 * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jdk.internal.classfile.impl;

import java.lang.classfile.*;
import java.lang.classfile.attribute.CodeAttribute;
import java.lang.classfile.attribute.RuntimeInvisibleTypeAnnotationsAttribute;
import java.lang.classfile.attribute.RuntimeVisibleTypeAnnotationsAttribute;
import java.lang.classfile.attribute.StackMapTableAttribute;
import java.lang.classfile.attribute.UnknownAttribute;
import java.lang.classfile.constantpool.ClassEntry;
import java.lang.classfile.instruction.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;

import static jdk.internal.classfile.impl.RawBytecodeHelper.*;

public final class CodeImpl
        extends BoundAttribute.BoundCodeAttribute
        implements LabelContext {

    static final Instruction[] SINGLETON_INSTRUCTIONS = new Instruction[256];

    static {
        for (var o : Opcode.values()) {
            if (o.sizeIfFixed() == 1) {
                SINGLETON_INSTRUCTIONS[o.bytecode()] = switch (o.kind()) {
                    case ARRAY_LOAD -> ArrayLoadInstruction.of(o);
                    case ARRAY_STORE -> ArrayStoreInstruction.of(o);
                    case CONSTANT -> ConstantInstruction.ofIntrinsic(o);
                    case CONVERT -> ConvertInstruction.of(o);
                    case LOAD -> new AbstractInstruction.UnboundLoadInstruction(o, BytecodeHelpers.intrinsicLoadSlot(o));
                    case MONITOR -> MonitorInstruction.of(o);
                    case NOP -> NopInstruction.of();
                    case OPERATOR -> OperatorInstruction.of(o);
                    case RETURN -> ReturnInstruction.of(o);
                    case STACK -> StackInstruction.of(o);
                    case STORE -> new AbstractInstruction.UnboundStoreInstruction(o, BytecodeHelpers.intrinsicStoreSlot(o));
                    case THROW_EXCEPTION -> ThrowInstruction.of();
                    default -> throw new AssertionError("invalid opcode: " + o);
                };
            }
        }
    }

    List<ExceptionCatch> exceptionTable;
    List<Attribute<?>> attributes;

    // Inflated for iteration
    LabelImpl[] labels;
    int[] lineNumbers;
    boolean inflated;

    public CodeImpl(AttributedElement enclosing,
                    ClassReader reader,
                    AttributeMapper<CodeAttribute> mapper,
                    int payloadStart) {
        super(enclosing, reader, mapper, payloadStart);
    }

    // LabelContext

    @Override
    public Label newLabel() {
        throw new UnsupportedOperationException("CodeAttribute only supports fixed labels");
    }

    @Override
    public void setLabelTarget(Label label, int bci) {
        throw new UnsupportedOperationException("CodeAttribute only supports fixed labels");
    }

    @Override
    public Label getLabel(int bci) {
        if (bci < 0 || bci > codeLength)
            throw new IllegalArgumentException(String.format("Bytecode offset out of range; bci=%d, codeLength=%d",
                                                             bci, codeLength));
        if (labels == null)
            labels = new LabelImpl[codeLength + 1];
        LabelImpl l = labels[bci];
        if (l == null)
            l = labels[bci] = new LabelImpl(this, bci);
        return l;
    }

    @Override
    public int labelToBci(Label label) {
        LabelImpl lab = (LabelImpl) label;
        if (lab.labelContext() != this)
            throw new IllegalArgumentException(String.format("Illegal label reuse; context=%s, label=%s",
                                                             this, lab.labelContext()));
        return lab.getBCI();
    }

    private void inflateMetadata() {
        if (!inflated) {
            if (labels == null)
                labels = new LabelImpl[codeLength + 1];
            if (classReader.context().passLineNumbers())
                inflateLineNumbers();
            inflateJumpTargets();
            inflateTypeAnnotations();
            inflated = true;
        }
    }

    // CodeAttribute

    @Override
    public List<Attribute<?>> attributes() {
        if (attributes == null) {
            attributes = BoundAttribute.readAttributes(this, classReader, attributePos, classReader.customAttributes());
        }
        return attributes;
    }

    @Override
    public void writeTo(BufWriterImpl buf) {
        if (buf.canWriteDirect(classReader)) {
            super.writeTo(buf);
        }
        else {
            DirectCodeBuilder.build((MethodInfo) enclosingMethod,
                                    Util.writingAll(this),
                                    (SplitConstantPool)buf.constantPool(),
                                    buf.context(),
                                    null).writeTo(buf);
        }
    }

    // CodeModel

    @Override
    public Optional<MethodModel> parent() {
        return Optional.of(enclosingMethod);
    }

    @Override
    public void forEach(Consumer<? super CodeElement> consumer) {
        Objects.requireNonNull(consumer);
        inflateMetadata();
        boolean doLineNumbers = (lineNumbers != null);
        generateCatchTargets(consumer);
        if (classReader.context().passDebugElements())
            generateDebugElements(consumer);
        generateUserAttributes(consumer);
        for (int pos=codeStart; pos<codeEnd; ) {
            if (labels[pos - codeStart] != null)
                consumer.accept(labels[pos - codeStart]);
            if (doLineNumbers && lineNumbers[pos - codeStart] != 0)
                consumer.accept(LineNumberImpl.of(lineNumbers[pos - codeStart]));
            int bc = classReader.readU1(pos);
            Instruction instr = bcToInstruction(bc, pos);
            consumer.accept(instr);
            pos += instr.sizeInBytes();
        }
        // There might be labels pointing to the bci at codeEnd
        if (labels[codeEnd-codeStart] != null)
            consumer.accept(labels[codeEnd - codeStart]);
        if (doLineNumbers && lineNumbers[codeEnd - codeStart] != 0)
            consumer.accept(LineNumberImpl.of(lineNumbers[codeEnd - codeStart]));
    }

    @Override
    public List<ExceptionCatch> exceptionHandlers() {
        if (exceptionTable == null) {
            inflateMetadata();
            exceptionTable = new ArrayList<>(exceptionHandlerCnt);
            iterateExceptionHandlers(new ExceptionHandlerAction() {
                @Override
                public void accept(int s, int e, int h, int c) {
                    ClassEntry catchTypeEntry = c == 0
                                                             ? null
                                                             : constantPool().entryByIndex(c, ClassEntry.class);
                    exceptionTable.add(new AbstractPseudoInstruction.ExceptionCatchImpl(getLabel(h), getLabel(s), getLabel(e), catchTypeEntry));
                }
            });
            exceptionTable = Collections.unmodifiableList(exceptionTable);
        }
        return exceptionTable;
    }

    private void generateUserAttributes(Consumer<? super CodeElement> consumer) {
        for (var attr : attributes) {
            if (attr instanceof CustomAttribute || attr instanceof UnknownAttribute) {
                consumer.accept((CodeElement) attr);
            }
        }
    }

    public boolean compareCodeBytes(BufWriterImpl buf, int offset, int len) {
        return codeLength == len
               && classReader.compare(buf, offset, codeStart, codeLength);
    }

    private int adjustForObjectOrUninitialized(int bci) {
        int vt = classReader.readU1(bci);
        //inflate newTarget labels from Uninitialized VTIs
        if (vt == 8) inflateLabel(classReader.readU2(bci + 1));
        return (vt == 7 || vt == 8) ? bci + 3 : bci + 1;
    }

    private void inflateLabel(int bci) {
        if (bci < 0 || bci > codeLength)
            throw new IllegalArgumentException(String.format("Bytecode offset out of range; bci=%d, codeLength=%d",
                                                             bci, codeLength));
        if (labels[bci] == null)
            labels[bci] = new LabelImpl(this, bci);
    }

    private void inflateLineNumbers() {
        for (Attribute<?> a : attributes()) {
            if (a.attributeMapper() == Attributes.lineNumberTable()) {
                BoundLineNumberTableAttribute attr = (BoundLineNumberTableAttribute) a;
                if (lineNumbers == null)
                    lineNumbers = new int[codeLength + 1];

                int nLn = classReader.readU2(attr.payloadStart);
                int p = attr.payloadStart + 2;
                int pEnd = p + (nLn * 4);
                for (; p < pEnd; p += 4) {
                    int startPc = classReader.readU2(p);
                    if (startPc > codeLength) {
                        throw new IllegalArgumentException(String.format(
                                "Line number start_pc out of range; start_pc=%d, codeLength=%d", startPc, codeLength));
                    }
                    int lineNumber = classReader.readU2(p + 2);
                    lineNumbers[startPc] = lineNumber;
                }
            }
        }
    }

    private void inflateJumpTargets() {
        Optional<StackMapTableAttribute> a = findAttribute(Attributes.stackMapTable());
        if (a.isEmpty()) {
            if (classReader.readU2(6) <= ClassFile.JAVA_6_VERSION) {
                //fallback to jump targets inflation without StackMapTableAttribute
                for (int pos=codeStart; pos<codeEnd; ) {
                    var i = bcToInstruction(classReader.readU1(pos), pos);
                    switch (i.opcode().kind()) {
                        case BRANCH -> ((BranchInstruction) i).target();
                        case DISCONTINUED_JSR -> ((DiscontinuedInstruction.JsrInstruction) i).target();
                        case LOOKUP_SWITCH -> {
                            var ls = (LookupSwitchInstruction) i;
                            ls.defaultTarget();
                            ls.cases();
                        }
                        case TABLE_SWITCH -> {
                            var ts = (TableSwitchInstruction) i;
                            ts.defaultTarget();
                            ts.cases();
                        }
                        default -> {}
                    }
                    pos += i.sizeInBytes();
                }
            }
            return;
        }
        int stackMapPos = ((BoundAttribute<StackMapTableAttribute>) a.get()).payloadStart;

        int bci = -1; //compensate for offsetDelta + 1
        int nEntries = classReader.readU2(stackMapPos);
        int p = stackMapPos + 2;
        for (int i = 0; i < nEntries; ++i) {
            int frameType = classReader.readU1(p);
            int offsetDelta;
            if (frameType < 64) {
                offsetDelta = frameType;
                ++p;
            }
            else if (frameType < 128) {
                offsetDelta = frameType & 0x3f;
                p = adjustForObjectOrUninitialized(p + 1);
            }
            else
                switch (frameType) {
                    case 247 -> {
                        offsetDelta = classReader.readU2(p + 1);
                        p = adjustForObjectOrUninitialized(p + 3);
                    }
                    case 248, 249, 250, 251 -> {
                        offsetDelta = classReader.readU2(p + 1);
                        p += 3;
                    }
                    case 252, 253, 254 -> {
                        offsetDelta = classReader.readU2(p + 1);
                        int k = frameType - 251;
                        p += 3;
                        for (int c = 0; c < k; ++c) {
                            p = adjustForObjectOrUninitialized(p);
                        }
                    }
                    case 255 -> {
                        offsetDelta = classReader.readU2(p + 1);
                        p += 3;
                        int k = classReader.readU2(p);
                        p += 2;
                        for (int c = 0; c < k; ++c) {
                            p = adjustForObjectOrUninitialized(p);
                        }
                        k = classReader.readU2(p);
                        p += 2;
                        for (int c = 0; c < k; ++c) {
                            p = adjustForObjectOrUninitialized(p);
                        }
                    }
                    default -> throw new IllegalArgumentException("Bad frame type: " + frameType);
                }
            bci += offsetDelta + 1;
            inflateLabel(bci);
        }
    }

    private void inflateTypeAnnotations() {
        findAttribute(Attributes.runtimeVisibleTypeAnnotations()).ifPresent(RuntimeVisibleTypeAnnotationsAttribute::annotations);
        findAttribute(Attributes.runtimeInvisibleTypeAnnotations()).ifPresent(RuntimeInvisibleTypeAnnotationsAttribute::annotations);
    }

    private void generateCatchTargets(Consumer<? super CodeElement> consumer) {
        // We attach all catch targets to bci zero, because trying to attach them
        // to their range could subtly affect the order of exception processing
        iterateExceptionHandlers(new ExceptionHandlerAction() {
            @Override
            public void accept(int s, int e, int h, int c) {
                ClassEntry catchType = c == 0
                                                    ? null
                                                    : classReader.entryByIndex(c, ClassEntry.class);
                consumer.accept(new AbstractPseudoInstruction.ExceptionCatchImpl(getLabel(h), getLabel(s), getLabel(e), catchType));
            }
        });
    }

    private void generateDebugElements(Consumer<? super CodeElement> consumer) {
        for (Attribute<?> a : attributes()) {
            if (a.attributeMapper() == Attributes.characterRangeTable()) {
                var attr = (BoundCharacterRangeTableAttribute) a;
                int cnt = classReader.readU2(attr.payloadStart);
                int p = attr.payloadStart + 2;
                int pEnd = p + (cnt * 14);
                for (; p < pEnd; p += 14) {
                    var instruction = new BoundCharacterRange(this, p);
                    inflateLabel(instruction.startPc());
                    inflateLabel(instruction.endPc() + 1);
                    consumer.accept(instruction);
                }
            }
            else if (a.attributeMapper() == Attributes.localVariableTable()) {
                var attr = (BoundLocalVariableTableAttribute) a;
                int cnt = classReader.readU2(attr.payloadStart);
                int p = attr.payloadStart + 2;
                int pEnd = p + (cnt * 10);
                for (; p < pEnd; p += 10) {
                    BoundLocalVariable instruction = new BoundLocalVariable(this, p);
                    inflateLabel(instruction.startPc());
                    inflateLabel(instruction.startPc() + instruction.length());
                    consumer.accept(instruction);
                }
            }
            else if (a.attributeMapper() == Attributes.localVariableTypeTable()) {
                var attr = (BoundLocalVariableTypeTableAttribute) a;
                int cnt = classReader.readU2(attr.payloadStart);
                int p = attr.payloadStart + 2;
                int pEnd = p + (cnt * 10);
                for (; p < pEnd; p += 10) {
                    BoundLocalVariableType instruction = new BoundLocalVariableType(this, p);
                    inflateLabel(instruction.startPc());
                    inflateLabel(instruction.startPc() + instruction.length());
                    consumer.accept(instruction);
                }
            }
            else if (a.attributeMapper() == Attributes.runtimeVisibleTypeAnnotations()) {
                consumer.accept((BoundRuntimeVisibleTypeAnnotationsAttribute) a);
            }
            else if (a.attributeMapper() == Attributes.runtimeInvisibleTypeAnnotations()) {
                consumer.accept((BoundRuntimeInvisibleTypeAnnotationsAttribute) a);
            }
        }
    }

    public interface ExceptionHandlerAction {
        void accept(int start, int end, int handler, int catchTypeIndex);
    }

    public void iterateExceptionHandlers(ExceptionHandlerAction a) {
        int p = exceptionHandlerPos + 2;
        for (int i = 0; i < exceptionHandlerCnt; ++i) {
            a.accept(classReader.readU2(p), classReader.readU2(p + 2), classReader.readU2(p + 4), classReader.readU2(p + 6));
            p += 8;
        }
    }

    private Instruction bcToInstruction(int bc, int pos) {
        return switch (bc) {
            case BIPUSH -> new AbstractInstruction.BoundArgumentConstantInstruction(Opcode.BIPUSH, CodeImpl.this, pos);
            case SIPUSH -> new AbstractInstruction.BoundArgumentConstantInstruction(Opcode.SIPUSH, CodeImpl.this, pos);
            case LDC -> new AbstractInstruction.BoundLoadConstantInstruction(Opcode.LDC, CodeImpl.this, pos);
            case LDC_W -> new AbstractInstruction.BoundLoadConstantInstruction(Opcode.LDC_W, CodeImpl.this, pos);
            case LDC2_W -> new AbstractInstruction.BoundLoadConstantInstruction(Opcode.LDC2_W, CodeImpl.this, pos);
            case ILOAD -> new AbstractInstruction.BoundLoadInstruction(Opcode.ILOAD, CodeImpl.this, pos);
            case LLOAD -> new AbstractInstruction.BoundLoadInstruction(Opcode.LLOAD, CodeImpl.this, pos);
            case FLOAD -> new AbstractInstruction.BoundLoadInstruction(Opcode.FLOAD, CodeImpl.this, pos);
            case DLOAD -> new AbstractInstruction.BoundLoadInstruction(Opcode.DLOAD, CodeImpl.this, pos);
            case ALOAD -> new AbstractInstruction.BoundLoadInstruction(Opcode.ALOAD, CodeImpl.this, pos);
            case ISTORE -> new AbstractInstruction.BoundStoreInstruction(Opcode.ISTORE, CodeImpl.this, pos);
            case LSTORE -> new AbstractInstruction.BoundStoreInstruction(Opcode.LSTORE, CodeImpl.this, pos);
            case FSTORE -> new AbstractInstruction.BoundStoreInstruction(Opcode.FSTORE, CodeImpl.this, pos);
            case DSTORE -> new AbstractInstruction.BoundStoreInstruction(Opcode.DSTORE, CodeImpl.this, pos);
            case ASTORE -> new AbstractInstruction.BoundStoreInstruction(Opcode.ASTORE, CodeImpl.this, pos);
            case IINC -> new AbstractInstruction.BoundIncrementInstruction(Opcode.IINC, CodeImpl.this, pos);
            case IFEQ -> new AbstractInstruction.BoundBranchInstruction(Opcode.IFEQ, CodeImpl.this, pos);
            case IFNE -> new AbstractInstruction.BoundBranchInstruction(Opcode.IFNE, CodeImpl.this, pos);
            case IFLT -> new AbstractInstruction.BoundBranchInstruction(Opcode.IFLT, CodeImpl.this, pos);
            case IFGE -> new AbstractInstruction.BoundBranchInstruction(Opcode.IFGE, CodeImpl.this, pos);
            case IFGT -> new AbstractInstruction.BoundBranchInstruction(Opcode.IFGT, CodeImpl.this, pos);
            case IFLE -> new AbstractInstruction.BoundBranchInstruction(Opcode.IFLE, CodeImpl.this, pos);
            case IF_ICMPEQ -> new AbstractInstruction.BoundBranchInstruction(Opcode.IF_ICMPEQ, CodeImpl.this, pos);
            case IF_ICMPNE -> new AbstractInstruction.BoundBranchInstruction(Opcode.IF_ICMPNE, CodeImpl.this, pos);
            case IF_ICMPLT -> new AbstractInstruction.BoundBranchInstruction(Opcode.IF_ICMPLT, CodeImpl.this, pos);
            case IF_ICMPGE -> new AbstractInstruction.BoundBranchInstruction(Opcode.IF_ICMPGE, CodeImpl.this, pos);
            case IF_ICMPGT -> new AbstractInstruction.BoundBranchInstruction(Opcode.IF_ICMPGT, CodeImpl.this, pos);
            case IF_ICMPLE -> new AbstractInstruction.BoundBranchInstruction(Opcode.IF_ICMPLE, CodeImpl.this, pos);
            case IF_ACMPEQ -> new AbstractInstruction.BoundBranchInstruction(Opcode.IF_ACMPEQ, CodeImpl.this, pos);
            case IF_ACMPNE -> new AbstractInstruction.BoundBranchInstruction(Opcode.IF_ACMPNE, CodeImpl.this, pos);
            case GOTO -> new AbstractInstruction.BoundBranchInstruction(Opcode.GOTO, CodeImpl.this, pos);
            case TABLESWITCH -> new AbstractInstruction.BoundTableSwitchInstruction(Opcode.TABLESWITCH, CodeImpl.this, pos);
            case LOOKUPSWITCH -> new AbstractInstruction.BoundLookupSwitchInstruction(Opcode.LOOKUPSWITCH, CodeImpl.this, pos);
            case GETSTATIC -> new AbstractInstruction.BoundFieldInstruction(Opcode.GETSTATIC, CodeImpl.this, pos);
            case PUTSTATIC -> new AbstractInstruction.BoundFieldInstruction(Opcode.PUTSTATIC, CodeImpl.this, pos);
            case GETFIELD -> new AbstractInstruction.BoundFieldInstruction(Opcode.GETFIELD, CodeImpl.this, pos);
            case PUTFIELD -> new AbstractInstruction.BoundFieldInstruction(Opcode.PUTFIELD, CodeImpl.this, pos);
            case INVOKEVIRTUAL -> new AbstractInstruction.BoundInvokeInstruction(Opcode.INVOKEVIRTUAL, CodeImpl.this, pos);
            case INVOKESPECIAL -> new AbstractInstruction.BoundInvokeInstruction(Opcode.INVOKESPECIAL, CodeImpl.this, pos);
            case INVOKESTATIC -> new AbstractInstruction.BoundInvokeInstruction(Opcode.INVOKESTATIC, CodeImpl.this, pos);
            case INVOKEINTERFACE -> new AbstractInstruction.BoundInvokeInterfaceInstruction(Opcode.INVOKEINTERFACE, CodeImpl.this, pos);
            case INVOKEDYNAMIC -> new AbstractInstruction.BoundInvokeDynamicInstruction(Opcode.INVOKEDYNAMIC, CodeImpl.this, pos);
            case NEW -> new AbstractInstruction.BoundNewObjectInstruction(CodeImpl.this, pos);
            case NEWARRAY -> new AbstractInstruction.BoundNewPrimitiveArrayInstruction(Opcode.NEWARRAY, CodeImpl.this, pos);
            case ANEWARRAY -> new AbstractInstruction.BoundNewReferenceArrayInstruction(Opcode.ANEWARRAY, CodeImpl.this, pos);
            case CHECKCAST -> new AbstractInstruction.BoundTypeCheckInstruction(Opcode.CHECKCAST, CodeImpl.this, pos);
            case INSTANCEOF -> new AbstractInstruction.BoundTypeCheckInstruction(Opcode.INSTANCEOF, CodeImpl.this, pos);

            case WIDE -> {
                int bclow = classReader.readU1(pos + 1);
                yield switch (bclow) {
                    case ILOAD -> new AbstractInstruction.BoundLoadInstruction(Opcode.ILOAD_W, this, pos);
                    case LLOAD -> new AbstractInstruction.BoundLoadInstruction(Opcode.LLOAD_W, this, pos);
                    case FLOAD -> new AbstractInstruction.BoundLoadInstruction(Opcode.FLOAD_W, this, pos);
                    case DLOAD -> new AbstractInstruction.BoundLoadInstruction(Opcode.DLOAD_W, this, pos);
                    case ALOAD -> new AbstractInstruction.BoundLoadInstruction(Opcode.ALOAD_W, this, pos);
                    case ISTORE -> new AbstractInstruction.BoundStoreInstruction(Opcode.ISTORE_W, this, pos);
                    case LSTORE -> new AbstractInstruction.BoundStoreInstruction(Opcode.LSTORE_W, this, pos);
                    case FSTORE -> new AbstractInstruction.BoundStoreInstruction(Opcode.FSTORE_W, this, pos);
                    case DSTORE -> new AbstractInstruction.BoundStoreInstruction(Opcode.DSTORE_W, this, pos);
                    case ASTORE -> new AbstractInstruction.BoundStoreInstruction(Opcode.ASTORE_W, this, pos);
                    case IINC -> new AbstractInstruction.BoundIncrementInstruction(Opcode.IINC_W, this, pos);
                    case RET ->  new AbstractInstruction.BoundRetInstruction(Opcode.RET_W, this, pos);
                    default -> throw new IllegalArgumentException("unknown wide instruction: " + bclow);
                };
            }

            case MULTIANEWARRAY -> new AbstractInstruction.BoundNewMultidimensionalArrayInstruction(Opcode.MULTIANEWARRAY, CodeImpl.this, pos);
            case IFNULL -> new AbstractInstruction.BoundBranchInstruction(Opcode.IFNULL, CodeImpl.this, pos);
            case IFNONNULL -> new AbstractInstruction.BoundBranchInstruction(Opcode.IFNONNULL, CodeImpl.this, pos);
            case GOTO_W -> new AbstractInstruction.BoundBranchInstruction(Opcode.GOTO_W, CodeImpl.this, pos);

            case JSR -> new AbstractInstruction.BoundJsrInstruction(Opcode.JSR, CodeImpl.this, pos);
            case RET ->  new AbstractInstruction.BoundRetInstruction(Opcode.RET, this, pos);
            case JSR_W -> new AbstractInstruction.BoundJsrInstruction(Opcode.JSR_W, CodeImpl.this, pos);
            default -> {
                Instruction instr = SINGLETON_INSTRUCTIONS[bc];
                if (instr == null)
                    throw new IllegalArgumentException("unknown instruction: " + bc);
                yield instr;
            }
        };
    }

    @Override
    public String toString() {
        return String.format("CodeModel[id=%d]", System.identityHashCode(this));
    }
}
