//
// Copyright 2024 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//

#ifdef UNSAFE_BUFFERS_BUILD
#    pragma allow_unsafe_buffers
#endif

#include "compiler/translator/wgsl/TranslatorWGSL.h"

#include <iostream>
#include <variant>

#include "GLSLANG/ShaderLang.h"
#include "common/log_utils.h"
#include "common/span.h"
#include "compiler/translator/BaseTypes.h"
#include "compiler/translator/Common.h"
#include "compiler/translator/Diagnostics.h"
#include "compiler/translator/ImmutableString.h"
#include "compiler/translator/ImmutableStringBuilder.h"
#include "compiler/translator/InfoSink.h"
#include "compiler/translator/IntermNode.h"
#include "compiler/translator/Operator_autogen.h"
#include "compiler/translator/OutputTree.h"
#include "compiler/translator/StaticType.h"
#include "compiler/translator/SymbolUniqueId.h"
#include "compiler/translator/Types.h"
#include "compiler/translator/tree_ops/GatherDefaultUniforms.h"
#include "compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.h"
#include "compiler/translator/tree_ops/ReduceInterfaceBlocks.h"
#include "compiler/translator/tree_ops/RewriteArrayOfArrayOfOpaqueUniforms.h"
#include "compiler/translator/tree_ops/RewriteStructSamplers.h"
#include "compiler/translator/tree_ops/SeparateDeclarations.h"
#include "compiler/translator/tree_ops/SeparateStructFromUniformDeclarations.h"
#include "compiler/translator/tree_ops/wgsl/RewriteMultielementSwizzleAssignment.h"
#include "compiler/translator/tree_util/BuiltIn_autogen.h"
#include "compiler/translator/tree_util/DriverUniform.h"
#include "compiler/translator/tree_util/FindMain.h"
#include "compiler/translator/tree_util/IntermNode_util.h"
#include "compiler/translator/tree_util/IntermTraverse.h"
#include "compiler/translator/tree_util/RunAtTheEndOfShader.h"
#include "compiler/translator/wgsl/OutputUniformBlocks.h"
#include "compiler/translator/wgsl/RewritePipelineVariables.h"
#include "compiler/translator/wgsl/Utils.h"

namespace sh
{
namespace
{

constexpr bool kOutputTreeBeforeTranslation = false;
constexpr bool kOutputTranslatedShader      = false;

struct VarDecl
{
    const SymbolType symbolType = SymbolType::Empty;
    const ImmutableString &symbolName;
    const TType &type;
};

// When emitting a list of statements, this determines whether a semicolon follows the statement.
bool RequiresSemicolonTerminator(TIntermNode &node)
{
    if (node.getAsBlock())
    {
        return false;
    }
    if (node.getAsLoopNode())
    {
        return false;
    }
    if (node.getAsSwitchNode())
    {
        return false;
    }
    if (node.getAsIfElseNode())
    {
        return false;
    }
    if (node.getAsFunctionDefinition())
    {
        return false;
    }
    if (node.getAsCaseNode())
    {
        return false;
    }

    return true;
}

// For pretty formatting of the resulting WGSL text.
bool NewlinePad(TIntermNode &node)
{
    if (node.getAsFunctionDefinition())
    {
        return true;
    }
    if (TIntermDeclaration *declNode = node.getAsDeclarationNode())
    {
        ASSERT(declNode->getChildCount() == 1);
        TIntermNode &childNode = *declNode->getChildNode(0);
        if (TIntermSymbol *symbolNode = childNode.getAsSymbolNode())
        {
            const TVariable &var = symbolNode->variable();
            return var.getType().isStructSpecifier();
        }
        return false;
    }
    return false;
}

// A traverser that generates WGSL as it walks the AST.
class OutputWGSLTraverser : public TIntermTraverser
{
  public:
    OutputWGSLTraverser(TInfoSinkBase *sink,
                        RewritePipelineVarOutput *rewritePipelineVarOutput,
                        UniformBlockMetadata *uniformBlockMetadata,
                        WGSLGenerationMetadataForUniforms *arrayElementTypesInUniforms);
    ~OutputWGSLTraverser() override;

  protected:
    void visitSymbol(TIntermSymbol *node) override;
    void visitConstantUnion(TIntermConstantUnion *node) override;
    bool visitSwizzle(Visit visit, TIntermSwizzle *node) override;
    bool visitBinary(Visit visit, TIntermBinary *node) override;
    bool visitUnary(Visit visit, TIntermUnary *node) override;
    bool visitTernary(Visit visit, TIntermTernary *node) override;
    bool visitIfElse(Visit visit, TIntermIfElse *node) override;
    bool visitSwitch(Visit visit, TIntermSwitch *node) override;
    bool visitCase(Visit visit, TIntermCase *node) override;
    void visitFunctionPrototype(TIntermFunctionPrototype *node) override;
    bool visitFunctionDefinition(Visit visit, TIntermFunctionDefinition *node) override;
    bool visitAggregate(Visit visit, TIntermAggregate *node) override;
    bool visitBlock(Visit visit, TIntermBlock *node) override;
    bool visitGlobalQualifierDeclaration(Visit visit,
                                         TIntermGlobalQualifierDeclaration *node) override;
    bool visitDeclaration(Visit visit, TIntermDeclaration *node) override;
    bool visitLoop(Visit visit, TIntermLoop *node) override;
    bool visitBranch(Visit visit, TIntermBranch *node) override;
    void visitPreprocessorDirective(TIntermPreprocessorDirective *node) override;

  private:
    struct EmitVariableDeclarationConfig
    {
        EmitTypeConfig typeConfig;
        bool isParameter            = false;
        bool disableStructSpecifier = false;
        bool needsVar               = false;
        bool isGlobalScope          = false;
    };

    void groupedTraverse(TIntermNode &node);
    void emitNameOf(const VarDecl &decl);
    void emitBareTypeName(const TType &type);
    void emitType(const TType &type);
    void emitSingleConstant(const TConstantUnion *const constUnion);
    const TConstantUnion *emitConstantUnionArray(const TConstantUnion *const constUnion,
                                                 const size_t size);
    const TConstantUnion *emitConstantUnion(const TType &type,
                                            const TConstantUnion *constUnionBegin);
    const TField &getDirectField(const TIntermTyped &fieldsNode, TIntermTyped &indexNode);
    void emitIndentation();
    void emitOpenBrace();
    void emitCloseBrace();
    bool emitBlock(angle::Span<TIntermNode *> nodes);
    void emitFunctionSignature(const TFunction &func);
    void emitFunctionReturn(const TFunction &func);
    void emitFunctionParameter(const TFunction &func, const TVariable &param);
    void emitStructDeclaration(const TType &type);
    void emitVariableDeclaration(const VarDecl &decl,
                                 const EmitVariableDeclarationConfig &evdConfig);
    void emitArrayIndex(TIntermTyped &leftNode, TIntermTyped &rightNode);
    void emitStructIndex(TIntermBinary *binaryNode);
    void emitStructIndexNoUnwrapping(TIntermBinary *binaryNode);
    void emitTextureBuiltin(const TOperator op, const TIntermSequence &args);

    bool emitForLoop(TIntermLoop *);
    bool emitWhileLoop(TIntermLoop *);
    bool emulateDoWhileLoop(TIntermLoop *);

    TInfoSinkBase &mSink;
    const RewritePipelineVarOutput *mRewritePipelineVarOutput;
    const UniformBlockMetadata *mUniformBlockMetadata;
    WGSLGenerationMetadataForUniforms *mWGSLGenerationMetadataForUniforms;

    int mIndentLevel        = -1;
    int mLastIndentationPos = -1;
};

OutputWGSLTraverser::OutputWGSLTraverser(
    TInfoSinkBase *sink,
    RewritePipelineVarOutput *rewritePipelineVarOutput,
    UniformBlockMetadata *uniformBlockMetadata,
    WGSLGenerationMetadataForUniforms *wgslGenerationMetadataForUniforms)
    : TIntermTraverser(true, false, false),
      mSink(*sink),
      mRewritePipelineVarOutput(rewritePipelineVarOutput),
      mUniformBlockMetadata(uniformBlockMetadata),
      mWGSLGenerationMetadataForUniforms(wgslGenerationMetadataForUniforms)
{}

OutputWGSLTraverser::~OutputWGSLTraverser() = default;

void OutputWGSLTraverser::groupedTraverse(TIntermNode &node)
{
    // TODO(anglebug.com/42267100): to make generated code more readable, do not always
    // emit parentheses like WGSL is some Lisp dialect.
    const bool emitParens = true;

    if (emitParens)
    {
        mSink << "(";
    }

    node.traverse(this);

    if (emitParens)
    {
        mSink << ")";
    }
}

void OutputWGSLTraverser::emitNameOf(const VarDecl &decl)
{
    WriteNameOf(mSink, decl.symbolType, decl.symbolName);
}

void OutputWGSLTraverser::emitIndentation()
{
    ASSERT(mIndentLevel >= 0);

    if (mLastIndentationPos == mSink.size())
    {
        return;  // Line is already indented.
    }

    for (int i = 0; i < mIndentLevel; ++i)
    {
        mSink << "  ";
    }

    mLastIndentationPos = mSink.size();
}

void OutputWGSLTraverser::emitOpenBrace()
{
    ASSERT(mIndentLevel >= 0);

    emitIndentation();
    mSink << "{\n";
    ++mIndentLevel;
}

void OutputWGSLTraverser::emitCloseBrace()
{
    ASSERT(mIndentLevel >= 1);

    --mIndentLevel;
    emitIndentation();
    mSink << "}";
}

void OutputWGSLTraverser::visitSymbol(TIntermSymbol *symbolNode)
{

    const TVariable &var = symbolNode->variable();
    const TType &type    = var.getType();
    ASSERT(var.symbolType() != SymbolType::Empty);

    // Default uniforms should no longer be referenced--they should all be in an interface block by
    // now.
    // TODO(anglebug.com/376553328): gl_DepthRange should be handled by referencing driver
    // uniforms--then the check for builtin default uniforms can be removed here.
    if (type.getBasicType() == TBasicType::EbtVoid ||
        (IsDefaultUniform(type) && var.symbolType() != SymbolType::BuiltIn))
    {
        UNREACHABLE();
    }
    else
    {
        // Accesses of pipeline variables should be rewritten as struct accesses.
        if (mRewritePipelineVarOutput->IsInputVar(var.uniqueId()))
        {
            mSink << kBuiltinInputStructName << "." << var.name();
        }
        else if (mRewritePipelineVarOutput->IsOutputVar(var.uniqueId()))
        {
            mSink << kBuiltinOutputStructName << "." << var.name();
        }
        else
        {
            WriteNameOf(mSink, var);
        }

        if (var.symbolType() == SymbolType::BuiltIn)
        {
            ASSERT(mRewritePipelineVarOutput->IsInputVar(var.uniqueId()) ||
                   mRewritePipelineVarOutput->IsOutputVar(var.uniqueId()) ||
                   var.uniqueId() == BuiltInId::gl_DepthRange);
            // TODO(anglebug.com/376553328): support gl_DepthRange.
            // Match the name of the struct field in `mRewritePipelineVarOutput`.
            mSink << "_";
        }
    }
}

void OutputWGSLTraverser::emitSingleConstant(const TConstantUnion *const constUnion)
{
    switch (constUnion->getType())
    {
        case TBasicType::EbtBool:
        {
            mSink << (constUnion->getBConst() ? "true" : "false");
        }
        break;

        case TBasicType::EbtFloat:
        {
            float value = constUnion->getFConst();
            if (std::isnan(value))
            {
                UNIMPLEMENTED();
                // TODO(anglebug.com/42267100): this is not a valid constant in WGPU.
                // You can't even do something like bitcast<f32>(0xffffffffu).
                // The WGSL compiler still complains. I think this is because
                // WGSL supports implementations compiling with -ffastmath and
                // therefore nans and infinities are assumed to not exist.
                // See also https://github.com/gpuweb/gpuweb/issues/3749.
                mSink << "NAN_INVALID";
            }
            else if (std::isinf(value))
            {
                UNIMPLEMENTED();
                // see above.
                mSink << "INFINITY_INVALID";
            }
            else
            {
                mSink << value << "f";
            }
        }
        break;

        case TBasicType::EbtInt:
        {
            mSink << constUnion->getIConst() << "i";
        }
        break;

        case TBasicType::EbtUInt:
        {
            mSink << constUnion->getUConst() << "u";
        }
        break;

        default:
        {
            UNIMPLEMENTED();
        }
    }
}

const TConstantUnion *OutputWGSLTraverser::emitConstantUnionArray(
    const TConstantUnion *const constUnion,
    const size_t size)
{
    const TConstantUnion *constUnionIterated = constUnion;
    for (size_t i = 0; i < size; i++, constUnionIterated++)
    {
        emitSingleConstant(constUnionIterated);

        if (i != size - 1)
        {
            mSink << ", ";
        }
    }
    return constUnionIterated;
}

const TConstantUnion *OutputWGSLTraverser::emitConstantUnion(const TType &type,
                                                             const TConstantUnion *constUnionBegin)
{
    const TConstantUnion *constUnionCurr = constUnionBegin;
    const TStructure *structure          = type.getStruct();
    if (structure)
    {
        emitType(type);
        // Structs are constructed with parentheses in WGSL.
        mSink << "(";
        // Emit the constructor parameters. Both GLSL and WGSL require there to be the same number
        // of parameters as struct fields.
        const TFieldList &fields = structure->fields();
        for (size_t i = 0; i < fields.size(); ++i)
        {
            const TType *fieldType = fields[i]->type();
            constUnionCurr         = emitConstantUnion(*fieldType, constUnionCurr);
            if (i != fields.size() - 1)
            {
                mSink << ", ";
            }
        }
        mSink << ")";
    }
    else
    {
        size_t size = type.getObjectSize();
        // If the type's size is more than 1, the type needs to be written with parentheses. This
        // applies for vectors, matrices, and arrays.
        bool writeType = size > 1;
        if (writeType)
        {
            emitType(type);
            mSink << "(";
        }
        constUnionCurr = emitConstantUnionArray(constUnionCurr, size);
        if (writeType)
        {
            mSink << ")";
        }
    }
    return constUnionCurr;
}

void OutputWGSLTraverser::visitConstantUnion(TIntermConstantUnion *constValueNode)
{
    emitConstantUnion(constValueNode->getType(), constValueNode->getConstantValue());
}

bool OutputWGSLTraverser::visitSwizzle(Visit, TIntermSwizzle *swizzleNode)
{
    groupedTraverse(*swizzleNode->getOperand());
    mSink << "." << swizzleNode->getOffsetsAsXYZW();

    return false;
}

struct OperatorInfo
{
    const char *opName;
    const char *wgslWrapperFn = nullptr;

    bool IsSymbolicOperator() const { return opName && !std::isalnum(opName[0]); }
};

OperatorInfo GetOperatorInfo(TOperator op,
                             const TType &resultType,
                             const TType *argType0,
                             const TType *argType1,
                             const TType *argType2)
{
    switch (op)
    {
        case TOperator::EOpComma:
            // WGSL does not have a comma operator or any other way to implement "statement list as
            // an expression", so nested expressions will have to be pulled out into statements.
            UNIMPLEMENTED();
            return {"TODO_operator"};
        case TOperator::EOpAssign:
            return {"="};
        case TOperator::EOpInitialize:
            return {"="};
        // Compound assignments now exist: https://www.w3.org/TR/WGSL/#compound-assignment-sec
        case TOperator::EOpAddAssign:
            return {"+="};
        case TOperator::EOpSubAssign:
            return {"-="};
        case TOperator::EOpMulAssign:
            return {"*="};
        case TOperator::EOpDivAssign:
            return {"/="};
        case TOperator::EOpIModAssign:
            return {"%="};
        case TOperator::EOpBitShiftLeftAssign:
            return {"<<="};
        case TOperator::EOpBitShiftRightAssign:
            return {">>="};
        case TOperator::EOpBitwiseAndAssign:
            return {"&="};
        case TOperator::EOpBitwiseXorAssign:
            return {"^="};
        case TOperator::EOpBitwiseOrAssign:
            return {"|="};
        case TOperator::EOpAdd:
            return {"+"};
        case TOperator::EOpSub:
            return {"-"};
        case TOperator::EOpMul:
            return {"*"};
        case TOperator::EOpDiv:
            return {"/"};
        // TODO(anglebug.com/42267100): Works different from GLSL for negative numbers.
        // https://github.com/gpuweb/gpuweb/discussions/2204#:~:text=not%20WGSL%3B%20etc.-,Inconsistent%20mod/%25%20operator,-At%20first%20glance
        // GLSL does `x - y * floor(x/y)`, WGSL does x - y * trunc(x/y).
        case TOperator::EOpIMod:
        case TOperator::EOpMod:
            return {"%"};
        // TODO(anglebug.com/42267100): bitwise operations can be between scalars and vectors, but
        // not in WGSL.
        case TOperator::EOpBitShiftLeft:
            return {"<<"};
        case TOperator::EOpBitShiftRight:
            return {">>"};
        case TOperator::EOpBitwiseAnd:
            return {"&"};
        case TOperator::EOpBitwiseXor:
            return {"^"};
        case TOperator::EOpBitwiseOr:
            return {"|"};
        case TOperator::EOpLessThan:
            return {"<"};
        case TOperator::EOpGreaterThan:
            return {">"};
        case TOperator::EOpLessThanEqual:
            return {"<="};
        case TOperator::EOpGreaterThanEqual:
            return {">="};
        // Component-wise comparisons are done with regular infix operators in WGSL:
        // https://www.w3.org/TR/WGSL/#comparison-expr
        case TOperator::EOpLessThanComponentWise:
            return {"<"};
        case TOperator::EOpLessThanEqualComponentWise:
            return {"<="};
        case TOperator::EOpGreaterThanEqualComponentWise:
            return {">="};
        case TOperator::EOpGreaterThanComponentWise:
            return {">"};
        case TOperator::EOpLogicalOr:
            return {"||"};
        // Logical XOR is only applied to boolean expressions so it's the same as "not equals".
        // Neither short-circuits.
        case TOperator::EOpLogicalXor:
            return {"!="};
        case TOperator::EOpLogicalAnd:
            return {"&&"};
        case TOperator::EOpNegative:
            return {"-"};
        case TOperator::EOpPositive:
            if (argType0->isMatrix())
            {
                return {""};
            }
            return {"+"};
        case TOperator::EOpLogicalNot:
            return {"!"};
        // Component-wise not done with normal prefix unary operator in WGSL:
        // https://www.w3.org/TR/WGSL/#logical-expr
        case TOperator::EOpNotComponentWise:
            return {"!"};
        case TOperator::EOpBitwiseNot:
            return {"~"};
        // TODO(anglebug.com/42267100): increment operations cannot be used as expressions in WGSL.
        case TOperator::EOpPostIncrement:
            return {"++"};
        case TOperator::EOpPostDecrement:
            return {"--"};
        case TOperator::EOpPreIncrement:
        case TOperator::EOpPreDecrement:
            // TODO(anglebug.com/42267100): pre increments and decrements do not exist in WGSL.
            UNIMPLEMENTED();
            return {"TODO_operator"};
        case TOperator::EOpVectorTimesScalarAssign:
            return {"*="};
        case TOperator::EOpVectorTimesMatrixAssign:
            return {"*="};
        case TOperator::EOpMatrixTimesScalarAssign:
            return {"*="};
        case TOperator::EOpMatrixTimesMatrixAssign:
            return {"*="};
        case TOperator::EOpVectorTimesScalar:
            return {"*"};
        case TOperator::EOpVectorTimesMatrix:
            return {"*"};
        case TOperator::EOpMatrixTimesVector:
            return {"*"};
        case TOperator::EOpMatrixTimesScalar:
            return {"*"};
        case TOperator::EOpMatrixTimesMatrix:
            return {"*"};
        case TOperator::EOpEqualComponentWise:
            return {"=="};
        case TOperator::EOpNotEqualComponentWise:
            return {"!="};

        // TODO(anglebug.com/42267100): structs, matrices, and arrays are not comparable with WGSL's
        // == or !=. Comparing vectors results in a component-wise comparison returning a boolean
        // vector, which is different from GLSL (which use equal(vec, vec) for component-wise
        // comparison)
        case TOperator::EOpEqual:
            if (argType0->isVector() && argType1->isVector())
            {
                return {"==", "all"};
            }

            if ((argType0->getStruct() && argType1->getStruct()) ||
                (argType0->isArray() && argType1->isArray()) ||
                (argType0->isMatrix() && argType1->isMatrix()))

            {
                UNIMPLEMENTED();
                return {"TODO_operator"};
            }

            return {"=="};

        case TOperator::EOpNotEqual:
            if ((argType0->isVector() && argType1->isVector()))
            {
                return {"!=", "all"};
            }

            if ((argType0->getStruct() && argType1->getStruct()) ||
                (argType0->isArray() && argType1->isArray()) ||
                (argType0->isMatrix() && argType1->isMatrix()))
            {
                UNIMPLEMENTED();
                return {"TODO_operator"};
            }

            return {"!="};

        case TOperator::EOpKill:
        case TOperator::EOpReturn:
        case TOperator::EOpBreak:
        case TOperator::EOpContinue:
            // These should all be emitted in visitBranch().
            UNREACHABLE();
            return {"UNREACHABLE_operator"};
        case TOperator::EOpRadians:
            return {"radians"};
        case TOperator::EOpDegrees:
            return {"degrees"};
        case TOperator::EOpAtan:
            return argType1 == nullptr ? OperatorInfo{"atan"} : OperatorInfo{"atan2"};
        case TOperator::EOpRefract:
            return argType0->isVector() ? OperatorInfo{"refract"} : OperatorInfo{"TODO_operator"};
        case TOperator::EOpDistance:
            return {"distance"};
        case TOperator::EOpLength:
            return {"length"};
        case TOperator::EOpDot:
            return argType0->isVector() ? OperatorInfo{"dot"} : OperatorInfo{"*"};
        case TOperator::EOpNormalize:
            return argType0->isVector() ? OperatorInfo{"normalize"} : OperatorInfo{"sign"};
        case TOperator::EOpFaceforward:
            return argType0->isVector() ? OperatorInfo{"faceForward"}
                                        : OperatorInfo{"TODO_Operator"};
        case TOperator::EOpReflect:
            return argType0->isVector() ? OperatorInfo{"reflect"} : OperatorInfo{"TODO_Operator"};
        case TOperator::EOpMatrixCompMult:
            return {"TODO_Operator"};
        case TOperator::EOpOuterProduct:
            return {"TODO_Operator"};
        case TOperator::EOpSign:
            return {"sign"};

        case TOperator::EOpAbs:
            return {"abs"};
        case TOperator::EOpAll:
            return {"all"};
        case TOperator::EOpAny:
            return {"any"};
        case TOperator::EOpSin:
            return {"sin"};
        case TOperator::EOpCos:
            return {"cos"};
        case TOperator::EOpTan:
            return {"tan"};
        case TOperator::EOpAsin:
            return {"asin"};
        case TOperator::EOpAcos:
            return {"acos"};
        case TOperator::EOpSinh:
            return {"sinh"};
        case TOperator::EOpCosh:
            return {"cosh"};
        case TOperator::EOpTanh:
            return {"tanh"};
        case TOperator::EOpAsinh:
            return {"asinh"};
        case TOperator::EOpAcosh:
            return {"acosh"};
        case TOperator::EOpAtanh:
            return {"atanh"};
        case TOperator::EOpFma:
            return {"fma"};
        // TODO(anglebug.com/42267100): Won't accept pow(vec<f32>, f32).
        // https://github.com/gpuweb/gpuweb/discussions/2204#:~:text=Similarly%20pow(vec3%3Cf32%3E%2C%20f32)%20works%20in%20GLSL%20but%20not%20WGSL
        case TOperator::EOpPow:
            return {"pow"};  // GLSL's pow excludes negative x
        case TOperator::EOpExp:
            return {"exp"};
        case TOperator::EOpExp2:
            return {"exp2"};
        case TOperator::EOpLog:
            return {"log"};
        case TOperator::EOpLog2:
            return {"log2"};
        case TOperator::EOpSqrt:
            return {"sqrt"};
        case TOperator::EOpFloor:
            return {"floor"};
        case TOperator::EOpTrunc:
            return {"trunc"};
        case TOperator::EOpCeil:
            return {"ceil"};
        case TOperator::EOpFract:
            return {"fract"};
        case TOperator::EOpMin:
            return {"min"};
        case TOperator::EOpMax:
            return {"max"};
        case TOperator::EOpRound:
            return {
                "round"};  // TODO(anglebug.com/42267100): this is wrong and must round away from
                           // zero if there is a tie. This always rounds to the even number.
        case TOperator::EOpRoundEven:
            return {"round"};
        // TODO(anglebug.com/42267100):
        // https://github.com/gpuweb/gpuweb/discussions/2204#:~:text=clamp(vec2%3Cf32%3E%2C%20f32%2C%20f32)%20works%20in%20GLSL%20but%20not%20WGSL%3B%20etc.
        // Need to expand clamp(vec<f32>, low : f32, high : f32) ->
        // clamp(vec<f32>, vec<f32>(low), vec<f32>(high))
        case TOperator::EOpClamp:
            return {"clamp"};
        case TOperator::EOpSaturate:
            return {"saturate"};
        case TOperator::EOpMix:
            if (!argType1->isScalar() && argType2 && argType2->getBasicType() == EbtBool)
            {
                return {"TODO_Operator"};
            }
            return {"mix"};
        case TOperator::EOpStep:
            return {"step"};
        case TOperator::EOpSmoothstep:
            return {"smoothstep"};
        case TOperator::EOpModf:
            UNIMPLEMENTED();  // TODO(anglebug.com/42267100): in WGSL this returns a struct, GLSL it
                              // uses a return value and an outparam
            return {"modf"};
        case TOperator::EOpIsnan:
        case TOperator::EOpIsinf:
            UNIMPLEMENTED();  // TODO(anglebug.com/42267100): WGSL does not allow NaNs or infinity.
                              // What to do about shaders that require this?
            // Implementations are allowed to assume overflow, infinities, and NaNs are not present
            // at runtime, however. https://www.w3.org/TR/WGSL/#floating-point-evaluation
            return {"TODO_Operator"};
        case TOperator::EOpLdexp:
            // TODO(anglebug.com/42267100): won't accept first arg vector, second arg scalar
            return {"ldexp"};
        case TOperator::EOpFrexp:
            return {"frexp"};  // TODO(anglebug.com/42267100): returns a struct
        case TOperator::EOpInversesqrt:
            return {"inverseSqrt"};
        case TOperator::EOpCross:
            return {"cross"};
            // TODO(anglebug.com/42267100): are these the same? dpdxCoarse() vs dpdxFine()?
        case TOperator::EOpDFdx:
            return {"dpdx"};
        case TOperator::EOpDFdy:
            return {"dpdy"};
        case TOperator::EOpFwidth:
            return {"fwidth"};
        case TOperator::EOpTranspose:
            return {"transpose"};
        case TOperator::EOpDeterminant:
            return {"determinant"};

        case TOperator::EOpInverse:
            return {"TODO_Operator"};  // No builtin invert().
                                       // https://github.com/gpuweb/gpuweb/issues/4115

        // TODO(anglebug.com/42267100): these interpolateAt*() are not builtin
        case TOperator::EOpInterpolateAtCentroid:
            return {"TODO_Operator"};
        case TOperator::EOpInterpolateAtSample:
            return {"TODO_Operator"};
        case TOperator::EOpInterpolateAtOffset:
            return {"TODO_Operator"};
        case TOperator::EOpInterpolateAtCenter:
            return {"TODO_Operator"};

        case TOperator::EOpFloatBitsToInt:
        case TOperator::EOpFloatBitsToUint:
        case TOperator::EOpIntBitsToFloat:
        case TOperator::EOpUintBitsToFloat:
        {
#define BITCAST_SCALAR()                   \
    do                                     \
        switch (resultType.getBasicType()) \
        {                                  \
            case TBasicType::EbtInt:       \
                return {"bitcast<i32>"};   \
            case TBasicType::EbtUInt:      \
                return {"bitcast<u32>"};   \
            case TBasicType::EbtFloat:     \
                return {"bitcast<f32>"};   \
            default:                       \
                UNIMPLEMENTED();           \
                return {"TOperator_TODO"}; \
        }                                  \
    while (false)

#define BITCAST_VECTOR(vecSize)                          \
    do                                                   \
        switch (resultType.getBasicType())               \
        {                                                \
            case TBasicType::EbtInt:                     \
                return {"bitcast<vec" vecSize "<i32>>"}; \
            case TBasicType::EbtUInt:                    \
                return {"bitcast<vec" vecSize "<u32>>"}; \
            case TBasicType::EbtFloat:                   \
                return {"bitcast<vec" vecSize "<f32>>"}; \
            default:                                     \
                UNIMPLEMENTED();                         \
                return {"TOperator_TODO"};               \
        }                                                \
    while (false)

            if (resultType.isScalar())
            {
                BITCAST_SCALAR();
            }
            else if (resultType.isVector())
            {
                switch (resultType.getNominalSize())
                {
                    case 2:
                        BITCAST_VECTOR("2");
                    case 3:
                        BITCAST_VECTOR("3");
                    case 4:
                        BITCAST_VECTOR("4");
                    default:
                        UNREACHABLE();
                        return {nullptr};
                }
            }
            else
            {
                UNIMPLEMENTED();
                return {"TOperator_TODO"};
            }

#undef BITCAST_SCALAR
#undef BITCAST_VECTOR
        }

        case TOperator::EOpPackUnorm2x16:
            return {"pack2x16unorm"};
        case TOperator::EOpPackSnorm2x16:
            return {"pack2x16snorm"};

        case TOperator::EOpPackUnorm4x8:
            return {"pack4x8unorm"};
        case TOperator::EOpPackSnorm4x8:
            return {"pack4x8snorm"};

        case TOperator::EOpUnpackUnorm2x16:
            return {"unpack2x16unorm"};
        case TOperator::EOpUnpackSnorm2x16:
            return {"unpack2x16snorm"};

        case TOperator::EOpUnpackUnorm4x8:
            return {"unpack4x8unorm"};
        case TOperator::EOpUnpackSnorm4x8:
            return {"unpack4x8snorm"};

        case TOperator::EOpPackHalf2x16:
            return {"pack2x16float"};
        case TOperator::EOpUnpackHalf2x16:
            return {"unpack2x16float"};

        case TOperator::EOpBarrier:
            UNREACHABLE();
            return {"TOperator_TODO"};
        case TOperator::EOpMemoryBarrier:
            // TODO(anglebug.com/42267100): does this exist in WGPU? Device-scoped memory barrier?
            // Maybe storageBarrier()?
            UNREACHABLE();
            return {"TOperator_TODO"};
        case TOperator::EOpGroupMemoryBarrier:
            return {"workgroupBarrier"};
        case TOperator::EOpMemoryBarrierAtomicCounter:
        case TOperator::EOpMemoryBarrierBuffer:
        case TOperator::EOpMemoryBarrierShared:
            UNREACHABLE();
            return {"TOperator_TODO"};
        case TOperator::EOpAtomicAdd:
            return {"atomicAdd"};
        case TOperator::EOpAtomicMin:
            return {"atomicMin"};
        case TOperator::EOpAtomicMax:
            return {"atomicMax"};
        case TOperator::EOpAtomicAnd:
            return {"atomicAnd"};
        case TOperator::EOpAtomicOr:
            return {"atomicOr"};
        case TOperator::EOpAtomicXor:
            return {"atomicXor"};
        case TOperator::EOpAtomicExchange:
            return {"atomicExchange"};
        case TOperator::EOpAtomicCompSwap:
            return {"atomicCompareExchangeWeak"};  // TODO(anglebug.com/42267100): returns a struct.
        case TOperator::EOpBitfieldExtract:
        case TOperator::EOpBitfieldInsert:
        case TOperator::EOpBitfieldReverse:
        case TOperator::EOpBitCount:
        case TOperator::EOpFindLSB:
        case TOperator::EOpFindMSB:
        case TOperator::EOpUaddCarry:
        case TOperator::EOpUsubBorrow:
        case TOperator::EOpUmulExtended:
        case TOperator::EOpImulExtended:
        case TOperator::EOpEmitVertex:
        case TOperator::EOpEndPrimitive:
        case TOperator::EOpArrayLength:
            UNIMPLEMENTED();
            return {"TOperator_TODO"};

        case TOperator::EOpNull:
        case TOperator::EOpConstruct:
        case TOperator::EOpCallFunctionInAST:
        case TOperator::EOpCallInternalRawFunction:
        case TOperator::EOpIndexDirect:
        case TOperator::EOpIndexIndirect:
        case TOperator::EOpIndexDirectStruct:
        case TOperator::EOpIndexDirectInterfaceBlock:
            UNREACHABLE();
            return {nullptr};
        default:
            // Any other built-in function.
            return {nullptr};
    }
}

const TField &OutputWGSLTraverser::getDirectField(const TIntermTyped &fieldsNode,
                                                  TIntermTyped &indexNode)
{
    const TType &fieldsType = fieldsNode.getType();

    const TFieldListCollection *fieldListCollection = fieldsType.getStruct();
    if (fieldListCollection == nullptr)
    {
        fieldListCollection = fieldsType.getInterfaceBlock();
    }
    ASSERT(fieldListCollection);

    const TIntermConstantUnion *indexNodeAsConstantUnion = indexNode.getAsConstantUnion();
    ASSERT(indexNodeAsConstantUnion);
    const TConstantUnion &index = *indexNodeAsConstantUnion->getConstantValue();

    ASSERT(index.getType() == TBasicType::EbtInt);

    const TFieldList &fieldList = fieldListCollection->fields();
    const int indexVal          = index.getIConst();
    const TField &field         = *fieldList[indexVal];

    return field;
}

void OutputWGSLTraverser::emitArrayIndex(TIntermTyped &leftNode, TIntermTyped &rightNode)
{
    TType leftType = leftNode.getType();

    // Some arrays within the uniform address space have their element types wrapped in a struct
    // when generating WGSL, so this unwraps the element (as an optimization of converting the
    // entire array back to the unwrapped type).
    bool needsUnwrapping                  = false;
    bool isUniformMatrixNeedingConversion = false;
    bool isUniformBoolNeedingConversion   = false;
    TIntermBinary *leftNodeBinary         = leftNode.getAsBinaryNode();
    if (leftNodeBinary && leftNodeBinary->getOp() == TOperator::EOpIndexDirectStruct)
    {
        const TStructure *structure = leftNodeBinary->getLeft()->getType().getStruct();

        bool isInUniformAddressSpace =
            mUniformBlockMetadata->structsInUniformAddressSpace.count(structure->uniqueId().get());

        needsUnwrapping =
            structure && ElementTypeNeedsUniformWrapperStruct(isInUniformAddressSpace, &leftType);

        isUniformMatrixNeedingConversion = isInUniformAddressSpace && IsMatCx2(&leftType);

        isUniformBoolNeedingConversion =
            isInUniformAddressSpace && leftType.getBasicType() == EbtBool;

        ASSERT(!needsUnwrapping || !isUniformMatrixNeedingConversion);
    }

    // Emit the left side, which should be of type array.
    if (needsUnwrapping || isUniformMatrixNeedingConversion || isUniformBoolNeedingConversion)
    {
        if (isUniformMatrixNeedingConversion)
        {
            // If this array index expression is yielding an std140 matCx2 (i.e.
            // array<ANGLE_wrapped_vec2, C>), just convert the entire expression to a WGSL matCx2,
            // instead of converting the entire array of std140 matCx2s into an array of WGSL
            // matCx2s and then indexing into it.
            TType baseType = leftType;
            baseType.toArrayBaseType();
            mSink << MakeMatCx2ConversionFunctionName(&baseType) << "(";
            // Make sure the conversion function referenced here is actually generated in the
            // resulting WGSL.
            mWGSLGenerationMetadataForUniforms->outputMatCx2Conversion.insert(baseType);
        }
        else if (isUniformBoolNeedingConversion)
        {
            // Convert just this one array element into a bool instead of converting the entire
            // array into an array of booleans and indexing into that.
            OutputUniformBoolOrBvecConversion(mSink, leftType);
        }
        emitStructIndexNoUnwrapping(leftNodeBinary);
    }
    else
    {
        groupedTraverse(leftNode);
    }

    mSink << "[";
    const TConstantUnion *constIndex = rightNode.getConstantValue();
    // If the array index is a constant that we can statically verify is within array
    // bounds, just emit that constant.
    if (!leftType.isUnsizedArray() && constIndex != nullptr && constIndex->getType() == EbtInt &&
        constIndex->getIConst() >= 0 &&
        constIndex->getIConst() < static_cast<int>(leftType.isArray()
                                                       ? leftType.getOutermostArraySize()
                                                       : leftType.getNominalSize()))
    {
        emitSingleConstant(constIndex);
    }
    else
    {
        // If the array index is not a constant within the bounds of the array, clamp the
        // index.
        mSink << "clamp(";
        groupedTraverse(rightNode);
        mSink << ", 0, ";
        // Now find the array size and clamp it.
        if (leftType.isUnsizedArray())
        {
            // TODO(anglebug.com/42267100): This is a bug to traverse the `leftNode` a
            // second time if `leftNode` has side effects (and could also have performance
            // implications). This should be stored in a temporary variable. This might also
            // be a bug in the MSL shader compiler.
            mSink << "arrayLength(&";
            groupedTraverse(leftNode);
            mSink << ")";
        }
        else
        {
            uint32_t maxSize;
            if (leftType.isArray())
            {
                maxSize = leftType.getOutermostArraySize() - 1;
            }
            else
            {
                maxSize = leftType.getNominalSize() - 1;
            }
            mSink << maxSize;
        }
        // End the clamp() function.
        mSink << ")";
    }
    // End the array index operation.
    mSink << "]";

    if (needsUnwrapping)
    {
        mSink << "." << kWrappedStructFieldName;
    }

    if (isUniformMatrixNeedingConversion || isUniformBoolNeedingConversion)
    {
        // Close conversion function call
        mSink << ")";
    }
}

void OutputWGSLTraverser::emitStructIndex(TIntermBinary *binaryNode)
{
    ASSERT(binaryNode->getOp() == TOperator::EOpIndexDirectStruct);
    TIntermTyped &leftNode  = *binaryNode->getLeft();
    const TType *binaryNodeType = &binaryNode->getType();

    const TStructure *structure = leftNode.getType().getStruct();
    ASSERT(structure);

    bool isInUniformAddressSpace =
        mUniformBlockMetadata->structsInUniformAddressSpace.count(structure->uniqueId().get());

    bool isUniformMatrixNeedingConversion = isInUniformAddressSpace && IsMatCx2(binaryNodeType);

    bool isUniformBoolNeedingConversion =
        isInUniformAddressSpace && binaryNode->getBasicType() == EbtBool;

    bool needsUnwrapping =
        ElementTypeNeedsUniformWrapperStruct(isInUniformAddressSpace, binaryNodeType);
    if (needsUnwrapping)
    {
        ASSERT(!isUniformMatrixNeedingConversion);

        mSink << MakeUnwrappingArrayConversionFunctionName(&binaryNode->getType()) << "(";
        // Make sure the conversion function referenced here is actually generated in the resulting
        // WGSL.
        mWGSLGenerationMetadataForUniforms->arrayElementTypesThatNeedUnwrappingConversions.insert(
            *binaryNodeType);
    }
    else if (isUniformMatrixNeedingConversion)
    {
        mSink << MakeMatCx2ConversionFunctionName(binaryNodeType) << "(";
        // Make sure the conversion function referenced here is actually generated in the resulting
        // WGSL.
        mWGSLGenerationMetadataForUniforms->outputMatCx2Conversion.insert(*binaryNodeType);
    }
    else if (isUniformBoolNeedingConversion)
    {
        // Should only trigger in case of a boolean not in an array.
        OutputUniformBoolOrBvecConversion(mSink, *binaryNodeType);
    }
    emitStructIndexNoUnwrapping(binaryNode);
    if (needsUnwrapping || isUniformMatrixNeedingConversion || isUniformBoolNeedingConversion)
    {
        mSink << ")";
    }
}

void OutputWGSLTraverser::emitStructIndexNoUnwrapping(TIntermBinary *binaryNode)
{
    ASSERT(binaryNode->getOp() == TOperator::EOpIndexDirectStruct);
    TIntermTyped &leftNode  = *binaryNode->getLeft();
    TIntermTyped &rightNode = *binaryNode->getRight();

    groupedTraverse(leftNode);
    mSink << ".";
    WriteNameOf(mSink, getDirectField(leftNode, rightNode));
}

bool OutputWGSLTraverser::visitBinary(Visit, TIntermBinary *binaryNode)
{
    const TOperator op      = binaryNode->getOp();
    TIntermTyped &leftNode  = *binaryNode->getLeft();
    TIntermTyped &rightNode = *binaryNode->getRight();

    switch (op)
    {
        case TOperator::EOpIndexDirectStruct:
            emitStructIndex(binaryNode);
            break;
        case TOperator::EOpIndexDirectInterfaceBlock:
            UNREACHABLE();  // Interface blocks should have been converted into structs.
            break;
        case TOperator::EOpIndexDirect:
        case TOperator::EOpIndexIndirect:
            emitArrayIndex(leftNode, rightNode);
            break;

        default:
        {
            const TType &resultType = binaryNode->getType();
            const TType &leftType   = leftNode.getType();
            const TType &rightType  = rightNode.getType();

            const OperatorInfo opInfo =
                GetOperatorInfo(op, resultType, &leftType, &rightType, nullptr);

            if (opInfo.wgslWrapperFn)
            {
                mSink << opInfo.wgslWrapperFn << "(";
            }

            // x * y, x ^ y, etc.
            if (opInfo.IsSymbolicOperator())
            {
                groupedTraverse(leftNode);
                if (op != TOperator::EOpComma)
                {
                    mSink << " ";
                }
                mSink << opInfo.opName << " ";
                groupedTraverse(rightNode);
            }
            // E.g. builtin function calls
            else
            {
                mSink << opInfo.opName << "(";
                leftNode.traverse(this);
                mSink << ", ";
                rightNode.traverse(this);
                mSink << ")";
            }

            if (opInfo.wgslWrapperFn)
            {
                mSink << ")";
            }
        }
    }

    return false;
}

bool IsPostfix(TOperator op)
{
    switch (op)
    {
        case TOperator::EOpPostIncrement:
        case TOperator::EOpPostDecrement:
            return true;

        default:
            return false;
    }
}

bool OutputWGSLTraverser::visitUnary(Visit, TIntermUnary *unaryNode)
{
    const TOperator op      = unaryNode->getOp();
    const TType &resultType = unaryNode->getType();

    TIntermTyped &arg    = *unaryNode->getOperand();
    const TType &argType = arg.getType();

    const OperatorInfo opInfo = GetOperatorInfo(op, resultType, &argType, nullptr, nullptr);

    if (opInfo.wgslWrapperFn)
    {
        mSink << opInfo.wgslWrapperFn << "(";
    }

    // Examples: -x, ~x, ~x
    if (opInfo.IsSymbolicOperator())
    {
        const bool postfix = IsPostfix(op);
        if (!postfix)
        {
            mSink << opInfo.opName;
        }
        groupedTraverse(arg);
        if (postfix)
        {
            mSink << opInfo.opName;
        }
    }
    else
    {
        mSink << opInfo.opName << "(";
        arg.traverse(this);
        mSink << ")";
    }

    if (opInfo.wgslWrapperFn)
    {
        mSink << ")";
    }

    return false;
}

bool OutputWGSLTraverser::visitTernary(Visit, TIntermTernary *conditionalNode)
{
    // WGSL does not have a ternary. https://github.com/gpuweb/gpuweb/issues/3747
    // The select() builtin is not short circuiting. Maybe we can get if () {} else {} as an
    // expression, which would also solve the comma operator problem.
    // TODO(anglebug.com/42267100): as mentioned above this is not correct if the operands have side
    // effects. Even if they don't have side effects it could have performance implications.
    // It also doesn't work with all types that ternaries do, e.g. arrays or structs.
    mSink << "select(";
    groupedTraverse(*conditionalNode->getFalseExpression());
    mSink << ", ";
    groupedTraverse(*conditionalNode->getTrueExpression());
    mSink << ", ";
    groupedTraverse(*conditionalNode->getCondition());
    mSink << ")";

    return false;
}

bool OutputWGSLTraverser::visitIfElse(Visit, TIntermIfElse *ifThenElseNode)
{
    TIntermTyped &condNode = *ifThenElseNode->getCondition();
    TIntermBlock *thenNode = ifThenElseNode->getTrueBlock();
    TIntermBlock *elseNode = ifThenElseNode->getFalseBlock();

    mSink << "if (";
    condNode.traverse(this);
    mSink << ")";

    if (thenNode)
    {
        mSink << "\n";
        thenNode->traverse(this);
    }
    else
    {
        mSink << " {}";
    }

    if (elseNode)
    {
        mSink << "\n";
        emitIndentation();
        mSink << "else\n";
        elseNode->traverse(this);
    }

    return false;
}

bool OutputWGSLTraverser::visitSwitch(Visit, TIntermSwitch *switchNode)
{
    TIntermBlock &stmtList = *switchNode->getStatementList();

    emitIndentation();
    mSink << "switch ";
    switchNode->getInit()->traverse(this);
    mSink << "\n";

    emitOpenBrace();

    // TODO(anglebug.com/42267100): Case statements that fall through need to combined into a single
    // case statement with multiple labels.

    const size_t stmtCount = stmtList.getChildCount();
    bool inCaseList        = false;
    size_t currStmt        = 0;
    while (currStmt < stmtCount)
    {
        TIntermNode &stmtNode = *stmtList.getChildNode(currStmt);
        TIntermCase *caseNode = stmtNode.getAsCaseNode();
        if (caseNode)
        {
            if (inCaseList)
            {
                mSink << ", ";
            }
            else
            {
                emitIndentation();
                mSink << "case ";
                inCaseList = true;
            }
            caseNode->traverse(this);

            // Process the next statement.
            currStmt++;
        }
        else
        {
            // The current statement is not a case statement, end the current case list and emit all
            // the code until the next case statement. WGSL requires braces around the case
            // statement's code.
            ASSERT(inCaseList);
            inCaseList = false;
            mSink << ":\n";

            // Count the statements until the next case (or the end of the switch) and emit them as
            // a block. This assumes that the current statement list will never fallthrough to the
            // next case statement.
            size_t nextCaseStmt = currStmt + 1;
            for (;
                 nextCaseStmt < stmtCount && !stmtList.getChildNode(nextCaseStmt)->getAsCaseNode();
                 nextCaseStmt++)
            {
            }
            angle::Span<TIntermNode *> stmtListView(&stmtList.getSequence()->at(currStmt),
                                                    nextCaseStmt - currStmt);
            emitBlock(stmtListView);
            mSink << "\n";

            // Skip to the next case statement.
            currStmt = nextCaseStmt;
        }
    }

    emitCloseBrace();

    return false;
}

bool OutputWGSLTraverser::visitCase(Visit, TIntermCase *caseNode)
{
    // "case" will have been emitted in the visitSwitch() override.

    if (caseNode->hasCondition())
    {
        TIntermTyped *condExpr = caseNode->getCondition();
        condExpr->traverse(this);
    }
    else
    {
        mSink << "default";
    }

    return false;
}

void OutputWGSLTraverser::emitFunctionReturn(const TFunction &func)
{
    const TType &returnType = func.getReturnType();
    if (returnType.getBasicType() == EbtVoid)
    {
        return;
    }
    mSink << " -> ";
    emitType(returnType);
}

// TODO(anglebug.com/42267100): Function overloads are not supported in WGSL, so function names
// should either be emitted mangled or overloaded functions should be renamed in the AST as a
// pre-pass. As of Apr 2024, WGSL function overloads are "not coming soon"
// (https://github.com/gpuweb/gpuweb/issues/876).
void OutputWGSLTraverser::emitFunctionSignature(const TFunction &func)
{
    mSink << "fn ";

    WriteNameOf(mSink, func);
    mSink << "(";

    bool emitComma          = false;
    const size_t paramCount = func.getParamCount();
    for (size_t i = 0; i < paramCount; ++i)
    {
        if (emitComma)
        {
            mSink << ", ";
        }
        emitComma = true;

        const TVariable &param = *func.getParam(i);
        emitFunctionParameter(func, param);
    }

    mSink << ")";

    emitFunctionReturn(func);
}

void OutputWGSLTraverser::emitFunctionParameter(const TFunction &func, const TVariable &param)
{
    // TODO(anglebug.com/42267100): function parameters are immutable and will need to be renamed if
    // they are mutated.
    EmitVariableDeclarationConfig evdConfig;
    evdConfig.isParameter = true;
    emitVariableDeclaration({param.symbolType(), param.name(), param.getType()}, evdConfig);
}

void OutputWGSLTraverser::visitFunctionPrototype(TIntermFunctionPrototype *funcProtoNode)
{
    const TFunction &func = *funcProtoNode->getFunction();

    emitIndentation();
    // TODO(anglebug.com/42267100): output correct signature for main() if main() is declared as a
    // function prototype, or perhaps just emit nothing.
    emitFunctionSignature(func);
}

bool OutputWGSLTraverser::visitFunctionDefinition(Visit, TIntermFunctionDefinition *funcDefNode)
{
    const TFunction &func = *funcDefNode->getFunction();
    TIntermBlock &body    = *funcDefNode->getBody();

    emitIndentation();
    emitFunctionSignature(func);
    mSink << "\n";
    body.traverse(this);

    return false;
}

void OutputWGSLTraverser::emitTextureBuiltin(const TOperator op, const TIntermSequence &args)
{

    ASSERT(BuiltInGroup::IsTexture(op));

    // The index in the GLSL function's argument list of each particular argument, e.g. bias.
    // `bias`, `lod`, `offset`, and `P` (the coordinates) are the common arguments to most texture
    // functions.
    size_t biasIndex   = 0;
    size_t lodIndex    = 0;
    size_t offsetIndex = 0;
    size_t pIndex      = 0;

    size_t dpdxIndex = 0;
    size_t dpdyIndex = 0;

    // TODO(anglebug.com/389145696): These are probably incorrect translations when sampling from
    // integer or unsigned integer samplers. Using texture() with a usampler
    // is similar to using texelFetch(), except wrap modes are respected. Possibly, the correct mip
    // levels are also selected.

    // The name of the equivalent texture function in WGSL.
    ImmutableString wgslFunctionName("");
    // GLSL stuffs 1, 2, or 3 arguments into a single vector. These represent the swizzles necessary
    // for extracting each argument from P to pass to the appropriate WGSL function.
    ImmutableString coordsSwizzle("");
    ImmutableString arrayIndexSwizzle("");
    ImmutableString depthRefSwizzle("");
    // For the projection forms of the texture builtins, the last coordinate will divide the other
    // three. This is just a swizzle for the last coordinate if the builtin call includes
    // projection.
    ImmutableString projectionDivisionSwizzle("");

    ImmutableString wgslTextureVarName("");
    ImmutableString wgslSamplerVarName("");

    constexpr char k2DCoordsSwizzle[] = ".xy";
    constexpr char k3DCoordsSwizzle[] = ".xyz";

    constexpr char kPossibleElems[] = "xyzw";

    // MonomorphizeUnsupportedFunctions() and RewriteStructSamplers() ensure that this is a
    // reference to the global sampler.
    TIntermSymbol *samplerNode = args[0]->getAsSymbolNode();
    // TODO(anglebug.com/389145696): this will fail if it's an array of samplers, which isn't yet
    // handled.
    if (!samplerNode)
    {
        UNIMPLEMENTED();
        mSink << "TODO_UNHANDLED_TEXTURE_FUNCTION()";
        return;
    }
    TBasicType samplerType = samplerNode->getType().getBasicType();
    ASSERT(IsSampler(samplerType));

    bool isProj = false;

    auto setWgslTextureVarName = [&]() {
        wgslTextureVarName =
            BuildConcatenatedImmutableString(kAngleTexturePrefix, samplerNode->getName());
    };

    auto setWgslSamplerVarName = [&]() {
        wgslSamplerVarName =
            BuildConcatenatedImmutableString(kAngleSamplerPrefix, samplerNode->getName());
    };

    auto setTextureSampleFunctionNameFromBias = [&]() {
        // TODO(anglebug.com/389145696): these are incorrect translations in vertex shaders, where
        // they should probably use textureLoad() (and textureDimensions()).
        if (IsShadowSampler(samplerType))
        {
            if (biasIndex != 0)
            {
                // TODO(anglebug.com/389145696): WGSL doesn't support using bias with shadow
                // samplers.
                UNIMPLEMENTED();
                wgslFunctionName = ImmutableString("TODO_CANNOT_USE_BIAS_WITH_SHADOW_SAMPLER");
            }
            else
            {
                wgslFunctionName = ImmutableString("textureSampleCompare");
            }
        }
        else
        {
            if (biasIndex == 0)
            {
                wgslFunctionName = ImmutableString("textureSample");
            }
            else
            {

                wgslFunctionName = ImmutableString("textureSampleBias");
            }
        }
    };

    switch (op)
    {
        case EOpTextureSize:
        {
            lodIndex = 1;
            ASSERT(args.size() == 2);

            wgslFunctionName = ImmutableString("textureDimensions");
            setWgslTextureVarName();
        }
        break;

        case EOpTexelFetchOffset:
        case EOpTexelFetch:
        {
            pIndex   = 1;
            lodIndex = 2;
            if (args.size() == 4)
            {
                offsetIndex = 3;
            }
            ASSERT(args.size() == 3 || args.size() == 4);

            wgslFunctionName = ImmutableString("textureLoad");
            setWgslTextureVarName();
        }
        break;

        // texture() use to be split into texture2D() and textureCube(). WGSL matches GLSL 3.0 and
        // combines them.
        case EOpTextureProj:
        case EOpTexture2DProj:
        case EOpTextureProjBias:
        case EOpTexture2DProjBias:
            isProj = true;
            [[fallthrough]];
        case EOpTexture:
        case EOpTexture2D:
        case EOpTextureCube:
        case EOpTextureBias:
        case EOpTexture2DBias:
        case EOpTextureCubeBias:
        {
            pIndex = 1;
            if (args.size() == 3)
            {
                biasIndex = 2;
            }
            ASSERT(args.size() == 2 || args.size() == 3);

            setTextureSampleFunctionNameFromBias();
            setWgslTextureVarName();
            setWgslSamplerVarName();
        }
        break;

        case EOpTextureProjLod:
        case EOpTextureProjLodOffset:
        case EOpTexture2DProjLodVS:
        case EOpTexture2DProjLodEXTFS:
            isProj = true;
            [[fallthrough]];
        case EOpTextureLod:
        case EOpTexture2DLodVS:
        case EOpTextureCubeLodVS:
        case EOpTexture2DLodEXTFS:
        case EOpTextureCubeLodEXTFS:
        case EOpTextureLodOffset:
        {
            pIndex   = 1;
            lodIndex = 2;
            if (args.size() == 4)
            {
                offsetIndex = 3;
            }
            ASSERT(args.size() == 3 || args.size() == 4);

            if (IsShadowSampler(samplerType))
            {
                // TODO(anglebug.com/389145696): WGSL may not support explicit LOD with shadow
                // samplers. textureSampleCompareLevel() only uses mip level 0.
                UNIMPLEMENTED();
                wgslFunctionName =
                    ImmutableString("TODO_CANNOT_USE_EXPLICIT_LOD_WITH_SHADOW_SAMPLER");
            }
            else
            {
                wgslFunctionName = ImmutableString("textureSampleLevel");
            }
            setWgslTextureVarName();
            setWgslSamplerVarName();
        }
        break;

        case EOpTextureProjOffset:
        case EOpTextureProjOffsetBias:
            isProj = true;
            [[fallthrough]];
        case EOpTextureOffset:
        case EOpTextureOffsetBias:
        {
            pIndex      = 1;
            offsetIndex = 2;
            if (args.size() == 4)
            {
                biasIndex = 3;
            }
            ASSERT(args.size() == 3 || args.size() == 4);

            setTextureSampleFunctionNameFromBias();
            setWgslTextureVarName();
            setWgslSamplerVarName();
        }
        break;

        case EOpTextureProjGrad:
        case EOpTextureProjGradOffset:
            isProj = true;
            [[fallthrough]];
        case EOpTextureGrad:
        case EOpTextureGradOffset:
        {
            pIndex    = 1;
            dpdxIndex = 2;
            dpdyIndex = 3;
            if (args.size() == 5)
            {
                offsetIndex = 4;
            }
            ASSERT(args.size() == 4 || args.size() == 5);

            if (IsShadowSampler(samplerType))
            {
                // TODO(anglebug.com/389145696): WGSL may not support explicit gradients with shadow
                // samplers.
                UNIMPLEMENTED();
                wgslFunctionName =
                    ImmutableString("TODO_CANNOT_USE_EXPLICIT_GRAD_WITH_SHADOW_SAMPLER");
            }
            else
            {
                wgslFunctionName = ImmutableString("textureSampleGrad");
            }
            setWgslTextureVarName();
            setWgslSamplerVarName();
        }
        break;

        default:
            UNIMPLEMENTED();
            mSink << "TODO_UNHANDLED_TEXTURE_FUNCTION()";
            return;
    }

    mSink << wgslFunctionName << "(";

    ASSERT(!wgslTextureVarName.empty());
    mSink << wgslTextureVarName;

    if (!wgslSamplerVarName.empty())
    {
        mSink << ", " << wgslSamplerVarName;

        // If using a projection division, set the swizzle that extracts the last argument from the
        // p vector.
        if (isProj)
        {
            ASSERT(pIndex == 1);
            const uint8_t vecSize = args[pIndex]->getAsTyped()->getNominalSize();
            ASSERT(vecSize == 3 || vecSize == 4);
            projectionDivisionSwizzle =
                BuildConcatenatedImmutableString('.', kPossibleElems[vecSize - 1]);
        }

        // If sampling from an array, set the swizzle that extracts the array layer number from the
        // p vector.
        if (IsSampler2DArray(samplerType))
        {
            arrayIndexSwizzle = ImmutableString(".z");
        }

        // If sampling from a shadow samplers, set the swizzle that extracts the D_ref argument from
        // the p vector.
        if (IsShadowSampler(samplerType))
        {
            size_t elemIndex = 0;
            if (IsSampler2D(samplerType))
            {
                elemIndex = 2;
            }
            else if (IsSampler2DArray(samplerType) || IsSampler3D(samplerType) ||
                     IsSamplerCube(samplerType))
            {
                elemIndex = 3;
            }

            depthRefSwizzle = BuildConcatenatedImmutableString('.', kPossibleElems[elemIndex]);
        }

        // Finally, set the swizzle for extracting coordinates from the p vector.
        if (IsSampler2D(samplerType) || IsSampler2DArray(samplerType))
        {
            coordsSwizzle = ImmutableString(k2DCoordsSwizzle);
        }
        else if (IsSampler3D(samplerType) || IsSamplerCube(samplerType))
        {
            coordsSwizzle = ImmutableString(k3DCoordsSwizzle);
        }
    }

    // TODO(anglebug.com/389145696): traversing the pArg multiple times is an error if it ever
    // contains side effects (e.g. a function call). There is also a problem if this traverses
    // function arguments in a different order, arguments with side effects that effect arguments
    // that come later may be reordered incorrectly. ESSL specs defined function argument evaluation
    // as left-to-right.
    auto traversePArg = [&]() {
        mSink << "(";
        ASSERT(pIndex != 0);
        args[pIndex]->traverse(this);
        mSink << ")";
    };

    auto outputProjectionDivisionIfNecessary = [&]() {
        if (projectionDivisionSwizzle.empty())
        {
            return;
        }
        mSink << " / ";
        traversePArg();
        mSink << projectionDivisionSwizzle;
    };

    // The arguments to the WGSL function always appear in a certain (partial) order, so output them
    // in that order.
    //
    // The order is always
    // - texture
    // - sampler
    // - coordinates
    // - array layer index
    // - depth_ref, bias, explicit level of detail (never appear together)
    // - dfdx
    // - dfdy
    // - offset
    //
    // See the texture builtin functions in the WGSL spec:
    // https://www.w3.org/TR/WGSL/#texture-builtin-functions
    //
    // For example
    // @must_use fn textureSampleLevel(t: texture_2d_array<f32>,
    //                             s: sampler,
    //                             coords: vec2<f32>,
    //                             array_index: A,
    //                             level: f32,
    //                             offset: vec2<i32>) -> vec4<f32>

    if (pIndex != 0)
    {
        mSink << ", ";
        traversePArg();
        mSink << coordsSwizzle;
        outputProjectionDivisionIfNecessary();
    }

    if (!arrayIndexSwizzle.empty())
    {
        mSink << ", i32(";
        traversePArg();
        mSink << arrayIndexSwizzle << ")";
    }

    if (!depthRefSwizzle.empty())
    {
        mSink << ", ";
        traversePArg();
        mSink << depthRefSwizzle;
        outputProjectionDivisionIfNecessary();
    }

    if (biasIndex != 0)
    {
        mSink << ", ";
        args[biasIndex]->traverse(this);
    }

    if (lodIndex != 0)
    {
        mSink << ", ";
        args[lodIndex]->traverse(this);
    }

    if (dpdxIndex != 0)
    {
        mSink << ", ";
        args[dpdxIndex]->traverse(this);
    }

    if (dpdyIndex != 0)
    {
        mSink << ", ";
        args[dpdyIndex]->traverse(this);
    }

    if (offsetIndex != 0)
    {
        mSink << ", ";
        // Both GLSL and WGSL require this to be a const expression.
        args[offsetIndex]->traverse(this);
    }

    mSink << ")";
}

bool OutputWGSLTraverser::visitAggregate(Visit, TIntermAggregate *aggregateNode)
{
    const TIntermSequence &args = *aggregateNode->getSequence();

    auto emitArgList = [&]() {
        mSink << "(";

        bool emitComma = false;
        for (TIntermNode *arg : args)
        {
            if (emitComma)
            {
                mSink << ", ";
            }
            emitComma = true;
            arg->traverse(this);
        }

        mSink << ")";
    };

    const TType &retType = aggregateNode->getType();

    if (aggregateNode->isConstructor())
    {

        emitType(retType);
        emitArgList();

        return false;
    }
    else
    {
        const TOperator op = aggregateNode->getOp();
        switch (op)
        {
            case TOperator::EOpCallFunctionInAST:
                WriteNameOf(mSink, *aggregateNode->getFunction());
                emitArgList();
                return false;

            default:
                // Do not allow raw function calls, i.e. calls to functions
                // not present in the AST.
                ASSERT(op != TOperator::EOpCallInternalRawFunction);
                auto getArgType = [&](size_t index) -> const TType * {
                    if (index < args.size())
                    {
                        TIntermTyped *arg = args[index]->getAsTyped();
                        ASSERT(arg);
                        return &arg->getType();
                    }
                    return nullptr;
                };

                const TType *argType0 = getArgType(0);
                const TType *argType1 = getArgType(1);
                const TType *argType2 = getArgType(2);

                const OperatorInfo opInfo =
                    GetOperatorInfo(op, retType, argType0, argType1, argType2);

                if (opInfo.IsSymbolicOperator())
                {
                    switch (args.size())
                    {
                        case 1:
                        {
                            TIntermNode &operandNode = *aggregateNode->getChildNode(0);
                            if (IsPostfix(op))
                            {
                                mSink << opInfo.opName;
                                groupedTraverse(operandNode);
                            }
                            else
                            {
                                groupedTraverse(operandNode);
                                mSink << opInfo.opName;
                            }
                        }
                        break;

                        case 2:
                        {
                            // symbolic operators with 2 args are emitted with infix notation.
                            TIntermNode &leftNode  = *aggregateNode->getChildNode(0);
                            TIntermNode &rightNode = *aggregateNode->getChildNode(1);
                            groupedTraverse(leftNode);
                            mSink << " " << opInfo.opName << " ";
                            groupedTraverse(rightNode);
                        }
                        break;

                        default:
                            UNREACHABLE();
                            return false;
                    }
                }
                else
                {
                    // Rewrite the calls to sampler functions.
                    if (BuiltInGroup::IsTexture(op))
                    {
                        emitTextureBuiltin(op, args);
                        ASSERT(opInfo.wgslWrapperFn == nullptr);
                        return false;
                    }
                    // If the operator is not symbolic then it is a builtin that uses function call
                    // syntax: builtin(arg1, arg2, ..);
                    mSink << (opInfo.opName == nullptr ? "TODO_Operator" : opInfo.opName);
                    emitArgList();
                }

                if (opInfo.wgslWrapperFn)
                {
                    mSink << ")";
                }

                return false;
        }
    }
}

bool OutputWGSLTraverser::emitBlock(angle::Span<TIntermNode *> nodes)
{
    ASSERT(mIndentLevel >= -1);
    const bool isGlobalScope = mIndentLevel == -1;

    if (isGlobalScope)
    {
        ++mIndentLevel;
    }
    else
    {
        emitOpenBrace();
    }

    TIntermNode *prevStmtNode = nullptr;

    const size_t stmtCount = nodes.size();
    for (size_t i = 0; i < stmtCount; ++i)
    {
        TIntermNode &stmtNode = *nodes[i];

        if (isGlobalScope && prevStmtNode && (NewlinePad(*prevStmtNode) || NewlinePad(stmtNode)))
        {
            mSink << "\n";
        }
        const bool isCase = stmtNode.getAsCaseNode();
        mIndentLevel -= isCase;
        emitIndentation();
        mIndentLevel += isCase;
        stmtNode.traverse(this);
        if (RequiresSemicolonTerminator(stmtNode))
        {
            mSink << ";";
        }
        mSink << "\n";

        prevStmtNode = &stmtNode;
    }

    if (isGlobalScope)
    {
        ASSERT(mIndentLevel == 0);
        --mIndentLevel;
    }
    else
    {
        emitCloseBrace();
    }

    return false;
}

bool OutputWGSLTraverser::visitBlock(Visit, TIntermBlock *blockNode)
{
    return emitBlock(
        angle::Span(blockNode->getSequence()->data(), blockNode->getSequence()->size()));
}

bool OutputWGSLTraverser::visitGlobalQualifierDeclaration(Visit,
                                                          TIntermGlobalQualifierDeclaration *)
{
    return false;
}

void OutputWGSLTraverser::emitStructDeclaration(const TType &type)
{
    ASSERT(type.getBasicType() == TBasicType::EbtStruct && type.isStructSpecifier());

    mSink << "struct ";
    emitBareTypeName(type);

    mSink << "\n";
    emitOpenBrace();

    const TStructure &structure = *type.getStruct();
    bool isInUniformAddressSpace =
        mUniformBlockMetadata->structsInUniformAddressSpace.count(structure.uniqueId().get()) != 0;

    bool alignTo16InUniformAddressSpace = true;
    for (const TField *field : structure.fields())
    {
        const TType *fieldType = field->type();

        emitIndentation();
        // If this struct is used in the uniform address space, it must obey the uniform address
        // space's layout constaints (https://www.w3.org/TR/WGSL/#address-space-layout-constraints).
        // WGSL's address space layout constraints nearly match std140, and the places they don't
        // are handled elsewhere.
        if (isInUniformAddressSpace)
        {
            // Here, the field must be aligned to 16 if:
            // 1. The field is a struct or array (note that matCx2 is represented as an array of
            // vec2)
            // 2. The previous field is a struct
            // 3. The field is the first in the struct (for convenience).
            if (field->type()->getStruct() || fieldType->isArray() || IsMatCx2(fieldType))
            {
                alignTo16InUniformAddressSpace = true;
            }
            if (alignTo16InUniformAddressSpace)
            {
                mSink << "@align(16) ";
            }

            // If this field is a struct, the next member should be aligned to 16.
            alignTo16InUniformAddressSpace = fieldType->getStruct();

            // If the field is an array whose stride is not aligned to 16, the element type must be
            // emitted with a wrapper struct. Record that the wrapper struct needs to be emitted.
            // Note that if the array element type is already of struct type, it doesn't need
            // another wrapper struct, it will automatically be aligned to 16 because its first
            // member is aligned to 16 (implemented above).
            if (ElementTypeNeedsUniformWrapperStruct(/*inUniformAddressSpace=*/true, fieldType))
            {
                TType innerType = *fieldType;
                innerType.toArrayElementType();
                // Multidimensional arrays not currently supported in uniforms in the WebGPU backend
                ASSERT(!innerType.isArray());
                mWGSLGenerationMetadataForUniforms->arrayElementTypesInUniforms.insert(innerType);
            }
        }

        // TODO(anglebug.com/42267100): emit qualifiers.
        EmitVariableDeclarationConfig evdConfig;
        evdConfig.typeConfig.addressSpace =
            isInUniformAddressSpace ? WgslAddressSpace::Uniform : WgslAddressSpace::NonUniform;
        evdConfig.disableStructSpecifier = true;
        emitVariableDeclaration({field->symbolType(), field->name(), *fieldType}, evdConfig);
        mSink << ",\n";
    }

    emitCloseBrace();
}

void OutputWGSLTraverser::emitVariableDeclaration(const VarDecl &decl,
                                                  const EmitVariableDeclarationConfig &evdConfig)
{
    const TBasicType basicType = decl.type.getBasicType();

    if ((decl.type.getQualifier() == EvqUniform || decl.type.getQualifier() == EvqBuffer) &&
        evdConfig.isGlobalScope)
    {
        // Uniforms/interface blocks are declared in a pre-pass, and don't need to be outputted
        // here.
        return;
    }

    if (basicType == TBasicType::EbtStruct && decl.type.isStructSpecifier() &&
        !evdConfig.disableStructSpecifier)
    {
        // TODO(anglebug.com/42267100): in WGSL structs probably can't be declared in
        // function parameters or in uniform declarations or in variable declarations, or
        // anonymously either within other structs or within a variable declaration. Handle
        // these with the same AST pre-passes as other shader translators.
        ASSERT(!evdConfig.isParameter);
        emitStructDeclaration(decl.type);
        if (decl.symbolType != SymbolType::Empty)
        {
            mSink << " ";
            emitNameOf(decl);
        }
        return;
    }

    ASSERT(basicType == TBasicType::EbtStruct || decl.symbolType != SymbolType::Empty ||
           evdConfig.isParameter);

    if (evdConfig.needsVar)
    {
        // "const" and "let" probably don't need to be ever emitted because they are more for
        // readability, and the GLSL compiler constant folds most (all?) the consts anyway.
        mSink << "var";
        // TODO(anglebug.com/42267100): <workgroup> or <storage>?
        if (evdConfig.isGlobalScope)
        {
            if (decl.type.getQualifier() == EvqUniform)
            {
                ASSERT(IsOpaqueType(decl.type.getBasicType()));
                mSink << "<uniform>";
            }
            else
            {
                mSink << "<private>";
            }
        }
        mSink << " ";
    }
    else
    {
        ASSERT(!evdConfig.isGlobalScope);
    }

    if (decl.symbolType != SymbolType::Empty)
    {
        emitNameOf(decl);
    }
    mSink << " : ";
    WriteWgslType(mSink, decl.type, evdConfig.typeConfig);
}

bool OutputWGSLTraverser::visitDeclaration(Visit, TIntermDeclaration *declNode)
{
    ASSERT(declNode->getChildCount() == 1);
    TIntermNode &node = *declNode->getChildNode(0);

    EmitVariableDeclarationConfig evdConfig;
    evdConfig.needsVar      = true;
    evdConfig.isGlobalScope = mIndentLevel == 0;

    if (TIntermSymbol *symbolNode = node.getAsSymbolNode())
    {
        const TVariable &var = symbolNode->variable();
        if (mRewritePipelineVarOutput->IsInputVar(var.uniqueId()) ||
            mRewritePipelineVarOutput->IsOutputVar(var.uniqueId()))
        {
            // Some variables, like shader inputs/outputs/builtins, are declared in the WGSL source
            // outside of the traverser.
            return false;
        }
        emitVariableDeclaration({var.symbolType(), var.name(), var.getType()}, evdConfig);
    }
    else if (TIntermBinary *initNode = node.getAsBinaryNode())
    {
        ASSERT(initNode->getOp() == TOperator::EOpInitialize);
        TIntermSymbol *leftSymbolNode = initNode->getLeft()->getAsSymbolNode();
        TIntermTyped *valueNode       = initNode->getRight()->getAsTyped();
        ASSERT(leftSymbolNode && valueNode);

        const TVariable &var = leftSymbolNode->variable();
        if (mRewritePipelineVarOutput->IsInputVar(var.uniqueId()) ||
            mRewritePipelineVarOutput->IsOutputVar(var.uniqueId()))
        {
            // Some variables, like shader inputs/outputs/builtins, are declared in the WGSL source
            // outside of the traverser.
            return false;
        }

        emitVariableDeclaration({var.symbolType(), var.name(), var.getType()}, evdConfig);
        mSink << " = ";
        groupedTraverse(*valueNode);
    }
    else
    {
        UNREACHABLE();
    }

    return false;
}

bool OutputWGSLTraverser::visitLoop(Visit, TIntermLoop *loopNode)
{
    const TLoopType loopType = loopNode->getType();

    switch (loopType)
    {
        case TLoopType::ELoopFor:
            return emitForLoop(loopNode);
        case TLoopType::ELoopWhile:
            return emitWhileLoop(loopNode);
        case TLoopType::ELoopDoWhile:
            return emulateDoWhileLoop(loopNode);
    }
}

bool OutputWGSLTraverser::emitForLoop(TIntermLoop *loopNode)
{
    ASSERT(loopNode->getType() == TLoopType::ELoopFor);

    TIntermNode *initNode  = loopNode->getInit();
    TIntermTyped *condNode = loopNode->getCondition();
    TIntermTyped *exprNode = loopNode->getExpression();

    mSink << "for (";

    if (initNode)
    {
        initNode->traverse(this);
    }
    else
    {
        mSink << " ";
    }

    mSink << "; ";

    if (condNode)
    {
        condNode->traverse(this);
    }

    mSink << "; ";

    if (exprNode)
    {
        exprNode->traverse(this);
    }

    mSink << ")\n";

    loopNode->getBody()->traverse(this);

    return false;
}

bool OutputWGSLTraverser::emitWhileLoop(TIntermLoop *loopNode)
{
    ASSERT(loopNode->getType() == TLoopType::ELoopWhile);

    TIntermNode *initNode  = loopNode->getInit();
    TIntermTyped *condNode = loopNode->getCondition();
    TIntermTyped *exprNode = loopNode->getExpression();
    ASSERT(condNode);
    ASSERT(!initNode && !exprNode);

    emitIndentation();
    mSink << "while (";
    condNode->traverse(this);
    mSink << ")\n";
    loopNode->getBody()->traverse(this);

    return false;
}

bool OutputWGSLTraverser::emulateDoWhileLoop(TIntermLoop *loopNode)
{
    ASSERT(loopNode->getType() == TLoopType::ELoopDoWhile);

    TIntermNode *initNode  = loopNode->getInit();
    TIntermTyped *condNode = loopNode->getCondition();
    TIntermTyped *exprNode = loopNode->getExpression();
    ASSERT(condNode);
    ASSERT(!initNode && !exprNode);

    emitIndentation();
    // Write an infinite loop.
    mSink << "loop {\n";
    mIndentLevel++;
    loopNode->getBody()->traverse(this);
    mSink << "\n";
    emitIndentation();
    // At the end of the loop, break if the loop condition dos not still hold.
    mSink << "if (!(";
    condNode->traverse(this);
    mSink << ") { break; }\n";
    mIndentLevel--;
    emitIndentation();
    mSink << "}";

    return false;
}

bool OutputWGSLTraverser::visitBranch(Visit, TIntermBranch *branchNode)
{
    const TOperator flowOp = branchNode->getFlowOp();
    TIntermTyped *exprNode = branchNode->getExpression();

    emitIndentation();

    switch (flowOp)
    {
        case TOperator::EOpKill:
        {
            ASSERT(exprNode == nullptr);
            mSink << "discard";
        }
        break;

        case TOperator::EOpReturn:
        {
            mSink << "return";
            if (exprNode)
            {
                mSink << " ";
                exprNode->traverse(this);
            }
        }
        break;

        case TOperator::EOpBreak:
        {
            ASSERT(exprNode == nullptr);
            mSink << "break";
        }
        break;

        case TOperator::EOpContinue:
        {
            ASSERT(exprNode == nullptr);
            mSink << "continue";
        }
        break;

        default:
        {
            UNREACHABLE();
        }
    }

    return false;
}

void OutputWGSLTraverser::visitPreprocessorDirective(TIntermPreprocessorDirective *node)
{
    // No preprocessor directives expected at this point.
    UNREACHABLE();
}

void OutputWGSLTraverser::emitBareTypeName(const TType &type)
{
    WriteWgslBareTypeName(mSink, type, {});
}

void OutputWGSLTraverser::emitType(const TType &type)
{
    WriteWgslType(mSink, type, {});
}

// Unlike Vulkan having auto viewport flipping extension, in WGPU we have to flip gl_Position.y
// manually.
// This operation performs flipping the gl_Position.y using this expression:
// gl_Position.y = gl_Position.y * negViewportScaleY
[[nodiscard]] bool AppendVertexShaderPositionYCorrectionToMain(TCompiler *compiler,
                                                               TIntermBlock *root,
                                                               TSymbolTable *symbolTable,
                                                               TIntermTyped *negFlipY)
{
    // Create a symbol reference to "gl_Position"
    const TVariable *position  = BuiltInVariable::gl_Position();
    TIntermSymbol *positionRef = new TIntermSymbol(position);

    // Create a swizzle to "gl_Position.y"
    TVector<uint32_t> swizzleOffsetY;
    swizzleOffsetY.push_back(1);
    TIntermSwizzle *positionY = new TIntermSwizzle(positionRef, swizzleOffsetY);

    // Create the expression "gl_Position.y * negFlipY"
    TIntermBinary *inverseY = new TIntermBinary(EOpMul, positionY->deepCopy(), negFlipY);

    // Create the assignment "gl_Position.y = gl_Position.y * negViewportScaleY
    TIntermTyped *positionYLHS = positionY->deepCopy();
    TIntermBinary *assignment  = new TIntermBinary(TOperator::EOpAssign, positionYLHS, inverseY);

    // Append the assignment as a statement at the end of the shader.
    return RunAtTheEndOfShader(compiler, root, assignment, symbolTable);
}

}  // namespace

TranslatorWGSL::TranslatorWGSL(sh::GLenum type, ShShaderSpec spec, ShShaderOutput output)
    : TCompiler(type, spec, output)
{}

bool TranslatorWGSL::preTranslateTreeModifications(TIntermBlock *root,
                                                   const TVariable **defaultUniformBlockOut)
{
    if (!RewriteMultielementSwizzleAssignment(this, root))
    {
        return false;
    }

    int aggregateTypesUsedForUniforms = 0;
    for (const auto &uniform : getUniforms())
    {
        if (uniform.isStruct() || uniform.isArrayOfArrays())
        {
            ++aggregateTypesUsedForUniforms;
        }
    }

    // TODO(anglebug.com/42267100): just use the struct mode to avoid a rewrite of the interface
    // block by ReduceInterfaceBlocks into a struct.
    DriverUniform driverUniforms(DriverUniformMode::InterfaceBlock);
    ASSERT(getShaderType() != GL_COMPUTE_SHADER);
    driverUniforms.addGraphicsDriverUniformsToShader(root, &getSymbolTable());

    if (getShaderType() == GL_VERTEX_SHADER)
    {
        TIntermTyped *flipNegY =
            driverUniforms.getFlipXY(&getSymbolTable(), DriverUniformFlip::PreFragment);
        flipNegY = (new TIntermSwizzle(flipNegY, {1}))->fold(nullptr);

        if (!AppendVertexShaderPositionYCorrectionToMain(this, root, &getSymbolTable(), flipNegY))
        {
            return false;
        }
    }

    // Samplers are legal as function parameters, but samplers within structs or arrays are not
    // allowed in WGSL
    // (https://www.w3.org/TR/WGSL/#function-call-expr:~:text=A%20function%20parameter,a%20sampler%20type).
    // TODO(anglebug.com/389145696): handle arrays of samplers here.

    // If there are any function calls that take array-of-array of opaque uniform parameters, or
    // other opaque uniforms that need special handling in WebGPU, monomorphize the functions by
    // removing said parameters and replacing them in the function body with the call arguments.
    //
    // This  dramatically simplifies future transformations w.r.t to samplers in structs, array of
    //   arrays of opaque types, atomic counters etc.
    UnsupportedFunctionArgsBitSet args{UnsupportedFunctionArgs::StructContainingSamplers,
                                       UnsupportedFunctionArgs::ArrayOfArrayOfSamplerOrImage,
                                       UnsupportedFunctionArgs::AtomicCounter,
                                       UnsupportedFunctionArgs::Image};
    if (!MonomorphizeUnsupportedFunctions(this, root, &getSymbolTable(), args))
    {
        return false;
    }

    if (aggregateTypesUsedForUniforms > 0)
    {
        if (!SeparateStructFromUniformDeclarations(this, root, &getSymbolTable()))
        {
            return false;
        }

        int removedUniformsCount;

        // Requires MonomorphizeUnsupportedFunctions() to have been run already.
        if (!RewriteStructSamplers(this, root, &getSymbolTable(), &removedUniformsCount))
        {
            return false;
        }
    }

    // Replace array of array of opaque uniforms with a flattened array.  This is run after
    // MonomorphizeUnsupportedFunctions and RewriteStructSamplers so that it's not possible for an
    // array of array of opaque type to be partially subscripted and passed to a function.
    // TODO(anglebug.com/389145696): Even single-level arrays of samplers are not allowed in WGSL.
    if (!RewriteArrayOfArrayOfOpaqueUniforms(this, root, &getSymbolTable()))
    {
        return false;
    }

    // RewriteStructSamplers should have already run at this point so there are not default
    // uniforms containing samplers, even within a nested struct.
    gl::ShaderType packedShaderType = gl::FromGLenum<gl::ShaderType>(getShaderType());
    if (!GatherDefaultUniforms(this, root, &getSymbolTable(), packedShaderType,
                               ImmutableString(kDefaultUniformBlockVarType),
                               ImmutableString(kDefaultUniformBlockVarName),
                               defaultUniformBlockOut))
    {
        return false;
    }

    // Note: It would be possible to to avoid running this AST modification by outputting
    // interface blocks like structs, with the wrinkle that interface blocks don't need an instance
    // variable name and so this translator would have to generate a new one and keep a map of
    // TInterfaceBlock -> WGSLName in order to output field accesses of the interface block.
    int uniqueStructId = 0;
    if (!ReduceInterfaceBlocks(*this, *root, [&uniqueStructId]() -> ImmutableString {
            return BuildConcatenatedImmutableString("ANGLE_unnamed_interface_block_",
                                                    uniqueStructId++);
        }))
    {
        return false;
    }
    return true;
}

bool TranslatorWGSL::translate(TIntermBlock *root,
                               const ShCompileOptions &compileOptions,
                               PerformanceDiagnostics *perfDiagnostics)
{
    if (kOutputTreeBeforeTranslation)
    {
        TInfoSinkBase treeOut;
        std::cout << "Initial tree for shader type "
                  << (getShaderType() == GL_VERTEX_SHADER     ? "vertex shader"
                      : getShaderType() == GL_FRAGMENT_SHADER ? "fragment shader "
                                                              : "unknown")
                  << std::endl;
        OutputTree(root, treeOut);
        std::cout << treeOut.c_str();
    }

    const TVariable *defaultUniformBlock = nullptr;

    if (!preTranslateTreeModifications(root, &defaultUniformBlock))
    {
        return false;
    }

    if (kOutputTreeBeforeTranslation)
    {
        TInfoSinkBase treeOut;
        std::cout << "After preTranslateTreeModifications(): " << std::endl;
        getInfoSink().info.erase();
        OutputTree(root, treeOut);
        std::cout << treeOut.c_str();
    }
    enableValidateNoMoreTransformations();

    RewritePipelineVarOutput rewritePipelineVarOutput(getShaderType());
    WGSLGenerationMetadataForUniforms wgslGenerationMetadataForUniforms;

    // WGSL's main() will need to take parameters or return values if any glsl (input/output)
    // builtin variables are used.
    if (!GenerateMainFunctionAndIOStructs(*this, *root, rewritePipelineVarOutput))
    {
        ANGLE_LOG(ERR) << "Failed to generate WGSL main functions";
        return false;
    }

    TInfoSinkBase &sink = getInfoSink().obj;

    // GLSL allows derivatives to be calculated as long as control flow is dynamically uniform. WGSL
    // triggers a derivative_uniformity diagnostic whenever it cannot statically determine that
    // control flow is uniform, which is by default an error. Since this compiler must implement
    // GLSL semantics, use a global diagnostic filter to turn derivative_uniformity diagnostics into
    // warnings instead of the default error.
    // See https://github.com/gpuweb/gpuweb/issues/3479 and the spec:
    // https://www.w3.org/TR/WGSL/#uniformity
    sink << "diagnostic(warning,derivative_uniformity);\n";

    // Start writing the output structs that will be referred to by the `traverser`'s output.'
    if (!rewritePipelineVarOutput.OutputStructs(sink))
    {
        ANGLE_LOG(ERR) << "Failed to output pipeline structs";
        return false;
    }

    if (!OutputUniformBlocksAndSamplers(this, root, defaultUniformBlock))
    {
        ANGLE_LOG(ERR) << "Failed to output uniform blocks and samplers";
        return false;
    }

    UniformBlockMetadata uniformBlockMetadata;
    if (!RecordUniformBlockMetadata(root, uniformBlockMetadata))
    {
        ANGLE_LOG(ERR) << "Failed to record uniform block metadata";
        return false;
    }

    // Generate the body of the WGSL including the GLSL main() function.
    TInfoSinkBase traverserOutput;
    OutputWGSLTraverser traverser(&traverserOutput, &rewritePipelineVarOutput,
                                  &uniformBlockMetadata, &wgslGenerationMetadataForUniforms);
    root->traverse(&traverser);

    sink << "\n";
    OutputUniformWrapperStructsAndConversions(sink, wgslGenerationMetadataForUniforms);

    // The traverser output needs to be in the code after uniform wrapper structs are emitted above,
    // since the traverser code references the wrapper struct types.
    sink << traverserOutput.str();

    // Write the actual WGSL main function, wgslMain(), which calls the GLSL main function.
    if (!rewritePipelineVarOutput.OutputMainFunction(sink))
    {
        ANGLE_LOG(ERR) << "Failed to output WGSL main function";
        return false;
    }

    if (kOutputTranslatedShader)
    {
        std::cout << sink.str();
    }

    return true;
}

bool TranslatorWGSL::shouldFlattenPragmaStdglInvariantAll()
{
    // Not neccesary for WGSL transformation.
    return false;
}
}  // namespace sh
