// slang-check-conversion.cpp
#include "slang-ast-synthesis.h"
#include "slang-check-impl.h"

// This file contains semantic-checking logic for dealing
// with conversion (both implicit and explicit) of expressions
// from one type to another.
//
// Type conversion is also the point at which a C-style initializer
// list (e.g., `float4 a = { 1, 2, 3, 4 };`) is validated against
// the desired type, so this file also contains all of the logic
// associated with validating initializer lists.

namespace Slang
{
ConversionCost SemanticsVisitor::getImplicitConversionCost(Decl* decl)
{
    if (auto modifier = decl->findModifier<ImplicitConversionModifier>())
    {
        return modifier->cost;
    }

    return kConversionCost_Explicit;
}

BuiltinConversionKind SemanticsVisitor::getImplicitConversionBuiltinKind(Decl* decl)
{
    if (auto modifier = decl->findModifier<ImplicitConversionModifier>())
    {
        return modifier->builtinConversionKind;
    }

    return kBuiltinConversion_Unknown;
}

bool SemanticsVisitor::isEffectivelyScalarForInitializerLists(Type* type)
{
    if (as<CoopVectorExpressionType>(type))
        return false;
    if (as<ArrayExpressionType>(type))
        return false;
    if (as<VectorExpressionType>(type))
        return false;
    if (as<MatrixExpressionType>(type))
        return false;

    if (as<BasicExpressionType>(type))
    {
        return true;
    }

    if (as<ResourceType>(type))
    {
        return true;
    }
    if (as<UntypedBufferResourceType>(type))
    {
        return true;
    }
    if (as<SamplerStateType>(type))
    {
        return true;
    }

    if (auto declRefType = as<DeclRefType>(type))
    {
        if (as<StructDecl>(declRefType->getDeclRef()))
            return false;
    }

    return true;
}

bool SemanticsVisitor::shouldUseInitializerDirectly(Type* toType, Expr* fromExpr)
{
    // A nested initializer list should always be used directly.
    //
    if (as<InitializerListExpr>(fromExpr))
    {
        return true;
    }

    // If the desired type is a scalar, then we should always initialize
    // directly, since it isn't an aggregate.
    //
    if (isEffectivelyScalarForInitializerLists(toType))
        return true;

    // If the type we are initializing isn't effectively scalar,
    // but the initialization expression *is*, then it doesn't
    // seem like direct initialization is intended.
    //
    if (isEffectivelyScalarForInitializerLists(fromExpr->type))
        return false;

    // Once the above cases are handled, the main thing
    // we want to check for is whether a direct initialization
    // is possible (a type conversion exists).
    //
    ConversionCost cost;
    if (canCoerce(toType, fromExpr->type, fromExpr, &cost))
    {
        if (cost >= kConversionCost_Explicit)
        {
            return false;
        }
        return true;
    }

    return false;
}

bool SemanticsVisitor::_readValueFromInitializerList(
    Type* toType,
    Expr** outToExpr,
    InitializerListExpr* fromInitializerListExpr,
    UInt& ioInitArgIndex)
{
    // First, we will check if we have run out of arguments
    // on the initializer list.
    //
    UInt initArgCount = fromInitializerListExpr->args.getCount();
    if (ioInitArgIndex >= initArgCount)
    {
        // If we are at the end of the initializer list,
        // then our ability to read an argument depends
        // on whether the type we are trying to read
        // is default-initializable.
        //
        // For now, we will just pretend like everything
        // is default-initializable and move along.
        return true;
    }

    // Okay, we have at least one initializer list expression,
    // so we will look at the next expression and decide
    // whether to use it to initialize the desired type
    // directly (possibly via casts), or as the first sub-expression
    // for aggregate initialization.
    //
    auto firstInitExpr = fromInitializerListExpr->args[ioInitArgIndex];
    if (shouldUseInitializerDirectly(toType, firstInitExpr))
    {
        ioInitArgIndex++;
        return _coerce(
            CoercionSite::Initializer,
            toType,
            outToExpr,
            firstInitExpr->type,
            firstInitExpr,
            getSink(),
            nullptr);
    }

    // If there is somehow an error in one of the initialization
    // expressions, then everything could be thrown off and we
    // shouldn't keep trying to read arguments.
    //
    if (IsErrorExpr(firstInitExpr))
    {
        // Stop reading arguments, as if we'd reached
        // the end of the list.
        //
        ioInitArgIndex = initArgCount;
        return true;
    }

    // The fallback case is to recursively read the
    // type from the same list as an aggregate.
    //
    return _readAggregateValueFromInitializerList(
        toType,
        outToExpr,
        fromInitializerListExpr,
        ioInitArgIndex);
}

DeclRefType* findBaseStructType(ASTBuilder* astBuilder, DeclRef<StructDecl> structTypeDeclRef)
{
    auto inheritanceDecl =
        getMembersOfType<InheritanceDecl>(astBuilder, structTypeDeclRef).getFirstOrNull();
    if (!inheritanceDecl)
        return nullptr;

    auto baseType = getBaseType(astBuilder, inheritanceDecl);
    auto baseDeclRefType = as<DeclRefType>(baseType);
    if (!baseDeclRefType)
        return nullptr;

    auto baseDeclRef = baseDeclRefType->getDeclRef();
    auto baseStructDeclRef = baseDeclRef.as<StructDecl>();
    if (!baseStructDeclRef)
        return nullptr;

    return baseDeclRefType;
}

DeclRef<StructDecl> findBaseStructDeclRef(
    ASTBuilder* astBuilder,
    DeclRef<StructDecl> structTypeDeclRef)
{
    auto inheritanceDecl =
        getMembersOfType<InheritanceDecl>(astBuilder, structTypeDeclRef).getFirstOrNull();
    if (!inheritanceDecl)
        return DeclRef<StructDecl>();

    auto baseType = getBaseType(astBuilder, inheritanceDecl);
    auto baseDeclRefType = as<DeclRefType>(baseType);
    if (!baseDeclRefType)
        return DeclRef<StructDecl>();

    auto baseDeclRef = baseDeclRefType->getDeclRef();
    auto baseStructDeclRef = baseDeclRef.as<StructDecl>();
    if (!baseStructDeclRef)
        return DeclRef<StructDecl>();

    return baseStructDeclRef;
}

ConstructorDecl* SemanticsVisitor::_getSynthesizedConstructor(
    StructDecl* structDecl,
    ConstructorDecl::ConstructorFlavor flavor)
{
    for (auto ctor : structDecl->getMembersOfType<ConstructorDecl>())
    {
        if (ctor->containsFlavor(flavor))
            return ctor;
    }
    return nullptr;
}

bool SemanticsVisitor::isCStyleType(Type* type, HashSet<Type*>& isVisit)
{
    isVisit.add(type);
    auto cacheResult = [&](bool result)
    {
        getShared()->cacheCStyleType(type, result);
        return result;
    };

    // Check cache first
    if (bool* isCStyle = getShared()->isCStyleType(type))
    {
        return *isCStyle;
    }

    // 1. It has to be basic scalar, vector or matrix type, or user-defined struct.
    if (as<VectorExpressionType>(type) || as<MatrixExpressionType>(type) ||
        as<BasicExpressionType>(type) || isDeclRefTypeOf<EnumDecl>(type).getDecl() ||
        as<PtrType>(type))
        return cacheResult(true);

    // Slang 2026 language fix: an interface type is not C-style.
    if (isSlang2026OrLater(this))
    {
        // TODO: some/dyn types are also not C-style.
        if (isDeclRefTypeOf<InterfaceDecl>(type))
            return cacheResult(false);
    }

    // Opaque types are not C-style.
    // These types have no well-defined default value (they usually need a separate API call to be
    // created and/or bound) and thus cannot be default-initialized.
    {
        TypeTag tags = getTypeTags(type);
        const bool isOpaque = ((int)tags & (int)TypeTag::Opaque) != 0;
        if (isOpaque)
            return cacheResult(false);
    }

    // A tuple type is C-style if all of its members are C-style.
    if (auto tupleType = as<TupleType>(type))
    {
        for (Index i = 0; i < tupleType->getMemberCount(); i++)
        {
            auto elementType = tupleType->getMember(i);
            // Avoid infinite loop in case of circular reference.
            if (isVisit.contains(elementType))
                continue;
            if (!isCStyleType(elementType, isVisit))
                return cacheResult(false);
        }
        return cacheResult(true);
    }

    if (auto structDecl = isDeclRefTypeOf<StructDecl>(type).getDecl())
    {
        // 2. It cannot have inheritance, but inherit from interface is fine.
        for (auto inheritanceDecl : structDecl->getMembersOfType<InheritanceDecl>())
        {
            if (!isDeclRefTypeOf<InterfaceDecl>(inheritanceDecl->base.type))
            {
                return cacheResult(false);
            }
        }

        // 3. It cannot have explicit constructor
        if (_hasExplicitConstructor(structDecl, true))
            return cacheResult(false);

        // 4. All of its members have to have the same visibility as the struct itself.
        DeclVisibility structVisibility = getDeclVisibility(structDecl);
        for (auto varDecl : structDecl->getMembersOfType<VarDeclBase>())
        {
            if (getDeclVisibility(varDecl) != structVisibility)
            {
                return cacheResult(false);
            }
        }

        for (auto varDecl : structDecl->getMembersOfType<VarDeclBase>())
        {
            Type* varType = varDecl->getType();

            if (isDeclRefTypeOf<StructDecl>(varType))
            {
                // Avoid infinite loop in case of circular reference.
                if (isVisit.contains(varType))
                    continue;
            }

            // Recursively check the type of the member.
            if (!isCStyleType(varType, isVisit))
                return cacheResult(false);
        }
    }

    // 5. All its members are legacy C-Style structs or arrays of legacy C-style structs
    if (auto arrayType = as<ArrayExpressionType>(type))
    {
        if (arrayType->isUnsized())
        {
            return cacheResult(false);
        }

        auto elementType = arrayType->getElementType();
        if (isDeclRefTypeOf<StructDecl>(elementType))
        {
            // Avoid infinite loop in case of circular reference.
            if (isVisit.contains(elementType))
                cacheResult(true);
        }

        if (!isCStyleType(elementType, isVisit))
            return cacheResult(false);
    }

    return cacheResult(true);
}

Expr* SemanticsVisitor::_createCtorInvokeExpr(
    Type* toType,
    const SourceLoc& loc,
    const List<Expr*>& coercedArgs)
{
    auto* varExpr = getASTBuilder()->create<VarExpr>();
    varExpr->type = (QualType)getASTBuilder()->getTypeType(toType);
    varExpr->declRef = isDeclRefTypeOf<Decl>(toType);
    varExpr->loc = loc;
    varExpr->checked = true;

    auto* constructorExpr = getASTBuilder()->create<ExplicitCtorInvokeExpr>();
    constructorExpr->functionExpr = varExpr;
    constructorExpr->arguments.addRange(coercedArgs);
    constructorExpr->loc = loc;

    return constructorExpr;
}

// translation from initializer list to constructor invocation if the struct has constructor.
bool SemanticsVisitor::createInvokeExprForExplicitCtor(
    Type* toType,
    InitializerListExpr* fromInitializerListExpr,
    Expr** outExpr)
{
    if (auto toStructDeclRef = isDeclRefTypeOf<StructDecl>(toType))
    {
        // TODO: This is just a special case for a backwards-compatibility feature
        // for HLSL, this flag will imply that the initializer list is synthesized
        // for a type cast from a literal zero to a 'struct'. In this case, we will fall
        // back to legacy initializer list logic.
        if (!fromInitializerListExpr->useCStyleInitialization)
        {
            HashSet<Type*> isVisit;
            if (!isCStyleType(toType, isVisit))
                return false;
        }

        if (_hasExplicitConstructor(toStructDeclRef.getDecl(), false))
        {
            auto ctorInvokeExpr = _createCtorInvokeExpr(
                toType,
                fromInitializerListExpr->loc,
                fromInitializerListExpr->args);

            DiagnosticSink tempSink(getSourceManager(), nullptr);
            tempSink.setFlags(getSink()->getFlags());

            SemanticsVisitor subVisitor(withSink(&tempSink));
            ctorInvokeExpr = subVisitor.CheckTerm(ctorInvokeExpr);

            if (tempSink.getErrorCount())
            {
                HashSet<Type*> isVisit;
                if (!isCStyleType(toType, isVisit))
                {
                    Slang::ComPtr<ISlangBlob> blob;
                    tempSink.getBlobIfNeeded(blob.writeRef());
                    getSink()->diagnoseRaw(
                        Severity::Error,
                        static_cast<char const*>(blob->getBufferPointer()));
                    // For non-c-style types, we will always return true when there
                    // is a ctor, so that we do not fallback to legacy initializer list logic
                    // in `_coerceInitializerList()` and produce unrelated errors.
                    if (outExpr)
                        *outExpr = CreateErrorExpr(ctorInvokeExpr);
                    return true;
                }
                return false;
            }

            if (outExpr)
            {
                *outExpr = ctorInvokeExpr;
                return true;
            }
        }
    }
    return false;
}

bool SemanticsVisitor::createInvokeExprForSynthesizedCtor(
    Type* toType,
    InitializerListExpr* fromInitializerListExpr,
    Expr** outExpr)
{
    StructDecl* structDecl = isDeclRefTypeOf<StructDecl>(toType).getDecl();

    if (!structDecl)
        return false;

    if (!structDecl->checkState.isBeingChecked())
        ensureDecl(structDecl, DeclCheckState::AttributesChecked);

    HashSet<Type*> isVisit;
    bool isCStyle = false;
    if (!_getSynthesizedConstructor(
            structDecl,
            ConstructorDecl::ConstructorFlavor::SynthesizedDefault))
    {
        // When a struct has no constructor and it's not a C-style type, the initializer list is
        // invalid.
        isCStyle = isCStyleType(toType, isVisit);

        // WAR: We currently still have to allow legacy initializer list for array type until we
        // have a more proper solution for array initialization, so if the right hand side is an
        // array type, we will not report an error and fall-back to legacy initializer list logic.
        bool isArrayType = as<ArrayExpressionType>(toType) != nullptr;
        if (!isCStyle && !isArrayType)
        {
            diagnoseOnce(
                fromInitializerListExpr->loc,
                Diagnostics::cannotUseInitializerListForType,
                toType);
        }

        return false;
    }

    isCStyle = isCStyleType(toType, isVisit);
    // TODO: This is just a special case for a backwards-compatibility feature
    // for HLSL, this flag will imply that the initializer list is synthesized
    // for a type cast from a literal zero to a 'struct'. In this case, we will fall
    // back to legacy initializer list logic.
    if (!fromInitializerListExpr->useCStyleInitialization)
    {
        if (isCStyle)
            return false;
    }

    DiagnosticSink tempSink(getSourceManager(), nullptr);
    SemanticsVisitor subVisitor(withSink(&tempSink));

    // First make sure the struct is fully checked, otherwise the synthesized constructor may not be
    // created yet.
    subVisitor.ensureDecl(structDecl, DeclCheckState::DefinitionChecked);

    List<Expr*> coercedArgs;
    auto ctorInvokeExpr =
        _createCtorInvokeExpr(toType, fromInitializerListExpr->loc, fromInitializerListExpr->args);

    ctorInvokeExpr = subVisitor.CheckExpr(ctorInvokeExpr);

    if (ctorInvokeExpr)
    {
        if (!tempSink.getErrorCount())
        {
            if (outExpr)
                *outExpr = ctorInvokeExpr;

            return true;
        }
        else if (!isCStyle)
        {
            Slang::ComPtr<ISlangBlob> blob;
            tempSink.getBlobIfNeeded(blob.writeRef());
            getSink()->diagnoseRaw(
                Severity::Error,
                static_cast<char const*>(blob->getBufferPointer()));
            return false;
        }
    }
    return false;
}

bool SemanticsVisitor::_readAggregateValueFromInitializerList(
    Type* inToType,
    Expr** outToExpr,
    InitializerListExpr* fromInitializerListExpr,
    UInt& ioArgIndex)
{
    auto toType = inToType;
    UInt argCount = fromInitializerListExpr->args.getCount();

    // In the case where we need to build a result expression,
    // we will collect the new arguments here
    List<Expr*> coercedArgs;

    if (isEffectivelyScalarForInitializerLists(toType))
    {
        // For any type that is effectively a non-aggregate,
        // we expect to read a single value from the initializer list
        //
        if (ioArgIndex < argCount)
        {
            auto arg = fromInitializerListExpr->args[ioArgIndex++];
            return _coerce(
                CoercionSite::Initializer,
                toType,
                outToExpr,
                arg->type,
                arg,
                getSink(),
                nullptr);
        }
        else
        {
            // If there wasn't an initialization
            // expression to be found, then we need
            // to perform default initialization here.
            //
            // We will let this case come through the front-end
            // as an `InitializerListExpr` with zero arguments,
            // and then have the IR generation logic deal with
            // synthesizing default values.
        }
    }
    else if (auto toVecType = as<VectorExpressionType>(toType))
    {
        auto toElementCount = toVecType->getElementCount();
        auto toElementType = toVecType->getElementType();

        UInt elementCount = 0;
        if (auto constElementCount = as<ConstantIntVal>(toElementCount))
        {
            elementCount = (UInt)constElementCount->getValue();
        }
        else
        {
            auto isLinkTimeVal =
                as<TypeCastIntVal>(toElementCount) || as<DeclRefIntVal>(toElementCount) ||
                as<PolynomialIntVal>(toElementCount) || as<FuncCallIntVal>(toElementCount);
            if (isLinkTimeVal)
            {
                auto defaultConstructExpr = m_astBuilder->create<DefaultConstructExpr>();
                defaultConstructExpr->loc = fromInitializerListExpr->loc;
                defaultConstructExpr->type = QualType(toType);

                *outToExpr = defaultConstructExpr;
                return true;
            }

            // We don't know the element count statically,
            // so what are we supposed to be doing?
            //
            if (outToExpr)
            {
                getSink()->diagnose(
                    fromInitializerListExpr,
                    Diagnostics::cannotUseInitializerListForVectorOfUnknownSize,
                    toElementCount);
            }
            return false;
        }

        for (UInt ee = 0; ee < elementCount; ++ee)
        {
            Expr* coercedArg = nullptr;
            bool argResult = _readValueFromInitializerList(
                toElementType,
                outToExpr ? &coercedArg : nullptr,
                fromInitializerListExpr,
                ioArgIndex);

            // No point in trying further if any argument fails
            if (!argResult)
                return false;

            if (coercedArg)
            {
                coercedArgs.add(coercedArg);
            }
        }
    }
    else if (auto toCoopVectorType = as<CoopVectorExpressionType>(toType))
    {
        auto toElementCount = toCoopVectorType->getElementCount();
        auto toElementType = toCoopVectorType->getElementType();

        UInt elementCount = 0;
        if (auto constElementCount = as<ConstantIntVal>(toElementCount))
        {
            elementCount = (UInt)constElementCount->getValue();
        }
        else
        {
            // We don't know the element count statically,
            // so what are we supposed to be doing?
            //
            if (outToExpr)
            {
                getSink()->diagnose(
                    fromInitializerListExpr,
                    Diagnostics::cannotUseInitializerListForCoopVectorOfUnknownSize,
                    toElementCount);
            }
            return false;
        }

        for (UInt ee = 0; ee < elementCount; ++ee)
        {
            Expr* coercedArg = nullptr;
            bool argResult = _readValueFromInitializerList(
                toElementType,
                outToExpr ? &coercedArg : nullptr,
                fromInitializerListExpr,
                ioArgIndex);

            // No point in trying further if any argument fails
            if (!argResult)
                return false;

            if (coercedArg)
            {
                coercedArgs.add(coercedArg);
            }
        }
    }
    else if (auto toArrayType = as<ArrayExpressionType>(toType))
    {
        // TODO(tfoley): If we can compute the size of the array statically,
        // then we want to check that there aren't too many initializers present

        auto toElementType = toArrayType->getElementType();
        if (!toArrayType->isUnsized())
        {
            auto toElementCount = toArrayType->getElementCount();

            // In the case of a sized array, we need to check that the number
            // of elements being initialized matches what was declared.
            //
            UInt elementCount = 0;
            if (auto constElementCount = as<ConstantIntVal>(toElementCount))
            {
                elementCount = (UInt)constElementCount->getValue();
            }
            else
            {
                auto toMakeArrayFromElementExpr = m_astBuilder->create<MakeArrayFromElementExpr>();
                toMakeArrayFromElementExpr->loc = fromInitializerListExpr->loc;
                toMakeArrayFromElementExpr->type = QualType(toType);
                if (outToExpr)
                    *outToExpr = toMakeArrayFromElementExpr;
                return true;
            }
            for (UInt ee = 0; ee < elementCount; ++ee)
            {
                Expr* coercedArg = nullptr;
                bool argResult = _readValueFromInitializerList(
                    toElementType,
                    outToExpr ? &coercedArg : nullptr,
                    fromInitializerListExpr,
                    ioArgIndex);

                // No point in trying further if any argument fails
                if (!argResult)
                    return false;

                if (coercedArg)
                {
                    coercedArgs.add(coercedArg);
                }
            }
        }
        else
        {
            // In the case of an unsized array type, we will use the
            // number of arguments to the initializer to determine
            // the element count.
            //
            UInt elementCount = 0;
            while (ioArgIndex < argCount)
            {
                Expr* coercedArg = nullptr;
                bool argResult = _readValueFromInitializerList(
                    toElementType,
                    outToExpr ? &coercedArg : nullptr,
                    fromInitializerListExpr,
                    ioArgIndex);

                // No point in trying further if any argument fails
                if (!argResult)
                    return false;

                elementCount++;

                if (coercedArg)
                {
                    coercedArgs.add(coercedArg);
                }
            }

            // We have a new type for the conversion, based on what
            // we learned.
            toType = m_astBuilder->getArrayType(
                toElementType,
                m_astBuilder->getIntVal(m_astBuilder->getIntType(), elementCount));
        }
    }
    else if (auto toMatrixType = as<MatrixExpressionType>(toType))
    {
        // In the general case, the initializer list might comprise
        // both vectors and scalars.
        //
        // The traditional HLSL compilers treat any vectors in
        // the initializer list exactly equivalent to their sequence
        // of scalar elements, and don't care how this might, or
        // might not, align with the rows of the matrix.
        //
        // We will draw a line in the sand and say that an initializer
        // list for a matrix will act as if the matrix type were an
        // array of vectors for the rows.


        UInt rowCount = 0;
        auto toRowType =
            createVectorType(toMatrixType->getElementType(), toMatrixType->getColumnCount());

        auto rowCountIntVal = toMatrixType->getRowCount();
        if (auto constRowCount = as<ConstantIntVal>(rowCountIntVal))
        {
            rowCount = (UInt)constRowCount->getValue();
        }
        else
        {
            auto isLinkTimeVal =
                as<TypeCastIntVal>(rowCountIntVal) || as<DeclRefIntVal>(rowCountIntVal) ||
                as<PolynomialIntVal>(rowCountIntVal) || as<FuncCallIntVal>(rowCountIntVal);
            if (isLinkTimeVal)
            {
                auto defaultConstructExpr = m_astBuilder->create<DefaultConstructExpr>();
                defaultConstructExpr->loc = fromInitializerListExpr->loc;
                defaultConstructExpr->type = QualType(toType);
                if (outToExpr)
                    *outToExpr = defaultConstructExpr;
                return true;
            }

            // We don't know the element count statically,
            // so what are we supposed to be doing?
            //
            if (outToExpr)
            {
                getSink()->diagnose(
                    fromInitializerListExpr,
                    Diagnostics::cannotUseInitializerListForMatrixOfUnknownSize,
                    toMatrixType->getRowCount());
            }
            return false;
        }

        for (UInt rr = 0; rr < rowCount; ++rr)
        {
            Expr* coercedArg = nullptr;
            bool argResult = _readValueFromInitializerList(
                toRowType,
                outToExpr ? &coercedArg : nullptr,
                fromInitializerListExpr,
                ioArgIndex);

            // No point in trying further if any argument fails
            if (!argResult)
                return false;

            if (coercedArg)
            {
                coercedArgs.add(coercedArg);
            }
        }
    }
    else if (auto tupleType = as<TupleType>(toType))
    {
        for (Index ee = 0; ee < tupleType->getMemberCount(); ++ee)
        {
            auto elementType = tupleType->getMember(ee);
            Expr* coercedArg = nullptr;
            bool argResult = _readValueFromInitializerList(
                elementType,
                outToExpr ? &coercedArg : nullptr,
                fromInitializerListExpr,
                ioArgIndex);

            // No point in trying further if any argument fails
            if (!argResult)
                return false;

            if (coercedArg)
            {
                coercedArgs.add(coercedArg);
            }
        }
    }
    else if (auto toDeclRefType = as<DeclRefType>(toType))
    {
        auto toTypeDeclRef = toDeclRefType->getDeclRef();
        if (auto toStructDeclRef = toTypeDeclRef.as<StructDecl>())
        {
            // Trying to initialize a `struct` type given an initializer list.
            //
            // Before we iterate over the fields, we want to check if this struct
            // inherits from another `struct` type. If so, we want to read
            // an initializer for that base type first.
            //
            if (auto baseStructType = findBaseStructType(m_astBuilder, toStructDeclRef))
            {
                Expr* coercedArg = nullptr;
                bool argResult = _readValueFromInitializerList(
                    baseStructType,
                    outToExpr ? &coercedArg : nullptr,
                    fromInitializerListExpr,
                    ioArgIndex);

                // No point in trying further if any argument fails
                if (!argResult)
                    return false;

                if (coercedArg)
                {
                    coercedArgs.add(coercedArg);
                }
            }

            // We will go through the fields in order and try to match them
            // up with initializer arguments.
            //
            for (auto fieldDeclRef : getMembersOfType<VarDecl>(
                     m_astBuilder,
                     toStructDeclRef,
                     MemberFilterStyle::Instance))
            {
                Expr* coercedArg = nullptr;
                bool argResult = _readValueFromInitializerList(
                    getType(m_astBuilder, fieldDeclRef),
                    outToExpr ? &coercedArg : nullptr,
                    fromInitializerListExpr,
                    ioArgIndex);

                // No point in trying further if any argument fails
                if (!argResult)
                    return false;

                if (coercedArg)
                {
                    coercedArgs.add(coercedArg);
                }
            }
        }
    }
    else
    {
        // We shouldn't get to this case in practice,
        // but just in case we'll consider an initializer
        // list invalid if we are trying to read something
        // off of it that wasn't handled by the cases above.
        //
        if (outToExpr)
        {
            getSink()->diagnose(
                fromInitializerListExpr,
                Diagnostics::cannotUseInitializerListForType,
                inToType);
        }
        return false;
    }

    // We were able to coerce all the arguments given, and so
    // we need to construct a suitable expression to remember the result
    //
    if (outToExpr)
    {
        auto toInitializerListExpr = m_astBuilder->create<InitializerListExpr>();
        toInitializerListExpr->loc = fromInitializerListExpr->loc;
        toInitializerListExpr->type = QualType(toType);
        toInitializerListExpr->args = coercedArgs;

        // Wrap initalizer list args if we're creating a non-differentiable struct within a
        // differentiable function.
        //
        if (auto func = getParentFuncOfVisitor())
        {
            if (func->findModifier<DifferentiableAttribute>() && !isTypeDifferentiable(toType))
            {
                for (auto& arg : toInitializerListExpr->args)
                {
                    if (isTypeDifferentiable(arg->type.type))
                    {
                        auto detachedArg = m_astBuilder->create<DetachExpr>();
                        detachedArg->inner = arg;
                        detachedArg->type = arg->type;
                        arg = detachedArg;
                    }
                }
            }
        }

        *outToExpr = toInitializerListExpr;
    }

    return true;
}

bool SemanticsVisitor::_coerceInitializerList(
    Type* toType,
    Expr** outToExpr,
    InitializerListExpr* fromInitializerListExpr)
{
    UInt argCount = fromInitializerListExpr->args.getCount();
    UInt argIndex = 0;

    // TODO: we should handle the special case of `{0}` as an initializer
    // for arbitrary `struct` types here.

    // If this initializer list has a more specific type than just
    // InitializerListType (i.e. it's already undergone a coercion) we
    // should ensure that we're allowed to coerce from that type to our
    // desired type.
    // If this isn't prohibited, then we can proceed to try and coerce from
    // the initializer list itself; assuming that coercion is closed under
    // composition this shouldn't fail.
    if (!as<InitializerListType>(fromInitializerListExpr->type) &&
        !canCoerce(toType, fromInitializerListExpr->type, nullptr))
        return _failedCoercion(toType, outToExpr, fromInitializerListExpr, getSink());

    // Try to invoke the user-defined constructor if it exists. This call will
    // report error diagnostics if the used-defined constructor exists but does not
    // match the initialize list.
    if (createInvokeExprForExplicitCtor(toType, fromInitializerListExpr, outToExpr))
    {
        return true;
    }

    // Try to invoke the synthesized constructor if it exists
    if (createInvokeExprForSynthesizedCtor(toType, fromInitializerListExpr, outToExpr))
    {
        return true;
    }

    // We will fall back to the legacy logic of initialize list.
    Expr* outInitListExpr = nullptr;
    if (!_readAggregateValueFromInitializerList(
            toType,
            &outInitListExpr,
            fromInitializerListExpr,
            argIndex))
        return false;
    if (argIndex != argCount)
    {
        if (outToExpr)
        {
            getSink()->diagnose(
                fromInitializerListExpr,
                Diagnostics::tooManyInitializers,
                argIndex,
                argCount);
        }
    }
    if (outToExpr)
        *outToExpr = outInitListExpr;

    return true;
}

bool SemanticsVisitor::_failedCoercion(
    Type* toType,
    Expr** outToExpr,
    Expr* fromExpr,
    DiagnosticSink* sink)
{
    if (outToExpr)
    {
        // As a special case, if the expression we are trying to convert
        // from is overloaded (implying an ambiguous reference), then we
        // will try to produce a more appropriately tailored error message.
        //
        auto fromType = fromExpr->type.type;
        if (as<OverloadGroupType>(fromType))
        {
            diagnoseAmbiguousReference(fromExpr);
        }
        else
        {
            if (sink)
            {
                sink->diagnose(fromExpr->loc, Diagnostics::typeMismatch, toType, fromExpr->type);
            }
        }
    }
    return false;
}

/// Do the `left` and `right` modifiers represent the same thing?
static bool _doModifiersMatch(Val* left, Val* right)
{
    if (left == right)
        return true;

    if (left->equals(right))
        return true;

    return false;
}

/// Does `type` have a modifier that matches `modifier`?
static bool _hasMatchingModifier(ModifiedType* type, Val* modifier)
{
    if (!type)
        return false;

    for (Index m = 0; m < type->getModifierCount(); m++)
    {
        if (_doModifiersMatch(type->getModifier(m), modifier))
            return true;
    }

    return false;
}

/// Can `modifier` be added to a type as part of a coercion?
///
/// For example, it is generally safe to convert from a value
/// of type `T` to a value of type `const T` in C/C++.
///
static bool _canModifierBeAddedDuringCoercion(Val* modifier)
{
    switch (modifier->astNodeType)
    {
    default:
        return false;

    case ASTNodeType::UNormModifierVal:
    case ASTNodeType::SNormModifierVal:
    case ASTNodeType::NoDiffModifierVal:
        return true;
    }
}

/// Can `modifier` be dropped from a type as part of a coercion?
///
/// For example, it is generally safe to convert from a value
/// of type `const T` to a value of type `T` in C/C++.
///
static bool _canModifierBeDroppedDuringCoercion(Val* modifier)
{
    switch (modifier->astNodeType)
    {
    default:
        return false;

    case ASTNodeType::UNormModifierVal:
    case ASTNodeType::SNormModifierVal:
    case ASTNodeType::NoDiffModifierVal:
        return true;
    }
}

static bool isSigned(Type* t)
{
    auto basicType = as<BasicExpressionType>(t);
    if (!basicType)
        return false;
    switch (basicType->getBaseType())
    {
    case BaseType::Int8:
    case BaseType::Int16:
    case BaseType::Int:
    case BaseType::Int64:
    case BaseType::IntPtr:
        return true;
    default:
        return false;
    }
}

int getMaximumTypeBitSize(Type* t)
{
    auto basicType = as<BasicExpressionType>(t);
    if (!basicType)
        return 0;

    switch (basicType->getBaseType())
    {
    case BaseType::Int8:
    case BaseType::UInt8:
        return 8;
    case BaseType::Int16:
    case BaseType::UInt16:
        return 16;
    case BaseType::Int:
    case BaseType::UInt:
        return 32;
    case BaseType::Int64:
    case BaseType::UInt64:
        return 64;
    case BaseType::IntPtr:
    case BaseType::UIntPtr:
        return 64;
    default:
        return 0;
    }
}

ConversionCost SemanticsVisitor::getImplicitConversionCostWithKnownArg(
    DeclRef<Decl> decl,
    Type* toType,
    Expr* arg)
{
    ConversionCost candidateCost = getImplicitConversionCost(decl.getDecl());

    if (candidateCost == kConversionCost_TypeCoercionConstraint ||
        candidateCost == kConversionCost_TypeCoercionConstraintPlusScalarToVector)
    {
        if (auto genApp = as<GenericAppDeclRef>(decl.declRefBase))
        {
            for (auto genArg : genApp->getArgs())
            {
                if (auto wit = as<TypeCoercionWitness>(genArg))
                {
                    candidateCost -= kConversionCost_TypeCoercionConstraint;
                    candidateCost += getConversionCost(wit->getToType(), wit->getFromType());
                    break;
                }
            }
        }
    }

    // Fix up the cost if the operand is a const lit.
    if (isScalarIntegerType(toType))
    {
        auto knownVal = as<IntegerLiteralExpr>(arg);
        if (!knownVal)
            return candidateCost;
        if (getIntValueBitSize(knownVal->value) <= getMaximumTypeBitSize(toType))
        {
            bool toTypeIsSigned = isSigned(toType);
            bool fromTypeIsSigned = isSigned(knownVal->type);
            if (toTypeIsSigned == fromTypeIsSigned)
                candidateCost = kConversionCost_InRangeIntLitConversion;
            else if (toTypeIsSigned)
                candidateCost = kConversionCost_InRangeIntLitUnsignedToSignedConversion;
            else
                candidateCost = kConversionCost_InRangeIntLitSignedToUnsignedConversion;
        }
    }
    return candidateCost;
}

bool SemanticsVisitor::_coerce(
    CoercionSite site,
    Type* toType,
    Expr** outToExpr,
    QualType fromType,
    Expr* fromExpr,
    DiagnosticSink* sink,
    ConversionCost* outCost)
{
    // If we are about to try and coerce an overloaded expression,
    // then we should start by trying to resolve the ambiguous reference
    // based on prioritization of the different candidates.
    //
    // If `fromExpr` is overloaded, we will try to coerce each
    // of the constituent overload candidates, filtering down to
    // those that are coercible, and then disambiguating the result.
    // Such an approach lets us disambiguate between overloaded
    // symbols based on their type (e.g., by casting the name of
    // an overloaded function to the type of the overload we mean
    // to reference).
    //
    if (auto fromOverloadedExpr = as<OverloadedExpr>(fromExpr))
    {
        auto resolvedExpr =
            maybeResolveOverloadedExpr(fromOverloadedExpr, LookupMask::Default, toType, nullptr);

        fromExpr = resolvedExpr;
        fromType = resolvedExpr->type;
    }
    else if (auto overloadedExpr2 = as<OverloadedExpr2>(fromExpr))
    {
        ShortList<Expr*> coercibleCandidates;
        for (auto candidate : overloadedExpr2->candidateExprs)
        {
            if (canCoerce(toType, candidate->type, candidate))
                coercibleCandidates.add(candidate);
        }
        if (coercibleCandidates.getCount() == 1)
        {
            return _coerce(
                site,
                toType,
                outToExpr,
                coercibleCandidates[0]->type,
                coercibleCandidates[0],
                sink,
                outCost);
        }
        if (sink)
        {
            auto firstCandidate = overloadedExpr2->candidateExprs.getCount() > 0
                                      ? overloadedExpr2->candidateExprs[0]
                                      : nullptr;
            if (auto declCandidate = as<DeclRefExpr>(firstCandidate))
            {
                sink->diagnose(
                    fromExpr->loc,
                    Diagnostics::ambiguousReference,
                    declCandidate->declRef);
            }
            else
            {
                sink->diagnose(fromExpr->loc, Diagnostics::ambiguousExpression);
            }
        }
        return false;
    }

    // An important and easy case is when the "to" and "from" types are equal.
    //
    if (toType->equals(fromType))
    {
        if (outToExpr)
            *outToExpr = fromExpr;
        if (outCost)
            *outCost = kConversionCost_None;
        return true;
    }

    // Fallback: Check if types are structurally identical but differ only in
    // their subtype witness arguments. This can happen when generic specialization
    // creates types through different inheritance paths
    // (e.g., int -> __BuiltinIntegerType -> __BuiltinArithmeticType vs
    // int -> __BuiltinSignedArithmeticType -> __BuiltinArithmeticType).
    //
    // We only apply this fallback for DeclRefType with GenericAppDeclRef where:
    // 1. The base declarations are the same
    // 2. All non-witness type arguments are equal
    //
    if (auto toDeclRefType = as<DeclRefType>(toType))
    {
        if (auto fromDeclRefType = as<DeclRefType>(fromType))
        {
            auto toGenApp = as<GenericAppDeclRef>(toDeclRefType->getDeclRefBase());
            auto fromGenApp = as<GenericAppDeclRef>(fromDeclRefType->getDeclRefBase());
            if (toGenApp && fromGenApp)
            {
                // Check if base declarations are the same
                if (toGenApp->getBase() == fromGenApp->getBase())
                {
                    auto toArgs = toGenApp->getArgs();
                    auto fromArgs = fromGenApp->getArgs();
                    if (toArgs.getCount() == fromArgs.getCount())
                    {
                        bool allNonWitnessArgsEqual = true;
                        for (Index i = 0; i < toArgs.getCount(); i++)
                        {
                            auto toArg = toArgs[i];
                            auto fromArg = fromArgs[i];
                            // Skip witness arguments (SubtypeWitness)
                            if (as<SubtypeWitness>(toArg) && as<SubtypeWitness>(fromArg))
                                continue;
                            // Non-witness arguments must be equal
                            if (!toArg->equals(fromArg))
                            {
                                allNonWitnessArgsEqual = false;
                                break;
                            }
                        }
                        if (allNonWitnessArgsEqual)
                        {
                            if (outToExpr)
                                *outToExpr = fromExpr;
                            if (outCost)
                                *outCost = kConversionCost_None;
                            return true;
                        }
                    }
                }
            }
        }
    }

    // Assume string literals are convertible to any string type.
    if (as<StringLiteralExpr>(fromExpr) && as<StringTypeBase>(toType))
    {
        if (outToExpr)
            *outToExpr = fromExpr;
        if (outCost)
            *outCost = kConversionCost_None;
        return true;
    }

    // Allow implicit conversion from sized array to unsized array when
    // calling a function.
    // Note: we implement the logic here instead of an implicit_conversion
    // intrinsic in the core module because we only want to allow this conversion
    // when calling a function.
    //
    if (site == CoercionSite::Argument)
    {
        if (auto fromArrayType = as<ArrayExpressionType>(fromType))
        {
            if (auto toArrayType = as<ArrayExpressionType>(toType))
            {
                if (fromArrayType->getElementType()->equals(toArrayType->getElementType()) &&
                    toArrayType->isUnsized())
                {
                    if (outToExpr)
                        *outToExpr = fromExpr;
                    if (outCost)
                        *outCost = kConversionCost_SizedArrayToUnsizedArray;
                    return true;
                }
            }
        }
    }

    // Another important case is when either the "to" or "from" type
    // represents an error. In such a case we must have already
    // reporeted the error, so it is better to allow the conversion
    // to pass than to report a "cascading" error that might not
    // make any sense.
    //
    if (as<ErrorType>(toType) || as<ErrorType>(fromType))
    {
        if (outToExpr)
            *outToExpr = CreateImplicitCastExpr(toType, fromExpr);
        if (outCost)
            *outCost = kConversionCost_None;
        return true;
    }

    {
        // It is possible that one or more of the types involved might have modifiers
        // on it, but the underlying types are otherwise the same.
        //
        auto toModified = as<ModifiedType>(toType);
        auto toBase = toModified ? toModified->getBase() : toType;
        //
        auto fromModified = as<ModifiedType>(fromType);
        auto fromBase =
            fromModified ? QualType(fromModified->getBase(), fromType.isLeftValue) : fromType;


        if ((toModified || fromModified) && toBase->equals(fromBase))
        {
            // We need to check each modifier present on either `toType`
            // or `fromType`. For each modifier, it will either be:
            //
            // * Present on both types; these are a non-issue
            // * Present only on `toType`
            // * Present only on `fromType`
            //
            if (toModified)
            {
                for (Index m = 0; m < toModified->getModifierCount(); m++)
                {
                    auto modifier = toModified->getModifier(m);
                    if (_hasMatchingModifier(fromModified, modifier))
                        continue;

                    // If `modifier` is present on `toType`, but not `fromType`,
                    // then we need to know whether this modifier can be added
                    // to the type of an expression as part of coercion.
                    //
                    if (!_canModifierBeAddedDuringCoercion(modifier))
                    {
                        return _failedCoercion(toType, outToExpr, fromExpr, sink);
                    }
                }
            }
            if (fromModified)
            {
                for (Index m = 0; m < fromModified->getModifierCount(); m++)
                {
                    auto modifier = fromModified->getModifier(m);

                    if (_hasMatchingModifier(toModified, modifier))
                        continue;

                    // If `modifier` is present on `fromType`, but not `toType`,
                    // then we need to know whether this modifier can be dropped
                    // to the type of an expression as part of coercion.
                    //
                    if (!_canModifierBeDroppedDuringCoercion(modifier))
                    {
                        return _failedCoercion(toType, outToExpr, fromExpr, sink);
                    }
                }
            }

            // If all the modifiers were okay, we can convert.

            // TODO: we may need a cost to allow disambiguation of overloads based on modifiers?
            if (outCost)
            {
                *outCost = kConversionCost_None;
            }
            if (outToExpr)
            {
                *outToExpr = createModifierCastExpr(toType, fromExpr);
            }

            return true;
        }
    }

    // Coercion from an initializer list is allowed for many types,
    // so we will farm that out to its own subroutine.
    //
    if (fromExpr && as<InitializerListType>(fromExpr->type.type))
    {
        if (auto fromInitializerListExpr = as<InitializerListExpr>(fromExpr))
        {
            if (!_coerceInitializerList(toType, outToExpr, fromInitializerListExpr))
            {
                return false;
            }

            // For now, we treat coercion from an initializer list
            // as having  no cost, so that all conversions from initializer
            // lists are equally valid. This is fine given where initializer
            // lists are allowed to appear now, but might need to be made
            // more strict if we allow for initializer lists in more
            // places in the language (e.g., as function arguments).
            //
            if (outCost)
            {
                *outCost = kConversionCost_None;
            }

            return true;
        }
    }

    // nullptr_t can be cast into any pointer type.
    if (as<NullPtrType>(fromType) && as<PtrType>(toType))
    {
        if (outCost)
        {
            *outCost = kConversionCost_NullPtrToPtr;
        }
        if (outToExpr)
        {
            auto* defaultExpr = getASTBuilder()->create<DefaultConstructExpr>();
            defaultExpr->type = QualType(toType);
            *outToExpr = defaultExpr;
        }
        return true;
    }
    // none_t can be cast into any Optional<T> type.
    if (as<NoneType>(fromType) && as<OptionalType>(toType))
    {
        if (outCost)
        {
            *outCost = kConversionCost_NoneToOptional;
        }
        if (outToExpr)
        {
            auto resultExpr = getASTBuilder()->create<MakeOptionalExpr>();
            resultExpr->loc = fromExpr->loc;
            resultExpr->type = toType;
            resultExpr->checked = true;
            *outToExpr = resultExpr;
        }
        return true;
    }

    // A enum type can be converted into its underlying tag type.
    if (auto enumDecl = isEnumType(fromType))
    {
        Type* tagType = enumDecl->tagType;
        if (tagType == toType)
        {
            if (outCost)
            {
                *outCost = kConversionCost_RankPromotion;
            }
            if (outToExpr)
            {
                auto rsExpr = getASTBuilder()->create<BuiltinCastExpr>();
                rsExpr->type = toType;
                rsExpr->loc = fromExpr->loc;
                rsExpr->base = fromExpr;
                *outToExpr = rsExpr;
            }
            return true;
        }
    }

    // matrix types with different layouts are convertible
    if (auto fromMatrixType = as<MatrixExpressionType>(fromType))
    {
        if (auto toMatrixType = as<MatrixExpressionType>(toType))
        {
            if (fromMatrixType->getElementType()->equals(toMatrixType->getElementType()) &&
                fromMatrixType->getRowCount()->equals(toMatrixType->getRowCount()) &&
                fromMatrixType->getColumnCount()->equals(toMatrixType->getColumnCount()))
            {
                if (outCost)
                {
                    *outCost = kConversionCost_MatrixLayout;
                }
                if (outToExpr)
                {
                    auto castExpr = getASTBuilder()->create<BuiltinCastExpr>();
                    castExpr->type = toType;
                    castExpr->loc = fromExpr->loc;
                    castExpr->base = fromExpr;
                    *outToExpr = castExpr;
                }
                return true;
            }
        }
    }

    // We allow a value of a `struct` type to be coerced to a function
    // type if the `struct` provides an appropriate method for calling
    // instances of that type.
    //
    // TODO(tfoley): This can and should be opened up to work for any
    // type (or at least any nominal type) that supports the required
    // operation.
    //
    if (auto toFuncType = as<FuncType>(toType))
    {
        if (auto fromLambdaType = isDeclRefTypeOf<StructDecl>(fromType))
        {
            if (tryCoerceLambdaToFuncType(fromLambdaType, toFuncType, fromExpr, outToExpr))
            {
                if (outCost)
                {
                    *outCost = kConversionCost_LambdaToFunc;
                }
                return true;
            }
        }
    }

    // A type is always convertible to any of its supertypes.
    //
    if (auto witness = tryGetSubtypeWitness(fromType, toType))
    {
        if (outToExpr)
        {
            *outToExpr = createCastToSuperTypeExpr(toType, fromExpr, witness);

            // If the original expression was an l-value, then the result
            // of the cast may be an l-value itself. We want to be able
            // to invoke `[mutating]` methods on a value that is cast to
            // an interface it conforms to, and we also expect to be able
            // to pass a value of a derived `struct` type into methods that
            // expect a value of its base type.
            //
            if (fromExpr && fromExpr->type.isLeftValue)
            {
                // If the original type is a concrete type and toType is an interface type,
                // we need to wrap the original expression into a MakeExistential, and the
                // result of MakeExistential is not an l-value.
                bool toTypeIsInterface = isInterfaceType(toType);
                bool fromTypeIsInterface = isInterfaceType(fromType);
                if (!toTypeIsInterface || toTypeIsInterface == fromTypeIsInterface)
                    (*outToExpr)->type.isLeftValue = true;
            }
        }
        if (outCost)
            *outCost = kConversionCost_CastToInterface;
        return true;
    }
    else if (auto fromIsToWitness = tryGetSubtypeWitness(toType, fromType))
    {
        // Is toType and fromType the same via some type equality witness?
        // If so there is no need to do any conversion.
        //
        // Note that this is a somewhat messy case to have, since we *already*
        // have a check for type equality above this point. For this code to
        // execute we would need to have a case where the `To` and `From` types
        // are considered distinct by `Type::equals` but `tryGetSubtypeWitness`
        // is still able to produce a witness for the equality of the two types.
        //
        // TODO(tfoley): Try to set things up so that we can have an invariant
        // that two types count as equal for `Type::equals` if and only if a
        // type equality witness for those types can be dervied.
        //
        if (isTypeEqualityWitness(fromIsToWitness))
        {
            if (outToExpr)
            {
                *outToExpr = createCastToSuperTypeExpr(toType, fromExpr, fromIsToWitness);
            }
            if (outCost)
                *outCost = 0;
            return true;
        }
    }

    // Disallow converting to a ParameterGroupType.
    //
    // TODO(tfoley): Under what circumstances would this check ever be needed?
    //
    if (as<ParameterGroupType>(toType))
    {
        return _failedCoercion(toType, outToExpr, fromExpr, sink);
    }

    // If the type that we are converting from is a parameter group type
    // (something like `ConstantBuffer<X>` or `ParameterBlock<X>`) and we
    // are converting to some type `Y`, then we want to allow for a multi-step
    // conversion where we first implicitly dereference the parameter group
    // to get an `X`, and then convert the resulting `X` to a `Y`.
    //
    // An important special case of the above is when `X == Y`, in which
    // case we are just converting, e.g., a `ConstantBuffer<X>` to an `X`.
    //
    // TODO(tfoley): When this conditional detects a parameter group type
    // it funnels the coercion logic into only considering conversions that
    // involve an automatic dereference. We need to ensure that any other
    // kinds of conversion that could apply to a parameter group are considered
    // earlier in this function, or else they will never actually be considered.
    // Notably, with this logic in place it is impossible for there to be any
    // conversion operations from a parameter-group type defined in code
    // (e.g., a constructor for a `DescriptorHandle`-like type that takes
    // a `ConstantBufer<T>` parameter will never be considered as part of conversion
    // logic, because we will first extract the `T` and then try to convert *that*).
    //
    if (auto fromParameterGroupType = as<ParameterGroupType>(fromType))
    {
        auto fromElementType = fromParameterGroupType->getElementType();

        DerefExpr* derefExpr = nullptr;
        if (outToExpr)
        {
            // TODO(tfoley): The logic here effectively assumes that any
            // parameter-group type is read-only, because we are not
            // setting the `isLeftValue` flag of the `QualType` based
            // on the type of the container. That is, a `StorageBuffer<X>`
            // and a `ConstantBuffer<X>` would both derive the `QualType`
            // of the dereferenced expression from `X` alone, and ignore
            // that one of these should yield an l-value and the other
            // shouldn't.
            //
            // In practice, we should have a centralized function that
            // handles dereferenencing of any `Expr`, and computes the
            // correct type for the result, so that the logic here can
            // exactly mirror other cases of implicit dereference.
            //
            derefExpr = m_astBuilder->create<DerefExpr>();
            derefExpr->base = fromExpr;
            derefExpr->type = QualType(fromElementType);
            derefExpr->checked = true;
        }

        ConversionCost subCost = kConversionCost_None;
        if (!_coerce(site, toType, outToExpr, fromElementType, derefExpr, sink, &subCost))
        {
            return false;
        }

        if (outCost)
            *outCost = subCost + kConversionCost_ImplicitDereference;
        return true;
    }

    // Because (for various bad reasons) we currently support an explicit
    // `Ref<T>` type (used to define some of our core-module functions),
    // we have to account for the case where an expression of type `T`
    // is being coerced to a `Ref<T>`.
    //
    if (auto refType = as<ExplicitRefType>(toType))
    {
        // TODO(tfoley): This logic is deeply and fundamentally incorrect.
        // It presumes that if an expression of type `T` can coerce to
        // type `U` then it can also coerce to a *reference* to `U`.
        // That means that because we support, say, implicit coercion of
        // an `int` to a `float`, this logic will support implicit coercion
        // of an `int` l-value to a `Ref<float>`!!!!
        //
        ConversionCost cost;
        if (!canCoerce(refType->getValueType(), fromType, fromExpr, &cost))
            return false;

        // Depending on whether the result of the coercion would be an l-value
        // or not, we may need to restrict the source to be an l-value.
        //
        // TODO(tfoley): Here we are again hijacking the `QualType` constructor
        // to do the direct work. It's still not clear where this logic should
        // live. In the longer run, I'm hopeful that we will get rid of
        // the explicit `Ref` type entirely (since it was a design mistake to
        // begin with), and thus not have to deal with the miserable mess that
        // it pushes back on various parts of the compiler.
        //
        auto qualRefType = QualType(refType);
        if (qualRefType.isLeftValue && !fromExpr->type.isLeftValue)
        {
            // The result type would be an l-value, but the source isn't,
            // so there is no way to support the conversion.
            //
            return false;
        }

        ConversionCost subCost = kConversionCost_GetRef;
        if (outCost)
            *outCost = subCost;

        if (outToExpr)
        {
            auto refExpr = m_astBuilder->create<MakeRefExpr>();
            refExpr->base = fromExpr;
            refExpr->type = qualRefType;
            refExpr->checked = true;
            *outToExpr = refExpr;
        }

        return true;
    }

    // TODO(tfoley): I was told that explicit `Ref` types should not
    // be seen by most of the compiler because they would be automatically
    // eliminated via `maybeOpenRef()` before other code needs to deal
    // with them... but that doesn't seem to be the case given how much
    // code here in type coercion is having to account for the possibility
    // of `Ref` types.

    // If we find ourselves in a situation where we need to coerce an
    // expression of type `Ref<T>`, we will first unwrap the reference
    // to get an expression of type `T` and then coerce *that*.
    //
    if (auto fromRefType = as<ExplicitRefType>(fromType))
    {
        auto fromValueType = fromRefType->getValueType();

        ConversionCost subCost = kConversionCost_None;

        Expr* openRefExpr = nullptr;
        if (outToExpr)
        {
            openRefExpr = maybeOpenRef(fromExpr);
        }

        if (!_coerce(site, toType, outToExpr, fromValueType, openRefExpr, sink, &subCost))
        {
            return false;
        }

        //
        // TODO(tfoley): This logic treats the implicit dereferencing
        // of a `Ref<T>` as an additional conversion cost, so that
        // a function with an explicit `Ref<T>` parameter would end up
        // being preferred over one with just a `T`.
        //
        // Making that distinction and introducing this cost seems to have
        // very little benefit, and risks causing developer confusion,
        // because for the most part references are invisible to the user
        // (intentionally).
        //
        // We don't want to support explicit `Ref<T>` types in parameter
        // positions anyway (people can use either a `ref` parameter or
        // an explicit `Ptr<T>`), so the whole thing is moot.
        //
        // For that matter, we probably should just remove explicit
        // `Ref<T>` types from the language, since they were never
        // intended to be there in the first place.
        //

        if (outCost)
            *outCost = subCost + kConversionCost_ImplicitDereference;
        return true;
    }

    // The main general-purpose approach for conversion is
    // using suitable marked initializer ("constructor")
    // declarations on the target type.
    //
    // This is treated as a form of overload resolution,
    // since we are effectively forming an overloaded
    // call to one of the initializers in the target type.

    OverloadResolveContext overloadContext;
    overloadContext.disallowNestedConversions = (site != CoercionSite::ExplicitCoercion);
    overloadContext.argCount = 1;
    List<Expr*> args;
    args.add(fromExpr);
    overloadContext.argTypes = &fromType.type;
    overloadContext.args = &args;
    overloadContext.sourceScope = m_outerScope;
    overloadContext.originalExpr = nullptr;
    if (fromExpr)
    {
        overloadContext.loc = fromExpr->loc;
        overloadContext.funcLoc = fromExpr->loc;
    }

    overloadContext.baseExpr = nullptr;
    overloadContext.mode = OverloadResolveContext::Mode::JustTrying;

    // Since the lookup and resolution of all possible implicit conversions
    // can be very costly, we use a cache to store the checking results.
    ImplicitCastMethodKey implicitCastKey = ImplicitCastMethodKey(fromType, toType, fromExpr);
    ImplicitCastMethod* cachedMethod = getShared()->tryGetImplicitCastMethod(implicitCastKey);

    if (cachedMethod)
    {
        if (cachedMethod->conversionFuncOverloadCandidate.status !=
            OverloadCandidate::Status::Applicable)
        {
            return _failedCoercion(toType, outToExpr, fromExpr, sink);
        }
        overloadContext.bestCandidateStorage = cachedMethod->conversionFuncOverloadCandidate;
        overloadContext.bestCandidate = &overloadContext.bestCandidateStorage;
        if (!outToExpr)
        {
            // If we are not requesting to create an expression, we can return early.
            if (outCost)
                *outCost = cachedMethod->cost;
            return true;
        }
        else
        {
            if (cachedMethod->isAmbiguous)
            {
                overloadContext.bestCandidate = nullptr;
                overloadContext.bestCandidates.add(cachedMethod->conversionFuncOverloadCandidate);
            }
        }
    }

    if (!overloadContext.bestCandidate)
    {
        AddTypeOverloadCandidates(toType, overloadContext);
    }

    // After all of the overload candidates have been added
    // to the context and processed, we need to see whether
    // there was one best overload or not.
    //
    if (overloadContext.bestCandidates.getCount() != 0)
    {
        // In this case there were multiple equally-good candidates to call.
        //
        // We will start by checking if the candidates are
        // even applicable, because if not, then we shouldn't
        // consider the conversion as possible.
        //
        if (overloadContext.bestCandidates[0].status != OverloadCandidate::Status::Applicable)
        {
            if (!cachedMethod)
            {
                getShared()->cacheImplicitCastMethod(implicitCastKey, ImplicitCastMethod{});
            }
            return _failedCoercion(toType, outToExpr, fromExpr, sink);
        }

        // If all of the candidates in `bestCandidates` are applicable,
        // then we have an ambiguity.
        //
        // We will compute a nominal conversion cost as the minimum over
        // all the conversions available.
        //
        ConversionCost bestCost = kConversionCost_Explicit;
        ImplicitCastMethod method;
        for (auto candidate : overloadContext.bestCandidates)
        {
            ConversionCost candidateCost =
                getImplicitConversionCostWithKnownArg(candidate.item.declRef, toType, fromExpr);
            if (candidateCost < bestCost)
            {
                method.conversionFuncOverloadCandidate = candidate;
                bestCost = candidateCost;
            }
        }

        bool result = true;

        // Conceptually, we want to treat the conversion as
        // possible, but report it as ambiguous if we actually
        // need to reify the result as an expression.
        //
        if (outToExpr)
        {
            if (sink)
            {
                sink->diagnose(fromExpr, Diagnostics::ambiguousConversion, fromType, toType);
                for (auto candidate : overloadContext.bestCandidates)
                {
                    sink->diagnose(
                        candidate.item.declRef,
                        Diagnostics::seeDeclarationOf,
                        candidate.item.declRef);
                }
            }

            *outToExpr = CreateErrorExpr(fromExpr);
            result = false;
        }

        if (!cachedMethod)
        {
            method.isAmbiguous = true;
            method.cost = bestCost;
            getShared()->cacheImplicitCastMethod(implicitCastKey, method);
        }

        if (outCost)
            *outCost = bestCost;
        return result;
    }
    else if (overloadContext.bestCandidate)
    {
        // If there is a single best candidate for conversion,
        // then we want to use it.
        //
        // It is possible that there was a single best candidate,
        // but it wasn't actually usable, so we will check for
        // that case first.
        //
        if (overloadContext.bestCandidate->status != OverloadCandidate::Status::Applicable)
        {
            if (!cachedMethod)
            {
                getShared()->cacheImplicitCastMethod(implicitCastKey, ImplicitCastMethod{});
            }
            return _failedCoercion(toType, outToExpr, fromExpr, sink);
        }

        // Next, we need to look at the implicit conversion
        // cost associated with the initializer we are invoking.
        //
        ConversionCost cost = getImplicitConversionCostWithKnownArg(
            overloadContext.bestCandidate->item.declRef,
            toType,
            fromExpr);

        // If the cost is too high to be usable as an
        // implicit conversion, then we will report the
        // conversion as possible (so that an overload involving
        // this conversion will be selected over one without),
        // but then emit a diagnostic when actually reifying
        // the result expression.
        //
        if (outToExpr && site != CoercionSite::ExplicitCoercion)
        {
            if (cost >= kConversionCost_Explicit)
            {
                if (sink)
                {
                    sink->diagnose(fromExpr, Diagnostics::typeMismatch, toType, fromType);
                    sink->diagnoseWithoutSourceView(
                        fromExpr,
                        Diagnostics::noteExplicitConversionPossible,
                        fromType,
                        toType);
                }
            }
            else if (cost >= kConversionCost_Default)
            {
                // For general types of implicit conversions, we issue a warning, unless `fromExpr`
                // is a known constant and we know it won't cause a problem.
                bool shouldEmitGeneralWarning = true;
                if (isScalarIntegerType(toType) || isHalfType(toType))
                {
                    if (auto intVal = tryFoldIntegerConstantExpression(
                            fromExpr,
                            ConstantFoldingKind::CompileTime,
                            nullptr))
                    {
                        if (auto val = as<ConstantIntVal>(intVal))
                        {
                            if (isIntValueInRangeOfType(val->getValue(), toType))
                            {
                                // OK.
                                shouldEmitGeneralWarning = false;
                            }
                        }
                    }
                }
                if (shouldEmitGeneralWarning && sink)
                {
                    sink->diagnose(
                        fromExpr,
                        Diagnostics::unrecommendedImplicitConversion,
                        fromType,
                        toType);
                }
            }

            if (site == CoercionSite::Argument && sink)
            {
                auto builtinConversionKind = getImplicitConversionBuiltinKind(
                    overloadContext.bestCandidate->item.declRef.getDecl());
                if (builtinConversionKind == kBuiltinConversion_FloatToDouble)
                {
                    if (!as<FloatingPointLiteralExpr>(fromExpr))
                        sink->diagnose(fromExpr, Diagnostics::implicitConversionToDouble);
                }
            }
        }
        if (fromType.isLeftValue)
        {
            // If we are implicitly casting the type of an l-value, we need to impose additional
            // cost.
            cost += kConversionCost_LValueCast;
        }
        if (outCost)
            *outCost = cost;

        if (outToExpr)
        {
            // The logic here is a bit ugly, to deal with the fact that
            // `CompleteOverloadCandidate` will, left to its own devices,
            // construct a vanilla `InvokeExpr` to represent the call
            // to the initializer we found, while we *want* it to
            // create some variety of `ImplicitCastExpr`.
            //
            // Now, it just so happens that `CompleteOverloadCandidate`
            // will use the "original" expression if one is available,
            // so we'll create one and initialize it here.
            // We fill in the location and arguments, but not the
            // base expression (the callee), since that will come
            // from the selected overload candidate.
            //
            InvokeExpr* castExpr = (site == CoercionSite::ExplicitCoercion)
                                       ? m_astBuilder->create<ExplicitCastExpr>()
                                       : createImplicitCastExpr();
            castExpr->loc = fromExpr->loc;
            castExpr->arguments.add(fromExpr);
            //
            // Next we need to set our cast expression as the "original"
            // expression and then complete the overload process.
            //
            overloadContext.originalExpr = castExpr;
            *outToExpr = CompleteOverloadCandidate(overloadContext, *overloadContext.bestCandidate);
            //
            // However, the above isn't *quite* enough, because
            // the process of completing the overload candidate
            // might overwrite the argument list that was passed
            // in to overload resolution, and in this case that
            // "argument list" was just a pointer to `fromExpr`.
            //
            // That means we need to clear the argument list and
            // reload it from `args[0]` to make sure that we
            // got the arguments *after* any transformations
            // were applied.
            // For right now this probably doesn't matter,
            // because we don't allow nested implicit conversions,
            // but I'd rather play it safe.
            //
            castExpr->arguments.clear();
            castExpr->arguments.add(args[0]);
        }
        if (!cachedMethod)
        {
            // We can only cache the method if it is a public, otherwise we may not be able to
            // use this method depending on where we are performing the coercion.
            if (overloadContext.bestCandidate->item.declRef &&
                getDeclVisibility(overloadContext.bestCandidate->item.declRef.getDecl()) ==
                    DeclVisibility::Public)
            {
                getShared()->cacheImplicitCastMethod(
                    implicitCastKey,
                    ImplicitCastMethod{*overloadContext.bestCandidate, cost});
            }
        }
        return true;
    }
    if (!cachedMethod)
    {
        getShared()->cacheImplicitCastMethod(implicitCastKey, ImplicitCastMethod{});
    }
    return _failedCoercion(toType, outToExpr, fromExpr, sink);
}

bool SemanticsVisitor::tryCoerceLambdaToFuncType(
    DeclRef<StructDecl> lambdaStruct,
    FuncType* toFuncType,
    Expr* fromExpr,
    Expr** outToExpr)
{
    FuncDecl* synStaticFunc = nullptr;
    FuncDecl* invokeFunc = nullptr;

    // First, check if `lambdaStruct` contains any fields.
    // If it does, we can't convert it to a function type.
    auto operatorName = getName("()");

    for (auto member : lambdaStruct.getDecl()->getDirectMemberDecls())
    {
        if (auto field = as<VarDecl>(member))
        {
            if (!isEffectivelyStatic(field))
                return false;
        }
        else if (auto inheritanceDecl = as<InheritanceDecl>(member))
        {
            // If the struct inherits from anything that is not an interface,
            // we will consider it to be non-empty and not convertible to a function type.
            if (!isDeclRefTypeOf<InterfaceDecl>(inheritanceDecl->base.type))
                return false;
        }
        else if (auto funcDecl = as<FuncDecl>(member))
        {
            // If the struct already contains a synthesized static invoke member, use it.
            if (isEffectivelyStatic(funcDecl) &&
                funcDecl->findModifier<SynthesizedStaticLambdaFuncModifier>() &&
                funcDecl->returnType.type == toFuncType->getResultType())
                synStaticFunc = funcDecl;
            if (funcDecl->getName() == operatorName)
            {
                // If we found operator(), keep it for later.
                invokeFunc = funcDecl;
            }
        }
    }

    if (!invokeFunc)
    {
        return false;
    }

    auto invokeFuncDeclRef = m_astBuilder->getMemberDeclRef(lambdaStruct, invokeFunc);

    // Verify that the function parameter types are exactly the same as toFuncType.
    if (invokeFunc->getParameters().getCount() != toFuncType->getParamCount())
    {
        return false;
    }
    Index paramId = 0;
    for (auto param : invokeFunc->getParameters())
    {
        auto paramType = getParamTypeWithModeWrapper(m_astBuilder, param);
        auto toParamType = toFuncType->getParamTypeWithModeWrapper(paramId);
        if (!paramType->equals(toParamType))
        {
            return false;
        }
        paramId++;
    }

    // Verify that the return type of the function is convertible to the function type.
    if (!canCoerce(toFuncType->getResultType(), invokeFunc->returnType.type, nullptr))
    {
        return false;
    }

    if (!synStaticFunc)
    {
        // If the struct doesn't contain a static method for operator(), we try to synthesize one.
        synStaticFunc = m_astBuilder->create<FuncDecl>();
        synStaticFunc->nameAndLoc.name = getName("__syn_static_invoke");
        addModifier(synStaticFunc, m_astBuilder->create<SynthesizedStaticLambdaFuncModifier>());
        addModifier(synStaticFunc, m_astBuilder->create<HLSLStaticModifier>());
        addModifier(synStaticFunc, m_astBuilder->create<SynthesizedModifier>());

        synStaticFunc->ownedScope = m_astBuilder->create<Scope>();
        synStaticFunc->ownedScope->containerDecl = synStaticFunc;
        synStaticFunc->ownedScope->parent = getScope(lambdaStruct.getDecl());
        synStaticFunc->parentDecl = lambdaStruct.getDecl();
        synStaticFunc->returnType.type = toFuncType->getResultType();

        List<Expr*> synArgs;
        addRequiredParamsToSynthesizedDecl(invokeFuncDeclRef, synStaticFunc, synArgs);
        ThisExpr* synThis = nullptr;
        addModifiersToSynthesizedDecl(nullptr, invokeFuncDeclRef, synStaticFunc, synThis);

        ASTSynthesizer synth(m_astBuilder, getNamePool());
        synth.pushContainerScope(synStaticFunc);
        auto blockStmt = m_astBuilder->create<BlockStmt>();
        synStaticFunc->body = blockStmt;
        auto seqStmt = synth.pushSeqStmtScope();
        blockStmt->body = seqStmt;

        synth.pushVarScope();

        // emit `return LambdaStructType().operator()(args...)`.
        auto tempThis = synth.emitInvokeExpr(
            synth.emitStaticTypeExpr(DeclRefType::create(m_astBuilder, lambdaStruct)),
            List<Expr*>());
        tempThis = dispatchExpr(tempThis, *this);

        Expr* operatorRefExpr = synth.emitMemberExpr(toFuncType, tempThis, invokeFuncDeclRef);

        auto invokeExpr = synth.emitInvokeExpr(operatorRefExpr, _Move(synArgs));
        invokeExpr = dispatchExpr(invokeExpr, *this);
        auto resultValue =
            coerce(CoercionSite::Return, toFuncType->getResultType(), invokeExpr, getSink());
        synth.emitReturnStmt(resultValue);

        lambdaStruct.getDecl()->addMember(synStaticFunc);
    }

    // If we have a static method for operator(), we can convert the lambda to a function type.
    if (outToExpr)
    {
        VarExpr* expr = m_astBuilder->create<VarExpr>();
        expr->loc = fromExpr->loc;
        expr->declRef = m_astBuilder->getMemberDeclRef(lambdaStruct, synStaticFunc);
        expr->type = QualType(toFuncType);
        expr->checked = true;
        *outToExpr = expr;
    }
    return true;
}

bool SemanticsVisitor::canCoerce(
    Type* toType,
    QualType fromType,
    Expr* fromExpr,
    ConversionCost* outCost)
{
    // As an optimization, we will maintain a cache of conversion results
    // for basic types such as scalars and vectors.
    //

    bool shouldAddToCache = false;
    ConversionCost cost;
    TypeCheckingCache* typeCheckingCache = getLinkage()->getTypeCheckingCache();

    BasicTypeKeyPair cacheKey;
    cacheKey.type1 = makeBasicTypeKey(toType);
    cacheKey.type2 = makeBasicTypeKey(fromType, fromExpr);

    if (cacheKey.isValid())
    {
        if (typeCheckingCache->conversionCostCache.tryGetValue(cacheKey, cost))
        {
            if (outCost)
                *outCost = cost;
            return cost != kConversionCost_Impossible;
        }
        else
            shouldAddToCache = true;
    }

    // If there was no suitable entry in the cache,
    // then we fall back to the general-purpose
    // conversion checking logic.
    //
    // Note that we are passing in `nullptr` as
    // the output expression to be constructed,
    // which suppresses emission of any diagnostics
    // during the coercion process.
    //
    bool rs = _coerce(CoercionSite::General, toType, nullptr, fromType, fromExpr, getSink(), &cost);

    if (outCost)
        *outCost = cost;

    if (shouldAddToCache)
    {
        if (!rs)
            cost = kConversionCost_Impossible;
        typeCheckingCache->conversionCostCache[cacheKey] = cost;
    }

    return rs;
}

TypeCastExpr* SemanticsVisitor::createImplicitCastExpr()
{
    return m_astBuilder->create<ImplicitCastExpr>();
}

Expr* SemanticsVisitor::CreateImplicitCastExpr(Type* toType, Expr* fromExpr)
{
    TypeCastExpr* castExpr = createImplicitCastExpr();

    auto typeType = m_astBuilder->getTypeType(toType);

    auto typeExpr = m_astBuilder->create<SharedTypeExpr>();
    typeExpr->type.type = typeType;
    typeExpr->base.type = toType;

    castExpr->loc = fromExpr->loc;
    castExpr->functionExpr = typeExpr;
    castExpr->type = QualType(toType);
    castExpr->arguments.add(fromExpr);
    return castExpr;
}

Expr* SemanticsVisitor::createCastToSuperTypeExpr(Type* toType, Expr* fromExpr, Val* witness)
{
    CastToSuperTypeExpr* expr = m_astBuilder->create<CastToSuperTypeExpr>();
    expr->loc = fromExpr->loc;
    expr->type = QualType(toType);
    expr->valueArg = fromExpr;
    expr->witnessArg = witness;
    return expr;
}

Expr* SemanticsVisitor::createModifierCastExpr(Type* toType, Expr* fromExpr)
{
    ModifierCastExpr* expr = m_astBuilder->create<ModifierCastExpr>();
    expr->loc = fromExpr->loc;
    expr->type = QualType(toType);
    expr->valueArg = fromExpr;
    return expr;
}


Expr* SemanticsVisitor::coerce(
    CoercionSite site,
    Type* toType,
    Expr* fromExpr,
    DiagnosticSink* sink)
{
    Expr* expr = nullptr;
    if (!_coerce(site, toType, &expr, fromExpr->type, fromExpr, sink, nullptr))
    {
        // Note(tfoley): We don't call `CreateErrorExpr` here, because that would
        // clobber the type on `fromExpr`, and an invariant here is that coercion
        // really shouldn't *change* the expression that is passed in, but should
        // introduce new AST nodes to coerce its value to a different type...
        //
        // TODO(tfoley): Based on the comment above it seems like my past self
        // wrote this code, but looking at it now, I'm unsure why we want to return
        // an expression with an error type when we have the `toType` that is
        // expected *right there*. It would be good to investigate whether changing
        // this to return an expression of the expected type would Just Work.
        //
        return CreateImplicitCastExpr(m_astBuilder->getErrorType(), fromExpr);
    }

    return expr;
}

bool SemanticsVisitor::canConvertImplicitly(ConversionCost conversionCost)
{
    // Is the conversion cheap enough to be done implicitly?
    if (conversionCost >= kConversionCost_GeneralConversion)
        return false;
    return true;
}

bool SemanticsVisitor::canConvertImplicitly(Type* toType, QualType fromType)
{
    auto conversionCost = getConversionCost(toType, fromType);

    // Is the conversion cheap enough to be done implicitly?
    if (canConvertImplicitly(conversionCost))
        return false;

    return true;
}

ConversionCost SemanticsVisitor::getConversionCost(Type* toType, QualType fromType)
{
    ConversionCost conversionCost = kConversionCost_Impossible;
    if (!canCoerce(toType, fromType, nullptr, &conversionCost))
        return kConversionCost_Impossible;
    return conversionCost;
}
} // namespace Slang
