[PATCH v2 27/50] helper-to-tcg: PrepareForTcgPass, canonicalize IR

Anton Johansson via qemu development <[email protected]>
Newsgroups gmane.comp.emulators.qemu
Message-ID <[email protected]>
Iterates over the IR with the goal of converting it to a form closer to
TCG, taking care of IR discrepancies between LLVM and TCG.  This also
simplifies the backend by containing the bulk of custom IR
transformations.

Signed-off-by: Anton Johansson <[email protected]>
---
 .../helper-to-tcg/include/CmdLineOptions.hpp  |    3 +
 .../include/PrepareForTcgPass.hpp             |    7 +-
 subprojects/helper-to-tcg/meson.build         |    1 +
 subprojects/helper-to-tcg/src/Pipeline.cpp    |   63 +-
 .../src/PrepareForTcgPass/CanonicalizeIR.cpp  | 1193 +++++++++++++++++
 .../src/PrepareForTcgPass/CanonicalizeIR.hpp  |   24 +
 .../PrepareForTcgPass/PrepareForTcgPass.cpp   |    2 +
 7 files changed, 1290 insertions(+), 3 deletions(-)
 create mode 100644 subprojects/helper-to-tcg/src/PrepareForTcgPass/CanonicalizeIR.cpp
 create mode 100644 subprojects/helper-to-tcg/src/PrepareForTcgPass/CanonicalizeIR.hpp

diff --git a/subprojects/helper-to-tcg/include/CmdLineOptions.hpp b/subprojects/helper-to-tcg/include/CmdLineOptions.hpp
index bfdd3ebe41..3a827f8ab7 100644
--- a/subprojects/helper-to-tcg/include/CmdLineOptions.hpp
+++ b/subprojects/helper-to-tcg/include/CmdLineOptions.hpp
@@ -25,3 +25,6 @@ extern llvm::cl::list<std::string> InputFiles;
 extern llvm::cl::opt<bool> TranslateAllHelpers;
 // Options for PrepareForTcgPass
 extern llvm::cl::opt<std::string> TcgGlobalMappingsName;
+extern llvm::cl::opt<std::string> UserPCRelBranchFunc;
+extern llvm::cl::opt<std::string> UserPCRelBranchFallthroughFunc;
+extern llvm::cl::opt<std::string> UserPCRelBranchConditionalFunc;
diff --git a/subprojects/helper-to-tcg/include/PrepareForTcgPass.hpp b/subprojects/helper-to-tcg/include/PrepareForTcgPass.hpp
index d0cd6e3cbd..078477ba2f 100644
--- a/subprojects/helper-to-tcg/include/PrepareForTcgPass.hpp
+++ b/subprojects/helper-to-tcg/include/PrepareForTcgPass.hpp
@@ -19,16 +19,19 @@
 
 #include "DebugInfo.hpp"
 #include "TcgGlobalMap.hpp"
+#include "VectorLayout.hpp"
 #include <llvm/IR/PassManager.h>
 
 class PrepareForTcgPass : public llvm::PassInfoMixin<PrepareForTcgPass> {
     TcgGlobalMap &ResultTcgGlobalMap;
     const DebugInfoMapTy &DebugInfo;
+    const VectorLayout &VL;
 
 public:
     PrepareForTcgPass(TcgGlobalMap &ResultTcgGlobalMap,
-                      const DebugInfoMapTy &DebugInfo)
-        : ResultTcgGlobalMap(ResultTcgGlobalMap), DebugInfo(DebugInfo) {}
+                      const DebugInfoMapTy &DebugInfo, const VectorLayout &VL)
+        : ResultTcgGlobalMap(ResultTcgGlobalMap), DebugInfo(DebugInfo), VL(VL) {
+    }
     llvm::PreservedAnalyses run(llvm::Module &M,
                                 llvm::ModuleAnalysisManager &MAM);
 };
diff --git a/subprojects/helper-to-tcg/meson.build b/subprojects/helper-to-tcg/meson.build
index 564191d328..79ce0c6dd8 100644
--- a/subprojects/helper-to-tcg/meson.build
+++ b/subprojects/helper-to-tcg/meson.build
@@ -46,6 +46,7 @@ sources = [
     'src/PseudoInst.cpp',
     'src/PrepareForTcgPass/PrepareForTcgPass.cpp',
     'src/PrepareForTcgPass/TransformGEPs.cpp',
+    'src/PrepareForTcgPass/CanonicalizeIR.cpp',
 ]
 
 clang = bindir / 'clang'
diff --git a/subprojects/helper-to-tcg/src/Pipeline.cpp b/subprojects/helper-to-tcg/src/Pipeline.cpp
index 9212bba29f..9b45664bdc 100644
--- a/subprojects/helper-to-tcg/src/Pipeline.cpp
+++ b/subprojects/helper-to-tcg/src/Pipeline.cpp
@@ -19,6 +19,7 @@
 #include "LlvmCompat.hpp"
 #include "PrepareForOptPass.hpp"
 #include "PrepareForTcgPass.hpp"
+#include "VectorLayout.hpp"
 
 #if LLVM_VERSION_MAJOR == 15
 #include <llvm/ADT/Triple.h>
@@ -42,6 +43,7 @@
 #include <llvm/Support/CommandLine.h>
 #include <llvm/Support/Debug.h>
 #include <llvm/Support/InitLLVM.h>
+#include <llvm/Support/MathExtras.h>
 #include <llvm/Support/SourceMgr.h>
 #include <llvm/Support/TargetSelect.h>
 #include <llvm/Target/TargetMachine.h>
@@ -80,6 +82,51 @@ cl::opt<std::string> TcgGlobalMappingsName(
              "into a struct to TCG globals"),
     cl::init("mappings"), cl::cat(Cat));
 
+cl::opt<std::string> UserPCRelBranchFunc(
+    "user-pcrel-branch-func",
+    cl::desc("Specify a function name in the input which represents a "
+             "PC-relative jump operation in QEMU."),
+    cl::init(""), cl::cat(Cat));
+
+cl::opt<std::string> UserPCRelBranchConditionalFunc(
+    "user-pcrel-branch-conditional-func",
+    cl::desc("Specify a function name in QEMU which represents a "
+             "PC-relative conditional jump operation.  Will be"
+             " emitted when the function supplied with -user-pcrel-branch-func"
+             " is called from inside a conditional block."),
+    cl::init(""), cl::cat(Cat));
+
+cl::opt<std::string> UserPCRelBranchFallthroughFunc(
+    "user-pcrel-branch-fallthrough-func",
+    cl::desc("Specify a function name in QEMU which represents a "
+             "PC-relative fallthrough operation.  Will be emitted when the"
+             " function supplied with -user-pcrel-branch-func"
+             " is called"
+             " from inside a conditional block, but that condition isn't taken."
+             " and we instead need to fallthrough to the next instruction."),
+    cl::init(""), cl::cat(Cat));
+ 
+static llvm::cl::opt<bool> VecForceBigEndian(
+    "vec-force-big-endian",
+    cl::desc("Force emission of vector operations as if on a big-endian host."),
+    cl::init(false), cl::cat(Cat));
+
+static llvm::cl::opt<size_t>
+    VecBlockBytes("vec-block-bytes",
+                  cl::desc("Sets the size of the host-endian blocks vector "
+                           "used to represent target vectors."),
+                  cl::init(64), cl::cat(Cat));
+
+static llvm::cl::opt<LanePreference> VecLane0(
+    "vec-lane-0",
+    cl::desc("Set whether lane 0 is treated as the high- or low-end "
+             "bytes of the vector blocks ([most|least]-significant)."),
+    cl::values(clEnumVal(LeastSignificant,
+                         "Vector lane 0 is in the least-significant bytes"),
+               clEnumVal(MostSignificant,
+                         "Vector lane 0 is in the most-significant bytes")),
+    cl::init(LeastSignificant), cl::cat(Cat));
+
 // Define a TargetTransformInfo (TTI) subclass, this allows for overriding
 // common per-llvm-target information expected by other LLVM passes, such
 // as the width of the largest scalar/vector registers.  Needed for consistent
@@ -146,10 +193,24 @@ int main(int argc, char **argv) {
     }
 #endif
 
+    if (!isPowerOf2_32(VecBlockBytes) or VecBlockBytes < 8) {
+        errs() << "--" << VecBlockBytes.ArgStr
+               << " must be a power-of-two and at least 8 bytes.";
+        return 1;
+    }
+
     LLVMContext Context;
     SMDiagnostic Err;
     std::unique_ptr<Module> M = parseIRFile(InputFile, Err, Context);
 
+    // After we have a module we can get the data layout and determine host
+    // endianness.
+    VectorLayout VL = {
+        .HostBigEndian = VecForceBigEndian or M->getDataLayout().isBigEndian(),
+        .Lane0 = VecLane0,
+        .BlockBytes = VecBlockBytes,
+    };
+
     // Create a new TargetMachine to represent a TCG target,
     // we use x86_64 as a base and derive from that using a
     // TargetTransformInfo to provide allowed scalar and vector
@@ -231,7 +292,7 @@ int main(int argc, char **argv) {
     //
 
     TcgGlobalMap TcgGlobals;
-    MPM.addPass(PrepareForTcgPass(TcgGlobals, DebugInfo));
+    MPM.addPass(PrepareForTcgPass(TcgGlobals, DebugInfo, VL));
     MPM.addPass(VerifierPass());
     {
         FunctionPassManager FPM;
diff --git a/subprojects/helper-to-tcg/src/PrepareForTcgPass/CanonicalizeIR.cpp b/subprojects/helper-to-tcg/src/PrepareForTcgPass/CanonicalizeIR.cpp
new file mode 100644
index 0000000000..54fdcade4e
--- /dev/null
+++ b/subprojects/helper-to-tcg/src/PrepareForTcgPass/CanonicalizeIR.cpp
@@ -0,0 +1,1193 @@
+//
+//  Copyright(c) 2026 rev.ng Labs Srl. All Rights Reserved.
+//
+//  This program is free software; you can redistribute it and/or modify
+//  it under the terms of the GNU General Public License as published by
+//  the Free Software Foundation; either version 2 of the License, or
+//  (at your option) any later version.
+//
+//  This program is distributed in the hope that it will be useful,
+//  but WITHOUT ANY WARRANTY; without even the implied warranty of
+//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//  GNU General Public License for more details.
+//
+//  You should have received a copy of the GNU General Public License
+//  along with this program; if not, see <http://www.gnu.org/licenses/>.
+//
+
+#include "CanonicalizeIR.hpp"
+#include "CmdLineOptions.hpp"
+#include "LlvmCompat.hpp"
+#include "PseudoInst.hpp"
+#include "VectorLayout.hpp"
+
+#include <llvm/ADT/PostOrderIterator.h>
+#include <llvm/ADT/SmallPtrSet.h>
+#include <llvm/ADT/SmallSet.h>
+#include <llvm/ADT/SmallVector.h>
+#include <llvm/Analysis/RegionInfo.h>
+#include <llvm/Analysis/VectorUtils.h>
+#include <llvm/IR/Constants.h>
+#include <llvm/IR/DerivedTypes.h>
+#include <llvm/IR/Dominators.h>
+#include <llvm/IR/Function.h>
+#include <llvm/IR/IRBuilder.h>
+#include <llvm/IR/InstIterator.h>
+#include <llvm/IR/InstrTypes.h>
+#include <llvm/IR/Instruction.h>
+#include <llvm/IR/Instructions.h>
+#include <llvm/IR/Intrinsics.h>
+#include <llvm/IR/Module.h>
+#include <llvm/IR/PatternMatch.h>
+#include <llvm/Support/Casting.h>
+
+#include <algorithm> // for std::max/min
+
+using namespace llvm;
+using namespace PatternMatch;
+
+// Needed to track and remove instructions not handled by a subsequent dead code
+// elimination, this applies to calls to pseudo instructions in particular.
+using EraseInstVec = SmallVector<Instruction *, 16>;
+using UsageCountMap = DenseMap<Value *, uint16_t>;
+using ExitSet = SmallSet<BasicBlock *, 2>;
+
+// Helper function to remove an instruction only if all uses have been removed.
+// This way we can keep track instruction uses without having to modify the IR,
+// or without having to iterate over all uses everytime we wish to remove an
+// instruction.
+static void addToEraseVectorIfUnused(EraseInstVec &InstToErase,
+                                     UsageCountMap &UsageMap, Value *V) {
+    auto *I = dyn_cast<Instruction>(V);
+    if (!I) {
+        return;
+    }
+
+    // Add V to map if not there
+    if (UsageMap.count(V) == 0) {
+        UsageMap[V] = V->getNumUses();
+    }
+
+    // Erase if count reaches zero
+    if (--UsageMap[V] == 0) {
+        InstToErase.push_back(I);
+        UsageMap.erase(V);
+    }
+}
+
+// Forward declarations of IR transformations used in canonicalizing the IR
+static void upcastAshr(Instruction *I);
+static void convertInsertShuffleToSplat(EraseInstVec &InstToErase,
+                                        UsageCountMap &UsageMap, Module &M,
+                                        Instruction *I);
+
+static void
+defineVectorConstants(Module &M, const VectorLayout &VL, Instruction *I,
+                      DenseMap<Constant *, CallInst *> &Replacements);
+
+static void simplifyVecBinOpWithSplat(EraseInstVec &InstToErase,
+                                      UsageCountMap &UsageMap, Module &M,
+                                      BinaryOperator *BinOp);
+
+static void convertSelectICmp(Module &M, SelectInst *Select, ICmpInst *ICmp);
+
+static void convertQemuLoadStoreToPseudoInst(Module &M, CallInst *Call,
+                                             EraseInstVec &InstToErase,
+                                             UsageCountMap &UsageMap);
+static void convertExceptionCallsToPseudoInst(Module &M, CallInst *Call);
+static void convertReturnAddrToPseudoInst(Module &M, CallInst *Call);
+static void convertImmediateSelectAccessGlobal(EraseInstVec &InstToErase,
+                                               Module &M, CallInst *Call);
+static void convertImmediateDeclCall(EraseInstVec &InstToErase, Module &M,
+                                     CallInst *Call);
+static void convertUserBranchInstructions(Module &M,
+                                          DominatorTreeAnalysis::Result &DT,
+                                          const ExitSet &Exits, CallInst *Call);
+static void convertVecStoreToPseudoInst(EraseInstVec &InstToErase, Module &M,
+                                        StoreInst *Store);
+
+void canonicalizeIR(Module &M, ModuleAnalysisManager &MAM,
+                    const VectorLayout &VL) {
+    auto &FAM =
+        MAM.getResult<FunctionAnalysisManagerModuleProxy>(M).getManager();
+    const bool HasUserBranches = !UserPCRelBranchFunc.empty() and
+                                 !UserPCRelBranchConditionalFunc.empty() and
+                                 !UserPCRelBranchFallthroughFunc.empty();
+    for (Function &F : M) {
+        if (F.isDeclaration()) {
+            continue;
+        }
+
+        EraseInstVec InstToErase;
+        UsageCountMap UsageMap;
+        ExitSet Exits;
+        auto &DT = FAM.getResult<DominatorTreeAnalysis>(F);
+
+        // Collect basic blocks which exit the function, needed to support
+        // user-supplied branch calls, and correctly handle conditional branches
+        if (HasUserBranches) {
+            for (auto &BB : F) {
+                if (isa<ReturnInst>(BB.getTerminator())) {
+                    Exits.insert(&BB);
+                }
+            }
+        }
+
+        SmallPtrSet<Constant *, 8> ReplacedVectorConstants;
+
+        // Perform a first pass over all instructions in the function and apply
+        // IR transformations sequentially.  NOTE: order matters here.
+        for (Instruction &I : instructions(F)) {
+            if (I.isArithmeticShift()) {
+                upcastAshr(&I);
+            }
+
+            convertInsertShuffleToSplat(InstToErase, UsageMap, M, &I);
+
+            // Depends on convertInsertShuffleToSplat for @VecSplat instructions
+            if (auto *BinOp = dyn_cast<BinaryOperator>(&I)) {
+                simplifyVecBinOpWithSplat(InstToErase, UsageMap, M, BinOp);
+            }
+
+            // Independent of above
+            if (auto *ICmp = dyn_cast<ICmpInst>(&I)) {
+                for (auto *U : ICmp->users()) {
+                    auto *Select = dyn_cast<SelectInst>(U);
+                    if (Select and Select->getCondition() == ICmp) {
+                        convertSelectICmp(M, Select, ICmp);
+                    }
+                }
+            }
+
+            // Independent of above, can run at any point
+            if (auto *Call = dyn_cast<CallInst>(&I)) {
+                convertQemuLoadStoreToPseudoInst(M, Call, InstToErase,
+                                                 UsageMap);
+                convertExceptionCallsToPseudoInst(M, Call);
+                convertReturnAddrToPseudoInst(M, Call);
+                convertImmediateSelectAccessGlobal(InstToErase, M, Call);
+                convertImmediateDeclCall(InstToErase, M, Call);
+                if (HasUserBranches) {
+                    convertUserBranchInstructions(M, DT, Exits, Call);
+                }
+            }
+
+            // Depends on other vector conversions performed above, needs to
+            // run last
+            if (auto *Store = dyn_cast<StoreInst>(&I)) {
+                convertVecStoreToPseudoInst(InstToErase, M, Store);
+            }
+        }
+
+        // Finally clean up instructions we need to remove manually
+        for (Instruction *I : InstToErase) {
+            I->eraseFromParent();
+        }
+
+        DenseMap<Constant *, CallInst *> Replacements;
+        for (Instruction &I : instructions(F)) {
+            defineVectorConstants(M, VL, &I, Replacements);
+        }
+    }
+}
+
+static Value *upcastInt(IRBuilder<> &Builder, IntegerType *FinalIntTy,
+                        Value *V) {
+    if (auto *ConstInt = dyn_cast<ConstantInt>(V)) {
+        return ConstantInt::get(FinalIntTy, ConstInt->getZExtValue());
+    } else {
+        return Builder.CreateSExt(V, FinalIntTy);
+    }
+}
+
+// Convert
+//
+//   %2 = ashr i[8|16] %1, %0
+//
+// to
+//
+//   %2 = zext i[8|16] %1 to i32
+//   %3 = zext i[8|16] %2 to i32
+//   %2 = ashr i32 %2, %3
+//
+static void upcastAshr(Instruction *I) {
+    // Only care about scalar shifts < on less than 32-bit integers
+    auto *IntTy = dyn_cast<IntegerType>(I->getType());
+    if (!IntTy or IntTy->getBitWidth() >= 32) {
+        return;
+    }
+
+    IRBuilder<> Builder(I);
+
+    Value *Op1 = I->getOperand(0);
+    Value *Op2 = I->getOperand(1);
+    auto *UpcastIntTy = Builder.getInt32Ty();
+    Op1 = upcastInt(Builder, UpcastIntTy, Op1);
+    Op2 = upcastInt(Builder, UpcastIntTy, Op2);
+
+    auto *AShr = Builder.CreateAShr(Op1, Op2);
+    auto *Trunc = Builder.CreateTrunc(AShr, I->getType());
+    I->replaceAllUsesWith(Trunc);
+}
+
+// Convert vector intrinsics
+//
+//   %0 = insertelement ...
+//   %1 = shuffle ...
+//
+// to
+//
+//   %0 = call @VecSplat.*
+//
+static void convertInsertShuffleToSplat(EraseInstVec &InstToErase,
+                                        UsageCountMap &UsageMap, Module &M,
+                                        Instruction *I) {
+    Value *SplatV;
+    if (match(I, compat_m_Shuffle(compat_m_InsertElt(m_Value(), m_Value(SplatV),
+                                                     m_ZeroInt()),
+                                  m_Value(), compat_m_ZeroMask()))) {
+        auto *VecTy = cast<VectorType>(I->getType());
+        IRBuilder<> Builder(I);
+        I->replaceAllUsesWith(
+            createPseudoInstCall(M, Builder, VecSplat, VecTy, {SplatV}));
+        addToEraseVectorIfUnused(InstToErase, UsageMap, I->getOperand(0));
+        InstToErase.push_back(I);
+    }
+}
+
+static void
+defineVectorConstants(Module &M, const VectorLayout &VL, Instruction *I,
+                      DenseMap<Constant *, CallInst *> &Replacements) {
+    for (size_t J = 0; J < I->getNumOperands(); ++J) {
+        Value *Op = I->getOperand(J);
+        auto *Const = dyn_cast<Constant>(Op);
+        auto *VecTy = dyn_cast<VectorType>(Op->getType());
+        if (!Const or !VecTy) {
+            // Only care about non-splatted constant vectors, skip
+            // everything else.
+            continue;
+        }
+
+        if (Replacements.count(Const)) {
+            I->setOperand(J, Replacements[Const]);
+            continue;
+        }
+
+        if (Value *Splat = Const->getSplatValue()) {
+            auto *VecTy = cast<VectorType>(Const->getType());
+            IRBuilder<> Builder(I);
+            CallInst *Call =
+                createPseudoInstCall(M, Builder, VecSplat, VecTy, {Splat});
+            I->setOperand(J, Call);
+            Replacements[Const] = Call;
+        } else {
+            // Constant non-splatted vector, attempt to combine elements
+            // to make it splattable.
+            SmallVector<uint64_t, 16> Ints;
+
+            // Copy over elements to a vector
+            const unsigned ElementCount = compat::getVectorElementCount(VecTy);
+            const unsigned ElementSize =
+                VecTy->getElementType()->getIntegerBitWidth();
+
+            for (unsigned I = 0; I < ElementCount; ++I) {
+                Constant *Element = Const->getAggregateElement(I);
+                uint64_t Value = Element->getUniqueInteger().getZExtValue();
+                Ints.push_back(Value);
+            }
+
+            // When combining adjacent elements, the maximum size supported
+            // by TCG is 64-bit.  MaxNumElements is the maximum amount of
+            // elements to attempt to merge
+            size_t PatternLen = 0;
+            const unsigned MaxNumElements = 8 * sizeof(uint64_t) / ElementSize;
+            for (unsigned N = MaxNumElements; N > 1; N /= 2) {
+                // Attempt to combine N elements by checking if the first
+                // N elements tile the vector.
+                bool Match = true;
+                for (unsigned J = 0; J < ElementCount; ++J) {
+                    if (Ints[J % N] != Ints[J]) {
+                        Match = false;
+                        break;
+                    }
+                }
+                // If tiling succeeded, break out
+                if (Match) {
+                    PatternLen = N;
+                    break;
+                }
+            }
+
+            if (PatternLen > 0) {
+                // Managed to tile vector with splattable element, compute
+                // final splattable value
+                // TODO: Move to VectorLayout.hpp
+                uint64_t Column = 0;
+                for (unsigned I = 0; I < PatternLen; ++I) {
+                    // Flips the indices if lane 0 is in the most-significant
+                    // bits.
+                    const size_t Index = VL.indexLane(PatternLen, 0, I);
+                    Column |= Ints[Index] << I * ElementSize;
+                }
+                IRBuilder<> Builder(I);
+                CallInst *Call = createPseudoInstCall(
+                    M, Builder, VecSplat, VecTy, {Builder.getInt64(Column)});
+                I->setOperand(J, Call);
+                Replacements[Const] = Call;
+            } else {
+                // Tiling failed, fall back to emitting an array copy from
+                // C to a gvec vector.
+                // TODO: Move to VectorLayout.hpp
+                IRBuilder<> Builder(I);
+                SmallVector<Constant *, 8> Columns;
+                const size_t VectorSize =
+                    (ElementCount * (ElementSize / 8) + 7);
+                const size_t ColumnCount = VectorSize / 8;
+                Columns.resize(ColumnCount);
+                for (size_t C = 0; C < ColumnCount; ++C) {
+                    uint64_t Column = 0;
+                    const size_t LanesPerColumn = 64 / ElementSize;
+                    for (size_t L = 0; L < LanesPerColumn; ++L) {
+                        const size_t Index = VL.indexLane(LanesPerColumn, C, L);
+                        Column |= Ints[Index] << (ElementSize * L);
+                    }
+                    assert(VL.BlockBytes >= 8);
+                    const size_t Size =
+                        std::min(ColumnCount, VL.BlockBytes / 8);
+                    const size_t Index = VL.indexBlock(Size, C);
+                    Columns[Index] = Builder.getInt64(Column);
+                }
+                CallInst *Call =
+                    createPseudoInstCall(M, Builder, VecConstant, VecTy,
+                                         {ConstantVector::get(Columns)});
+                I->setOperand(J, Call);
+                Replacements[Const] = Call;
+            }
+        }
+    }
+}
+
+// Convert
+//
+//   %1 = @VecSplat(%0)
+//   %2 = <NxM> ... op <NxM> %1
+//
+// to
+//
+//   %2 = call @Vec[op]Scalar(..., %0)
+//
+// which more closely matches TCG gvec operations.
+static void simplifyVecBinOpWithSplat(EraseInstVec &InstToErase,
+                                      UsageCountMap &UsageMap, Module &M,
+                                      BinaryOperator *BinOp) {
+    Value *Lhs = BinOp->getOperand(0);
+    Value *Rhs = BinOp->getOperand(1);
+    if (!Lhs->getType()->isVectorTy() or !Rhs->getType()->isVectorTy()) {
+        return;
+    }
+
+    // Get splat value from constant or @VecSplat call
+    Value *SplatValue = nullptr;
+    if (auto *Const = dyn_cast<Constant>(Rhs)) {
+        SplatValue = Const->getSplatValue();
+    } else if (auto *Call = dyn_cast<CallInst>(Rhs)) {
+        if (getPseudoInstFromCall(Call) == VecSplat) {
+            SplatValue = Call->getOperand(0);
+        }
+    }
+
+    if (SplatValue == nullptr) {
+        return;
+    }
+
+    auto *VecTy = cast<VectorType>(Lhs->getType());
+    auto *ConstInt = dyn_cast<ConstantInt>(SplatValue);
+    bool ConstIsNegOne = ConstInt and ConstInt->getSExtValue() == -1;
+    bool IsNot = BinOp->getOpcode() == Instruction::Xor and ConstIsNegOne;
+    if (IsNot) {
+        IRBuilder<> Builder(BinOp);
+        BinOp->replaceAllUsesWith(
+            createPseudoInstCall(M, Builder, VecNot, VecTy, {Lhs}));
+    } else {
+        PseudoInst Inst;
+        switch (BinOp->getOpcode()) {
+        case Instruction::Add:
+            Inst = VecAddScalar;
+            break;
+        case Instruction::Sub:
+            Inst = VecSubScalar;
+            break;
+        case Instruction::Mul:
+            Inst = VecMulScalar;
+            break;
+        case Instruction::Xor:
+            Inst = VecXorScalar;
+            break;
+        case Instruction::Or:
+            Inst = VecOrScalar;
+            break;
+        case Instruction::And:
+            Inst = VecAndScalar;
+            break;
+        case Instruction::Shl:
+            Inst = VecShlScalar;
+            break;
+        case Instruction::LShr:
+            Inst = VecLShrScalar;
+            break;
+        case Instruction::AShr:
+            Inst = VecAShrScalar;
+            break;
+        default:
+            abort();
+        }
+
+        IRBuilder<> Builder(BinOp);
+        // Scalar gvec shift operations uses 32-bit scalars, whereas arithmetic
+        // operations uses 64-bit scalars.
+        uint32_t SplatSize = SplatValue->getType()->getIntegerBitWidth();
+        if (BinOp->isShift()) {
+            if (SplatSize > 32) {
+                SplatValue =
+                    Builder.CreateTrunc(SplatValue, Builder.getInt32Ty());
+            }
+        } else {
+            if (SplatSize < 64) {
+                SplatValue =
+                    Builder.CreateZExt(SplatValue, Builder.getInt64Ty());
+            }
+        }
+        BinOp->replaceAllUsesWith(
+            createPseudoInstCall(M, Builder, Inst, VecTy, {Lhs, SplatValue}));
+    }
+
+    InstToErase.push_back(BinOp);
+    addToEraseVectorIfUnused(InstToErase, UsageMap, Rhs);
+}
+
+// Convert
+//
+//   %2 = icmp [sgt|ugt|slt|ult] %0, %1
+//   %5 = select %2, %3, %4
+//
+// to
+//
+//   %5 = [s|u][max|min] %0, %1
+//
+// if possible.  Results in cleaner IR, particularly useful for vector
+// instructions.
+static bool convertSelectICmpToMinMax(Module &M, SelectInst *Select,
+                                      ICmpInst *ICmp, ICmpInst::Predicate &Pred,
+                                      Value *ICmpOp0, Value *ICmpOp1,
+                                      Value *SelectOp0, Value *SelectOp1) {
+    if (ICmpOp0 != SelectOp0 or ICmpOp1 != SelectOp1) {
+        return false;
+    }
+
+    Intrinsic::ID Intrin;
+    switch (Pred) {
+    case ICmpInst::ICMP_SGT:
+        Intrin = Intrinsic::smax;
+        break;
+    case ICmpInst::ICMP_UGT:
+        Intrin = Intrinsic::umax;
+        break;
+    case ICmpInst::ICMP_SLT:
+        Intrin = Intrinsic::smin;
+        break;
+    case ICmpInst::ICMP_ULT:
+        Intrin = Intrinsic::umin;
+        break;
+    default:
+        return false;
+    }
+
+    auto Ty = Select->getType();
+    auto MaxMinF = compat::Intrinsic::getOrInsertDeclaration(&M, Intrin, {Ty});
+
+    IRBuilder<> Builder(Select);
+    auto Call = Builder.CreateCall(MaxMinF, {ICmpOp0, ICmpOp1});
+    Select->replaceAllUsesWith(Call);
+
+    return true;
+}
+
+// In LLVM, icmp on vectors returns a vector on i1s whereas TCGs gvec_cmp
+// returns a vector of the element type of its operands.  This can result in
+// some subtle bugs.  Convert
+//
+//   icmp -> call @VecCompare
+//   select -> call @VecWideCondBitsel
+//
+static bool convertSelectICmpToVecBitsel(Module &M, SelectInst *Select,
+                                         ICmpInst *ICmp,
+                                         ICmpInst::Predicate &Pred,
+                                         Value *ICmpOp0, Value *ICmpOp1,
+                                         Value *SelectOp0, Value *SelectOp1) {
+    auto *ICmpVecTy = dyn_cast<VectorType>(ICmpOp0->getType());
+    auto *SelectVecTy = dyn_cast<VectorType>(Select->getType());
+    if (!ICmpVecTy or !SelectVecTy) {
+        return false;
+    }
+
+    Instruction *Cmp = ICmp;
+    {
+        IRBuilder<> Builder(Cmp);
+        ICmpInst::Predicate Pred = ICmp->getPredicate();
+        CallInst *Call = createPseudoInstCall(
+            M, Builder, VecCompare, ICmpVecTy,
+            {ConstantInt::get(Builder.getInt32Ty(), Pred), ICmpOp0, ICmpOp1});
+        Cmp = Call;
+    }
+
+    unsigned SrcWidth = ICmpVecTy->getElementType()->getIntegerBitWidth();
+    unsigned DstWidth = SelectVecTy->getElementType()->getIntegerBitWidth();
+
+    IRBuilder<> Builder(Select);
+    if (SrcWidth < DstWidth) {
+        Cmp = cast<Instruction>(Builder.CreateSExt(Cmp, SelectVecTy));
+    } else if (SrcWidth > DstWidth) {
+        Cmp = cast<Instruction>(Builder.CreateTrunc(Cmp, SelectVecTy));
+    }
+    Select->replaceAllUsesWith(
+        createPseudoInstCall(M, Builder, VecWideCondBitsel, SelectVecTy,
+                             {Cmp, SelectOp0, SelectOp1}));
+
+    return true;
+}
+
+// Convert
+//
+//   %2 = icmp [sgt|ugt|slt|ult] %0, %1
+//   %5 = select %2, %3, %4
+//
+// to
+//
+//   5 = call @Movcond.[cond].*(%1, %0, %3, %4)
+//
+// to more closely match TCG semantics.
+static bool convertSelectICmpToMovcond(Module &M, SelectInst *Select,
+                                       ICmpInst *ICmp,
+                                       ICmpInst::Predicate &Pred,
+                                       Value *ICmpOp0, Value *ICmpOp1,
+                                       Value *SelectOp0, Value *SelectOp1) {
+    // We only handle integers, we have no movcond equivalent in gvec
+    auto *IntTy = dyn_cast<IntegerType>(Select->getType());
+    if (!IntTy) {
+        return false;
+    }
+
+    // If the type of the comparison does not match the return type of the
+    // select statement, we cannot do anything so skip
+    if (ICmpOp0->getType() != IntTy) {
+        return false;
+    }
+
+    IRBuilder<> Builder(Select);
+    if (cast<IntegerType>(ICmpOp0->getType())->getBitWidth() <
+        IntTy->getBitWidth()) {
+        if (ICmp->isSigned(Pred)) {
+            ICmpOp0 = Builder.CreateSExt(ICmpOp0, IntTy);
+            ICmpOp1 = Builder.CreateSExt(ICmpOp1, IntTy);
+        } else {
+            ICmpOp0 = Builder.CreateZExt(ICmpOp0, IntTy);
+            ICmpOp1 = Builder.CreateZExt(ICmpOp1, IntTy);
+        }
+    }
+
+    // Create @Movcond.[slt|...].* function
+    Select->replaceAllUsesWith(
+        createPseudoInstCall(M, Builder, Movcond, IntTy,
+                             {ConstantInt::get(IntTy, Pred), ICmpOp0, ICmpOp1,
+                              SelectOp0, SelectOp1}));
+
+    return true;
+}
+
+// Specialize
+//
+//   %2 = icmp [sgt|ugt|slt|ult] %0, %1
+//   %5 = select %2, %3, %4
+//
+// to either maximum/minimum, vector operations matching TCG, or a conditional
+// move that also matches TCG in sematics.
+static void convertSelectICmp(Module &M, SelectInst *Select, ICmpInst *ICmp) {
+    // Given
+    //   %2 = icmp [sgt|ugt|slt|ult] %0, %1
+    //   %5 = select %2, %3, %4
+    assert(Select->getCondition() == ICmp);
+    Value *ICmpOp0 = ICmp->getOperand(0);
+    Value *ICmpOp1 = ICmp->getOperand(1);
+    Value *SelectOp0 = Select->getTrueValue();
+    Value *SelectOp1 = Select->getFalseValue();
+    ICmpInst::Predicate Pred = ICmp->getPredicate();
+
+    // First try to convert to min/max
+    //   %5 = [s|u][max|min] %0, %1
+    if (convertSelectICmpToMinMax(M, Select, ICmp, Pred, ICmpOp0, ICmpOp1,
+                                  SelectOp0, SelectOp1)) {
+        return;
+    }
+
+    // Secondly try convert icmp -> @VecCompare, select -> @VecWideCondBitsel
+    if (convertSelectICmpToVecBitsel(M, Select, ICmp, Pred, ICmpOp0, ICmpOp1,
+                                     SelectOp0, SelectOp1)) {
+        return;
+    }
+
+    // If min/max and vector conversion failed we fallback to a movcond
+    //   %5 = call @Movcond.[cond].*(%1, %0, %3, %4)
+    convertSelectICmpToMovcond(M, Select, ICmp, Pred, ICmpOp0, ICmpOp1,
+                               SelectOp0, SelectOp1);
+}
+
+// Convert QEMU guest loads/stores represented by calls such as
+//
+//   call cpu_ldsw_be*(),
+//   call cpu_stq_le*(),
+//
+// and friends, to pseudo instructions
+//
+//   %5 = call @GuestLoad.*(%addr, %sign, %size, %endian);
+//   %5 = call @GuestStore.*(%addr, %value, %size, %endian);
+//
+// Makes the backend agnostic to what instructions or calls are used to
+// represent loads and stores.
+static void convertQemuLoadStoreToPseudoInst(Module &M, CallInst *Call,
+                                             EraseInstVec &InstToErase,
+                                             UsageCountMap &UsageMap) {
+    Function *F = Call->getCalledFunction();
+    StringRef Name = F->getName();
+    if (Name.consume_front("cpu_")) {
+        bool IsLoad = Name.consume_front("ld");
+        bool IsStore = !IsLoad and Name.consume_front("st");
+        if (IsLoad or IsStore) {
+            bool Signed = !Name.consume_front("u");
+
+            uint8_t Size = 0;
+            switch (Name[0]) {
+            case 'b':
+                Size = 1;
+                break;
+            case 'w':
+                Size = 2;
+                break;
+            case 'l':
+                Size = 4;
+                break;
+            case 'q':
+                Size = 8;
+                break;
+            default:
+                abort();
+            }
+
+            uint8_t Endianness = 0;
+            if (Size > 1 and Size < 8) {
+                Name = Name.drop_front(2);
+                switch (Name[0]) {
+                case 'l':
+                    Endianness = 1;
+                    break;
+                case 'b':
+                    Endianness = 2;
+                    break;
+                default:
+                    // TODO: we need to parse the `MemOpIndex` in the second to
+                    // last argument, for now just assume little endian, same
+                    // goes for signedness.
+                    Endianness = 1;
+                }
+            }
+
+            IRBuilder<> Builder(Call);
+            Value *AddrOp = Call->getArgOperand(1);
+            IntegerType *FlagTy = Builder.getInt8Ty();
+            Value *SizeOp = ConstantInt::get(FlagTy, Size);
+            Value *EndianOp = ConstantInt::get(FlagTy, Endianness);
+            CallInst *NewCall;
+            if (IsLoad) {
+                Value *SignOp = ConstantInt::get(FlagTy, Signed);
+                IntegerType *RetTy = cast<IntegerType>(Call->getType());
+                NewCall =
+                    createPseudoInstCall(M, Builder, GuestLoad, RetTy,
+                                         {AddrOp, SignOp, SizeOp, EndianOp});
+            } else {
+                Value *ValueOp = Call->getArgOperand(2);
+                NewCall = createPseudoInstCall(
+                    M, Builder, GuestStore, Builder.getVoidTy(),
+                    {AddrOp, ValueOp, SizeOp, EndianOp});
+            }
+            Call->replaceAllUsesWith(NewCall);
+            InstToErase.push_back(Call);
+        }
+    }
+}
+
+// Convert QEMU exception calls
+//
+//   call raise_exception_ra(...),
+//   ...
+//
+// to a pseudo instruction
+//
+//   %5 = call @Exception.*(...);
+//
+// Makes the backend agnostic to what instructions or calls are used to
+// represent exceptions, and the list of sources can be expanded here.
+static void convertExceptionCallsToPseudoInst(Module &M, CallInst *Call) {
+    Function *F = Call->getCalledFunction();
+    if (F->getName() == "raise_exception_ra") {
+        IRBuilder<> Builder(Call);
+        Value *Op0 = Call->getArgOperand(0);
+        Value *Op1 = Call->getArgOperand(1);
+        Call->replaceAllUsesWith(createPseudoInstCall(
+            M, Builder, Exception, Builder.getVoidTy(), {Op0, Op1}));
+    }
+}
+
+// Convert QEMU exception calls
+//
+//   %0 = call @llvm.returnaddr(...),
+//   %1 = ptrtoint %0
+//   ...
+//
+// to a pseudo instruction
+//
+//   %0 = call @getpc(...);
+//
+static void convertReturnAddrToPseudoInst(Module &M, CallInst *Call) {
+    Function *F = Call->getCalledFunction();
+    if (!F->isIntrinsic() or F->getIntrinsicID() != Intrinsic::returnaddress) {
+        return;
+    }
+    if (!Call->hasOneUse()) {
+        return;
+    }
+
+    auto *PtrToInt = dyn_cast<PtrToIntInst>(*Call->user_begin());
+    if (!PtrToInt) {
+        return;
+    }
+
+    IRBuilder<> Builder(PtrToInt);
+    PtrToInt->replaceAllUsesWith(
+        createPseudoInstCall(M, Builder, GetPC, PtrToInt->getType(), {}));
+}
+
+// Convert QEMU exception calls
+//
+//   %0 = call @llvm.returnaddr(...),
+//   %1 = ptrtoint %0
+//   ...
+//
+// to a pseudo instruction
+//
+//   %0 = call @getpc(...);
+//
+static void convertImmediateSelectAccessGlobal(EraseInstVec &InstToErase,
+                                               Module &M, CallInst *Call) {
+    PseudoInst PI = getPseudoInstFromCall(Call);
+    if (PI != AccessGlobalArray) {
+        return;
+    }
+
+    if (!Call->hasOneUser()) {
+        return;
+    }
+
+    auto *Load = dyn_cast<LoadInst>(*Call->user_begin());
+    if (!Load) {
+        return;
+    }
+
+    auto *Zext = dyn_cast<ZExtInst>(Call->getArgOperand(1));
+    if (!Zext) {
+        return;
+    }
+
+    auto *Select = dyn_cast<SelectInst>(Zext->getOperand(0));
+    if (!Select) {
+        return;
+    }
+
+    if (!Zext->hasOneUse() or !Select->hasOneUse()) {
+        return;
+    }
+
+    Value *Cond = Select->getCondition();
+    Value *Arg1 = Select->getTrueValue();
+    Value *Arg2 = Select->getFalseValue();
+
+    if (!isa<Argument>(Arg1) or !isa<Argument>(Arg2)) {
+        return;
+    }
+
+    IRBuilder<> Builder(Select);
+    Function *AccessGlobalFn = Call->getCalledFunction();
+    Value *TypeIndex = Call->getArgOperand(0);
+    Value *Offset = Call->getArgOperand(1);
+    Value *Arg1Zext = Builder.CreateZExt(Arg1, Zext->getType());
+    Value *Arg2Zext = Builder.CreateZExt(Arg2, Zext->getType());
+    CallInst *Access1 =
+        Builder.CreateCall(AccessGlobalFn, {TypeIndex, Offset, Arg1Zext});
+    CallInst *Access2 =
+        Builder.CreateCall(AccessGlobalFn, {TypeIndex, Offset, Arg2Zext});
+    LoadInst *Load1 = Builder.CreateLoad(Load->getType(), Access1);
+    LoadInst *Load2 = Builder.CreateLoad(Load->getType(), Access2);
+    Value *NewSelect = Builder.CreateSelect(Cond, Load1, Load2);
+
+    InstToErase.push_back(Load);
+    InstToErase.push_back(Call);
+
+    Load->replaceAllUsesWith(NewSelect);
+}
+
+struct DeclReplaceInfo {
+    bool Valid = false;
+    Value *Zext = nullptr;
+    Value *Select = nullptr;
+    Value *Cond = nullptr;
+    Value *Arg1 = nullptr;
+    Value *Arg2 = nullptr;
+};
+
+static DeclReplaceInfo isReplaceableDeclCall(Value *A) {
+    auto *Zext = dyn_cast<ZExtInst>(A);
+    if (!Zext) {
+        return {};
+    }
+
+    auto *Select = dyn_cast<SelectInst>(Zext->getOperand(0));
+    if (!Select) {
+        return {};
+    }
+
+    if (!Zext->hasOneUse() or !Select->hasOneUse()) {
+        return {};
+    }
+
+    Value *Cond = Select->getCondition();
+    Value *Arg1 = Select->getTrueValue();
+    Value *Arg2 = Select->getFalseValue();
+
+    if (!isa<Argument>(Arg1) or !isa<Argument>(Arg2)) {
+        return {};
+    }
+
+    return {true, Zext, Select, Cond, Arg1, Arg2};
+}
+
+static void convertImmediateDeclCall(EraseInstVec &InstToErase, Module &M,
+                                     CallInst *Call) {
+    if (!Call->getCalledFunction()->isDeclaration()) {
+        return;
+    }
+
+    if (!Call->hasOneUser()) {
+        return;
+    }
+
+    SmallVector<Value *, 8> Args1;
+    SmallVector<Value *, 8> Args2;
+    DeclReplaceInfo Info{};
+    IRBuilder<> Builder(Call);
+    for (auto &A : Call->args()) {
+        if (!Info.Valid) {
+            Info = isReplaceableDeclCall(A);
+            if (Info.Valid) {
+                Value *Arg1Zext =
+                    Builder.CreateZExt(Info.Arg1, Info.Zext->getType());
+                Value *Arg2Zext =
+                    Builder.CreateZExt(Info.Arg2, Info.Zext->getType());
+                Args1.push_back(Arg1Zext);
+                Args2.push_back(Arg2Zext);
+                continue;
+            }
+        }
+
+        Args1.push_back(A);
+        Args2.push_back(A);
+    }
+
+    if (!Info.Valid) {
+        return;
+    }
+
+    Function *F = Call->getCalledFunction();
+    CallInst *Access1 = Builder.CreateCall(F, Args1);
+    CallInst *Access2 = Builder.CreateCall(F, Args2);
+    Value *NewSelect = Builder.CreateSelect(Info.Cond, Access1, Access2);
+    Access1->setDebugLoc(Call->getDebugLoc());
+    Access2->setDebugLoc(Call->getDebugLoc());
+
+    // InstToErase.push_back(Load);
+    InstToErase.push_back(Call);
+    InstToErase.push_back(cast<Instruction>(Info.Zext));
+
+    Call->replaceAllUsesWith(NewSelect);
+}
+
+static void convertUserBranchInstructions(Module &M,
+                                          DominatorTreeAnalysis::Result &DT,
+                                          const ExitSet &Exits,
+                                          CallInst *Call) {
+    Function *F = Call->getCalledFunction();
+    if (F->getName() != UserPCRelBranchFunc) {
+        return;
+    }
+
+    // Make sure the user-supplied functions exist and have the correct
+    // signature.
+    Function *BranchF = M.getFunction(UserPCRelBranchFunc);
+    Function *CondF = M.getFunction(UserPCRelBranchConditionalFunc);
+    Function *FallF = M.getFunction(UserPCRelBranchFallthroughFunc);
+    if (!CondF or !FallF or BranchF->getType() != CondF->getType() or
+        FallF->getType()->getFunctionNumParams()) {
+        return;
+    }
+
+    BasicBlock *BB = Call->getParent();
+    bool DominatesAllExits = true;
+    for (BasicBlock *E : Exits) {
+        if (!DT.dominates(BB, E)) {
+            DominatesAllExits = false;
+        }
+    }
+    // If the call to the user-supplied pc-relative jump function dominates all
+    // exit blocks, then leave it be, it's a single relative jump with one exit.
+    if (DominatesAllExits) {
+        return;
+    }
+
+    // Otherwise add a fallthrough operation to each exit block.
+    for (BasicBlock *E : Exits) {
+        IRBuilder<> Builder(E->getTerminator());
+        Builder.CreateCall(FallF, {});
+    }
+
+    // And replace the current jump operation with a "conditinal" one, so
+    // QEMU knows how to chain the generated code.
+    Call->setCalledFunction(CondF);
+}
+
+//
+// Following functions help with converting between different types of
+// instructions to pseudo instructions, particularly ones that write
+// to a pointer, aka the Vec*Store pseudo instructions
+//
+
+static PseudoInst instructionToStorePseudoInst(unsigned Opcode) {
+    switch (Opcode) {
+    case Instruction::Trunc:
+        return VecTruncStore;
+    case Instruction::ZExt:
+        return VecZExtStore;
+    case Instruction::SExt:
+        return VecSExtStore;
+    case Instruction::Select:
+        return VecSelectStore;
+    case Instruction::Add:
+        return VecAddStore;
+    case Instruction::Sub:
+        return VecSubStore;
+    case Instruction::Mul:
+        return VecMulStore;
+    case Instruction::Xor:
+        return VecXorStore;
+    case Instruction::Or:
+        return VecOrStore;
+    case Instruction::And:
+        return VecAndStore;
+    case Instruction::Shl:
+        return VecShlStore;
+    case Instruction::LShr:
+        return VecLShrStore;
+    case Instruction::AShr:
+        return VecAShrStore;
+    default:
+        abort();
+    }
+}
+
+static PseudoInst pseudoInstToStorePseudoInst(PseudoInst Inst) {
+    switch (Inst) {
+    case VecNot:
+        return VecNotStore;
+    case VecAddScalar:
+        return VecAddScalarStore;
+    case VecSubScalar:
+        return VecSubScalarStore;
+    case VecMulScalar:
+        return VecMulScalarStore;
+    case VecXorScalar:
+        return VecXorScalarStore;
+    case VecOrScalar:
+        return VecOrScalarStore;
+    case VecAndScalar:
+        return VecAndScalarStore;
+    case VecShlScalar:
+        return VecShlScalarStore;
+    case VecLShrScalar:
+        return VecLShrScalarStore;
+    case VecAShrScalar:
+        return VecAShrScalarStore;
+    case VecWideCondBitsel:
+        return VecWideCondBitselStore;
+    default:
+        abort();
+    }
+}
+
+static PseudoInst intrinsicToStorePseudoInst(unsigned IntrinsicID) {
+    switch (IntrinsicID) {
+    case Intrinsic::sadd_sat:
+        return VecSignedSatAddStore;
+    case Intrinsic::ssub_sat:
+        return VecSignedSatSubStore;
+    case Intrinsic::fshr:
+        return VecFunnelShrStore;
+    case Intrinsic::abs:
+        return VecAbsStore;
+    case Intrinsic::smax:
+        return VecSignedMaxStore;
+    case Intrinsic::umax:
+        return VecUnsignedMaxStore;
+    case Intrinsic::smin:
+        return VecSignedMinStore;
+    case Intrinsic::umin:
+        return VecUnsignedMinStore;
+    case Intrinsic::ctlz:
+        return VecCtlzStore;
+    case Intrinsic::cttz:
+        return VecCttzStore;
+    case Intrinsic::ctpop:
+        return VecCtpopStore;
+    default:
+        abort();
+    }
+}
+
+// For binary/unary ops on vectors where the result is stored to a
+// pointer
+//
+//   %3 = <NxM> %1 [op] <NxM> %2
+//   %4 = bitcast i8* %0 to <NxM>*
+//   store <NxM> %3, <NxM>* %4
+//
+// to
+//
+//   call @Vec[Op]Store.*(%0, %1, %2)
+//
+// This deals with the duality of pointers and vectors, and
+// simplifies the backend.  We previously kept a map on the
+// side to propagate "vector"-ness from %3 to %4 via the store,
+// no longer!
+static void convertVecStoreToPseudoInst(EraseInstVec &InstToErase, Module &M,
+                                        StoreInst *Store) {
+    Value *ValueOp = Store->getValueOperand();
+    Type *ValueTy = ValueOp->getType();
+    if (!ValueTy->isVectorTy()) {
+        return;
+    }
+
+    // Ensure store and binary op. are in the same basic
+    // block since the op. is moved to the store.
+    bool InSameBB =
+        cast<Instruction>(ValueOp)->getParent() == Store->getParent();
+    if (!InSameBB) {
+        return;
+    }
+
+    SmallVector<Value *, 3> Args;
+    Value *PtrOp = Store->getPointerOperand();
+    if (auto *BinOp = dyn_cast<BinaryOperator>(ValueOp)) {
+        Instruction *Inst = cast<Instruction>(ValueOp);
+        PseudoInst NewInst = instructionToStorePseudoInst(BinOp->getOpcode());
+        IRBuilder<> Builder(Store);
+        // Add one to account for extra store pointer
+        // argument of Vec*Store pseudo instructions.
+        const uint8_t SharedArgCount = pseudoInstArgCount(NewInst) - 1;
+        Args.push_back(PtrOp);
+        for (unsigned I = 0; I < SharedArgCount; ++I) {
+            Value *Op = Inst->getOperand(I);
+            Args.push_back(Op);
+        }
+        createPseudoInstCall(M, Builder, NewInst, Builder.getVoidTy(), Args);
+    } else if (auto *Call = dyn_cast<CallInst>(ValueOp)) {
+        Function *F = Call->getCalledFunction();
+        PseudoInst OldInst = getPseudoInstFromCall(Call);
+        if (OldInst != InvalidPseudoInst) {
+            // Map scalar vector pseudo instructions to
+            // store variants
+            PseudoInst NewInst = pseudoInstToStorePseudoInst(OldInst);
+            IRBuilder<> Builder(Store);
+            Args.push_back(PtrOp);
+            for (Value *Op : Call->args()) {
+                Args.push_back(Op);
+            }
+            createPseudoInstCall(M, Builder, NewInst, Builder.getVoidTy(),
+                                 Args);
+        } else if (F->isIntrinsic()) {
+            Instruction *Inst = cast<Instruction>(ValueOp);
+            PseudoInst NewInst =
+                intrinsicToStorePseudoInst(F->getIntrinsicID());
+            // Add one to account for extra store pointer
+            // argument of Vec*Store pseudo instructions.
+            const uint8_t SharedArgCount = pseudoInstArgCount(NewInst) - 1;
+            IRBuilder<> Builder(Store);
+            Args.push_back(PtrOp);
+            for (unsigned I = 0; I < SharedArgCount; ++I) {
+                Args.push_back(Inst->getOperand(I));
+            }
+            createPseudoInstCall(M, Builder, NewInst, Builder.getVoidTy(),
+                                 Args);
+        }
+    } else if (auto *Load = dyn_cast<LoadInst>(ValueOp)) {
+        auto *VecTy = cast<VectorType>(Load->getType());
+        auto *IntTy = cast<IntegerType>(VecTy->getElementType());
+        uint32_t LlvmSize = IntTy->getBitWidth();
+        uint32_t VectorElements = compat::getVectorElementCount(VecTy);
+        IRBuilder<> Builder(Store);
+        auto *Size = Builder.getInt64(LlvmSize * VectorElements);
+        Builder.CreateMemCpy(Store->getPointerOperand(),
+                             Store->getPointerAlignment(M.getDataLayout()),
+                             Load->getPointerOperand(),
+                             Load->getPointerAlignment(M.getDataLayout()),
+                             Size);
+        // Remove load if possible, won't get cleaned up by DCE
+        if (Load->hasOneUse()) {
+            InstToErase.push_back(cast<Instruction>(Load));
+        }
+    } else {
+        Instruction *Inst = cast<Instruction>(ValueOp);
+
+        PseudoInst NewInst = instructionToStorePseudoInst(Inst->getOpcode());
+        // Add one to account for extra store pointer
+        // argument of Vec*Store pseudo instructions.
+        const uint8_t SharedArgCount = pseudoInstArgCount(NewInst) - 2;
+
+        assert(SharedArgCount > 0 and
+               SharedArgCount <= (uint8_t)Inst->getNumOperands());
+        IRBuilder<> Builder(Store);
+        SmallVector<Value *, 8> Args;
+        auto SizeTy = Type::getInt8Ty(M.getContext());
+        Args.push_back(ConstantInt::get(
+            SizeTy,
+            cast<VectorType>(ValueTy)->getElementType()->getIntegerBitWidth()));
+        Args.push_back(PtrOp);
+        for (uint8_t I = 0; I < SharedArgCount; ++I) {
+            Args.push_back(Inst->getOperand(I));
+        }
+        createPseudoInstCall(M, Builder, NewInst, Builder.getVoidTy(), Args);
+    }
+
+    // Remove store instruction, this ensures DCE
+    // can cleanup the rest, we also remove ValueOp
+    // here since it's a call and won't get cleaned
+    // by DCE.
+    if (!isa<LoadInst>(ValueOp)) {
+        InstToErase.push_back(cast<Instruction>(ValueOp));
+    }
+    InstToErase.push_back(Store);
+}
diff --git a/subprojects/helper-to-tcg/src/PrepareForTcgPass/CanonicalizeIR.hpp b/subprojects/helper-to-tcg/src/PrepareForTcgPass/CanonicalizeIR.hpp
new file mode 100644
index 0000000000..089cd75a7b
--- /dev/null
+++ b/subprojects/helper-to-tcg/src/PrepareForTcgPass/CanonicalizeIR.hpp
@@ -0,0 +1,24 @@
+//
+//  Copyright(c) 2026 rev.ng Labs Srl. All Rights Reserved.
+//
+//  This program is free software; you can redistribute it and/or modify
+//  it under the terms of the GNU General Public License as published by
+//  the Free Software Foundation; either version 2 of the License, or
+//  (at your option) any later version.
+//
+//  This program is distributed in the hope that it will be useful,
+//  but WITHOUT ANY WARRANTY; without even the implied warranty of
+//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//  GNU General Public License for more details.
+//
+//  You should have received a copy of the GNU General Public License
+//  along with this program; if not, see <http://www.gnu.org/licenses/>.
+//
+
+#pragma once
+
+#include <llvm/IR/PassManager.h>
+
+struct VectorLayout;
+
+void canonicalizeIR(llvm::Module &M, llvm::ModuleAnalysisManager &MAM, const VectorLayout &VL);
diff --git a/subprojects/helper-to-tcg/src/PrepareForTcgPass/PrepareForTcgPass.cpp b/subprojects/helper-to-tcg/src/PrepareForTcgPass/PrepareForTcgPass.cpp
index 10129fa245..71749e2f6f 100644
--- a/subprojects/helper-to-tcg/src/PrepareForTcgPass/PrepareForTcgPass.cpp
+++ b/subprojects/helper-to-tcg/src/PrepareForTcgPass/PrepareForTcgPass.cpp
@@ -16,6 +16,7 @@
 //
 
 #include "PrepareForTcgPass.hpp"
+#include "CanonicalizeIR.hpp"
 #include "CmdLineOptions.hpp"
 #include "TransformGEPs.hpp"
 
@@ -140,5 +141,6 @@ PreservedAnalyses PrepareForTcgPass::run(Module &M,
     for (Function &F : M) {
         transformGEPs(M, F, ResultTcgGlobalMap, TypeIndexMap, DebugInfo);
     }
+    canonicalizeIR(M, MAM, VL);
     return PreservedAnalyses::none();
 }
-- 
2.52.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.