package p.studio.compiler.backend.irvm;

import p.studio.compiler.backend.bytecode.BytecodeEmitter;
import p.studio.compiler.backend.bytecode.BytecodeModule;
import p.studio.compiler.models.IRBackend;
import p.studio.compiler.models.IRBackendExecutableFunction;
import p.studio.compiler.models.IRSyntheticCallableKind;
import p.studio.compiler.models.IRHiddenGlobalKind;
import p.studio.compiler.source.Span;
import p.studio.compiler.source.identifiers.CallableId;
import p.studio.compiler.source.identifiers.ModuleId;
import p.studio.compiler.source.tables.ModuleReference;
import p.studio.utilities.structures.ReadOnlyList;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.StringJoiner;

public class LowerToIRVMService {
    private final IRVMValidator validator;

    public LowerToIRVMService() {
        this(new IRVMValidator());
    }

    LowerToIRVMService(final IRVMValidator validator) {
        this.validator = validator;
    }

    public IRVMProgram lower(final IRBackend backend) {
        return lower(backend, "core-v1");
    }

    public IRVMProgram lower(
            final IRBackend backend,
            final String vmProfile) {
        if (backend == null || backend.getExecutableFunctions().isEmpty()) {
            throw new IRVMLoweringException(
                    IRVMLoweringErrorCode.LOWER_IRVM_EMPTY_EXECUTABLE_INPUT,
                    "IRBackend has no executable functions");
        }

        validatePbsLifecycleStructure(backend);
        final var ordered = orderFunctions(backend);
        final var funcIdByCallableId = new HashMap<CallableId, Integer>();
        for (var i = 0; i < ordered.size(); i++) {
            final var fn = ordered.get(i);
            if (funcIdByCallableId.putIfAbsent(fn.callableId(), i) != null) {
                throw loweringError(
                        fn,
                        IRVMLoweringErrorCode.LOWER_IRVM_MISSING_CALLEE,
                        "duplicate callable id in executable set: " + fn.callableId());
            }
        }

        final var loweredFunctions = new ArrayList<IRVMFunction>(ordered.size());
        final var emissionFunctions = new ArrayList<BytecodeEmitter.FunctionPlan>(ordered.size());
        final var constPool = new ArrayList<BytecodeModule.ConstantPoolEntry>();
        final var constPoolIndexByString = new HashMap<String, Integer>();

        for (var i = 0; i < ordered.size(); i++) {
            final var fn = ordered.get(i);
            final var instructions = new ArrayList<IRVMInstruction>(fn.instructions().size());
            final var operations = new ArrayList<BytecodeEmitter.Operation>(fn.instructions().size());
            final var labelToPc = new HashMap<String, Integer>();
            final var jumpPatches = new ArrayList<JumpPatch>();
            var functionPc = 0;
            for (final var instr : fn.instructions()) {
                final var sourceSpan = toBytecodeSpan(fn.fileId().getId(), instr.span());
                switch (instr.kind()) {
                    case PUSH_I32 -> {
                        final int value;
                        try {
                            value = Integer.parseInt(instr.label());
                        } catch (NumberFormatException e) {
                            throw loweringError(
                                    fn,
                                    instr,
                                    IRVMLoweringErrorCode.LOWER_IRVM_INVALID_INTRINSIC_ID,
                                    "invalid PUSH_I32 literal payload: " + instr.label());
                        }
                        instructions.add(new IRVMInstruction(IRVMOp.PUSH_I32, value));
                        operations.add(BytecodeEmitter.Operation.pushI32(value, sourceSpan));
                        functionPc += IRVMOp.PUSH_I32.immediateSize() + 2;
                    }
                    case PUSH_CONST -> {
                        final var value = instr.label() == null ? "" : instr.label();
                        final var constPoolIndex = constPoolIndexByString.computeIfAbsent(value, ignored -> {
                            constPool.add(new BytecodeModule.StringConstant(value));
                            return constPool.size() - 1;
                        });
                        instructions.add(new IRVMInstruction(IRVMOp.PUSH_CONST, constPoolIndex));
                        operations.add(BytecodeEmitter.Operation.pushConst(constPoolIndex, sourceSpan));
                        functionPc += IRVMOp.PUSH_CONST.immediateSize() + 2;
                    }
                    case PUSH_BOOL -> {
                        final var raw = instr.label() == null ? "" : instr.label().trim().toLowerCase();
                        final Integer value = switch (raw) {
                            case "1", "true" -> 1;
                            case "0", "false" -> 0;
                            default -> null;
                        };
                        if (value == null) {
                            throw loweringError(
                                    fn,
                                    instr,
                                    IRVMLoweringErrorCode.LOWER_IRVM_INVALID_INTRINSIC_ID,
                                    "invalid PUSH_BOOL literal payload: " + instr.label());
                        }
                        instructions.add(new IRVMInstruction(IRVMOp.PUSH_BOOL, value));
                        operations.add(BytecodeEmitter.Operation.pushBool(value != 0, sourceSpan));
                        functionPc += IRVMOp.PUSH_BOOL.immediateSize() + 2;
                    }
                    case GET_LOCAL -> {
                        final var slot = instr.expectedArgSlots();
                        if (slot == null) {
                            throw loweringError(
                                    fn,
                                    instr,
                                    IRVMLoweringErrorCode.LOWER_IRVM_MISSING_CALLEE,
                                    "missing local slot for GET_LOCAL");
                        }
                        instructions.add(new IRVMInstruction(IRVMOp.GET_LOCAL, slot));
                        operations.add(BytecodeEmitter.Operation.getLocal(slot, sourceSpan));
                        functionPc += IRVMOp.GET_LOCAL.immediateSize() + 2;
                    }
                    case GET_GLOBAL -> {
                        final var slot = instr.expectedArgSlots();
                        if (slot == null) {
                            throw loweringError(
                                    fn,
                                    instr,
                                    IRVMLoweringErrorCode.LOWER_IRVM_MISSING_CALLEE,
                                    "missing global slot for GET_GLOBAL");
                        }
                        instructions.add(new IRVMInstruction(IRVMOp.GET_GLOBAL, slot));
                        operations.add(BytecodeEmitter.Operation.getGlobal(slot, sourceSpan));
                        functionPc += IRVMOp.GET_GLOBAL.immediateSize() + 2;
                    }
                    case SET_LOCAL -> {
                        final var slot = instr.expectedArgSlots();
                        if (slot == null) {
                            throw loweringError(
                                    fn,
                                    instr,
                                    IRVMLoweringErrorCode.LOWER_IRVM_MISSING_CALLEE,
                                    "missing local slot for SET_LOCAL");
                        }
                        instructions.add(new IRVMInstruction(IRVMOp.SET_LOCAL, slot));
                        operations.add(BytecodeEmitter.Operation.setLocal(slot, sourceSpan));
                        functionPc += IRVMOp.SET_LOCAL.immediateSize() + 2;
                    }
                    case SET_GLOBAL -> {
                        final var slot = instr.expectedArgSlots();
                        if (slot == null) {
                            throw loweringError(
                                    fn,
                                    instr,
                                    IRVMLoweringErrorCode.LOWER_IRVM_MISSING_CALLEE,
                                    "missing global slot for SET_GLOBAL");
                        }
                        instructions.add(new IRVMInstruction(IRVMOp.SET_GLOBAL, slot));
                        operations.add(BytecodeEmitter.Operation.setGlobal(slot, sourceSpan));
                        functionPc += IRVMOp.SET_GLOBAL.immediateSize() + 2;
                    }
                    case POP -> {
                        instructions.add(new IRVMInstruction(IRVMOp.POP, null));
                        operations.add(BytecodeEmitter.Operation.pop(sourceSpan));
                        functionPc += IRVMOp.POP.immediateSize() + 2;
                    }
                    case ADD -> {
                        instructions.add(new IRVMInstruction(IRVMOp.ADD, null));
                        operations.add(BytecodeEmitter.Operation.add(sourceSpan));
                        functionPc += IRVMOp.ADD.immediateSize() + 2;
                    }
                    case SUB -> {
                        instructions.add(new IRVMInstruction(IRVMOp.SUB, null));
                        operations.add(BytecodeEmitter.Operation.sub(sourceSpan));
                        functionPc += IRVMOp.SUB.immediateSize() + 2;
                    }
                    case MUL -> {
                        instructions.add(new IRVMInstruction(IRVMOp.MUL, null));
                        operations.add(BytecodeEmitter.Operation.mul(sourceSpan));
                        functionPc += IRVMOp.MUL.immediateSize() + 2;
                    }
                    case DIV -> {
                        instructions.add(new IRVMInstruction(IRVMOp.DIV, null));
                        operations.add(BytecodeEmitter.Operation.div(sourceSpan));
                        functionPc += IRVMOp.DIV.immediateSize() + 2;
                    }
                    case MOD -> {
                        instructions.add(new IRVMInstruction(IRVMOp.MOD, null));
                        operations.add(BytecodeEmitter.Operation.mod(sourceSpan));
                        functionPc += IRVMOp.MOD.immediateSize() + 2;
                    }
                    case EQ -> {
                        instructions.add(new IRVMInstruction(IRVMOp.EQ, null));
                        operations.add(BytecodeEmitter.Operation.eq(sourceSpan));
                        functionPc += IRVMOp.EQ.immediateSize() + 2;
                    }
                    case NEQ -> {
                        instructions.add(new IRVMInstruction(IRVMOp.NEQ, null));
                        operations.add(BytecodeEmitter.Operation.neq(sourceSpan));
                        functionPc += IRVMOp.NEQ.immediateSize() + 2;
                    }
                    case LT -> {
                        instructions.add(new IRVMInstruction(IRVMOp.LT, null));
                        operations.add(BytecodeEmitter.Operation.lt(sourceSpan));
                        functionPc += IRVMOp.LT.immediateSize() + 2;
                    }
                    case LTE -> {
                        instructions.add(new IRVMInstruction(IRVMOp.LTE, null));
                        operations.add(BytecodeEmitter.Operation.lte(sourceSpan));
                        functionPc += IRVMOp.LTE.immediateSize() + 2;
                    }
                    case GT -> {
                        instructions.add(new IRVMInstruction(IRVMOp.GT, null));
                        operations.add(BytecodeEmitter.Operation.gt(sourceSpan));
                        functionPc += IRVMOp.GT.immediateSize() + 2;
                    }
                    case GTE -> {
                        instructions.add(new IRVMInstruction(IRVMOp.GTE, null));
                        operations.add(BytecodeEmitter.Operation.gte(sourceSpan));
                        functionPc += IRVMOp.GTE.immediateSize() + 2;
                    }
                    case AND -> {
                        instructions.add(new IRVMInstruction(IRVMOp.AND, null));
                        operations.add(BytecodeEmitter.Operation.and(sourceSpan));
                        functionPc += IRVMOp.AND.immediateSize() + 2;
                    }
                    case OR -> {
                        instructions.add(new IRVMInstruction(IRVMOp.OR, null));
                        operations.add(BytecodeEmitter.Operation.or(sourceSpan));
                        functionPc += IRVMOp.OR.immediateSize() + 2;
                    }
                    case NOT -> {
                        instructions.add(new IRVMInstruction(IRVMOp.NOT, null));
                        operations.add(BytecodeEmitter.Operation.not(sourceSpan));
                        functionPc += IRVMOp.NOT.immediateSize() + 2;
                    }
                    case NEG -> {
                        instructions.add(new IRVMInstruction(IRVMOp.NEG, null));
                        operations.add(BytecodeEmitter.Operation.neg(sourceSpan));
                        functionPc += IRVMOp.NEG.immediateSize() + 2;
                    }
                    case HALT -> {
                        instructions.add(new IRVMInstruction(IRVMOp.HALT, null));
                        operations.add(BytecodeEmitter.Operation.halt(sourceSpan));
                        functionPc += IRVMOp.HALT.immediateSize() + 2;
                    }
                    case RET -> {
                        instructions.add(new IRVMInstruction(IRVMOp.RET, null));
                        operations.add(BytecodeEmitter.Operation.ret(sourceSpan));
                        functionPc += IRVMOp.RET.immediateSize() + 2;
                    }
                    case CALL_FUNC -> {
                        if (instr.calleeCallableId() == null) {
                            throw loweringError(
                                    fn,
                                    instr,
                                    IRVMLoweringErrorCode.LOWER_IRVM_MISSING_CALLEE,
                                    "missing callee callable id");
                        }
                        final var calleeId = funcIdByCallableId.get(instr.calleeCallableId());
                        if (calleeId == null) {
                            throw loweringError(
                                    fn,
                                    instr,
                                    IRVMLoweringErrorCode.LOWER_IRVM_MISSING_CALLEE,
                                    "missing callee function for callable_id=" + instr.calleeCallableId());
                        }
                        final var calleeFunction = ordered.get(calleeId);
                        if (instr.expectedArgSlots() != null
                                && instr.expectedArgSlots() != calleeFunction.paramSlots()) {
                            throw loweringError(
                                    fn,
                                    instr,
                                    IRVMLoweringErrorCode.LOWER_IRVM_CALL_ARG_SLOTS_MISMATCH,
                                    "call arg_slots mismatch for %s. expected=%d actual=%d".formatted(
                                            instr.calleeCallableName(),
                                            instr.expectedArgSlots(),
                                            calleeFunction.paramSlots()));
                        }
                        if (instr.expectedRetSlots() != null
                                && instr.expectedRetSlots() != calleeFunction.returnSlots()) {
                            throw loweringError(
                                    fn,
                                    instr,
                                    IRVMLoweringErrorCode.LOWER_IRVM_CALL_RET_SLOTS_MISMATCH,
                                    "call ret_slots mismatch for %s. expected=%d actual=%d".formatted(
                                            instr.calleeCallableName(),
                                            instr.expectedRetSlots(),
                                            calleeFunction.returnSlots()));
                        }
                        instructions.add(new IRVMInstruction(IRVMOp.CALL, calleeId));
                        operations.add(BytecodeEmitter.Operation.callFunc(calleeId, sourceSpan));
                        functionPc += IRVMOp.CALL.immediateSize() + 2;
                    }
                    case CALL_HOST -> {
                        final var host = instr.hostCall();
                        instructions.add(new IRVMInstruction(IRVMOp.HOSTCALL, 0));
                        operations.add(BytecodeEmitter.Operation.hostcall(
                                new p.studio.compiler.backend.bytecode.BytecodeModule.SyscallDecl(
                                        host.module(),
                                        host.name(),
                                        (int) host.version(),
                                        host.argSlots(),
                                        host.retSlots()),
                                host.argSlots(),
                                host.retSlots(),
                                sourceSpan));
                        functionPc += IRVMOp.HOSTCALL.immediateSize() + 2;
                    }
                    case CALL_INTRINSIC -> {
                        final var intrinsic = instr.intrinsicCall();
                        final var intrinsicIndex = intrinsic.intrinsicId().getIndex();
                        if (intrinsicIndex < 0 || intrinsicIndex >= backend.getIntrinsicPool().size()) {
                            throw loweringError(
                                    fn,
                                    instr,
                                    IRVMLoweringErrorCode.LOWER_IRVM_INVALID_INTRINSIC_ID,
                                    "invalid intrinsic id: " + intrinsic.intrinsicId());
                        }
                        final var canonicalIntrinsic = backend.getIntrinsicPool().get(intrinsicIndex);
                        if (!canonicalIntrinsic.canonicalName().equals(intrinsic.canonicalName())
                                || canonicalIntrinsic.canonicalVersion() != intrinsic.canonicalVersion()) {
                            throw loweringError(
                                    fn,
                                    instr,
                                    IRVMLoweringErrorCode.LOWER_IRVM_INVALID_INTRINSIC_ID,
                                    "intrinsic identity mismatch between callsite and pool: call=%s@%d pool=%s@%d".formatted(
                                            intrinsic.canonicalName(),
                                            intrinsic.canonicalVersion(),
                                            canonicalIntrinsic.canonicalName(),
                                            canonicalIntrinsic.canonicalVersion()));
                        }
                        final var finalIntrinsicId = IRVMIntrinsicRegistry
                                .resolveFinalId(canonicalIntrinsic.canonicalName(), canonicalIntrinsic.canonicalVersion())
                                .orElseThrow(() -> loweringError(
                                        fn,
                                        instr,
                                        IRVMLoweringErrorCode.LOWER_IRVM_INVALID_INTRINSIC_ID,
                                        "unknown VM-owned intrinsic identity: %s@%d".formatted(
                                                canonicalIntrinsic.canonicalName(),
                                                canonicalIntrinsic.canonicalVersion())));
                        instructions.add(new IRVMInstruction(IRVMOp.INTRINSIC, finalIntrinsicId));
                        operations.add(BytecodeEmitter.Operation.intrinsic(
                                finalIntrinsicId,
                                instr.expectedArgSlots(),
                                instr.expectedRetSlots(),
                                sourceSpan));
                        functionPc += IRVMOp.INTRINSIC.immediateSize() + 2;
                    }
                    case LABEL -> {
                        final var label = instr.label();
                        if (labelToPc.putIfAbsent(label, functionPc) != null) {
                            throw loweringError(
                                    fn,
                                    instr,
                                    IRVMLoweringErrorCode.LOWER_IRVM_MISSING_JUMP_TARGET,
                                    "duplicate label in function '" + fn.callableName() + "': " + label);
                        }
                    }
                    case JMP -> {
                        instructions.add(new IRVMInstruction(IRVMOp.JMP, 0));
                        operations.add(BytecodeEmitter.Operation.jmp(0, sourceSpan));
                        jumpPatches.add(new JumpPatch(instructions.size() - 1, operations.size() - 1, instr.targetLabel(), IRVMOp.JMP, sourceSpan));
                        functionPc += IRVMOp.JMP.immediateSize() + 2;
                    }
                    case JMP_IF_TRUE -> {
                        instructions.add(new IRVMInstruction(IRVMOp.JMP_IF_TRUE, 0));
                        operations.add(BytecodeEmitter.Operation.jmpIfTrue(0, sourceSpan));
                        jumpPatches.add(new JumpPatch(instructions.size() - 1, operations.size() - 1, instr.targetLabel(), IRVMOp.JMP_IF_TRUE, sourceSpan));
                        functionPc += IRVMOp.JMP_IF_TRUE.immediateSize() + 2;
                    }
                    case JMP_IF_FALSE -> {
                        instructions.add(new IRVMInstruction(IRVMOp.JMP_IF_FALSE, 0));
                        operations.add(BytecodeEmitter.Operation.jmpIfFalse(0, sourceSpan));
                        jumpPatches.add(new JumpPatch(instructions.size() - 1, operations.size() - 1, instr.targetLabel(), IRVMOp.JMP_IF_FALSE, sourceSpan));
                        functionPc += IRVMOp.JMP_IF_FALSE.immediateSize() + 2;
                    }
                }
            }
            for (final var patch : jumpPatches) {
                final var targetPc = labelToPc.get(patch.targetLabel());
                if (targetPc == null) {
                    throw loweringError(
                            fn,
                            IRVMLoweringErrorCode.LOWER_IRVM_MISSING_JUMP_TARGET,
                            "missing jump target label '" + patch.targetLabel() + "' in function '" + fn.callableName() + "'");
                }
                instructions.set(patch.instructionIndex(), new IRVMInstruction(patch.op(), targetPc));
                final BytecodeEmitter.Operation operation;
                if (patch.op() == IRVMOp.JMP) {
                    operation = BytecodeEmitter.Operation.jmp(targetPc, patch.span());
                } else if (patch.op() == IRVMOp.JMP_IF_TRUE) {
                    operation = BytecodeEmitter.Operation.jmpIfTrue(targetPc, patch.span());
                } else if (patch.op() == IRVMOp.JMP_IF_FALSE) {
                    operation = BytecodeEmitter.Operation.jmpIfFalse(targetPc, patch.span());
                } else {
                    throw new IllegalStateException("unexpected jump op: " + patch.op().name());
                }
                operations.set(patch.operationIndex(), operation);
            }
            loweredFunctions.add(new IRVMFunction(
                    fn.callableName(),
                    fn.paramSlots(),
                    fn.localSlots(),
                    fn.returnSlots(),
                    fn.maxStackSlots(),
                    ReadOnlyList.wrap(instructions)));
            emissionFunctions.add(new BytecodeEmitter.FunctionPlan(
                    fn.callableName(),
                    fn.paramSlots(),
                    fn.localSlots(),
                    fn.returnSlots(),
                    fn.maxStackSlots(),
                    ReadOnlyList.wrap(operations)));
        }

        final var program = new IRVMProgram(
                new IRVMModule(vmProfile, ReadOnlyList.wrap(loweredFunctions)),
                new BytecodeEmitter.EmissionPlan(
                        0,
                        ReadOnlyList.wrap(constPool),
                        ReadOnlyList.empty(),
                        ReadOnlyList.wrap(emissionFunctions)));
        validator.validate(program, false);
        return program;
    }

    private ReadOnlyList<IRBackendExecutableFunction> orderFunctions(final IRBackend backend) {
        final var entryPointCallableName = backend.getEntryPointCallableName();
        if (entryPointCallableName == null || entryPointCallableName.isBlank()) {
            throw new IRVMLoweringException(
                    IRVMLoweringErrorCode.LOWER_IRVM_ENTRYPOINT_DECLARATION_MISSING,
                    "frontend IRBackend entrypoint declaration is missing");
        }
        final var entryPointModuleId = backend.getEntryPointModuleId();
        if (entryPointModuleId == null || entryPointModuleId.isNone()) {
            throw new IRVMLoweringException(
                    IRVMLoweringErrorCode.LOWER_IRVM_ENTRYPOINT_DECLARATION_MISSING,
                    "frontend IRBackend qualified entrypoint declaration is missing entryPointModuleId");
        }
        final var sorted = new ArrayList<>(backend.getExecutableFunctions().asList());
        sorted.sort((left, right) -> {
            final var leftModuleKey = moduleSortKey(backend, left.moduleId());
            final var rightModuleKey = moduleSortKey(backend, right.moduleId());
            final var moduleCmp = leftModuleKey.compareTo(rightModuleKey);
            if (moduleCmp != 0) {
                return moduleCmp;
            }
            final var nameCmp = left.callableName().compareTo(right.callableName());
            if (nameCmp != 0) {
                return nameCmp;
            }
            final var idCmp = Integer.compare(left.callableId().getIndex(), right.callableId().getIndex());
            if (idCmp != 0) {
                return idCmp;
            }
            return Integer.compare(left.sourceStart(), right.sourceStart());
        });
        final var expectedModuleIndex = entryPointModuleId.getIndex();
        final var entrypoints = sorted.stream()
                .filter(candidate -> entryPointCallableName.equals(candidate.callableName()))
                .filter(candidate -> !candidate.moduleId().isNone() && candidate.moduleId().getIndex() == expectedModuleIndex)
                .toList();
        if (entrypoints.isEmpty()) {
            throw new IRVMLoweringException(
                    IRVMLoweringErrorCode.LOWER_IRVM_ENTRYPOINT_MISSING,
                    "missing qualified entrypoint callable '%s' for moduleId=%d".formatted(
                            entryPointCallableName,
                            entryPointModuleId.getIndex()));
        }
        if (entrypoints.size() > 1) {
            throw new IRVMLoweringException(
                    IRVMLoweringErrorCode.LOWER_IRVM_ENTRYPOINT_AMBIGUOUS,
                    "ambiguous qualified entrypoint: found %d callables named '%s' in moduleId=%d".formatted(
                            entrypoints.size(),
                            entryPointCallableName,
                            entryPointModuleId.getIndex()));
        }
        final var entrypoint = entrypoints.getFirst();
        final var publishedWrapper = resolvePublishedWrapper(backend);
        if (publishedWrapper != null
                && (!entryPointCallableName.equals(publishedWrapper.callableName())
                || !sameModuleIdentity(entryPointModuleId, publishedWrapper.moduleId()))) {
            throw new IRVMLoweringException(
                    IRVMLoweringErrorCode.LOWER_IRVM_PUBLISHED_ENTRYPOINT_NOT_ZERO,
                    "published entrypoint is not function index 0 because entrypoint identity does not select the published wrapper");
        }
        final var ordered = new ArrayList<IRBackendExecutableFunction>(sorted.size());
        ordered.add(entrypoint);
        for (final var fn : sorted) {
            if (fn != entrypoint) {
                ordered.add(fn);
            }
        }
        return ReadOnlyList.wrap(ordered);
    }

    private void validatePbsLifecycleStructure(final IRBackend backend) {
        if (backend.getSyntheticFunctions().isEmpty()) {
            return;
        }
        for (final var syntheticFunction : backend.getSyntheticFunctions()) {
            if (syntheticFunction.origin() == null
                    || syntheticFunction.origin().fileId() == null
                    || syntheticFunction.origin().anchorCallableName().isBlank()
                    || syntheticFunction.origin().span() == null
                    || syntheticFunction.origin().span().isNone()) {
                throw new IRVMLoweringException(
                        IRVMLoweringErrorCode.LOWER_IRVM_SYNTHETIC_ORIGIN_MISSING,
                        "synthetic callable origin missing for " + syntheticFunction.callableName());
            }
        }

        final var publishedWrapper = resolvePublishedWrapper(backend);
        if (publishedWrapper == null) {
            return;
        }
        final var wrapperExecutablePresent = backend.getExecutableFunctions().stream()
                .anyMatch(function -> publishedWrapper.callableName().equals(function.callableName())
                        && sameModuleIdentity(publishedWrapper.moduleId(), function.moduleId()));
        if (!wrapperExecutablePresent) {
            throw new IRVMLoweringException(
                    IRVMLoweringErrorCode.LOWER_IRVM_SYNTHETIC_WRAPPER_ENTRYPOINT_MISSING,
                    "synthetic wrapper entrypoint missing: " + publishedWrapper.callableName());
        }

        final var bootGuardPresent = backend.getGlobals().stream()
                .anyMatch(global -> global.hiddenKind() == IRHiddenGlobalKind.BOOT_GUARD
                        && global.isHidden()
                        && sameModuleIdentity(publishedWrapper.moduleId(), global.moduleId()));
        if (!bootGuardPresent) {
            throw new IRVMLoweringException(
                    IRVMLoweringErrorCode.LOWER_IRVM_HIDDEN_BOOT_GUARD_MISSING,
                    "hidden boot guard is missing for moduleId=%d".formatted(
                            publishedWrapper.moduleId() == null || publishedWrapper.moduleId().isNone()
                                    ? -1
                                    : publishedWrapper.moduleId().getIndex()));
        }
    }

    private p.studio.compiler.models.IRSyntheticFunction resolvePublishedWrapper(final IRBackend backend) {
        p.studio.compiler.models.IRSyntheticFunction publishedWrapper = null;
        for (final var syntheticFunction : backend.getSyntheticFunctions()) {
            if (syntheticFunction.kind() != IRSyntheticCallableKind.PUBLISHED_FRAME_WRAPPER) {
                continue;
            }
            if (publishedWrapper != null) {
                throw new IRVMLoweringException(
                        IRVMLoweringErrorCode.LOWER_IRVM_ENTRYPOINT_AMBIGUOUS,
                        "multiple published frame wrappers found in executable backend");
            }
            publishedWrapper = syntheticFunction;
        }
        return publishedWrapper;
    }

    private boolean sameModuleIdentity(
            final ModuleId left,
            final ModuleId right) {
        final var normalizedLeft = left == null ? ModuleId.none() : left;
        final var normalizedRight = right == null ? ModuleId.none() : right;
        if (normalizedLeft.isNone() && normalizedRight.isNone()) {
            return true;
        }
        return !normalizedLeft.isNone()
                && !normalizedRight.isNone()
                && normalizedLeft.getIndex() == normalizedRight.getIndex();
    }

    private String moduleSortKey(
            final IRBackend backend,
            final ModuleId moduleId) {
        if (moduleId != null && !moduleId.isNone()) {
            final var index = moduleId.getIndex();
            if (index >= 0 && index < backend.getModulePool().size()) {
                return canonicalModuleSortKey(backend.getModulePool().get(index));
            }
        }
        return "";
    }

    private String canonicalModuleSortKey(final ModuleReference reference) {
        final var joiner = new StringJoiner("/", reference.project() + ":", "");
        for (final var segment : reference.pathSegments()) {
            joiner.add(segment);
        }
        return joiner.toString();
    }

    private BytecodeModule.SourceSpan toBytecodeSpan(
            final int fallbackFileId,
            final Span span) {
        final var sourceSpan = span == null ? Span.none() : span;
        final var fileId = sourceSpan.getFileId() == null || sourceSpan.getFileId().isNone()
                ? fallbackFileId
                : sourceSpan.getFileId().getId();
        return new BytecodeModule.SourceSpan(
                fileId,
                safeToInt(sourceSpan.getStart()),
                safeToInt(sourceSpan.getEnd()));
    }

    private int safeToInt(final long value) {
        if (value > Integer.MAX_VALUE) {
            return Integer.MAX_VALUE;
        }
        if (value < Integer.MIN_VALUE) {
            return Integer.MIN_VALUE;
        }
        return (int) value;
    }

    private IRVMLoweringException loweringError(
            final IRBackendExecutableFunction function,
            final IRVMLoweringErrorCode code,
            final String message) {
        return new IRVMLoweringException(
                code,
                message,
                function.fileId().getId(),
                function.sourceStart(),
                function.sourceEnd());
    }

    private IRVMLoweringException loweringError(
            final IRBackendExecutableFunction function,
            final IRBackendExecutableFunction.Instruction instruction,
            final IRVMLoweringErrorCode code,
            final String message) {
        return loweringError(function, instruction == null ? Span.none() : instruction.span(), code, message);
    }

    private IRVMLoweringException loweringError(
            final IRBackendExecutableFunction function,
            final Span span,
            final IRVMLoweringErrorCode code,
            final String message) {
        final var effectiveSpan = span == null ? Span.none() : span;
        final var fileId = effectiveSpan.getFileId() == null || effectiveSpan.getFileId().isNone()
                ? function.fileId().getId()
                : effectiveSpan.getFileId().getId();
        final int start;
        final int end;
        if (effectiveSpan.getFileId() == null || effectiveSpan.getFileId().isNone()) {
            start = function.sourceStart();
            end = function.sourceEnd();
        } else {
            start = safeToInt(effectiveSpan.getStart());
            end = safeToInt(effectiveSpan.getEnd());
        }
        return new IRVMLoweringException(code, message, fileId, start, end);
    }

    private record JumpPatch(
            int instructionIndex,
            int operationIndex,
            String targetLabel,
            IRVMOp op,
            BytecodeModule.SourceSpan span) {
    }
}
