// Copyright 2024 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
//    list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
//    contributors may be used to endorse or promote products derived from
//    this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include "src/tint/lang/spirv/reader/lower/builtins.h"

#include "src/tint/lang/core/ir/builder.h"
#include "src/tint/lang/core/ir/module.h"
#include "src/tint/lang/core/ir/validator.h"
#include "src/tint/lang/core/type/builtin_structs.h"
#include "src/tint/lang/spirv/ir/builtin_call.h"

namespace tint::spirv::reader::lower {
namespace {

using namespace tint::core::fluent_types;     // NOLINT
using namespace tint::core::number_suffixes;  // NOLINT

/// PIMPL state for the transform.
struct State {
    /// The IR module.
    core::ir::Module& ir;

    /// The IR builder.
    core::ir::Builder b{ir};

    /// The type manager.
    core::type::Manager& ty{ir.Types()};

    /// Process the module.
    void Process() {
        Vector<spirv::ir::BuiltinCall*, 4> builtin_worklist;
        for (auto* inst : ir.Instructions()) {
            if (auto* builtin = inst->As<spirv::ir::BuiltinCall>()) {
                builtin_worklist.Push(builtin);
            }
        }

        // Replace the builtins that we found.
        for (auto* builtin : builtin_worklist) {
            switch (builtin->Func()) {
                case spirv::BuiltinFn::kNormalize:
                    Normalize(builtin);
                    break;
                case spirv::BuiltinFn::kInverse:
                    Inverse(builtin);
                    break;
                case spirv::BuiltinFn::kSign:
                    Sign(builtin);
                    break;
                case spirv::BuiltinFn::kAbs:
                    Abs(builtin);
                    break;
                case spirv::BuiltinFn::kSMax:
                    SMax(builtin);
                    break;
                case spirv::BuiltinFn::kSMin:
                    SMin(builtin);
                    break;
                case spirv::BuiltinFn::kSClamp:
                    SClamp(builtin);
                    break;
                case spirv::BuiltinFn::kUMax:
                    UMax(builtin);
                    break;
                case spirv::BuiltinFn::kUMin:
                    UMin(builtin);
                    break;
                case spirv::BuiltinFn::kUClamp:
                    UClamp(builtin);
                    break;
                case spirv::BuiltinFn::kFindILsb:
                    FindILsb(builtin);
                    break;
                case spirv::BuiltinFn::kFindSMsb:
                    FindSMsb(builtin);
                    break;
                case spirv::BuiltinFn::kFindUMsb:
                    FindUMsb(builtin);
                    break;
                case spirv::BuiltinFn::kRefract:
                    Refract(builtin);
                    break;
                case spirv::BuiltinFn::kReflect:
                    Reflect(builtin);
                    break;
                case spirv::BuiltinFn::kFaceForward:
                    FaceForward(builtin);
                    break;
                case spirv::BuiltinFn::kLdexp:
                    Ldexp(builtin);
                    break;
                case spirv::BuiltinFn::kModf:
                    Modf(builtin);
                    break;
                case spirv::BuiltinFn::kFrexp:
                    Frexp(builtin);
                    break;
                case spirv::BuiltinFn::kBitCount:
                    BitCount(builtin);
                    break;
                case spirv::BuiltinFn::kBitFieldInsert:
                    BitFieldInsert(builtin);
                    break;
                case spirv::BuiltinFn::kBitFieldSExtract:
                    BitFieldSExtract(builtin);
                    break;
                case spirv::BuiltinFn::kBitFieldUExtract:
                    BitFieldUExtract(builtin);
                    break;
                case spirv::BuiltinFn::kAdd:
                    Add(builtin);
                    break;
                case spirv::BuiltinFn::kSub:
                    Sub(builtin);
                    break;
                case spirv::BuiltinFn::kMul:
                    Mul(builtin);
                    break;
                case spirv::BuiltinFn::kSDiv:
                    SDiv(builtin);
                    break;
                case spirv::BuiltinFn::kSMod:
                    SMod(builtin);
                    break;
                case spirv::BuiltinFn::kConvertFToS:
                    ConvertFToS(builtin);
                    break;
                case spirv::BuiltinFn::kConvertSToF:
                    ConvertSToF(builtin);
                    break;
                case spirv::BuiltinFn::kConvertUToF:
                    ConvertUToF(builtin);
                    break;
                case spirv::BuiltinFn::kBitwiseAnd:
                    BitwiseAnd(builtin);
                    break;
                case spirv::BuiltinFn::kBitwiseOr:
                    BitwiseOr(builtin);
                    break;
                case spirv::BuiltinFn::kBitwiseXor:
                    BitwiseXor(builtin);
                    break;
                case spirv::BuiltinFn::kEqual:
                    Equal(builtin);
                    break;
                case spirv::BuiltinFn::kNotEqual:
                    NotEqual(builtin);
                    break;
                case spirv::BuiltinFn::kSGreaterThan:
                    SGreaterThan(builtin);
                    break;
                case spirv::BuiltinFn::kSGreaterThanEqual:
                    SGreaterThanEqual(builtin);
                    break;
                case spirv::BuiltinFn::kSLessThan:
                    SLessThan(builtin);
                    break;
                case spirv::BuiltinFn::kSLessThanEqual:
                    SLessThanEqual(builtin);
                    break;
                case spirv::BuiltinFn::kUGreaterThan:
                    UGreaterThan(builtin);
                    break;
                case spirv::BuiltinFn::kUGreaterThanEqual:
                    UGreaterThanEqual(builtin);
                    break;
                case spirv::BuiltinFn::kULessThan:
                    ULessThan(builtin);
                    break;
                case spirv::BuiltinFn::kULessThanEqual:
                    ULessThanEqual(builtin);
                    break;
                case spirv::BuiltinFn::kShiftLeftLogical:
                    ShiftLeftLogical(builtin);
                    break;
                case spirv::BuiltinFn::kShiftRightLogical:
                    ShiftRightLogical(builtin);
                    break;
                case spirv::BuiltinFn::kShiftRightArithmetic:
                    ShiftRightArithmetic(builtin);
                    break;
                case spirv::BuiltinFn::kNot:
                    Not(builtin);
                    break;
                case spirv::BuiltinFn::kSNegate:
                    SNegate(builtin);
                    break;
                case spirv::BuiltinFn::kFMod:
                    FMod(builtin);
                    break;
                case spirv::BuiltinFn::kSelect:
                    Select(builtin);
                    break;
                case spirv::BuiltinFn::kOuterProduct:
                    OuterProduct(builtin);
                    break;
                case spirv::BuiltinFn::kGroupNonUniformBroadcast:
                    GroupNonUniformBuiltin(builtin, core::BuiltinFn::kSubgroupBroadcast);
                    break;
                case spirv::BuiltinFn::kGroupNonUniformShuffle:
                    GroupNonUniformBuiltin(builtin, core::BuiltinFn::kSubgroupShuffle);
                    break;
                case spirv::BuiltinFn::kGroupNonUniformShuffleXor:
                    GroupNonUniformBuiltin(builtin, core::BuiltinFn::kSubgroupShuffleXor);
                    break;
                case spirv::BuiltinFn::kGroupNonUniformShuffleDown:
                    GroupNonUniformBuiltin(builtin, core::BuiltinFn::kSubgroupShuffleDown);
                    break;
                case spirv::BuiltinFn::kGroupNonUniformShuffleUp:
                    GroupNonUniformBuiltin(builtin, core::BuiltinFn::kSubgroupShuffleUp);
                    break;
                case spirv::BuiltinFn::kGroupNonUniformBroadcastFirst:
                    GroupNonUniformBroadcastFirst(builtin);
                    break;
                case spirv::BuiltinFn::kGroupNonUniformQuadBroadcast:
                    GroupNonUniformBuiltin(builtin, core::BuiltinFn::kQuadBroadcast);
                    break;
                case spirv::BuiltinFn::kGroupNonUniformQuadSwap:
                    GroupNonUniformQuadSwap(builtin);
                    break;
                case spirv::BuiltinFn::kGroupNonUniformSMin:
                    GroupNonUniformMinMax(builtin, core::BuiltinFn::kSubgroupMin);
                    break;
                case spirv::BuiltinFn::kGroupNonUniformSMax:
                    GroupNonUniformMinMax(builtin, core::BuiltinFn::kSubgroupMax);
                    break;
                case spirv::BuiltinFn::kAtomicLoad:
                case spirv::BuiltinFn::kAtomicStore:
                case spirv::BuiltinFn::kAtomicExchange:
                case spirv::BuiltinFn::kAtomicCompareExchange:
                case spirv::BuiltinFn::kAtomicIAdd:
                case spirv::BuiltinFn::kAtomicISub:
                case spirv::BuiltinFn::kAtomicSMax:
                case spirv::BuiltinFn::kAtomicSMin:
                case spirv::BuiltinFn::kAtomicUMax:
                case spirv::BuiltinFn::kAtomicUMin:
                case spirv::BuiltinFn::kAtomicAnd:
                case spirv::BuiltinFn::kAtomicOr:
                case spirv::BuiltinFn::kAtomicXor:
                case spirv::BuiltinFn::kAtomicIIncrement:
                case spirv::BuiltinFn::kAtomicIDecrement:
                    // Ignore Atomics, they'll be handled by the `Atomics` transform.
                    break;
                case spirv::BuiltinFn::kOpImage:
                case spirv::BuiltinFn::kOpSampledImage:
                case spirv::BuiltinFn::kImageRead:
                case spirv::BuiltinFn::kImageFetch:
                case spirv::BuiltinFn::kImageGather:
                case spirv::BuiltinFn::kImageDrefGather:
                case spirv::BuiltinFn::kImageQueryLevels:
                case spirv::BuiltinFn::kImageQuerySamples:
                case spirv::BuiltinFn::kImageQuerySize:
                case spirv::BuiltinFn::kImageQuerySizeLod:
                case spirv::BuiltinFn::kImageSampleExplicitLod:
                case spirv::BuiltinFn::kImageSampleImplicitLod:
                case spirv::BuiltinFn::kImageSampleProjImplicitLod:
                case spirv::BuiltinFn::kImageSampleProjExplicitLod:
                case spirv::BuiltinFn::kImageSampleDrefImplicitLod:
                case spirv::BuiltinFn::kImageSampleDrefExplicitLod:
                case spirv::BuiltinFn::kImageSampleProjDrefImplicitLod:
                case spirv::BuiltinFn::kImageSampleProjDrefExplicitLod:
                case spirv::BuiltinFn::kImageWrite:
                    // Ignore image methods, they'll be handled by the `Texture` transform.
                    break;
                default:
                    TINT_UNREACHABLE() << "unknown spirv builtin: " << builtin->Func();
            }
        }
    }

    void GroupNonUniformMinMax(spirv::ir::BuiltinCall* call, core::BuiltinFn fn) {
        auto* value = call->Args()[2];

        auto* orig_type = call->Result()->Type();
        b.InsertBefore(call, [&] {
            auto* type = orig_type;
            if (orig_type->IsUnsignedIntegerScalarOrVector()) {
                type = ty.MatchWidth(ty.i32(), type);
                value = b.Convert(type, value)->Result();
            }

            value = b.Call(type, fn, Vector{value})->Result();

            if (type != orig_type) {
                value = b.Convert(call->Result()->Type(), value)->Result();
            }

            call->Result()->ReplaceAllUsesWith(value);
        });
        call->Destroy();
    }

    void GroupNonUniformQuadSwap(spirv::ir::BuiltinCall* call) {
        auto* value = call->Args()[1];
        auto* dir_val = call->Args()[2];

        TINT_ASSERT(dir_val->Is<core::ir::Constant>());
        auto* cnst = dir_val->As<core::ir::Constant>();
        TINT_ASSERT(cnst);

        uint32_t dir = cnst->Value()->ValueAs<uint32_t>();
        core::BuiltinFn fn = core::BuiltinFn::kNone;
        switch (dir) {
            case 0:
                fn = core::BuiltinFn::kQuadSwapX;
                break;
            case 1:
                fn = core::BuiltinFn::kQuadSwapY;
                break;
            case 2:
                fn = core::BuiltinFn::kQuadSwapDiagonal;
                break;
            default:
                TINT_UNREACHABLE();
        }

        auto* type = call->Result()->Type();
        b.InsertBefore(call, [&] {
            if (type->DeepestElement()->Is<core::type::Bool>()) {
                type = ty.MatchWidth(ty.u32(), type);
                value = b.Convert(type, value)->Result();
            }

            core::ir::Value* c = b.Call(type, fn, Vector{value})->Result();

            if (type != call->Result()->Type()) {
                c = b.Convert(call->Result()->Type(), c)->Result();
            }

            call->Result()->ReplaceAllUsesWith(c);
        });
        call->Destroy();
    }

    void GroupNonUniformBuiltin(spirv::ir::BuiltinCall* call, core::BuiltinFn fn) {
        auto* value = call->Args()[1];
        auto* id = call->Args()[2];

        auto* type = call->Result()->Type();
        b.InsertBefore(call, [&] {
            if (type->DeepestElement()->Is<core::type::Bool>()) {
                type = ty.MatchWidth(ty.u32(), type);
                value = b.Convert(type, value)->Result();
            }

            core::ir::Value* c = b.Call(type, fn, Vector{value, id})->Result();

            if (type != call->Result()->Type()) {
                c = b.Convert(call->Result()->Type(), c)->Result();
            }

            call->Result()->ReplaceAllUsesWith(c);
        });
        call->Destroy();
    }

    void GroupNonUniformBroadcastFirst(spirv::ir::BuiltinCall* call) {
        auto* value = call->Args()[1];

        auto* type = call->Result()->Type();
        b.InsertBefore(call, [&] {
            if (type->DeepestElement()->Is<core::type::Bool>()) {
                type = ty.MatchWidth(ty.u32(), type);
                value = b.Convert(type, value)->Result();
            }

            core::ir::Value* c =
                b.Call(type, core::BuiltinFn::kSubgroupBroadcastFirst, Vector{value})->Result();

            if (type != call->Result()->Type()) {
                c = b.Convert(call->Result()->Type(), c)->Result();
            }

            call->Result()->ReplaceAllUsesWith(c);
        });
        call->Destroy();
    }

    void OuterProduct(spirv::ir::BuiltinCall* call) {
        auto* vector1 = call->Args()[0];
        auto* vector2 = call->Args()[1];

        uint32_t rows = vector1->Type()->As<core::type::Vector>()->Width();
        uint32_t cols = vector2->Type()->As<core::type::Vector>()->Width();

        auto* elem_ty = vector1->Type()->DeepestElement();

        b.InsertBefore(call, [&] {
            Vector<core::ir::Value*, 4> col_vectors;

            for (uint32_t col = 0; col < cols; ++col) {
                Vector<core::ir::Value*, 4> col_elements;
                auto* v2_element = b.Access(elem_ty, vector2, u32(col));

                for (uint32_t row = 0; row < rows; ++row) {
                    auto* v1_element = b.Access(elem_ty, vector1, u32(row));
                    auto* result = b.Multiply(v1_element, v2_element)->Result();
                    col_elements.Push(result);
                }

                auto* row_vector = b.Construct(ty.vec(elem_ty, rows), col_elements)->Result();
                col_vectors.Push(row_vector);
            }
            b.ConstructWithResult(call->DetachResult(), col_vectors);
        });

        call->Destroy();
    }

    void Select(spirv::ir::BuiltinCall* call) {
        auto* cond = call->Args()[0];
        auto* true_ = call->Args()[1];
        auto* false_ = call->Args()[2];
        b.InsertBefore(call, [&] {
            b.CallWithResult(call->DetachResult(), core::BuiltinFn::kSelect, false_, true_, cond);
        });
        call->Destroy();
    }

    // FMod(x, y) emulated with: x - y * floor(x / y)
    void FMod(spirv::ir::BuiltinCall* call) {
        auto* x = call->Args()[0];
        auto* y = call->Args()[1];

        auto* res_ty = call->Result()->Type();
        b.InsertBefore(call, [&] {
            auto* div = b.Divide(x, y);
            auto* floor = b.Call(res_ty, core::BuiltinFn::kFloor, div);
            auto* mul = b.Multiply(y, floor);
            auto* sub = b.Subtract(x, mul);

            call->Result()->ReplaceAllUsesWith(sub->Result());
        });
        call->Destroy();
    }

    void SNegate(spirv::ir::BuiltinCall* call) {
        auto* val = call->Args()[0];

        auto* res_ty = call->Result()->Type();
        auto* neg_ty = ty.MatchWidth(ty.i32(), val->Type());
        b.InsertBefore(call, [&] {
            if (val->Type() != neg_ty) {
                val = b.Bitcast(neg_ty, val)->Result();
            }
            val = b.Negation(val)->Result();

            if (neg_ty != res_ty) {
                val = b.Bitcast(res_ty, val)->Result();
            }
            call->Result()->ReplaceAllUsesWith(val);
        });
        call->Destroy();
    }

    void Not(spirv::ir::BuiltinCall* call) {
        auto* val = call->Args()[0];
        auto* result_ty = call->Result()->Type();
        b.InsertBefore(call, [&] {
            auto* complement = b.Complement(val)->Result();
            if (val->Type() != result_ty) {
                complement = b.Bitcast(result_ty, complement)->Result();
            }
            call->Result()->ReplaceAllUsesWith(complement);
        });
        call->Destroy();
    }

    void ConvertSToF(spirv::ir::BuiltinCall* call) {
        b.InsertBefore(call, [&] {
            auto* result_ty = call->Result()->Type();

            auto* arg = call->Args()[0];
            if (arg->Type()->IsUnsignedIntegerScalarOrVector()) {
                arg = b.Bitcast(ty.MatchWidth(ty.i32(), result_ty), arg)->Result();
            }

            b.ConvertWithResult(call->DetachResult(), arg);
        });
        call->Destroy();
    }

    void ConvertUToF(spirv::ir::BuiltinCall* call) {
        b.InsertBefore(call, [&] {
            auto* result_ty = call->Result()->Type();

            auto* arg = call->Args()[0];
            if (arg->Type()->IsSignedIntegerScalarOrVector()) {
                arg = b.Bitcast(ty.MatchWidth(ty.u32(), result_ty), arg)->Result();
            }

            b.ConvertWithResult(call->DetachResult(), arg);
        });
        call->Destroy();
    }

    void ConvertFToS(spirv::ir::BuiltinCall* call) {
        b.InsertBefore(call, [&] {
            auto* res_ty = call->Result()->Type();
            auto deepest = res_ty->DeepestElement();

            auto* res = b.Convert(ty.MatchWidth(ty.i32(), res_ty), call->Args()[0])->Result();
            if (deepest->IsUnsignedIntegerScalar()) {
                res = b.Bitcast(res_ty, res)->Result();
            }
            call->Result()->ReplaceAllUsesWith(res);
        });
        call->Destroy();
    }

    void EmitBinaryWrappedAsFirstArg(spirv::ir::BuiltinCall* call, core::BinaryOp op) {
        const auto& args = call->Args();
        auto* lhs = args[0];
        auto* rhs = args[1];

        auto* op_ty = lhs->Type();
        auto* res_ty = call->Result()->Type();

        b.InsertBefore(call, [&] {
            if (rhs->Type() != op_ty) {
                rhs = b.Bitcast(op_ty, rhs)->Result();
            }

            auto* c = b.Binary(op, op_ty, lhs, rhs)->Result();
            if (res_ty != op_ty) {
                c = b.Bitcast(res_ty, c)->Result();
            }
            call->Result()->ReplaceAllUsesWith(c);
        });
        call->Destroy();
    }

    void BitwiseAnd(spirv::ir::BuiltinCall* call) {
        EmitBinaryWrappedAsFirstArg(call, core::BinaryOp::kAnd);
    }
    void BitwiseOr(spirv::ir::BuiltinCall* call) {
        EmitBinaryWrappedAsFirstArg(call, core::BinaryOp::kOr);
    }
    void BitwiseXor(spirv::ir::BuiltinCall* call) {
        EmitBinaryWrappedAsFirstArg(call, core::BinaryOp::kXor);
    }

    void Add(spirv::ir::BuiltinCall* call) {
        EmitBinaryWrappedAsFirstArg(call, core::BinaryOp::kAdd);
    }
    void Sub(spirv::ir::BuiltinCall* call) {
        EmitBinaryWrappedAsFirstArg(call, core::BinaryOp::kSubtract);
    }
    void Mul(spirv::ir::BuiltinCall* call) {
        EmitBinaryWrappedAsFirstArg(call, core::BinaryOp::kMultiply);
    }

    void EmitBinaryWrappedSignedSpirvMethods(spirv::ir::BuiltinCall* call, core::BinaryOp op) {
        const auto& args = call->Args();
        auto* lhs = args[0];
        auto* rhs = args[1];

        auto* res_ty = call->Result()->Type();
        auto* op_ty = ty.MatchWidth(ty.i32(), res_ty);

        b.InsertBefore(call, [&] {
            if (lhs->Type() != op_ty) {
                lhs = b.Bitcast(op_ty, lhs)->Result();
            }
            if (rhs->Type() != op_ty) {
                rhs = b.Bitcast(op_ty, rhs)->Result();
            }

            auto* c = b.Binary(op, op_ty, lhs, rhs)->Result();
            if (res_ty != op_ty) {
                c = b.Bitcast(res_ty, c)->Result();
            }
            call->Result()->ReplaceAllUsesWith(c);
        });
        call->Destroy();
    }

    void SDiv(spirv::ir::BuiltinCall* call) {
        EmitBinaryWrappedSignedSpirvMethods(call, core::BinaryOp::kDivide);
    }
    void SMod(spirv::ir::BuiltinCall* call) {
        EmitBinaryWrappedSignedSpirvMethods(call, core::BinaryOp::kModulo);
    }

    void EmitBinaryMatchedArgs(spirv::ir::BuiltinCall* call, core::BinaryOp op) {
        const auto& args = call->Args();
        auto* lhs = args[0];
        auto* rhs = args[1];

        b.InsertBefore(call, [&] {
            if (rhs->Type() != lhs->Type()) {
                rhs = b.Bitcast(lhs->Type(), rhs)->Result();
            }

            b.BinaryWithResult(call->DetachResult(), op, lhs, rhs)->Result();
        });
        call->Destroy();
    }
    void Equal(spirv::ir::BuiltinCall* call) {
        EmitBinaryMatchedArgs(call, core::BinaryOp::kEqual);
    }
    void NotEqual(spirv::ir::BuiltinCall* call) {
        EmitBinaryMatchedArgs(call, core::BinaryOp::kNotEqual);
    }

    void EmitBinaryWithSignedArgs(spirv::ir::BuiltinCall* call, core::BinaryOp op) {
        const auto& args = call->Args();
        auto* lhs = args[0];
        auto* rhs = args[1];

        auto* arg_ty = ty.MatchWidth(ty.i32(), call->Result()->Type());
        b.InsertBefore(call, [&] {
            if (lhs->Type() != arg_ty) {
                lhs = b.Bitcast(arg_ty, lhs)->Result();
            }
            if (rhs->Type() != arg_ty) {
                rhs = b.Bitcast(arg_ty, rhs)->Result();
            }

            b.BinaryWithResult(call->DetachResult(), op, lhs, rhs)->Result();
        });
        call->Destroy();
    }
    void SGreaterThan(spirv::ir::BuiltinCall* call) {
        EmitBinaryWithSignedArgs(call, core::BinaryOp::kGreaterThan);
    }
    void SGreaterThanEqual(spirv::ir::BuiltinCall* call) {
        EmitBinaryWithSignedArgs(call, core::BinaryOp::kGreaterThanEqual);
    }
    void SLessThan(spirv::ir::BuiltinCall* call) {
        EmitBinaryWithSignedArgs(call, core::BinaryOp::kLessThan);
    }
    void SLessThanEqual(spirv::ir::BuiltinCall* call) {
        EmitBinaryWithSignedArgs(call, core::BinaryOp::kLessThanEqual);
    }

    void EmitBinaryWithUnsignedArgs(spirv::ir::BuiltinCall* call, core::BinaryOp op) {
        const auto& args = call->Args();
        auto* lhs = args[0];
        auto* rhs = args[1];

        auto* arg_ty = ty.MatchWidth(ty.u32(), call->Result()->Type());
        b.InsertBefore(call, [&] {
            if (lhs->Type() != arg_ty) {
                lhs = b.Bitcast(arg_ty, lhs)->Result();
            }
            if (rhs->Type() != arg_ty) {
                rhs = b.Bitcast(arg_ty, rhs)->Result();
            }

            b.BinaryWithResult(call->DetachResult(), op, lhs, rhs)->Result();
        });
        call->Destroy();
    }
    void UGreaterThan(spirv::ir::BuiltinCall* call) {
        EmitBinaryWithUnsignedArgs(call, core::BinaryOp::kGreaterThan);
    }
    void UGreaterThanEqual(spirv::ir::BuiltinCall* call) {
        EmitBinaryWithUnsignedArgs(call, core::BinaryOp::kGreaterThanEqual);
    }
    void ULessThan(spirv::ir::BuiltinCall* call) {
        EmitBinaryWithUnsignedArgs(call, core::BinaryOp::kLessThan);
    }
    void ULessThanEqual(spirv::ir::BuiltinCall* call) {
        EmitBinaryWithUnsignedArgs(call, core::BinaryOp::kLessThanEqual);
    }

    // The SPIR-V Signed methods all interpret their arguments as signed (regardless of the type of
    // the argument). In order to satisfy this, we must bitcast any unsigned argument to a signed
    // type before calling the WGSL equivalent method.
    //
    // The result of the WGSL method will match the arguments, or in this case a signed value. If
    // the SPIR-V instruction expected an unsigned result we must bitcast the WGSL result to the
    // corrrect unsigned type.
    void WrapSignedSpirvMethods(spirv::ir::BuiltinCall* call, core::BuiltinFn func) {
        auto args = call->Args();

        b.InsertBefore(call, [&] {
            auto* result_ty = call->Result()->Type();
            Vector<core::ir::Value*, 2> new_args;

            for (auto* arg : args) {
                if (arg->Type()->IsUnsignedIntegerScalarOrVector()) {
                    arg = b.Bitcast(ty.MatchWidth(ty.i32(), result_ty), arg)->Result();
                }
                new_args.Push(arg);
            }

            auto* new_call = b.Call(result_ty, func, new_args);

            core::ir::Value* replacement = new_call->Result();
            if (result_ty->DeepestElement() == ty.u32()) {
                new_call->Result()->SetType(ty.MatchWidth(ty.i32(), result_ty));
                replacement = b.Bitcast(result_ty, replacement)->Result();
            }
            call->Result()->ReplaceAllUsesWith(replacement);
        });
        call->Destroy();
    }

    void Sign(spirv::ir::BuiltinCall* call) {
        WrapSignedSpirvMethods(call, core::BuiltinFn::kSign);
    }
    void Abs(spirv::ir::BuiltinCall* call) { WrapSignedSpirvMethods(call, core::BuiltinFn::kAbs); }
    void FindSMsb(spirv::ir::BuiltinCall* call) {
        WrapSignedSpirvMethods(call, core::BuiltinFn::kFirstLeadingBit);
    }
    void SMax(spirv::ir::BuiltinCall* call) { WrapSignedSpirvMethods(call, core::BuiltinFn::kMax); }
    void SMin(spirv::ir::BuiltinCall* call) { WrapSignedSpirvMethods(call, core::BuiltinFn::kMin); }
    void SClamp(spirv::ir::BuiltinCall* call) {
        WrapSignedSpirvMethods(call, core::BuiltinFn::kClamp);
    }

    void Ldexp(spirv::ir::BuiltinCall* call) {
        WrapSignedSpirvMethods(call, core::BuiltinFn::kLdexp);
    }

    // The SPIR-V Unsigned methods all interpret their arguments as unsigned (regardless of the type
    // of the argument). In order to satisfy this, we must bitcast any signed argument to an
    // unsigned type before calling the WGSL equivalent method.
    //
    // The result of the WGSL method will match the arguments, or in this case an unsigned value. If
    // the SPIR-V instruction expected a signed result we must bitcast the WGSL result to the
    // correct signed type.
    void WrapUnsignedSpirvMethods(spirv::ir::BuiltinCall* call, core::BuiltinFn func) {
        auto args = call->Args();

        b.InsertBefore(call, [&] {
            auto* result_ty = call->Result()->Type();
            Vector<core::ir::Value*, 2> new_args;

            for (auto* arg : args) {
                if (arg->Type()->IsSignedIntegerScalarOrVector()) {
                    arg = b.Bitcast(ty.MatchWidth(ty.u32(), result_ty), arg)->Result();
                }
                new_args.Push(arg);
            }

            auto* new_call = b.Call(result_ty, func, new_args);

            core::ir::Value* replacement = new_call->Result();
            if (result_ty->DeepestElement() == ty.i32()) {
                new_call->Result()->SetType(ty.MatchWidth(ty.u32(), result_ty));
                replacement = b.Bitcast(result_ty, replacement)->Result();
            }
            call->Result()->ReplaceAllUsesWith(replacement);
        });
        call->Destroy();
    }

    void UMax(spirv::ir::BuiltinCall* call) {
        WrapUnsignedSpirvMethods(call, core::BuiltinFn::kMax);
    }
    void UMin(spirv::ir::BuiltinCall* call) {
        WrapUnsignedSpirvMethods(call, core::BuiltinFn::kMin);
    }
    void UClamp(spirv::ir::BuiltinCall* call) {
        WrapUnsignedSpirvMethods(call, core::BuiltinFn::kClamp);
    }
    void FindUMsb(spirv::ir::BuiltinCall* call) {
        WrapUnsignedSpirvMethods(call, core::BuiltinFn::kFirstLeadingBit);
    }

    void Normalize(spirv::ir::BuiltinCall* call) {
        auto* arg = call->Args()[0];

        b.InsertBefore(call, [&] {
            core::BuiltinFn fn = core::BuiltinFn::kNormalize;
            if (arg->Type()->IsScalar()) {
                fn = core::BuiltinFn::kSign;
            }
            b.CallWithResult(call->DetachResult(), fn, Vector<core::ir::Value*, 1>{arg});
        });
        call->Destroy();
    }

    void FindILsb(spirv::ir::BuiltinCall* call) {
        auto* arg = call->Args()[0];

        b.InsertBefore(call, [&] {
            auto* arg_ty = arg->Type();
            auto* ret_ty = call->Result()->Type();

            auto* v =
                b.Call(arg_ty, core::BuiltinFn::kFirstTrailingBit, Vector<core::ir::Value*, 1>{arg})
                    ->Result();
            if (arg_ty != ret_ty) {
                v = b.Bitcast(ret_ty, v)->Result();
            }
            call->Result()->ReplaceAllUsesWith(v);
        });
        call->Destroy();
    }

    void Refract(spirv::ir::BuiltinCall* call) {
        auto args = call->Args();

        auto* I = args[0];
        auto* N = args[1];
        auto* eta = args[2];

        b.InsertBefore(call, [&] {
            if (I->Type()->IsFloatScalar()) {
                auto* src_ty = I->Type();
                auto* vec_ty = ty.vec(src_ty, 2);
                auto* zero = b.Zero(src_ty);
                I = b.Construct(vec_ty, I, zero)->Result();
                N = b.Construct(vec_ty, N, zero)->Result();

                auto* c = b.Call(vec_ty, core::BuiltinFn::kRefract,
                                 Vector<core::ir::Value*, 3>{I, N, eta});
                auto* s = b.Swizzle(src_ty, c, {0});
                call->Result()->ReplaceAllUsesWith(s->Result());
            } else {
                b.CallWithResult(call->DetachResult(), core::BuiltinFn::kRefract,
                                 Vector<core::ir::Value*, 3>{I, N, eta});
            }
        });
        call->Destroy();
    }

    void Reflect(spirv::ir::BuiltinCall* call) {
        auto args = call->Args();

        auto* I = args[0];
        auto* N = args[1];

        b.InsertBefore(call, [&] {
            if (I->Type()->IsFloatScalar()) {
                auto* v = b.Multiply(I, N)->Result();
                v = b.Multiply(v, N)->Result();
                v = b.Multiply(v, 2.0_f)->Result();
                v = b.Subtract(I, v)->Result();
                call->Result()->ReplaceAllUsesWith(v);
            } else {
                b.CallWithResult(call->DetachResult(), core::BuiltinFn::kReflect,
                                 Vector<core::ir::Value*, 2>{I, N});
            }
        });
        call->Destroy();
    }

    void FaceForward(spirv::ir::BuiltinCall* call) {
        auto args = call->Args();
        auto* N = args[0];
        auto* I = args[1];
        auto* Nref = args[2];

        b.InsertBefore(call, [&] {
            if (I->Type()->IsFloatScalar()) {
                auto* neg = b.Negation(N);
                auto* sel = b.Multiply(I, Nref)->Result();
                sel = b.LessThan(sel, b.Zero(sel->Type()))->Result();
                b.CallWithResult(call->DetachResult(), core::BuiltinFn::kSelect, neg, N, sel);
            } else {
                b.CallWithResult(call->DetachResult(), core::BuiltinFn::kFaceForward, N, I, Nref);
            }
        });
        call->Destroy();
    }

    void Modf(spirv::ir::BuiltinCall* call) {
        auto* x = call->Args()[0];
        auto* i = call->Args()[1];
        auto* result_ty = call->Result()->Type();
        auto* modf_result_ty = core::type::CreateModfResult(ty, ir.symbols, result_ty);

        b.InsertBefore(call, [&] {
            auto* c = b.Call(modf_result_ty, core::BuiltinFn::kModf, x);
            auto* whole = b.Access(result_ty, c, 1_u);
            b.Store(i, whole);

            b.AccessWithResult(call->DetachResult(), c, 0_u);
        });
        call->Destroy();
    }

    void Frexp(spirv::ir::BuiltinCall* call) {
        auto* x = call->Args()[0];
        auto* i = call->Args()[1];
        auto* result_ty = call->Result()->Type();
        auto* frexp_result_ty = core::type::CreateFrexpResult(ty, ir.symbols, result_ty);

        b.InsertBefore(call, [&] {
            auto* c = b.Call(frexp_result_ty, core::BuiltinFn::kFrexp, x);
            auto* exp = b.Access(ty.MatchWidth(ty.i32(), result_ty), c, 1_u)->Result();

            if (i->Type()->UnwrapPtr()->DeepestElement()->IsUnsignedIntegerScalar()) {
                exp = b.Bitcast(i->Type()->UnwrapPtr(), exp)->Result();
            }
            b.Store(i, exp);

            b.AccessWithResult(call->DetachResult(), c, 0_u);
        });
        call->Destroy();
    }

    void BitFieldInsert(spirv::ir::BuiltinCall* call) {
        const auto& args = call->Args();
        auto* e = args[0];
        auto* newbits = args[1];
        auto* offset = args[2];
        auto* count = args[3];

        b.InsertBefore(call, [&] {
            if (offset->Type()->IsSignedIntegerScalar()) {
                offset = b.Bitcast(ty.u32(), offset)->Result();
            }
            if (count->Type()->IsSignedIntegerScalar()) {
                count = b.Bitcast(ty.u32(), count)->Result();
            }
            b.CallWithResult(call->DetachResult(), core::BuiltinFn::kInsertBits, e, newbits, offset,
                             count);
        });
        call->Destroy();
    }

    void BitFieldUExtract(spirv::ir::BuiltinCall* call) {
        const auto& args = call->Args();
        auto* e = args[0];
        auto* offset = args[1];
        auto* count = args[2];

        b.InsertBefore(call, [&] {
            bool cast_result = false;
            auto* call_ty = ty.MatchWidth(ty.u32(), e->Type());

            if (e->Type()->DeepestElement()->IsSignedIntegerScalar()) {
                e = b.Bitcast(call_ty, e)->Result();
                cast_result = true;
            }

            if (offset->Type()->IsSignedIntegerScalar()) {
                offset = b.Bitcast(ty.u32(), offset)->Result();
            }
            if (count->Type()->IsSignedIntegerScalar()) {
                count = b.Bitcast(ty.u32(), count)->Result();
            }

            auto* res = b.Call(call_ty, core::BuiltinFn::kExtractBits, e, offset, count)->Result();
            if (cast_result) {
                res = b.Bitcast(call->Result()->Type(), res)->Result();
            }
            call->Result()->ReplaceAllUsesWith(res);
        });
        call->Destroy();
    }

    void BitFieldSExtract(spirv::ir::BuiltinCall* call) {
        const auto& args = call->Args();
        auto* e = args[0];
        auto* offset = args[1];
        auto* count = args[2];

        b.InsertBefore(call, [&] {
            bool cast_result = false;
            auto* call_ty = ty.MatchWidth(ty.i32(), e->Type());

            if (e->Type()->DeepestElement()->IsUnsignedIntegerScalar()) {
                e = b.Bitcast(call_ty, e)->Result();
                cast_result = true;
            }

            if (offset->Type()->IsSignedIntegerScalar()) {
                offset = b.Bitcast(ty.u32(), offset)->Result();
            }
            if (count->Type()->IsSignedIntegerScalar()) {
                count = b.Bitcast(ty.u32(), count)->Result();
            }

            auto* res = b.Call(call_ty, core::BuiltinFn::kExtractBits, e, offset, count)->Result();
            if (cast_result) {
                res = b.Bitcast(call->Result()->Type(), res)->Result();
            }
            call->Result()->ReplaceAllUsesWith(res);
        });
        call->Destroy();
    }

    void BitCount(spirv::ir::BuiltinCall* call) {
        auto arg = call->Args()[0];

        b.InsertBefore(call, [&] {
            auto* res_ty = call->Result()->Type();
            auto* arg_ty = arg->Type();

            auto* bc = b.Call(arg_ty, core::BuiltinFn::kCountOneBits, arg)->Result();
            if (res_ty != arg_ty) {
                bc = b.Bitcast(res_ty, bc)->Result();
            }
            call->Result()->ReplaceAllUsesWith(bc);
        });
        call->Destroy();
    }

    void ShiftLeftLogical(spirv::ir::BuiltinCall* call) {
        const auto& args = call->Args();

        b.InsertBefore(call, [&] {
            auto* base = args[0];
            auto* shift = args[1];

            if (!shift->Type()->IsUnsignedIntegerScalarOrVector()) {
                shift = b.Bitcast(ty.MatchWidth(ty.u32(), shift->Type()), shift)->Result();
            }

            auto* bin = b.Binary(core::BinaryOp::kShiftLeft, base->Type(), base, shift)->Result();
            if (base->Type() != call->Result()->Type()) {
                bin = b.Bitcast(call->Result()->Type(), bin)->Result();
            }
            call->Result()->ReplaceAllUsesWith(bin);
        });
        call->Destroy();
    }

    void ShiftRightLogical(spirv::ir::BuiltinCall* call) {
        const auto& args = call->Args();

        b.InsertBefore(call, [&] {
            auto* base = args[0];
            auto* shift = args[1];

            auto* u_ty = ty.MatchWidth(ty.u32(), base->Type());
            if (!base->Type()->IsUnsignedIntegerScalarOrVector()) {
                base = b.Bitcast(u_ty, base)->Result();
            }
            if (!shift->Type()->IsUnsignedIntegerScalarOrVector()) {
                shift = b.Bitcast(u_ty, shift)->Result();
            }

            auto* bin = b.Binary(core::BinaryOp::kShiftRight, u_ty, base, shift)->Result();
            if (u_ty != call->Result()->Type()) {
                bin = b.Bitcast(call->Result()->Type(), bin)->Result();
            }
            call->Result()->ReplaceAllUsesWith(bin);
        });
        call->Destroy();
    }

    void ShiftRightArithmetic(spirv::ir::BuiltinCall* call) {
        const auto& args = call->Args();

        b.InsertBefore(call, [&] {
            auto* base = args[0];
            auto* shift = args[1];

            auto* s_ty = ty.MatchWidth(ty.i32(), base->Type());
            if (!base->Type()->IsSignedIntegerScalarOrVector()) {
                base = b.Bitcast(s_ty, base)->Result();
            }
            if (!shift->Type()->IsUnsignedIntegerScalarOrVector()) {
                shift = b.Bitcast(ty.MatchWidth(ty.u32(), shift->Type()), shift)->Result();
            }

            auto* bin = b.Binary(core::BinaryOp::kShiftRight, s_ty, base, shift)->Result();
            if (s_ty != call->Result()->Type()) {
                bin = b.Bitcast(call->Result()->Type(), bin)->Result();
            }
            call->Result()->ReplaceAllUsesWith(bin);
        });
        call->Destroy();
    }

    void Inverse(spirv::ir::BuiltinCall* call) {
        auto* arg = call->Args()[0];
        auto* mat_ty = arg->Type()->As<core::type::Matrix>();
        TINT_ASSERT(mat_ty);
        TINT_ASSERT(mat_ty->Columns() == mat_ty->Rows());

        auto* elem_ty = mat_ty->Type();

        b.InsertBefore(call, [&] {
            auto* det =
                b.Call(elem_ty, core::BuiltinFn::kDeterminant, Vector<core::ir::Value*, 1>{arg});
            core::ir::Value* one = nullptr;
            if (elem_ty->Is<core::type::F32>()) {
                one = b.Constant(1.0_f);
            } else if (elem_ty->Is<core::type::F16>()) {
                one = b.Constant(1.0_h);
            } else {
                TINT_UNREACHABLE();
            }
            auto* inv_det = b.Divide(one, det);

            // Returns (m * n) - (o * p)
            auto sub_mul2 = [&](auto* m, auto* n, auto* o, auto* p) {
                auto* x = b.Multiply(m, n);
                auto* y = b.Multiply(o, p);
                return b.Subtract(x, y);
            };

            // Returns (m * n) - (o * p) + (q * r)
            auto sub_add_mul3 = [&](auto* m, auto* n, auto* o, auto* p, auto* q, auto* r) {
                auto* w = b.Multiply(m, n);
                auto* x = b.Multiply(o, p);
                auto* y = b.Multiply(q, r);

                auto* z = b.Subtract(w, x);
                return b.Add(z, y);
            };

            // Returns (m * n) + (o * p) - (q * r)
            auto add_sub_mul3 = [&](auto* m, auto* n, auto* o, auto* p, auto* q, auto* r) {
                auto* w = b.Multiply(m, n);
                auto* x = b.Multiply(o, p);
                auto* y = b.Multiply(q, r);

                auto* z = b.Add(w, x);
                return b.Subtract(z, y);
            };

            switch (mat_ty->Columns()) {
                case 2: {
                    auto* neg_inv_det = b.Negation(inv_det);

                    auto* ma = b.Access(elem_ty, arg, 0_u, 0_u);
                    auto* mb = b.Access(elem_ty, arg, 0_u, 1_u);
                    auto* mc = b.Access(elem_ty, arg, 1_u, 0_u);
                    auto* md = b.Access(elem_ty, arg, 1_u, 1_u);

                    auto* r_00 = b.Multiply(inv_det, md);
                    auto* r_01 = b.Multiply(neg_inv_det, mb);
                    auto* r_10 = b.Multiply(neg_inv_det, mc);
                    auto* r_11 = b.Multiply(inv_det, ma);

                    auto* r1 = b.Construct(ty.vec2(elem_ty), r_00, r_01);
                    auto* r2 = b.Construct(ty.vec2(elem_ty), r_10, r_11);
                    b.ConstructWithResult(call->DetachResult(), r1, r2);
                    break;
                }
                case 3: {
                    auto* ma = b.Access(elem_ty, arg, 0_u, 0_u);
                    auto* mb = b.Access(elem_ty, arg, 0_u, 1_u);
                    auto* mc = b.Access(elem_ty, arg, 0_u, 2_u);
                    auto* md = b.Access(elem_ty, arg, 1_u, 0_u);
                    auto* me = b.Access(elem_ty, arg, 1_u, 1_u);
                    auto* mf = b.Access(elem_ty, arg, 1_u, 2_u);
                    auto* mg = b.Access(elem_ty, arg, 2_u, 0_u);
                    auto* mh = b.Access(elem_ty, arg, 2_u, 1_u);
                    auto* mi = b.Access(elem_ty, arg, 2_u, 2_u);

                    // e * i - f * h
                    auto* r_00 = sub_mul2(me, mi, mf, mh);
                    // c * h - b * i
                    auto* r_01 = sub_mul2(mc, mh, mb, mi);
                    // b * f - c * e
                    auto* r_02 = sub_mul2(mb, mf, mc, me);

                    // f * g - d * i
                    auto* r_10 = sub_mul2(mf, mg, md, mi);
                    // a * i - c * g
                    auto* r_11 = sub_mul2(ma, mi, mc, mg);
                    // c * d - a * f
                    auto* r_12 = sub_mul2(mc, md, ma, mf);

                    // d * h - e * g
                    auto* r_20 = sub_mul2(md, mh, me, mg);
                    // b * g - a * h
                    auto* r_21 = sub_mul2(mb, mg, ma, mh);
                    // a * e - b * d
                    auto* r_22 = sub_mul2(ma, me, mb, md);

                    auto* r1 = b.Construct(ty.vec3(elem_ty), r_00, r_01, r_02);
                    auto* r2 = b.Construct(ty.vec3(elem_ty), r_10, r_11, r_12);
                    auto* r3 = b.Construct(ty.vec3(elem_ty), r_20, r_21, r_22);

                    auto* m = b.Construct(mat_ty, r1, r2, r3);
                    auto* inv = b.Multiply(inv_det, m);
                    call->Result()->ReplaceAllUsesWith(inv->Result());
                    break;
                }
                case 4: {
                    auto* ma = b.Access(elem_ty, arg, 0_u, 0_u);
                    auto* mb = b.Access(elem_ty, arg, 0_u, 1_u);
                    auto* mc = b.Access(elem_ty, arg, 0_u, 2_u);
                    auto* md = b.Access(elem_ty, arg, 0_u, 3_u);
                    auto* me = b.Access(elem_ty, arg, 1_u, 0_u);
                    auto* mf = b.Access(elem_ty, arg, 1_u, 1_u);
                    auto* mg = b.Access(elem_ty, arg, 1_u, 2_u);
                    auto* mh = b.Access(elem_ty, arg, 1_u, 3_u);
                    auto* mi = b.Access(elem_ty, arg, 2_u, 0_u);
                    auto* mj = b.Access(elem_ty, arg, 2_u, 1_u);
                    auto* mk = b.Access(elem_ty, arg, 2_u, 2_u);
                    auto* ml = b.Access(elem_ty, arg, 2_u, 3_u);
                    auto* mm = b.Access(elem_ty, arg, 3_u, 0_u);
                    auto* mn = b.Access(elem_ty, arg, 3_u, 1_u);
                    auto* mo = b.Access(elem_ty, arg, 3_u, 2_u);
                    auto* mp = b.Access(elem_ty, arg, 3_u, 3_u);

                    // kplo = k * p - l * o
                    auto* kplo = sub_mul2(mk, mp, ml, mo);
                    // jpln = j * p - l * n
                    auto* jpln = sub_mul2(mj, mp, ml, mn);
                    // jokn = j * o - k * n;
                    auto* jokn = sub_mul2(mj, mo, mk, mn);
                    // gpho = g * p - h * o
                    auto* gpho = sub_mul2(mg, mp, mh, mo);
                    // fphn = f * p - h * n
                    auto* fphn = sub_mul2(mf, mp, mh, mn);
                    // fogn = f * o - g * n;
                    auto* fogn = sub_mul2(mf, mo, mg, mn);
                    // glhk = g * l - h * k
                    auto* glhk = sub_mul2(mg, ml, mh, mk);
                    // flhj = f * l - h * j
                    auto* flhj = sub_mul2(mf, ml, mh, mj);
                    // fkgj = f * k - g * j;
                    auto* fkgj = sub_mul2(mf, mk, mg, mj);
                    // iplm = i * p - l * m
                    auto* iplm = sub_mul2(mi, mp, ml, mm);
                    // iokm = i * o - k * m
                    auto* iokm = sub_mul2(mi, mo, mk, mm);
                    // ephm = e * p - h * m;
                    auto* ephm = sub_mul2(me, mp, mh, mm);
                    // eogm = e * o - g * m
                    auto* eogm = sub_mul2(me, mo, mg, mm);
                    // elhi = e * l - h * i
                    auto* elhi = sub_mul2(me, ml, mh, mi);
                    // ekgi = e * k - g * i;
                    auto* ekgi = sub_mul2(me, mk, mg, mi);
                    // injm = i * n - j * m
                    auto* injm = sub_mul2(mi, mn, mj, mm);
                    // enfm = e * n - f * m
                    auto* enfm = sub_mul2(me, mn, mf, mm);
                    // ejfi = e * j - f * i;
                    auto* ejfi = sub_mul2(me, mj, mf, mi);

                    auto* neg_b = b.Negation(mb);
                    // f * kplo - g * jpln + h * jokn
                    auto* r_00 = sub_add_mul3(mf, kplo, mg, jpln, mh, jokn);
                    // -b * kplo + c * jpln - d * jokn
                    auto* r_01 = add_sub_mul3(neg_b, kplo, mc, jpln, md, jokn);
                    // b * gpho - c * fphn + d * fogn
                    auto* r_02 = sub_add_mul3(mb, gpho, mc, fphn, md, fogn);
                    // -b * glhk + c * flhj - d * fkgj
                    auto* r_03 = add_sub_mul3(neg_b, glhk, mc, flhj, md, fkgj);

                    auto* neg_e = b.Negation(me);
                    auto* neg_a = b.Negation(ma);
                    // -e * kplo + g * iplm - h * iokm
                    auto* r_10 = add_sub_mul3(neg_e, kplo, mg, iplm, mh, iokm);
                    // a * kplo - c * iplm + d * iokm
                    auto* r_11 = sub_add_mul3(ma, kplo, mc, iplm, md, iokm);
                    // -a * gpho + c * ephm - d * eogm
                    auto* r_12 = add_sub_mul3(neg_a, gpho, mc, ephm, md, eogm);
                    // a * glhk - c * elhi + d * ekgi
                    auto* r_13 = sub_add_mul3(ma, glhk, mc, elhi, md, ekgi);

                    // e * jpln - f * iplm + h * injm
                    auto* r_20 = sub_add_mul3(me, jpln, mf, iplm, mh, injm);
                    // -a * jpln + b * iplm - d * injm
                    auto* r_21 = add_sub_mul3(neg_a, jpln, mb, iplm, md, injm);
                    // a * fphn - b * ephm + d * enfm
                    auto* r_22 = sub_add_mul3(ma, fphn, mb, ephm, md, enfm);
                    // -a * flhj + b * elhi - d * ejfi
                    auto* r_23 = add_sub_mul3(neg_a, flhj, mb, elhi, md, ejfi);

                    // -e * jokn + f * iokm - g * injm
                    auto* r_30 = add_sub_mul3(neg_e, jokn, mf, iokm, mg, injm);
                    // a * jokn - b * iokm + c * injm
                    auto* r_31 = sub_add_mul3(ma, jokn, mb, iokm, mc, injm);
                    // -a * fogn + b * eogm - c * enfm
                    auto* r_32 = add_sub_mul3(neg_a, fogn, mb, eogm, mc, enfm);
                    // a * fkgj - b * ekgi + c * ejfi
                    auto* r_33 = sub_add_mul3(ma, fkgj, mb, ekgi, mc, ejfi);

                    auto* r1 = b.Construct(ty.vec4(elem_ty), r_00, r_01, r_02, r_03);
                    auto* r2 = b.Construct(ty.vec4(elem_ty), r_10, r_11, r_12, r_13);
                    auto* r3 = b.Construct(ty.vec4(elem_ty), r_20, r_21, r_22, r_23);
                    auto* r4 = b.Construct(ty.vec4(elem_ty), r_30, r_31, r_32, r_33);

                    auto* m = b.Construct(mat_ty, r1, r2, r3, r4);
                    auto* inv = b.Multiply(inv_det, m);
                    call->Result()->ReplaceAllUsesWith(inv->Result());
                    break;
                }
                default: {
                    TINT_UNREACHABLE();
                }
            }
        });
        call->Destroy();
    }
};

}  // namespace

Result<SuccessType> Builtins(core::ir::Module& ir) {
    TINT_CHECK_RESULT(
        ValidateBeforeIfNeeded(ir,
                               core::ir::Capabilities{
                                   core::ir::Capability::kAllowMultipleEntryPoints,
                                   core::ir::Capability::kAllowOverrides,
                                   core::ir::Capability::kAllowNonCoreTypes,
                                   core::ir::Capability::kAllowStructMatrixDecorations,
                                   core::ir::Capability::kAllowPointerToHandle,
                               },
                               "spirv.Builtins"));

    State{ir}.Process();

    return Success;
}

}  // namespace tint::spirv::reader::lower
