#include "slang-ir-any-value-marshalling.h"

#include "core/slang-math.h"
#include "slang-ir-insts.h"
#include "slang-ir-layout.h"
#include "slang-ir-util.h"
#include "slang-legalize-types.h"
#include "slang-target.h"

namespace Slang
{

// The scalar ops copied verbatim into one AnyValue payload word (`int`/`float` via
// `bit_cast<uint>`, `uint` directly). Leaves needing normalization, sub-word masking, or
// 64-bit splitting (`bool`, `half`, `int64_t`, ...) are excluded. `countWordScalarLeaves`
// and both `marshalBasicType` overrides share this one classifier so whole-object
// bulk-copy eligibility cannot diverge from what the per-leaf store actually does.
static bool isVerbatimWordScalar(IROp op)
{
    switch (op)
    {
    case kIROp_IntType:
    case kIROp_UIntType:
    case kIROp_FloatType:
        return true;
    default:
        return false;
    }
}

// This is a subpass of generics lowering IR transformation.
// This pass generates packing/unpacking functions for `AnyValue`s,
// and replaces all `IRPackAnyValue` and `IRUnpackAnyValue` with calls to these
// functions.
struct AnyValueMarshallingContext
{
    IRModule* module;
    TargetProgram* targetProgram;

    // We will use a single work list of instructions that need
    // to be considered for lowering.
    //
    InstWorkList workList;
    InstHashSet workListSet;

    AnyValueMarshallingContext(IRModule* module, TargetProgram* targetProgram)
        : module(module), targetProgram(targetProgram), workList(module), workListSet(module)
    {
    }

    void addToWorkList(IRInst* inst)
    {
        if (!inst)
            return;

        for (auto ii = inst->getParent(); ii; ii = ii->getParent())
        {
            if (as<IRGeneric>(ii))
                return;
        }

        if (workListSet.contains(inst))
            return;

        workList.add(inst);
        workListSet.add(inst);
    }

    // Stores information about generated `AnyValue` struct types.
    struct AnyValueTypeInfo : RefObject
    {
        IRType* type;                 // The generated IR value for the `AnyValue<N>` struct type.
        List<IRStructKey*> fieldKeys; // `IRStructKey`s for the fields of the generated type.
    };

    Dictionary<IRIntegerValue, RefPtr<AnyValueTypeInfo>> generatedAnyValueTypes;

    struct MarshallingFunctionKey
    {
        IRType* originalType;
        IRIntegerValue anyValueSize;
        bool operator==(MarshallingFunctionKey other) const
        {
            return originalType == other.originalType && anyValueSize == other.anyValueSize;
        }
        HashCode getHashCode() const
        {
            return combineHash(Slang::getHashCode(originalType), Slang::getHashCode(anyValueSize));
        }
    };

    struct MarshallingFunctionSet
    {
        IRFunc* packFunc;
        IRFunc* unpackFunc;
    };

    // Stores the generated packing/unpacking functions for lookup.
    Dictionary<MarshallingFunctionKey, MarshallingFunctionSet> mapTypeMarshalingFunctions;

    AnyValueTypeInfo* ensureAnyValueType(IRAnyValueType* type)
    {
        auto size = getIntVal(type->getSize());
        if (auto typeInfo = generatedAnyValueTypes.tryGetValue(size))
            return typeInfo->Ptr();
        RefPtr<AnyValueTypeInfo> info = new AnyValueTypeInfo();
        IRBuilder builder(module);
        builder.setInsertBefore(type);
        auto structType = builder.createStructType();
        info->type = structType;
        StringBuilder nameSb;
        nameSb << "AnyValue" << size;
        builder.addExportDecoration(structType, nameSb.getUnownedSlice());
        auto fieldCount = (size + sizeof(uint32_t) - 1) / sizeof(uint32_t);
        for (decltype(fieldCount) i = 0; i < fieldCount; i++)
        {
            auto key = builder.createStructKey();
            nameSb.clear();
            nameSb << "field" << i;
            builder.addNameHintDecoration(key, nameSb.getUnownedSlice());
            nameSb << "_anyVal" << size;
            builder.addExportDecoration(key, nameSb.getUnownedSlice());
            builder.createStructField(structType, key, builder.getUIntType());
            info->fieldKeys.add(key);
        }
        generatedAnyValueTypes[size] = info;
        return info.Ptr();
    }

    struct TypeMarshallingContext
    {
        TargetRequest* targetRequest;
        AnyValueTypeInfo* anyValInfo;
        uint32_t fieldOffset;
        uint32_t intraFieldOffset;
        IRType* uintPtrType;
        IRInst* anyValueVar;
        TargetProgram* targetProgram;

        // Defines what to do with basic typed data elements.
        virtual void marshalBasicType(
            IRBuilder* builder,
            IRType* dataType,
            IRInst* concreteTypedVar) = 0;
        // Defines what to do with resource handle elements.
        virtual void marshalResourceHandle(
            IRBuilder* builder,
            IRType* dataType,
            IRInst* concreteTypedVar) = 0;

        // Defines what to do with DescriptorHandle types.
        // For non-bindless targets, uses CastDescriptorHandleToUInt2.
        // For bindless targets, marshals based on actual byte size.
        virtual void marshalDescriptorHandle(
            IRBuilder* builder,
            IRType* dataType,
            IRInst* concreteTypedVar,
            bool isBindless,
            IRIntegerValue sizeInBytes) = 0;

        // Round the cursor up to the next multiple of `n` bytes, skipping
        // the padding bytes the payload layout has at this position. The
        // fixed-size variants are convenience wrappers.
        void ensureOffsetAtNByteBoundary(int n)
        {
            SLANG_ASSERT(n > 0);
            auto alignedByteOffset = (uint32_t)align(fieldOffset * 4 + intraFieldOffset, n);
            fieldOffset = alignedByteOffset / 4;
            intraFieldOffset = alignedByteOffset % 4;
        }
        void ensureOffsetAt2ByteBoundary() { ensureOffsetAtNByteBoundary(2); }
        void ensureOffsetAt4ByteBoundary() { ensureOffsetAtNByteBoundary(4); }
        void ensureOffsetAt8ByteBoundary() { ensureOffsetAtNByteBoundary(8); }

        void advanceOffset(uint32_t bytes)
        {
            intraFieldOffset += bytes;
            fieldOffset += intraFieldOffset / 4;
            intraFieldOffset = intraFieldOffset % 4;
        }
    };

    void emitMarshallingCode(
        IRBuilder* builder,
        TypeMarshallingContext* context,
        IRInst* concreteTypedVar)
    {
        auto dataType = cast<IRPtrTypeBase>(concreteTypedVar->getDataType())->getValueType();
        switch (dataType->getOp())
        {
        case kIROp_VoidType:
            // Void type has no data to marshal.
            break;
        case kIROp_IntType:
        case kIROp_FloatType:
        case kIROp_UIntType:
        case kIROp_UInt64Type:
        case kIROp_Int64Type:
        case kIROp_DoubleType:
        case kIROp_Int8Type:
        case kIROp_Int16Type:
        case kIROp_UInt8Type:
        case kIROp_UInt16Type:
        case kIROp_HalfType:
        case kIROp_BoolType:
        case kIROp_IntPtrType:
        case kIROp_UIntPtrType:
        case kIROp_PtrType:
        case kIROp_FloatE4M3Type:
        case kIROp_FloatE5M2Type:
        case kIROp_BFloat16Type:
            context->marshalBasicType(builder, dataType, concreteTypedVar);
            break;
        case kIROp_EnumType:
            {
                auto enumType = static_cast<IREnumType*>(dataType);
                auto tagType = enumType->getTagType();
                context->marshalBasicType(builder, tagType, concreteTypedVar);
                break;
            }
        case kIROp_VectorType:
            {
                auto vectorType = static_cast<IRVectorType*>(dataType);
                auto elementCount = getIntVal(vectorType->getElementCount());
                for (IRIntegerValue i = 0; i < elementCount; i++)
                {
                    auto elementAddr = builder->emitElementAddress(
                        concreteTypedVar,
                        builder->getIntValue(builder->getIntType(), i));
                    emitMarshallingCode(builder, context, elementAddr);
                }
                break;
            }
        case kIROp_MatrixType:
            {
                auto matrixType = static_cast<IRMatrixType*>(dataType);
                auto colCount = getIntVal(matrixType->getColumnCount());
                auto rowCount = getIntVal(matrixType->getRowCount());
                if (getIntVal(matrixType->getLayout()) == SLANG_MATRIX_LAYOUT_COLUMN_MAJOR)
                {
                    for (IRIntegerValue i = 0; i < colCount; i++)
                    {
                        for (IRIntegerValue j = 0; j < rowCount; j++)
                        {
                            auto row = builder->emitElementAddress(
                                concreteTypedVar,
                                builder->getIntValue(builder->getIntType(), j));
                            auto element = builder->emitElementAddress(
                                row,
                                builder->getIntValue(builder->getIntType(), i));
                            emitMarshallingCode(builder, context, element);
                        }
                    }
                }
                else
                {
                    for (IRIntegerValue i = 0; i < rowCount; i++)
                    {
                        auto row = builder->emitElementAddress(
                            concreteTypedVar,
                            builder->getIntValue(builder->getIntType(), i));
                        for (IRIntegerValue j = 0; j < colCount; j++)
                        {
                            auto element = builder->emitElementAddress(
                                row,
                                builder->getIntValue(builder->getIntType(), j));
                            emitMarshallingCode(builder, context, element);
                        }
                    }
                }
                break;
            }
        case kIROp_StructType:
            {
                auto structType = cast<IRStructType>(dataType);

                // Align the struct's start to its natural alignment,
                // and pad its end out to its natural size.
                IRSizeAndAlignment structLayout;
                bool hasStructLayout =
                    SLANG_SUCCEEDED(getNaturalSizeAndAlignment(
                        context->targetRequest,
                        dataType,
                        &structLayout)) &&
                    structLayout.size != IRSizeAndAlignment::kIndeterminateSize &&
                    structLayout.size >= 0;
                if (hasStructLayout)
                    context->ensureOffsetAtNByteBoundary((int)structLayout.alignment);
                uint32_t startFieldOffset = context->fieldOffset;
                uint32_t startIntraFieldOffset = context->intraFieldOffset;

                for (auto field : structType->getFields())
                {
                    auto fieldAddr = builder->emitFieldAddress(
                        builder->getPtrType(field->getFieldType()),
                        concreteTypedVar,
                        field->getKey());
                    emitMarshallingCode(builder, context, fieldAddr);
                }

                if (hasStructLayout)
                {
                    auto packedSize = (int64_t)(context->fieldOffset - startFieldOffset) * 4 +
                                      (int64_t)context->intraFieldOffset - startIntraFieldOffset;
                    // A struct that starts at its natural alignment packs
                    // within its natural size; a violation means the walk
                    // and the natural layout have diverged.
                    SLANG_ASSERT(packedSize <= structLayout.size);
                    if (packedSize < structLayout.size)
                        context->advanceOffset(uint32_t(structLayout.size - packedSize));
                }
                break;
            }
        case kIROp_ArrayType:
            {
                auto arrayType = cast<IRArrayType>(dataType);
                for (IRIntegerValue i = 0; i < getIntVal(arrayType->getElementCount()); i++)
                {
                    auto fieldAddr = builder->emitElementAddress(
                        concreteTypedVar,
                        builder->getIntValue(builder->getIntType(), i));
                    emitMarshallingCode(builder, context, fieldAddr);
                }
                break;
            }
        case kIROp_AnyValueType:
            {
                auto anyValType = cast<IRAnyValueType>(dataType);
                auto info = ensureAnyValueType(anyValType);
                for (auto field : info->fieldKeys)
                {
                    auto fieldAddr = builder->emitFieldAddress(
                        builder->getPtrType(builder->getUIntType()),
                        concreteTypedVar,
                        field);
                    emitMarshallingCode(builder, context, fieldAddr);
                }
                break;
            }
        case kIROp_DescriptorHandleType:
            {
                // DescriptorHandle<T> representation depends on target:
                // - Non-bindless (HLSL, GLSL/SPIRV): uint2 (8 bytes)
                // - Bindless (CUDA, CPU, Metal): same as T (variable size)
                auto targetReq = context->targetProgram->getTargetReq();
                bool bindless = areResourceTypesBindlessOnTarget(targetReq);

                IRSizeAndAlignment sizeAndAlign;
                auto result = getNaturalSizeAndAlignment(targetReq, dataType, &sizeAndAlign);

                IRIntegerValue size = 8; // Default to 8 bytes for non-bindless
                if (SLANG_SUCCEEDED(result) && sizeAndAlign.size > 0)
                {
                    size = sizeAndAlign.size;
                }

                context
                    ->marshalDescriptorHandle(builder, dataType, concreteTypedVar, bindless, size);
                break;
            }
        default:
            if (isResourceType(dataType))
            {
                context->marshalResourceHandle(builder, dataType, concreteTypedVar);
                return;
            }
            SLANG_UNIMPLEMENTED_X("Unimplemented type packing");
            break;
        }
    }

    struct TypePackingContext : TypeMarshallingContext
    {
        virtual void marshalBasicType(IRBuilder* builder, IRType* dataType, IRInst* concreteVar)
            override
        {
            if (isVerbatimWordScalar(dataType->getOp()))
            {
                ensureOffsetAt4ByteBoundary();
                if (fieldOffset < static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                {
                    auto srcVal = builder->emitLoad(concreteVar);
                    auto dstVal = dataType->getOp() == kIROp_UIntType
                                      ? srcVal
                                      : builder->emitBitCast(builder->getUIntType(), srcVal);
                    auto dstAddr = builder->emitFieldAddress(
                        uintPtrType,
                        anyValueVar,
                        anyValInfo->fieldKeys[fieldOffset]);
                    builder->emitStore(dstAddr, dstVal);
                }
                advanceOffset(4);
                return;
            }
            switch (dataType->getOp())
            {
            case kIROp_BoolType:
                {
                    ensureOffsetAt4ByteBoundary();
                    if (fieldOffset < static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                    {
                        auto srcVal = builder->emitLoad(concreteVar);
                        IRInst* args[] = {
                            srcVal,
                            builder->getIntValue(builder->getUIntType(), 1),
                            builder->getIntValue(builder->getUIntType(), 0)};
                        auto dstVal = builder->emitIntrinsicInst(
                            builder->getUIntType(),
                            kIROp_Select,
                            3,
                            args);
                        auto dstAddr = builder->emitFieldAddress(
                            uintPtrType,
                            anyValueVar,
                            anyValInfo->fieldKeys[fieldOffset]);
                        builder->emitStore(dstAddr, dstVal);
                    }
                    advanceOffset(4);
                    break;
                }
            case kIROp_Int16Type:
            case kIROp_UInt16Type:
            case kIROp_HalfType:
            case kIROp_BFloat16Type:
                {
                    ensureOffsetAt2ByteBoundary();
                    if (fieldOffset < static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                    {
                        auto srcVal = builder->emitLoad(concreteVar);
                        if (!isIntegralType(dataType))
                        {
                            srcVal =
                                builder->emitBitCast(builder->getType(kIROp_UInt16Type), srcVal);
                        }
                        srcVal = builder->emitCast(builder->getType(kIROp_UIntType), srcVal);
                        auto dstAddr = builder->emitFieldAddress(
                            uintPtrType,
                            anyValueVar,
                            anyValInfo->fieldKeys[fieldOffset]);
                        auto dstVal = builder->emitLoad(dstAddr);
                        if (intraFieldOffset == 0)
                        {
                            dstVal = builder->emitBitAnd(
                                dstVal->getFullType(),
                                dstVal,
                                builder->getIntValue(builder->getUIntType(), 0xFFFF0000));
                        }
                        else
                        {
                            srcVal = builder->emitShl(
                                srcVal->getFullType(),
                                srcVal,
                                builder->getIntValue(builder->getUIntType(), 16));
                            dstVal = builder->emitBitAnd(
                                dstVal->getFullType(),
                                dstVal,
                                builder->getIntValue(builder->getUIntType(), 0xFFFF));
                        }
                        dstVal = builder->emitBitOr(dstVal->getFullType(), dstVal, srcVal);
                        builder->emitStore(dstAddr, dstVal);
                    }
                    advanceOffset(2);
                    break;
                }
            case kIROp_Int8Type:
            case kIROp_UInt8Type:
            case kIROp_FloatE4M3Type:
            case kIROp_FloatE5M2Type:
                if (fieldOffset < static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                {
                    auto srcVal = builder->emitLoad(concreteVar);
                    if (!isIntegralType(dataType))
                    {
                        srcVal = builder->emitBitCast(builder->getType(kIROp_UInt8Type), srcVal);
                    }
                    srcVal = builder->emitCast(builder->getType(kIROp_UIntType), srcVal);
                    auto dstAddr = builder->emitFieldAddress(
                        uintPtrType,
                        anyValueVar,
                        anyValInfo->fieldKeys[fieldOffset]);
                    auto dstVal = builder->emitLoad(dstAddr);
                    dstVal = builder->emitBitfieldInsert(
                        dstVal->getFullType(),
                        dstVal,
                        srcVal,
                        builder->getIntValue(builder->getUIntType(), 8 * intraFieldOffset),
                        builder->getIntValue(builder->getUIntType(), 8));
                    builder->emitStore(dstAddr, dstVal);
                }
                advanceOffset(1);
                break;
            case kIROp_UInt64Type:
            case kIROp_Int64Type:
            case kIROp_DoubleType:
                ensureOffsetAt8ByteBoundary();
                if (fieldOffset < static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                {
                    auto srcVal = builder->emitLoad(concreteVar);
                    auto dstVal = builder->emitBitCast(builder->getUInt64Type(), srcVal);
                    auto lowBits = builder->emitCast(builder->getUIntType(), dstVal);
                    auto highBits = builder->emitShr(
                        builder->getUInt64Type(),
                        dstVal,
                        builder->getIntValue(builder->getIntType(), 32));
                    highBits = builder->emitCast(builder->getUIntType(), highBits);

                    auto dstAddr = builder->emitFieldAddress(
                        uintPtrType,
                        anyValueVar,
                        anyValInfo->fieldKeys[fieldOffset]);
                    builder->emitStore(dstAddr, lowBits);
                    fieldOffset++;
                    if (fieldOffset < static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                    {
                        dstAddr = builder->emitFieldAddress(
                            uintPtrType,
                            anyValueVar,
                            anyValInfo->fieldKeys[fieldOffset]);
                        builder->emitStore(dstAddr, highBits);
                        fieldOffset++;
                    }
                }
                break;
            case kIROp_PtrType:
            case kIROp_UIntPtrType:
            case kIROp_IntPtrType:
                {
                    auto ptrSize = getPointerSize(targetRequest);
                    ensureOffsetAtNByteBoundary(int(ptrSize));
                    if (fieldOffset < static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                    {
                        auto srcVal = builder->emitLoad(concreteVar);

                        if (ptrSize == 8)
                        {
                            // Use uint2 instead of uint64 to avoid Int64 capability requirement
                            auto uint2Type = builder->getVectorType(builder->getUIntType(), 2);
                            auto uint2Val = builder->emitBitCast(uint2Type, srcVal);
                            auto lowBits = builder->emitElementExtract(uint2Val, IRIntegerValue(0));
                            auto highBits =
                                builder->emitElementExtract(uint2Val, IRIntegerValue(1));

                            auto dstAddr = builder->emitFieldAddress(
                                uintPtrType,
                                anyValueVar,
                                anyValInfo->fieldKeys[fieldOffset]);
                            builder->emitStore(dstAddr, lowBits);
                            fieldOffset++;
                            if (fieldOffset <
                                static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                            {
                                dstAddr = builder->emitFieldAddress(
                                    uintPtrType,
                                    anyValueVar,
                                    anyValInfo->fieldKeys[fieldOffset]);
                                builder->emitStore(dstAddr, highBits);
                                fieldOffset++;
                            }
                        }
                        else if (ptrSize == 4)
                        {
                            auto dstVal = builder->emitBitCast(builder->getUIntType(), srcVal);
                            auto dstAddr = builder->emitFieldAddress(
                                uintPtrType,
                                anyValueVar,
                                anyValInfo->fieldKeys[fieldOffset]);
                            builder->emitStore(dstAddr, dstVal);
                        }
                        else
                            SLANG_UNIMPLEMENTED_X(
                                "Pointer sizes other than 32 or 64 haven't been implemented!");
                    }
                }
                break;
            default:
                SLANG_UNREACHABLE("unknown basic type");
            }
        }

        virtual void marshalResourceHandle(
            IRBuilder* builder,
            IRType* dataType,
            IRInst* concreteVar) override
        {
            ensureOffsetAt4ByteBoundary();

            IRSizeAndAlignment sizeAndAlign;
            IRIntegerValue size = 8;
            if (SLANG_SUCCEEDED(
                    getNaturalSizeAndAlignment(targetRequest, dataType, &sizeAndAlign)) &&
                sizeAndAlign.size > 0)
            {
                size = sizeAndAlign.size;
            }

            IRIntegerValue numUints = (size + 3) / 4;
            if (fieldOffset + numUints - 1 <
                static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
            {
                auto srcVal = builder->emitLoad(concreteVar);
                auto uintNType =
                    builder->getVectorType(builder->getUIntType(), (IRIntegerValue)numUints);
                auto uintNVal = builder->emitBitCast(uintNType, srcVal);
                for (IRIntegerValue i = 0; i < numUints; i++)
                {
                    auto bits = builder->emitElementExtract(uintNVal, i);
                    auto dstAddr = builder->emitFieldAddress(
                        uintPtrType,
                        anyValueVar,
                        anyValInfo->fieldKeys[fieldOffset + (uint32_t)i]);
                    builder->emitStore(dstAddr, bits);
                }
                advanceOffset((uint32_t)size);
            }
        }

        // pack: DescriptorHandle -> AnyValue
        virtual void marshalDescriptorHandle(
            IRBuilder* builder,
            IRType* dataType,
            IRInst* concreteVar,
            bool isBindless,
            IRIntegerValue sizeInBytes) override
        {
            ensureOffsetAt4ByteBoundary();

            if (!isBindless)
            {
                // Non-bindless targets: Use CastDescriptorHandleToUInt2
                // This has proper lowering support in GLSL/SPIRV/HLSL emitters
                if (fieldOffset + 1 < static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                {
                    auto srcVal = builder->emitLoad(concreteVar);
                    auto uint2Type = builder->getVectorType(builder->getUIntType(), 2);

                    // Use the explicit IR cast op instead of bitcast
                    auto uint2Val = builder->emitIntrinsicInst(
                        uint2Type,
                        kIROp_CastDescriptorHandleToUInt2,
                        1,
                        &srcVal);

                    auto lowBits = builder->emitElementExtract(uint2Val, IRIntegerValue(0));
                    auto highBits = builder->emitElementExtract(uint2Val, IRIntegerValue(1));

                    auto dstAddr1 = builder->emitFieldAddress(
                        uintPtrType,
                        anyValueVar,
                        anyValInfo->fieldKeys[fieldOffset]);
                    builder->emitStore(dstAddr1, lowBits);

                    auto dstAddr2 = builder->emitFieldAddress(
                        uintPtrType,
                        anyValueVar,
                        anyValInfo->fieldKeys[fieldOffset + 1]);
                    builder->emitStore(dstAddr2, highBits);
                }
                advanceOffset(8);
            }
            else
            {
                // Bindless targets (CUDA, CPU, Metal): DescriptorHandle is a native type
                // Marshal as raw bytes based on actual size (8 or 16 bytes)
                SLANG_UNUSED(dataType);

                auto srcVal = builder->emitLoad(concreteVar);

                // sizeInBytes should be either 8 or 16
                const IRIntegerValue numFieldsNeeded = (sizeInBytes + 3) / 4;
                SLANG_ASSERT(numFieldsNeeded == 2 || numFieldsNeeded == 4);

                if (fieldOffset + numFieldsNeeded <=
                    static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                {
                    if (isMetalTarget(targetRequest) && sizeInBytes == 8)
                    {
                        // Metal: DescriptorHandle is a pointer; as_type<> can't cast
                        // between pointer and non-pointer types. Convert through uint64
                        // using CastDescriptorHandleToUInt64 which emits a C-style cast.
                        auto uint64Val = builder->emitIntrinsicInst(
                            builder->getUInt64Type(),
                            kIROp_CastDescriptorHandleToUInt64,
                            1,
                            &srcVal);
                        auto lowBits = builder->emitCast(builder->getUIntType(), uint64Val);
                        auto highBits = builder->emitShr(
                            builder->getUInt64Type(),
                            uint64Val,
                            builder->getIntValue(builder->getIntType(), 32));
                        highBits = builder->emitCast(builder->getUIntType(), highBits);

                        auto dstAddr0 = builder->emitFieldAddress(
                            uintPtrType,
                            anyValueVar,
                            anyValInfo->fieldKeys[fieldOffset]);
                        builder->emitStore(dstAddr0, lowBits);
                        auto dstAddr1 = builder->emitFieldAddress(
                            uintPtrType,
                            anyValueVar,
                            anyValInfo->fieldKeys[fieldOffset + 1]);
                        builder->emitStore(dstAddr1, highBits);
                    }
                    else
                    {
                        // Metal pointers are always 8 bytes; bitcast is invalid for
                        // pointers on Metal so this path must not be reached.
                        SLANG_ASSERT(!isMetalTarget(targetRequest));
                        // CUDA/CPU: bitcast works for non-pointer native handles
                        auto uintVectorType =
                            builder->getVectorType(builder->getUIntType(), numFieldsNeeded);
                        auto uintVectorVal = builder->emitBitCast(uintVectorType, srcVal);

                        for (IRIntegerValue i = 0; i < numFieldsNeeded; i++)
                        {
                            auto bits = builder->emitElementExtract(uintVectorVal, i);
                            auto dstAddr = builder->emitFieldAddress(
                                uintPtrType,
                                anyValueVar,
                                anyValInfo->fieldKeys[fieldOffset + i]);
                            builder->emitStore(dstAddr, bits);
                        }
                    }
                }
                advanceOffset((uint32_t)sizeInBytes);
            }
        }
    };

    // Count the leaves of `type`, requiring every one to be a 4-byte scalar whose
    // bit pattern is marshalled verbatim into a `uint` payload slot; return -1 if any
    // leaf is not such a scalar. A word scalar is packed by
    // `TypePackingContext::marshalBasicType` as a plain `bit_cast<uint>` store with no
    // normalization, sub-word masking, 64-bit splitting, or target-specific handling,
    // so a whole-object copy of it reproduces the field-wise store exactly.
    //
    // Consider `struct Circle { float3 center; float radius; }`: its four `float`
    // leaves each become one `field[k] = bit_cast<uint>(...)`, so this returns 4.
    // Types with a `bool` (normalized to 0/1), a `half`/`int8`/`int64`/pointer/
    // `DescriptorHandle` leaf, or a resource handle return -1 because their per-leaf
    // marshalling is not a verbatim word copy.
    //
    // Matrices always return -1 (via the explicit `kIROp_MatrixType` case below). For a
    // column-major matrix this is required for correctness: `emitMarshallingCode` walks it
    // column-by-column, but the emitted C++/CUDA `Matrix<T,R,C>` stores rows contiguously,
    // so a whole-object copy would reorder the elements even though the leaf count and size
    // match. A row-major matrix is walked in storage order and would not be reordered, so
    // rejecting it is a conservative choice rather than a byte-equivalence requirement (and
    // on CUDA a matrix row is often a `float4`, which the alignment condition rejects
    // anyway). Rejecting both keeps the predicate uniform and independent of matrix layout.
    IRIntegerValue countWordScalarLeaves(IRType* type)
    {
        if (isVerbatimWordScalar(type->getOp()))
            return 1;
        switch (type->getOp())
        {
        case kIROp_EnumType:
            return countWordScalarLeaves(cast<IREnumType>(type)->getTagType());
        case kIROp_VectorType:
            {
                auto vecType = cast<IRVectorType>(type);
                auto elementLeaves = countWordScalarLeaves(vecType->getElementType());
                if (elementLeaves < 0)
                    return -1;
                auto total = elementLeaves * getIntVal(vecType->getElementCount());
                // Reject a zero-width vector (e.g. `vector<float,0>`) for the same reason
                // as an empty struct/array below: it contributes no payload words, but a
                // value containing it is split by legalization into a non-simple tuple that
                // the whole-object bit-cast cannot handle.
                return total > 0 ? total : -1;
            }
        case kIROp_MatrixType:
            return -1;
        case kIROp_ArrayType:
            {
                auto arrType = cast<IRArrayType>(type);
                auto elementLeaves = countWordScalarLeaves(arrType->getElementType());
                if (elementLeaves < 0)
                    return -1;
                auto total = elementLeaves * getIntVal(arrType->getElementCount());
                // Reject an empty (zero-leaf) aggregate: it carries no payload words,
                // but `legalizeEmptyTypes` still splits a value containing it into a
                // non-simple tuple, and a whole-object bit-cast has no type-legalization
                // handler for such an operand (it would abort with "non-simple operand").
                return total > 0 ? total : -1;
            }
        case kIROp_StructType:
            {
                IRIntegerValue total = 0;
                for (auto field : cast<IRStructType>(type)->getFields())
                {
                    auto fieldLeaves = countWordScalarLeaves(field->getFieldType());
                    if (fieldLeaves < 0)
                        return -1;
                    total += fieldLeaves;
                }
                // See the array case: reject empty aggregates so a value that
                // legalization would split never reaches the whole-object bit-cast.
                return total > 0 ? total : -1;
            }
        default:
            return -1;
        }
    }

    // Return true when boxing `type` into `anyValueType` can be a single whole-object
    // bit-cast instead of the per-leaf `emitMarshallingCode` walk.
    //
    // This holds only on the C-family source emitters whose prelude defines
    // `slang_bit_cast` over arbitrary types (CUDA/OptiX and CPU/C++, which share one
    // emitter — but not the CPU-via-LLVM path, where an aggregate bit-cast is not a
    // valid instruction), and only when `type` is byte-compatible with the flat `uint`
    // payload:
    //   1. every leaf is a 4-byte scalar marshalled verbatim (`countWordScalarLeaves`);
    //   2. `type` has no interior alignment padding in its *emitted* layout — the
    //      target C/CUDA ABI size equals `4 * (leaf count)`. This must use the emitted
    //      layout rules, not the natural rules: the per-leaf walk packs leaves densely
    //      at a 4-byte stride (and the box size is inferred with natural rules), but the
    //      whole-object copy moves the type in its emitted ABI layout. For example
    //      `struct { float a; float4 b; }` is dense under natural rules (size 20) yet
    //      the emitted CUDA `float4` is 16-byte aligned, giving `sizeof == 32` with a
    //      12-byte gap after `a`; the ABI-size check rejects it, the natural-size check
    //      would not; and
    //   3. that emitted size equals the payload byte size, so the copy neither
    //      over-reads the payload nor leaves high payload words that the field-wise
    //      path zero-initializes; and
    //   4. `type`'s emitted alignment is no stricter than the payload's. The payload is
    //      an array of `uint`, so it is 4-byte aligned; `slang_bit_cast<T>` reads its
    //      argument through a `T*`, so a `T` needing stricter alignment (e.g. CUDA's
    //      `float4`, which is 16-byte aligned) would be dereferenced through an
    //      under-aligned pointer. This is what distinguishes an all-`float4` type on
    //      CUDA (rejected, 16-byte aligned) from CPP (accepted, C-rules 4-byte aligned).
    bool canBulkCopyMarshal(IRType* type, IRIntegerValue anyValueSize)
    {
        auto targetReq = targetProgram->getTargetReq();

        // The whole-object `slang_bit_cast` is only emittable by the C++/CUDA source
        // emitters. The LLVM CPU path lowers to an aggregate LLVM bit-cast, which is
        // invalid, so exclude it explicitly.
        bool cLikeSourceEmitter =
            (isCUDATarget(targetReq) || isCPUTarget(targetReq)) && !isCPUTargetViaLLVM(targetReq);
        if (!cLikeSourceEmitter)
            return false;

        auto leafCount = countWordScalarLeaves(type);
        if (leafCount < 0)
            return false;

        // Use the emitted C/CUDA ABI layout (the same rules the target emitter uses),
        // not the natural layout, so interior alignment padding is visible here.
        auto rules =
            isCUDATarget(targetReq) ? IRTypeLayoutRules::getCUDA() : IRTypeLayoutRules::getC();
        IRSizeAndAlignment layout;
        if (SLANG_FAILED(getSizeAndAlignment(targetReq, rules, type, &layout)))
            return false;
        if (layout.size == IRSizeAndAlignment::kIndeterminateSize || layout.size <= 0)
            return false;

        // The payload is a `uint` array (4-byte aligned); a stricter-aligned `type`
        // would be read through an under-aligned pointer by `slang_bit_cast` (condition 4).
        const int kPayloadAlignment = 4;
        if (layout.alignment > kPayloadAlignment)
            return false;

        // No interior padding (condition 2) and an exact fit against the box
        // (condition 3), so `bit_cast<AnyValueN>` reproduces the field-wise payload.
        return layout.size == leafCount * 4 && layout.size == anyValueSize;
    }

    IRFunc* generatePackingFunc(IRType* type, IRAnyValueType* anyValueType)
    {
        IRBuilder builder(module);
        builder.setInsertBefore(type);
        auto anyValInfo = ensureAnyValueType(anyValueType);

        auto func = builder.createFunc();

        StringBuilder nameSb;
        nameSb << "packAnyValue" << getIntVal(anyValueType->getSize());
        builder.addNameHintDecoration(func, nameSb.getUnownedSlice());
        // Currently we don't add linkage to the generated func, since we
        // do not have a way to compute mangled names from an IR entity.
        // This will leads to duplicate packing functions in linked code
        // but there won't be correctness issues.

        auto funcType = builder.getFuncType(1, &type, anyValInfo->type);
        func->setFullType(funcType);
        builder.setInsertInto(func);

        builder.emitBlock();

        auto param = builder.emitParam(type);

        if (canBulkCopyMarshal(type, getIntVal(anyValueType->getSize())))
        {
            builder.emitReturn(builder.emitBitCast(anyValInfo->type, param));
            return func;
        }

        auto concreteTypedVar = builder.emitVar(type);
        builder.emitStore(concreteTypedVar, param);
        auto resultVar = builder.emitVar(anyValInfo->type);

        // Initialize fields to 0 to prevent downstream compiler error.
        for (uint32_t offset = 0; offset < (uint32_t)anyValInfo->fieldKeys.getCount(); offset++)
        {
            auto fieldAddr = builder.emitFieldAddress(
                builder.getPtrType(builder.getUIntType()),
                resultVar,
                anyValInfo->fieldKeys[offset]);
            builder.emitStore(fieldAddr, builder.getIntValue(builder.getUIntType(), 0));
        }

        TypePackingContext context;
        context.targetRequest = targetProgram->getTargetReq();
        context.anyValInfo = anyValInfo;
        context.fieldOffset = context.intraFieldOffset = 0;
        context.uintPtrType = builder.getPtrType(builder.getUIntType());
        context.anyValueVar = resultVar;
        context.targetProgram = targetProgram;
        emitMarshallingCode(&builder, &context, concreteTypedVar);

        auto load = builder.emitLoad(resultVar);
        builder.emitReturn(load);
        return func;
    }

    struct TypeUnpackingContext : TypeMarshallingContext
    {
        virtual void marshalBasicType(IRBuilder* builder, IRType* dataType, IRInst* concreteVar)
            override
        {
            if (isVerbatimWordScalar(dataType->getOp()))
            {
                ensureOffsetAt4ByteBoundary();
                if (fieldOffset < static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                {
                    auto srcAddr = builder->emitFieldAddress(
                        uintPtrType,
                        anyValueVar,
                        anyValInfo->fieldKeys[fieldOffset]);
                    auto srcVal = builder->emitLoad(srcAddr);
                    if (dataType->getOp() != kIROp_UIntType)
                        srcVal = builder->emitBitCast(dataType, srcVal);
                    builder->emitStore(concreteVar, srcVal);
                }
                advanceOffset(4);
                return;
            }
            switch (dataType->getOp())
            {
            case kIROp_BoolType:
                {
                    ensureOffsetAt4ByteBoundary();
                    if (fieldOffset < static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                    {
                        auto srcAddr = builder->emitFieldAddress(
                            uintPtrType,
                            anyValueVar,
                            anyValInfo->fieldKeys[fieldOffset]);
                        auto srcVal = builder->emitLoad(srcAddr);
                        srcVal = builder->emitNeq(
                            srcVal,
                            builder->getIntValue(builder->getUIntType(), 0));
                        builder->emitStore(concreteVar, srcVal);
                    }
                    advanceOffset(4);
                    break;
                }
            case kIROp_Int16Type:
            case kIROp_UInt16Type:
            case kIROp_HalfType:
            case kIROp_BFloat16Type:
                {
                    ensureOffsetAt2ByteBoundary();
                    if (fieldOffset < static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                    {
                        auto srcAddr = builder->emitFieldAddress(
                            uintPtrType,
                            anyValueVar,
                            anyValInfo->fieldKeys[fieldOffset]);
                        auto srcVal = builder->emitLoad(srcAddr);
                        if (intraFieldOffset == 0)
                        {
                            srcVal = builder->emitBitAnd(
                                srcVal->getFullType(),
                                srcVal,
                                builder->getIntValue(builder->getUIntType(), 0xFFFF));
                        }
                        else
                        {
                            srcVal = builder->emitShr(
                                srcVal->getFullType(),
                                srcVal,
                                builder->getIntValue(builder->getUIntType(), 16));
                        }
                        if (dataType->getOp() == kIROp_Int16Type)
                        {
                            srcVal = builder->emitCast(builder->getType(kIROp_Int16Type), srcVal);
                        }
                        else
                        {
                            srcVal = builder->emitCast(builder->getType(kIROp_UInt16Type), srcVal);
                        }
                        if (dataType != srcVal->getDataType())
                        {
                            srcVal = builder->emitBitCast(dataType, srcVal);
                        }
                        builder->emitStore(concreteVar, srcVal);
                    }
                    advanceOffset(2);
                    break;
                }
            case kIROp_Int8Type:
            case kIROp_UInt8Type:
            case kIROp_FloatE4M3Type:
            case kIROp_FloatE5M2Type:
                if (fieldOffset < static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                {
                    auto srcAddr = builder->emitFieldAddress(
                        uintPtrType,
                        anyValueVar,
                        anyValInfo->fieldKeys[fieldOffset]);
                    auto srcVal = builder->emitLoad(srcAddr);
                    srcVal = builder->emitBitfieldExtract(
                        srcVal->getFullType(),
                        srcVal,
                        builder->getIntValue(builder->getUIntType(), 8 * intraFieldOffset),
                        builder->getIntValue(builder->getUIntType(), 8));
                    if (dataType->getOp() == kIROp_Int8Type)
                    {
                        srcVal = builder->emitCast(builder->getType(kIROp_Int8Type), srcVal);
                    }
                    else
                    {
                        srcVal = builder->emitCast(builder->getType(kIROp_UInt8Type), srcVal);
                    }
                    if (dataType != srcVal->getDataType())
                    {
                        srcVal = builder->emitBitCast(dataType, srcVal);
                    }
                    builder->emitStore(concreteVar, srcVal);
                }
                advanceOffset(1);
                break;
            case kIROp_UInt64Type:
            case kIROp_Int64Type:
            case kIROp_DoubleType:
                ensureOffsetAt8ByteBoundary();
                if (fieldOffset < static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                {
                    auto srcAddr = builder->emitFieldAddress(
                        uintPtrType,
                        anyValueVar,
                        anyValInfo->fieldKeys[fieldOffset]);
                    auto lowBits = builder->emitLoad(srcAddr);
                    fieldOffset++;
                    if (fieldOffset < static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                    {
                        auto srcAddr1 = builder->emitFieldAddress(
                            uintPtrType,
                            anyValueVar,
                            anyValInfo->fieldKeys[fieldOffset]);
                        fieldOffset++;
                        auto highBits = builder->emitLoad(srcAddr1);
                        auto combinedBits = builder->emitMakeUInt64(lowBits, highBits);
                        if (dataType->getOp() != kIROp_UInt64Type)
                            combinedBits = builder->emitBitCast(dataType, combinedBits);
                        builder->emitStore(concreteVar, combinedBits);
                    }
                }
                break;
            case kIROp_PtrType:
            case kIROp_IntPtrType:
            case kIROp_UIntPtrType:
                {
                    auto ptrSize = getPointerSize(targetRequest);
                    ensureOffsetAtNByteBoundary(int(ptrSize));
                    if (fieldOffset < static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                    {
                        auto srcAddr = builder->emitFieldAddress(
                            uintPtrType,
                            anyValueVar,
                            anyValInfo->fieldKeys[fieldOffset]);
                        if (ptrSize == 8)
                        {
                            auto lowBits = builder->emitLoad(srcAddr);
                            fieldOffset++;
                            if (fieldOffset <
                                static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                            {
                                auto srcAddr1 = builder->emitFieldAddress(
                                    uintPtrType,
                                    anyValueVar,
                                    anyValInfo->fieldKeys[fieldOffset]);
                                fieldOffset++;
                                auto highBits = builder->emitLoad(srcAddr1);
                                // Use uint2 instead of uint64 to avoid Int64 capability requirement
                                auto uint2Type = builder->getVectorType(builder->getUIntType(), 2);
                                IRInst* components[2] = {lowBits, highBits};
                                auto uint2Val = builder->emitMakeVector(uint2Type, 2, components);
                                auto combinedBits = builder->emitBitCast(dataType, uint2Val);
                                builder->emitStore(concreteVar, combinedBits);
                            }
                        }
                        else if (ptrSize == 4)
                        {
                            auto srcVal = builder->emitLoad(srcAddr);
                            srcVal = builder->emitBitCast(dataType, srcVal);
                            builder->emitStore(concreteVar, srcVal);
                            advanceOffset(4);
                        }
                        else
                            SLANG_UNIMPLEMENTED_X(
                                "Pointer sizes other than 32 or 64 haven't been implemented!");
                    }
                }
                break;
            default:
                SLANG_UNREACHABLE("unknown basic type");
            }
        }

        virtual void marshalResourceHandle(
            IRBuilder* builder,
            IRType* dataType,
            IRInst* concreteVar) override
        {
            ensureOffsetAt4ByteBoundary();

            IRSizeAndAlignment sizeAndAlign;
            IRIntegerValue size = 8;
            if (SLANG_SUCCEEDED(
                    getNaturalSizeAndAlignment(targetRequest, dataType, &sizeAndAlign)) &&
                sizeAndAlign.size > 0)
            {
                size = sizeAndAlign.size;
            }

            IRIntegerValue numUints = (size + 3) / 4;
            if (fieldOffset + numUints - 1 <
                static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
            {
                auto uintNType =
                    builder->getVectorType(builder->getUIntType(), (IRIntegerValue)numUints);
                List<IRInst*> components;
                for (IRIntegerValue i = 0; i < numUints; i++)
                {
                    auto srcAddr = builder->emitFieldAddress(
                        uintPtrType,
                        anyValueVar,
                        anyValInfo->fieldKeys[fieldOffset + (uint32_t)i]);
                    components.add(builder->emitLoad(srcAddr));
                }
                auto uintNVal =
                    builder->emitMakeVector(uintNType, (UInt)numUints, components.getBuffer());
                auto combinedBits = builder->emitBitCast(dataType, uintNVal);
                builder->emitStore(concreteVar, combinedBits);
                advanceOffset((uint32_t)size);
            }
        }

        // unpack: AnyValue -> DescriptorHandle
        virtual void marshalDescriptorHandle(
            IRBuilder* builder,
            IRType* dataType,
            IRInst* concreteVar,
            bool isBindless,
            IRIntegerValue sizeInBytes) override
        {
            ensureOffsetAt4ByteBoundary();

            if (!isBindless)
            {
                // Non-bindless targets: Use CastUInt2ToDescriptorHandle
                if (fieldOffset + 1 < static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                {
                    auto srcAddr = builder->emitFieldAddress(
                        uintPtrType,
                        anyValueVar,
                        anyValInfo->fieldKeys[fieldOffset]);
                    auto lowBits = builder->emitLoad(srcAddr);

                    auto srcAddr1 = builder->emitFieldAddress(
                        uintPtrType,
                        anyValueVar,
                        anyValInfo->fieldKeys[fieldOffset + 1]);
                    auto highBits = builder->emitLoad(srcAddr1);

                    // Build uint2 from components
                    auto uint2Type = builder->getVectorType(builder->getUIntType(), 2);
                    IRInst* components[2] = {lowBits, highBits};
                    auto uint2Val = builder->emitMakeVector(uint2Type, 2, components);

                    // Use the explicit IR cast op instead of bitcast
                    auto result = builder->emitIntrinsicInst(
                        dataType,
                        kIROp_CastUInt2ToDescriptorHandle,
                        1,
                        &uint2Val);

                    builder->emitStore(concreteVar, result);
                }
                advanceOffset(8);
            }
            else
            {
                // Bindless targets (CUDA, CPU, Metal): DescriptorHandle is a native type
                // Unmarshal from raw bytes based on actual size (8 or 16 bytes)

                // sizeInBytes should be either 8 or 16
                const IRIntegerValue numFieldsNeeded = (sizeInBytes + 3) / 4;
                SLANG_ASSERT(numFieldsNeeded == 2 || numFieldsNeeded == 4);

                if (fieldOffset + numFieldsNeeded <=
                    static_cast<uint32_t>(anyValInfo->fieldKeys.getCount()))
                {
                    if (isMetalTarget(targetRequest) && sizeInBytes == 8)
                    {
                        // Metal: DescriptorHandle is a pointer; as_type<> can't cast
                        // between pointer and non-pointer types. Reconstruct through
                        // uint64 using CastUInt64ToDescriptorHandle.
                        auto srcAddr0 = builder->emitFieldAddress(
                            uintPtrType,
                            anyValueVar,
                            anyValInfo->fieldKeys[fieldOffset]);
                        auto lowBits = builder->emitLoad(srcAddr0);

                        auto srcAddr1 = builder->emitFieldAddress(
                            uintPtrType,
                            anyValueVar,
                            anyValInfo->fieldKeys[fieldOffset + 1]);
                        auto highBits = builder->emitLoad(srcAddr1);

                        auto lowBits64 = builder->emitCast(builder->getUInt64Type(), lowBits);
                        auto highBits64 = builder->emitCast(builder->getUInt64Type(), highBits);
                        auto shiftedHigh = builder->emitShl(
                            builder->getUInt64Type(),
                            highBits64,
                            builder->getIntValue(builder->getIntType(), 32));
                        auto combined =
                            builder->emitBitOr(builder->getUInt64Type(), lowBits64, shiftedHigh);

                        auto result = builder->emitIntrinsicInst(
                            dataType,
                            kIROp_CastUInt64ToDescriptorHandle,
                            1,
                            &combined);
                        builder->emitStore(concreteVar, result);
                    }
                    else
                    {
                        // Metal pointers are always 8 bytes; bitcast is invalid for
                        // pointers on Metal so this path must not be reached.
                        SLANG_ASSERT(!isMetalTarget(targetRequest));
                        // CUDA/CPU: bitcast works for non-pointer native handles
                        auto uintVectorType =
                            builder->getVectorType(builder->getUIntType(), numFieldsNeeded);
                        IRInst* components[4]; // max 4 uints needed

                        for (IRIntegerValue i = 0; i < numFieldsNeeded; i++)
                        {
                            auto srcAddr = builder->emitFieldAddress(
                                uintPtrType,
                                anyValueVar,
                                anyValInfo->fieldKeys[fieldOffset + i]);
                            components[i] = builder->emitLoad(srcAddr);
                        }

                        auto uintVecVal =
                            builder->emitMakeVector(uintVectorType, numFieldsNeeded, components);
                        auto result = builder->emitBitCast(dataType, uintVecVal);
                        builder->emitStore(concreteVar, result);
                    }
                }
                advanceOffset((uint32_t)sizeInBytes);
            }
        }
    };

    IRFunc* generateUnpackingFunc(IRType* type, IRAnyValueType* anyValueType)
    {
        IRBuilder builder(module);
        builder.setInsertBefore(type);
        auto anyValInfo = ensureAnyValueType(anyValueType);

        auto func = builder.createFunc();

        StringBuilder nameSb;
        nameSb << "unpackAnyValue" << getIntVal(anyValueType->getSize());
        builder.addNameHintDecoration(func, nameSb.getUnownedSlice());

        auto funcType = builder.getFuncType(1, &anyValInfo->type, type);
        func->setFullType(funcType);
        builder.setInsertInto(func);

        builder.emitBlock();

        auto param = builder.emitParam(anyValInfo->type);

        if (canBulkCopyMarshal(type, getIntVal(anyValueType->getSize())))
        {
            builder.emitReturn(builder.emitBitCast(type, param));
            return func;
        }

        auto anyValueVar = builder.emitVar(anyValInfo->type);
        builder.emitStore(anyValueVar, param);
        auto resultVar = builder.emitVar(type);

        TypeUnpackingContext context;
        context.targetRequest = targetProgram->getTargetReq();
        context.anyValInfo = anyValInfo;
        context.fieldOffset = context.intraFieldOffset = 0;
        context.uintPtrType = builder.getPtrType(builder.getUIntType());
        context.anyValueVar = anyValueVar;
        context.targetProgram = targetProgram;
        emitMarshallingCode(&builder, &context, resultVar);
        auto load = builder.emitLoad(resultVar);
        builder.emitReturn(load);
        return func;
    }

    // Ensures the marshalling functions between `type` and `anyValueType` are already generated.
    // Returns the generated marshalling functions.
    MarshallingFunctionSet ensureMarshallingFunc(IRType* type, IRAnyValueType* anyValueType)
    {
        auto size = getIntVal(anyValueType->getSize());
        MarshallingFunctionKey key;
        key.originalType = type;
        key.anyValueSize = size;
        MarshallingFunctionSet funcSet;
        if (mapTypeMarshalingFunctions.tryGetValue(key, funcSet))
            return funcSet;
        funcSet.packFunc = generatePackingFunc(type, anyValueType);
        funcSet.unpackFunc = generateUnpackingFunc(type, anyValueType);
        mapTypeMarshalingFunctions[key] = funcSet;
        return funcSet;
    }

    void processPackInst(IRPackAnyValue* packInst)
    {
        auto operand = packInst->getValue();
        auto resultType = packInst->getDataType();

        // Earlier lowering can make a symbolic AnyValue pack physically trivial. For example,
        // lowering `UntaggedUnion({Foo, none})` to `Foo` turns `PackAnyValue<Foo>(foo)` into an
        // identity. Remove that identity here, where all AnyValue packs are normally lowered, but
        // retain genuine packs whose result is still an `IRAnyValueType`.
        if (operand->getDataType() == resultType && !as<IRAnyValueType>(resultType))
        {
            packInst->replaceUsesWith(operand);
            packInst->removeAndDeallocate();
            return;
        }

        auto func = ensureMarshallingFunc(operand->getDataType(), cast<IRAnyValueType>(resultType));
        IRBuilder builderStorage(module);
        auto builder = &builderStorage;
        builder->setInsertBefore(packInst);
        auto callInst = builder->emitCallInst(resultType, func.packFunc, 1, &operand);
        packInst->replaceUsesWith(callInst);
        packInst->removeAndDeallocate();
    }

    void processUnpackInst(IRUnpackAnyValue* unpackInst)
    {
        auto operand = unpackInst->getValue();
        auto resultType = unpackInst->getDataType();

        // Apply the same normalization to an unpack whose symbolic input has lowered to its
        // concrete result type. For example, `UnpackAnyValue<Foo>(foo)` becomes `foo`; handling it
        // in the ordinary marshalling pass keeps direct-payload lowering free of a cleanup scan.
        if (operand->getDataType() == resultType && !as<IRAnyValueType>(resultType))
        {
            unpackInst->replaceUsesWith(operand);
            unpackInst->removeAndDeallocate();
            return;
        }

        auto func = ensureMarshallingFunc(resultType, cast<IRAnyValueType>(operand->getDataType()));
        IRBuilder builderStorage(module);
        auto builder = &builderStorage;
        builder->setInsertBefore(unpackInst);
        auto callInst = builder->emitCallInst(resultType, func.unpackFunc, 1, &operand);
        unpackInst->replaceUsesWith(callInst);
        unpackInst->removeAndDeallocate();
    }

    void processAnyValueType(IRAnyValueType* type)
    {
        auto info = ensureAnyValueType(type);
        type->replaceUsesWith(info->type);
    }

    void processInst(IRInst* inst)
    {
        if (auto packInst = as<IRPackAnyValue>(inst))
        {
            processPackInst(packInst);
        }
        else if (auto unpackInst = as<IRUnpackAnyValue>(inst))
        {
            processUnpackInst(unpackInst);
        }
    }

    void processModule()
    {
        // We start by initializing our shared IR building state,
        // since we will re-use that state for any code we
        // generate along the way.
        //
        addToWorkList(module->getModuleInst());

        while (workList.getCount() != 0)
        {
            IRInst* inst = workList.getLast();

            workList.removeLast();
            workListSet.remove(inst);

            processInst(inst);

            for (auto child = inst->getLastChild(); child; child = child->getPrevInst())
            {
                addToWorkList(child);
            }
        }

        // Finally, replace all `AnyValueType` with the actual struct type that implements it.
        for (auto inst : module->getModuleInst()->getChildren())
        {
            if (auto anyValueType = as<IRAnyValueType>(inst))
                processAnyValueType(anyValueType);
        }
    }
};

void generateAnyValueMarshallingFunctions(IRModule* module, TargetProgram* targetProgram)
{
    AnyValueMarshallingContext context(module, targetProgram);
    context.processModule();
}

SlangInt alignUp(SlangInt x, SlangInt alignment)
{
    return (x + alignment - 1) / alignment * alignment;
}

SlangInt _getAnyValueSizeRaw(IRType* type, TargetRequest* targetReq, SlangInt offset)
{
    // Try to get size and alignment from the layout system.
    IRSizeAndAlignment sizeAndAlignment;

    if (SLANG_SUCCEEDED(getSizeAndAlignment(
            targetReq,
            IRTypeLayoutRules::getNatural(),
            type,
            &sizeAndAlignment)))
    {
        // Check for valid size (not indeterminate and non-negative)
        if (sizeAndAlignment.size != IRSizeAndAlignment::kIndeterminateSize &&
            sizeAndAlignment.size >= 0)
        {
            return alignUp(offset, sizeAndAlignment.alignment) + sizeAndAlignment.size;
        }
    }

    // Handle special cases not covered by the layout system
    switch (type->getOp())
    {
    case kIROp_AssociatedType:
        {
            auto associatedType = cast<IRAssociatedType>(type);
            SlangInt maxSize = 0;
            for (UInt i = 0; i < associatedType->getOperandCount(); i++)
                maxSize = Math::Max(
                    maxSize,
                    _getAnyValueSizeRaw((IRType*)associatedType->getOperand(i), targetReq, offset));
            return maxSize;
        }
    case kIROp_ThisType:
        {
            auto thisType = cast<IRThisType>(type);
            auto interfaceType = thisType->getConstraintType();
            auto size = getInterfaceAnyValueSize(interfaceType, interfaceType->sourceLoc);
            return alignUp(offset, 4) + alignUp((SlangInt)size, 4);
        }
    case kIROp_ExtractExistentialType:
        {
            auto existentialValue = type->getOperand(0);
            auto interfaceType = cast<IRInterfaceType>(existentialValue->getDataType());
            auto size = getInterfaceAnyValueSize(interfaceType, interfaceType->sourceLoc);
            return alignUp(offset, 4) + alignUp((SlangInt)size, 4);
        }
    case kIROp_LookupWitnessMethod:
        {
            auto witnessTableVal = type->getOperand(0);
            auto key = type->getOperand(1);
            IRType* assocType = nullptr;
            if (auto witnessTableType = as<IRWitnessTableTypeBase>(witnessTableVal->getDataType()))
            {
                auto interfaceType = as<IRInterfaceType>(witnessTableType->getConformanceType());

                // Walk through interface operands to find a match, the result should be an
                // associated type entry.
                //
                for (UIndex ii = 0; ii < interfaceType->getOperandCount(); ii++)
                {
                    auto entry = cast<IRInterfaceRequirementEntry>(interfaceType->getOperand(ii));
                    if (entry->getRequirementKey() == key &&
                        as<IRAssociatedType>(entry->getRequirementVal()))
                    {
                        assocType = (IRType*)entry->getRequirementVal();
                        break;
                    }
                }
            }

            if (!assocType)
                return -1;

            IRIntegerValue anyValueSize = kInvalidAnyValueSize;
            for (UInt i = 0; i < assocType->getOperandCount(); i++)
            {
                anyValueSize = Math::Min(
                    anyValueSize,
                    getInterfaceAnyValueSize(assocType->getOperand(i), type->sourceLoc));
            }

            if (anyValueSize == kInvalidAnyValueSize)
                return -1;

            return alignUp(offset, 4) + alignUp((SlangInt)anyValueSize, 4);
        }

        // treat CoopVec as an opaque handle type
    case kIROp_CoopVectorType:
        return alignUp(offset, 4) + 8;

    default:
        return -1;
    }
}

SlangInt getAnyValueSize(IRType* type, TargetRequest* targetReq)
{
    auto rawSize = _getAnyValueSizeRaw(type, targetReq, 0);
    if (rawSize < 0)
        return rawSize;
    return alignUp(rawSize, 4);
}
} // namespace Slang
