package p.studio.compiler.pbs.lowering;

import p.studio.compiler.models.IRBackendExecutableFunction;
import p.studio.compiler.pbs.PbsCallableSlotCounter;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.semantics.PbsSemanticsErrors;
import p.studio.compiler.source.diagnostics.DiagnosticPhase;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.ModuleId;
import p.studio.compiler.source.identifiers.NameId;
import p.studio.compiler.source.tables.IntrinsicTable;
import p.studio.compiler.source.tables.NameTable;
import p.studio.utilities.structures.ReadOnlyList;

import java.util.HashMap;
import java.util.Map;

import static p.studio.compiler.pbs.lowering.PbsExecutableLoweringModels.*;

final class PbsExecutableBodyLowerer {
    private final PbsExecutableCallsiteEmitter callsiteEmitter = new PbsExecutableCallsiteEmitter();
    private final PbsExecutableStackAnalyzer stackAnalyzer = new PbsExecutableStackAnalyzer();

    PbsLoweredExecutableBody lower(
            final PbsAst.FunctionDecl functionDecl,
            final DiagnosticSink diagnostics,
            final NameTable nameTable,
            final PbsExecutableMetadataIndex metadataIndex,
            final PbsExecutableCallableRegistry callableRegistry,
            final IntrinsicTable intrinsicIdTable) {
        final var localSlotByNameId = initialLocalSlots(functionDecl, nameTable);
        final var localOwnerByNameId = initialLocalOwners(functionDecl, nameTable, metadataIndex);
        final var context = new PbsExecutableLoweringContext(
                diagnostics,
                nameTable,
                metadataIndex,
                callableRegistry,
                intrinsicIdTable,
                localSlotByNameId,
                localOwnerByNameId,
                functionDecl.parameters().size());
        final var terminated = lowerBlock(functionDecl.body(), context);
        finalizeFunctionInstructions(functionDecl, context, terminated);

        final var returnSlots = returnSlotsFor(functionDecl);
        final int maxStackSlots;
        try {
            maxStackSlots = stackAnalyzer.analyzeMaxStackSlots(ReadOnlyList.wrap(context.instructions()), returnSlots);
        } catch (PbsExecutableStackAnalyzer.ExecutableLoweringAnalysisException e) {
            diagnostics.error(
                    DiagnosticPhase.STATIC_SEMANTICS,
                    PbsSemanticsErrors.E_SEM_EXEC_LOWERING_UNRESOLVED_CALLEE.name(),
                    PbsSemanticsErrors.E_SEM_EXEC_LOWERING_UNRESOLVED_CALLEE.name(),
                    Map.of(),
                    e.getMessage(),
                    functionDecl.span());
            return null;
        }
        final var localSlots = Math.max(0, context.nextLocalSlot() - functionDecl.parameters().size());
        return new PbsLoweredExecutableBody(
                ReadOnlyList.wrap(context.instructions()),
                localSlots,
                maxStackSlots);
    }

    private Map<NameId, Integer> initialLocalSlots(
            final PbsAst.FunctionDecl functionDecl,
            final NameTable nameTable) {
        final var localSlotByNameId = new HashMap<NameId, Integer>();
        for (var paramIndex = 0; paramIndex < functionDecl.parameters().size(); paramIndex++) {
            localSlotByNameId.put(nameTable.register(functionDecl.parameters().get(paramIndex).name()), paramIndex);
        }
        return localSlotByNameId;
    }

    private Map<NameId, String> initialLocalOwners(
            final PbsAst.FunctionDecl functionDecl,
            final NameTable nameTable,
            final PbsExecutableMetadataIndex metadataIndex) {
        final var localOwnerByNameId = new HashMap<NameId, String>();
        for (final var parameter : functionDecl.parameters()) {
            final var ownerCanonical = resolveBuiltinOwnerCanonical(parameter.typeRef(), metadataIndex);
            if (!ownerCanonical.isBlank()) {
                localOwnerByNameId.put(nameTable.register(parameter.name()), ownerCanonical);
            }
        }
        return localOwnerByNameId;
    }

    private int returnSlotsFor(final PbsAst.FunctionDecl functionDecl) {
        return PbsCallableSlotCounter.returnSlots(functionDecl.returnKind(), functionDecl.returnType());
    }

    private void finalizeFunctionInstructions(
            final PbsAst.FunctionDecl functionDecl,
            final PbsExecutableLoweringContext context,
            final boolean terminated) {
        if (terminated) {
            return;
        }
        context.instructions().add(new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.RET,
                "",
                null,
                null,
                null,
                functionDecl.span()));
    }

    private boolean lowerBlock(
            final PbsAst.Block block,
            final PbsExecutableLoweringContext context) {
        if (block == null) {
            return false;
        }
        var terminated = false;
        for (final var statement : block.statements()) {
            if (terminated) {
                break;
            }
            terminated = lowerStatement(statement, context);
        }
        if (!terminated && block.tailExpression() != null) {
            lowerExpression(block.tailExpression(), context);
        }
        return terminated;
    }

    private boolean lowerStatement(
            final PbsAst.Statement statement,
            final PbsExecutableLoweringContext context) {
        if (statement == null) {
            return false;
        }
        return switch (statement) {
            case PbsAst.LetStatement letStatement -> lowerLetStatement(letStatement, context);
            case PbsAst.AssignStatement assignStatement -> lowerAssignStatement(assignStatement, context);
            case PbsAst.ReturnStatement returnStatement -> lowerReturnStatement(returnStatement, context);
            case PbsAst.IfStatement ifStatement -> lowerIfStatement(ifStatement, context);
            case PbsAst.ForStatement forStatement -> lowerForStatement(forStatement, context);
            case PbsAst.WhileStatement whileStatement -> lowerWhileStatement(whileStatement, context);
            case PbsAst.ExpressionStatement expressionStatement -> {
                lowerExpression(expressionStatement.expression(), context);
                discardIgnoredValueSlots(expressionStatement.expression(), context);
                yield false;
            }
            case PbsAst.BreakStatement breakStatement -> lowerBreakStatement(breakStatement, context);
            case PbsAst.ContinueStatement continueStatement -> lowerContinueStatement(continueStatement, context);
        };
    }

    private boolean lowerLetStatement(
            final PbsAst.LetStatement letStatement,
            final PbsExecutableLoweringContext context) {
        final var localOwner = callsiteEmitter.resolveExpressionOwnerForLowering(letStatement.initializer(), context);
        final var tupleProjection = tupleProjectionOf(letStatement.initializer(), context);
        final var valueSlots = Math.max(1, materializedValueSlots(letStatement.initializer(), context));
        lowerExpression(letStatement.initializer(), context);
        final var localSlot = context.declareLocalSlots(letStatement.name(), valueSlots);
        context.bindLocalOwner(letStatement.name(), localOwner);
        context.bindLocalTupleProjection(letStatement.name(), tupleProjection);
        emitStoreLocalSlots(localSlot, valueSlots, letStatement.span(), context);
        return false;
    }

    private boolean lowerAssignStatement(
            final PbsAst.AssignStatement assignStatement,
            final PbsExecutableLoweringContext context) {
        final var target = assignStatement.target();
        if (target == null || target.rootName() == null || target.rootName().isBlank()) {
            reportUnsupportedLowering("assignment target requires a local root name", assignStatement.span(), context);
            return false;
        }
        if (target.pathSegments() != null && !target.pathSegments().isEmpty()) {
            reportUnsupportedLowering("path assignment lowering is not supported in executable lowering v1", assignStatement.span(), context);
            return false;
        }
        final var targetLocalSlot = context.resolveLocalSlot(target.rootName());
        final var targetGlobalSlot = targetLocalSlot == null ? context.resolveGlobalSlot(target.rootName()) : null;
        if (targetLocalSlot == null && targetGlobalSlot == null) {
            reportUnsupportedLowering("assignment target is not a known local or global: " + target.rootName(), assignStatement.span(), context);
            return false;
        }
        if (assignStatement.operator() == PbsAst.AssignOperator.ASSIGN) {
            final var localOwner = callsiteEmitter.resolveExpressionOwnerForLowering(assignStatement.value(), context);
            final var tupleProjection = tupleProjectionOf(assignStatement.value(), context);
            final var valueSlots = Math.max(1, materializedValueSlots(assignStatement.value(), context));
            lowerExpression(assignStatement.value(), context);
            if (targetLocalSlot != null) {
                context.bindLocalOwner(target.rootName(), localOwner);
                context.bindLocalTupleProjection(target.rootName(), tupleProjection);
                emitStoreLocalSlots(targetLocalSlot, valueSlots, assignStatement.span(), context);
            } else {
                emitSetGlobal(targetGlobalSlot, assignStatement.span(), context);
            }
            return false;
        }
        if (targetLocalSlot != null) {
            emitGetLocal(targetLocalSlot, assignStatement.span(), context);
        } else {
            emitGetGlobal(targetGlobalSlot, assignStatement.span(), context);
        }
        lowerExpression(assignStatement.value(), context);
        emitBinaryOperatorInstruction(compoundAssignBinaryOperator(assignStatement.operator()), assignStatement.span(), context);
        if (targetLocalSlot != null) {
            context.bindLocalOwner(target.rootName(), "");
            emitSetLocal(targetLocalSlot, assignStatement.span(), context);
        } else {
            emitSetGlobal(targetGlobalSlot, assignStatement.span(), context);
        }
        return false;
    }

    private String resolveBuiltinOwnerCanonical(
            final PbsAst.TypeRef typeRef,
            final PbsExecutableMetadataIndex metadataIndex) {
        final var unwrapped = unwrapGroup(typeRef);
        if (unwrapped == null || unwrapped.kind() != PbsAst.TypeRefKind.SIMPLE) {
            return "";
        }
        return metadataIndex.builtinCanonicalBySourceType().getOrDefault(unwrapped.name(), "");
    }

    private PbsAst.TypeRef unwrapGroup(final PbsAst.TypeRef typeRef) {
        if (typeRef == null) {
            return null;
        }
        if (typeRef.kind() != PbsAst.TypeRefKind.GROUP) {
            return typeRef;
        }
        return unwrapGroup(typeRef.inner());
    }

    private boolean lowerReturnStatement(
            final PbsAst.ReturnStatement returnStatement,
            final PbsExecutableLoweringContext context) {
        if (returnStatement.value() != null) {
            lowerExpression(returnStatement.value(), context);
        }
        context.instructions().add(new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.RET,
                "",
                null,
                null,
                null,
                returnStatement.span()));
        return true;
    }

    private boolean lowerIfStatement(
            final PbsAst.IfStatement ifStatement,
            final PbsExecutableLoweringContext context) {
        lowerExpression(ifStatement.condition(), context);
        final var elseLabel = context.nextLabel("if_else");
        final var endLabel = context.nextLabel("if_end");
        emitJump(IRBackendExecutableFunction.InstructionKind.JMP_IF_FALSE, elseLabel, ifStatement.span(), context);
        final var thenTerminated = lowerBlock(ifStatement.thenBlock(), context);
        if (!thenTerminated) {
            emitJump(IRBackendExecutableFunction.InstructionKind.JMP, endLabel, ifStatement.span(), context);
        }
        emitLabel(elseLabel, ifStatement.span(), context);
        final var elseTerminated = lowerElseBranch(ifStatement, context);
        if (!thenTerminated || !elseTerminated) {
            emitLabel(endLabel, ifStatement.span(), context);
        }
        return hasElseBranch(ifStatement) && thenTerminated && elseTerminated;
    }

    private boolean lowerElseBranch(
            final PbsAst.IfStatement ifStatement,
            final PbsExecutableLoweringContext context) {
        if (ifStatement.elseIf() != null) {
            return lowerStatement(ifStatement.elseIf(), context);
        }
        if (ifStatement.elseBlock() != null) {
            return lowerBlock(ifStatement.elseBlock(), context);
        }
        return false;
    }

    private boolean hasElseBranch(final PbsAst.IfStatement ifStatement) {
        return ifStatement.elseIf() != null || ifStatement.elseBlock() != null;
    }

    private boolean lowerForStatement(
            final PbsAst.ForStatement forStatement,
            final PbsExecutableLoweringContext context) {
        final var iteratorSlot = context.declareLocalSlot(forStatement.iteratorName());
        lowerExpression(forStatement.fromExpression(), context);
        emitSetLocal(iteratorSlot, forStatement.span(), context);
        final var loopStart = context.nextLabel("for_start");
        final var loopExit = context.nextLabel("for_exit");
        final var loopContinue = context.nextLabel("for_continue");
        emitLabel(loopStart, forStatement.span(), context);
        emitGetLocal(iteratorSlot, forStatement.span(), context);
        lowerExpression(forStatement.untilExpression(), context);
        emitBinaryOperatorInstruction("<", forStatement.span(), context);
        emitJump(IRBackendExecutableFunction.InstructionKind.JMP_IF_FALSE, loopExit, forStatement.span(), context);
        context.pushLoop(loopContinue, loopExit);
        lowerBlock(forStatement.body(), context);
        context.popLoop();
        emitLabel(loopContinue, forStatement.span(), context);
        emitGetLocal(iteratorSlot, forStatement.span(), context);
        if (forStatement.stepExpression() != null) {
            lowerExpression(forStatement.stepExpression(), context);
        } else {
            emitPushI32(1, forStatement.span(), context);
        }
        emitBinaryOperatorInstruction("+", forStatement.span(), context);
        emitSetLocal(iteratorSlot, forStatement.span(), context);
        emitJump(IRBackendExecutableFunction.InstructionKind.JMP, loopStart, forStatement.span(), context);
        emitLabel(loopExit, forStatement.span(), context);
        return false;
    }

    private boolean lowerWhileStatement(
            final PbsAst.WhileStatement whileStatement,
            final PbsExecutableLoweringContext context) {
        final var loopStart = context.nextLabel("while_start");
        final var loopExit = context.nextLabel("while_exit");
        emitLabel(loopStart, whileStatement.span(), context);
        lowerExpression(whileStatement.condition(), context);
        emitJump(IRBackendExecutableFunction.InstructionKind.JMP_IF_FALSE, loopExit, whileStatement.span(), context);
        context.pushLoop(loopStart, loopExit);
        lowerBlock(whileStatement.body(), context);
        context.popLoop();
        emitJump(IRBackendExecutableFunction.InstructionKind.JMP, loopStart, whileStatement.span(), context);
        emitLabel(loopExit, whileStatement.span(), context);
        return false;
    }

    private boolean lowerBreakStatement(
            final PbsAst.BreakStatement breakStatement,
            final PbsExecutableLoweringContext context) {
        if (context.loopTargets().isEmpty()) {
            reportUnsupportedLowering("break statement outside loop", breakStatement.span(), context);
            return true;
        }
        emitJump(IRBackendExecutableFunction.InstructionKind.JMP, context.loopTargets().peek().breakLabel(), breakStatement.span(), context);
        return true;
    }

    private boolean lowerContinueStatement(
            final PbsAst.ContinueStatement continueStatement,
            final PbsExecutableLoweringContext context) {
        if (context.loopTargets().isEmpty()) {
            reportUnsupportedLowering("continue statement outside loop", continueStatement.span(), context);
            return true;
        }
        emitJump(
                IRBackendExecutableFunction.InstructionKind.JMP,
                context.loopTargets().peek().continueLabel(),
                continueStatement.span(),
                context);
        return true;
    }

    private void lowerExpression(
            final PbsAst.Expression expression,
            final PbsExecutableLoweringContext context) {
        if (expression == null) {
            return;
        }
        switch (expression) {
            case PbsAst.CallExpr callExpr -> lowerCallExpression(callExpr, context);
            case PbsAst.ApplyExpr applyExpr -> {
                lowerExpression(applyExpr.callee(), context);
                lowerExpression(applyExpr.argument(), context);
            }
            case PbsAst.BinaryExpr binaryExpr -> {
                lowerExpression(binaryExpr.left(), context);
                lowerExpression(binaryExpr.right(), context);
                emitBinaryOperatorInstruction(binaryExpr.operator(), binaryExpr.span(), context);
            }
            case PbsAst.UnaryExpr unaryExpr -> {
                lowerExpression(unaryExpr.expression(), context);
                emitUnaryOperatorInstruction(unaryExpr.operator(), unaryExpr.span(), context);
            }
            case PbsAst.ElseExpr elseExpr -> {
                lowerExpression(elseExpr.optionalExpression(), context);
                lowerExpression(elseExpr.fallbackExpression(), context);
            }
            case PbsAst.IfExpr ifExpr -> lowerIfExpression(ifExpr, context);
            case PbsAst.SwitchExpr switchExpr -> lowerUnsupportedExpression(
                    switchExpr.selector(),
                    "switch expression is not lowerable in executable lowering v1",
                    switchExpr.span(),
                    context);
            case PbsAst.HandleExpr handleExpr -> lowerUnsupportedExpression(
                    handleExpr.value(),
                    "handle expression is not lowerable in executable lowering v1",
                    handleExpr.span(),
                    context);
            case PbsAst.AsExpr asExpr -> lowerExpression(asExpr.expression(), context);
            case PbsAst.MemberExpr memberExpr -> lowerMemberExpression(memberExpr, context);
            case PbsAst.PropagateExpr propagateExpr -> lowerExpression(propagateExpr.expression(), context);
            case PbsAst.GroupExpr groupExpr -> lowerExpression(groupExpr.expression(), context);
            case PbsAst.NewExpr newExpr -> lowerExpressionList(newExpr.arguments(), context);
            case PbsAst.BindExpr bindExpr -> lowerExpression(bindExpr.contextExpression(), context);
            case PbsAst.SomeExpr someExpr -> lowerExpression(someExpr.value(), context);
            case PbsAst.OkExpr okExpr -> lowerExpression(okExpr.value(), context);
            case PbsAst.TupleExpr tupleExpr -> {
                for (final var item : tupleExpr.items()) {
                    lowerExpression(item.expression(), context);
                }
            }
            case PbsAst.BlockExpr blockExpr -> lowerBlock(blockExpr.block(), context);
            case PbsAst.IdentifierExpr identifierExpr -> lowerIdentifierExpression(identifierExpr, context);
            case PbsAst.IntLiteralExpr intLiteralExpr -> lowerIntLiteral(intLiteralExpr, context);
            case PbsAst.FloatLiteralExpr ignored -> {
            }
            case PbsAst.BoundedLiteralExpr ignored -> {
            }
            case PbsAst.StringLiteralExpr stringLiteralExpr -> emitPushConst(stringLiteralExpr.value(), stringLiteralExpr.span(), context);
            case PbsAst.BoolLiteralExpr boolLiteralExpr -> emitPushBool(boolLiteralExpr.value(), boolLiteralExpr.span(), context);
            case PbsAst.ThisExpr ignored -> {
            }
            case PbsAst.NoneExpr ignored -> {
            }
            case PbsAst.ErrExpr ignored -> {
            }
            case PbsAst.UnitExpr ignored -> {
            }
        }
    }

    private void lowerCallExpression(
            final PbsAst.CallExpr callExpr,
            final PbsExecutableLoweringContext context) {
        lowerCallsiteReceiver(callExpr.callee(), context);
        lowerCallArguments(callExpr, context);
        callsiteEmitter.emitCallsite(callExpr, context);
    }

    private void lowerCallArguments(
            final PbsAst.CallExpr callExpr,
            final PbsExecutableLoweringContext context) {
        final var hostBinding = callsiteEmitter.resolveUniqueHostBinding(callExpr, context);
        if (hostBinding == null || hostBinding.assetLoweringParam() == null) {
            lowerExpressionList(callExpr.arguments(), context);
            return;
        }
        for (int i = 0; i < callExpr.arguments().size(); i++) {
            final var argument = callExpr.arguments().get(i);
            if (i == hostBinding.assetLoweringParam()) {
                lowerAssetFacingArgument(argument, hostBinding, context);
                continue;
            }
            lowerExpression(argument, context);
        }
    }

    private void lowerExpressionList(
            final ReadOnlyList<PbsAst.Expression> expressions,
            final PbsExecutableLoweringContext context) {
        for (final var expression : expressions) {
            lowerExpression(expression, context);
        }
    }

    private void discardIgnoredValueSlots(
            final PbsAst.Expression expression,
            final PbsExecutableLoweringContext context) {
        final var slots = materializedValueSlots(expression, context);
        for (int i = 0; i < slots; i++) {
            context.instructions().add(new IRBackendExecutableFunction.Instruction(
                    IRBackendExecutableFunction.InstructionKind.POP,
                    "",
                    null,
                    null,
                    null,
                    expression == null ? p.studio.compiler.source.Span.none() : expression.span()));
        }
    }

    private void lowerMemberExpression(
            final PbsAst.MemberExpr memberExpr,
            final PbsExecutableLoweringContext context) {
        final var assetReference = flattenAssetReference(memberExpr);
        if (assetReference != null) {
            final var assetId = context.resolveAssetId(assetReference);
            if (assetId == null) {
                reportUnsupportedLowering(
                        "asset reference does not resolve in backend-owned surface: " + assetReference,
                        memberExpr.span(),
                        context);
                return;
            }
            emitPushI32(assetId, memberExpr.span(), context);
            return;
        }
        if (memberExpr.receiver() instanceof PbsAst.IdentifierExpr identifierExpr) {
            final var tupleProjection = context.resolveLocalTupleProjection(identifierExpr.name());
            final var localSlot = context.resolveLocalSlot(identifierExpr.name());
            if (tupleProjection != null && localSlot != null) {
                final var fieldProjection = tupleProjection.fieldsByLabel().get(memberExpr.memberName());
                if (fieldProjection != null) {
                    emitLoadLocalSlots(localSlot + fieldProjection.slotOffset(), fieldProjection.slotCount(), memberExpr.span(), context);
                    return;
                }
            }
        }
        lowerExpression(memberExpr.receiver(), context);
    }

    private void lowerAssetFacingArgument(
            final PbsAst.Expression expression,
            final p.studio.compiler.models.IRReservedMetadata.HostMethodBinding hostBinding,
            final PbsExecutableLoweringContext context) {
        final var assetReference = flattenAssetReference(expression);
        if (assetReference == null) {
            lowerExpression(expression, context);
            return;
        }
        final var assetId = context.resolveAssetId(assetReference);
        if (assetId == null) {
            reportUnsupportedLowering(
                    "asset-facing host parameter does not resolve in backend-owned surface: " + assetReference,
                    expression.span(),
                    context);
            return;
        }
        emitPushI32(assetId, expression.span(), context);
    }

    private void lowerIfExpression(
            final PbsAst.IfExpr ifExpr,
            final PbsExecutableLoweringContext context) {
        lowerExpression(ifExpr.condition(), context);
        final var elseLabel = context.nextLabel("ifexpr_else");
        final var endLabel = context.nextLabel("ifexpr_end");
        emitJump(IRBackendExecutableFunction.InstructionKind.JMP_IF_FALSE, elseLabel, ifExpr.span(), context);
        final var thenTerminated = lowerBlock(ifExpr.thenBlock(), context);
        if (!thenTerminated) {
            emitJump(IRBackendExecutableFunction.InstructionKind.JMP, endLabel, ifExpr.span(), context);
        }
        emitLabel(elseLabel, ifExpr.span(), context);
        lowerExpression(ifExpr.elseExpression(), context);
        if (!thenTerminated) {
            emitLabel(endLabel, ifExpr.span(), context);
        }
    }

    private void lowerUnsupportedExpression(
            final PbsAst.Expression nestedExpression,
            final String message,
            final p.studio.compiler.source.Span span,
            final PbsExecutableLoweringContext context) {
        lowerExpression(nestedExpression, context);
        reportUnsupportedLowering(message, span, context);
    }

    private void lowerIdentifierExpression(
            final PbsAst.IdentifierExpr identifierExpr,
            final PbsExecutableLoweringContext context) {
        if ("assets".equals(identifierExpr.name())) {
            return;
        }
        final var slot = context.localSlotByNameId().get(context.nameTable().register(identifierExpr.name()));
        if (slot != null) {
            final var tupleProjection = context.resolveLocalTupleProjection(identifierExpr.name());
            if (tupleProjection != null) {
                emitLoadLocalSlots(slot, tupleProjection.slotCount(), identifierExpr.span(), context);
            } else {
                emitGetLocal(slot, identifierExpr.span(), context);
            }
            return;
        }
        final var globalSlot = context.resolveGlobalSlot(identifierExpr.name());
        if (globalSlot != null) {
            emitGetGlobal(globalSlot, identifierExpr.span(), context);
            return;
        }
        final var constDecl = context.resolveConstDecl(identifierExpr.name());
        if (constDecl != null && constDecl.initializer() != null) {
            lowerConstExpression(constDecl.initializer(), context);
        }
    }

    private String flattenAssetReference(final PbsAst.Expression expression) {
        if (expression instanceof PbsAst.IdentifierExpr identifierExpr) {
            return "assets".equals(identifierExpr.name()) ? "assets" : null;
        }
        if (!(expression instanceof PbsAst.MemberExpr memberExpr)) {
            return null;
        }
        final var receiverPath = flattenAssetReference(memberExpr.receiver());
        if (receiverPath == null || receiverPath.isBlank()) {
            return null;
        }
        return receiverPath + "." + memberExpr.memberName();
    }

    private int materializedValueSlots(
            final PbsAst.Expression expression,
            final PbsExecutableLoweringContext context) {
        if (expression == null) {
            return 0;
        }
        return switch (expression) {
            case PbsAst.CallExpr callExpr -> callsiteEmitter.returnSlotsOf(callExpr, context);
            case PbsAst.ApplyExpr ignored -> 0;
            case PbsAst.BinaryExpr ignored -> 1;
            case PbsAst.UnaryExpr ignored -> 1;
            case PbsAst.ElseExpr elseExpr ->
                    Math.max(
                            materializedValueSlots(elseExpr.optionalExpression(), context),
                            materializedValueSlots(elseExpr.fallbackExpression(), context));
            case PbsAst.IfExpr ifExpr ->
                    Math.max(
                            blockValueSlots(ifExpr.thenBlock(), context),
                            materializedValueSlots(ifExpr.elseExpression(), context));
            case PbsAst.SwitchExpr ignored -> 0;
            case PbsAst.HandleExpr ignored -> 0;
            case PbsAst.AsExpr asExpr -> materializedValueSlots(asExpr.expression(), context);
            case PbsAst.MemberExpr memberExpr -> {
                final var assetReference = flattenAssetReference(memberExpr);
                if (assetReference != null && context.resolveAssetId(assetReference) != null) {
                    yield 1;
                }
                if (memberExpr.receiver() instanceof PbsAst.IdentifierExpr identifierExpr) {
                    final var tupleProjection = context.resolveLocalTupleProjection(identifierExpr.name());
                    if (tupleProjection != null) {
                        final var fieldProjection = tupleProjection.fieldsByLabel().get(memberExpr.memberName());
                        if (fieldProjection != null) {
                            yield fieldProjection.slotCount();
                        }
                    }
                }
                yield materializedValueSlots(memberExpr.receiver(), context);
            }
            case PbsAst.PropagateExpr propagateExpr -> materializedValueSlots(propagateExpr.expression(), context);
            case PbsAst.GroupExpr groupExpr -> materializedValueSlots(groupExpr.expression(), context);
            case PbsAst.NewExpr newExpr -> expressionListValueSlots(newExpr.arguments(), context);
            case PbsAst.BindExpr bindExpr -> materializedValueSlots(bindExpr.contextExpression(), context);
            case PbsAst.SomeExpr someExpr -> materializedValueSlots(someExpr.value(), context);
            case PbsAst.OkExpr okExpr -> materializedValueSlots(okExpr.value(), context);
            case PbsAst.TupleExpr tupleExpr -> {
                var slots = 0;
                for (final var item : tupleExpr.items()) {
                    slots += materializedValueSlots(item.expression(), context);
                }
                yield slots;
            }
            case PbsAst.BlockExpr blockExpr -> blockValueSlots(blockExpr.block(), context);
            case PbsAst.IdentifierExpr identifierExpr -> identifierValueSlots(identifierExpr, context);
            case PbsAst.IntLiteralExpr ignored -> 1;
            case PbsAst.StringLiteralExpr ignoredString -> 1;
            case PbsAst.BoolLiteralExpr ignoredBool -> 1;
            case PbsAst.FloatLiteralExpr ignoredFloat -> 0;
            case PbsAst.BoundedLiteralExpr ignoredBounded -> 0;
            case PbsAst.ThisExpr ignoredThis -> 0;
            case PbsAst.NoneExpr ignoredNone -> 0;
            case PbsAst.ErrExpr ignoredErr -> 0;
            case PbsAst.UnitExpr ignoredUnit -> 0;
        };
    }

    private int blockValueSlots(
            final PbsAst.Block block,
            final PbsExecutableLoweringContext context) {
        if (block == null || block.tailExpression() == null) {
            return 0;
        }
        return materializedValueSlots(block.tailExpression(), context);
    }

    private int expressionListValueSlots(
            final ReadOnlyList<PbsAst.Expression> expressions,
            final PbsExecutableLoweringContext context) {
        var slots = 0;
        for (final var expression : expressions) {
            slots += materializedValueSlots(expression, context);
        }
        return slots;
    }

    private boolean producesIdentifierValue(
            final PbsAst.IdentifierExpr identifierExpr,
            final PbsExecutableLoweringContext context) {
        if ("assets".equals(identifierExpr.name())) {
            return false;
        }
        return context.resolveLocalSlot(identifierExpr.name()) != null
                || context.resolveGlobalSlot(identifierExpr.name()) != null
                || context.resolveConstDecl(identifierExpr.name()) != null;
    }

    private int identifierValueSlots(
            final PbsAst.IdentifierExpr identifierExpr,
            final PbsExecutableLoweringContext context) {
        if (!producesIdentifierValue(identifierExpr, context)) {
            return 0;
        }
        final var tupleProjection = context.resolveLocalTupleProjection(identifierExpr.name());
        return tupleProjection == null ? 1 : tupleProjection.slotCount();
    }

    private PbsTupleProjection tupleProjectionOf(
            final PbsAst.Expression expression,
            final PbsExecutableLoweringContext context) {
        if (expression instanceof PbsAst.CallExpr callExpr) {
            final var callableId = callsiteEmitter.resolveUniqueCallableId(callExpr, context);
            if (callableId != null) {
                return context.tupleProjectionByCallableId().get(callableId);
            }
        }
        if (expression instanceof PbsAst.IdentifierExpr identifierExpr) {
            return context.resolveLocalTupleProjection(identifierExpr.name());
        }
        return null;
    }

    private void lowerConstExpression(
            final PbsAst.Expression expression,
            final PbsExecutableLoweringContext context) {
        if (expression == null) {
            return;
        }
        switch (expression) {
            case PbsAst.IntLiteralExpr intLiteralExpr -> lowerIntLiteral(intLiteralExpr, context);
            case PbsAst.BoundedLiteralExpr boundedLiteralExpr ->
                    lowerIntLiteral(new PbsAst.IntLiteralExpr(boundedLiteralExpr.value(), boundedLiteralExpr.span()), context);
            case PbsAst.StringLiteralExpr stringLiteralExpr -> emitPushConst(stringLiteralExpr.value(), stringLiteralExpr.span(), context);
            case PbsAst.BoolLiteralExpr boolLiteralExpr -> emitPushBool(boolLiteralExpr.value(), boolLiteralExpr.span(), context);
            case PbsAst.GroupExpr groupExpr -> lowerConstExpression(groupExpr.expression(), context);
            case PbsAst.UnaryExpr unaryExpr -> {
                lowerConstExpression(unaryExpr.expression(), context);
                emitUnaryOperatorInstruction(unaryExpr.operator(), unaryExpr.span(), context);
            }
            case PbsAst.BinaryExpr binaryExpr -> {
                lowerConstExpression(binaryExpr.left(), context);
                lowerConstExpression(binaryExpr.right(), context);
                emitBinaryOperatorInstruction(binaryExpr.operator(), binaryExpr.span(), context);
            }
            case PbsAst.IdentifierExpr identifierExpr -> {
                final var referenced = context.resolveConstDecl(identifierExpr.name());
                if (referenced != null && referenced.initializer() != null) {
                    lowerConstExpression(referenced.initializer(), context);
                } else {
                    reportUnsupportedLowering("const identifier is not lowerable: " + identifierExpr.name(), identifierExpr.span(), context);
                }
            }
            default -> reportUnsupportedLowering(
                    "const expression is not lowerable in executable lowering v1",
                    expression.span(),
                    context);
        }
    }

    private void lowerIntLiteral(
            final PbsAst.IntLiteralExpr intLiteralExpr,
            final PbsExecutableLoweringContext context) {
        if (intLiteralExpr.value() < Integer.MIN_VALUE || intLiteralExpr.value() > Integer.MAX_VALUE) {
            reportUnsupportedLowering(
                    "int literal exceeds i32 lowering range: " + intLiteralExpr.value(),
                    intLiteralExpr.span(),
                    context);
            return;
        }
        emitPushI32((int) intLiteralExpr.value(), intLiteralExpr.span(), context);
    }

    private void lowerCallsiteReceiver(
            final PbsAst.Expression callee,
            final PbsExecutableLoweringContext context) {
        if (callee == null) {
            return;
        }
        switch (callee) {
            case PbsAst.MemberExpr memberExpr -> lowerExpression(memberExpr.receiver(), context);
            case PbsAst.GroupExpr groupExpr -> lowerCallsiteReceiver(groupExpr.expression(), context);
            default -> {
            }
        }
    }

    private void emitJump(
            final IRBackendExecutableFunction.InstructionKind jumpKind,
            final String targetLabel,
            final p.studio.compiler.source.Span span,
            final PbsExecutableLoweringContext context) {
        context.instructions().add(new IRBackendExecutableFunction.Instruction(
                jumpKind,
                ModuleId.none(),
                "",
                null,
                null,
                null,
                "",
                targetLabel,
                null,
                null,
                span));
    }

    private void emitLabel(
            final String label,
            final p.studio.compiler.source.Span span,
            final PbsExecutableLoweringContext context) {
        context.instructions().add(new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.LABEL,
                ModuleId.none(),
                "",
                null,
                null,
                null,
                label,
                "",
                null,
                null,
                span));
    }

    private void emitPushI32(
            final int value,
            final p.studio.compiler.source.Span span,
            final PbsExecutableLoweringContext context) {
        context.instructions().add(new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.PUSH_I32,
                ModuleId.none(),
                "",
                null,
                null,
                null,
                Integer.toString(value),
                "",
                null,
                null,
                span));
    }

    private void emitPushBool(
            final boolean value,
            final p.studio.compiler.source.Span span,
            final PbsExecutableLoweringContext context) {
        context.instructions().add(new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.PUSH_BOOL,
                ModuleId.none(),
                "",
                null,
                null,
                null,
                value ? "true" : "false",
                "",
                null,
                null,
                span));
    }

    private void emitPushConst(
            final String value,
            final p.studio.compiler.source.Span span,
            final PbsExecutableLoweringContext context) {
        context.instructions().add(new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.PUSH_CONST,
                ModuleId.none(),
                "",
                null,
                null,
                null,
                value == null ? "" : value,
                "",
                null,
                null,
                span));
    }

    private void emitSetLocal(
            final int slot,
            final p.studio.compiler.source.Span span,
            final PbsExecutableLoweringContext context) {
        context.instructions().add(new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.SET_LOCAL,
                ModuleId.none(),
                "",
                null,
                null,
                null,
                "",
                "",
                slot,
                null,
                span));
    }

    private void emitStoreLocalSlots(
            final int firstSlot,
            final int slotCount,
            final p.studio.compiler.source.Span span,
            final PbsExecutableLoweringContext context) {
        for (int i = slotCount - 1; i >= 0; i--) {
            emitSetLocal(firstSlot + i, span, context);
        }
    }

    private void emitSetGlobal(
            final int slot,
            final p.studio.compiler.source.Span span,
            final PbsExecutableLoweringContext context) {
        context.instructions().add(new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.SET_GLOBAL,
                ModuleId.none(),
                "",
                null,
                null,
                null,
                "",
                "",
                slot,
                null,
                span));
    }

    private String compoundAssignBinaryOperator(final PbsAst.AssignOperator operator) {
        return switch (operator) {
            case ADD_ASSIGN -> "+";
            case SUB_ASSIGN -> "-";
            case MUL_ASSIGN -> "*";
            case DIV_ASSIGN -> "/";
            case MOD_ASSIGN -> "%";
            case ASSIGN -> "=";
        };
    }

    private void emitUnaryOperatorInstruction(
            final String operator,
            final p.studio.compiler.source.Span span,
            final PbsExecutableLoweringContext context) {
        final var normalized = operator == null ? "" : operator;
        final IRBackendExecutableFunction.InstructionKind instructionKind = switch (normalized) {
            case "-", "neg" -> IRBackendExecutableFunction.InstructionKind.NEG;
            case "!", "not" -> IRBackendExecutableFunction.InstructionKind.NOT;
            default -> null;
        };
        if (instructionKind == null) {
            reportUnsupportedLowering("unsupported unary operator in executable lowering: " + normalized, span, context);
            return;
        }
        context.instructions().add(new IRBackendExecutableFunction.Instruction(
                instructionKind,
                ModuleId.none(),
                "",
                null,
                null,
                null,
                "",
                "",
                null,
                null,
                span));
    }

    private void emitBinaryOperatorInstruction(
            final String operator,
            final p.studio.compiler.source.Span span,
            final PbsExecutableLoweringContext context) {
        final var normalized = operator == null ? "" : operator;
        final IRBackendExecutableFunction.InstructionKind instructionKind = switch (normalized) {
            case "+" -> IRBackendExecutableFunction.InstructionKind.ADD;
            case "-" -> IRBackendExecutableFunction.InstructionKind.SUB;
            case "*" -> IRBackendExecutableFunction.InstructionKind.MUL;
            case "/" -> IRBackendExecutableFunction.InstructionKind.DIV;
            case "%" -> IRBackendExecutableFunction.InstructionKind.MOD;
            case "==" -> IRBackendExecutableFunction.InstructionKind.EQ;
            case "!=" -> IRBackendExecutableFunction.InstructionKind.NEQ;
            case "<" -> IRBackendExecutableFunction.InstructionKind.LT;
            case "<=" -> IRBackendExecutableFunction.InstructionKind.LTE;
            case ">" -> IRBackendExecutableFunction.InstructionKind.GT;
            case ">=" -> IRBackendExecutableFunction.InstructionKind.GTE;
            case "&&", "and" -> IRBackendExecutableFunction.InstructionKind.AND;
            case "||", "or" -> IRBackendExecutableFunction.InstructionKind.OR;
            default -> null;
        };
        if (instructionKind == null) {
            reportUnsupportedLowering("unsupported binary operator in executable lowering: " + normalized, span, context);
            return;
        }
        context.instructions().add(new IRBackendExecutableFunction.Instruction(
                instructionKind,
                ModuleId.none(),
                "",
                null,
                null,
                null,
                "",
                "",
                null,
                null,
                span));
    }

    private void emitGetLocal(
            final int slot,
            final p.studio.compiler.source.Span span,
            final PbsExecutableLoweringContext context) {
        context.instructions().add(new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.GET_LOCAL,
                ModuleId.none(),
                "",
                null,
                null,
                null,
                "",
                "",
                slot,
                null,
                span));
    }

    private void emitLoadLocalSlots(
            final int firstSlot,
            final int slotCount,
            final p.studio.compiler.source.Span span,
            final PbsExecutableLoweringContext context) {
        for (int i = 0; i < slotCount; i++) {
            emitGetLocal(firstSlot + i, span, context);
        }
    }

    private void emitGetGlobal(
            final int slot,
            final p.studio.compiler.source.Span span,
            final PbsExecutableLoweringContext context) {
        context.instructions().add(new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.GET_GLOBAL,
                ModuleId.none(),
                "",
                null,
                null,
                null,
                "",
                "",
                slot,
                null,
                span));
    }

    private void reportUnsupportedLowering(
            final String message,
            final p.studio.compiler.source.Span span,
            final PbsExecutableLoweringContext context) {
        context.diagnostics().error(
                DiagnosticPhase.STATIC_SEMANTICS,
                PbsSemanticsErrors.E_SEM_EXEC_LOWERING_UNRESOLVED_CALLEE.name(),
                PbsSemanticsErrors.E_SEM_EXEC_LOWERING_UNRESOLVED_CALLEE.name(),
                Map.of(),
                message,
                span);
    }
}
