/*
 * Copyright 2022 Google LLC
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "src/sksl/codegen/SkSLRasterPipelineCodeGenerator.h"

#include "include/core/SkPoint.h"
#include "include/core/SkSpan.h"
#include "include/private/base/SkTArray.h"
#include "include/private/base/SkTo.h"
#include "src/base/SkEnumBitMask.h"
#include "src/base/SkStringView.h"
#include "src/base/SkUtils.h"
#include "src/core/SkTHash.h"
#include "src/sksl/SkSLAnalysis.h"
#include "src/sksl/SkSLBuiltinTypes.h"
#include "src/sksl/SkSLCompiler.h"
#include "src/sksl/SkSLConstantFolder.h"
#include "src/sksl/SkSLContext.h"
#include "src/sksl/SkSLDefines.h"
#include "src/sksl/SkSLIntrinsicList.h"
#include "src/sksl/SkSLOperator.h"
#include "src/sksl/SkSLPosition.h"
#include "src/sksl/analysis/SkSLProgramUsage.h"
#include "src/sksl/codegen/SkSLRasterPipelineBuilder.h"
#include "src/sksl/ir/SkSLBinaryExpression.h"
#include "src/sksl/ir/SkSLBlock.h"
#include "src/sksl/ir/SkSLBreakStatement.h"
#include "src/sksl/ir/SkSLChildCall.h"
#include "src/sksl/ir/SkSLConstructor.h"
#include "src/sksl/ir/SkSLConstructorDiagonalMatrix.h"
#include "src/sksl/ir/SkSLConstructorMatrixResize.h"
#include "src/sksl/ir/SkSLConstructorSplat.h"
#include "src/sksl/ir/SkSLContinueStatement.h"
#include "src/sksl/ir/SkSLDoStatement.h"
#include "src/sksl/ir/SkSLExpression.h"
#include "src/sksl/ir/SkSLExpressionStatement.h"
#include "src/sksl/ir/SkSLFieldAccess.h"
#include "src/sksl/ir/SkSLForStatement.h"
#include "src/sksl/ir/SkSLFunctionCall.h"
#include "src/sksl/ir/SkSLFunctionDeclaration.h"
#include "src/sksl/ir/SkSLFunctionDefinition.h"
#include "src/sksl/ir/SkSLIRNode.h"
#include "src/sksl/ir/SkSLIfStatement.h"
#include "src/sksl/ir/SkSLIndexExpression.h"
#include "src/sksl/ir/SkSLLayout.h"
#include "src/sksl/ir/SkSLLiteral.h"
#include "src/sksl/ir/SkSLModifierFlags.h"
#include "src/sksl/ir/SkSLPostfixExpression.h"
#include "src/sksl/ir/SkSLPrefixExpression.h"
#include "src/sksl/ir/SkSLProgram.h"
#include "src/sksl/ir/SkSLProgramElement.h"
#include "src/sksl/ir/SkSLReturnStatement.h"
#include "src/sksl/ir/SkSLStatement.h"
#include "src/sksl/ir/SkSLSwitchCase.h"
#include "src/sksl/ir/SkSLSwitchStatement.h"
#include "src/sksl/ir/SkSLSwizzle.h"
#include "src/sksl/ir/SkSLTernaryExpression.h"
#include "src/sksl/ir/SkSLType.h"
#include "src/sksl/ir/SkSLVarDeclarations.h"
#include "src/sksl/ir/SkSLVariable.h"
#include "src/sksl/ir/SkSLVariableReference.h"
#include "src/sksl/tracing/SkSLDebugTracePriv.h"
#include "src/sksl/transform/SkSLTransform.h"

#include <algorithm>
#include <climits>
#include <cstddef>
#include <cstdint>
#include <float.h>
#include <iterator>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

using namespace skia_private;

namespace SkSL {
namespace RP {

static bool unsupported() {
    // If MakeRasterPipelineProgram returns false, set a breakpoint here for more information.
    return false;
}

class AutoContinueMask;
class Generator;
class LValue;

class SlotManager {
public:
    SlotManager(std::vector<SlotDebugInfo>* i) : fSlotDebugInfo(i) {}

    /** Used by `createSlots` to add this variable to SlotDebugInfo inside the DebugTrace. */
    void addSlotDebugInfoForGroup(const std::string& varName,
                                  const Type& type,
                                  Position pos,
                                  int* groupIndex,
                                  bool isFunctionReturnValue);
    void addSlotDebugInfo(const std::string& varName,
                          const Type& type,
                          Position pos,
                          bool isFunctionReturnValue);

    /** Creates slots associated with an SkSL variable or return value. */
    SlotRange createSlots(std::string name,
                          const Type& type,
                          Position pos,
                          bool isFunctionReturnValue);

    /**
     * Associates previously-created slots with an SkSL variable; this can allow multiple variables
     * to share overlapping ranges. If the variable was already associated with a slot range,
     * returns the previously associated range.
     */
    std::optional<SlotRange> mapVariableToSlots(const Variable& v, SlotRange range);

    /**
     * Deletes the existing mapping between a variable and its slots; a future call to
     * `getVariableSlots` will see this as a brand new variable and associate new slots.
     */
    void unmapVariableSlots(const Variable& v);

    /** Looks up the slots associated with an SkSL variable; creates the slot if necessary. */
    SlotRange getVariableSlots(const Variable& v);

    /**
     * Looks up the slots associated with an SkSL function's return value; creates the range if
     * necessary. Note that recursion is never supported, so we don't need to maintain return values
     * in a stack; we can just statically allocate one slot per function call-site.
     */
    SlotRange getFunctionSlots(const IRNode& callSite, const FunctionDeclaration& f);

    /** Returns the total number of slots consumed. */
    int slotCount() const { return fSlotCount; }

private:
    THashMap<const IRNode*, SlotRange> fSlotMap;
    int fSlotCount = 0;
    std::vector<SlotDebugInfo>* fSlotDebugInfo;
};

class AutoStack {
public:
    /**
     * Creates a temporary stack. The caller is responsible for discarding every entry on this
     * stack before ~AutoStack is reached.
     */
    explicit AutoStack(Generator* g);
    ~AutoStack();

    /** Activates the associated stack. */
    void enter();

    /** Undoes a call to `enter`, returning to the previously-active stack. */
    void exit();

    /** Returns the stack ID of this AutoStack. */
    int stackID() { return fStackID; }

    /** Clones values from this stack onto the top of the active stack. */
    void pushClone(int slots);

    /** Clones values from a fixed range of this stack onto the top of the active stack. */
    void pushClone(SlotRange range, int offsetFromStackTop);

    /** Clones values from a dynamic range of this stack onto the top of the active stack. */
    void pushCloneIndirect(SlotRange range, int dynamicStackID, int offsetFromStackTop);

private:
    Generator* fGenerator;
    int fStackID = 0;
    int fParentStackID = 0;
};

class Generator {
public:
    Generator(const SkSL::Program& program, DebugTracePriv* debugTrace, bool writeTraceOps)
            : fProgram(program)
            , fContext(fProgram.fContext->fTypes, *fProgram.fContext->fErrors)
            , fDebugTrace(debugTrace)
            , fWriteTraceOps(writeTraceOps)
            , fProgramSlots(debugTrace ? &debugTrace->fSlotInfo : nullptr)
            , fUniformSlots(debugTrace ? &debugTrace->fUniformInfo : nullptr)
            , fImmutableSlots(nullptr) {
        fContext.fConfig = fProgram.fConfig.get();
        fContext.fModule = fProgram.fContext->fModule;
    }

    ~Generator() {
        // ~AutoStack calls into the Generator, so we need to make sure the trace mask is reset
        // before the Generator is destroyed.
        fTraceMask.reset();
    }

    /** Converts the SkSL main() function into a set of Instructions. */
    bool writeProgram(const FunctionDefinition& function);

    /** Returns the generated program. */
    std::unique_ptr<RP::Program> finish();

    /**
     * Converts an SkSL function into a set of Instructions. Returns nullopt if the function
     * contained unsupported statements or expressions.
     */
    std::optional<SlotRange> writeFunction(const IRNode& callSite,
                                           const FunctionDefinition& function,
                                           SkSpan<std::unique_ptr<Expression> const> arguments);

    /**
     * Returns the slot index of this function inside the FunctionDebugInfo array in DebugTracePriv.
     * The FunctionDebugInfo slot will be created if it doesn't already exist.
     */
    int getFunctionDebugInfo(const FunctionDeclaration& decl);

    /** Returns true for variables with slots in fProgramSlots; immutables or uniforms are false. */
    bool hasVariableSlots(const Variable& v) {
        return !IsUniform(v) && !fImmutableVariables.contains(&v);
    }

    /** Looks up the slots associated with an SkSL variable; creates the slots if necessary. */
    SlotRange getVariableSlots(const Variable& v) {
        SkASSERT(this->hasVariableSlots(v));
        return fProgramSlots.getVariableSlots(v);
    }

    /**
     * Looks up the slots associated with an immutable variable; creates the slots if necessary.
     */
    SlotRange getImmutableSlots(const Variable& v) {
        SkASSERT(!IsUniform(v));
        SkASSERT(fImmutableVariables.contains(&v));
        return fImmutableSlots.getVariableSlots(v);
    }

    /** Looks up the slots associated with an SkSL uniform; creates the slots if necessary. */
    SlotRange getUniformSlots(const Variable& v) {
        SkASSERT(IsUniform(v));
        SkASSERT(!fImmutableVariables.contains(&v));
        return fUniformSlots.getVariableSlots(v);
    }

    /**
     * Looks up the slots associated with an SkSL function's return value; creates the range if
     * necessary. Note that recursion is never supported, so we don't need to maintain return values
     * in a stack; we can just statically allocate one slot per function call-site.
     */
    SlotRange getFunctionSlots(const IRNode& callSite, const FunctionDeclaration& f) {
        return fProgramSlots.getFunctionSlots(callSite, f);
    }

    /**
     * Creates an additional stack for the program to push values onto. The stack will not become
     * actively in-use until `setCurrentStack` is called.
     */
    int createStack();

    /** Frees a stack generated by `createStack`. The freed stack must be completely empty. */
    void recycleStack(int stackID);

    /** Redirects builder ops to point to a different stack (created by `createStack`). */
    void setCurrentStack(int stackID);

    /** Reports the currently active stack. */
    int currentStack() {
        return fCurrentStack;
    }

    /**
     * Returns an LValue for the passed-in expression; if the expression isn't supported as an
     * LValue, returns nullptr.
     */
    std::unique_ptr<LValue> makeLValue(const Expression& e, bool allowScratch = false);

    /** Copies the top-of-stack value into this lvalue, without discarding it from the stack. */
    [[nodiscard]] bool store(LValue& lvalue);

    /** Pushes the lvalue onto the top-of-stack. */
    [[nodiscard]] bool push(LValue& lvalue);

    /** The Builder stitches our instructions together into Raster Pipeline code. */
    Builder* builder() { return &fBuilder; }

    /** Appends a statement to the program. */
    [[nodiscard]] bool writeStatement(const Statement& s);
    [[nodiscard]] bool writeBlock(const Block& b);
    [[nodiscard]] bool writeBreakStatement(const BreakStatement& b);
    [[nodiscard]] bool writeContinueStatement(const ContinueStatement& b);
    [[nodiscard]] bool writeDoStatement(const DoStatement& d);
    [[nodiscard]] bool writeExpressionStatement(const ExpressionStatement& e);
    [[nodiscard]] bool writeMasklessForStatement(const ForStatement& f);
    [[nodiscard]] bool writeForStatement(const ForStatement& f);
    [[nodiscard]] bool writeGlobals();
    [[nodiscard]] bool writeIfStatement(const IfStatement& i);
    [[nodiscard]] bool writeDynamicallyUniformIfStatement(const IfStatement& i);
    [[nodiscard]] bool writeReturnStatement(const ReturnStatement& r);
    [[nodiscard]] bool writeSwitchStatement(const SwitchStatement& s);
    [[nodiscard]] bool writeVarDeclaration(const VarDeclaration& v);
    [[nodiscard]] bool writeImmutableVarDeclaration(const VarDeclaration& d);

    /** Pushes an expression to the value stack. */
    [[nodiscard]] bool pushBinaryExpression(const BinaryExpression& e);
    [[nodiscard]] bool pushBinaryExpression(const Expression& left,
                                            Operator op,
                                            const Expression& right);
    [[nodiscard]] bool pushChildCall(const ChildCall& c);
    [[nodiscard]] bool pushConstructorCast(const AnyConstructor& c);
    [[nodiscard]] bool pushConstructorCompound(const AnyConstructor& c);
    [[nodiscard]] bool pushConstructorDiagonalMatrix(const ConstructorDiagonalMatrix& c);
    [[nodiscard]] bool pushConstructorMatrixResize(const ConstructorMatrixResize& c);
    [[nodiscard]] bool pushConstructorSplat(const ConstructorSplat& c);
    [[nodiscard]] bool pushExpression(const Expression& e, bool usesResult = true);
    [[nodiscard]] bool pushFieldAccess(const FieldAccess& f);
    [[nodiscard]] bool pushFunctionCall(const FunctionCall& c);
    [[nodiscard]] bool pushIndexExpression(const IndexExpression& i);
    [[nodiscard]] bool pushIntrinsic(const FunctionCall& c);
    [[nodiscard]] bool pushIntrinsic(IntrinsicKind intrinsic, const Expression& arg0);
    [[nodiscard]] bool pushIntrinsic(IntrinsicKind intrinsic,
                                     const Expression& arg0,
                                     const Expression& arg1);
    [[nodiscard]] bool pushIntrinsic(IntrinsicKind intrinsic,
                                     const Expression& arg0,
                                     const Expression& arg1,
                                     const Expression& arg2);
    [[nodiscard]] bool pushLiteral(const Literal& l);
    [[nodiscard]] bool pushPostfixExpression(const PostfixExpression& p, bool usesResult);
    [[nodiscard]] bool pushPrefixExpression(const PrefixExpression& p);
    [[nodiscard]] bool pushPrefixExpression(Operator op, const Expression& expr);
    [[nodiscard]] bool pushSwizzle(const Swizzle& s);
    [[nodiscard]] bool pushTernaryExpression(const TernaryExpression& t);
    [[nodiscard]] bool pushTernaryExpression(const Expression& test,
                                             const Expression& ifTrue,
                                             const Expression& ifFalse);
    [[nodiscard]] bool pushDynamicallyUniformTernaryExpression(const Expression& test,
                                                               const Expression& ifTrue,
                                                               const Expression& ifFalse);
    [[nodiscard]] bool pushVariableReference(const VariableReference& v);

    /** Support methods for immutable data, which trade more slots for smaller code size. */
    using ImmutableBits = int32_t;

    [[nodiscard]] bool pushImmutableData(const Expression& e);
    [[nodiscard]] std::optional<SlotRange> findPreexistingImmutableData(
            const TArray<ImmutableBits>& immutableValues);
    [[nodiscard]] std::optional<ImmutableBits> getImmutableBitsForSlot(const Expression& expr,
                                                                       size_t slot);
    [[nodiscard]] bool getImmutableValueForExpression(const Expression& expr,
                                                      TArray<ImmutableBits>* immutableValues);
    void storeImmutableValueToSlots(const TArray<ImmutableBits>& immutableValues, SlotRange slots);

    /** Pops an expression from the value stack and copies it into slots. */
    void popToSlotRange(SlotRange r) {
        fBuilder.pop_slots(r);
        if (this->shouldWriteTraceOps()) {
            fBuilder.trace_var(fTraceMask->stackID(), r);
        }
    }
    void popToSlotRangeUnmasked(SlotRange r) {
        fBuilder.pop_slots_unmasked(r);
        if (this->shouldWriteTraceOps()) {
            fBuilder.trace_var(fTraceMask->stackID(), r);
        }
    }

    /** Pops an expression from the value stack and discards it. */
    void discardExpression(int slots) { fBuilder.discard_stack(slots); }

    /** Zeroes out a range of slots. */
    void zeroSlotRangeUnmasked(SlotRange r) {
        fBuilder.zero_slots_unmasked(r);
        if (this->shouldWriteTraceOps()) {
            fBuilder.trace_var(fTraceMask->stackID(), r);
        }
    }

    /**
     * Emits a trace_line opcode. writeStatement does this, and statements that alter control flow
     * may need to explicitly add additional traces.
     */
    void emitTraceLine(Position pos);

    /**
     * Emits a trace_scope opcode, which alters the SkSL variable-scope depth.
     * Unlike the other trace ops, trace_scope takes a dedicated mask instead of the trace-scope
     * mask. Call `pushTraceScopeMask` to synthesize this mask; discard it when you're done.
     */
    void pushTraceScopeMask();
    void discardTraceScopeMask();
    void emitTraceScope(int delta);

    /** Prepares our position-to-line-offset conversion table (stored in `fLineOffsets`). */
    void calculateLineOffsets();

    bool shouldWriteTraceOps() { return fDebugTrace && fWriteTraceOps; }
    int traceMaskStackID() { return fTraceMask->stackID(); }

    /** Expression utilities. */
    struct TypedOps {
        BuilderOp fFloatOp;
        BuilderOp fSignedOp;
        BuilderOp fUnsignedOp;
        BuilderOp fBooleanOp;
    };

    static BuilderOp GetTypedOp(const SkSL::Type& type, const TypedOps& ops);

    [[nodiscard]] bool unaryOp(const SkSL::Type& type, const TypedOps& ops);
    [[nodiscard]] bool binaryOp(const SkSL::Type& type, const TypedOps& ops);
    [[nodiscard]] bool ternaryOp(const SkSL::Type& type, const TypedOps& ops);
    [[nodiscard]] bool pushIntrinsic(const TypedOps& ops, const Expression& arg0);
    [[nodiscard]] bool pushIntrinsic(const TypedOps& ops,
                                     const Expression& arg0,
                                     const Expression& arg1);
    [[nodiscard]] bool pushIntrinsic(BuilderOp builderOp, const Expression& arg0);
    [[nodiscard]] bool pushIntrinsic(BuilderOp builderOp,
                                     const Expression& arg0,
                                     const Expression& arg1);
    [[nodiscard]] bool pushAbsFloatIntrinsic(int slots);
    [[nodiscard]] bool pushLengthIntrinsic(int slotCount);
    [[nodiscard]] bool pushVectorizedExpression(const Expression& expr, const Type& vectorType);
    [[nodiscard]] bool pushVariableReferencePartial(const VariableReference& v, SlotRange subset);
    [[nodiscard]] bool pushLValueOrExpression(LValue* lvalue, const Expression& expr);
    [[nodiscard]] bool pushMatrixMultiply(LValue* lvalue,
                                          const Expression& left,
                                          const Expression& right,
                                          int leftColumns, int leftRows,
                                          int rightColumns, int rightRows);
    [[nodiscard]] bool pushStructuredComparison(LValue* left,
                                                Operator op,
                                                LValue* right,
                                                const Type& type);

    void foldWithMultiOp(BuilderOp op, int elements);
    void foldComparisonOp(Operator op, int elements);

    BuilderOp getTypedOp(const SkSL::Type& type, const TypedOps& ops) const;

    Analysis::ReturnComplexity returnComplexity(const FunctionDefinition* func) {
        Analysis::ReturnComplexity* complexity = fReturnComplexityMap.find(func);
        if (!complexity) {
            complexity = fReturnComplexityMap.set(fCurrentFunction,
                                                  Analysis::GetReturnComplexity(*func));
        }
        return *complexity;
    }

    bool needsReturnMask(const FunctionDefinition* func) {
        return this->returnComplexity(func) >= Analysis::ReturnComplexity::kEarlyReturns;
    }

    bool needsFunctionResultSlots(const FunctionDefinition* func) {
        return this->shouldWriteTraceOps() || (this->returnComplexity(func) >
                                               Analysis::ReturnComplexity::kSingleSafeReturn);
    }

    static bool IsUniform(const Variable& var) {
       return var.modifierFlags().isUniform();
    }

    static bool IsOutParameter(const Variable& var) {
        return (var.modifierFlags() & (ModifierFlag::kIn | ModifierFlag::kOut)) ==
               ModifierFlag::kOut;
    }

    static bool IsInoutParameter(const Variable& var) {
        return (var.modifierFlags() & (ModifierFlag::kIn | ModifierFlag::kOut)) ==
               (ModifierFlag::kIn | ModifierFlag::kOut);
    }

private:
    const SkSL::Program& fProgram;
    SkSL::Context fContext;
    Builder fBuilder;
    DebugTracePriv* fDebugTrace = nullptr;
    bool fWriteTraceOps = false;
    THashMap<const Variable*, int> fChildEffectMap;

    SlotManager fProgramSlots;
    SlotManager fUniformSlots;
    SlotManager fImmutableSlots;

    std::optional<AutoStack> fTraceMask;
    const FunctionDefinition* fCurrentFunction = nullptr;
    SlotRange fCurrentFunctionResult;
    AutoContinueMask* fCurrentContinueMask = nullptr;
    int fCurrentBreakTarget = -1;
    int fCurrentStack = 0;
    int fNextStackID = 0;
    TArray<int> fRecycledStacks;

    THashMap<const FunctionDefinition*, Analysis::ReturnComplexity> fReturnComplexityMap;

    THashMap<ImmutableBits, THashSet<Slot>> fImmutableSlotMap;
    THashSet<const Variable*> fImmutableVariables;

    // `fInsideCompoundStatement` will be nonzero if we are currently writing statements inside of a
    // compound-statement Block. (Conceptually those statements should all count as one.)
    int fInsideCompoundStatement = 0;

    // `fLineOffsets` contains the position of each newline in the source, plus a zero at the
    // beginning, and the total source length at the end, as sentinels.
    TArray<int> fLineOffsets;

    static constexpr auto kAddOps = TypedOps{BuilderOp::add_n_floats,
                                             BuilderOp::add_n_ints,
                                             BuilderOp::add_n_ints,
                                             BuilderOp::unsupported};
    static constexpr auto kSubtractOps = TypedOps{BuilderOp::sub_n_floats,
                                                  BuilderOp::sub_n_ints,
                                                  BuilderOp::sub_n_ints,
                                                  BuilderOp::unsupported};
    static constexpr auto kMultiplyOps = TypedOps{BuilderOp::mul_n_floats,
                                                  BuilderOp::mul_n_ints,
                                                  BuilderOp::mul_n_ints,
                                                  BuilderOp::unsupported};
    static constexpr auto kDivideOps = TypedOps{BuilderOp::div_n_floats,
                                                BuilderOp::div_n_ints,
                                                BuilderOp::div_n_uints,
                                                BuilderOp::unsupported};
    static constexpr auto kLessThanOps = TypedOps{BuilderOp::cmplt_n_floats,
                                                  BuilderOp::cmplt_n_ints,
                                                  BuilderOp::cmplt_n_uints,
                                                  BuilderOp::unsupported};
    static constexpr auto kLessThanEqualOps = TypedOps{BuilderOp::cmple_n_floats,
                                                       BuilderOp::cmple_n_ints,
                                                       BuilderOp::cmple_n_uints,
                                                       BuilderOp::unsupported};
    static constexpr auto kEqualOps = TypedOps{BuilderOp::cmpeq_n_floats,
                                               BuilderOp::cmpeq_n_ints,
                                               BuilderOp::cmpeq_n_ints,
                                               BuilderOp::cmpeq_n_ints};
    static constexpr auto kNotEqualOps = TypedOps{BuilderOp::cmpne_n_floats,
                                                  BuilderOp::cmpne_n_ints,
                                                  BuilderOp::cmpne_n_ints,
                                                  BuilderOp::cmpne_n_ints};
    static constexpr auto kModOps = TypedOps{BuilderOp::mod_n_floats,
                                             BuilderOp::unsupported,
                                             BuilderOp::unsupported,
                                             BuilderOp::unsupported};
    static constexpr auto kMinOps = TypedOps{BuilderOp::min_n_floats,
                                             BuilderOp::min_n_ints,
                                             BuilderOp::min_n_uints,
                                             BuilderOp::min_n_uints};
    static constexpr auto kMaxOps = TypedOps{BuilderOp::max_n_floats,
                                             BuilderOp::max_n_ints,
                                             BuilderOp::max_n_uints,
                                             BuilderOp::max_n_uints};
    static constexpr auto kMixOps = TypedOps{BuilderOp::mix_n_floats,
                                             BuilderOp::unsupported,
                                             BuilderOp::unsupported,
                                             BuilderOp::unsupported};
    static constexpr auto kInverseSqrtOps = TypedOps{BuilderOp::invsqrt_float,
                                                     BuilderOp::unsupported,
                                                     BuilderOp::unsupported,
                                                     BuilderOp::unsupported};
    friend class AutoContinueMask;
};

AutoStack::AutoStack(Generator* g)
        : fGenerator(g)
        , fStackID(g->createStack()) {}

AutoStack::~AutoStack() {
    fGenerator->recycleStack(fStackID);
}

void AutoStack::enter() {
    fParentStackID = fGenerator->currentStack();
    fGenerator->setCurrentStack(fStackID);
}

void AutoStack::exit() {
    SkASSERT(fGenerator->currentStack() == fStackID);
    fGenerator->setCurrentStack(fParentStackID);
}

void AutoStack::pushClone(int slots) {
    this->pushClone(SlotRange{0, slots}, /*offsetFromStackTop=*/slots);
}

void AutoStack::pushClone(SlotRange range, int offsetFromStackTop) {
    fGenerator->builder()->push_clone_from_stack(range, fStackID, offsetFromStackTop);
}

void AutoStack::pushCloneIndirect(SlotRange range, int dynamicStackID, int offsetFromStackTop) {
    fGenerator->builder()->push_clone_indirect_from_stack(
            range, dynamicStackID, /*otherStackID=*/fStackID, offsetFromStackTop);
}

class AutoContinueMask {
public:
    AutoContinueMask(Generator* gen) : fGenerator(gen) {}

    ~AutoContinueMask() {
        if (fPreviousContinueMask) {
            fGenerator->fCurrentContinueMask = fPreviousContinueMask;
        }
    }

    void enable() {
        SkASSERT(!fContinueMaskStack.has_value());

        fContinueMaskStack.emplace(fGenerator);
        fPreviousContinueMask = fGenerator->fCurrentContinueMask;
        fGenerator->fCurrentContinueMask = this;
    }

    void enter() {
        SkASSERT(fContinueMaskStack.has_value());
        fContinueMaskStack->enter();
    }

    void exit() {
        SkASSERT(fContinueMaskStack.has_value());
        fContinueMaskStack->exit();
    }

    void enterLoopBody() {
        if (fContinueMaskStack.has_value()) {
            fContinueMaskStack->enter();
            fGenerator->builder()->push_constant_i(0);
            fContinueMaskStack->exit();
        }
    }

    void exitLoopBody() {
        if (fContinueMaskStack.has_value()) {
            fContinueMaskStack->enter();
            fGenerator->builder()->pop_and_reenable_loop_mask();
            fContinueMaskStack->exit();
        }
    }

    int stackID() {
        SkASSERT(fContinueMaskStack.has_value());
        return fContinueMaskStack->stackID();
    }

private:
    std::optional<AutoStack> fContinueMaskStack;
    Generator* fGenerator = nullptr;
    AutoContinueMask* fPreviousContinueMask = nullptr;
};

class AutoLoopTarget {
public:
    AutoLoopTarget(Generator* gen, int* targetPtr) : fGenerator(gen), fLoopTargetPtr(targetPtr) {
        fLabelID = fGenerator->builder()->nextLabelID();
        fPreviousLoopTarget = *fLoopTargetPtr;
        *fLoopTargetPtr = fLabelID;
    }

    ~AutoLoopTarget() {
        *fLoopTargetPtr = fPreviousLoopTarget;
    }

    int labelID() {
        return fLabelID;
    }

private:
    Generator* fGenerator = nullptr;
    int* fLoopTargetPtr = nullptr;
    int fPreviousLoopTarget;
    int fLabelID;
};

class LValue {
public:
    virtual ~LValue() = default;

    /** Returns true if this lvalue is actually writable--temporaries and uniforms are not. */
    virtual bool isWritable() const = 0;

    /**
     * Returns the fixed slot range of the lvalue, after it is winnowed down to the selected
     * field/index. The range is calculated assuming every dynamic index will evaluate to zero.
     */
    virtual SlotRange fixedSlotRange(Generator* gen) = 0;

    /**
     * Returns a stack which holds a single integer, representing the dynamic offset of the lvalue.
     * This value does not incorporate the fixed offset. If null is returned, the lvalue doesn't
     * have a dynamic offset. `evaluateDynamicIndices` must be called before this is used.
     */
    virtual AutoStack* dynamicSlotRange() = 0;

    /** Returns the swizzle components of the lvalue, or an empty span for non-swizzle LValues. */
    virtual SkSpan<const int8_t> swizzle() { return {}; }

    /** Pushes values directly onto the stack. */
    [[nodiscard]] virtual bool push(Generator* gen,
                                    SlotRange fixedOffset,
                                    AutoStack* dynamicOffset,
                                    SkSpan<const int8_t> swizzle) = 0;

    /** Stores topmost values from the stack directly into the lvalue. */
    [[nodiscard]] virtual bool store(Generator* gen,
                                     SlotRange fixedOffset,
                                     AutoStack* dynamicOffset,
                                     SkSpan<const int8_t> swizzle) = 0;
    /**
     * Some lvalues refer to a temporary expression; these temps can be held in the
     * scratch-expression field to ensure that they exist for the lifetime of the lvalue.
     */
    std::unique_ptr<Expression> fScratchExpression;
};

class ScratchLValue final : public LValue {
public:
    explicit ScratchLValue(const Expression& e)
            : fExpression(&e)
            , fNumSlots(e.type().slotCount()) {}

    ~ScratchLValue() override {
        if (fGenerator && fDedicatedStack.has_value()) {
            // Jettison the scratch expression.
            fDedicatedStack->enter();
            fGenerator->discardExpression(fNumSlots);
            fDedicatedStack->exit();
        }
    }

    bool isWritable() const override {
        return false;
    }

    SlotRange fixedSlotRange(Generator* gen) override {
        return SlotRange{0, fNumSlots};
    }

    AutoStack* dynamicSlotRange() override {
        return nullptr;
    }

    [[nodiscard]] bool push(Generator* gen,
                            SlotRange fixedOffset,
                            AutoStack* dynamicOffset,
                            SkSpan<const int8_t> swizzle) override {
        if (!fDedicatedStack.has_value()) {
            // Push the scratch expression onto a dedicated stack.
            fGenerator = gen;
            fDedicatedStack.emplace(fGenerator);
            fDedicatedStack->enter();
            if (!fGenerator->pushExpression(*fExpression)) {
                return unsupported();
            }
            fDedicatedStack->exit();
        }

        if (dynamicOffset) {
            fDedicatedStack->pushCloneIndirect(fixedOffset, dynamicOffset->stackID(), fNumSlots);
        } else {
            fDedicatedStack->pushClone(fixedOffset, fNumSlots);
        }
        if (!swizzle.empty()) {
            gen->builder()->swizzle(fixedOffset.count, swizzle);
        }
        return true;
    }

    [[nodiscard]] bool store(Generator*, SlotRange, AutoStack*, SkSpan<const int8_t>) override {
        SkDEBUGFAIL("scratch lvalues cannot be stored into");
        return unsupported();
    }

private:
    Generator* fGenerator = nullptr;
    const Expression* fExpression = nullptr;
    std::optional<AutoStack> fDedicatedStack;
    int fNumSlots = 0;
};

class VariableLValue final : public LValue {
public:
    explicit VariableLValue(const Variable* v) : fVariable(v) {}

    bool isWritable() const override {
        return !Generator::IsUniform(*fVariable);
    }

    SlotRange fixedSlotRange(Generator* gen) override {
        return Generator::IsUniform(*fVariable) ? gen->getUniformSlots(*fVariable)
                                                : gen->getVariableSlots(*fVariable);
    }

    AutoStack* dynamicSlotRange() override {
        return nullptr;
    }

    [[nodiscard]] bool push(Generator* gen,
                            SlotRange fixedOffset,
                            AutoStack* dynamicOffset,
                            SkSpan<const int8_t> swizzle) override {
        if (Generator::IsUniform(*fVariable)) {
            if (dynamicOffset) {
                gen->builder()->push_uniform_indirect(fixedOffset, dynamicOffset->stackID(),
                                                      this->fixedSlotRange(gen));
            } else {
                gen->builder()->push_uniform(fixedOffset);
            }
        } else {
            if (dynamicOffset) {
                gen->builder()->push_slots_indirect(fixedOffset, dynamicOffset->stackID(),
                                                    this->fixedSlotRange(gen));
            } else {
                gen->builder()->push_slots(fixedOffset);
            }
        }
        if (!swizzle.empty()) {
            gen->builder()->swizzle(fixedOffset.count, swizzle);
        }
        return true;
    }

    [[nodiscard]] bool store(Generator* gen,
                             SlotRange fixedOffset,
                             AutoStack* dynamicOffset,
                             SkSpan<const int8_t> swizzle) override {
        SkASSERT(!Generator::IsUniform(*fVariable));

        if (swizzle.empty()) {
            if (dynamicOffset) {
                gen->builder()->copy_stack_to_slots_indirect(fixedOffset, dynamicOffset->stackID(),
                                                             this->fixedSlotRange(gen));
            } else {
                gen->builder()->copy_stack_to_slots(fixedOffset);
            }
        } else {
            if (dynamicOffset) {
                gen->builder()->swizzle_copy_stack_to_slots_indirect(fixedOffset,
                                                                     dynamicOffset->stackID(),
                                                                     this->fixedSlotRange(gen),
                                                                     swizzle,
                                                                     swizzle.size());
            } else {
                gen->builder()->swizzle_copy_stack_to_slots(fixedOffset, swizzle, swizzle.size());
            }
        }
        if (gen->shouldWriteTraceOps()) {
            if (dynamicOffset) {
                gen->builder()->trace_var_indirect(gen->traceMaskStackID(),
                                                   fixedOffset,
                                                   dynamicOffset->stackID(),
                                                   this->fixedSlotRange(gen));
            } else {
                gen->builder()->trace_var(gen->traceMaskStackID(), fixedOffset);
            }
        }
        return true;
    }

private:
    const Variable* fVariable;
};

class ImmutableLValue final : public LValue {
public:
    explicit ImmutableLValue(const Variable* v) : fVariable(v) {}

    bool isWritable() const override {
        return false;
    }

    SlotRange fixedSlotRange(Generator* gen) override {
        return gen->getImmutableSlots(*fVariable);
    }

    AutoStack* dynamicSlotRange() override {
        return nullptr;
    }

    [[nodiscard]] bool push(Generator* gen,
                            SlotRange fixedOffset,
                            AutoStack* dynamicOffset,
                            SkSpan<const int8_t> swizzle) override {
        if (dynamicOffset) {
            gen->builder()->push_immutable_indirect(fixedOffset, dynamicOffset->stackID(),
                                                    this->fixedSlotRange(gen));
        } else {
            gen->builder()->push_immutable(fixedOffset);
        }
        if (!swizzle.empty()) {
            gen->builder()->swizzle(fixedOffset.count, swizzle);
        }
        return true;
    }

    [[nodiscard]] bool store(Generator* gen,
                             SlotRange fixedOffset,
                             AutoStack* dynamicOffset,
                             SkSpan<const int8_t> swizzle) override {
        SkDEBUGFAIL("immutable values cannot be stored into");
        return unsupported();
    }

private:
    const Variable* fVariable;
};

class SwizzleLValue final : public LValue {
public:
    explicit SwizzleLValue(std::unique_ptr<LValue> p, const ComponentArray& c)
            : fParent(std::move(p))
            , fComponents(c) {
        SkASSERT(!fComponents.empty() && fComponents.size() <= 4);
    }

    bool isWritable() const override {
        return fParent->isWritable();
    }

    SlotRange fixedSlotRange(Generator* gen) override {
        return fParent->fixedSlotRange(gen);
    }

    AutoStack* dynamicSlotRange() override {
        return fParent->dynamicSlotRange();
    }

    SkSpan<const int8_t> swizzle() override {
        return fComponents;
    }

    [[nodiscard]] bool push(Generator* gen,
                            SlotRange fixedOffset,
                            AutoStack* dynamicOffset,
                            SkSpan<const int8_t> swizzle) override {
        if (!swizzle.empty()) {
            SkDEBUGFAIL("swizzle-of-a-swizzle should have been folded out in front end");
            return unsupported();
        }
        return fParent->push(gen, fixedOffset, dynamicOffset, fComponents);
    }

    [[nodiscard]] bool store(Generator* gen,
                             SlotRange fixedOffset,
                             AutoStack* dynamicOffset,
                             SkSpan<const int8_t> swizzle) override {
        if (!swizzle.empty()) {
            SkDEBUGFAIL("swizzle-of-a-swizzle should have been folded out in front end");
            return unsupported();
        }
        return fParent->store(gen, fixedOffset, dynamicOffset, fComponents);
    }

private:
    std::unique_ptr<LValue> fParent;
    const ComponentArray& fComponents;
};

class UnownedLValueSlice : public LValue {
public:
    explicit UnownedLValueSlice(LValue* p, int initialSlot, int numSlots)
            : fParent(p)
            , fInitialSlot(initialSlot)
            , fNumSlots(numSlots) {
        SkASSERT(fInitialSlot >= 0);
        SkASSERT(fNumSlots > 0);
    }

    bool isWritable() const override {
        return fParent->isWritable();
    }

    SlotRange fixedSlotRange(Generator* gen) override {
        SlotRange range = fParent->fixedSlotRange(gen);
        SlotRange adjusted = range;
        adjusted.index += fInitialSlot;
        adjusted.count = fNumSlots;
        SkASSERT((adjusted.index + adjusted.count) <= (range.index + range.count));
        return adjusted;
    }

    AutoStack* dynamicSlotRange() override {
        return fParent->dynamicSlotRange();
    }

    [[nodiscard]] bool push(Generator* gen,
                            SlotRange fixedOffset,
                            AutoStack* dynamicOffset,
                            SkSpan<const int8_t> swizzle) override {
        return fParent->push(gen, fixedOffset, dynamicOffset, swizzle);
    }

    [[nodiscard]] bool store(Generator* gen,
                             SlotRange fixedOffset,
                             AutoStack* dynamicOffset,
                             SkSpan<const int8_t> swizzle) override {
        return fParent->store(gen, fixedOffset, dynamicOffset, swizzle);
    }

protected:
    LValue* fParent;

private:
    int fInitialSlot = 0;
    int fNumSlots = 0;
};

class LValueSlice final : public UnownedLValueSlice {
public:
    explicit LValueSlice(std::unique_ptr<LValue> p, int initialSlot, int numSlots)
            : UnownedLValueSlice(p.release(), initialSlot, numSlots) {}

    ~LValueSlice() override {
        delete fParent;
    }
};

class DynamicIndexLValue final : public LValue {
public:
    explicit DynamicIndexLValue(std::unique_ptr<LValue> p, const IndexExpression& i)
            : fParent(std::move(p))
            , fIndexExpr(&i) {
        SkASSERT(fIndexExpr->index()->type().isInteger());
    }

    ~DynamicIndexLValue() override {
        if (fDedicatedStack.has_value()) {
            SkASSERT(fGenerator);

            // Jettison the index expression.
            fDedicatedStack->enter();
            fGenerator->discardExpression(/*slots=*/1);
            fDedicatedStack->exit();
        }
    }

    bool isWritable() const override {
        return fParent->isWritable();
    }

    [[nodiscard]] bool evaluateDynamicIndices(Generator* gen) {
        // The index must only be computed once; the index-expression could have side effects.
        // Once it has been computed, the offset lives on `fDedicatedStack`.
        SkASSERT(!fDedicatedStack.has_value());
        SkASSERT(!fGenerator);
        fGenerator = gen;
        fDedicatedStack.emplace(fGenerator);

        if (!fParent->swizzle().empty()) {
            SkDEBUGFAIL("an indexed-swizzle should have been handled by RewriteIndexedSwizzle");
            return unsupported();
        }

        // Push the index expression onto the dedicated stack.
        fDedicatedStack->enter();
        if (!fGenerator->pushExpression(*fIndexExpr->index())) {
            return unsupported();
        }

        // Multiply the index-expression result by the per-value slot count.
        int slotCount = fIndexExpr->type().slotCount();
        if (slotCount != 1) {
            fGenerator->builder()->push_constant_i(fIndexExpr->type().slotCount());
            fGenerator->builder()->binary_op(BuilderOp::mul_n_ints, 1);
        }

        // Check to see if a parent LValue already has a dynamic index. If so, we need to
        // incorporate its value into our own.
        if (AutoStack* parentDynamicIndexStack = fParent->dynamicSlotRange()) {
            parentDynamicIndexStack->pushClone(/*slots=*/1);
            fGenerator->builder()->binary_op(BuilderOp::add_n_ints, 1);
        }
        fDedicatedStack->exit();
        return true;
    }

    SlotRange fixedSlotRange(Generator* gen) override {
        // Compute the fixed slot range as if we are indexing into position zero.
        SlotRange range = fParent->fixedSlotRange(gen);
        range.count = fIndexExpr->type().slotCount();
        return range;
    }

    AutoStack* dynamicSlotRange() override {
        // We incorporated any parent dynamic offsets when `evaluateDynamicIndices` was called.
        SkASSERT(fDedicatedStack.has_value());
        return &*fDedicatedStack;
    }

    [[nodiscard]] bool push(Generator* gen,
                            SlotRange fixedOffset,
                            AutoStack* dynamicOffset,
                            SkSpan<const int8_t> swizzle) override {
        return fParent->push(gen, fixedOffset, dynamicOffset, swizzle);
    }

    [[nodiscard]] bool store(Generator* gen,
                             SlotRange fixedOffset,
                             AutoStack* dynamicOffset,
                             SkSpan<const int8_t> swizzle) override {
        return fParent->store(gen, fixedOffset, dynamicOffset, swizzle);
    }

private:
    Generator* fGenerator = nullptr;
    std::unique_ptr<LValue> fParent;
    std::optional<AutoStack> fDedicatedStack;
    const IndexExpression* fIndexExpr = nullptr;
};

void SlotManager::addSlotDebugInfoForGroup(const std::string& varName,
                                           const Type& type,
                                           Position pos,
                                           int* groupIndex,
                                           bool isFunctionReturnValue) {
    SkASSERT(fSlotDebugInfo);
    switch (type.typeKind()) {
        case Type::TypeKind::kArray: {
            int nslots = type.columns();
            const Type& elemType = type.componentType();
            for (int slot = 0; slot < nslots; ++slot) {
                this->addSlotDebugInfoForGroup(varName + "[" + std::to_string(slot) + "]", elemType,
                                               pos, groupIndex, isFunctionReturnValue);
            }
            break;
        }
        case Type::TypeKind::kStruct: {
            for (const Field& field : type.fields()) {
                this->addSlotDebugInfoForGroup(varName + "." + std::string(field.fName),
                                               *field.fType, pos, groupIndex,
                                               isFunctionReturnValue);
            }
            break;
        }
        default:
            SkASSERTF(0, "unsupported slot type %d", (int)type.typeKind());
            [[fallthrough]];

        case Type::TypeKind::kScalar:
        case Type::TypeKind::kVector:
        case Type::TypeKind::kMatrix: {
            Type::NumberKind numberKind = type.componentType().numberKind();
            int nslots = type.slotCount();

            for (int slot = 0; slot < nslots; ++slot) {
                SlotDebugInfo slotInfo;
                slotInfo.name = varName;
                slotInfo.columns = type.columns();
                slotInfo.rows = type.rows();
                slotInfo.componentIndex = slot;
                slotInfo.groupIndex = (*groupIndex)++;
                slotInfo.numberKind = numberKind;
                slotInfo.pos = pos;
                slotInfo.fnReturnValue = isFunctionReturnValue ? 1 : -1;
                fSlotDebugInfo->push_back(std::move(slotInfo));
            }
            break;
        }
    }
}

void SlotManager::addSlotDebugInfo(const std::string& varName,
                                   const Type& type,
                                   Position pos,
                                   bool isFunctionReturnValue) {
    int groupIndex = 0;
    this->addSlotDebugInfoForGroup(varName, type, pos, &groupIndex, isFunctionReturnValue);
    SkASSERT((size_t)groupIndex == type.slotCount());
}

SlotRange SlotManager::createSlots(std::string name,
                                   const Type& type,
                                   Position pos,
                                   bool isFunctionReturnValue) {
    size_t nslots = type.slotCount();
    if (nslots == 0) {
        return {};
    }
    if (fSlotDebugInfo) {
        // Our debug slot-info table should have the same length as the actual slot table.
        SkASSERT(fSlotDebugInfo->size() == (size_t)fSlotCount);

        // Append slot names and types to our debug slot-info table.
        fSlotDebugInfo->reserve(fSlotCount + nslots);
        this->addSlotDebugInfo(name, type, pos, isFunctionReturnValue);

        // Confirm that we added the expected number of slots.
        SkASSERT(fSlotDebugInfo->size() == (size_t)(fSlotCount + nslots));
    }

    SlotRange result = {fSlotCount, (int)nslots};
    fSlotCount += nslots;
    return result;
}

std::optional<SlotRange> SlotManager::mapVariableToSlots(const Variable& v, SlotRange range) {
    SkASSERT(v.type().slotCount() == SkToSizeT(range.count));
    const SlotRange* existingEntry = fSlotMap.find(&v);
    std::optional<SlotRange> originalRange = existingEntry ? std::optional(*existingEntry)
                                                           : std::nullopt;
    fSlotMap.set(&v, range);
    return originalRange;
}

void SlotManager::unmapVariableSlots(const Variable& v) {
    fSlotMap.remove(&v);
}

SlotRange SlotManager::getVariableSlots(const Variable& v) {
    SlotRange* entry = fSlotMap.find(&v);
    if (entry != nullptr) {
        return *entry;
    }
    SlotRange range = this->createSlots(std::string(v.name()),
                                        v.type(),
                                        v.fPosition,
                                        /*isFunctionReturnValue=*/false);
    this->mapVariableToSlots(v, range);
    return range;
}

SlotRange SlotManager::getFunctionSlots(const IRNode& callSite, const FunctionDeclaration& f) {
    SlotRange* entry = fSlotMap.find(&callSite);
    if (entry != nullptr) {
        return *entry;
    }
    SlotRange range = this->createSlots("[" + std::string(f.name()) + "].result",
                                        f.returnType(),
                                        f.fPosition,
                                        /*isFunctionReturnValue=*/true);
    fSlotMap.set(&callSite, range);
    return range;
}

static bool is_sliceable_swizzle(SkSpan<const int8_t> components) {
    // Determine if the swizzle rearranges its elements, or if it's a simple subset of its elements.
    // (A simple subset would be a sequential non-repeating range of components, like `.xyz` or
    // `.yzw` or `.z`, but not `.xx` or `.xz`, which can be accessed as a slice of the variable.)
    for (size_t index = 1; index < components.size(); ++index) {
        if (components[index] != int8_t(components[0] + index)) {
            return false;
        }
    }
    return true;
}

std::unique_ptr<LValue> Generator::makeLValue(const Expression& e, bool allowScratch) {
    if (e.is<VariableReference>()) {
        const Variable* variable = e.as<VariableReference>().variable();
        if (fImmutableVariables.contains(variable)) {
            return std::make_unique<ImmutableLValue>(variable);
        }
        return std::make_unique<VariableLValue>(variable);
    }
    if (e.is<Swizzle>()) {
        const Swizzle& swizzleExpr = e.as<Swizzle>();
        if (std::unique_ptr<LValue> base = this->makeLValue(*swizzleExpr.base(),
                                                            allowScratch)) {
            const ComponentArray& components = swizzleExpr.components();
            if (is_sliceable_swizzle(components)) {
                // If the swizzle is a contiguous subset, we can represent it with a fixed slice.
                return std::make_unique<LValueSlice>(std::move(base), components[0],
                                                     components.size());
            }
            return std::make_unique<SwizzleLValue>(std::move(base), components);
        }
        return nullptr;
    }
    if (e.is<FieldAccess>()) {
        const FieldAccess& fieldExpr = e.as<FieldAccess>();
        if (std::unique_ptr<LValue> base = this->makeLValue(*fieldExpr.base(),
                                                            allowScratch)) {
            // Represent field access with a slice.
            return std::make_unique<LValueSlice>(std::move(base), fieldExpr.initialSlot(),
                                                 fieldExpr.type().slotCount());
        }
        return nullptr;
    }
    if (e.is<IndexExpression>()) {
        const IndexExpression& indexExpr = e.as<IndexExpression>();

        // If the index base is swizzled (`vec.zyx[idx]`), rewrite it into an equivalent
        // non-swizzled form (`vec[uint3(2,1,0)[idx]]`).
        if (std::unique_ptr<Expression> rewritten = Transform::RewriteIndexedSwizzle(fContext,
                                                                                     indexExpr)) {
            // Convert the rewritten expression into an lvalue.
            std::unique_ptr<LValue> lvalue = this->makeLValue(*rewritten, allowScratch);
            if (!lvalue) {
                return nullptr;
            }
            // We need to hold onto the rewritten expression for the lifetime of the lvalue.
            lvalue->fScratchExpression = std::move(rewritten);
            return lvalue;
        }
        if (std::unique_ptr<LValue> base = this->makeLValue(*indexExpr.base(),
                                                            allowScratch)) {
            // If the index is a compile-time constant, we can represent it with a fixed slice.
            SKSL_INT indexValue;
            if (ConstantFolder::GetConstantInt(*indexExpr.index(), &indexValue)) {
                int numSlots = indexExpr.type().slotCount();
                return std::make_unique<LValueSlice>(std::move(base), numSlots * indexValue,
                                                     numSlots);
            }

            // Represent non-constant indexing via a dynamic index.
            auto dynLValue = std::make_unique<DynamicIndexLValue>(std::move(base), indexExpr);
            return dynLValue->evaluateDynamicIndices(this) ? std::move(dynLValue)
                                                           : nullptr;
        }
        return nullptr;
    }
    if (allowScratch) {
        // This path allows us to perform field- and index-accesses on an expression as if it were
        // an lvalue, but is a temporary and shouldn't be written back to.
        return std::make_unique<ScratchLValue>(e);
    }
    return nullptr;
}

bool Generator::push(LValue& lvalue) {
    return lvalue.push(this,
                       lvalue.fixedSlotRange(this),
                       lvalue.dynamicSlotRange(),
                       /*swizzle=*/{});
}

bool Generator::store(LValue& lvalue) {
    SkASSERT(lvalue.isWritable());
    return lvalue.store(this,
                        lvalue.fixedSlotRange(this),
                        lvalue.dynamicSlotRange(),
                        /*swizzle=*/{});
}

int Generator::getFunctionDebugInfo(const FunctionDeclaration& decl) {
    SkASSERT(fDebugTrace);

    std::string name = decl.description();

    // When generating the debug trace, we typically mark every function as `noinline`. This makes
    // the trace more confusing, since this isn't in the source program, so remove it.
    static constexpr std::string_view kNoInline = "noinline ";
    if (skstd::starts_with(name, kNoInline)) {
        name = name.substr(kNoInline.size());
    }

    // Look for a matching FunctionDebugInfo slot.
    for (size_t index = 0; index < fDebugTrace->fFuncInfo.size(); ++index) {
        if (fDebugTrace->fFuncInfo[index].name == name) {
            return index;
        }
    }

    // We've never called this function before; create a new slot to hold its information.
    int slot = (int)fDebugTrace->fFuncInfo.size();
    fDebugTrace->fFuncInfo.push_back(FunctionDebugInfo{std::move(name)});
    return slot;
}

int Generator::createStack() {
    if (!fRecycledStacks.empty()) {
        int stackID = fRecycledStacks.back();
        fRecycledStacks.pop_back();
        return stackID;
    }
    return ++fNextStackID;
}

void Generator::recycleStack(int stackID) {
    fRecycledStacks.push_back(stackID);
}

void Generator::setCurrentStack(int stackID) {
    if (fCurrentStack != stackID) {
        fCurrentStack = stackID;
        fBuilder.set_current_stack(stackID);
    }
}

std::optional<SlotRange> Generator::writeFunction(
        const IRNode& callSite,
        const FunctionDefinition& function,
        SkSpan<std::unique_ptr<Expression> const> arguments) {
    // Generate debug information and emit a trace-enter op.
    int funcIndex = -1;
    if (fDebugTrace) {
        funcIndex = this->getFunctionDebugInfo(function.declaration());
        SkASSERT(funcIndex >= 0);
        if (this->shouldWriteTraceOps()) {
            fBuilder.trace_enter(fTraceMask->stackID(), funcIndex);
        }
    }

    // Handle parameter lvalues.
    struct RemappedSlotRange {
        const Variable* fVariable;
        std::optional<SlotRange> fSlotRange;
    };
    SkSpan<Variable* const> parameters = function.declaration().parameters();
    TArray<std::unique_ptr<LValue>> lvalues;
    TArray<RemappedSlotRange> remappedSlotRanges;

    if (function.declaration().isMain()) {
        // For main(), the parameter slots have already been populated by `writeProgram`, but we
        // still need to explicitly emit trace ops for the variables in main(), since they are
        // initialized before it is safe to use trace-var. (We can't invoke init-lane-masks until
        // after we've copied the inputs from main into slots, because dst.rgba is used to pass in a
        // blend-destination color, but we clobber it and put in the execution mask instead.)
        if (this->shouldWriteTraceOps()) {
            for (const Variable* var : parameters) {
                fBuilder.trace_var(fTraceMask->stackID(), this->getVariableSlots(*var));
            }
        }
    } else {
        // Write all the arguments into their parameter's variable slots. Because we never allow
        // recursion, we don't need to worry about overwriting any existing values in those slots.
        // (In fact, we don't even need to apply the write mask.)
        lvalues.resize(arguments.size());

        for (size_t index = 0; index < arguments.size(); ++index) {
            const Expression& arg = *arguments[index];
            const Variable& param = *parameters[index];

            // If we are passing a child effect to a function, we need to add its mapping to our
            // child map.
            if (arg.type().isEffectChild()) {
                if (int* childIndex = fChildEffectMap.find(arg.as<VariableReference>()
                                                              .variable())) {
                    SkASSERT(!fChildEffectMap.find(&param));
                    fChildEffectMap[&param] = *childIndex;
                }
                continue;
            }

            // Use LValues for out-parameters and inout-parameters, so we can store back to them
            // later.
            if (IsInoutParameter(param) || IsOutParameter(param)) {
                lvalues[index] = this->makeLValue(arg);
                if (!lvalues[index]) {
                    return std::nullopt;
                }
                // There are no guarantees on the starting value of an out-parameter, so we only
                // need to store the lvalues associated with an inout parameter.
                if (IsInoutParameter(param)) {
                    if (!this->push(*lvalues[index])) {
                        return std::nullopt;
                    }
                    this->popToSlotRangeUnmasked(this->getVariableSlots(param));
                }
                continue;
            }

            // If a parameter is never read by the function, we don't need to populate its slots.
            ProgramUsage::VariableCounts paramCounts = fProgram.fUsage->get(param);
            if (paramCounts.fRead == 0) {
                // Honor the expression's side effects, if any.
                if (Analysis::HasSideEffects(arg)) {
                    if (!this->pushExpression(arg, /*usesResult=*/false)) {
                        return std::nullopt;
                    }
                    this->discardExpression(arg.type().slotCount());
                }
                continue;
            }

            // If the expression is a plain variable and the parameter is never written to, we don't
            // need to copy it; we can just share the slots from the existing variable.
            if (paramCounts.fWrite == 0 && arg.is<VariableReference>()) {
                const Variable& var = *arg.as<VariableReference>().variable();
                if (this->hasVariableSlots(var)) {
                    std::optional<SlotRange> originalRange =
                            fProgramSlots.mapVariableToSlots(param, this->getVariableSlots(var));
                    remappedSlotRanges.push_back({&param, originalRange});
                    continue;
                }
            }

            // Copy input arguments into their respective parameter slots.
            if (!this->pushExpression(arg)) {
                return std::nullopt;
            }
            this->popToSlotRangeUnmasked(this->getVariableSlots(param));
        }
    }

    // Set up a slot range dedicated to this function's return value.
    SlotRange lastFunctionResult = fCurrentFunctionResult;
    fCurrentFunctionResult = this->getFunctionSlots(callSite, function.declaration());

    // Save off the return mask.
    if (this->needsReturnMask(&function)) {
        fBuilder.enableExecutionMaskWrites();
        if (!function.declaration().isMain()) {
            fBuilder.push_return_mask();
        }
    }

    // Emit the function body.
    if (!this->writeStatement(*function.body())) {
        return std::nullopt;
    }

    // Restore the original return mask.
    if (this->needsReturnMask(&function)) {
        if (!function.declaration().isMain()) {
            fBuilder.pop_return_mask();
        }
        fBuilder.disableExecutionMaskWrites();
    }

    // Restore the function-result slot range.
    SlotRange functionResult = fCurrentFunctionResult;
    fCurrentFunctionResult = lastFunctionResult;

    // Emit a trace-exit op.
    if (fDebugTrace && fWriteTraceOps) {
        fBuilder.trace_exit(fTraceMask->stackID(), funcIndex);
    }

    // Copy out-parameters and inout-parameters back to their homes.
    for (int index = 0; index < lvalues.size(); ++index) {
        if (lvalues[index]) {
            // Only out- and inout-parameters should have an associated lvalue.
            const Variable& param = *parameters[index];
            SkASSERT(IsInoutParameter(param) || IsOutParameter(param));

            // Copy the parameter's slots directly into the lvalue.
            fBuilder.push_slots(this->getVariableSlots(param));
            if (!this->store(*lvalues[index])) {
                return std::nullopt;
            }
            this->discardExpression(param.type().slotCount());
        }
    }

    // Restore any remapped parameter slot ranges to their original values.
    for (const RemappedSlotRange& remapped : remappedSlotRanges) {
        if (remapped.fSlotRange.has_value()) {
            fProgramSlots.mapVariableToSlots(*remapped.fVariable, *remapped.fSlotRange);
        } else {
            fProgramSlots.unmapVariableSlots(*remapped.fVariable);
        }
    }

    // Remove any child-effect mappings that were made for this call.
    for (size_t index = 0; index < arguments.size(); ++index) {
        const Expression& arg = *arguments[index];
        if (arg.type().isEffectChild()) {
            fChildEffectMap.remove(parameters[index]);
        }
    }

    return functionResult;
}

void Generator::emitTraceLine(Position pos) {
    if (fDebugTrace && fWriteTraceOps && pos.valid() && fInsideCompoundStatement == 0) {
        // Binary search within fLineOffets to convert the position into a line number.
        SkASSERT(fLineOffsets.size() >= 2);
        SkASSERT(fLineOffsets[0] == 0);
        SkASSERT(fLineOffsets.back() == (int)fProgram.fSource->length());
        int lineNumber = std::distance(
                fLineOffsets.begin(),
                std::upper_bound(fLineOffsets.begin(), fLineOffsets.end(), pos.startOffset()));

        fBuilder.trace_line(fTraceMask->stackID(), lineNumber);
    }
}

void Generator::pushTraceScopeMask() {
    if (this->shouldWriteTraceOps()) {
        // Take the intersection of the trace mask and the execution mask. To do this, start with an
        // all-zero mask, then use select to overwrite those zeros with the trace mask across all
        // executing lanes. We'll get the trace mask in executing lanes, and zero in dead lanes.
        fBuilder.push_constant_i(0);
        fTraceMask->pushClone(/*slots=*/1);
        fBuilder.select(/*slots=*/1);
    }
}

void Generator::discardTraceScopeMask() {
    if (this->shouldWriteTraceOps()) {
        this->discardExpression(/*slots=*/1);
    }
}

void Generator::emitTraceScope(int delta) {
    if (this->shouldWriteTraceOps()) {
        fBuilder.trace_scope(this->currentStack(), delta);
    }
}

void Generator::calculateLineOffsets() {
    SkASSERT(fLineOffsets.empty());
    fLineOffsets.push_back(0);
    for (size_t i = 0; i < fProgram.fSource->length(); ++i) {
        if ((*fProgram.fSource)[i] == '\n') {
            fLineOffsets.push_back(i);
        }
    }
    fLineOffsets.push_back(fProgram.fSource->length());
}

bool Generator::writeGlobals() {
    for (const ProgramElement* e : fProgram.elements()) {
        if (e->is<GlobalVarDeclaration>()) {
            const GlobalVarDeclaration& gvd = e->as<GlobalVarDeclaration>();
            const VarDeclaration& decl = gvd.varDeclaration();
            const Variable* var = decl.var();

            if (var->type().isEffectChild()) {
                // Associate each child effect variable with its numeric index.
                SkASSERT(!fChildEffectMap.find(var));
                int childEffectIndex = fChildEffectMap.count();
                fChildEffectMap[var] = childEffectIndex;
                continue;
            }

            // Opaque types include child processors and GL objects (samplers, textures, etc).
            // Of those, only child processors are legal variables.
            SkASSERT(!var->type().isVoid());
            SkASSERT(!var->type().isOpaque());

            // Builtin variables are system-defined, with special semantics.
            if (int builtin = var->layout().fBuiltin; builtin >= 0) {
                if (builtin == SK_FRAGCOORD_BUILTIN) {
                    fBuilder.store_device_xy01(this->getVariableSlots(*var));
                    continue;
                }
                // The only builtin variable exposed to runtime effects is sk_FragCoord.
                return unsupported();
            }

            if (IsUniform(*var)) {
                // Create the uniform slot map in first-to-last order.
                SlotRange uniformSlotRange = this->getUniformSlots(*var);

                if (this->shouldWriteTraceOps()) {
                    // We expect uniform values to show up in the debug trace. To make this happen
                    // without updating the file format, we synthesize a value-slot range for the
                    // uniform here, and copy the uniform data into the value slots. This allows
                    // trace_var to work naturally. This wastes a bit of memory, but debug traces
                    // don't need to be hyper-efficient.
                    SlotRange copyRange = fProgramSlots.getVariableSlots(*var);
                    fBuilder.push_uniform(uniformSlotRange);
                    this->popToSlotRangeUnmasked(copyRange);
                }

                continue;
            }

            // Other globals are treated as normal variable declarations.
            if (!this->writeVarDeclaration(decl)) {
                return unsupported();
            }
        }
    }

    return true;
}

bool Generator::writeStatement(const Statement& s) {
    switch (s.kind()) {
        case Statement::Kind::kBlock:
            // The debugger will stop on statements inside Blocks; there's no need for an additional
            // stop on the block's initial open-brace.
        case Statement::Kind::kFor:
            // The debugger will stop on the init-statement of a for statement, so we don't need to
            // stop on the outer for-statement itself as well.
            break;

        default:
            // The debugger should stop on other statements.
            this->emitTraceLine(s.fPosition);
            break;
    }

    switch (s.kind()) {
        case Statement::Kind::kBlock:
            return this->writeBlock(s.as<Block>());

        case Statement::Kind::kBreak:
            return this->writeBreakStatement(s.as<BreakStatement>());

        case Statement::Kind::kContinue:
            return this->writeContinueStatement(s.as<ContinueStatement>());

        case Statement::Kind::kDo:
            return this->writeDoStatement(s.as<DoStatement>());

        case Statement::Kind::kExpression:
            return this->writeExpressionStatement(s.as<ExpressionStatement>());

        case Statement::Kind::kFor:
            return this->writeForStatement(s.as<ForStatement>());

        case Statement::Kind::kIf:
            return this->writeIfStatement(s.as<IfStatement>());

        case Statement::Kind::kNop:
            return true;

        case Statement::Kind::kReturn:
            return this->writeReturnStatement(s.as<ReturnStatement>());

        case Statement::Kind::kSwitch:
            return this->writeSwitchStatement(s.as<SwitchStatement>());

        case Statement::Kind::kVarDeclaration:
            return this->writeVarDeclaration(s.as<VarDeclaration>());

        default:
            return unsupported();
    }
}

bool Generator::writeBlock(const Block& b) {
    if (b.blockKind() == Block::Kind::kCompoundStatement) {
        this->emitTraceLine(b.fPosition);
        ++fInsideCompoundStatement;
    } else {
        this->pushTraceScopeMask();
        this->emitTraceScope(+1);
    }

    for (const std::unique_ptr<Statement>& stmt : b.children()) {
        if (!this->writeStatement(*stmt)) {
            return unsupported();
        }
    }

    if (b.blockKind() == Block::Kind::kCompoundStatement) {
        --fInsideCompoundStatement;
    } else {
        this->emitTraceScope(-1);
        this->discardTraceScopeMask();
    }

    return true;
}

bool Generator::writeBreakStatement(const BreakStatement&) {
    // If all lanes have reached this break, we can just branch straight to the break target instead
    // of updating masks.
    fBuilder.branch_if_all_lanes_active(fCurrentBreakTarget);
    fBuilder.mask_off_loop_mask();
    return true;
}

bool Generator::writeContinueStatement(const ContinueStatement&) {
    fBuilder.continue_op(fCurrentContinueMask->stackID());
    return true;
}

bool Generator::writeDoStatement(const DoStatement& d) {
    // Set up a break target.
    AutoLoopTarget breakTarget(this, &fCurrentBreakTarget);

    // Save off the original loop mask.
    fBuilder.enableExecutionMaskWrites();
    fBuilder.push_loop_mask();

    // If `continue` is used in the loop...
    Analysis::LoopControlFlowInfo loopInfo = Analysis::GetLoopControlFlowInfo(*d.statement());
    AutoContinueMask autoContinueMask(this);
    if (loopInfo.fHasContinue) {
        // ... create a temporary slot for continue-mask storage.
        autoContinueMask.enable();
    }

    // Write the do-loop body.
    int labelID = fBuilder.nextLabelID();
    fBuilder.label(labelID);

    autoContinueMask.enterLoopBody();

    if (!this->writeStatement(*d.statement())) {
        return false;
    }

    autoContinueMask.exitLoopBody();

    // Point the debugger at the do-statement's test-expression before we run it.
    this->emitTraceLine(d.test()->fPosition);

    // Emit the test-expression, in order to combine it with the loop mask.
    if (!this->pushExpression(*d.test())) {
        return false;
    }

    // Mask off any lanes in the loop mask where the test-expression is false; this breaks the loop.
    // We don't use the test expression for anything else, so jettison it.
    fBuilder.merge_loop_mask();
    this->discardExpression(/*slots=*/1);

    // If any lanes are still running, go back to the top and run the loop body again.
    fBuilder.branch_if_any_lanes_active(labelID);

    // If we hit a break statement on all lanes, we will branch here to escape from the loop.
    fBuilder.label(breakTarget.labelID());

    // Restore the loop mask.
    fBuilder.pop_loop_mask();
    fBuilder.disableExecutionMaskWrites();

    return true;
}

bool Generator::writeMasklessForStatement(const ForStatement& f) {
    SkASSERT(f.unrollInfo());
    SkASSERT(f.unrollInfo()->fCount > 0);
    SkASSERT(f.initializer());
    SkASSERT(f.test());
    SkASSERT(f.next());

    // We want the loop index to disappear at the end of the loop, so wrap the for statement in a
    // trace scope.
    this->pushTraceScopeMask();
    this->emitTraceScope(+1);

    // If no lanes are active, skip over the loop entirely. This guards against looping forever;
    // with no lanes active, we wouldn't be able to write the loop variable back to its slot, so
    // we'd never make forward progress.
    int loopExitID = fBuilder.nextLabelID();
    int loopBodyID = fBuilder.nextLabelID();
    fBuilder.branch_if_no_lanes_active(loopExitID);

    // Run the loop initializer.
    if (!this->writeStatement(*f.initializer())) {
        return unsupported();
    }

    // Write the for-loop body. We know the for-loop has a standard ES2 unrollable structure, and
    // that it runs for at least one iteration, so we can plow straight ahead into the loop body
    // instead of running the loop-test first.
    fBuilder.label(loopBodyID);

    if (!this->writeStatement(*f.statement())) {
        return unsupported();
    }

    // Point the debugger at the for-statement's next-expression before we run it, or as close as we
    // can reasonably get.
    if (f.next()) {
        this->emitTraceLine(f.next()->fPosition);
    } else if (f.test()) {
        this->emitTraceLine(f.test()->fPosition);
    } else {
        this->emitTraceLine(f.fPosition);
    }

    // If the loop only runs for a single iteration, we are already done. If not...
    if (f.unrollInfo()->fCount > 1) {
        // ... run the next-expression, and immediately discard its result.
        if (!this->pushExpression(*f.next(), /*usesResult=*/false)) {
            return unsupported();
        }
        this->discardExpression(f.next()->type().slotCount());

        // Run the test-expression, and repeat the loop until the test-expression evaluates false.
        if (!this->pushExpression(*f.test())) {
            return unsupported();
        }
        fBuilder.branch_if_no_active_lanes_on_stack_top_equal(0, loopBodyID);

        // Jettison the test-expression.
        this->discardExpression(/*slots=*/1);
    }

    fBuilder.label(loopExitID);

    this->emitTraceScope(-1);
    this->discardTraceScopeMask();
    return true;
}

bool Generator::writeForStatement(const ForStatement& f) {
    // If we've determined that the loop does not run, omit its code entirely.
    if (f.unrollInfo() && f.unrollInfo()->fCount == 0) {
        return true;
    }

    // If the loop doesn't escape early due to a `continue`, `break` or `return`, and the loop
    // conforms to ES2 structure, we know that we will run the full number of iterations across all
    // lanes and don't need to use a loop mask.
    Analysis::LoopControlFlowInfo loopInfo = Analysis::GetLoopControlFlowInfo(*f.statement());
    if (!loopInfo.fHasContinue && !loopInfo.fHasBreak && !loopInfo.fHasReturn && f.unrollInfo()) {
        return this->writeMasklessForStatement(f);
    }

    // We want the loop index to disappear at the end of the loop, so wrap the for statement in a
    // trace scope.
    this->pushTraceScopeMask();
    this->emitTraceScope(+1);

    // Set up a break target.
    AutoLoopTarget breakTarget(this, &fCurrentBreakTarget);

    // Run the loop initializer.
    if (f.initializer()) {
        if (!this->writeStatement(*f.initializer())) {
            return unsupported();
        }
    } else {
        this->emitTraceLine(f.fPosition);
    }

    AutoContinueMask autoContinueMask(this);
    if (loopInfo.fHasContinue) {
        // Acquire a temporary slot for continue-mask storage.
        autoContinueMask.enable();
    }

    // Save off the original loop mask.
    fBuilder.enableExecutionMaskWrites();
    fBuilder.push_loop_mask();

    int loopTestID = fBuilder.nextLabelID();
    int loopBodyID = fBuilder.nextLabelID();

    // Jump down to the loop test so we can fall out of the loop immediately if it's zero-iteration.
    fBuilder.jump(loopTestID);

    // Write the for-loop body.
    fBuilder.label(loopBodyID);

    autoContinueMask.enterLoopBody();

    if (!this->writeStatement(*f.statement())) {
        return unsupported();
    }

    autoContinueMask.exitLoopBody();

    // Point the debugger at the for-statement's next-expression before we run it, or as close as we
    // can reasonably get.
    if (f.next()) {
        this->emitTraceLine(f.next()->fPosition);
    } else if (f.test()) {
        this->emitTraceLine(f.test()->fPosition);
    } else {
        this->emitTraceLine(f.fPosition);
    }

    // Run the next-expression. Immediately discard its result.
    if (f.next()) {
        if (!this->pushExpression(*f.next(), /*usesResult=*/false)) {
            return unsupported();
        }
        this->discardExpression(f.next()->type().slotCount());
    }

    fBuilder.label(loopTestID);
    if (f.test()) {
        // Emit the test-expression, in order to combine it with the loop mask.
        if (!this->pushExpression(*f.test())) {
            return unsupported();
        }
        // Mask off any lanes in the loop mask where the test-expression is false; this breaks the
        // loop. We don't use the test expression for anything else, so jettison it.
        fBuilder.merge_loop_mask();
        this->discardExpression(/*slots=*/1);
    }

    // If any lanes are still running, go back to the top and run the loop body again.
    fBuilder.branch_if_any_lanes_active(loopBodyID);

    // If we hit a break statement on all lanes, we will branch here to escape from the loop.
    fBuilder.label(breakTarget.labelID());

    // Restore the loop mask.
    fBuilder.pop_loop_mask();
    fBuilder.disableExecutionMaskWrites();

    this->emitTraceScope(-1);
    this->discardTraceScopeMask();
    return true;
}

bool Generator::writeExpressionStatement(const ExpressionStatement& e) {
    if (!this->pushExpression(*e.expression(), /*usesResult=*/false)) {
        return unsupported();
    }
    this->discardExpression(e.expression()->type().slotCount());
    return true;
}

bool Generator::writeDynamicallyUniformIfStatement(const IfStatement& i) {
    SkASSERT(Analysis::IsDynamicallyUniformExpression(*i.test()));

    int falseLabelID = fBuilder.nextLabelID();
    int exitLabelID = fBuilder.nextLabelID();

    if (!this->pushExpression(*i.test())) {
        return unsupported();
    }

    fBuilder.branch_if_no_active_lanes_on_stack_top_equal(~0, falseLabelID);

    if (!this->writeStatement(*i.ifTrue())) {
        return unsupported();
    }

    if (!i.ifFalse()) {
        // We don't have an if-false condition at all.
        fBuilder.label(falseLabelID);
    } else {
        // We do have an if-false condition. We've just completed the if-true block, so we need to
        // jump past the if-false block to avoid executing it.
        fBuilder.jump(exitLabelID);

        // The if-false block starts here.
        fBuilder.label(falseLabelID);

        if (!this->writeStatement(*i.ifFalse())) {
            return unsupported();
        }

        fBuilder.label(exitLabelID);
    }

    // Jettison the test-expression.
    this->discardExpression(/*slots=*/1);
    return true;
}

bool Generator::writeIfStatement(const IfStatement& i) {
    // If the test condition is known to be uniform, we can skip over the untrue portion entirely.
    if (Analysis::IsDynamicallyUniformExpression(*i.test())) {
        return this->writeDynamicallyUniformIfStatement(i);
    }

    // Save the current condition-mask.
    fBuilder.enableExecutionMaskWrites();
    fBuilder.push_condition_mask();

    // Push the test condition mask.
    if (!this->pushExpression(*i.test())) {
        return unsupported();
    }

    // Merge the current condition-mask with the test condition, then run the if-true branch.
    fBuilder.merge_condition_mask();
    if (!this->writeStatement(*i.ifTrue())) {
        return unsupported();
    }

    if (i.ifFalse()) {
        // Apply the inverse condition-mask. Then run the if-false branch.
        fBuilder.merge_inv_condition_mask();
        if (!this->writeStatement(*i.ifFalse())) {
            return unsupported();
        }
    }

    // Jettison the test-expression, and restore the the condition-mask.
    this->discardExpression(/*slots=*/1);
    fBuilder.pop_condition_mask();
    fBuilder.disableExecutionMaskWrites();

    return true;
}

bool Generator::writeReturnStatement(const ReturnStatement& r) {
    if (r.expression()) {
        if (!this->pushExpression(*r.expression())) {
            return unsupported();
        }
        if (this->needsFunctionResultSlots(fCurrentFunction)) {
            this->popToSlotRange(fCurrentFunctionResult);
        }
    }
    if (fBuilder.executionMaskWritesAreEnabled() && this->needsReturnMask(fCurrentFunction)) {
        fBuilder.mask_off_return_mask();
    }
    return true;
}

bool Generator::writeSwitchStatement(const SwitchStatement& s) {
    const StatementArray& cases = s.cases();
    SkASSERT(std::all_of(cases.begin(), cases.end(), [](const std::unique_ptr<Statement>& stmt) {
        return stmt->is<SwitchCase>();
    }));

    // Set up a break target.
    AutoLoopTarget breakTarget(this, &fCurrentBreakTarget);

    // Save off the original loop mask.
    fBuilder.enableExecutionMaskWrites();
    fBuilder.push_loop_mask();

    // Push the switch-case value, and write a default-mask that enables every lane which already
    // has an active loop mask. As we match cases, the default mask will get pared down.
    if (!this->pushExpression(*s.value())) {
        return unsupported();
    }
    fBuilder.push_loop_mask();

    // Zero out the loop mask; each case op will re-enable it as we go.
    fBuilder.mask_off_loop_mask();

    // Write each switch-case.
    bool foundDefaultCase = false;
    for (const std::unique_ptr<Statement>& stmt : cases) {
        int skipLabelID = fBuilder.nextLabelID();

        const SwitchCase& sc = stmt->as<SwitchCase>();
        if (sc.isDefault()) {
            foundDefaultCase = true;
            if (stmt.get() != cases.back().get()) {
                // We only support a default case when it is the very last case. If that changes,
                // this logic will need to be updated.
                return unsupported();
            }
            // Keep whatever lanes are executing now, and also enable any lanes in the default mask.
            fBuilder.pop_and_reenable_loop_mask();
            // Execute the switch-case block, if any lanes are alive to see it.
            fBuilder.branch_if_no_lanes_active(skipLabelID);
            if (!this->writeStatement(*sc.statement())) {
                return unsupported();
            }
        } else {
            // The case-op will enable the loop mask if the switch-value matches, and mask off lanes
            // from the default-mask.
            fBuilder.case_op(sc.value());
            // Execute the switch-case block, if any lanes are alive to see it.
            fBuilder.branch_if_no_lanes_active(skipLabelID);
            if (!this->writeStatement(*sc.statement())) {
                return unsupported();
            }
        }
        fBuilder.label(skipLabelID);
    }

    // Jettison the switch value, and the default case mask if it was never consumed above.
    this->discardExpression(/*slots=*/foundDefaultCase ? 1 : 2);

    // If we hit a break statement on all lanes, we will branch here to escape from the switch.
    fBuilder.label(breakTarget.labelID());

    // Restore the loop mask.
    fBuilder.pop_loop_mask();
    fBuilder.disableExecutionMaskWrites();
    return true;
}

bool Generator::writeImmutableVarDeclaration(const VarDeclaration& d) {
    // In a debugging session, we expect debug traces for a variable declaration to appear, even if
    // it's constant, so we don't use immutable slots for variables when tracing is on.
    if (this->shouldWriteTraceOps()) {
        return false;
    }

    // Find the constant value for this variable.
    const Expression* initialValue = ConstantFolder::GetConstantValueForVariable(*d.value());
    SkASSERT(initialValue);

    // For a variable to be immutable, it cannot be written-to besides its initial declaration.
    ProgramUsage::VariableCounts counts = fProgram.fUsage->get(*d.var());
    if (counts.fWrite != 1) {
        return false;
    }

    STArray<16, ImmutableBits> immutableValues;
    if (!this->getImmutableValueForExpression(*initialValue, &immutableValues)) {
        return false;
    }

    fImmutableVariables.add(d.var());

    std::optional<SlotRange> preexistingSlots = this->findPreexistingImmutableData(immutableValues);
    if (preexistingSlots.has_value()) {
        // Associate this variable with a preexisting range of immutable data (no new data or code).
        fImmutableSlots.mapVariableToSlots(*d.var(), *preexistingSlots);
    } else {
        // Write out the constant value back to immutable slots. (This generates data, but no
        // runtime code.)
        SlotRange slots = this->getImmutableSlots(*d.var());
        this->storeImmutableValueToSlots(immutableValues, slots);
    }

    return true;
}

bool Generator::writeVarDeclaration(const VarDeclaration& v) {
    if (v.value()) {
        // If a variable never actually changes, we can make it immutable.
        if (this->writeImmutableVarDeclaration(v)) {
            return true;
        }
        // This is a real variable which can change over the course of execution.
        if (!this->pushExpression(*v.value())) {
            return unsupported();
        }
        this->popToSlotRangeUnmasked(this->getVariableSlots(*v.var()));
    } else {
        this->zeroSlotRangeUnmasked(this->getVariableSlots(*v.var()));
    }
    return true;
}

bool Generator::pushExpression(const Expression& e, bool usesResult) {
    switch (e.kind()) {
        case Expression::Kind::kBinary:
            return this->pushBinaryExpression(e.as<BinaryExpression>());

        case Expression::Kind::kChildCall:
            return this->pushChildCall(e.as<ChildCall>());

        case Expression::Kind::kConstructorArray:
        case Expression::Kind::kConstructorArrayCast:
        case Expression::Kind::kConstructorCompound:
        case Expression::Kind::kConstructorStruct:
            return this->pushConstructorCompound(e.asAnyConstructor());

        case Expression::Kind::kConstructorCompoundCast:
        case Expression::Kind::kConstructorScalarCast:
            return this->pushConstructorCast(e.asAnyConstructor());

        case Expression::Kind::kConstructorDiagonalMatrix:
            return this->pushConstructorDiagonalMatrix(e.as<ConstructorDiagonalMatrix>());

        case Expression::Kind::kConstructorMatrixResize:
            return this->pushConstructorMatrixResize(e.as<ConstructorMatrixResize>());

        case Expression::Kind::kConstructorSplat:
            return this->pushConstructorSplat(e.as<ConstructorSplat>());

        case Expression::Kind::kEmpty:
            return true;

        case Expression::Kind::kFieldAccess:
            return this->pushFieldAccess(e.as<FieldAccess>());

        case Expression::Kind::kFunctionCall:
            return this->pushFunctionCall(e.as<FunctionCall>());

        case Expression::Kind::kIndex:
            return this->pushIndexExpression(e.as<IndexExpression>());

        case Expression::Kind::kLiteral:
            return this->pushLiteral(e.as<Literal>());

        case Expression::Kind::kPrefix:
            return this->pushPrefixExpression(e.as<PrefixExpression>());

        case Expression::Kind::kPostfix:
            return this->pushPostfixExpression(e.as<PostfixExpression>(), usesResult);

        case Expression::Kind::kSwizzle:
            return this->pushSwizzle(e.as<Swizzle>());

        case Expression::Kind::kTernary:
            return this->pushTernaryExpression(e.as<TernaryExpression>());

        case Expression::Kind::kVariableReference:
            return this->pushVariableReference(e.as<VariableReference>());

        default:
            return unsupported();
    }
}

BuilderOp Generator::GetTypedOp(const SkSL::Type& type, const TypedOps& ops) {
    switch (type.componentType().numberKind()) {
        case Type::NumberKind::kFloat:    return ops.fFloatOp;
        case Type::NumberKind::kSigned:   return ops.fSignedOp;
        case Type::NumberKind::kUnsigned: return ops.fUnsignedOp;
        case Type::NumberKind::kBoolean:  return ops.fBooleanOp;
        default:                          return BuilderOp::unsupported;
    }
}

bool Generator::unaryOp(const SkSL::Type& type, const TypedOps& ops) {
    BuilderOp op = GetTypedOp(type, ops);
    if (op == BuilderOp::unsupported) {
        return unsupported();
    }
    fBuilder.unary_op(op, type.slotCount());
    return true;
}

bool Generator::binaryOp(const SkSL::Type& type, const TypedOps& ops) {
    BuilderOp op = GetTypedOp(type, ops);
    if (op == BuilderOp::unsupported) {
        return unsupported();
    }
    fBuilder.binary_op(op, type.slotCount());
    return true;
}

bool Generator::ternaryOp(const SkSL::Type& type, const TypedOps& ops) {
    BuilderOp op = GetTypedOp(type, ops);
    if (op == BuilderOp::unsupported) {
        return unsupported();
    }
    fBuilder.ternary_op(op, type.slotCount());
    return true;
}

void Generator::foldWithMultiOp(BuilderOp op, int elements) {
    // Fold the top N elements on the stack using an op that supports multiple slots, e.g.:
    // (A + B + C + D) -> add_2_floats $0..1 += $2..3
    //                    add_float    $0    += $1
    for (; elements >= 8; elements -= 4) {
        fBuilder.binary_op(op, /*slots=*/4);
    }
    for (; elements >= 6; elements -= 3) {
        fBuilder.binary_op(op, /*slots=*/3);
    }
    for (; elements >= 4; elements -= 2) {
        fBuilder.binary_op(op, /*slots=*/2);
    }
    for (; elements >= 2; elements -= 1) {
        fBuilder.binary_op(op, /*slots=*/1);
    }
}

bool Generator::pushLValueOrExpression(LValue* lvalue, const Expression& expr) {
    return lvalue ? this->push(*lvalue)
                  : this->pushExpression(expr);
}

bool Generator::pushMatrixMultiply(LValue* lvalue,
                                   const Expression& left,
                                   const Expression& right,
                                   int leftColumns,
                                   int leftRows,
                                   int rightColumns,
                                   int rightRows) {
    SkASSERT(left.type().isMatrix() || left.type().isVector());
    SkASSERT(right.type().isMatrix() || right.type().isVector());

    // Insert padding space on the stack to hold the result.
    fBuilder.pad_stack(rightColumns * leftRows);

    // Push the left and right matrices onto the stack.
    if (!this->pushLValueOrExpression(lvalue, left) || !this->pushExpression(right)) {
        return unsupported();
    }

    fBuilder.matrix_multiply(leftColumns, leftRows, rightColumns, rightRows);

    // If this multiply was actually an assignment (via *=), write the result back to the lvalue.
    return lvalue ? this->store(*lvalue)
                  : true;
}

void Generator::foldComparisonOp(Operator op, int elements) {
    switch (op.kind()) {
        case OperatorKind::EQEQ:
            // equal(x,y) returns a vector; use & to fold into a scalar.
            this->foldWithMultiOp(BuilderOp::bitwise_and_n_ints, elements);
            break;

        case OperatorKind::NEQ:
            // notEqual(x,y) returns a vector; use | to fold into a scalar.
            this->foldWithMultiOp(BuilderOp::bitwise_or_n_ints, elements);
            break;

        default:
            SkDEBUGFAIL("comparison only allows == and !=");
            break;
    }
}

bool Generator::pushStructuredComparison(LValue* left,
                                         Operator op,
                                         LValue* right,
                                         const Type& type) {
    if (type.isStruct()) {
        // Compare every field in the struct.
        SkSpan<const Field> fields = type.fields();
        int currentSlot = 0;
        for (size_t index = 0; index < fields.size(); ++index) {
            const Type& fieldType = *fields[index].fType;
            const int   fieldSlotCount = fieldType.slotCount();
            UnownedLValueSlice fieldLeft {left,  currentSlot, fieldSlotCount};
            UnownedLValueSlice fieldRight{right, currentSlot, fieldSlotCount};
            if (!this->pushStructuredComparison(&fieldLeft, op, &fieldRight, fieldType)) {
                return unsupported();
            }
            currentSlot += fieldSlotCount;
        }

        this->foldComparisonOp(op, fields.size());
        return true;
    }

    if (type.isArray()) {
        const Type& indexedType = type.componentType();
        if (indexedType.numberKind() == Type::NumberKind::kNonnumeric) {
            // Compare every element in the array.
            const int indexedSlotCount = indexedType.slotCount();
            int       currentSlot = 0;
            for (int index = 0; index < type.columns(); ++index) {
                UnownedLValueSlice indexedLeft {left,  currentSlot, indexedSlotCount};
                UnownedLValueSlice indexedRight{right, currentSlot, indexedSlotCount};
                if (!this->pushStructuredComparison(&indexedLeft, op, &indexedRight, indexedType)) {
                    return unsupported();
                }
                currentSlot += indexedSlotCount;
            }

            this->foldComparisonOp(op, type.columns());
            return true;
        }
    }

    // We've winnowed down to a single element, or an array of homogeneous numeric elements.
    // Push the elements onto the stack, then compare them.
    if (!this->push(*left) || !this->push(*right)) {
        return unsupported();
    }
    switch (op.kind()) {
        case OperatorKind::EQEQ:
            if (!this->binaryOp(type, kEqualOps)) {
                return unsupported();
            }
            break;

        case OperatorKind::NEQ:
            if (!this->binaryOp(type, kNotEqualOps)) {
                return unsupported();
            }
            break;

        default:
            SkDEBUGFAIL("comparison only allows == and !=");
            break;
    }

    this->foldComparisonOp(op, type.slotCount());
    return true;
}

bool Generator::pushBinaryExpression(const BinaryExpression& e) {
    return this->pushBinaryExpression(*e.left(), e.getOperator(), *e.right());
}

bool Generator::pushBinaryExpression(const Expression& left, Operator op, const Expression& right) {
    switch (op.kind()) {
        // Rewrite greater-than ops as their less-than equivalents.
        case OperatorKind::GT:
            return this->pushBinaryExpression(right, OperatorKind::LT, left);

        case OperatorKind::GTEQ:
            return this->pushBinaryExpression(right, OperatorKind::LTEQ, left);

        // Handle struct and array comparisons.
        case OperatorKind::EQEQ:
        case OperatorKind::NEQ:
            if (left.type().isStruct() || left.type().isArray()) {
                SkASSERT(left.type().matches(right.type()));
                std::unique_ptr<LValue> lvLeft = this->makeLValue(left, /*allowScratch=*/true);
                std::unique_ptr<LValue> lvRight = this->makeLValue(right, /*allowScratch=*/true);
                return this->pushStructuredComparison(lvLeft.get(), op, lvRight.get(), left.type());
            }
            [[fallthrough]];

        // Rewrite commutative ops so that the literal is on the right-hand side. This gives the
        // Builder more opportunities to use immediate-mode ops.
        case OperatorKind::PLUS:
        case OperatorKind::STAR:
        case OperatorKind::BITWISEAND:
        case OperatorKind::BITWISEXOR:
        case OperatorKind::LOGICALXOR: {
            double unused;
            if (ConstantFolder::GetConstantValue(left, &unused) &&
                !ConstantFolder::GetConstantValue(right, &unused)) {
                return this->pushBinaryExpression(right, op, left);
            }
            break;
        }
        // Emit comma expressions.
        case OperatorKind::COMMA:
            if (Analysis::HasSideEffects(left)) {
                if (!this->pushExpression(left, /*usesResult=*/false)) {
                    return unsupported();
                }
                this->discardExpression(left.type().slotCount());
            }
            return this->pushExpression(right);

        default:
            break;
    }

    // Handle binary expressions with mismatched types.
    bool vectorizeLeft = false, vectorizeRight = false;
    if (!left.type().matches(right.type())) {
        if (left.type().componentType().numberKind() != right.type().componentType().numberKind()) {
            return unsupported();
        }
        if (left.type().isScalar() && (right.type().isVector() || right.type().isMatrix())) {
            vectorizeLeft = true;
        } else if ((left.type().isVector() || left.type().isMatrix()) && right.type().isScalar()) {
            vectorizeRight = true;
        }
    }

    const Type& type = vectorizeLeft ? right.type() : left.type();

    // If this is an assignment...
    std::unique_ptr<LValue> lvalue;
    if (op.isAssignment()) {
        // ... turn the left side into an lvalue.
        lvalue = this->makeLValue(left);
        if (!lvalue) {
            return unsupported();
        }

        // Handle simple assignment (`var = expr`).
        if (op.kind() == OperatorKind::EQ) {
            return this->pushExpression(right) &&
                   this->store(*lvalue);
        }

        // Strip off the assignment from the op (turning += into +).
        op = op.removeAssignment();
    }

    // Handle matrix multiplication (MxM/MxV/VxM).
    if (op.kind() == OperatorKind::STAR) {
        // Matrix * matrix:
        if (left.type().isMatrix() && right.type().isMatrix()) {
            return this->pushMatrixMultiply(lvalue.get(), left, right,
                                            left.type().columns(), left.type().rows(),
                                            right.type().columns(), right.type().rows());
        }

        // Vector * matrix:
        if (left.type().isVector() && right.type().isMatrix()) {
            return this->pushMatrixMultiply(lvalue.get(), left, right,
                                            left.type().columns(), 1,
                                            right.type().columns(), right.type().rows());
        }

        // Matrix * vector:
        if (left.type().isMatrix() && right.type().isVector()) {
            return this->pushMatrixMultiply(lvalue.get(), left, right,
                                            left.type().columns(), left.type().rows(),
                                            1, right.type().columns());
        }
    }

    if (!vectorizeLeft && !vectorizeRight && !type.matches(right.type())) {
        // We have mismatched types but don't know how to handle them.
        return unsupported();
    }

    // Handle binary ops which require short-circuiting.
    switch (op.kind()) {
        case OperatorKind::LOGICALAND:
            if (Analysis::HasSideEffects(right)) {
                // If the RHS has side effects, we rewrite `a && b` as `a ? b : false`. This
                // generates pretty solid code and gives us the required short-circuit behavior.
                SkASSERT(!op.isAssignment());
                SkASSERT(type.componentType().isBoolean());
                SkASSERT(type.slotCount() == 1);  // operator&& only works with scalar types
                Literal falseLiteral{Position{}, 0.0, &right.type()};
                return this->pushTernaryExpression(left, right, falseLiteral);
            }
            break;

        case OperatorKind::LOGICALOR:
            if (Analysis::HasSideEffects(right)) {
                // If the RHS has side effects, we rewrite `a || b` as `a ? true : b`.
                SkASSERT(!op.isAssignment());
                SkASSERT(type.componentType().isBoolean());
                SkASSERT(type.slotCount() == 1);  // operator|| only works with scalar types
                Literal trueLiteral{Position{}, 1.0, &right.type()};
                return this->pushTernaryExpression(left, trueLiteral, right);
            }
            break;

        default:
            break;
    }

    // Push the left- and right-expressions onto the stack.
    if (!this->pushLValueOrExpression(lvalue.get(), left)) {
        return unsupported();
    }
    if (vectorizeLeft) {
        fBuilder.push_duplicates(right.type().slotCount() - 1);
    }
    if (!this->pushExpression(right)) {
        return unsupported();
    }
    if (vectorizeRight) {
        fBuilder.push_duplicates(left.type().slotCount() - 1);
    }

    switch (op.kind()) {
        case OperatorKind::PLUS:
            if (!this->binaryOp(type, kAddOps)) {
                return unsupported();
            }
            break;

        case OperatorKind::MINUS:
            if (!this->binaryOp(type, kSubtractOps)) {
                return unsupported();
            }
            break;

        case OperatorKind::STAR:
            if (!this->binaryOp(type, kMultiplyOps)) {
                return unsupported();
            }
            break;

        case OperatorKind::SLASH:
            if (!this->binaryOp(type, kDivideOps)) {
                return unsupported();
            }
            break;

        case OperatorKind::LT:
        case OperatorKind::GT:
            if (!this->binaryOp(type, kLessThanOps)) {
                return unsupported();
            }
            SkASSERT(type.slotCount() == 1);  // operator< only works with scalar types
            break;

        case OperatorKind::LTEQ:
        case OperatorKind::GTEQ:
            if (!this->binaryOp(type, kLessThanEqualOps)) {
                return unsupported();
            }
            SkASSERT(type.slotCount() == 1);  // operator<= only works with scalar types
            break;

        case OperatorKind::EQEQ:
            if (!this->binaryOp(type, kEqualOps)) {
                return unsupported();
            }
            this->foldComparisonOp(op, type.slotCount());
            break;

        case OperatorKind::NEQ:
            if (!this->binaryOp(type, kNotEqualOps)) {
                return unsupported();
            }
            this->foldComparisonOp(op, type.slotCount());
            break;

        case OperatorKind::LOGICALAND:
        case OperatorKind::BITWISEAND:
            // For logical-and, we verified above that the RHS does not have side effects, so we
            // don't need to worry about short-circuiting side effects.
            fBuilder.binary_op(BuilderOp::bitwise_and_n_ints, type.slotCount());
            break;

        case OperatorKind::LOGICALOR:
        case OperatorKind::BITWISEOR:
            // For logical-or, we verified above that the RHS does not have side effects.
            fBuilder.binary_op(BuilderOp::bitwise_or_n_ints, type.slotCount());
            break;

        case OperatorKind::LOGICALXOR:
        case OperatorKind::BITWISEXOR:
            // Logical-xor does not short circuit.
            fBuilder.binary_op(BuilderOp::bitwise_xor_n_ints, type.slotCount());
            break;

        default:
            return unsupported();
    }

    // If we have an lvalue, we need to write the result back into it.
    return lvalue ? this->store(*lvalue)
                  : true;
}

std::optional<Generator::ImmutableBits> Generator::getImmutableBitsForSlot(const Expression& expr,
                                                                           size_t slot) {
    // Determine the constant-value of the slot; bail if it isn't constant.
    std::optional<double> v = expr.getConstantValue(slot);
    if (!v.has_value()) {
        return std::nullopt;
    }
    // Determine the number-kind of the slot, and convert the value to its bit-representation.
    Type::NumberKind kind = expr.type().slotType(slot).numberKind();
    double value = *v;
    switch (kind) {
        case Type::NumberKind::kFloat:
            return sk_bit_cast<ImmutableBits>((float)value);

        case Type::NumberKind::kSigned:
            return sk_bit_cast<ImmutableBits>((int32_t)value);

        case Type::NumberKind::kUnsigned:
            return sk_bit_cast<ImmutableBits>((uint32_t)value);

        case Type::NumberKind::kBoolean:
            return value ? ~0 : 0;

        default:
            return std::nullopt;
    }
}

bool Generator::getImmutableValueForExpression(const Expression& expr,
                                               TArray<ImmutableBits>* immutableValues) {
    if (!expr.supportsConstantValues()) {
        return false;
    }
    size_t numSlots = expr.type().slotCount();
    immutableValues->reserve_exact(numSlots);
    for (size_t index = 0; index < numSlots; ++index) {
        std::optional<ImmutableBits> bits = this->getImmutableBitsForSlot(expr, index);
        if (!bits.has_value()) {
            return false;
        }
        immutableValues->push_back(*bits);
    }
    return true;
}

void Generator::storeImmutableValueToSlots(const TArray<ImmutableBits>& immutableValues,
                                           SlotRange slots) {
    for (int index = 0; index < slots.count; ++index) {
        // Store the immutable value in its slot.
        const Slot slot = slots.index++;
        const ImmutableBits bits = immutableValues[index];
        fBuilder.store_immutable_value_i(slot, bits);

        // Keep track of every stored immutable value for potential later reuse.
        fImmutableSlotMap[bits].add(slot);
    }
}

std::optional<SlotRange> Generator::findPreexistingImmutableData(
        const TArray<ImmutableBits>& immutableValues) {
    STArray<16, const THashSet<Slot>*> slotArray;
    slotArray.reserve_exact(immutableValues.size());

    // Find all the slots associated with each immutable-value bit representation.
    // If a given bit-pattern doesn't exist anywhere in our program yet, we can stop searching.
    for (const ImmutableBits& immutableValue : immutableValues) {
        const THashSet<Slot>* slotsForValue = fImmutableSlotMap.find(immutableValue);
        if (!slotsForValue) {
            return std::nullopt;
        }
        slotArray.push_back(slotsForValue);
    }

    // Look for the group with the fewest number of entries, since that can be searched in the
    // least amount of effort.
    int leastSlotIndex = 0, leastSlotCount = INT_MAX;
    for (int index = 0; index < slotArray.size(); ++index) {
        int currentCount = slotArray[index]->count();
        if (currentCount < leastSlotCount) {
            leastSlotIndex = index;
            leastSlotCount = currentCount;
        }
    }

    // See if we can reconstitute the value that we want with any of the data we've already got.
    for (int slot : *slotArray[leastSlotIndex]) {
        int firstSlot = slot - leastSlotIndex;
        bool found = true;
        for (int index = 0; index < slotArray.size(); ++index) {
            if (!slotArray[index]->contains(firstSlot + index)) {
                found = false;
                break;
            }
        }
        if (found) {
            // We've found an exact match for the input value; return its slot-range.
            return SlotRange{firstSlot, slotArray.size()};
        }
    }

    // We didn't find any reusable slot ranges.
    return std::nullopt;
}

bool Generator::pushImmutableData(const Expression& e) {
    STArray<16, ImmutableBits> immutableValues;
    if (!this->getImmutableValueForExpression(e, &immutableValues)) {
        return false;
    }
    std::optional<SlotRange> preexistingData = this->findPreexistingImmutableData(immutableValues);
    if (preexistingData.has_value()) {
        fBuilder.push_immutable(*preexistingData);
        return true;
    }
    SlotRange range = fImmutableSlots.createSlots(e.description(),
                                                  e.type(),
                                                  e.fPosition,
                                                  /*isFunctionReturnValue=*/false);
    this->storeImmutableValueToSlots(immutableValues, range);
    fBuilder.push_immutable(range);
    return true;
}

bool Generator::pushConstructorCompound(const AnyConstructor& c) {
    if (c.type().slotCount() > 1 && this->pushImmutableData(c)) {
        return true;
    }
    for (const std::unique_ptr<Expression> &arg : c.argumentSpan()) {
        if (!this->pushExpression(*arg)) {
            return unsupported();
        }
    }
    return true;
}

bool Generator::pushChildCall(const ChildCall& c) {
    int* childIdx = fChildEffectMap.find(&c.child());
    SkASSERT(childIdx != nullptr);
    SkASSERT(!c.arguments().empty());

    // All child calls have at least one argument.
    const Expression* arg = c.arguments()[0].get();
    if (!this->pushExpression(*arg)) {
        return unsupported();
    }

    // Copy arguments from the stack into src/dst as required by this particular child-call.
    switch (c.child().type().typeKind()) {
        case Type::TypeKind::kShader: {
            // The argument must be a float2.
            SkASSERT(c.arguments().size() == 1);
            SkASSERT(arg->type().matches(*fContext.fTypes.fFloat2));

            // `exchange_src` will use the top four values on the stack, but we don't care what goes
            // into the blue/alpha components. We inject padding here to balance the stack.
            fBuilder.pad_stack(2);

            // Move the argument into src.rgba while also preserving the execution mask.
            fBuilder.exchange_src();
            fBuilder.invoke_shader(*childIdx);
            break;
        }
        case Type::TypeKind::kColorFilter: {
            // The argument must be a half4/float4.
            SkASSERT(c.arguments().size() == 1);
            SkASSERT(arg->type().matches(*fContext.fTypes.fHalf4) ||
                     arg->type().matches(*fContext.fTypes.fFloat4));

            // Move the argument into src.rgba while also preserving the execution mask.
            fBuilder.exchange_src();
            fBuilder.invoke_color_filter(*childIdx);
            break;
        }
        case Type::TypeKind::kBlender: {
            // Both arguments must be half4/float4.
            SkASSERT(c.arguments().size() == 2);
            SkASSERT(c.arguments()[0]->type().matches(*fContext.fTypes.fHalf4) ||
                     c.arguments()[0]->type().matches(*fContext.fTypes.fFloat4));
            SkASSERT(c.arguments()[1]->type().matches(*fContext.fTypes.fHalf4) ||
                     c.arguments()[1]->type().matches(*fContext.fTypes.fFloat4));

            // Move the second argument into dst.rgba, and the first argument into src.rgba, while
            // simultaneously preserving the execution mask.
            if (!this->pushExpression(*c.arguments()[1])) {
                return unsupported();
            }
            fBuilder.pop_dst_rgba();
            fBuilder.exchange_src();
            fBuilder.invoke_blender(*childIdx);
            break;
        }
        default: {
            SkDEBUGFAILF("cannot sample from type '%s'", c.child().type().description().c_str());
        }
    }

    // The child call has returned the result color via src.rgba, and the SkRP execution mask is
    // on top of the stack. Swapping the two puts the result color on top of the stack, and also
    // restores our execution masks.
    fBuilder.exchange_src();
    return true;
}

bool Generator::pushConstructorCast(const AnyConstructor& c) {
    SkASSERT(c.argumentSpan().size() == 1);
    const Expression& inner = *c.argumentSpan().front();
    SkASSERT(inner.type().slotCount() == c.type().slotCount());

    if (!this->pushExpression(inner)) {
        return unsupported();
    }
    const Type::NumberKind innerKind = inner.type().componentType().numberKind();
    const Type::NumberKind outerKind = c.type().componentType().numberKind();

    if (innerKind == outerKind) {
        // Since we ignore type precision, this cast is effectively a no-op.
        return true;
    }

    switch (innerKind) {
        case Type::NumberKind::kSigned:
            if (outerKind == Type::NumberKind::kUnsigned) {
                // Treat uint(int) as a no-op.
                return true;
            }
            if (outerKind == Type::NumberKind::kFloat) {
                fBuilder.unary_op(BuilderOp::cast_to_float_from_int, c.type().slotCount());
                return true;
            }
            break;

        case Type::NumberKind::kUnsigned:
            if (outerKind == Type::NumberKind::kSigned) {
                // Treat int(uint) as a no-op.
                return true;
            }
            if (outerKind == Type::NumberKind::kFloat) {
                fBuilder.unary_op(BuilderOp::cast_to_float_from_uint, c.type().slotCount());
                return true;
            }
            break;

        case Type::NumberKind::kBoolean:
            // Converting boolean to int or float can be accomplished via bitwise-and.
            if (outerKind == Type::NumberKind::kFloat) {
                fBuilder.push_constant_f(1.0f);
            } else if (outerKind == Type::NumberKind::kSigned ||
                       outerKind == Type::NumberKind::kUnsigned) {
                fBuilder.push_constant_i(1);
            } else {
                SkDEBUGFAILF("unexpected cast from bool to %s", c.type().description().c_str());
                return unsupported();
            }
            fBuilder.push_duplicates(c.type().slotCount() - 1);
            fBuilder.binary_op(BuilderOp::bitwise_and_n_ints, c.type().slotCount());
            return true;

        case Type::NumberKind::kFloat:
            if (outerKind == Type::NumberKind::kSigned) {
                fBuilder.unary_op(BuilderOp::cast_to_int_from_float, c.type().slotCount());
                return true;
            }
            if (outerKind == Type::NumberKind::kUnsigned) {
                fBuilder.unary_op(BuilderOp::cast_to_uint_from_float, c.type().slotCount());
                return true;
            }
            break;

        case Type::NumberKind::kNonnumeric:
            break;
    }

    if (outerKind == Type::NumberKind::kBoolean) {
        // Converting int or float to boolean can be accomplished via `notEqual(x, 0)`.
        fBuilder.push_zeros(c.type().slotCount());
        return this->binaryOp(inner.type(), kNotEqualOps);
    }

    SkDEBUGFAILF("unexpected cast from %s to %s",
                 c.type().description().c_str(), inner.type().description().c_str());
    return unsupported();
}

bool Generator::pushConstructorDiagonalMatrix(const ConstructorDiagonalMatrix& c) {
    if (this->pushImmutableData(c)) {
        return true;
    }
    fBuilder.push_zeros(1);
    if (!this->pushExpression(*c.argument())) {
        return unsupported();
    }
    fBuilder.diagonal_matrix(c.type().columns(), c.type().rows());

    return true;
}

bool Generator::pushConstructorMatrixResize(const ConstructorMatrixResize& c) {
    if (!this->pushExpression(*c.argument())) {
        return unsupported();
    }
    fBuilder.matrix_resize(c.argument()->type().columns(),
                           c.argument()->type().rows(),
                           c.type().columns(),
                           c.type().rows());
    return true;
}

bool Generator::pushConstructorSplat(const ConstructorSplat& c) {
    if (!this->pushExpression(*c.argument())) {
        return unsupported();
    }
    fBuilder.push_duplicates(c.type().slotCount() - 1);
    return true;
}

bool Generator::pushFieldAccess(const FieldAccess& f) {
    // If possible, get direct field access via the lvalue.
    std::unique_ptr<LValue> lvalue = this->makeLValue(f, /*allowScratch=*/true);
    return lvalue && this->push(*lvalue);
}

bool Generator::pushFunctionCall(const FunctionCall& c) {
    if (c.function().isIntrinsic()) {
        return this->pushIntrinsic(c);
    }

    // Keep track of the current function.
    const FunctionDefinition* lastFunction = fCurrentFunction;
    fCurrentFunction = c.function().definition();

    // Skip over the function body entirely if there are no active lanes.
    // (If the function call was trivial, it would likely have been inlined in the frontend, so we
    // assume here that function calls generally represent a significant amount of work.)
    int skipLabelID = fBuilder.nextLabelID();
    fBuilder.branch_if_no_lanes_active(skipLabelID);

    // Emit the function body.
    std::optional<SlotRange> r = this->writeFunction(c, *fCurrentFunction, c.arguments());
    if (!r.has_value()) {
        return unsupported();
    }

    // If the function uses result slots, move its result from slots onto the stack.
    if (this->needsFunctionResultSlots(fCurrentFunction)) {
        fBuilder.push_slots(*r);
    }

    // We've returned back to the last function.
    fCurrentFunction = lastFunction;

    // Copy the function result from its slots onto the stack.
    fBuilder.label(skipLabelID);
    return true;
}

bool Generator::pushIndexExpression(const IndexExpression& i) {
    std::unique_ptr<LValue> lvalue = this->makeLValue(i, /*allowScratch=*/true);
    return lvalue && this->push(*lvalue);
}

bool Generator::pushIntrinsic(const FunctionCall& c) {
    const ExpressionArray& args = c.arguments();
    switch (args.size()) {
        case 1:
            return this->pushIntrinsic(c.function().intrinsicKind(), *args[0]);

        case 2:
            return this->pushIntrinsic(c.function().intrinsicKind(), *args[0], *args[1]);

        case 3:
            return this->pushIntrinsic(c.function().intrinsicKind(), *args[0], *args[1], *args[2]);

        default:
            break;
    }

    return unsupported();
}

bool Generator::pushLengthIntrinsic(int slotCount) {
    if (slotCount == 1) {
        // `length(scalar)` is `sqrt(x^2)`, which is equivalent to `abs(x)`.
        return this->pushAbsFloatIntrinsic(/*slots=*/1);
    }
    // Implement `length(vec)` as `sqrt(dot(x, x))`.
    fBuilder.push_clone(slotCount);
    fBuilder.dot_floats(slotCount);
    fBuilder.unary_op(BuilderOp::sqrt_float, 1);
    return true;
}

bool Generator::pushAbsFloatIntrinsic(int slots) {
    // Perform abs(float) by masking off the sign bit.
    fBuilder.push_constant_u(0x7FFFFFFF, slots);
    fBuilder.binary_op(BuilderOp::bitwise_and_n_ints, slots);
    return true;
}

bool Generator::pushVectorizedExpression(const Expression& expr, const Type& vectorType) {
    if (!this->pushExpression(expr)) {
        return unsupported();
    }
    if (vectorType.slotCount() > expr.type().slotCount()) {
        SkASSERT(expr.type().slotCount() == 1);
        fBuilder.push_duplicates(vectorType.slotCount() - expr.type().slotCount());
    }
    return true;
}

bool Generator::pushIntrinsic(const TypedOps& ops, const Expression& arg0) {
    if (!this->pushExpression(arg0)) {
        return unsupported();
    }
    return this->unaryOp(arg0.type(), ops);
}

bool Generator::pushIntrinsic(BuilderOp builderOp, const Expression& arg0) {
    if (!this->pushExpression(arg0)) {
        return unsupported();
    }
    fBuilder.unary_op(builderOp, arg0.type().slotCount());
    return true;
}

bool Generator::pushIntrinsic(IntrinsicKind intrinsic, const Expression& arg0) {
    switch (intrinsic) {
        case IntrinsicKind::k_abs_IntrinsicKind:
            if (arg0.type().componentType().isFloat()) {
                // Perform abs(float) by masking off the sign bit.
                if (!this->pushExpression(arg0)) {
                    return unsupported();
                }
                return this->pushAbsFloatIntrinsic(arg0.type().slotCount());
            }
            // We have a dedicated op for abs(int).
            return this->pushIntrinsic(BuilderOp::abs_int, arg0);

        case IntrinsicKind::k_any_IntrinsicKind:
            if (!this->pushExpression(arg0)) {
                return unsupported();
            }
            this->foldWithMultiOp(BuilderOp::bitwise_or_n_ints, arg0.type().slotCount());
            return true;

        case IntrinsicKind::k_all_IntrinsicKind:
            if (!this->pushExpression(arg0)) {
                return unsupported();
            }
            this->foldWithMultiOp(BuilderOp::bitwise_and_n_ints, arg0.type().slotCount());
            return true;

        case IntrinsicKind::k_acos_IntrinsicKind:
            return this->pushIntrinsic(BuilderOp::acos_float, arg0);

        case IntrinsicKind::k_asin_IntrinsicKind:
            return this->pushIntrinsic(BuilderOp::asin_float, arg0);

        case IntrinsicKind::k_atan_IntrinsicKind:
            return this->pushIntrinsic(BuilderOp::atan_float, arg0);

        case IntrinsicKind::k_ceil_IntrinsicKind:
            return this->pushIntrinsic(BuilderOp::ceil_float, arg0);

        case IntrinsicKind::k_cos_IntrinsicKind:
            return this->pushIntrinsic(BuilderOp::cos_float, arg0);

        case IntrinsicKind::k_degrees_IntrinsicKind: {
            Literal lit180OverPi{Position{}, 57.2957795131f, &arg0.type().componentType()};
            return this->pushBinaryExpression(arg0, OperatorKind::STAR, lit180OverPi);
        }
        case IntrinsicKind::k_floatBitsToInt_IntrinsicKind:
        case IntrinsicKind::k_floatBitsToUint_IntrinsicKind:
        case IntrinsicKind::k_intBitsToFloat_IntrinsicKind:
        case IntrinsicKind::k_uintBitsToFloat_IntrinsicKind:
            return this->pushExpression(arg0);

        case IntrinsicKind::k_exp_IntrinsicKind:
            return this->pushIntrinsic(BuilderOp::exp_float, arg0);

        case IntrinsicKind::k_exp2_IntrinsicKind:
            return this->pushIntrinsic(BuilderOp::exp2_float, arg0);

        case IntrinsicKind::k_floor_IntrinsicKind:
            return this->pushIntrinsic(BuilderOp::floor_float, arg0);

        case IntrinsicKind::k_fract_IntrinsicKind:
            // Implement fract as `x - floor(x)`.
            if (!this->pushExpression(arg0)) {
                return unsupported();
            }
            fBuilder.push_clone(arg0.type().slotCount());
            fBuilder.unary_op(BuilderOp::floor_float, arg0.type().slotCount());
            return this->binaryOp(arg0.type(), kSubtractOps);

        case IntrinsicKind::k_inverse_IntrinsicKind:
            SkASSERT(arg0.type().isMatrix());
            SkASSERT(arg0.type().rows() == arg0.type().columns());
            if (!this->pushExpression(arg0)) {
                return unsupported();
            }
            fBuilder.inverse_matrix(arg0.type().rows());
            return true;

        case IntrinsicKind::k_inversesqrt_IntrinsicKind:
            return this->pushIntrinsic(kInverseSqrtOps, arg0);

        case IntrinsicKind::k_length_IntrinsicKind:
            return this->pushExpression(arg0) &&
                   this->pushLengthIntrinsic(arg0.type().slotCount());

        case IntrinsicKind::k_log_IntrinsicKind:
            if (!this->pushExpression(arg0)) {
                return unsupported();
            }
            fBuilder.unary_op(BuilderOp::log_float, arg0.type().slotCount());
            return true;

        case IntrinsicKind::k_log2_IntrinsicKind:
            if (!this->pushExpression(arg0)) {
                return unsupported();
            }
            fBuilder.unary_op(BuilderOp::log2_float, arg0.type().slotCount());
            return true;

        case IntrinsicKind::k_normalize_IntrinsicKind: {
            // Implement normalize as `x / length(x)`. First, push the expression.
            if (!this->pushExpression(arg0)) {
                return unsupported();
            }
            int slotCount = arg0.type().slotCount();
            if (slotCount > 1) {
#if defined(SK_USE_RSQRT_IN_RP_NORMALIZE)
                // Instead of `x / sqrt(dot(x, x))`, we can get roughly the same result in less time
                // by computing `x * invsqrt(dot(x, x))`.
                fBuilder.push_clone(slotCount);
                fBuilder.push_clone(slotCount);
                fBuilder.dot_floats(slotCount);

                // Compute `vec(inversesqrt(dot(x, x)))`.
                fBuilder.unary_op(BuilderOp::invsqrt_float, 1);
                fBuilder.push_duplicates(slotCount - 1);

                // Return `x * vec(inversesqrt(dot(x, x)))`.
                return this->binaryOp(arg0.type(), kMultiplyOps);
#else
                // TODO: We can get roughly the same result in less time by using `invsqrt`, but
                // that leads to more variance across architectures, which Chromium layout tests do
                // not handle nicely.
                fBuilder.push_clone(slotCount);
                fBuilder.push_clone(slotCount);
                fBuilder.dot_floats(slotCount);

                // Compute `vec(sqrt(dot(x, x)))`.
                fBuilder.unary_op(BuilderOp::sqrt_float, 1);
                fBuilder.push_duplicates(slotCount - 1);

                // Return `x / vec(sqrt(dot(x, x)))`.
                return this->binaryOp(arg0.type(), kDivideOps);
#endif
            } else {
                // For single-slot normalization, we can simplify `sqrt(x * x)` into `abs(x)`.
                fBuilder.push_clone(slotCount);
                return this->pushAbsFloatIntrinsic(/*slots=*/1) &&
                       this->binaryOp(arg0.type(), kDivideOps);
            }
        }
        case IntrinsicKind::k_not_IntrinsicKind:
            return this->pushPrefixExpression(OperatorKind::LOGICALNOT, arg0);

        case IntrinsicKind::k_radians_IntrinsicKind: {
            Literal litPiOver180{Position{}, 0.01745329251f, &arg0.type().componentType()};
            return this->pushBinaryExpression(arg0, OperatorKind::STAR, litPiOver180);
        }
        case IntrinsicKind::k_saturate_IntrinsicKind: {
            // Implement saturate as clamp(arg, 0, 1).
            Literal zeroLiteral{Position{}, 0.0, &arg0.type().componentType()};
            Literal oneLiteral{Position{}, 1.0, &arg0.type().componentType()};
            return this->pushIntrinsic(k_clamp_IntrinsicKind, arg0, zeroLiteral, oneLiteral);
        }
        case IntrinsicKind::k_sign_IntrinsicKind: {
            // Implement floating-point sign() as `clamp(arg * FLT_MAX, -1, 1)`.
            // FLT_MIN * FLT_MAX evaluates to 4, so multiplying any float value against FLT_MAX is
            // sufficient to ensure that |value| is always 1 or greater (excluding zero and nan).
            // Integer sign() doesn't need to worry about fractional values or nans, and can simply
            // be `clamp(arg, -1, 1)`.
            if (!this->pushExpression(arg0)) {
                return unsupported();
            }
            if (arg0.type().componentType().isFloat()) {
                Literal fltMaxLiteral{Position{}, FLT_MAX, &arg0.type().componentType()};
                if (!this->pushVectorizedExpression(fltMaxLiteral, arg0.type())) {
                    return unsupported();
                }
                if (!this->binaryOp(arg0.type(), kMultiplyOps)) {
                    return unsupported();
                }
            }
            Literal neg1Literal{Position{}, -1.0, &arg0.type().componentType()};
            if (!this->pushVectorizedExpression(neg1Literal, arg0.type())) {
                return unsupported();
            }
            if (!this->binaryOp(arg0.type(), kMaxOps)) {
                return unsupported();
            }
            Literal pos1Literal{Position{}, 1.0, &arg0.type().componentType()};
            if (!this->pushVectorizedExpression(pos1Literal, arg0.type())) {
                return unsupported();
            }
            return this->binaryOp(arg0.type(), kMinOps);
        }
        case IntrinsicKind::k_sin_IntrinsicKind:
            return this->pushIntrinsic(BuilderOp::sin_float, arg0);

        case IntrinsicKind::k_sqrt_IntrinsicKind:
            return this->pushIntrinsic(BuilderOp::sqrt_float, arg0);

        case IntrinsicKind::k_tan_IntrinsicKind:
            return this->pushIntrinsic(BuilderOp::tan_float, arg0);

        case IntrinsicKind::k_transpose_IntrinsicKind:
            SkASSERT(arg0.type().isMatrix());
            if (!this->pushExpression(arg0)) {
                return unsupported();
            }
            fBuilder.transpose(arg0.type().columns(), arg0.type().rows());
            return true;

        case IntrinsicKind::k_trunc_IntrinsicKind:
            // Implement trunc as `float(int(x))`, since float-to-int rounds toward zero.
            if (!this->pushExpression(arg0)) {
                return unsupported();
            }
            fBuilder.unary_op(BuilderOp::cast_to_int_from_float, arg0.type().slotCount());
            fBuilder.unary_op(BuilderOp::cast_to_float_from_int, arg0.type().slotCount());
            return true;

        case IntrinsicKind::k_fromLinearSrgb_IntrinsicKind:
        case IntrinsicKind::k_toLinearSrgb_IntrinsicKind:
            // The argument must be a half3.
            SkASSERT(arg0.type().matches(*fContext.fTypes.fHalf3));
            if (!this->pushExpression(arg0)) {
                return unsupported();
            }

            if (intrinsic == IntrinsicKind::k_fromLinearSrgb_IntrinsicKind) {
                fBuilder.invoke_from_linear_srgb();
            } else {
                fBuilder.invoke_to_linear_srgb();
            }
            return true;

        default:
            break;
    }
    return unsupported();
}

bool Generator::pushIntrinsic(const TypedOps& ops, const Expression& arg0, const Expression& arg1) {
    if (!this->pushExpression(arg0) || !this->pushVectorizedExpression(arg1, arg0.type())) {
        return unsupported();
    }
    return this->binaryOp(arg0.type(), ops);
}

bool Generator::pushIntrinsic(BuilderOp builderOp, const Expression& arg0, const Expression& arg1) {
    if (!this->pushExpression(arg0) || !this->pushVectorizedExpression(arg1, arg0.type())) {
        return unsupported();
    }
    fBuilder.binary_op(builderOp, arg0.type().slotCount());
    return true;
}

bool Generator::pushIntrinsic(IntrinsicKind intrinsic,
                              const Expression& arg0,
                              const Expression& arg1) {
    switch (intrinsic) {
        case IntrinsicKind::k_atan_IntrinsicKind:
            return this->pushIntrinsic(BuilderOp::atan2_n_floats, arg0, arg1);

        case IntrinsicKind::k_cross_IntrinsicKind: {
            // Implement cross as `arg0.yzx * arg1.zxy - arg0.zxy * arg1.yzx`. We use two stacks so
            // that each subexpression can be multiplied separately.
            SkASSERT(arg0.type().matches(arg1.type()));
            SkASSERT(arg0.type().slotCount() == 3);
            SkASSERT(arg1.type().slotCount() == 3);

            // Push `arg0.yzx` onto this stack and `arg0.zxy` onto a separate subexpression stack.
            AutoStack subexpressionStack(this);
            subexpressionStack.enter();
            if (!this->pushExpression(arg0)) {
                return unsupported();
            }
            subexpressionStack.exit();
            subexpressionStack.pushClone(/*slots=*/3);

            fBuilder.swizzle(/*consumedSlots=*/3, {{1, 2, 0}});
            subexpressionStack.enter();
            fBuilder.swizzle(/*consumedSlots=*/3, {{2, 0, 1}});
            subexpressionStack.exit();

            // Push `arg1.zxy` onto this stack and `arg1.yzx` onto the next stack. Perform the
            // multiply on each subexpression (`arg0.yzx * arg1.zxy` on the first stack, and
            // `arg0.zxy * arg1.yzx` on the next).
            subexpressionStack.enter();
            if (!this->pushExpression(arg1)) {
                return unsupported();
            }
            subexpressionStack.exit();
            subexpressionStack.pushClone(/*slots=*/3);

            fBuilder.swizzle(/*consumedSlots=*/3, {{2, 0, 1}});
            fBuilder.binary_op(BuilderOp::mul_n_floats, 3);

            subexpressionStack.enter();
            fBuilder.swizzle(/*consumedSlots=*/3, {{1, 2, 0}});
            fBuilder.binary_op(BuilderOp::mul_n_floats, 3);
            subexpressionStack.exit();

            // Migrate the result of the second subexpression (`arg0.zxy * arg1.yzx`) back onto the
            // main stack and subtract it from the first subexpression (`arg0.yzx * arg1.zxy`).
            subexpressionStack.pushClone(/*slots=*/3);
            fBuilder.binary_op(BuilderOp::sub_n_floats, 3);

            // Now that the calculation is complete, discard the subexpression on the next stack.
            subexpressionStack.enter();
            this->discardExpression(/*slots=*/3);
            subexpressionStack.exit();
            return true;
        }
        case IntrinsicKind::k_distance_IntrinsicKind:
            // Implement distance as `length(a - b)`.
            SkASSERT(arg0.type().slotCount() == arg1.type().slotCount());
            return this->pushBinaryExpression(arg0, OperatorKind::MINUS, arg1) &&
                   this->pushLengthIntrinsic(arg0.type().slotCount());

        case IntrinsicKind::k_dot_IntrinsicKind:
            SkASSERT(arg0.type().matches(arg1.type()));
            if (!this->pushExpression(arg0) || !this->pushExpression(arg1)) {
                return unsupported();
            }
            fBuilder.dot_floats(arg0.type().slotCount());
            return true;

        case IntrinsicKind::k_equal_IntrinsicKind:
            SkASSERT(arg0.type().matches(arg1.type()));
            return this->pushIntrinsic(kEqualOps, arg0, arg1);

        case IntrinsicKind::k_notEqual_IntrinsicKind:
            SkASSERT(arg0.type().matches(arg1.type()));
            return this->pushIntrinsic(kNotEqualOps, arg0, arg1);

        case IntrinsicKind::k_lessThan_IntrinsicKind:
            SkASSERT(arg0.type().matches(arg1.type()));
            return this->pushIntrinsic(kLessThanOps, arg0, arg1);

        case IntrinsicKind::k_greaterThan_IntrinsicKind:
            SkASSERT(arg0.type().matches(arg1.type()));
            return this->pushIntrinsic(kLessThanOps, arg1, arg0);

        case IntrinsicKind::k_lessThanEqual_IntrinsicKind:
            SkASSERT(arg0.type().matches(arg1.type()));
            return this->pushIntrinsic(kLessThanEqualOps, arg0, arg1);

        case IntrinsicKind::k_greaterThanEqual_IntrinsicKind:
            SkASSERT(arg0.type().matches(arg1.type()));
            return this->pushIntrinsic(kLessThanEqualOps, arg1, arg0);

        case IntrinsicKind::k_min_IntrinsicKind:
            SkASSERT(arg0.type().componentType().matches(arg1.type().componentType()));
            return this->pushIntrinsic(kMinOps, arg0, arg1);

        case IntrinsicKind::k_matrixCompMult_IntrinsicKind:
            SkASSERT(arg0.type().matches(arg1.type()));
            return this->pushIntrinsic(kMultiplyOps, arg0, arg1);

        case IntrinsicKind::k_max_IntrinsicKind:
            SkASSERT(arg0.type().componentType().matches(arg1.type().componentType()));
            return this->pushIntrinsic(kMaxOps, arg0, arg1);

        case IntrinsicKind::k_mod_IntrinsicKind:
            SkASSERT(arg0.type().componentType().matches(arg1.type().componentType()));
            return this->pushIntrinsic(kModOps, arg0, arg1);

        case IntrinsicKind::k_pow_IntrinsicKind:
            SkASSERT(arg0.type().matches(arg1.type()));
            return this->pushIntrinsic(BuilderOp::pow_n_floats, arg0, arg1);

        case IntrinsicKind::k_reflect_IntrinsicKind: {
            // Implement reflect as `I - (N * dot(I,N) * 2)`.
            SkASSERT(arg0.type().matches(arg1.type()));
            SkASSERT(arg0.type().slotCount() == arg1.type().slotCount());
            SkASSERT(arg0.type().componentType().isFloat());
            int slotCount = arg0.type().slotCount();

            // Stack: I, N.
            if (!this->pushExpression(arg0) || !this->pushExpression(arg1)) {
                return unsupported();
            }
            // Stack: I, N, I, N.
            fBuilder.push_clone(2 * slotCount);
            // Stack: I, N, dot(I,N)
            fBuilder.dot_floats(slotCount);
            // Stack: I, N, dot(I,N), 2
            fBuilder.push_constant_f(2.0);
            // Stack: I, N, dot(I,N) * 2
            fBuilder.binary_op(BuilderOp::mul_n_floats, 1);
            // Stack: I, N * dot(I,N) * 2
            fBuilder.push_duplicates(slotCount - 1);
            fBuilder.binary_op(BuilderOp::mul_n_floats, slotCount);
            // Stack: I - (N * dot(I,N) * 2)
            fBuilder.binary_op(BuilderOp::sub_n_floats, slotCount);
            return true;
        }
        case IntrinsicKind::k_step_IntrinsicKind: {
            // Compute step as `float(lessThanEqual(edge, x))`. We convert from boolean 0/~0 to
            // floating point zero/one by using a bitwise-and against the bit-pattern of 1.0.
            SkASSERT(arg0.type().componentType().matches(arg1.type().componentType()));
            if (!this->pushVectorizedExpression(arg0, arg1.type()) || !this->pushExpression(arg1)) {
                return unsupported();
            }
            if (!this->binaryOp(arg1.type(), kLessThanEqualOps)) {
                return unsupported();
            }
            Literal pos1Literal{Position{}, 1.0, &arg1.type().componentType()};
            if (!this->pushVectorizedExpression(pos1Literal, arg1.type())) {
                return unsupported();
            }
            fBuilder.binary_op(BuilderOp::bitwise_and_n_ints, arg1.type().slotCount());
            return true;
        }

        default:
            break;
    }
    return unsupported();
}

bool Generator::pushIntrinsic(IntrinsicKind intrinsic,
                              const Expression& arg0,
                              const Expression& arg1,
                              const Expression& arg2) {
    switch (intrinsic) {
        case IntrinsicKind::k_clamp_IntrinsicKind:
            // Implement clamp as min(max(arg, low), high).
            SkASSERT(arg0.type().componentType().matches(arg1.type().componentType()));
            SkASSERT(arg0.type().componentType().matches(arg2.type().componentType()));
            if (!this->pushExpression(arg0) || !this->pushVectorizedExpression(arg1, arg0.type())) {
                return unsupported();
            }
            if (!this->binaryOp(arg0.type(), kMaxOps)) {
                return unsupported();
            }
            if (!this->pushVectorizedExpression(arg2, arg0.type())) {
                return unsupported();
            }
            if (!this->binaryOp(arg0.type(), kMinOps)) {
                return unsupported();
            }
            return true;

        case IntrinsicKind::k_faceforward_IntrinsicKind: {
            // Implement faceforward as `N ^ ((0 <= dot(I, NRef)) & 0x80000000)`.
            // In other words, flip the sign bit of N if `0 <= dot(I, NRef)`.
            SkASSERT(arg0.type().matches(arg1.type()));
            SkASSERT(arg0.type().matches(arg2.type()));
            int slotCount = arg0.type().slotCount();

            // Stack: N, 0, I, Nref
            if (!this->pushExpression(arg0)) {
                return unsupported();
            }
            fBuilder.push_constant_f(0.0);
            if (!this->pushExpression(arg1) || !this->pushExpression(arg2)) {
                return unsupported();
            }
            // Stack: N, 0, dot(I,NRef)
            fBuilder.dot_floats(slotCount);
            // Stack: N, (0 <= dot(I,NRef))
            fBuilder.binary_op(BuilderOp::cmple_n_floats, 1);
            // Stack: N, (0 <= dot(I,NRef)), 0x80000000
            fBuilder.push_constant_u(0x80000000);
            // Stack: N, (0 <= dot(I,NRef)) & 0x80000000)
            fBuilder.binary_op(BuilderOp::bitwise_and_n_ints, 1);
            // Stack: N, vec(0 <= dot(I,NRef)) & 0x80000000)
            fBuilder.push_duplicates(slotCount - 1);
            // Stack: N ^ vec((0 <= dot(I,NRef)) & 0x80000000)
            fBuilder.binary_op(BuilderOp::bitwise_xor_n_ints, slotCount);
            return true;
        }
        case IntrinsicKind::k_mix_IntrinsicKind:
            // Note: our SkRP mix op takes the interpolation point first, not the interpolants.
            SkASSERT(arg0.type().matches(arg1.type()));
            if (arg2.type().componentType().isFloat()) {
                SkASSERT(arg0.type().componentType().matches(arg2.type().componentType()));
                if (!this->pushVectorizedExpression(arg2, arg0.type())) {
                    return unsupported();
                }
                if (!this->pushExpression(arg0) || !this->pushExpression(arg1)) {
                    return unsupported();
                }
                return this->ternaryOp(arg0.type(), kMixOps);
            }
            if (arg2.type().componentType().isBoolean()) {
                if (!this->pushExpression(arg2)) {
                    return unsupported();
                }
                if (!this->pushExpression(arg0) || !this->pushExpression(arg1)) {
                    return unsupported();
                }
                // The `mix_int` op isn't doing a lerp; it uses the third argument to select values
                // from the first and second arguments. It's safe for use with any type in arguments
                // 0 and 1.
                fBuilder.ternary_op(BuilderOp::mix_n_ints, arg0.type().slotCount());
                return true;
            }
            return unsupported();

        case IntrinsicKind::k_refract_IntrinsicKind: {
            // We always calculate refraction using vec4s, so we pad out unused N/I slots with zero.
            int padding = 4 - arg0.type().slotCount();
            if (!this->pushExpression(arg0)) {
                return unsupported();
            }
            fBuilder.push_zeros(padding);

            if (!this->pushExpression(arg1)) {
                return unsupported();
            }
            fBuilder.push_zeros(padding);

            // eta is always a scalar and doesn't need padding.
            if (!this->pushExpression(arg2)) {
                return unsupported();
            }
            fBuilder.refract_floats();

            // The result vector was returned as a vec4, so discard the extra columns.
            fBuilder.discard_stack(padding);
            return true;
        }
        case IntrinsicKind::k_smoothstep_IntrinsicKind:
            SkASSERT(arg0.type().componentType().isFloat());
            SkASSERT(arg1.type().matches(arg0.type()));
            SkASSERT(arg2.type().componentType().isFloat());

            if (!this->pushVectorizedExpression(arg0, arg2.type()) ||
                !this->pushVectorizedExpression(arg1, arg2.type()) ||
                !this->pushExpression(arg2)) {
                return unsupported();
            }
            fBuilder.ternary_op(BuilderOp::smoothstep_n_floats, arg2.type().slotCount());
            return true;

        default:
            break;
    }
    return unsupported();
}

bool Generator::pushLiteral(const Literal& l) {
    switch (l.type().numberKind()) {
        case Type::NumberKind::kFloat:
            fBuilder.push_constant_f(l.floatValue());
            return true;

        case Type::NumberKind::kSigned:
            fBuilder.push_constant_i(l.intValue());
            return true;

        case Type::NumberKind::kUnsigned:
            fBuilder.push_constant_u(l.intValue());
            return true;

        case Type::NumberKind::kBoolean:
            fBuilder.push_constant_i(l.boolValue() ? ~0 : 0);
            return true;

        default:
            SkUNREACHABLE;
    }
}

bool Generator::pushPostfixExpression(const PostfixExpression& p, bool usesResult) {
    // If the result is ignored...
    if (!usesResult) {
        // ... just emit a prefix expression instead.
        return this->pushPrefixExpression(p.getOperator(), *p.operand());
    }
    // Get the operand as an lvalue, and push it onto the stack as-is.
    std::unique_ptr<LValue> lvalue = this->makeLValue(*p.operand());
    if (!lvalue || !this->push(*lvalue)) {
        return unsupported();
    }

    // Push a scratch copy of the operand.
    fBuilder.push_clone(p.type().slotCount());

    // Increment or decrement the scratch copy by one.
    Literal oneLiteral{Position{}, 1.0, &p.type().componentType()};
    if (!this->pushVectorizedExpression(oneLiteral, p.type())) {
        return unsupported();
    }

    switch (p.getOperator().kind()) {
        case OperatorKind::PLUSPLUS:
            if (!this->binaryOp(p.type(), kAddOps)) {
                return unsupported();
            }
            break;

        case OperatorKind::MINUSMINUS:
            if (!this->binaryOp(p.type(), kSubtractOps)) {
                return unsupported();
            }
            break;

        default:
            SkUNREACHABLE;
    }

    // Write the new value back to the operand.
    if (!this->store(*lvalue)) {
        return unsupported();
    }

    // Discard the scratch copy, leaving only the original value as-is.
    this->discardExpression(p.type().slotCount());
    return true;
}

bool Generator::pushPrefixExpression(const PrefixExpression& p) {
    return this->pushPrefixExpression(p.getOperator(), *p.operand());
}

bool Generator::pushPrefixExpression(Operator op, const Expression& expr) {
    switch (op.kind()) {
        case OperatorKind::BITWISENOT:
        case OperatorKind::LOGICALNOT:
            // Handle operators ! and ~.
            if (!this->pushExpression(expr)) {
                return unsupported();
            }
            fBuilder.push_constant_u(~0, expr.type().slotCount());
            fBuilder.binary_op(BuilderOp::bitwise_xor_n_ints, expr.type().slotCount());
            return true;

        case OperatorKind::MINUS: {
            if (!this->pushExpression(expr)) {
                return unsupported();
            }
            if (expr.type().componentType().isFloat()) {
                // Handle float negation as an integer `x ^ 0x80000000`. This toggles the sign bit.
                fBuilder.push_constant_u(0x80000000, expr.type().slotCount());
                fBuilder.binary_op(BuilderOp::bitwise_xor_n_ints, expr.type().slotCount());
            } else {
                // Handle integer negation as a componentwise `expr * -1`.
                fBuilder.push_constant_i(-1, expr.type().slotCount());
                fBuilder.binary_op(BuilderOp::mul_n_ints, expr.type().slotCount());
            }
            return true;
        }
        case OperatorKind::PLUSPLUS: {
            // Rewrite as `expr += 1`.
            Literal oneLiteral{Position{}, 1.0, &expr.type().componentType()};
            return this->pushBinaryExpression(expr, OperatorKind::PLUSEQ, oneLiteral);
        }
        case OperatorKind::MINUSMINUS: {
            // Rewrite as `expr += -1`.
            Literal minusOneLiteral{expr.fPosition, -1.0, &expr.type().componentType()};
            return this->pushBinaryExpression(expr, OperatorKind::PLUSEQ, minusOneLiteral);
        }
        default:
            break;
    }

    return unsupported();
}

bool Generator::pushSwizzle(const Swizzle& s) {
    SkASSERT(!s.components().empty() && s.components().size() <= 4);

    // If this is a simple subset of a variable's slots...
    bool isSimpleSubset = is_sliceable_swizzle(s.components());
    if (isSimpleSubset && s.base()->is<VariableReference>()) {
        // ... we can just push part of the variable directly onto the stack, rather than pushing
        // the whole expression and then immediately cutting it down. (Either way works, but this
        // saves a step.)
        return this->pushVariableReferencePartial(
                s.base()->as<VariableReference>(),
                SlotRange{/*index=*/s.components()[0], /*count=*/s.components().size()});
    }
    // Push the base expression.
    if (!this->pushExpression(*s.base())) {
        return false;
    }
    // An identity swizzle doesn't rearrange the data; it just (potentially) discards tail elements.
    if (isSimpleSubset && s.components()[0] == 0) {
        int discardedElements = s.base()->type().slotCount() - s.components().size();
        SkASSERT(discardedElements >= 0);
        fBuilder.discard_stack(discardedElements);
        return true;
    }
    // Perform the swizzle.
    fBuilder.swizzle(s.base()->type().slotCount(), s.components());
    return true;
}

bool Generator::pushTernaryExpression(const TernaryExpression& t) {
    return this->pushTernaryExpression(*t.test(), *t.ifTrue(), *t.ifFalse());
}

bool Generator::pushDynamicallyUniformTernaryExpression(const Expression& test,
                                                        const Expression& ifTrue,
                                                        const Expression& ifFalse) {
    SkASSERT(Analysis::IsDynamicallyUniformExpression(test));

    int falseLabelID = fBuilder.nextLabelID();
    int exitLabelID = fBuilder.nextLabelID();

    // First, push the test-expression into a separate stack.
    AutoStack testStack(this);
    testStack.enter();
    if (!this->pushExpression(test)) {
        return unsupported();
    }

    // Branch to the true- or false-expression based on the test-expression. We can skip the
    // non-true path entirely since the test is known to be uniform.
    fBuilder.branch_if_no_active_lanes_on_stack_top_equal(~0, falseLabelID);
    testStack.exit();

    if (!this->pushExpression(ifTrue)) {
        return unsupported();
    }

    fBuilder.jump(exitLabelID);

    // The builder doesn't understand control flow, and assumes that every push moves the stack-top
    // forwards. We need to manually balance out the `pushExpression` from the if-true path by
    // moving the stack position backwards, so that the if-false path pushes its expression into the
    // same as the if-true result.
    this->discardExpression(/*slots=*/ifTrue.type().slotCount());

    fBuilder.label(falseLabelID);

    if (!this->pushExpression(ifFalse)) {
        return unsupported();
    }

    fBuilder.label(exitLabelID);

    // Jettison the text-expression from the separate stack.
    testStack.enter();
    this->discardExpression(/*slots=*/1);
    testStack.exit();
    return true;
}

bool Generator::pushTernaryExpression(const Expression& test,
                                      const Expression& ifTrue,
                                      const Expression& ifFalse) {
    // If the test-expression is dynamically-uniform, we can skip over the non-true expressions
    // entirely, and not need to involve the condition mask.
    if (Analysis::IsDynamicallyUniformExpression(test)) {
        return this->pushDynamicallyUniformTernaryExpression(test, ifTrue, ifFalse);
    }

    // Analyze the ternary to see which corners we can safely cut.
    bool ifFalseHasSideEffects = Analysis::HasSideEffects(ifFalse);
    bool ifTrueHasSideEffects  = Analysis::HasSideEffects(ifTrue);
    bool ifTrueIsTrivial       = Analysis::IsTrivialExpression(ifTrue);
    int  cleanupLabelID        = fBuilder.nextLabelID();

    // If the true- and false-expressions both lack side effects, we evaluate both of them safely
    // without masking off their effects. In that case, we can emit both sides and use boolean mix
    // to select the correct result without using the condition mask at all.
    if (!ifFalseHasSideEffects && !ifTrueHasSideEffects && ifTrueIsTrivial) {
        // Push all of the arguments to mix.
        if (!this->pushVectorizedExpression(test, ifTrue.type())) {
            return unsupported();
        }
        if (!this->pushExpression(ifFalse)) {
            return unsupported();
        }
        if (!this->pushExpression(ifTrue)) {
            return unsupported();
        }
        // Use boolean mix to select the true- or false-expression via the test-expression.
        fBuilder.ternary_op(BuilderOp::mix_n_ints, ifTrue.type().slotCount());
        return true;
    }

    // First, push the current condition-mask and the test-expression into a separate stack.
    fBuilder.enableExecutionMaskWrites();
    AutoStack testStack(this);
    testStack.enter();
    fBuilder.push_condition_mask();
    if (!this->pushExpression(test)) {
        return unsupported();
    }
    testStack.exit();

    // We can take some shortcuts with condition-mask handling if the false-expression is entirely
    // side-effect free. (We can evaluate it without masking off its effects.) We always handle the
    // condition mask properly for the test-expression and true-expression properly.
    if (!ifFalseHasSideEffects) {
        // Push the false-expression onto the primary stack.
        if (!this->pushExpression(ifFalse)) {
            return unsupported();
        }

        // Next, merge the condition mask (on the separate stack) with the test expression.
        testStack.enter();
        fBuilder.merge_condition_mask();
        testStack.exit();

        // If no lanes are active, we can skip the true-expression entirely. This isn't super likely
        // to happen, so it's probably only a win for non-trivial true-expressions.
        if (!ifTrueIsTrivial) {
            fBuilder.branch_if_no_lanes_active(cleanupLabelID);
        }

        // Push the true-expression onto the primary stack, immediately after the false-expression.
        if (!this->pushExpression(ifTrue)) {
            return unsupported();
        }

        // Use a select to conditionally mask-merge the true-expression and false-expression lanes.
        fBuilder.select(/*slots=*/ifTrue.type().slotCount());
        fBuilder.label(cleanupLabelID);
    } else {
        // Merge the condition mask (on the separate stack) with the test expression.
        testStack.enter();
        fBuilder.merge_condition_mask();
        testStack.exit();

        // Push the true-expression onto the primary stack.
        if (!this->pushExpression(ifTrue)) {
            return unsupported();
        }

        // Switch back to the test-expression stack and apply the inverted test condition.
        testStack.enter();
        fBuilder.merge_inv_condition_mask();
        testStack.exit();

        // Push the false-expression onto the primary stack, immediately after the true-expression.
        if (!this->pushExpression(ifFalse)) {
            return unsupported();
        }

        // Use a select to conditionally mask-merge the true-expression and false-expression lanes;
        // the mask is already set up for this.
        fBuilder.select(/*slots=*/ifTrue.type().slotCount());
    }

    // Restore the condition-mask to its original state and jettison the test-expression.
    testStack.enter();
    this->discardExpression(/*slots=*/1);
    fBuilder.pop_condition_mask();
    testStack.exit();

    fBuilder.disableExecutionMaskWrites();
    return true;
}

bool Generator::pushVariableReference(const VariableReference& var) {
    // If we are pushing a constant-value variable, push the value directly; literal values are more
    // amenable to optimization.
    if (var.type().isScalar() || var.type().isVector()) {
        if (const Expression* expr = ConstantFolder::GetConstantValueOrNull(var)) {
            return this->pushExpression(*expr);
        }
        if (fImmutableVariables.contains(var.variable())) {
            return this->pushExpression(*var.variable()->initialValue());
        }
    }
    return this->pushVariableReferencePartial(var, SlotRange{0, (int)var.type().slotCount()});
}

bool Generator::pushVariableReferencePartial(const VariableReference& v, SlotRange subset) {
    const Variable& var = *v.variable();
    SlotRange r;
    if (IsUniform(var)) {
        // Push a uniform.
        r = this->getUniformSlots(var);
        SkASSERT(r.count == (int)var.type().slotCount());
        r.index += subset.index;
        r.count = subset.count;
        fBuilder.push_uniform(r);
    } else if (fImmutableVariables.contains(&var)) {
        // If we only need a single slot, we can push a constant. This saves a lookup, and can
        // occasionally permit the use of an immediate-mode op.
        if (subset.count == 1) {
            const Expression& expr = *v.variable()->initialValue();
            std::optional<ImmutableBits> bits = this->getImmutableBitsForSlot(expr, subset.index);
            if (bits.has_value()) {
                fBuilder.push_constant_i(*bits);
                return true;
            }
        }
        // Push the immutable slot range.
        r = this->getImmutableSlots(var);
        SkASSERT(r.count == (int)var.type().slotCount());
        r.index += subset.index;
        r.count = subset.count;
        fBuilder.push_immutable(r);
    } else {
        // Push the variable.
        r = this->getVariableSlots(var);
        SkASSERT(r.count == (int)var.type().slotCount());
        r.index += subset.index;
        r.count = subset.count;
        fBuilder.push_slots(r);
    }
    return true;
}

bool Generator::writeProgram(const FunctionDefinition& function) {
    fCurrentFunction = &function;

    if (fDebugTrace) {
        // Copy the program source into the debug info so that it will be written in the trace file.
        fDebugTrace->setSource(*fProgram.fSource);

        if (fWriteTraceOps) {
            // The Raster Pipeline blitter generates centered pixel coordinates. (0.5, 1.5, 2.5,
            // etc.) Add 0.5 to the requested trace coordinate to match this, then compare against
            // src.rg, which contains the shader's coordinates. We keep this result in a dedicated
            // trace-mask stack.
            fTraceMask.emplace(this);
            fTraceMask->enter();
            fBuilder.push_device_xy01();
            fBuilder.discard_stack(2);
            fBuilder.push_constant_f(fDebugTrace->fTraceCoord.fX + 0.5f);
            fBuilder.push_constant_f(fDebugTrace->fTraceCoord.fY + 0.5f);
            fBuilder.binary_op(BuilderOp::cmpeq_n_floats, 2);
            fBuilder.binary_op(BuilderOp::bitwise_and_n_ints, 1);
            fTraceMask->exit();

            // Assemble a position-to-line-number mapping for the debugger.
            this->calculateLineOffsets();
        }
    }

    // Assign slots to the parameters of main; copy src and dst into those slots as appropriate.
    const SkSL::Variable* mainCoordsParam = function.declaration().getMainCoordsParameter();
    const SkSL::Variable* mainInputColorParam = function.declaration().getMainInputColorParameter();
    const SkSL::Variable* mainDestColorParam = function.declaration().getMainDestColorParameter();

    for (const SkSL::Variable* param : function.declaration().parameters()) {
        if (param == mainCoordsParam) {
            // Coordinates are passed via RG.
            SlotRange fragCoord = this->getVariableSlots(*param);
            SkASSERT(fragCoord.count == 2);
            fBuilder.store_src_rg(fragCoord);
        } else if (param == mainInputColorParam) {
            // Input colors are passed via RGBA.
            SlotRange srcColor = this->getVariableSlots(*param);
            SkASSERT(srcColor.count == 4);
            fBuilder.store_src(srcColor);
        } else if (param == mainDestColorParam) {
            // Dest colors are passed via dRGBA.
            SlotRange destColor = this->getVariableSlots(*param);
            SkASSERT(destColor.count == 4);
            fBuilder.store_dst(destColor);
        } else {
            SkDEBUGFAIL("Invalid parameter to main()");
            return unsupported();
        }
    }

    // Initialize the program.
    fBuilder.init_lane_masks();

    // Emit global variables.
    if (!this->writeGlobals()) {
        return unsupported();
    }

    // Invoke main().
    std::optional<SlotRange> mainResult = this->writeFunction(function, function, /*arguments=*/{});
    if (!mainResult.has_value()) {
        return unsupported();
    }

    // Move the result of main() from slots into RGBA.
    SkASSERT(mainResult->count == 4);
    if (this->needsFunctionResultSlots(fCurrentFunction)) {
        fBuilder.load_src(*mainResult);
    } else {
        fBuilder.pop_src_rgba();
    }

    // Discard the trace mask.
    if (fTraceMask.has_value()) {
        fTraceMask->enter();
        fBuilder.discard_stack(1);
        fTraceMask->exit();
    }

    return true;
}

std::unique_ptr<RP::Program> Generator::finish() {
    return fBuilder.finish(fProgramSlots.slotCount(),
                           fUniformSlots.slotCount(),
                           fImmutableSlots.slotCount(),
                           fDebugTrace);
}

}  // namespace RP

std::unique_ptr<RP::Program> MakeRasterPipelineProgram(const SkSL::Program& program,
                                                       const FunctionDefinition& function,
                                                       DebugTracePriv* debugTrace,
                                                       bool writeTraceOps) {
    RP::Generator generator(program, debugTrace, writeTraceOps);
    if (!generator.writeProgram(function)) {
        return nullptr;
    }
    return generator.finish();
}

}  // namespace SkSL
