// slang-check-conversion.cpp
#include "slang-ast-synthesis.h"
#include "slang-check-impl.h"
#include "slang-rich-diagnostics.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
{
namespace
{

static bool _diagnoseKnownInvalidShapePackTransformArg(
    Val* val,
    SourceLoc loc,
    DiagnosticSink* sink)
{
    if (!val || !sink)
        return false;

    if (auto shapeConcatPack = as<ShapeConcatIntValPack>(val))
    {
        auto leftPack = as<ConcreteIntValPack>(shapeConcatPack->getLeftPack()->resolve());
        auto rightPack = as<ConcreteIntValPack>(shapeConcatPack->getRightPack()->resolve());
        IntegerLiteralValue axisValue = 0;
        auto knownRankPack = leftPack ? leftPack : rightPack;
        if (knownRankPack && (!leftPack || !rightPack))
        {
            if (tryGetConstantIntVal(shapeConcatPack->getAxis(), axisValue))
            {
                if (axisValue < 0 || axisValue >= knownRankPack->getCount())
                {
                    sink->diagnose(Diagnostics::ShapePackAxisOutOfRange{
                        .opName = "__shapeConcat",
                        .axis = (int)axisValue,
                        .rank = (int)knownRankPack->getCount(),
                        .location = loc});
                    return true;
                }
            }
            else if (knownRankPack->getCount() == 0)
            {
                sink->diagnose(Diagnostics::ShapePackNoValidAxis{
                    .opName = "__shapeConcat",
                    .rank = 0,
                    .location = loc});
                return true;
            }
        }

        if (leftPack && rightPack)
        {
            if (leftPack->getCount() != rightPack->getCount())
            {
                sink->diagnose(Diagnostics::ShapePackRankMismatch{
                    .opName = "__shapeConcat",
                    .leftRank = (int)leftPack->getCount(),
                    .rightRank = (int)rightPack->getCount(),
                    .location = loc});
                return true;
            }

            if (tryGetConstantIntVal(shapeConcatPack->getAxis(), axisValue))
            {
                if (axisValue < 0 || axisValue >= leftPack->getCount())
                {
                    sink->diagnose(Diagnostics::ShapePackAxisOutOfRange{
                        .opName = "__shapeConcat",
                        .axis = (int)axisValue,
                        .rank = (int)leftPack->getCount(),
                        .location = loc});
                    return true;
                }

                for (Index i = 0; i < leftPack->getCount(); ++i)
                {
                    if (i != axisValue && areProvablyDifferentShapeElements(
                                              leftPack->getElement(i),
                                              rightPack->getElement(i)))
                    {
                        sink->diagnose(Diagnostics::ShapeConcatNonAxisMismatch{
                            .axis = (int)axisValue,
                            .dimIndex = (int)i,
                            .location = loc});
                        return true;
                    }
                }
            }
            else if (leftPack->getCount() == 0)
            {
                sink->diagnose(Diagnostics::ShapePackNoValidAxis{
                    .opName = "__shapeConcat",
                    .rank = 0,
                    .location = loc});
                return true;
            }
            else if (!hasAnyPotentialConcatAxis(leftPack, rightPack))
            {
                sink->diagnose(Diagnostics::ShapeConcatNoValidAxis{
                    .rank = (int)leftPack->getCount(),
                    .location = loc});
                return true;
            }
        }

        if (_diagnoseKnownInvalidShapePackTransformArg(shapeConcatPack->getLeftPack(), loc, sink))
            return true;
        if (_diagnoseKnownInvalidShapePackTransformArg(shapeConcatPack->getRightPack(), loc, sink))
            return true;
        if (_diagnoseKnownInvalidShapePackTransformArg(shapeConcatPack->getAxis(), loc, sink))
            return true;
    }
    else if (auto shapePermutePack = as<ShapePermuteIntValPack>(val))
    {
        auto valuePack = as<ConcreteIntValPack>(shapePermutePack->getValuePack()->resolve());
        auto orderPack = as<ConcreteIntValPack>(shapePermutePack->getOrderPack()->resolve());
        if (valuePack && orderPack)
        {
            if (valuePack->getCount() != orderPack->getCount())
            {
                sink->diagnose(Diagnostics::ShapePermuteOrderLengthMismatch{
                    .orderRank = (int)orderPack->getCount(),
                    .valueRank = (int)valuePack->getCount(),
                    .location = loc});
                return true;
            }

            Index firstPosition = 0;
            Index secondPosition = 0;
            IntegerLiteralValue orderIndex = 0;
            bool hasConcreteIndex = false;
            if (tryFindProvableDuplicateOrderIndices(
                    orderPack,
                    firstPosition,
                    secondPosition,
                    orderIndex,
                    hasConcreteIndex))
            {
                if (hasConcreteIndex)
                {
                    sink->diagnose(Diagnostics::ShapePermuteDuplicateIndex{
                        .indexValue = (int)orderIndex,
                        .firstPosition = (int)firstPosition,
                        .secondPosition = (int)secondPosition,
                        .location = loc});
                }
                else
                {
                    sink->diagnose(Diagnostics::ShapePermuteDuplicateEquivalentIndex{
                        .firstPosition = (int)firstPosition,
                        .secondPosition = (int)secondPosition,
                        .location = loc});
                }
                return true;
            }

            for (Index i = 0; i < orderPack->getCount(); ++i)
            {
                orderIndex = 0;
                if (!tryGetConstantIntVal(orderPack->getElement(i), orderIndex))
                    continue;

                if (orderIndex < 0 || orderIndex >= valuePack->getCount())
                {
                    sink->diagnose(Diagnostics::ShapePermuteIndexOutOfRange{
                        .indexValue = (int)orderIndex,
                        .indexPosition = (int)i,
                        .rank = (int)valuePack->getCount(),
                        .location = loc});
                    return true;
                }
            }
        }

        if (_diagnoseKnownInvalidShapePackTransformArg(shapePermutePack->getValuePack(), loc, sink))
            return true;
        if (_diagnoseKnownInvalidShapePackTransformArg(shapePermutePack->getOrderPack(), loc, sink))
            return true;
    }
    else if (auto shapeSwapPack = as<ShapeSwapIntValPack>(val))
    {
        auto valuePack = as<ConcreteIntValPack>(shapeSwapPack->getValuePack()->resolve());
        IntegerLiteralValue dimValue = 0;
        if (valuePack && tryGetConstantIntVal(shapeSwapPack->getDim0(), dimValue) &&
            (dimValue < 0 || dimValue >= valuePack->getCount()))
        {
            sink->diagnose(Diagnostics::ShapePackAxisOutOfRange{
                .opName = "__shapeSwap",
                .axis = (int)dimValue,
                .rank = (int)valuePack->getCount(),
                .location = loc});
            return true;
        }

        if (valuePack && tryGetConstantIntVal(shapeSwapPack->getDim1(), dimValue) &&
            (dimValue < 0 || dimValue >= valuePack->getCount()))
        {
            sink->diagnose(Diagnostics::ShapePackAxisOutOfRange{
                .opName = "__shapeSwap",
                .axis = (int)dimValue,
                .rank = (int)valuePack->getCount(),
                .location = loc});
            return true;
        }

        if (valuePack && valuePack->getCount() == 0)
        {
            sink->diagnose(Diagnostics::ShapePackNoValidAxis{
                .opName = "__shapeSwap",
                .rank = 0,
                .location = loc});
            return true;
        }

        if (_diagnoseKnownInvalidShapePackTransformArg(shapeSwapPack->getValuePack(), loc, sink))
            return true;
        if (_diagnoseKnownInvalidShapePackTransformArg(shapeSwapPack->getDim0(), loc, sink))
            return true;
        if (_diagnoseKnownInvalidShapePackTransformArg(shapeSwapPack->getDim1(), loc, sink))
            return true;
    }
    else if (auto shapeReducePack = as<ShapeReduceIntValPack>(val))
    {
        auto valuePack = as<ConcreteIntValPack>(shapeReducePack->getValuePack()->resolve());
        IntegerLiteralValue axisValue = 0;
        if (valuePack && tryGetConstantIntVal(shapeReducePack->getAxis(), axisValue) &&
            (axisValue < 0 || axisValue >= valuePack->getCount()))
        {
            sink->diagnose(Diagnostics::ShapePackAxisOutOfRange{
                .opName = "__shapeReduce",
                .axis = (int)axisValue,
                .rank = (int)valuePack->getCount(),
                .location = loc});
            return true;
        }

        if (valuePack && valuePack->getCount() == 0)
        {
            sink->diagnose(Diagnostics::ShapePackNoValidAxis{
                .opName = "__shapeReduce",
                .rank = 0,
                .location = loc});
            return true;
        }

        if (_diagnoseKnownInvalidShapePackTransformArg(shapeReducePack->getValuePack(), loc, sink))
            return true;
        if (_diagnoseKnownInvalidShapePackTransformArg(shapeReducePack->getAxis(), loc, sink))
            return true;
    }
    else if (auto concretePack = as<ConcreteIntValPack>(val))
    {
        for (Index i = 0; i < concretePack->getCount(); ++i)
        {
            if (_diagnoseKnownInvalidShapePackTransformArg(concretePack->getElement(i), loc, sink))
                return true;
        }
    }
    else if (auto firstVal = as<FirstIntVal>(val))
    {
        if (_diagnoseKnownInvalidShapePackTransformArg(firstVal->getBasePack(), loc, sink))
            return true;
    }
    else if (auto lastVal = as<LastIntVal>(val))
    {
        if (_diagnoseKnownInvalidShapePackTransformArg(lastVal->getBasePack(), loc, sink))
            return true;
    }
    else if (auto trimFirstPack = as<TrimFirstIntValPack>(val))
    {
        if (_diagnoseKnownInvalidShapePackTransformArg(trimFirstPack->getBasePack(), loc, sink))
            return true;
    }
    else if (auto trimLastPack = as<TrimLastIntValPack>(val))
    {
        if (_diagnoseKnownInvalidShapePackTransformArg(trimLastPack->getBasePack(), loc, sink))
            return true;
    }

    return false;
}

static bool _diagnoseKnownInvalidShapePackTransforms(
    Type* toType,
    Type* fromType,
    SourceLoc loc,
    DiagnosticSink* sink)
{
    if (!sink)
        return false;

    auto toDeclRefType = as<DeclRefType>(toType);
    auto fromDeclRefType = as<DeclRefType>(fromType);
    if (!toDeclRefType || !fromDeclRefType)
        return false;

    auto toGenApp = as<GenericAppDeclRef>(toDeclRefType->getDeclRefBase());
    auto fromGenApp = as<GenericAppDeclRef>(fromDeclRefType->getDeclRefBase());
    if (!toGenApp || !fromGenApp || toGenApp->getBase() != fromGenApp->getBase())
        return false;

    auto toArgs = toGenApp->getArgs();
    auto fromArgs = fromGenApp->getArgs();
    auto argCount =
        toArgs.getCount() < fromArgs.getCount() ? toArgs.getCount() : fromArgs.getCount();
    for (Index i = 0; i < argCount; ++i)
    {
        if (_diagnoseKnownInvalidShapePackTransformArg(toArgs[i], loc, sink))
            return true;
        if (_diagnoseKnownInvalidShapePackTransformArg(fromArgs[i], loc, sink))
            return true;
    }

    return false;
}

} // namespace

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;
    }

    if (auto modifiedType = as<ModifiedType>(type))
    {
        return isEffectivelyScalarForInitializerLists(modifiedType->getBase());
    }

    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,
            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;
}

static bool _canLookupConstructorsThroughAbstractType(Type* type)
{
    return isDeclRefTypeOf<GenericTypeParamDeclBase>(type) || as<FirstPackElementType>(type) ||
           as<LastPackElementType>(type) || as<PackBranchType>(type);
}

bool SemanticsVisitor::createCtorInvokeExprForAbstractType(
    Type* toType,
    InitializerListExpr* fromInitializerListExpr,
    Expr** outExpr)
{
    if (!_canLookupConstructorsThroughAbstractType(toType))
        return false;

    auto ctors = lookupConstructorsInType(toType, m_outerScope);
    if (!ctors.isValid())
        return false;

    Expr* ctorExpr = nullptr;
    if (ctors.isOverloaded())
    {
        auto overloadedExpr = m_astBuilder->create<OverloadedExpr2>();
        overloadedExpr->type = m_astBuilder->getOverloadedType();
        for (auto item : ctors.items)
        {
            overloadedExpr->candidateExprs.add(ConstructDeclRefExpr(
                item.declRef,
                nullptr,
                item.declRef.getName(),
                fromInitializerListExpr->loc,
                nullptr));
        }
        ctorExpr = overloadedExpr;
    }
    else
    {
        ctorExpr = ConstructDeclRefExpr(
            ctors.item.declRef,
            nullptr,
            ctors.item.declRef.getName(),
            fromInitializerListExpr->loc,
            nullptr);
    }

    auto ctorInvokeExpr = m_astBuilder->create<InvokeExpr>();
    ctorInvokeExpr->functionExpr = ctorExpr;
    ctorInvokeExpr->arguments.addRange(fromInitializerListExpr->args);
    ctorInvokeExpr->loc = fromInitializerListExpr->loc;

    DiagnosticSink tempSink(getSourceManager(), nullptr, getSink());

    SemanticsVisitor subVisitor(withSink(&tempSink));
    Expr* checkedCtorInvokeExpr = subVisitor.CheckExpr(ctorInvokeExpr);

    if (tempSink.getErrorCount())
    {
        return false;
    }

    if (outExpr)
        *outExpr = checkedCtorInvokeExpr;
    return true;
}

// Try to coerce an initializer list to `toType` by invoking one of `toType`'s
// explicit constructors, e.g. turning `float3 v = {a, b}` into `float3(a, b)`.
// Returns whether such a coercion is possible. The return value must be the same
// whether or not `outExpr` is requested: overload resolution first calls this with
// `outExpr == nullptr` to probe whether a candidate is viable (see `canCoerce`),
// then again with `outExpr` set to actually build the expression. Reporting success
// only when `outExpr` is non-null would make the viability probe a false negative and
// reject an otherwise-valid candidate.
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, getSink());

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

            // The constructor is type-checked against a temporary sink so that a
            // genuine overload-match failure can be swallowed and retried through
            // the legacy initializer-list path. Once we commit to the
            // constructor, however, any diagnostics it produced are real and must
            // be surfaced on the caller's sink. This includes warnings such as a
            // `[deprecated]` constructor (which leave `getErrorCount() == 0` and
            // were previously lost) as well as errors such as a `[RemovedSince]`
            // constructor. We only forward when actually building the result
            // expression (`outExpr != nullptr`); a `canCoerce()` viability probe
            // passes `outExpr == nullptr` and must stay silent (see `canCoerce`).
            auto forwardDiagnostics = [&]()
            {
                if (!outExpr || tempSink.outputBuffer.getLength() == 0)
                    return;
                getSink()->diagnoseRaw(
                    tempSink.getErrorCount() ? Severity::Error : Severity::Warning,
                    tempSink.outputBuffer.getUnownedSlice());
            };

            // A genuine overload-match failure yields an error expression,
            // whereas a matched constructor that merely emitted diagnostics
            // (e.g. it is `[deprecated]` or `[RemovedSince]`) still yields a
            // valid constructor-call expression.
            if (tempSink.getErrorCount())
            {
                HashSet<Type*> isVisit;
                const bool ctorMatched = !IsErrorExpr(ctorInvokeExpr);

                // For a C-style type, a genuine match failure should fall back to
                // the legacy initializer-list logic in `_coerceInitializerList()`.
                // But when the constructor actually matched and the error is about
                // that constructor itself (for example it has been removed via
                // `[RemovedSince]`), falling back would silently discard the
                // error, so we surface it here instead.
                if (isCStyleType(toType, isVisit) && !ctorMatched)
                    return false;

                forwardDiagnostics();

                // For non-c-style types (and matched-but-errored C-style ctors),
                // we 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;
            }

            // The explicit constructor matched and type-checked, so this is a
            // valid coercion. Report success even when no output expression was
            // requested (e.g. the `canCoerce` viability probe passes
            // `outExpr == nullptr`); otherwise overload resolution would treat
            // the conversion as impossible and reject an otherwise-viable
            // candidate. This mirrors `createInvokeExprForSynthesizedCtor` and
            // `createCtorInvokeExprForAbstractType`, which return `true`
            // independent of `outExpr`.
            forwardDiagnostics();
            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(Diagnostics::CannotUseInitializerListForType{
                .type = toType,
                .initList = fromInitializerListExpr});

            // Emit a note naming the first member whose visibility mismatches the
            // struct's. Guard against emitting it when a different isCStyleType
            // condition is the real cause — those conditions are checked first:
            //   - Non-interface inheritance (rule 2): guard 1.
            //   - Explicit constructor (rule 3): guard 2.
            bool hasNonInterfaceBase = false;
            for (auto inheritanceDecl : structDecl->getMembersOfType<InheritanceDecl>())
            {
                if (!isDeclRefTypeOf<InterfaceDecl>(inheritanceDecl->base.type))
                {
                    hasNonInterfaceBase = true;
                    break;
                }
            }
            if (!hasNonInterfaceBase && !_hasExplicitConstructor(structDecl, true))
            {
                DeclVisibility structVis = getDeclVisibility(structDecl);
                for (auto varDecl : structDecl->getMembersOfType<VarDeclBase>())
                {
                    DeclVisibility memberVis = getDeclVisibility(varDecl);
                    if (memberVis != structVis)
                    {
                        diagnoseOnce(Diagnostics::InitializerListMemberVisibilityMismatch{
                            .memberVis = memberVis,
                            .type = toType,
                            .structVis = structVis,
                            .member = varDecl});
                        break;
                    }
                }
            }
        }

        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, getSink());
    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()));

            // When the synthesized constructor has fewer parameters than
            // members because some members are less visible than the struct
            // itself, the user gets "too many arguments" without
            // understanding why certain members were excluded from the
            // constructor. Emit a note naming each non-public member so
            // they know what to fix (issue #11005).
            //
            // Use the same criteria as `collectInitializableMembers`:
            // instance members whose visibility is strictly less than the
            // struct's are excluded from the synthesized ctor.
            DeclVisibility structVis = getDeclVisibility(structDecl);
            for (auto varDecl : structDecl->getMembersOfType<VarDeclBase>())
            {
                if (varDecl->hasModifier<HLSLStaticModifier>())
                    continue;
                DeclVisibility memberVis = getDeclVisibility(varDecl);
                if (memberVis < structVis)
                {
                    diagnoseOnce(Diagnostics::InitializerListMemberVisibilityMismatch{
                        .memberVis = memberVis,
                        .type = toType,
                        .structVis = structVis,
                        .member = varDecl});
                }
            }

            return false;
        }
    }
    return false;
}

bool SemanticsVisitor::_readAggregateValueFromInitializerList(
    Type* inToType,
    Expr** outToExpr,
    InitializerListExpr* fromInitializerListExpr,
    UInt& ioArgIndex)
{
    // TODO(sai): there's a massive number of special cases to handle for
    // modified types..
    auto toType = unwrapModifiedType(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,
                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<BuiltinOperationIntVal>(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(Diagnostics::CannotUseInitializerListForVectorOfUnknownSize{
                    .elementCount = toElementCount,
                    .initList = fromInitializerListExpr});
            }
            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(Diagnostics::CannotUseInitializerListForCoopVectorOfUnknownSize{
                    .elementCount = toElementCount,
                    .initList = fromInitializerListExpr});
            }
            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<BuiltinOperationIntVal>(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(Diagnostics::CannotUseInitializerListForMatrixOfUnknownSize{
                    .rowCount = toMatrixType->getRowCount(),
                    .initList = fromInitializerListExpr});
            }
            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(Diagnostics::CannotUseInitializerListForType{
                .type = inToType,
                .initList = fromInitializerListExpr});
        }
        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;
    }

    // Finally, allow selected abstract wrapper types to surface a common
    // default constructor through their facets.
    if (createCtorInvokeExprForAbstractType(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(Diagnostics::TooManyInitializers{
                .expected = (int64_t)argIndex,
                .got = (int64_t)argCount,
                .initList = fromInitializerListExpr});
        }
    }
    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(Diagnostics::TypeMismatch{
                    .expectedType = toType,
                    .actualType = fromExpr->type,
                    .expr = fromExpr});

                // When calling an interface method on an existential, the
                // 'This' type in the parameter becomes an
                // ExtractExistentialType. If the argument is also an opened
                // existential (a different ExtractExistentialType), both
                // print as "This" in the error, making the message confusing.
                // Add a note explaining the type-erasure issue.
                if (auto toExtType = as<ExtractExistentialType>(toType))
                {
                    if (auto fromExtType = as<ExtractExistentialType>(fromType))
                    {
                        Type* toInterfaceType = toExtType->getOriginalInterfaceType();
                        Type* fromInterfaceType = fromExtType->getOriginalInterfaceType();
                        if (toInterfaceType && fromInterfaceType &&
                            toInterfaceType->equals(fromInterfaceType))
                        {
                            sink->diagnose(Diagnostics::ThisTypeMismatchAfterErasure{
                                .interfaceType = toInterfaceType,
                                .location = fromExpr->loc});
                        }
                    }
                }
            }
        }
    }
    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(SemanticsVisitor* ctx, Type* fromType, Val* modifier)
{
    switch (modifier->astNodeType)
    {
    default:
        return false;

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

    case ASTNodeType::NoDiffModifierVal:
        if (ctx->isTypeDifferentiable(fromType) && !ctx->getAllowDroppingDerivatives())
            return false;
        else
            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,
    TypeCoercionWitness** outWitnessOfConversion)
{
    auto setWitnessOfConversionToBuiltinConversion = [&]()
    {
        if (outWitnessOfConversion)
            *outWitnessOfConversion =
                getASTBuilder()->getBuiltinTypeCoercionWitness(fromType, toType);
    };


    // 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 ((candidate->type.type) && canCoerce(toType, candidate->type, candidate))
                coercibleCandidates.add(candidate);
        }
        if (coercibleCandidates.getCount() == 1)
        {
            return _coerce(
                site,
                toType,
                outToExpr,
                coercibleCandidates[0]->type,
                coercibleCandidates[0],
                sink,
                outCost,
                outWitnessOfConversion);
        }
        if (sink)
        {
            auto firstCandidate = overloadedExpr2->candidateExprs.getCount() > 0
                                      ? overloadedExpr2->candidateExprs[0]
                                      : nullptr;
            if (auto declCandidate = as<DeclRefExpr>(firstCandidate))
            {
                sink->diagnose(Diagnostics::AmbiguousReference{
                    .name = getText(declCandidate->declRef.getName()),
                    .location = fromExpr->loc});
            }
            else
            {
                sink->diagnose(Diagnostics::AmbiguousExpression{.expr = fromExpr});
            }
        }
        return false;
    }

    if (outToExpr)
    {
        // Before falling back to generic type equality, check for fully-known invalid
        // shape-pack transforms in the generic arguments of the types being compared.
        // This lets us report the more specific `__shapeConcat` / `__shapePermute` /
        // `__shapeSwap` / `__shapeReduce` diagnostics instead of a later generic
        // type-mismatch error.
        bool diagnosedKnownInvalidShapePack =
            _diagnoseKnownInvalidShapePackTransforms(toType, fromType.type, fromExpr->loc, sink);
        if (diagnosedKnownInvalidShapePack)
        {
            *outToExpr = CreateErrorExpr(fromExpr);
            if (outCost)
                *outCost = kConversionCost_None;
            return true;
        }
    }

    // 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;
        setWitnessOfConversionToBuiltinConversion();
        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;
                            setWitnessOfConversionToBuiltinConversion();
                            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;
        setWitnessOfConversionToBuiltinConversion();
        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 coercible.
        //
        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)
        {
            // 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(this, fromType, 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, recursively coerce the base types.
            Expr* coercedBaseExpr = nullptr;
            ConversionCost baseCost = kConversionCost_None;

            if (!_coerce(
                    site,
                    toBase,
                    outToExpr ? &coercedBaseExpr : nullptr,
                    fromBase,
                    fromExpr,
                    sink,
                    &baseCost,
                    outWitnessOfConversion))
            {
                return false;
            }

            // TODO: we may need a cost to allow disambiguation of overloads based on modifiers?
            if (outCost)
            {
                *outCost = baseCost;
            }
            if (outToExpr)
            {
                // If the target type has modifiers, wrap the coerced expression
                // in a modifier cast to apply them.
                if (toModified)
                {
                    *outToExpr = createModifierCast(toType, coercedBaseExpr->type, coercedBaseExpr);
                }
                else
                {
                    *outToExpr = coercedBaseExpr;
                }
            }

            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;
            }
            setWitnessOfConversionToBuiltinConversion();

            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))
    {
        // Guard against Optional<T> where T is opaque — this catches the generic
        // specialization case (e.g. process<SamplerState>(none)) that CoerceToUsableType
        // misses because T was still abstract at declaration time.
        // Only emit the diagnostic when we are constructing the final expression
        // (outToExpr != nullptr); canCoerce() probes with outToExpr=nullptr but
        // also passes getSink(), so we must not emit there to avoid duplicates.
        auto optType = as<OptionalType>(toType);
        if (typeTransitivelyContainsOpaqueHandle(this, optType->getValueType()))
        {
            if (sink && outToExpr)
                sink->diagnose(Diagnostics::OptionalCannotWrapResourceType{
                    .type = optType->getValueType(),
                    .expr = fromExpr});
            return false;
        }
        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;
    }

    // Optional<T> can be implicitly cast to Optional<U> when T is coercible to U.
    if (auto fromOptType = as<OptionalType>(fromType))
    {
        if (auto toOptType = as<OptionalType>(toType))
        {
            Type* fromInner = fromOptType->getValueType();
            Type* toInner = toOptType->getValueType();

            if (!fromInner->equals(toInner))
            {
                // Create a synthetic VarDecl of type fromInner as a placeholder for the
                // extracted inner value. We build innerVarExpr here (even in cost-probe mode)
                // because the inner _coerce call needs a properly-typed fromExpr; passing
                // nullptr would risk a null-deref in the ExplicitRefType path.
                auto innerVarDecl = getASTBuilder()->create<VarDecl>();
                innerVarDecl->nameAndLoc.name = getName("__optInner");
                innerVarDecl->type.type = fromInner;

                auto innerVarExpr = getASTBuilder()->create<VarExpr>();
                innerVarExpr->type = QualType(fromInner);
                innerVarExpr->loc = fromExpr ? fromExpr->loc : SourceLoc();
                innerVarExpr->declRef = makeDeclRef(innerVarDecl);

                // Cost probe: suppress diagnostics on failure.
                ConversionCost innerCost = kConversionCost_None;
                bool innerCoercible = _coerce(
                    site,
                    toInner,
                    nullptr,
                    QualType(fromInner),
                    innerVarExpr,
                    nullptr,
                    &innerCost,
                    nullptr);

                if (innerCoercible)
                {
                    if (outCost)
                    {
                        // +1 so that Optional<T>->Optional<U> (with T!=U) costs more than an
                        // exact Optional<T>->Optional<T> match (cost 0), while still ranking
                        // below any direct non-Optional conversion.
                        *outCost = innerCost + 1;
                    }
                    if (outToExpr)
                    {
                        // Re-run the inner conversion in build mode to construct the
                        // coerced expression. Note that a probe (outToExpr==nullptr) can
                        // succeed for an *ambiguous* inner conversion while the build path
                        // fails: _coerce returns true when probing an ambiguous conversion
                        // but sets outToExpr to an error expr and returns false when asked
                        // to reify it (see the AmbiguousConversion path). So do not assert
                        // success here — on failure the inner call has already emitted the
                        // specific diagnostic (via sink), so just propagate the failure.
                        Expr* innerCoercedExpr = nullptr;
                        bool ok = _coerce(
                            site,
                            toInner,
                            &innerCoercedExpr,
                            QualType(fromInner),
                            innerVarExpr,
                            sink,
                            nullptr,
                            nullptr);
                        if (!ok)
                        {
                            *outToExpr = CreateErrorExpr(fromExpr);
                            return false;
                        }

                        auto castExpr = getASTBuilder()->create<CastOptionalExpr>();
                        castExpr->loc = fromExpr->loc;
                        castExpr->type = QualType(toType);
                        castExpr->checked = true;
                        castExpr->valueArg = fromExpr;
                        castExpr->innerVarDecl = innerVarDecl;
                        castExpr->innerCoercedExpr = innerCoercedExpr;
                        *outToExpr = castExpr;
                    }
                    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;
        }
    }

    // The reverse direction is not an implicit conversion, but explicit cast/constructor syntax
    // (`EnumTag(x)` or `(EnumTag)x`) should first coerce `x` to the enum's tag type and then wrap
    // that tag value in the nominal enum type.
    if (site == CoercionSite::ExplicitCoercion)
    {
        if (auto toEnumDeclRef = isDeclRefTypeOf<EnumDecl>(toType))
        {
            auto tagType = getTagType(m_astBuilder, toEnumDeclRef);
            if (!tagType)
                return false;

            Expr* tagExpr = nullptr;
            ConversionCost tagCost = kConversionCost_None;
            if (_coerce(
                    site,
                    tagType,
                    outToExpr ? &tagExpr : nullptr,
                    fromType,
                    fromExpr,
                    sink,
                    &tagCost,
                    nullptr))
            {
                if (outCost)
                    *outCost = tagCost + kConversionCost_Explicit;
                if (outToExpr)
                {
                    auto enumExpr = getASTBuilder()->create<BuiltinCastExpr>();
                    enumExpr->type = toType;
                    if (fromExpr)
                        enumExpr->loc = fromExpr->loc;
                    enumExpr->base = tagExpr;
                    *outToExpr = enumExpr;
                }
                setWitnessOfConversionToBuiltinConversion();
                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))
    {
        // Interface-to-interface upcast may receive a compressed witness
        // whose internal `DeclRef` is rooted in an intermediate interface
        // rather than `fromType`.  Expand it into a transitive chain so
        // IR lowering can walk the inheritance path mechanically without
        // inspecting the inheritance graph itself.
        if (isInterfaceType(fromType) && isInterfaceType(toType))
        {
            witness = normalizeSubtypeWitnessForInterfaceUpcast(fromType, witness);
        }

        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 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.
                (*outToExpr)->type.isLeftValue = !isInterfaceType(toType);
            }
        }
        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);
                // Type-equality casts preserve l-value status since the
                // types are structurally equivalent (e.g., T.Differential == T
                // via a where clause).
                if (fromExpr)
                    (*outToExpr)->type.isLeftValue = fromExpr->type.isLeftValue;
            }
            if (outCost)
                *outCost = 0;
            return true;
        }
    }

    // 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,
                outWitnessOfConversion))
        {
            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,
                outWitnessOfConversion))
        {
            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;
    }

    // Fast path: at an implicit coercion site, an opaque generic type parameter
    // cannot convert to a concrete scalar builtin, so skip the initializer-based
    // conversion search that makes up the rest of this function. Ranking an
    // operator call on a constrained generic `T` against the many concrete
    // scalar overloads otherwise runs one full (always-failing) conversion
    // search per rejected candidate — the dominant semantic-checking cost in
    // generic-heavy code (issue #11897). The `ImplicitCastMethodKey` failure
    // cache below cannot absorb this: its key includes the `Type*`, and every
    // generic declaration has a distinct `T` type object.
    //
    // Soundness: by this point the exact-match, subtype-witness, and
    // type-equality-witness paths have all failed, and at an implicit site
    // nested conversions are disallowed, so an initializer could only match
    // `T` exactly — and no scalar builtin declares an implicit initializer
    // with an opaque generic parameter type (a contingent core-module
    // property, pinned by generic-arithmetic-coerce-diag.slang). The one
    // exception is `bool` (`__init<T : __EnumType>(T)` in core.meta.slang),
    // so `bool` targets are excluded. Everything else stays on the search
    // path: explicit casts, vector/matrix/aggregate targets, and `DeclRefType`
    // scalars such as `BFloat16`/FP8 (which do declare generic initializers).
    // Pack elements never reach this test (`each T` has type `EachType`, not
    // a decl-ref); a bare pack-parameter decl-ref, should a checker path ever
    // form one at a coercion, is intentionally rejected under the same
    // argument (no scalar builtin accepts a pack). The boundaries are pinned
    // by tests/language-feature/generics/generic-arithmetic-coerce*.slang.
    const auto toTypeBasic = as<BasicExpressionType>(toType);
    const bool toTypeIsScalarBuiltin = toTypeBasic && toTypeBasic->getBaseType() != BaseType::Bool;
    const auto fromTypeGenericParamDeclRef =
        isDeclRefTypeOf<GenericTypeParamDeclBase>(fromType.type);
    if (site != CoercionSite::ExplicitCoercion && toTypeIsScalarBuiltin &&
        fromTypeGenericParamDeclRef)
    {
        return _failedCoercion(toType, outToExpr, fromExpr, sink);
    }

    // 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 (outWitnessOfConversion)
            *outWitnessOfConversion = getASTBuilder()->getDeclRefTypeCoercionWitness(
                fromType,
                toType,
                overloadContext.bestCandidate->item.declRef);
        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(Diagnostics::AmbiguousConversion{
                    .fromType = fromType.type,
                    .toType = toType,
                    .expr = fromExpr});
                for (auto candidate : overloadContext.bestCandidates)
                {
                    sink->diagnose(
                        Diagnostics::SeeDeclarationOf{.decl = candidate.item.declRef.getDecl()});
                }
            }

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

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

        if (outWitnessOfConversion)
            *outWitnessOfConversion = getASTBuilder()->getDeclRefTypeCoercionWitness(
                fromType,
                toType,
                method.conversionFuncOverloadCandidate.item.declRef);
        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)
        {
            bool overflowWarningDetected = false;
            bool isCoreModule = false;
            if (auto module = getShared()->getModule())
                if (auto moduleDecl = module->getModuleDecl())
                    isCoreModule = moduleDecl->hasModifier<FromCoreModuleModifier>();

            // Cache the constant-fold result so both the overflow check and
            // the UnrecommendedImplicitConversion check can reuse it.
            ConstantIntVal* cachedFoldedVal = nullptr;
            bool hasFolded = false;
            auto getFoldedIntVal = [&]() -> ConstantIntVal*
            {
                if (!hasFolded)
                {
                    cachedFoldedVal = as<ConstantIntVal>(tryFoldIntegerConstantExpression(
                        fromExpr,
                        ConstantFoldingKind::CompileTime,
                        nullptr));
                    hasFolded = true;
                }
                return cachedFoldedVal;
            };

            int maxBitSize = getMaximumTypeBitSize(toType);
            if (!isCoreModule && maxBitSize > 0 && cost < kConversionCost_Explicit)
            {
                if (auto val = getFoldedIntVal())
                {
                    IntegerLiteralValue v = val->getValue();
                    bool overflow = false;
                    if (v < 0)
                    {
                        // Two's complement minimum for N bits is -(2^(N-1)).
                        // For 64-bit targets, any int64_t value fits by definition.
                        if (maxBitSize < 64)
                        {
                            int64_t minValue = -(INT64_C(1) << (maxBitSize - 1));
                            overflow = v < minValue;
                        }
                    }
                    else
                    {
                        // Positive: bit-width comparison to avoid false positives
                        // on hex bit-pattern idioms like int x = 0xFF030206.
                        overflow = getIntValueBitSize(v) > maxBitSize;
                    }

                    if (overflow)
                    {
                        // Set even when sink is null so that the
                        // UnrecommendedImplicitConversion path below is
                        // consistently suppressed for overflow cases.
                        overflowWarningDetected = true;
                        if (sink)
                        {
                            sink->diagnose(Diagnostics::IntegerConstantOverflow{
                                .value = String(val->getValue()),
                                .toType = toType,
                                .expr = fromExpr});
                        }
                    }
                }
            }

            if (cost >= kConversionCost_Explicit)
            {
                if (sink)
                {
                    sink->diagnose(Diagnostics::TypeMismatch{
                        .expectedType = toType,
                        .actualType = fromType,
                        .expr = fromExpr});
                    sink->diagnose(Diagnostics::NoteExplicitConversionPossible{
                        .fromType = fromType.type,
                        .toType = toType,
                        .location = fromExpr->loc});

                    if (auto fromPtrType = as<PtrType>(fromType.type))
                    {
                        if (auto toPtrType = as<PtrType>(toType))
                        {
                            auto fromVal = fromPtrType->getValueType();
                            auto toVal = toPtrType->getValueType();
                            if (!isInterfaceType(fromVal) && isInterfaceType(toVal) &&
                                tryGetSubtypeWitness(fromVal, toVal))
                            {
                                sink->diagnose(Diagnostics::NoteConcreteToInterfacePtrUnsafe{
                                    .from = fromVal,
                                    .to = toVal,
                                    .location = fromExpr->loc});
                            }
                        }
                    }
                }
            }
            // For general implicit conversions with high cost, emit a warning
            // unless the value is a known constant within the target type's
            // range. Skip if the overflow check already covered this case.
            else if (cost >= kConversionCost_Default && !overflowWarningDetected)
            {
                bool shouldEmitGeneralWarning = true;
                if (isScalarIntegerType(toType) || isHalfType(toType))
                {
                    if (auto val = getFoldedIntVal())
                    {
                        if (isIntValueInRangeOfType(val->getValue(), toType))
                        {
                            shouldEmitGeneralWarning = false;
                        }
                    }
                }
                if (shouldEmitGeneralWarning && sink)
                {
                    sink->diagnose(Diagnostics::UnrecommendedImplicitConversion{
                        .fromType = fromType.type,
                        .toType = toType,
                        .expr = fromExpr});
                }
            }

            if (site == CoercionSite::Argument && sink)
            {
                auto builtinConversionKind = getImplicitConversionBuiltinKind(
                    overloadContext.bestCandidate->item.declRef.getDecl());
                if (builtinConversionKind == kBuiltinConversion_FloatToDouble)
                {
                    if (!as<FloatingPointLiteralExpr>(fromExpr))
                        sink->diagnose(Diagnostics::ImplicitConversionToDouble{.expr = fromExpr});
                }
            }
        }
        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]);

            // TODO: Register associated differentiable methods & types here as well.
        }
        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 shouldAddToGlobalCache = 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
            shouldAddToGlobalCache = true;
    }

    // If this type pair isn't covered by the global cache, use
    // the cache that is local to the module.
    if (getShared()->m_typeConversionCostCache.tryGetValue(TypePair{toType, fromType.type}, cost))
    {
        if (outCost)
            *outCost = cost;
        return cost != kConversionCost_Impossible;
    }


    // Store "impossible" to conversion cost cache to prevent infinite recursion in case
    // checking for coercion between toType and fromType requires checking itself again via
    // extensions/overloads.
    getShared()->m_typeConversionCostCache.add(
        TypePair{toType, fromType.type},
        kConversionCost_Impossible);

    // 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,
        nullptr);

    if (outCost)
        *outCost = cost;

    if (!rs)
        cost = kConversionCost_Impossible;
    if (shouldAddToGlobalCache)
    {
        typeCheckingCache->conversionCostCache[cacheKey] = cost;
        getShared()->m_typeConversionCostCache.remove(TypePair{toType, fromType.type});
    }
    else
    {
        getShared()->m_typeConversionCostCache[TypePair{toType, fromType.type}] = 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;
}

SubtypeWitness* SemanticsVisitor::normalizeSubtypeWitnessForInterfaceUpcast(
    Type* subType,
    SubtypeWitness* witness,
    int depth)
{
    // Bound against a corrupt or cyclic inheritance graph.  Well-formed
    // hierarchies are acyclic and extremely shallow in practice; 64 is a
    // generous defensive limit.
    static const int kMaxDepth = 64;
    SLANG_RELEASE_ASSERT(depth < kMaxDepth);

    if (!witness)
        return witness;

    if (auto transitive = as<TransitiveSubtypeWitness>(witness))
    {
        auto subToMid = normalizeSubtypeWitnessForInterfaceUpcast(
            subType,
            transitive->getSubToMid(),
            depth + 1);
        auto midType = subToMid->getSup();
        auto midToSup = normalizeSubtypeWitnessForInterfaceUpcast(
            midType,
            transitive->getMidToSup(),
            depth + 1);
        if (subToMid == transitive->getSubToMid() && midToSup == transitive->getMidToSup())
            return witness;
        return m_astBuilder->getTransitiveSubtypeWitness(subToMid, midToSup);
    }

    if (auto declared = as<DeclaredSubtypeWitness>(witness))
    {
        // Use `getParentDeclRef` so substitutions from the witness's DeclRef
        // propagate to the parent (e.g. for `IDerived<float>: IBase<float>`
        // we need the parent to be `IDerived<float>`, not the unspecialized
        // `IDerived<T>`).
        auto parentInterfaceDeclRef = getParentDeclRef(declared->getDeclRef()).as<InterfaceDecl>();
        if (!parentInterfaceDeclRef)
            return witness;

        auto parentInterfaceType = DeclRefType::create(m_astBuilder, parentInterfaceDeclRef);
        if (parentInterfaceType->equals(subType))
            return witness;

        // The declRef belongs to a different interface than `subType`.
        // Split into (subType : parentInterfaceType) ∘ (parentInterfaceType : sup).
        auto subToParent = tryGetSubtypeWitness(subType, parentInterfaceType);
        SLANG_RELEASE_ASSERT(subToParent);
        subToParent = normalizeSubtypeWitnessForInterfaceUpcast(subType, subToParent, depth + 1);

        auto parentToSup = m_astBuilder->getDeclaredSubtypeWitness(
            parentInterfaceType,
            declared->getSup(),
            declared->getDeclRef());

        return m_astBuilder->getTransitiveSubtypeWitness(subToParent, parentToSup);
    }

    return witness;
}

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::createModifierCast(Type* toType, Type* fromType, Expr* fromExpr)
{
    if (!isTypeDifferentiable(toType) && isTypeDifferentiable(fromType))
    {
        // TODO: It's possible that multiple modifiers are changing..
        // Right now, this just works because no other modifier actually needs any non-trivial
        // handling.
        //
        // However, if that changes, we may need more complex logic for modifier casting.
        //
        auto detachExpr = getCurrentASTBuilder()->create<DetachExpr>();
        detachExpr->loc = fromExpr->loc;
        detachExpr->inner = fromExpr;
        detachExpr->type.type = toType;
        return detachExpr;
    }
    else
    {
        // General case..
        return createModifierCastExpr(toType, fromExpr);
    }
}


Expr* SemanticsVisitor::coerce(
    CoercionSite site,
    Type* toType,
    Expr* fromExpr,
    DiagnosticSink* sink)
{
    Expr* expr = nullptr;
    if (!_coerce(site, toType, &expr, fromExpr->type, fromExpr, sink, nullptr, 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
