// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once

#include "Luau/IrBuilder.h"

LUAU_FASTFLAG(LuauCodegenSplitFloat)
LUAU_FASTFLAG(LuauCodegenFloatOps)

static const char* kUserdataRunTypes[] = {"extra", "color", "vec2", "mat3", "vertex", nullptr};

constexpr uint8_t kUserdataExtra = 0;
constexpr uint8_t kUserdataColor = 1;
constexpr uint8_t kUserdataVec2 = 2;
constexpr uint8_t kUserdataMat3 = 3;
constexpr uint8_t kUserdataVertex = 4;

// Userdata tags can be different from userdata bytecode type indices
constexpr uint8_t kTagVec2 = 12;
constexpr uint8_t kTagVertex = 13;

struct Vec2
{
    float x;
    float y;
};

struct Vertex
{
    float pos[3];
    float normal[3];
    float uv[2];
};

inline bool compareMemberName(const char* member, size_t memberLength, const char* str)
{
    return memberLength == strlen(str) && strcmp(member, str) == 0;
}

inline uint8_t typeToUserdataIndex(uint8_t type)
{
    // Underflow will push the type into a value that is not comparable to any kUserdata* constants
    return type - LBC_TYPE_TAGGED_USERDATA_BASE;
}

inline uint8_t userdataIndexToType(uint8_t userdataIndex)
{
    return LBC_TYPE_TAGGED_USERDATA_BASE + userdataIndex;
}

inline uint8_t vectorAccessBytecodeType(const char* member, size_t memberLength)
{
    using namespace Luau::CodeGen;

    if (compareMemberName(member, memberLength, "Magnitude"))
        return LBC_TYPE_NUMBER;

    if (compareMemberName(member, memberLength, "Unit"))
        return LBC_TYPE_VECTOR;

    return LBC_TYPE_ANY;
}

inline bool vectorAccess(Luau::CodeGen::IrBuilder& build, const char* member, size_t memberLength, int resultReg, int sourceReg, int pcpos)
{
    using namespace Luau::CodeGen;

    if (compareMemberName(member, memberLength, "Magnitude"))
    {
        IrOp x = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(0));
        IrOp y = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(4));
        IrOp z = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(8));

        if (FFlag::LuauCodegenFloatOps && FFlag::LuauCodegenSplitFloat)
        {
            // Intentionally not using DOT_VEC to check other kind of math compared to vector.magnitude
            IrOp x2 = build.inst(IrCmd::MUL_FLOAT, x, x);
            IrOp y2 = build.inst(IrCmd::MUL_FLOAT, y, y);
            IrOp z2 = build.inst(IrCmd::MUL_FLOAT, z, z);

            IrOp sum = build.inst(IrCmd::ADD_FLOAT, build.inst(IrCmd::ADD_FLOAT, x2, y2), z2);

            IrOp mag = build.inst(IrCmd::SQRT_FLOAT, sum);

            build.inst(IrCmd::STORE_DOUBLE, build.vmReg(resultReg), build.inst(IrCmd::FLOAT_TO_NUM, mag));
            build.inst(IrCmd::STORE_TAG, build.vmReg(resultReg), build.constTag(LUA_TNUMBER));
        }
        else
        {
            if (FFlag::LuauCodegenSplitFloat)
            {
                x = build.inst(IrCmd::FLOAT_TO_NUM, x);
                y = build.inst(IrCmd::FLOAT_TO_NUM, y);
                z = build.inst(IrCmd::FLOAT_TO_NUM, z);
            }

            IrOp x2 = build.inst(IrCmd::MUL_NUM, x, x);
            IrOp y2 = build.inst(IrCmd::MUL_NUM, y, y);
            IrOp z2 = build.inst(IrCmd::MUL_NUM, z, z);

            IrOp sum = build.inst(IrCmd::ADD_NUM, build.inst(IrCmd::ADD_NUM, x2, y2), z2);

            IrOp mag = build.inst(IrCmd::SQRT_NUM, sum);

            build.inst(IrCmd::STORE_DOUBLE, build.vmReg(resultReg), mag);
            build.inst(IrCmd::STORE_TAG, build.vmReg(resultReg), build.constTag(LUA_TNUMBER));
        }

        return true;
    }

    if (compareMemberName(member, memberLength, "Unit"))
    {
        IrOp x = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(0));
        IrOp y = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(4));
        IrOp z = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(8));

        if (FFlag::LuauCodegenFloatOps && FFlag::LuauCodegenSplitFloat)
        {
            // Intentionally not using DOT_VEC to check other kind of math compared to vector.normalize
            IrOp x2 = build.inst(IrCmd::MUL_FLOAT, x, x);
            IrOp y2 = build.inst(IrCmd::MUL_FLOAT, y, y);
            IrOp z2 = build.inst(IrCmd::MUL_FLOAT, z, z);

            IrOp sum = build.inst(IrCmd::ADD_FLOAT, build.inst(IrCmd::ADD_FLOAT, x2, y2), z2);

            IrOp mag = build.inst(IrCmd::SQRT_FLOAT, sum);
            IrOp inv = build.inst(IrCmd::DIV_FLOAT, build.constDouble(1.0f), mag);

            IrOp xr = build.inst(IrCmd::MUL_FLOAT, x, inv);
            IrOp yr = build.inst(IrCmd::MUL_FLOAT, y, inv);
            IrOp zr = build.inst(IrCmd::MUL_FLOAT, z, inv);

            build.inst(IrCmd::STORE_VECTOR, build.vmReg(resultReg), xr, yr, zr);
            build.inst(IrCmd::STORE_TAG, build.vmReg(resultReg), build.constTag(LUA_TVECTOR));
        }
        else
        {
            if (FFlag::LuauCodegenSplitFloat)
            {
                x = build.inst(IrCmd::FLOAT_TO_NUM, x);
                y = build.inst(IrCmd::FLOAT_TO_NUM, y);
                z = build.inst(IrCmd::FLOAT_TO_NUM, z);
            }

            IrOp x2 = build.inst(IrCmd::MUL_NUM, x, x);
            IrOp y2 = build.inst(IrCmd::MUL_NUM, y, y);
            IrOp z2 = build.inst(IrCmd::MUL_NUM, z, z);

            IrOp sum = build.inst(IrCmd::ADD_NUM, build.inst(IrCmd::ADD_NUM, x2, y2), z2);

            IrOp mag = build.inst(IrCmd::SQRT_NUM, sum);
            IrOp inv = build.inst(IrCmd::DIV_NUM, build.constDouble(1.0), mag);

            IrOp xr = build.inst(IrCmd::MUL_NUM, x, inv);
            IrOp yr = build.inst(IrCmd::MUL_NUM, y, inv);
            IrOp zr = build.inst(IrCmd::MUL_NUM, z, inv);

            if (FFlag::LuauCodegenSplitFloat)
            {
                xr = build.inst(IrCmd::NUM_TO_FLOAT, xr);
                yr = build.inst(IrCmd::NUM_TO_FLOAT, yr);
                zr = build.inst(IrCmd::NUM_TO_FLOAT, zr);
            }

            build.inst(IrCmd::STORE_VECTOR, build.vmReg(resultReg), xr, yr, zr);
            build.inst(IrCmd::STORE_TAG, build.vmReg(resultReg), build.constTag(LUA_TVECTOR));
        }

        return true;
    }

    return false;
}

inline uint8_t vectorNamecallBytecodeType(const char* member, size_t memberLength)
{
    if (compareMemberName(member, memberLength, "Dot"))
        return LBC_TYPE_NUMBER;

    if (compareMemberName(member, memberLength, "Cross"))
        return LBC_TYPE_VECTOR;

    return LBC_TYPE_ANY;
}

inline bool vectorNamecall(
    Luau::CodeGen::IrBuilder& build,
    const char* member,
    size_t memberLength,
    int argResReg,
    int sourceReg,
    int params,
    int results,
    int pcpos
)
{
    using namespace Luau::CodeGen;

    if (compareMemberName(member, memberLength, "Dot") && params == 2 && results <= 1)
    {
        build.loadAndCheckTag(build.vmReg(argResReg + 2), LUA_TVECTOR, build.vmExit(pcpos));

        if (FFlag::LuauCodegenFloatOps && FFlag::LuauCodegenSplitFloat)
        {
            // Intentionally not using DOT_VEC to check other kind of math compared to vector.dot
            IrOp x1 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(0));
            IrOp x2 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(argResReg + 2), build.constInt(0));

            IrOp xx = build.inst(IrCmd::MUL_FLOAT, x1, x2);

            IrOp y1 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(4));
            IrOp y2 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(argResReg + 2), build.constInt(4));

            IrOp yy = build.inst(IrCmd::MUL_FLOAT, y1, y2);

            IrOp z1 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(8));
            IrOp z2 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(argResReg + 2), build.constInt(8));

            IrOp zz = build.inst(IrCmd::MUL_FLOAT, z1, z2);

            IrOp sum = build.inst(IrCmd::ADD_FLOAT, build.inst(IrCmd::ADD_FLOAT, xx, yy), zz);

            build.inst(IrCmd::STORE_DOUBLE, build.vmReg(argResReg), build.inst(IrCmd::FLOAT_TO_NUM, sum));
            build.inst(IrCmd::STORE_TAG, build.vmReg(argResReg), build.constTag(LUA_TNUMBER));
        }
        else
        {
            IrOp x1 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(0));
            IrOp x2 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(argResReg + 2), build.constInt(0));

            if (FFlag::LuauCodegenSplitFloat)
            {
                x1 = build.inst(IrCmd::FLOAT_TO_NUM, x1);
                x2 = build.inst(IrCmd::FLOAT_TO_NUM, x2);
            }

            IrOp xx = build.inst(IrCmd::MUL_NUM, x1, x2);

            IrOp y1 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(4));
            IrOp y2 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(argResReg + 2), build.constInt(4));

            if (FFlag::LuauCodegenSplitFloat)
            {
                y1 = build.inst(IrCmd::FLOAT_TO_NUM, y1);
                y2 = build.inst(IrCmd::FLOAT_TO_NUM, y2);
            }

            IrOp yy = build.inst(IrCmd::MUL_NUM, y1, y2);

            IrOp z1 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(8));
            IrOp z2 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(argResReg + 2), build.constInt(8));

            if (FFlag::LuauCodegenSplitFloat)
            {
                z1 = build.inst(IrCmd::FLOAT_TO_NUM, z1);
                z2 = build.inst(IrCmd::FLOAT_TO_NUM, z2);
            }

            IrOp zz = build.inst(IrCmd::MUL_NUM, z1, z2);

            IrOp sum = build.inst(IrCmd::ADD_NUM, build.inst(IrCmd::ADD_NUM, xx, yy), zz);

            build.inst(IrCmd::STORE_DOUBLE, build.vmReg(argResReg), sum);
            build.inst(IrCmd::STORE_TAG, build.vmReg(argResReg), build.constTag(LUA_TNUMBER));
        }

        // If the function is called in multi-return context, stack has to be adjusted
        if (results == LUA_MULTRET)
            build.inst(IrCmd::ADJUST_STACK_TO_REG, build.vmReg(argResReg), build.constInt(1));

        return true;
    }

    if (compareMemberName(member, memberLength, "Cross") && params == 2 && results <= 1)
    {
        build.loadAndCheckTag(build.vmReg(argResReg + 2), LUA_TVECTOR, build.vmExit(pcpos));

        IrOp x1 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(0));
        IrOp x2 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(argResReg + 2), build.constInt(0));

        IrOp y1 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(4));
        IrOp y2 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(argResReg + 2), build.constInt(4));

        IrOp z1 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(sourceReg), build.constInt(8));
        IrOp z2 = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(argResReg + 2), build.constInt(8));

        if (FFlag::LuauCodegenFloatOps && FFlag::LuauCodegenSplitFloat)
        {
            IrOp y1z2 = build.inst(IrCmd::MUL_FLOAT, y1, z2);
            IrOp z1y2 = build.inst(IrCmd::MUL_FLOAT, z1, y2);
            IrOp xr = build.inst(IrCmd::SUB_FLOAT, y1z2, z1y2);

            IrOp z1x2 = build.inst(IrCmd::MUL_FLOAT, z1, x2);
            IrOp x1z2 = build.inst(IrCmd::MUL_FLOAT, x1, z2);
            IrOp yr = build.inst(IrCmd::SUB_FLOAT, z1x2, x1z2);

            IrOp x1y2 = build.inst(IrCmd::MUL_FLOAT, x1, y2);
            IrOp y1x2 = build.inst(IrCmd::MUL_FLOAT, y1, x2);
            IrOp zr = build.inst(IrCmd::SUB_FLOAT, x1y2, y1x2);

            build.inst(IrCmd::STORE_VECTOR, build.vmReg(argResReg), xr, yr, zr);
            build.inst(IrCmd::STORE_TAG, build.vmReg(argResReg), build.constTag(LUA_TVECTOR));
        }
        else
        {
            if (FFlag::LuauCodegenSplitFloat)
            {
                x1 = build.inst(IrCmd::FLOAT_TO_NUM, x1);
                x2 = build.inst(IrCmd::FLOAT_TO_NUM, x2);

                y1 = build.inst(IrCmd::FLOAT_TO_NUM, y1);
                y2 = build.inst(IrCmd::FLOAT_TO_NUM, y2);

                z1 = build.inst(IrCmd::FLOAT_TO_NUM, z1);
                z2 = build.inst(IrCmd::FLOAT_TO_NUM, z2);
            }

            IrOp y1z2 = build.inst(IrCmd::MUL_NUM, y1, z2);
            IrOp z1y2 = build.inst(IrCmd::MUL_NUM, z1, y2);
            IrOp xr = build.inst(IrCmd::SUB_NUM, y1z2, z1y2);

            IrOp z1x2 = build.inst(IrCmd::MUL_NUM, z1, x2);
            IrOp x1z2 = build.inst(IrCmd::MUL_NUM, x1, z2);
            IrOp yr = build.inst(IrCmd::SUB_NUM, z1x2, x1z2);

            IrOp x1y2 = build.inst(IrCmd::MUL_NUM, x1, y2);
            IrOp y1x2 = build.inst(IrCmd::MUL_NUM, y1, x2);
            IrOp zr = build.inst(IrCmd::SUB_NUM, x1y2, y1x2);

            if (FFlag::LuauCodegenSplitFloat)
            {
                xr = build.inst(IrCmd::NUM_TO_FLOAT, xr);
                yr = build.inst(IrCmd::NUM_TO_FLOAT, yr);
                zr = build.inst(IrCmd::NUM_TO_FLOAT, zr);
            }

            build.inst(IrCmd::STORE_VECTOR, build.vmReg(argResReg), xr, yr, zr);
            build.inst(IrCmd::STORE_TAG, build.vmReg(argResReg), build.constTag(LUA_TVECTOR));
        }

        // If the function is called in multi-return context, stack has to be adjusted
        if (results == LUA_MULTRET)
            build.inst(IrCmd::ADJUST_STACK_TO_REG, build.vmReg(argResReg), build.constInt(1));

        return true;
    }

    return false;
}

inline uint8_t userdataAccessBytecodeType(uint8_t type, const char* member, size_t memberLength)
{
    switch (typeToUserdataIndex(type))
    {
    case kUserdataColor:
        if (compareMemberName(member, memberLength, "R"))
            return LBC_TYPE_NUMBER;

        if (compareMemberName(member, memberLength, "G"))
            return LBC_TYPE_NUMBER;

        if (compareMemberName(member, memberLength, "B"))
            return LBC_TYPE_NUMBER;
        break;
    case kUserdataVec2:
        if (compareMemberName(member, memberLength, "X"))
            return LBC_TYPE_NUMBER;

        if (compareMemberName(member, memberLength, "Y"))
            return LBC_TYPE_NUMBER;

        if (compareMemberName(member, memberLength, "Magnitude"))
            return LBC_TYPE_NUMBER;

        if (compareMemberName(member, memberLength, "Unit"))
            return userdataIndexToType(kUserdataVec2);
        break;
    case kUserdataMat3:
        if (compareMemberName(member, memberLength, "Row1"))
            return LBC_TYPE_VECTOR;

        if (compareMemberName(member, memberLength, "Row2"))
            return LBC_TYPE_VECTOR;

        if (compareMemberName(member, memberLength, "Row3"))
            return LBC_TYPE_VECTOR;
        break;
    case kUserdataVertex:
        if (compareMemberName(member, memberLength, "pos"))
            return LBC_TYPE_VECTOR;

        if (compareMemberName(member, memberLength, "normal"))
            return LBC_TYPE_VECTOR;

        if (compareMemberName(member, memberLength, "uv"))
            return userdataIndexToType(kUserdataVec2);
        break;
    }

    return LBC_TYPE_ANY;
}

inline bool userdataAccess(
    Luau::CodeGen::IrBuilder& build,
    uint8_t type,
    const char* member,
    size_t memberLength,
    int resultReg,
    int sourceReg,
    int pcpos
)
{
    using namespace Luau::CodeGen;

    switch (typeToUserdataIndex(type))
    {
    case kUserdataColor:
        break;
    case kUserdataVec2:
        if (compareMemberName(member, memberLength, "X"))
        {
            IrOp udata = build.inst(IrCmd::LOAD_POINTER, build.vmReg(sourceReg));
            build.inst(IrCmd::CHECK_USERDATA_TAG, udata, build.constInt(kTagVec2), build.vmExit(pcpos));

            IrOp value = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA));

            if (FFlag::LuauCodegenSplitFloat)
                value = build.inst(IrCmd::FLOAT_TO_NUM, value);

            build.inst(IrCmd::STORE_DOUBLE, build.vmReg(resultReg), value);
            build.inst(IrCmd::STORE_TAG, build.vmReg(resultReg), build.constTag(LUA_TNUMBER));
            return true;
        }

        if (compareMemberName(member, memberLength, "Y"))
        {
            IrOp udata = build.inst(IrCmd::LOAD_POINTER, build.vmReg(sourceReg));
            build.inst(IrCmd::CHECK_USERDATA_TAG, udata, build.constInt(kTagVec2), build.vmExit(pcpos));

            IrOp value = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA));

            if (FFlag::LuauCodegenSplitFloat)
                value = build.inst(IrCmd::FLOAT_TO_NUM, value);

            build.inst(IrCmd::STORE_DOUBLE, build.vmReg(resultReg), value);
            build.inst(IrCmd::STORE_TAG, build.vmReg(resultReg), build.constTag(LUA_TNUMBER));
            return true;
        }

        if (compareMemberName(member, memberLength, "Magnitude"))
        {
            IrOp udata = build.inst(IrCmd::LOAD_POINTER, build.vmReg(sourceReg));
            build.inst(IrCmd::CHECK_USERDATA_TAG, udata, build.constInt(kTagVec2), build.vmExit(pcpos));

            IrOp x = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA));
            IrOp y = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA));

            if (FFlag::LuauCodegenFloatOps && FFlag::LuauCodegenSplitFloat)
            {
                IrOp x2 = build.inst(IrCmd::MUL_FLOAT, x, x);
                IrOp y2 = build.inst(IrCmd::MUL_FLOAT, y, y);

                IrOp sum = build.inst(IrCmd::ADD_FLOAT, x2, y2);

                IrOp mag = build.inst(IrCmd::SQRT_FLOAT, sum);

                build.inst(IrCmd::STORE_DOUBLE, build.vmReg(resultReg), build.inst(IrCmd::FLOAT_TO_NUM, mag));
                build.inst(IrCmd::STORE_TAG, build.vmReg(resultReg), build.constTag(LUA_TNUMBER));
            }
            else
            {
                if (FFlag::LuauCodegenSplitFloat)
                {
                    x = build.inst(IrCmd::FLOAT_TO_NUM, x);
                    y = build.inst(IrCmd::FLOAT_TO_NUM, y);
                }

                IrOp x2 = build.inst(IrCmd::MUL_NUM, x, x);
                IrOp y2 = build.inst(IrCmd::MUL_NUM, y, y);

                IrOp sum = build.inst(IrCmd::ADD_NUM, x2, y2);

                IrOp mag = build.inst(IrCmd::SQRT_NUM, sum);

                build.inst(IrCmd::STORE_DOUBLE, build.vmReg(resultReg), mag);
                build.inst(IrCmd::STORE_TAG, build.vmReg(resultReg), build.constTag(LUA_TNUMBER));
            }
            return true;
        }

        if (compareMemberName(member, memberLength, "Unit"))
        {
            IrOp udata = build.inst(IrCmd::LOAD_POINTER, build.vmReg(sourceReg));
            build.inst(IrCmd::CHECK_USERDATA_TAG, udata, build.constInt(kTagVec2), build.vmExit(pcpos));

            IrOp x = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA));
            IrOp y = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA));

            IrOp xr, yr;

            if (FFlag::LuauCodegenFloatOps && FFlag::LuauCodegenSplitFloat)
            {
                IrOp x2 = build.inst(IrCmd::MUL_FLOAT, x, x);
                IrOp y2 = build.inst(IrCmd::MUL_FLOAT, y, y);

                IrOp sum = build.inst(IrCmd::ADD_FLOAT, x2, y2);

                IrOp mag = build.inst(IrCmd::SQRT_FLOAT, sum);
                IrOp inv = build.inst(IrCmd::DIV_FLOAT, build.constDouble(1.0), mag);

                xr = build.inst(IrCmd::MUL_FLOAT, x, inv);
                yr = build.inst(IrCmd::MUL_FLOAT, y, inv);
            }
            else
            {
                if (FFlag::LuauCodegenSplitFloat)
                {
                    x = build.inst(IrCmd::FLOAT_TO_NUM, x);
                    y = build.inst(IrCmd::FLOAT_TO_NUM, y);
                }

                IrOp x2 = build.inst(IrCmd::MUL_NUM, x, x);
                IrOp y2 = build.inst(IrCmd::MUL_NUM, y, y);

                IrOp sum = build.inst(IrCmd::ADD_NUM, x2, y2);

                IrOp mag = build.inst(IrCmd::SQRT_NUM, sum);
                IrOp inv = build.inst(IrCmd::DIV_NUM, build.constDouble(1.0), mag);

                xr = build.inst(IrCmd::MUL_NUM, x, inv);
                yr = build.inst(IrCmd::MUL_NUM, y, inv);

                if (FFlag::LuauCodegenSplitFloat)
                {
                    xr = build.inst(IrCmd::NUM_TO_FLOAT, xr);
                    yr = build.inst(IrCmd::NUM_TO_FLOAT, yr);
                }
            }

            build.inst(IrCmd::CHECK_GC);
            IrOp udatar = build.inst(IrCmd::NEW_USERDATA, build.constInt(sizeof(Vec2)), build.constInt(kTagVec2));

            build.inst(IrCmd::BUFFER_WRITEF32, udatar, build.constInt(offsetof(Vec2, x)), xr, build.constTag(LUA_TUSERDATA));
            build.inst(IrCmd::BUFFER_WRITEF32, udatar, build.constInt(offsetof(Vec2, y)), yr, build.constTag(LUA_TUSERDATA));

            build.inst(IrCmd::STORE_POINTER, build.vmReg(resultReg), udatar);
            build.inst(IrCmd::STORE_TAG, build.vmReg(resultReg), build.constTag(LUA_TUSERDATA));
            return true;
        }
        break;
    case kUserdataMat3:
        break;
    case kUserdataVertex:
        if (compareMemberName(member, memberLength, "pos"))
        {
            IrOp udata = build.inst(IrCmd::LOAD_POINTER, build.vmReg(sourceReg));
            build.inst(IrCmd::CHECK_USERDATA_TAG, udata, build.constInt(kTagVertex), build.vmExit(pcpos));

            IrOp x = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vertex, pos[0])), build.constTag(LUA_TUSERDATA));
            IrOp y = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vertex, pos[1])), build.constTag(LUA_TUSERDATA));
            IrOp z = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vertex, pos[2])), build.constTag(LUA_TUSERDATA));

            build.inst(IrCmd::STORE_VECTOR, build.vmReg(resultReg), x, y, z);
            build.inst(IrCmd::STORE_TAG, build.vmReg(resultReg), build.constTag(LUA_TVECTOR));
            return true;
        }

        if (compareMemberName(member, memberLength, "normal"))
        {
            IrOp udata = build.inst(IrCmd::LOAD_POINTER, build.vmReg(sourceReg));
            build.inst(IrCmd::CHECK_USERDATA_TAG, udata, build.constInt(kTagVertex), build.vmExit(pcpos));

            IrOp x = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vertex, normal[0])), build.constTag(LUA_TUSERDATA));
            IrOp y = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vertex, normal[1])), build.constTag(LUA_TUSERDATA));
            IrOp z = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vertex, normal[2])), build.constTag(LUA_TUSERDATA));

            build.inst(IrCmd::STORE_VECTOR, build.vmReg(resultReg), x, y, z);
            build.inst(IrCmd::STORE_TAG, build.vmReg(resultReg), build.constTag(LUA_TVECTOR));
            return true;
        }

        if (compareMemberName(member, memberLength, "uv"))
        {
            IrOp udata = build.inst(IrCmd::LOAD_POINTER, build.vmReg(sourceReg));
            build.inst(IrCmd::CHECK_USERDATA_TAG, udata, build.constInt(kTagVertex), build.vmExit(pcpos));

            IrOp x = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vertex, uv[0])), build.constTag(LUA_TUSERDATA));
            IrOp y = build.inst(IrCmd::BUFFER_READF32, udata, build.constInt(offsetof(Vertex, uv[1])), build.constTag(LUA_TUSERDATA));

            build.inst(IrCmd::CHECK_GC);
            IrOp result = build.inst(IrCmd::NEW_USERDATA, build.constInt(sizeof(Vec2)), build.constInt(kTagVec2));

            build.inst(IrCmd::BUFFER_WRITEF32, result, build.constInt(offsetof(Vec2, x)), x, build.constTag(LUA_TUSERDATA));
            build.inst(IrCmd::BUFFER_WRITEF32, result, build.constInt(offsetof(Vec2, y)), y, build.constTag(LUA_TUSERDATA));

            build.inst(IrCmd::STORE_POINTER, build.vmReg(resultReg), result);
            build.inst(IrCmd::STORE_TAG, build.vmReg(resultReg), build.constTag(LUA_TUSERDATA));
            return true;
        }
        break;
    }

    return false;
}

inline uint8_t userdataMetamethodBytecodeType(uint8_t lhsTy, uint8_t rhsTy, Luau::CodeGen::HostMetamethod method)
{
    switch (method)
    {
    case Luau::CodeGen::HostMetamethod::Add:
    case Luau::CodeGen::HostMetamethod::Sub:
    case Luau::CodeGen::HostMetamethod::Mul:
    case Luau::CodeGen::HostMetamethod::Div:
        if (typeToUserdataIndex(lhsTy) == kUserdataVec2 || typeToUserdataIndex(rhsTy) == kUserdataVec2)
            return userdataIndexToType(kUserdataVec2);
        break;
    case Luau::CodeGen::HostMetamethod::Minus:
        if (typeToUserdataIndex(lhsTy) == kUserdataVec2)
            return userdataIndexToType(kUserdataVec2);
        break;
    default:
        break;
    }

    return LBC_TYPE_ANY;
}

inline bool userdataMetamethod(
    Luau::CodeGen::IrBuilder& build,
    uint8_t lhsTy,
    uint8_t rhsTy,
    int resultReg,
    Luau::CodeGen::IrOp lhs,
    Luau::CodeGen::IrOp rhs,
    Luau::CodeGen::HostMetamethod method,
    int pcpos
)
{
    using namespace Luau::CodeGen;

    switch (method)
    {
    case Luau::CodeGen::HostMetamethod::Add:
        if (typeToUserdataIndex(lhsTy) == kUserdataVec2 && typeToUserdataIndex(rhsTy) == kUserdataVec2)
        {
            build.loadAndCheckTag(lhs, LUA_TUSERDATA, build.vmExit(pcpos));
            build.loadAndCheckTag(rhs, LUA_TUSERDATA, build.vmExit(pcpos));

            IrOp udata1 = build.inst(IrCmd::LOAD_POINTER, lhs);
            build.inst(IrCmd::CHECK_USERDATA_TAG, udata1, build.constInt(kTagVec2), build.vmExit(pcpos));

            IrOp udata2 = build.inst(IrCmd::LOAD_POINTER, rhs);
            build.inst(IrCmd::CHECK_USERDATA_TAG, udata2, build.constInt(kTagVec2), build.vmExit(pcpos));

            IrOp x1 = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA));
            IrOp x2 = build.inst(IrCmd::BUFFER_READF32, udata2, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA));

            IrOp mx, my;

            if (FFlag::LuauCodegenFloatOps && FFlag::LuauCodegenSplitFloat)
            {
                mx = build.inst(IrCmd::ADD_FLOAT, x1, x2);

                IrOp y1 = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA));
                IrOp y2 = build.inst(IrCmd::BUFFER_READF32, udata2, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA));

                my = build.inst(IrCmd::ADD_FLOAT, y1, y2);
            }
            else
            {
                if (FFlag::LuauCodegenSplitFloat)
                {
                    x1 = build.inst(IrCmd::FLOAT_TO_NUM, x1);
                    x2 = build.inst(IrCmd::FLOAT_TO_NUM, x2);
                }

                mx = build.inst(IrCmd::ADD_NUM, x1, x2);

                IrOp y1 = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA));
                IrOp y2 = build.inst(IrCmd::BUFFER_READF32, udata2, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA));

                if (FFlag::LuauCodegenSplitFloat)
                {
                    y1 = build.inst(IrCmd::FLOAT_TO_NUM, y1);
                    y2 = build.inst(IrCmd::FLOAT_TO_NUM, y2);
                }

                my = build.inst(IrCmd::ADD_NUM, y1, y2);

                if (FFlag::LuauCodegenSplitFloat)
                {
                    mx = build.inst(IrCmd::NUM_TO_FLOAT, mx);
                    my = build.inst(IrCmd::NUM_TO_FLOAT, my);
                }
            }

            build.inst(IrCmd::CHECK_GC);
            IrOp udatar = build.inst(IrCmd::NEW_USERDATA, build.constInt(sizeof(Vec2)), build.constInt(kTagVec2));

            build.inst(IrCmd::BUFFER_WRITEF32, udatar, build.constInt(offsetof(Vec2, x)), mx, build.constTag(LUA_TUSERDATA));
            build.inst(IrCmd::BUFFER_WRITEF32, udatar, build.constInt(offsetof(Vec2, y)), my, build.constTag(LUA_TUSERDATA));

            build.inst(IrCmd::STORE_POINTER, build.vmReg(resultReg), udatar);
            build.inst(IrCmd::STORE_TAG, build.vmReg(resultReg), build.constTag(LUA_TUSERDATA));

            return true;
        }
        break;
    case Luau::CodeGen::HostMetamethod::Mul:
        if (typeToUserdataIndex(lhsTy) == kUserdataVec2 && typeToUserdataIndex(rhsTy) == kUserdataVec2)
        {
            build.loadAndCheckTag(lhs, LUA_TUSERDATA, build.vmExit(pcpos));
            build.loadAndCheckTag(rhs, LUA_TUSERDATA, build.vmExit(pcpos));

            IrOp udata1 = build.inst(IrCmd::LOAD_POINTER, lhs);
            build.inst(IrCmd::CHECK_USERDATA_TAG, udata1, build.constInt(kTagVec2), build.vmExit(pcpos));

            IrOp udata2 = build.inst(IrCmd::LOAD_POINTER, rhs);
            build.inst(IrCmd::CHECK_USERDATA_TAG, udata2, build.constInt(kTagVec2), build.vmExit(pcpos));

            IrOp x1 = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA));
            IrOp x2 = build.inst(IrCmd::BUFFER_READF32, udata2, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA));

            IrOp mx, my;

            if (FFlag::LuauCodegenFloatOps && FFlag::LuauCodegenSplitFloat)
            {
                mx = build.inst(IrCmd::MUL_FLOAT, x1, x2);

                IrOp y1 = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA));
                IrOp y2 = build.inst(IrCmd::BUFFER_READF32, udata2, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA));

                my = build.inst(IrCmd::MUL_FLOAT, y1, y2);
            }
            else
            {
                if (FFlag::LuauCodegenSplitFloat)
                {
                    x1 = build.inst(IrCmd::FLOAT_TO_NUM, x1);
                    x2 = build.inst(IrCmd::FLOAT_TO_NUM, x2);
                }

                mx = build.inst(IrCmd::MUL_NUM, x1, x2);

                IrOp y1 = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA));
                IrOp y2 = build.inst(IrCmd::BUFFER_READF32, udata2, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA));

                if (FFlag::LuauCodegenSplitFloat)
                {
                    y1 = build.inst(IrCmd::FLOAT_TO_NUM, y1);
                    y2 = build.inst(IrCmd::FLOAT_TO_NUM, y2);
                }

                my = build.inst(IrCmd::MUL_NUM, y1, y2);

                if (FFlag::LuauCodegenSplitFloat)
                {
                    mx = build.inst(IrCmd::NUM_TO_FLOAT, mx);
                    my = build.inst(IrCmd::NUM_TO_FLOAT, my);
                }
            }

            build.inst(IrCmd::CHECK_GC);
            IrOp udatar = build.inst(IrCmd::NEW_USERDATA, build.constInt(sizeof(Vec2)), build.constInt(kTagVec2));

            build.inst(IrCmd::BUFFER_WRITEF32, udatar, build.constInt(offsetof(Vec2, x)), mx, build.constTag(LUA_TUSERDATA));
            build.inst(IrCmd::BUFFER_WRITEF32, udatar, build.constInt(offsetof(Vec2, y)), my, build.constTag(LUA_TUSERDATA));

            build.inst(IrCmd::STORE_POINTER, build.vmReg(resultReg), udatar);
            build.inst(IrCmd::STORE_TAG, build.vmReg(resultReg), build.constTag(LUA_TUSERDATA));

            return true;
        }
        break;
    case Luau::CodeGen::HostMetamethod::Minus:
        if (typeToUserdataIndex(lhsTy) == kUserdataVec2)
        {
            build.loadAndCheckTag(lhs, LUA_TUSERDATA, build.vmExit(pcpos));

            IrOp udata1 = build.inst(IrCmd::LOAD_POINTER, lhs);
            build.inst(IrCmd::CHECK_USERDATA_TAG, udata1, build.constInt(kTagVec2), build.vmExit(pcpos));

            IrOp x = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA));
            IrOp y = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA));

            IrOp mx, my;

            if (FFlag::LuauCodegenFloatOps && FFlag::LuauCodegenSplitFloat)
            {
                mx = build.inst(IrCmd::UNM_FLOAT, x);
                my = build.inst(IrCmd::UNM_FLOAT, y);
            }
            else
            {
                if (FFlag::LuauCodegenSplitFloat)
                {
                    x = build.inst(IrCmd::FLOAT_TO_NUM, x);
                    y = build.inst(IrCmd::FLOAT_TO_NUM, y);
                }

                mx = build.inst(IrCmd::UNM_NUM, x);
                my = build.inst(IrCmd::UNM_NUM, y);

                if (FFlag::LuauCodegenSplitFloat)
                {
                    mx = build.inst(IrCmd::NUM_TO_FLOAT, mx);
                    my = build.inst(IrCmd::NUM_TO_FLOAT, my);
                }
            }

            build.inst(IrCmd::CHECK_GC);
            IrOp udatar = build.inst(IrCmd::NEW_USERDATA, build.constInt(sizeof(Vec2)), build.constInt(kTagVec2));

            build.inst(IrCmd::BUFFER_WRITEF32, udatar, build.constInt(offsetof(Vec2, x)), mx, build.constTag(LUA_TUSERDATA));
            build.inst(IrCmd::BUFFER_WRITEF32, udatar, build.constInt(offsetof(Vec2, y)), my, build.constTag(LUA_TUSERDATA));

            build.inst(IrCmd::STORE_POINTER, build.vmReg(resultReg), udatar);
            build.inst(IrCmd::STORE_TAG, build.vmReg(resultReg), build.constTag(LUA_TUSERDATA));

            return true;
        }
        break;
    default:
        break;
    }

    return false;
}

inline uint8_t userdataNamecallBytecodeType(uint8_t type, const char* member, size_t memberLength)
{
    switch (typeToUserdataIndex(type))
    {
    case kUserdataColor:
        break;
    case kUserdataVec2:
        if (compareMemberName(member, memberLength, "Dot"))
            return LBC_TYPE_NUMBER;

        if (compareMemberName(member, memberLength, "Min"))
            return userdataIndexToType(kUserdataVec2);
        break;
    case kUserdataMat3:
        break;
    }

    return LBC_TYPE_ANY;
}

inline bool userdataNamecall(
    Luau::CodeGen::IrBuilder& build,
    uint8_t type,
    const char* member,
    size_t memberLength,
    int argResReg,
    int sourceReg,
    int params,
    int results,
    int pcpos
)
{
    using namespace Luau::CodeGen;

    switch (typeToUserdataIndex(type))
    {
    case kUserdataColor:
        break;
    case kUserdataVec2:
        if (compareMemberName(member, memberLength, "Dot"))
        {
            IrOp udata1 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(sourceReg));
            build.inst(IrCmd::CHECK_USERDATA_TAG, udata1, build.constInt(kTagVec2), build.vmExit(pcpos));

            build.loadAndCheckTag(build.vmReg(argResReg + 2), LUA_TUSERDATA, build.vmExit(pcpos));

            IrOp udata2 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(argResReg + 2));
            build.inst(IrCmd::CHECK_USERDATA_TAG, udata2, build.constInt(kTagVec2), build.vmExit(pcpos));

            IrOp x1 = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA));
            IrOp x2 = build.inst(IrCmd::BUFFER_READF32, udata2, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA));

            if (FFlag::LuauCodegenSplitFloat)
            {
                x1 = build.inst(IrCmd::FLOAT_TO_NUM, x1);
                x2 = build.inst(IrCmd::FLOAT_TO_NUM, x2);
            }

            IrOp xx = build.inst(IrCmd::MUL_NUM, x1, x2);

            IrOp y1 = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA));
            IrOp y2 = build.inst(IrCmd::BUFFER_READF32, udata2, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA));

            if (FFlag::LuauCodegenSplitFloat)
            {
                y1 = build.inst(IrCmd::FLOAT_TO_NUM, y1);
                y2 = build.inst(IrCmd::FLOAT_TO_NUM, y2);
            }

            IrOp yy = build.inst(IrCmd::MUL_NUM, y1, y2);

            IrOp sum = build.inst(IrCmd::ADD_NUM, xx, yy);

            build.inst(IrCmd::STORE_DOUBLE, build.vmReg(argResReg), sum);
            build.inst(IrCmd::STORE_TAG, build.vmReg(argResReg), build.constTag(LUA_TNUMBER));

            // If the function is called in multi-return context, stack has to be adjusted
            if (results == LUA_MULTRET)
                build.inst(IrCmd::ADJUST_STACK_TO_REG, build.vmReg(argResReg), build.constInt(1));

            return true;
        }

        if (compareMemberName(member, memberLength, "Min"))
        {
            IrOp udata1 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(sourceReg));
            build.inst(IrCmd::CHECK_USERDATA_TAG, udata1, build.constInt(kTagVec2), build.vmExit(pcpos));

            build.loadAndCheckTag(build.vmReg(argResReg + 2), LUA_TUSERDATA, build.vmExit(pcpos));

            IrOp udata2 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(argResReg + 2));
            build.inst(IrCmd::CHECK_USERDATA_TAG, udata2, build.constInt(kTagVec2), build.vmExit(pcpos));

            IrOp x1 = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA));
            IrOp x2 = build.inst(IrCmd::BUFFER_READF32, udata2, build.constInt(offsetof(Vec2, x)), build.constTag(LUA_TUSERDATA));

            if (FFlag::LuauCodegenSplitFloat)
            {
                x1 = build.inst(IrCmd::FLOAT_TO_NUM, x1);
                x2 = build.inst(IrCmd::FLOAT_TO_NUM, x2);
            }

            IrOp mx = build.inst(IrCmd::MIN_NUM, x1, x2);

            IrOp y1 = build.inst(IrCmd::BUFFER_READF32, udata1, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA));
            IrOp y2 = build.inst(IrCmd::BUFFER_READF32, udata2, build.constInt(offsetof(Vec2, y)), build.constTag(LUA_TUSERDATA));

            if (FFlag::LuauCodegenSplitFloat)
            {
                y1 = build.inst(IrCmd::FLOAT_TO_NUM, y1);
                y2 = build.inst(IrCmd::FLOAT_TO_NUM, y2);
            }

            IrOp my = build.inst(IrCmd::MIN_NUM, y1, y2);

            if (FFlag::LuauCodegenSplitFloat)
            {
                mx = build.inst(IrCmd::NUM_TO_FLOAT, mx);
                my = build.inst(IrCmd::NUM_TO_FLOAT, my);
            }

            build.inst(IrCmd::CHECK_GC);
            IrOp udatar = build.inst(IrCmd::NEW_USERDATA, build.constInt(sizeof(Vec2)), build.constInt(kTagVec2));

            build.inst(IrCmd::BUFFER_WRITEF32, udatar, build.constInt(offsetof(Vec2, x)), mx, build.constTag(LUA_TUSERDATA));
            build.inst(IrCmd::BUFFER_WRITEF32, udatar, build.constInt(offsetof(Vec2, y)), my, build.constTag(LUA_TUSERDATA));

            build.inst(IrCmd::STORE_POINTER, build.vmReg(argResReg), udatar);
            build.inst(IrCmd::STORE_TAG, build.vmReg(argResReg), build.constTag(LUA_TUSERDATA));

            // If the function is called in multi-return context, stack has to be adjusted
            if (results == LUA_MULTRET)
                build.inst(IrCmd::ADJUST_STACK_TO_REG, build.vmReg(argResReg), build.constInt(1));

            return true;
        }
        break;
    case kUserdataMat3:
        break;
    }

    return false;
}
