/// \file CPUStateAccessAnalysisPass.cpp
/// This file performs an analysis for reconstructing the access patterns to the
/// CPU State Variables (CSV).

//
// This file is distributed under the MIT License. See LICENSE.md for details.
//

#include <sstream>
#include <stack>
#include <string>
#include <vector>

#include "llvm/ADT/SetVector.h"
#include "llvm/ADT/SmallPtrSet.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Type.h"
#include "llvm/IR/Value.h"
#include "llvm/IR/Verifier.h"
#include "llvm/Support/Casting.h"
#include "llvm/Support/raw_ostream.h"

#include "revng/Support/Debug.h"
#include "revng/Support/FunctionTags.h"
#include "revng/Support/IRHelpers.h"

#include "CPUStateAccessAnalysisPass.h"
#include "VariableManager.h"

namespace llvm {
class DataLayout;
}

using namespace llvm;

using std::optional;

using ConstFunctionPtrSet = std::set<const Function *>;
using InstrPtrSet = llvm::SetVector<Instruction *>;
using CallPtrSet = std::set<CallInst *>;
using ConstValuePtrSet = std::set<const Value *>;

/// Logger for forwardTaintAnalysis
static auto TaintLog = Logger<>("cpustate-taint-analysis");
/// Logger for the creation of WorkItem
static auto CSVAccessLog = Logger<>("cpustate-access-analysis");
/// Logger for fixing the accesses to CPUState
static auto FixAccessLog = Logger<>("cpustate-fix-access");

static uint64_t NumUnknown = 0;
static std::map<std::string, uint64_t> FunToNumUnknown;
static std::map<std::string, std::set<std::string>> FunToUnknowns;

/// Computes the set of Functions reachable from a given Function through direct
/// calls.
///
/// \param RootFunction is a pointer to the Function from which is the
///        starting point for computing reachability.
/// \param LoadMDKind is the metadata kind used for decorating call sites that
///        access CPU State to load data
/// \param StoreMDKind is the metadata kind used for decorating call sites that
///        access CPU State to store data
/// \param Lazy tells if the analysis is running in Lazy mode
/// \return set of pointers to the reachable Functions
///
/// This function can probably be implemented using the call graph utilities
/// already present in LLVM, but when I tried it I spent a day doing it and I
/// gave up because of bugs in the functions coming from LLVM. Then I
/// implemented my own version, i.e. this function
static ConstFunctionPtrSet
computeDirectlyReachableFunctions(const Function *RootFunction,
                                  const unsigned LoadMDKind,
                                  const unsigned StoreMDKind,
                                  const bool Lazy) {
  std::map<const Function *, ConstFunctionPtrSet> CallGraph;
  const Module &M = *RootFunction->getParent();

  // Initialize empty CallGraph
  for (const Function &F : M)
    CallGraph[&F] = {};

  for (const Function &F : M) {
    for (const Use &U : F.uses()) {
      const User *TheUser = U.getUser();
      if (const auto *TheCall = dyn_cast<const CallInst>(TheUser)) {
        // If this call has already been decorated with LoadMDKind or
        // StoreMDKind metadata it means that it has already been processed by
        // a previous run of CPUStateAccessAnalysis.
        // If we're running in lazy mode, we can skip calls that have already
        // been decorated.
        if (Lazy) {
          if (TheCall->getMetadata(LoadMDKind) != nullptr
              or TheCall->getMetadata(StoreMDKind) != nullptr) {
            continue;
          }
        }
        const Function *Caller = TheCall->getFunction();
        const Function *Callee = getCallee(TheCall);
        if (Callee == &F) {
          CallGraph[Caller].insert(Callee);
        }
      } else if (const auto *CExpr = dyn_cast<const ConstantExpr>(TheUser)) {
        SmallPtrSet<const ConstantExpr *, 16> CurBitCasts;
        SmallPtrSet<const ConstantExpr *, 16> NextBitCasts;
        CurBitCasts.insert(CExpr);
        while (not CurBitCasts.empty()) {
          NextBitCasts.clear();
          for (const ConstantExpr *BitCast : CurBitCasts) {
            for (const User *BitCastUser : BitCast->users()) {
              const auto *TheCall = dyn_cast<const CallInst>(BitCastUser);
              const auto *NewCExpr = dyn_cast<const ConstantExpr>(BitCastUser);
              if (TheCall) {
                // If this call has already been decorated with LoadMDKind or
                // StoreMDKind metadata it means that it has already been
                // processed by a previous run of CPUStateAccessAnalysis.
                // If we're running in lazy mode, we can skip calls that have
                // already been decorated.
                if (Lazy) {
                  if (TheCall->getMetadata(LoadMDKind) != nullptr
                      or TheCall->getMetadata(StoreMDKind) != nullptr) {
                    continue;
                  }
                }
                const Function *Caller = TheCall->getFunction();
                const Function *Callee = getCallee(TheCall);
                if (Callee == &F) {
                  CallGraph[Caller].insert(Callee);
                }
              } else if (NewCExpr) {
                NextBitCasts.insert(NewCExpr);
              }
            }
          }
          std::swap(CurBitCasts, NextBitCasts);
        }
      }
    }
  }

  ConstFunctionPtrSet ReachableFunctions = { RootFunction };
  ConstFunctionPtrSet CurrentChildren = { RootFunction };
  ConstFunctionPtrSet NextChildren;
  while (not CurrentChildren.empty()) {
    NextChildren.clear();
    for (const Function *F : CurrentChildren) {
      for (const Function *Callee : CallGraph.at(F)) {
        bool NewInsertion = ReachableFunctions.insert(Callee).second;
        if (NewInsertion)
          NextChildren.insert(Callee);
      }
    }
    std::swap(CurrentChildren, NextChildren);
  }

  return ReachableFunctions;
}

struct TaintResults {

  // A set of Instructions that access the CSV to load data. They can be
  // LoadInst or CallInst to Intrinsic::memcpy for which the size is known.
  InstrPtrSet TaintedLoads;

  // A set of Instructions that access the CSV to store data. They can be
  // LoadInst or CallInst to Intrinsic::memcpy for which the size is known.
  InstrPtrSet TaintedStores;

  // A set of Values that are tainted during the analysis.
  ConstValuePtrSet TaintedValues;

  // A set of CallInst that are considered illegal. These include indirect
  // calls, calls to functions without body, and calls to Intrinsinc::memcpy
  // with unknown size. They are considered illegal because we have no way of
  // knowing how they access the CSV.
  CallPtrSet IllegalCalls;

  bool empty() const noexcept {
    return TaintedLoads.empty() and TaintedStores.empty()
           and IllegalCalls.empty();
  }
};

/// Interprocedural forward taint analysis.
//
/// \param CPUStatePtr is a pointer to the CPU State Variable, which is a global
///        variable. This variable is the Value that taints all the others.
/// \param ReachableFunctions is a set of functions that are reachable from
///        the root function. The analysis is restricted to those functions.
/// \param LoadMDKind is the metadata kind used for decorating call sites that
///        access CPU State to load data
/// \param StoreMDKind is the metadata kind used for decorating call sites that
///        access CPU State to store data
/// \param Lazy tells if the analysis is running in Lazy mode
/// \return a TaintResults containing information on:
///         1) a set of instructions that access the CSV to load data;
///         2) a set of instructions that access the CSV to store data;
///         3) a set of illegal calls, for which it is impossible to understand
///            if and how they will access the CSV.
static TaintResults
forwardTaintAnalysis(const Module *M,
                     Value *CPUStatePtr,
                     const ConstFunctionPtrSet &ReachableFunctions,
                     const unsigned LoadMDKind,
                     const unsigned StoreMDKind,
                     const bool Lazy) {
  //
  // Interprocedural Forward Taint Analysis
  //

  // This analysis aims to understand all the Values that are affected by the
  // CPUStatePtr pointer, that points to the CPUStateVariables (CSVs).
  // The main idea is that we explore the use chains depth first, propagating
  // interprocedurally when we find a Use whose User is a CallInstr.
  //
  // The function is structured as follows:
  // 1. Iterate on the users of `CPUStatePtr`
  //   2. For each user of `CPUStatePtr`, consider its next user,
  //      building a WorkList of `Use`s in exploration
  //   3. If we find unexplored uses keep pushing them on the WorkList. If we
  //      find a Load or a Store we taint it and don't push anything on the
  //      WorkList. If it's not a Load or Store we mark it as tainted separately
  //      3(a) If the next `Use` to explore is a `CallInst` the taint is
  //      propagated interprocedurally to the callee, through the arguments
  //      (propagation from caller to callee)
  //      3(b) If the next `Use` to explore is a `RetInst` the taint is
  //      propagated interprocedurally to the Function.
  //      (propagation from the callee to all call sites)
  //   4. If we didn't push anything on the WorkList we can start exploring the
  //      other `Use`s of the item that is currently on top of the WorkList
  //   5. If we didn't push anything on the WorkList we can start popping `Use`s
  //      from the WorkList, until we reach a `Value` that still has unexplored
  //      `Use`s
  //      5(a) If we're popping an argument of a function this means that we've
  //      finished analyzing the uses of that argument. We have to make sure
  //      that, if the taint reached the return instructions in the function,
  //      the taint is propagated to the call sites.
  //   6. After popping the top of the WorkList in 5., if the new top of the
  //      WorkList still has unexplored uses start to explore them.

  TaintResults Results;
  std::set<std::pair<const Function *, const Argument *>>
    FunctionArgTaintsReturn;

  revng_assert(CPUStatePtr != nullptr);
  revng_assert(CPUStatePtr->getType()->isPointerTy());

  struct CallSiteInfo {
    const CallInst *CallSite = nullptr;
    const Argument *Arg = nullptr;
    const unsigned ArgNo;
    CallSiteInfo(const CallInst *C, const Argument *A, const unsigned N) :
      CallSite(C), Arg(A), ArgNo(N) {}
  };

  if (TaintLog.isEnabled()) {
    TaintLog << "MODULE:" << DoLog;
    TaintLog << dumpToString(M) << DoLog;
  }

  // 1. Iterate on the users of `CPUStatePtr`
  std::set<const LoadInst *> Loads;
  {
    std::queue<const User *> WorkList;
    for (const User *U : CPUStatePtr->users())
      WorkList.push(U);

    while (not WorkList.empty()) {
      const User *U = WorkList.front();
      WorkList.pop();

      if (auto *CE = dyn_cast<ConstantExpr>(U)) {
        if (CE->isCast()) {
          for (const User *UCE : CE->users()) {
            WorkList.push(UCE);
          }
        }
      } else if (auto *Load = dyn_cast<LoadInst>(U)) {
        Loads.insert(Load);
      } else {
        revng_abort("Unexpected user");
      }
    }
  }

  for (const LoadInst *Load : Loads) {

    // During the analysis we keep two stacks.
    // ToTaintWorkList is a stack representing the Values currently enqued that
    // must be tainted and for which we still have to analyze the uses.
    // CallSites is a stack representing the CallInst that we entered for
    // interprocedural propagation.
    std::stack<const Use *> ToTaintWorkList;
    std::stack<CallSiteInfo> CallSiteInfos;

    // Sanity check for the uses of CPUStatePtr.
    // They must all be direct Loads from CPUStatePtr.
    revng_assert(Load->getPointerOperand()->stripPointerCasts() == CPUStatePtr);

    // Push the first use on the WorkList
    const Function *F = Load->getFunction();
    if (Load->getNumUses() != 0 and ReachableFunctions.contains(F)) {
      if (TaintLog.isEnabled()) {
        TaintLog << "Tainted origin: " << Load << DoLog;
        TaintLog << dumpToString(Load) << DoLog;
        TaintLog.indent();
      }
      ToTaintWorkList.push(&*Load->use_begin());
    }

    //   2. For each user of `CPUStatePtr`, consider its next user,
    //      building a WorkList of `Use`s in exploration
    while (not ToTaintWorkList.empty()) {
      const Use *TheUse = ToTaintWorkList.top();
      revng_assert(TheUse != nullptr);
      auto *TheUser = cast<Instruction>(TheUse->getUser());
      const auto OpCode = TheUser->getOpcode();
      if (TaintLog.isEnabled()) {
        TaintLog << "Inst: " << TheUser << DoLog;
        TaintLog << dumpToString(TheUser) << DoLog;
      }

      const size_t Size = ToTaintWorkList.size();

      // 3. If we find unexplored uses keep pushing them on the WorkList. If we
      // find a Load or a Store we taint it and don't push anything on the
      // WorkList. If it's not a Load or Store we mark it as tainted separately

      // This switch explores the use-chains depth-first, pushing unexplored
      // uses on the ToTaintWorkList if necessary.
      unsigned OperandNo = TheUse->getOperandNo();
      switch (OpCode) {
      case Instruction::Load: {
        TaintLog << "LOAD" << DoLog;

        revng_assert(OperandNo == LoadInst::getPointerOperandIndex());
        auto *L = cast<LoadInst>(TheUser);
        if (TheUse->get() == L->getPointerOperand()) {
          if (TaintLog.isEnabled()) {
            TaintLog << "TAINT: " << TheUser << DoLog;
            TaintLog << dumpToString(TheUser) << DoLog;
          }
          Results.TaintedLoads.insert(TheUser);
        }
      } break;
      case Instruction::Store: {
        TaintLog << "STORE" << DoLog;
        revng_assert(OperandNo == StoreInst::getPointerOperandIndex());
        auto *S = cast<StoreInst>(TheUser);
        if (TheUse->get() == S->getPointerOperand()) {
          if (TaintLog.isEnabled()) {
            TaintLog << "TAINT: " << TheUser << DoLog;
            TaintLog << dumpToString(TheUser) << DoLog;
          }
          Results.TaintedStores.insert(TheUser);
        }
      } break;
      case Instruction::Trunc:
      case Instruction::ZExt:
      case Instruction::SExt:
      case Instruction::BitCast:
      case Instruction::IntToPtr:
      case Instruction::PtrToInt:
      case Instruction::GetElementPtr:
      case Instruction::PHI:
      case Instruction::Add: {
        TaintLog << "OP" << DoLog;
        auto OperandId = GetElementPtrInst::getPointerOperandIndex();
        revng_assert(OpCode != Instruction::GetElementPtr
                     or TheUse->getOperandNo() == OperandId);

        // Taint TheUser, and if this is the first time we taint it we also push
        // on the ToTaintWorkList its first use that is not tainted
        bool JustTainted = Results.TaintedValues.insert(TheUser).second;
        if (TaintLog.isEnabled()) {
          TaintLog << "TAINT: " << TheUser << DoLog;
          TaintLog << dumpToString(TheUser) << DoLog;
        }
        if (JustTainted) {
          TaintLog << "Just Tainted" << DoLog;
          for (const Use &U : TheUser->uses()) {
            TaintLog << "User: " << U.getUser() << DoLog;
            if (!Results.TaintedValues.contains(U.getUser())) {
              TaintLog << "PUSH" << DoLog;
              ToTaintWorkList.push(&U);
              TaintLog.indent();
              break;
            }
          }
        }
      } break;
      case Instruction::Call: {
        TaintLog << "CALL" << DoLog;
        auto *TheCall = cast<CallInst>(TheUser);

        // If this call has already been decorated with LoadMDKind or
        // StoreMDKind metadata it means that it has already been processed by
        // a previous run of CPUStateAccessAnalysis.
        // If we're running in lazy mode, we can skip calls that have already
        // been decorated.
        if (Lazy) {
          if (TheCall->getMetadata(LoadMDKind) != nullptr
              or TheCall->getMetadata(StoreMDKind) != nullptr) {
            TaintLog << "Decorated in a previous run" << DoLog;
            break;
          }
        }

        // 3(a) If the next `Use` to explore is a `CallInst` the taint is
        // propagated interprocedurally to the callee, through the arguments
        // (propagation from caller to callee)
        Function *Callee = getCallee(TheCall);

        // Indirect calls, calls to functions without body, and calls to
        // Intrinsic::memcpy with non-constant size are considered illegal,
        // because we cannot know how they will affect the CPU State
        if (Callee == nullptr) {
          TaintLog << "Illegal -- indirect call" << DoLog;
          Results.IllegalCalls.insert(TheCall);
          break;
        }
        if (Callee->getIntrinsicID() == Intrinsic::memcpy) {
          unsigned OpNo = TheUse->getOperandNo();
          revng_assert(OpNo == 0 or OpNo == 1);
          if (isa<ConstantInt>(TheCall->getArgOperand(2))) {
            if (OpNo == 0) {
              if (TaintLog.isEnabled()) {
                TaintLog << "TAINT: " << TheUser << DoLog;
                TaintLog << dumpToString(TheUser) << DoLog;
              }
              Results.TaintedStores.insert(TheUser);
            }
            if (OpNo == 1) {
              if (TaintLog.isEnabled()) {
                TaintLog << "TAINT: " << TheUser << DoLog;
                TaintLog << dumpToString(TheUser) << DoLog;
              }
              Results.TaintedLoads.insert(TheUser);
            }
          } else {
            TaintLog << "Illegal -- unknown size memcpy" << DoLog;
            Results.IllegalCalls.insert(TheCall);
          }
          break;
        } else if (Callee->empty()) {
          TaintLog << "Illegal -- no body" << DoLog;
          Results.IllegalCalls.insert(TheCall);
          break;
        }
        revng_assert(ReachableFunctions.contains(Callee));

        // Select the correct formal argument associated with this use
        const Argument *FormalArgument = nullptr;
        unsigned ArgNo = 0;
        for (const Argument &Arg : Callee->args()) {
          ArgNo = Arg.getArgNo();
          if (TheUse->getOperandNo() == ArgNo) {
            FormalArgument = &Arg;
            break;
          }
        }
        TaintLog << "Found Argument" << DoLog;
        revng_assert(FormalArgument != nullptr);

        // Taint the Argument, and if this is the first time we taint it we
        // also push on the ToTaintWorkList its first use that is not tainted.
        if (TaintLog.isEnabled()) {
          TaintLog << "Argument: " << FormalArgument << DoLog;
          TaintLog << dumpToString(FormalArgument) << DoLog;
        }
        bool JustTainted = Results.TaintedValues.insert(FormalArgument).second;
        if (JustTainted) {
          TaintLog << "Just Tainted" << DoLog;
          for (const Use &U : FormalArgument->uses()) {
            if (TaintLog.isEnabled()) {
              TaintLog << "User: " << U.getUser() << DoLog;
              TaintLog << dumpToString(U.getUser()) << DoLog;
            }
            if (!Results.TaintedValues.contains(U.getUser())) {
              TaintLog << "PUSH" << DoLog;
              ToTaintWorkList.push(&U);
              TaintLog.indent();

              // We also push the call to the CallSites stack, because if we put
              // the uses of the argument on the ToTaintWorkList we are actually
              // starting to perform the analysis inside the callee.
              CallSiteInfos.push(CallSiteInfo(TheCall, FormalArgument, ArgNo));
              break;
            }
          }
        } else if (FunctionArgTaintsReturn.contains({ Callee,
                                                      FormalArgument })) {
          // It means that a previous exploration of the graph has reached this
          // argument, and propagated a taint until a return value. This means
          // that this we must taint the call and start exploring its unexplored
          // users if any.
          bool JustTainted = Results.TaintedValues.insert(TheUser).second;
          if (TaintLog.isEnabled()) {
            TaintLog << "TAINT: " << TheUser << DoLog;
            TaintLog << dumpToString(TheUser) << DoLog;
          }
          if (JustTainted) {
            TaintLog << "Just Tainted" << DoLog;
            for (const Use &U : TheUser->uses()) {
              TaintLog << "User: " << U.getUser() << DoLog;
              if (!Results.TaintedValues.contains(U.getUser())) {
                TaintLog << "PUSH" << DoLog;
                ToTaintWorkList.push(&U);
                TaintLog.indent();
                break;
              }
            }
          }
        }
      } break;
      case Instruction::Ret: {

        // 3(b) If the next `Use` to explore is a `RetInst` the taint is
        // propagated interprocedurally to the Function.
        // (propagation from the callee to all call sites)
        TaintLog << "RET" << DoLog;
        revng_assert(not CallSiteInfos.empty());

        // Taint the return instruction, then, if this is the first time that we
        // taint also the call site, so that it's marked for propagation of the
        // taint analysis to its uses.
        if (TaintLog.isEnabled()) {
          TaintLog << "TAINT: " << TheUser << DoLog;
          TaintLog << dumpToString(TheUser) << DoLog;
        }
        bool JustTainted = Results.TaintedValues.insert(TheUser).second;
        if (JustTainted) {
          const CallSiteInfo &CSInfo = CallSiteInfos.top();

          Results.TaintedValues.insert(CSInfo.CallSite);

          const Function *Callee = getCallee(CSInfo.CallSite);
          FunctionArgTaintsReturn.insert({ Callee, CSInfo.Arg });

          if (TaintLog.isEnabled()) {
            TaintLog << "TAINT: " << CSInfo.CallSite << DoLog;
            TaintLog << dumpToString(CallSiteInfos.top().CallSite) << DoLog;
            llvm::StringRef Name = getCallee(CSInfo.CallSite)->getName();
            TaintLog << "pair: < " << Name << ", " << CSInfo.ArgNo << " > "
                     << DoLog;
          }
        }
      } break;
      case Instruction::Switch:
      case Instruction::ICmp:
      case Instruction::And:
      case Instruction::Or:
        break;
      default:
        revng_abort();
      }

      // If we pushed something new on the ToTaintWorkList we want to keep
      // exploring its uses until we reach a leaf.
      if (Size < ToTaintWorkList.size())
        continue;
      TaintLog << "not grown" << DoLog;

      // 4. If we didn't push anything on the WorkList we can start exploring
      // the other `Use`s of the item that is currently on top of the WorkList
      if (Size == ToTaintWorkList.size()) {
        Use *NextUse = TheUse->getNext();
        if (NextUse != nullptr) {
          TaintLog << "advance" << DoLog;
          ToTaintWorkList.top() = NextUse;
          continue;
        }
      }

      TaintLog << "Done" << DoLog;

      // 5. If we didn't push anything on the WorkList we can start popping
      //    `Use`s from the WorkList, until we reach a `Value` that still has
      //    unexplored `Use`s
      //
      // If we reach this point we have finished to explore all the uses of the
      // item that is currently on top of the ToTaintWorkList stack.
      // We want to pop it and to handle Arguments in a special way.
      const Use *UnexploredUse = nullptr;
      while (not ToTaintWorkList.empty() and UnexploredUse == nullptr) {
        const Use *PoppedTopUse = ToTaintWorkList.top();
        if (TaintLog.isEnabled()) {
          TaintLog << "POP : " << PoppedTopUse->get() << DoLog;
          TaintLog << dumpToString(PoppedTopUse->get()) << DoLog;
        }
        if (TaintLog.isEnabled()) {
          TaintLog << "PoppedUser : " << PoppedTopUse->getUser() << DoLog;
          TaintLog << dumpToString(PoppedTopUse->getUser()) << DoLog;
        }
        ToTaintWorkList.pop();
        TaintLog.unindent();
        Argument *Arg = dyn_cast<Argument>(PoppedTopUse->get());
        bool PoppedHasAllExploredSources = PoppedTopUse->getNext() == nullptr;
        if (Arg and PoppedHasAllExploredSources) {
          // 5(a) If we're popping an argument of a function this means that we
          // finished analyzing the uses of that argument. We have to make sure
          // that, if the taint reached the return instructions in the function,
          // the taint is propagated to the call sites.
          TaintLog << "Finish Argument" << DoLog;

          const CallSiteInfo &CSInfo = CallSiteInfos.top();
          revng_assert(CSInfo.Arg == Arg);

          unsigned ArgNo = Arg->getArgNo();
          revng_assert(CSInfo.ArgNo == ArgNo);

          const User *ArgUser = ToTaintWorkList.top()->getUser();
          const auto *CallSite = cast<const CallInst>(ArgUser);
          const Function *Callee = getCallee(CallSite);

          if (TaintLog.isEnabled()) {
            TaintLog << "CallSite: " << CallSite << DoLog;
            TaintLog << dumpToString(CallSite) << DoLog;
          }

          // If the CallSite was tainted it means that the taint analysis
          // reached at least one of the return values of the callee.
          // Hence, the taint can propagate to the uses of the call.
          // The same holds if we already know that the pair (Callee, ArgNo)
          // taints the return.
          if (Results.TaintedValues.contains(CallSite)
              or FunctionArgTaintsReturn.contains({ Callee, Arg })) {
            if (CallSite->getNumUses() != 0) {
              UnexploredUse = &*CallSite->use_begin();
            }
          }

          // If PoppedTopUse is an Use of an Argument the next available Use on
          // the stack must be an argument of a CallInst. This CallInst was the
          // call from where we started analyzing the uses of the function
          // Argument for which we just popped the PoppedTopUse.
          CallSiteInfos.pop();
        } else {
          TaintLog << "NOT Finished or NOT Argument" << DoLog;
          UnexploredUse = PoppedTopUse->getNext();
        }
      }

      // 6. After popping the top of the WorkList in 5., if the new top of the
      //    WorkList still has unexplored uses start to explore them.

      // If we have a new UnexploredUse we push it and we continue
      // because that's the new use that must be analyzed.
      if (UnexploredUse != nullptr) {
        TaintLog << "PUSH: " << UnexploredUse->get() << DoLog;
        TaintLog << "User: " << UnexploredUse->getUser() << DoLog;
        ToTaintWorkList.push(UnexploredUse);
        TaintLog.indent();
      }
    }
  }

  if (not Lazy) {
    QuickMetadata QMD(M->getContext());
    for (CallInst *Call : Results.IllegalCalls) {
      CallInst *Abort = CallInst::Create(M->getFunction("abort"), {}, Call);
      auto IllegalCallsMDKind = M->getContext().getMDKindID("revng.csaa."
                                                            "illegal.calls");
      Abort->setMetadata(IllegalCallsMDKind, QMD.tuple((uint32_t) 0));
    }
  }
  return Results;
}

class WorkItem {

public:
  using size_type = SmallVector<const Use *, 3>::size_type;
  using iterator = SmallVector<const Use *, 3>::iterator;
  using const_iterator = SmallVector<const Use *, 3>::const_iterator;

private:
  // The value whose sources we're analyzing
  Value *CurrentValue = nullptr;

  // Sources are kind of the opposite of `Use`s. Every pointer in this vector
  // points to a `Use` whose `User` is the `Value` pointed by the `CurrentValue`
  // member of this `WorkItem`.
  SmallVector<const Use *, 3> Sources;

  // The index of the Source that is currently considered for the analysis
  size_type SourceIndex;

public:
  WorkItem() : CurrentValue(nullptr), Sources(), SourceIndex(0) {}

  explicit WorkItem(Instruction *I) :
    CurrentValue(I), Sources(), SourceIndex(0) {
    if (not isa<StoreInst>(I)) {
      for (const Use &OpUse : I->operands()) {
        Sources.push_back(&OpUse);
      }
    } else {
      const auto PtrOpNum = StoreInst::getPointerOperandIndex();
      Sources.push_back(&I->getOperandUse(PtrOpNum));
    }
    revng_assert(not Sources.empty());
  }

  explicit WorkItem(Argument *A,
                    const ConstFunctionPtrSet &ReachableFunctions,
                    const bool IsLazy,
                    const unsigned LoadMDKind,
                    const unsigned StoreMDKind) :
    CurrentValue(A), Sources(), SourceIndex(0) {
    const Function *F = A->getParent();
    revng_assert(not F->empty());
    revng_log(CSVAccessLog, "Function: " << F);
    const unsigned ArgNo = A->getArgNo();
    revng_log(CSVAccessLog, "ArgNo: " << ArgNo);
    for (const Use &FUse : F->uses()) {
      const User *FUser = FUse.getUser();
      revng_log(CSVAccessLog, "FUser: " << FUser);
      if (const auto *FCall = dyn_cast<const CallInst>(FUser)) {
        revng_log(CSVAccessLog, "Is a Call");
        const Function *Caller = FCall->getFunction();
        revng_log(CSVAccessLog, "Caller: " << Caller);

        // If this call has already been decorated with LoadMDKind or
        // StoreMDKind metadata it means that it has already been processed by
        // a previous run of CPUStateAccessAnalysis.
        // If we're running in lazy mode, we can skip calls that have already
        // been decorated.
        if (IsLazy) {
          if (FCall->getMetadata(LoadMDKind) != nullptr
              or FCall->getMetadata(StoreMDKind) != nullptr) {
            revng_log(CSVAccessLog, "Already marked");
            continue;
          }
        }

        if (ReachableFunctions.contains(Caller)) {
          revng_log(CSVAccessLog,
                    "Is Reachable CallInst:" << FCall << " : "
                                             << dumpToString(FCall));
          const Use &ActualArgUse = FCall->getArgOperandUse(ArgNo);
          revng_log(CSVAccessLog,
                    "ActualUse:" << ActualArgUse.getUser() << " : "
                                 << dumpToString(ActualArgUse.getUser()));
          Sources.push_back(&ActualArgUse);
        } else {
          revng_log(CSVAccessLog, "NOT Reachable");
        }
      } else if (const auto *CExpr = dyn_cast<const ConstantExpr>(FUser)) {
        revng_log(CSVAccessLog, "Bitcast");
        const auto OpCode = CExpr->getOpcode();
        revng_assert(OpCode == Instruction::BitCast);
        for (const User *RealCall : CExpr->users()) {
          revng_log(CSVAccessLog,
                    "RealCall: " << RealCall << " : "
                                 << dumpToString(RealCall));
          const auto *FCall = dyn_cast<const CallInst>(RealCall);
          revng_log(CSVAccessLog,
                    "CallInst: " << FCall << " : " << dumpToString(FCall));

          // If this call has already been decorated with LoadMDKind or
          // StoreMDKind metadata it means that it has already been processed by
          // a previous run of CPUStateAccessAnalysis.
          // If we're running in lazy mode, we can skip calls that have already
          // been decorated.
          if (IsLazy) {
            if (FCall->getMetadata(LoadMDKind) != nullptr
                or FCall->getMetadata(StoreMDKind) != nullptr) {
              revng_log(CSVAccessLog, "Already marked");
              continue;
            }
          }

          if (FCall) {
            const Function *Caller = FCall->getFunction();
            revng_log(CSVAccessLog, "Caller: " << Caller);
            if (ReachableFunctions.contains(Caller)) {
              const Use &ActualArgUse = FCall->getArgOperandUse(ArgNo);
              revng_log(CSVAccessLog,
                        "ActualUse:" << ActualArgUse.getUser() << " : "
                                     << dumpToString(ActualArgUse.getUser()));
              Sources.push_back(&ActualArgUse);
            }
          }
        }
      }
    }
    // This might be too strict, because the arguments of the root function
    // don't have any sources. However, we assume that we never reach them.
    revng_assert(not Sources.empty());
  }

  explicit WorkItem(CallInst *C,
                    const bool IsLoad,
                    const bool IsLazy,
                    const unsigned LoadMDKind,
                    const unsigned StoreMDKind) :
    CurrentValue(C), Sources(), SourceIndex(0) {

    revng_assert(not IsLazy
                 or (C->getMetadata(LoadMDKind) == nullptr
                     and C->getMetadata(StoreMDKind) == nullptr));

    const Function *F = getCallee(C);
    revng_assert(F != nullptr); // Assume no indirect calls
    if (F->getIntrinsicID() == Intrinsic::memcpy) {
      const Use &AddrOp = C->getOperandUse(IsLoad ? 1 : 0);
      Sources.push_back(&AddrOp);
      const Use &SizeOp = C->getOperandUse(2);
      Sources.push_back(&SizeOp);
    } else {
      for (const BasicBlock &BB : *F) {
        const Instruction *I = BB.getTerminator();
        if (I and isa<ReturnInst>(I) and I->getNumOperands() != 0) {
          revng_assert(I->getNumOperands() == 1);
          const Use &RetValUse = I->getOperandUse(0);
          Sources.push_back(&RetValUse);
        }
      }
    }
    revng_assert(not Sources.empty());
  }

public:
  friend inline void writeToLog(Logger<true> &L, const WorkItem &I, int) {
    L << "Value: " << I.val() << " : " << dumpToString(I.val()) << DoLog;
    L << "Sources = {" << DoLog;
    L.indent();
    for (const Use *U : I.sources())
      L << U->get() << " : " << dumpToString(U->get()) << DoLog;
    L << "}" << DoLog;
    L << "Curr Src Id: " << I.SourceIndex;
    L.unindent();
  }

public:
  Value *val() const { return CurrentValue; };

  const Use *currentSourceUse() const {
    if (SourceIndex < Sources.size())
      return Sources[SourceIndex];
    return nullptr;
  }

  Value *currentSourceValue() const {
    const Use *CurrUse = currentSourceUse();
    return CurrUse ? CurrUse->get() : nullptr;
  }

  const Use *nextSourceUse() const {
    const auto Size = Sources.size();
    if (SourceIndex < Size and (SourceIndex + 1) < Size)
      return Sources[SourceIndex + 1];
    return nullptr;
  }

  Value *nextSourceValue() const {
    const Use *CurrUse = nextSourceUse();
    return CurrUse ? CurrUse->get() : nullptr;
  }

  size_type getSourceIndex() const { return SourceIndex; }

  void setSourceIndex(size_type I) {
    revng_assert(I < Sources.size());
    SourceIndex = I;
  }

  size_type getNumSources() const { return Sources.size(); }

  llvm::iterator_range<const_iterator> sources() const {
    return { Sources.begin(), Sources.end() };
  }
};

/// Gets a valid pointer to `CallInst` if the current source of `Item` is a call
/// from `Root`
///
/// This function returns `nullptr` if the current source of `Item` is not a
/// call from `Root`
static CallInst *getCurSourceRootCall(const WorkItem &Item,
                                      const Function *Root) {
  revng_log(CSVAccessLog, "getCurSourceRootCall");
  CallInst *RootCall = nullptr;
  if (isa<Argument>(Item.val())) {
    revng_log(CSVAccessLog, "isa<Argument>");
    User *ActualArgUser = Item.currentSourceUse()->getUser();
    auto *Call = cast<CallInst>(ActualArgUser);
    revng_log(CSVAccessLog, "argument: " << dumpToString(Item.val()));
    revng_log(CSVAccessLog, "call: " << dumpToString(Call));
    Function *F = Call->getFunction();
    revng_log(CSVAccessLog, "parent: " << F->getName());
    if (F == Root)
      RootCall = Call;
  }
  revng_log(CSVAccessLog, RootCall);
  return RootCall;
}

/// Gets a valid pointer to `CallInst` if the next source of `Item` is a call
/// from `Root`
///
/// This function return `nullptr` if the next source of `Item` is not a call
/// from `Root`
static CallInst *getNextSourceRootCall(const WorkItem &Item,
                                       const Function *Root) {
  revng_log(CSVAccessLog, "getNextSourceRootCall");
  CallInst *RootCall = nullptr;
  if (isa<Argument>(Item.val())) {
    revng_log(CSVAccessLog, "isa<Argument>");
    const Use *NextSrcUse = Item.nextSourceUse();
    if (NextSrcUse != nullptr) {
      User *ActualArgUser = NextSrcUse->getUser();
      auto *Call = cast<CallInst>(ActualArgUser);
      revng_log(CSVAccessLog, "argument: " << dumpToString(Item.val()));
      revng_log(CSVAccessLog, "call: " << dumpToString(Call));
      Function *F = Call->getFunction();
      revng_log(CSVAccessLog, "parent: " << F->getName());
      if (F == Root)
        RootCall = Call;
    }
  }
  revng_log(CSVAccessLog, RootCall);
  return RootCall;
}

/// Gets the `Shift`-th bit of `Input`
static int getBit(uint64_t Input, int Shift) {
  return (Input >> Shift) & 1;
};

using CallSiteOffsetMap = std::map<CallInst *, CSVOffsets>;
using ValueCallSiteOffsetMap = std::map<Value *, CallSiteOffsetMap>;
using OptCSVOffsets = std::optional<CSVOffsets>;

/// This class is used to fold constant offsets on different instructions
template<class T>
class CRTPOffsetFolder {

protected:
  using offset_iterator = std::set<int64_t>::const_iterator;
  using offset_iterator_range = llvm::iterator_range<offset_iterator>;
  using OffsetPair = std::pair<const CSVOffsets *, const CSVOffsets *>;

protected:
  // These should be constant but ConstantInt::get() does not have
  // const-qualifier on the first argument:
  // static ConstantInt *get(IntegerType *Ty, uint64_t V, bool isSigned=false)
  // However, it does not really change Ty, because it only call const
  // methods on it, so it should be safe.
  IntegerType *Int64Ty = nullptr;
  IntegerType *Int32Ty = nullptr;
  const DataLayout &DL;

public:
  CRTPOffsetFolder(const Module &M) :
    Int64Ty(IntegerType::getInt64Ty(M.getContext())),
    Int32Ty(IntegerType::getInt32Ty(M.getContext())),
    DL(M.getDataLayout()) {}

  static void insertOrCombine(Value *V,
                              CallInst *C,
                              CSVOffsets &&O,
                              ValueCallSiteOffsetMap &OffsetMap) {
    revng_log(CSVAccessLog, "MAP: " << V);
    bool Inserted;
    CallSiteOffsetMap::iterator It;
    std::tie(It, Inserted) = OffsetMap[V].insert(std::make_pair(C, O));
    if (not Inserted)
      It->second.combine(O);
  }

public:
  /// This method folds the offsets on the sources to Item
  ///
  /// \param Item is the `WorkItem` whose sources must be folded
  /// \param [in, out] is the Map used to retrieve the values of the offsets of
  ///                  the sources of `Item`, and also to store the result of
  ///                  the folded offsets for `Item`.
  void fold(const WorkItem &Item, ValueCallSiteOffsetMap &OffsetMap) {
    WorkItem::size_type NumSrcs = Item.getNumSources();
    revng_assert(NumSrcs);
    SmallVector<Constant *, 4> Operands(NumSrcs, nullptr);
    // Collect Call Sites across all the sources of this User
    CallPtrSet CallSites;
    SmallVector<const CallSiteOffsetMap *, 4> SrcCallSiteOffsetsPtrs;
    SmallVector<const CSVOffsets *, 4> NonRootOffsetsPtrs;
    SrcCallSiteOffsetsPtrs.reserve(NumSrcs);
    NonRootOffsetsPtrs.reserve(NumSrcs);
    for (const Use *U : Item.sources()) {
      const CallSiteOffsetMap &CallSiteOffsets = OffsetMap.at(U->get());
      revng_assert(not CallSiteOffsets.empty());
      const CSVOffsets *NonRootOffsets = nullptr;
      for (const auto &CSO : CallSiteOffsets) {
        CallInst *TheCall = CSO.first;
        CallSites.insert(TheCall);
        if (TheCall == nullptr) {
          NonRootOffsets = &CSO.second;
        }
      }

      // The order of iteration on SrcCallSiteOffsetsPtrs is the same
      // as the order of iteration on Item.sources()
      SrcCallSiteOffsetsPtrs.push_back(&CallSiteOffsets);

      // The order of iteration on NonRootOffsets is the same as the order
      // of iteration on Item.sources()
      NonRootOffsetsPtrs.push_back(NonRootOffsets);
    }
    revng_assert(NumSrcs == SrcCallSiteOffsetsPtrs.size());
    revng_assert(NumSrcs == NonRootOffsetsPtrs.size());

    if (CSVAccessLog.isEnabled())
      for (const CallInst *C : CallSites)
        CSVAccessLog << "C: " << C << " : " << dumpToString(C) << DoLog;

    // Check that each source has all the callsites or nullptr
    for (const auto &CSOffsets : SrcCallSiteOffsetsPtrs) {
      bool FoundNullptr = false;
      if (CSOffsets->contains(nullptr))
        FoundNullptr = true;

      bool FoundAllCalls = true;
      for (CallInst *C : CallSites) {
        if (C != nullptr and not CSOffsets->contains(C)) {
          FoundAllCalls = false;
          break;
        }
      }
      revng_assert(FoundNullptr or FoundAllCalls);
    }

    for (CallInst *C : CallSites) {
      SmallVector<OffsetPair, 4> SrcOffsets(NumSrcs,
                                            OffsetPair(nullptr, nullptr));
      bool EmptyPair = false;
      for (WorkItem::size_type SI = 0; SI < NumSrcs; ++SI) {
        const CSVOffsets *NonRootOffset = NonRootOffsetsPtrs[SI];
        if (C != nullptr)
          SrcOffsets[SI].second = NonRootOffset;
        const auto CallSiteOffsets = SrcCallSiteOffsetsPtrs[SI];
        auto OffsetsEnd = CallSiteOffsets->end();
        auto OffsetsIt = CallSiteOffsets->find(C);
        if (OffsetsIt != OffsetsEnd)
          SrcOffsets[SI].first = &OffsetsIt->second;
        if (SrcOffsets[SI].first == nullptr
            and SrcOffsets[SI].second == nullptr) {
          // This means that one the pairs is empty and we can drop entirely
          // this call. This happens when C == nullptr and one of the sources
          // has no nullptr call site
          EmptyPair = true;
        }
      }
      if (EmptyPair)
        continue;
      revng_log(CSVAccessLog, "start");
      revng_assert(NumSrcs < (8ULL * sizeof(uint64_t)));
      uint64_t Combinations = 1ULL << NumSrcs;
      Value *V = Item.val();
      Instruction *I = cast<Instruction>(V);
      for (uint64_t Index = 0; Index < Combinations; ++Index) {
        revng_log(CSVAccessLog, "Index: " << Index);
        SmallVector<const CSVOffsets *, 4> OffsetTuple;
        OffsetTuple.reserve(NumSrcs);

        // Build the tuple of offset sets that we want to use to compute the
        // transfer function. If the analyzed call is nullptr we can skip
        // some stuff and keep the computation smaller.
        revng_log(CSVAccessLog, "callsite: " << C << " : " << dumpToString(C));
        if (C != nullptr) {
          for (WorkItem::size_type SI = 0; SI < NumSrcs; ++SI) {
            int Bit = getBit(Index, SI);
            const CSVOffsets *O0 = SrcOffsets[SI].first;
            const CSVOffsets *O1 = SrcOffsets[SI].second;
            const CSVOffsets *O = Bit ? O1 : O0;
            if (O == nullptr)
              break;
            revng_log(CSVAccessLog, "nonnull");
            OffsetTuple.push_back(O);
          }
        } else {
          for (WorkItem::size_type SI = 0; SI < NumSrcs; ++SI) {
            const CSVOffsets *O = NonRootOffsetsPtrs[SI];
            if (O == nullptr)
              break;
            revng_log(CSVAccessLog, "nonnull");
            OffsetTuple.push_back(O);
          }
        }
        revng_log(CSVAccessLog,
                  "NumSrcs:" << NumSrcs
                             << " -- Tuple Size:" << OffsetTuple.size());
        if (OffsetTuple.size() != NumSrcs)
          continue;

        bool Valid;
        CSVOffsets::Kind ResKind;
        SmallVector<optional<CSVOffsets>, 4> UpdatedOffsetTuple(NumSrcs);
        revng_assert(not UpdatedOffsetTuple[0].has_value());
        std::tie(Valid,
                 ResKind) = T::checkOffsetTupleIsValid(OffsetTuple,
                                                       I,
                                                       UpdatedOffsetTuple);
        revng_assert(UpdatedOffsetTuple.size() == OffsetTuple.size());
        if (not Valid) {
          insertOrCombine(V, C, CSVOffsets(ResKind), OffsetMap);
          continue;
        }
        revng_log(CSVAccessLog, "valid tuple");

        SmallVector<offset_iterator_range, 4> OffsetsRanges;
        SmallVector<offset_iterator, 4> OffsetsIt;
        OffsetsRanges.reserve(NumSrcs);
        OffsetsIt.reserve(NumSrcs);

        WorkItem::size_type CartesianSize = 1;
        for (WorkItem::size_type SI = 0; SI < NumSrcs; ++SI) {
          OptCSVOffsets &UpdatedOffsets = UpdatedOffsetTuple[SI];
          bool WasUpdated = UpdatedOffsets.has_value();
          const CSVOffsets *Tuple = WasUpdated ? &*UpdatedOffsets :
                                                 OffsetTuple[SI];
          OffsetsRanges.push_back(make_range(Tuple->begin(), Tuple->end()));
          OffsetsIt.push_back(Tuple->begin());
          const WorkItem::size_type OffsetSize = Tuple->size();
          revng_assert(OffsetSize);
          revng_assert(CartesianSize <= CartesianSize * OffsetSize);
          CartesianSize *= OffsetSize;
        }

        do {
          CSVOffsets ResOffset = foldOffsets(ResKind, NumSrcs, I, OffsetsIt);
          insertOrCombine(V, C, std::move(ResOffset), OffsetMap);
          // Advance the iterators
          {
            WorkItem::size_type SI = 0;
            bool Wrapped = false;
            do {
              revng_log(CSVAccessLog, "SI: " << SI);
              if (std::next(OffsetsIt[SI]) == OffsetsRanges[SI].end()) {
                OffsetsIt[SI] = OffsetsRanges[SI].begin();
                Wrapped = true;
                revng_log(CSVAccessLog, "WRAP");
              } else {
                revng_log(CSVAccessLog, "NO-WRAP");
                std::advance(OffsetsIt[SI], 1);
                Wrapped = false;
              }
            } while (Wrapped and ++SI < NumSrcs);
            revng_log(CSVAccessLog, "incremented");
          }
        } while (--CartesianSize);
      }
    }
  }

private:
  CSVOffsets foldOffsets(CSVOffsets::Kind ResKind,
                         WorkItem::size_type NumSrcs,
                         Instruction *I,
                         const SmallVector<offset_iterator, 4> &OffsetsIt) {
    return static_cast<T *>(this)->foldOffsets(ResKind, NumSrcs, I, OffsetsIt);
  }
};

/// Specialization of CRTPOffsetFolder for sums and subtractions
class AddSubOffsetFolder : public CRTPOffsetFolder<AddSubOffsetFolder> {

public:
  AddSubOffsetFolder(const Module &M) :
    CRTPOffsetFolder<AddSubOffsetFolder>(M) {}

public:
  friend class CRTPOffsetFolder<AddSubOffsetFolder>;

private:
  static std::pair<bool, CSVOffsets::Kind>
  checkOffsetTupleIsValid(const SmallVector<const CSVOffsets *, 4> &OffsetTuple,
                          const Instruction *I,
                          SmallVector<OptCSVOffsets, 4> &) {
    revng_assert(OffsetTuple.size() == 2);
    auto OpCode = I->getOpcode();
    revng_assert(OpCode == Instruction::Add or OpCode == Instruction::Sub);
    const auto O0 = OffsetTuple[0], O1 = OffsetTuple[1];
    if (OpCode == Instruction::Add) {
      // Cannot add pointers
      revng_assert(not(O0->isPtr() and O1->isPtr()));
    } else {
      // Cannot subtract a pointer from something else
      revng_assert(not O1->isPtr());
    }

    if (O0->isUnknown() or O1->isUnknown()) {
      if (O0->isOnlyInPtr() or O1->isOnlyInPtr())
        return { false, CSVOffsets::Kind::UnknownInPtr };
      if (O0->isInOutPtr() or O1->isInOutPtr())
        return { false, CSVOffsets::Kind::OutAndUnknownInPtr };
      return { false, CSVOffsets::Kind::Unknown };
    }

    {
      bool Num0 = O0->isNumeric();
      if (Num0 or O1->isNumeric()) {
        CSVOffsets::Kind ResKind = Num0 ? O1->getKind() : O0->getKind();
        if (O0->isUnknownInPtr() or O1->isUnknownInPtr())
          return { false, ResKind };
        else
          return { true, ResKind };
      }
    }
    revng_abort();
  }

  CSVOffsets foldOffsets(CSVOffsets::Kind ResultKind,
                         WorkItem::size_type NumSrcs,
                         Instruction *I,
                         const SmallVector<offset_iterator, 4> &OffsetsIt) {
    auto OpCode = I->getOpcode();
    revng_assert(OpCode == Instruction::Add or OpCode == Instruction::Sub);
    SmallVector<Constant *, 4> Operands(NumSrcs, nullptr);
    // Setup operands
    for (WorkItem::size_type SI = 0; SI < NumSrcs; ++SI) {
      const int64_t O = *OffsetsIt[SI];
      Operands[SI] = ConstantInt::get(Int64Ty, APInt(64, O, true));
    }
    // Constant fold the operation with the selected operands
    ArrayRef<Constant *> TmpOp(Operands);
    Constant *Res = ConstantFoldInstOperands(I, TmpOp, DL);
    ConstantInt *R = cast<ConstantInt>(Res);
    const int64_t ResO = R->getSExtValue();
    return CSVOffsets(ResultKind, ResO);
  }
};

/// Specialization of CRTPOffsetFolder for GEPs
class GEPOffsetFolder : public CRTPOffsetFolder<GEPOffsetFolder> {

public:
  GEPOffsetFolder(const Module &M) : CRTPOffsetFolder<GEPOffsetFolder>(M) {}

public:
  friend class CRTPOffsetFolder<GEPOffsetFolder>;

private:
  static std::pair<bool, CSVOffsets::Kind>
  checkOffsetTupleIsValid(const SmallVector<const CSVOffsets *, 4> &OffsetTuple,
                          const Instruction *I,
                          SmallVector<OptCSVOffsets, 4> &UpdatedOffsetTuple) {
    auto OpCode = I->getOpcode();
    revng_assert(OpCode == Instruction::GetElementPtr);
    size_t NOperands = OffsetTuple.size();
    revng_assert(NOperands > 1);
    CSVOffsets::Kind GEPOp0Kind = OffsetTuple[0]->getKind();
    if (CSVOffsets::isUnknownInPtr(GEPOp0Kind)
        or CSVOffsets::isUnknown(GEPOp0Kind))
      return { false, GEPOp0Kind };

    auto *GEP = cast<GetElementPtrInst>(I);
    Type *PointeeTy = GEP->getSourceElementType();

    if (GEP->hasIndices() and PointeeTy->isArrayTy()) {

      auto FirstIdxConst = dyn_cast<ConstantInt>(*GEP->idx_begin());
      bool FirstIdxPropagatedConst = OffsetTuple[1]->size() == 1;
      revng_assert(not(FirstIdxConst != nullptr) or FirstIdxPropagatedConst);

      if (FirstIdxPropagatedConst) {

        bool FirstIdxZero = FirstIdxConst->isZero();
        bool FirstIdxPropagatedZero = *OffsetTuple[1]->begin() == 0;
        revng_assert(not FirstIdxZero or FirstIdxPropagatedZero);

        if (FirstIdxPropagatedZero) {
          SmallVector<uint64_t, 4> ConstIdxList = { 0 };

          auto IdxIt = GEP->idx_begin();
          auto IdxEnd = GEP->idx_end();
          int IdxOpNum = 1;
          std::set<int64_t> LastTypeOffsets = { 0 };

          for (; IdxIt != IdxEnd; ++IdxIt, ++IdxOpNum) {
            const CSVOffsets *IdxCSVOffset = OffsetTuple[IdxOpNum];
            revng_assert(not IdxCSVOffset->isPtr());

            Type *ElementTy = GEP->getIndexedType(PointeeTy, ConstIdxList);

            if (ElementTy->isAggregateType()) {
              if (ElementTy->isArrayTy()) {
                if (IdxCSVOffset->isUnknown()) {
                  CSVOffsets U(CSVOffsets::Kind::Numeric, LastTypeOffsets);
                  UpdatedOffsetTuple[IdxOpNum] = std::move(U);
                } else {
                  // If it's not Unknown we can leave it like it is.
                }

                auto *ArrayTy = cast<ArrayType>(ElementTy);
                uint64_t ArrayNumElem = ArrayTy->getNumElements();
                revng_assert(ArrayNumElem);
                LastTypeOffsets.clear();
                for (uint64_t O = 0; O < ArrayNumElem; ++O)
                  LastTypeOffsets.insert(O);

                ConstIdxList.push_back(0);
              } else if (ElementTy->isStructTy()) {
                if (IdxCSVOffset->isUnknown()) {
                  if (IdxIt + 1 != IdxEnd) {
                    // I cannot fold structs with unknown index.
                    // Early exit.
                    return { false, CSVOffsets::makeUnknown(GEPOp0Kind) };
                  } else {
                    revng_assert(not LastTypeOffsets.empty());
                    CSVOffsets U(CSVOffsets::Kind::Numeric, LastTypeOffsets);
                    UpdatedOffsetTuple[IdxOpNum] = std::move(U);
                    break;
                  }
                } else {
                  // If it's not Unknown we can leave it like it is.
                  revng_assert(not IdxCSVOffset->empty());
                  ConstIdxList.push_back(*IdxCSVOffset->begin());

                  revng_assert(IdxCSVOffset->size() != 0);
                  LastTypeOffsets.clear();
                  LastTypeOffsets.insert(IdxCSVOffset->begin(),
                                         IdxCSVOffset->end());
                }
              } else {
                revng_abort();
              }
            } else {
              // I'm done.
              revng_assert(IdxIt + 1 == IdxEnd);
              if (IdxCSVOffset->isUnknown()) {
                CSVOffsets U(CSVOffsets::Kind::Numeric, LastTypeOffsets);
                UpdatedOffsetTuple[IdxOpNum] = std::move(U);
              } else {
                // If it's not Unknown we can leave it like it is.
              }
            }
          }
          return { true, GEPOp0Kind };
        } else {
          // For now we don't handle cases when the first index of the GEP is
          // not zero, so in those case we fall back outside the if and we fold
          // them as usual.
        }
      } else {
        // For now we don't handle cases when the first index of the GEP is
        // not constant, so in those case we fall back outside the if and we
        // fold them as usual.
      }
    } else {
      // For now we don't handle cases when the GEP does not index an array, so
      // in those case we fall back outside the if and we fold them as usual.
    }

    for (size_t O = 1; O < NOperands; O++) {
      revng_assert(not OffsetTuple[O]->isPtr());
      if (OffsetTuple[O]->isUnknown())
        return { false, CSVOffsets::makeUnknown(GEPOp0Kind) };
    }
    return { true, GEPOp0Kind };
  }

  CSVOffsets foldOffsets(CSVOffsets::Kind ResultKind,
                         WorkItem::size_type NumSrcs,
                         Instruction *I,
                         const SmallVector<offset_iterator, 4> &OffsetsIt) {
    const auto *GEP = cast<const GetElementPtrInst>(I);
    const auto PtrOpTy = GEP->getPointerOperand()->getType();
    SmallVector<Constant *, 4> Operands(NumSrcs, nullptr);
    // Setup operands
    int64_t PtrOp = *OffsetsIt[0];
    Operands[0] = Constant::getIntegerValue(PtrOpTy, APInt(64, PtrOp, true));
    for (WorkItem::size_type SI = 1; SI < NumSrcs; ++SI) {
      const int64_t O = *OffsetsIt[SI];
      Operands[SI] = ConstantInt::get(Int32Ty, APInt(32, O, true));
    }
    // Constant fold the operation with the selected operands
    ArrayRef<Constant *> TmpOp(Operands);
    Constant *Res = ConstantFoldInstOperands(I, TmpOp, DL);
    ConstantInt *R = getConstValue(Res, DL);
    const int64_t ResO = getSExtValue(R, DL);
    return CSVOffsets(ResultKind, ResO);
  }
};

/// Specialization of CRTPOffsetFolder for non-address binary operations
class NumericOffsetFolder : public CRTPOffsetFolder<NumericOffsetFolder> {

public:
  NumericOffsetFolder(const Module &M) :
    CRTPOffsetFolder<NumericOffsetFolder>(M) {}

public:
  friend class CRTPOffsetFolder<NumericOffsetFolder>;

private:
  static std::pair<bool, CSVOffsets::Kind>
  checkOffsetTupleIsValid(const SmallVector<const CSVOffsets *, 4> &OffsetTuple,
                          const Instruction *I,
                          SmallVector<OptCSVOffsets, 4> &) {
    revng_assert(OffsetTuple.size() == 2);

    auto OpCode = I->getOpcode();
    revng_assert(OpCode == Instruction::Shl or OpCode == Instruction::AShr
                 or OpCode == Instruction::LShr or OpCode == Instruction::Mul
                 or OpCode == Instruction::URem or OpCode == Instruction::SRem
                 or OpCode == Instruction::SDiv or OpCode == Instruction::UDiv);

    const auto O0 = OffsetTuple[0], O1 = OffsetTuple[1];
    revng_assert(not O0->isPtr() and not O1->isPtr());
    if (O0->isUnknown() or O1->isUnknown()) {
      return std::make_pair(false, CSVOffsets::Kind::Unknown);
    } else {
      return std::make_pair(true, CSVOffsets::Kind::Numeric);
    }
  }

  CSVOffsets foldOffsets(CSVOffsets::Kind ResultKind,
                         WorkItem::size_type NumSrcs,
                         Instruction *I,
                         const SmallVector<offset_iterator, 4> &OffsetsIt) {

    auto OpCode = I->getOpcode();
    revng_assert(OpCode == Instruction::Shl or OpCode == Instruction::AShr
                 or OpCode == Instruction::LShr or OpCode == Instruction::Mul
                 or OpCode == Instruction::URem or OpCode == Instruction::SRem
                 or OpCode == Instruction::SDiv or OpCode == Instruction::UDiv);

    SmallVector<Constant *, 4> Operands(NumSrcs, nullptr);
    // Setup operands
    for (WorkItem::size_type SI = 0; SI < NumSrcs; ++SI) {
      const int64_t O = *OffsetsIt[SI];
      Operands[SI] = ConstantInt::get(Int64Ty, APInt(64, O, true));
    }
    // Constant fold the operation with the selected operands
    ArrayRef<Constant *> TmpOp(Operands);
    Constant *Res = ConstantFoldInstOperands(I, TmpOp, DL);
    ConstantInt *R = cast<ConstantInt>(Res);
    const int64_t ResO = R->getSExtValue();
    return CSVOffsets(ResultKind, ResO);
  }
};

using AccessOffsetMap = CPUStateAccessAnalysisPass::AccessOffsetMap;

class CPUStateAccessOffsetAnalysis {

private:
  const bool Lazy;
  const unsigned LoadMDKind;
  const unsigned StoreMDKind;
  const Module &M;
  const Value *CPUStatePtr = nullptr;
  const Function *RootFunction = nullptr;
  const ConstFunctionPtrSet &ReachableFunctions;
  const TaintResults &TaintedAccesses;
  VariableManager *Variables = nullptr;

  AccessOffsetMap &LoadOffsets; // result, maps load or load-memcpy to offsets
  AccessOffsetMap &StoreOffsets; // result, maps store or store-memcpy to
                                 // offsets
  CallSiteOffsetMap &CallSiteLoadOffsets; // result, maps call in root to load
                                          // offsets
  CallSiteOffsetMap &CallSiteStoreOffsets; // result, maps call in root to store
                                           // offsets

  // ValueCallSiteOffsets is used to keep track of the offsets associated with
  // each value. The primary key is the `Value` for which we're tracking the
  // offsets. The secondary key is a `CallInst` representing a call site in
  // root. This call site represents the call from which the mapped offsets are
  // possible. The mapped value is a `CSVOffsets`.
  ValueCallSiteOffsetMap ValueCallSiteOffsets;

  // The two following maps have the same structure as ValueCallSiteOffsets, but
  // they are use to hold results on the specific loads and stores that access
  // CSV. They are used after the computation to generate the metadata to attach
  // to the root call sites and the final results of the analysis.
  ValueCallSiteOffsetMap LoadCallSiteOffsets;
  ValueCallSiteOffsetMap StoreCallSiteOffsets;

  CallPtrSet CrossedCallSites;
  using WorkListVector = std::vector<WorkItem>;
  WorkListVector WorkList;
  ConstValuePtrSet InExploration;

  // Helper folders
  AddSubOffsetFolder AddSubFolder;
  NumericOffsetFolder NumericFolder;
  GEPOffsetFolder GEPFolder;

  const Function *CpuLoop = nullptr;

public:
  CPUStateAccessOffsetAnalysis(const Module &Mod,
                               const Value *EnvPtr,
                               const Function *Root,
                               const ConstFunctionPtrSet &Reachable,
                               const TaintResults &Tainted,
                               const bool IsLazy,
                               const unsigned LoadKind,
                               const unsigned StoreKind,
                               VariableManager *Vars,
                               AccessOffsetMap &LoadOff,
                               AccessOffsetMap &StoreOff,
                               CallSiteOffsetMap &CallSiteLoadOff,
                               CallSiteOffsetMap &CallSiteStoreOff) :
    Lazy(IsLazy),
    LoadMDKind(LoadKind),
    StoreMDKind(StoreKind),
    M(Mod),
    CPUStatePtr(EnvPtr),
    RootFunction(Root),
    ReachableFunctions(Reachable),
    TaintedAccesses(Tainted),
    Variables(Vars),
    LoadOffsets(LoadOff),
    StoreOffsets(StoreOff),
    CallSiteLoadOffsets(CallSiteLoadOff),
    CallSiteStoreOffsets(CallSiteStoreOff),
    ValueCallSiteOffsets(),
    LoadCallSiteOffsets(),
    StoreCallSiteOffsets(),
    CrossedCallSites(),
    WorkList(),
    InExploration(),
    AddSubFolder(M),
    NumericFolder(M),
    GEPFolder(M),
    CpuLoop(M.getFunction("cpu_loop")) {}

public:
  bool run();

private:
  void cleanup() {
    ValueCallSiteOffsets = {};
    LoadCallSiteOffsets = {};
    StoreCallSiteOffsets = {};
    CrossedCallSites = {};
    WorkList = {};
    InExploration = {};
  }

  /// Analyzes the access to env performed by \p I, saving results according to
  /// \p IsLoad
  ///
  /// \param I is the `Instruction` whose accesses are analyzed
  /// \param IsLoad must be true if called when analyzing loads, false if called
  ///        when analyzing stores. This is important because it is used to
  ///        update the correct ValueCallSiteOffsetMap (either
  ///        LoadCallSiteOffsets or StoreCallSiteOffsets) if during
  ///        the exploration the analysis ends because all the immediate sources
  ///        are already resolved.
  void analyzeAccess(Instruction *I, bool IsLoad);

  /// Explores the sources of `V` and pushes a `WorkItem` on `WorkList`
  /// if something new is found
  ///
  /// \param V is the `Value` whose sources are analyzed
  /// \param IsLoad must be true if called when propagating loads, false if
  /// called when propagating stores. This is important because it is used to
  /// update the correct ValueCallSiteOffsetMap (either LoadCallSiteOffsets or
  /// StoreCallSiteOffsets) if during the exploration the analysis ends because
  /// all the immediate sources are already resolved.
  bool exploreImmediateSources(Value *V, bool IsLoad);

  /// Returns an emptyoOptional and fill W if there are unexplored sources,
  /// otherwise return the offsets
  ///
  /// \param V the `Value` whose sources must be explored.
  /// \param [out] W a `WorkItem` that will be initialized with the unexplored
  ///                sources of `V` if any.
  /// \param IsLoad true if we're exploring from a load
  OptCSVOffsets
  getOffsetsOrExploreSrc(Value *V, WorkItem &W, bool IsLoad) const;

  void insertCallSiteOffset(Value *V, CSVOffsets &&Offset);

  /// Removes the root call site associated with `Item` (if any) from
  /// `CrossedCallSites`
  ///
  /// \return `true` if it was removed, `false` otherwise
  bool tryRemoveCurCrossedCallSite(const WorkItem &Item) {
    if (CallInst *RootCallSite = getCurSourceRootCall(Item, RootFunction))
      return CrossedCallSites.erase(RootCallSite);
    return false;
  }

  /// Returns true if `V` is visited for the first time with the callsite
  /// `NewCallSite`
  ///
  /// \param V is the `Value` that is being visited
  /// \param NewCallSite is the new call site from which we're exploring V and
  ///        we want to check if it's the first time we visit V with that
  ///        particular call site
  ///
  /// This function returns `true` if this is the first visit, `false` otherwise
  bool isNewVisitWithCallSite(Value *V, CallInst *NewCallSite) const {

    if (CSVAccessLog.isEnabled()) {
      if (NewCallSite != nullptr) {
        llvm::StringRef NameRef = NewCallSite->getFunction()->getName();
        revng_log(CSVAccessLog, "caller: " << NameRef);
      } else {
        revng_log(CSVAccessLog, "caller: nullptr");
      }
    }

    // Handle constants in a special way. Constants are kind of global values
    // that can be used across different functions without properly propagating
    // on the call graph across call sites.
    // This has two consequences.
    // 1) On the one hand we don't want to track their exact call site, because
    // they can propagate independently of the call sites. Hence all constants
    // are collected with an 'artificial' nullptr callsite (see the else).
    // 2) On the other hand, when we cross root call sites and we find that one
    // of the arguments is a constant, we want to mark it every time as a new
    // visit. If we don't mark it as a new visit, the root call site will not be
    // crossed, hence it will not be inserted in the `CrossedCallSites`, which
    // is not what we want because it would lead to errors in computing the root
    // call sites that are active for a given exploration. For this reason, if V
    // is a constant and (NewCallSite != nullptr) we always say it's a new
    // visit, because it means we're crossing a root call site towards a
    // constant argument.
    if (isa<ConstantInt>(V)) {
      if (NewCallSite != nullptr)
        return true;
      else
        NewCallSite = nullptr;
    }

    const auto CallSiteOffsetIt = ValueCallSiteOffsets.find(V);
    // If the ValueCallSiteOffsets does not contain V it's a new visit
    if (CallSiteOffsetIt == ValueCallSiteOffsets.end())
      return true;

    // If the ValueCallSiteOffsets contains V we have already analyzed visited
    // this value, but we don't know which call sites were contained in
    // CrossedCallSites during the last visit.
    const CallSiteOffsetMap &ACSOMap = CallSiteOffsetIt->second;

    // If NewCallSite is not nullptr, we are crossing a new callsite in the root
    // function, so we start looking in the ValueCallSiteOffsets for an entry
    // associated to NewCallSite
    if (NewCallSite) {
      // If we find that ValueCallSiteOffsets still does not contain an entry
      // associated to NewCallSite this visit is considered new
      if (!ACSOMap.contains(NewCallSite))
        return true;
    }

    // If CrossedCallSites is not empty, we have crossed at least one call site
    // in the root function, so we need to look in CrossedCallSites if there is
    // a new call site to analyze
    if (not CrossedCallSites.empty()) {
      for (CallInst *Call : CrossedCallSites) {
        // If we find a call site in CrossedCallSites for which the
        // ValueCallSiteOffsets
        // still does not contain a result for this value this visit is
        // considered new
        if (!ACSOMap.contains(Call))
          return true;
      }
    } else {
      // If CrossedCallSites is empty, we haven't crossed any call site in the
      // root function, so we look for nullptr.
      if (!ACSOMap.contains(nullptr))
        return true;
    }

    // If we reach this point the ValueCallSiteOffsets already contains an entry
    // for V, and all the current CrossedCallSites have already been computed
    // for that entry, so the visit is not new
    return false;
  }

  /// If it's a new visit, insert the call `RootCall` (associated with `U`)
  /// in `CrossedCallSites`
  ///
  /// \param RootCall must be nullptr or a valid call instruction in root
  /// \param U if RootCall is not `nullptr` this is a `Use` whose `User` must be
  ///        RootCall
  /// \return `true` if it was a new visit, `false` otherwise.
  ///
  /// If this is the first visit, `RootCall` is inserted in `CrossedCallSites`
  /// and the function returns `true`.
  /// If this is not the first visit or `RootCall` it returns `false`
  bool checkNewVisitAndInsertCrossedCallSite(CallInst *RootCall, const Use *U) {
    revng_log(CSVAccessLog, "isNewVisitWithCallSite?");
    if (isNewVisitWithCallSite(U->get(), RootCall)) {
      if (RootCall) {
        revng_log(CSVAccessLog, "is RootCall");
        bool New = CrossedCallSites.insert(RootCall).second;
        revng_assert(New);
      }
      revng_log(CSVAccessLog, "isNewVisitWithCallSite true");
      return true;
    }
    revng_log(CSVAccessLog, "isNewVisitWithCallSite false");
    return false;
  }

  bool checkNewVisitAndInsertCurCrossedCallSite(const WorkItem &Item) {
    CallInst *RootCallSite = getCurSourceRootCall(Item, RootFunction);
    return checkNewVisitAndInsertCrossedCallSite(RootCallSite,
                                                 Item.currentSourceUse());
  }

  bool checkNewVisitAndInsertNextCrossedCallSite(const WorkItem &Item) {
    CallInst *RootCallSite = getNextSourceRootCall(Item, RootFunction);
    return checkNewVisitAndInsertCrossedCallSite(RootCallSite,
                                                 Item.nextSourceUse());
  }

  bool tryPush(WorkItem &&N) {
    // If we're visiting the sources of an argument we are crossing a
    // new call site, which might lead us into the root function.
    // If it does, we want to register it in the CrossedCallSites
    if (checkNewVisitAndInsertCurCrossedCallSite(N)) {
      revng_log(CSVAccessLog, "Found!");
      push(std::move(N));
      return true;
    }
    revng_log(CSVAccessLog, "NOT Found!");
    return false;
  }

  /// Selects the next source of `Item`, if possible, returning true on success.
  bool selectNextSource(WorkItem &Item) {
    if (Item.nextSourceUse()) {
      tryRemoveCurCrossedCallSite(Item);
      checkNewVisitAndInsertNextCrossedCallSite(Item);
      auto NumSrcs = Item.getNumSources();
      auto NextSrcId = Item.getSourceIndex() + 1;
      revng_assert(NextSrcId < NumSrcs);
      Item.setSourceIndex(NextSrcId);
      return true;
    }
    return false;
  }

  void push(WorkItem &&Item) {
    InExploration.insert(Item.val());
    WorkList.push_back(Item);
    CSVAccessLog.indent(2);
  }

  void pop() {
    InExploration.erase(WorkList.back().val());
    WorkList.pop_back();
    CSVAccessLog.unindent(2);
  }

  bool isInExploration(const Value *V) const {
    return InExploration.contains(V);
  }

  void computeOffsetsFromSources(const WorkItem &Item, bool IsLoad);

  template<bool IsLoad>
  void computeAggregatedOffsets();
};

using CPUSAOA = CPUStateAccessOffsetAnalysis;

void CPUSAOA::computeOffsetsFromSources(const WorkItem &Item, bool IsLoad) {
  Value *ItemVal = Item.val();
  if (isa<PHINode>(ItemVal) or isa<Argument>(ItemVal)
      or isa<CallInst>(ItemVal)) {

    // These three cases represent points of convergence of information coming
    // from different origins.
    //
    // For `PHINode` the information comes from the different branches of the
    // phi. As an example the 'then' branch of an 'if' could compute an offsets,
    // whereas the 'else' branch could compute a different offset.
    //
    // For `Argument` the different sources of information are all the actual
    // arguments of all the calls to the function that are associated to the
    // formal `Argument` that we're analyzing. An example is that if the
    // function `int a(int b, int c)` if called in two places, such as `f(1,2)`
    // and `f(3,4)`, we have that 1 and 3 are the sources of the argument `b`.
    //
    // For `CallInst` we are considering the propagation of values from the
    // `return` instructions inside the callee to the call site.
    // For example if we have:
    // ```
    // int a(int b) {
    //   if (b) {
    //     int ret1 = b >> 2;
    //     return ret1;
    //   } else {
    //     int ret2 = b << 2;
    //     return ret2;
    //   }
    // }
    // ```
    // and `a` is called such as:
    // ```
    // y = f(x);
    // ...
    // ```
    // the sources of the `CallInst` are `ret1` and `ret2`.

    revng_log(CSVAccessLog, "POP JOIN");

    SmallVector<const CallSiteOffsetMap *, 10> SrcCallSiteOffsets;
    SrcCallSiteOffsets.reserve(Item.getNumSources());
    std::map<CallInst *, std::set<WorkItem::size_type>> CallSiteSrcIds;

    // This loop fills `SrcCallSiteOffsets` so that its n-th element will point
    // to the `CallSiteOffsetMap` associated with the n-th source of the Value
    // that we're considering.
    WorkItem::size_type SI = 0;
    for (const Use *Src : Item.sources()) {
      Value *SrcVal = Src->get();
      revng_log(CSVAccessLog,
                "SrcVal: " << SrcVal << " : " << dumpToString(SrcVal));
      const CallSiteOffsetMap &CallSiteOffset = ValueCallSiteOffsets.at(SrcVal);

      // The `CallSiteOffsetMap` associated with `SrcVal` is pushed back into
      // `SrcCallSiteOffsets`, into position `SI`.
      SrcCallSiteOffsets.push_back(&CallSiteOffset);

      // Then, this loop inserts the source index `SI` into the set of source
      // indices associated with the call site `C.first`
      for (const auto &C : CallSiteOffset)
        CallSiteSrcIds[C.first].insert(SI);
      ++SI;
    }

    // Here `SrcCallSiteOffsets[i]` contains a pointer the the
    // `CallSiteOffsetMap` associated with the source number `i` of the analyzed
    // `Value`.
    // Here, for a given call site `C` in `root` the value
    // `CallSiteSrcIds.at(C)` is the set of source indices for which the value
    // currently analyzed is reached from the call site `C`.

    auto Call = dyn_cast<CallInst>(ItemVal);
    const Function *Callee = Call ? getCallee(Call) : nullptr;
    if (Callee != nullptr and Callee->getIntrinsicID() == Intrinsic::memcpy) {

      // Separately handle Intrinsic::memcpy;
      // Calls to Intrinsic::memcpy are a special case.
      // Given that they don't generate any Value, they cannot be pushed on the
      // worklist as a consequence of the backward exploration towards sources.
      // For this reason, if we reach a point where we're trying to compute the
      // offsets from the sources of a call to Intrinsic::memcpy it must be the
      // memcpy where we started from and we don't really want to propagate from
      // the return to the call site, but we want to compute the result and
      // store it in the proper map.

      revng_log(CSVAccessLog,
                "MAP Intrinsic::memcpy: " << ItemVal << " : "
                                          << dumpToString(ItemVal));

      revng_assert(isa<ConstantInt>(Call->getArgOperand(2)));
      ValueCallSiteOffsetMap &VCSOffsets = IsLoad ? LoadCallSiteOffsets :
                                                    StoreCallSiteOffsets;
      Value *PtrOp = IsLoad ? Call->getArgOperand(1) : Call->getArgOperand(0);
      auto CSOff = std::make_pair(ItemVal, ValueCallSiteOffsets.at(PtrOp));
      bool New = VCSOffsets.insert(CSOff).second;
      revng_assert(New);

    } else {

      // This loop iterates over each call instruction in `CallSiteSrcIds`,
      // and, for all the source indices associated to that call site, it
      // combines all of the `CSVOffsets` to compute the new `CSVOffsets` of the
      // node that we're popping.
      for (const auto &CallSrc : CallSiteSrcIds) {
        std::optional<CSVOffsets> New;
        CallInst *TheCall = CallSrc.first;
        for (const auto I : CallSrc.second) {
          revng_log(CSVAccessLog,
                    "AT: " << TheCall << " : " << dumpToString(TheCall));
          const CSVOffsets &SrcOffset = SrcCallSiteOffsets[I]->at(TheCall);
          if (New) {
            New->combine(SrcOffset);
          } else {
            revng_log(CSVAccessLog, "NEW");
            New = SrcOffset;
          }
          revng_log(CSVAccessLog, "SrcOffsets : " << SrcOffset);
          revng_log(CSVAccessLog, "New Offsets: " << *New);
        }
        revng_log(CSVAccessLog,
                  "MAP JOIN: " << ItemVal << " : " << dumpToString(ItemVal)
                               << "\n    " << *New);

        // Insert the `New` in the `ValueCallSiteOffsets`
        ValueCallSiteOffsets[ItemVal][TheCall] = std::move(*New);
        revng_log(CSVAccessLog,
                  "CallSite: " << TheCall << " : " << dumpToString(TheCall)
                               << "\n    "
                               << ValueCallSiteOffsets.at(ItemVal).at(TheCall));
      }
    }

    tryRemoveCurCrossedCallSite(Item);

  } else if (auto *Instr = dyn_cast<Instruction>(ItemVal)) {

    revng_log(CSVAccessLog, "POP INST");

    const auto OpCode = Instr->getOpcode();
    switch (OpCode) {
    case Instruction::ZExt:
    case Instruction::SExt:
    case Instruction::Trunc:
    case Instruction::PtrToInt:
    case Instruction::IntToPtr:
    case Instruction::BitCast: {
      revng_assert(Item.getNumSources() == Instr->getNumOperands());
      revng_log(CSVAccessLog,
                "MAP CAST: " << ItemVal << " : " << dumpToString(ItemVal));
      Value *Op = Instr->getOperand(0);
      ValueCallSiteOffsets[ItemVal] = ValueCallSiteOffsets.at(Op);
    } break;
    case Instruction::Sub:
    case Instruction::Add: {
      revng_assert(Item.getNumSources() == Instr->getNumOperands());
      revng_log(CSVAccessLog, "Add/Sub");
      AddSubFolder.fold(Item, ValueCallSiteOffsets);
    } break;
    case Instruction::Shl:
    case Instruction::AShr:
    case Instruction::LShr:
    case Instruction::Mul:
    case Instruction::URem:
    case Instruction::SRem:
    case Instruction::SDiv:
    case Instruction::UDiv: {
      revng_assert(Item.getNumSources() == Instr->getNumOperands());
      revng_log(CSVAccessLog, "NumericFold");
      NumericFolder.fold(Item, ValueCallSiteOffsets);
    } break;
    case Instruction::GetElementPtr: {
      revng_assert(Item.getNumSources() == Instr->getNumOperands());
      revng_log(CSVAccessLog, "GEP");
      GEPFolder.fold(Item, ValueCallSiteOffsets);
    } break;
    case Instruction::Load: {
      revng_assert(Item.getNumSources() == Instr->getNumOperands());
      Value *AddressValue = cast<LoadInst>(Instr)->getPointerOperand();
      auto LoadCSOff = std::make_pair(ItemVal,
                                      ValueCallSiteOffsets.at(AddressValue));
      bool New = LoadCallSiteOffsets.insert(LoadCSOff).second;
      if (CSVAccessLog.isEnabled()) {
        revng_log(CSVAccessLog, "Load " << dumpToString(Instr));
        for (const auto &CS2O : LoadCSOff.second) {
          revng_log(CSVAccessLog, "CallSite: ");
          if (CS2O.first)
            revng_log(CSVAccessLog, dumpToString(CS2O.first));
          else
            revng_log(CSVAccessLog, "nullptr");
          revng_log(CSVAccessLog, CS2O.second);
        }
      }
      revng_assert(New);
    } break;
    case Instruction::Store: {
      revng_assert(Item.getNumSources() == 1);
      Value *AddressValue = cast<StoreInst>(Instr)->getPointerOperand();
      auto StoreCSOff = std::make_pair(ItemVal,
                                       ValueCallSiteOffsets.at(AddressValue));
      bool New = StoreCallSiteOffsets.insert(StoreCSOff).second;
      if (CSVAccessLog.isEnabled()) {
        revng_log(CSVAccessLog, "Store  " << dumpToString(Instr));
        for (const auto &CS2O : StoreCSOff.second) {
          revng_log(CSVAccessLog, "CallSite: ");
          if (CS2O.first)
            revng_log(CSVAccessLog, dumpToString(CS2O.first));
          else
            revng_log(CSVAccessLog, "nullptr");
          revng_log(CSVAccessLog, CS2O.second);
        }
      }
      revng_assert(New);
    } break;
    default:
      revng_abort(dumpToString(Instr).data());
    }
  } else {
    revng_abort();
  }
}

void CPUSAOA::insertCallSiteOffset(Value *V, CSVOffsets &&Offset) {
  revng_log(CSVAccessLog, "MAP INSERT: " << V);
  // If CrossedCallSites is empty we haven't reached the root function during
  // the backward exploration, so the only active call site is nullptr.
  // The same holds if we're inserting the offset for a ConstantInt. The problem
  // with constants is that they are not propagated through the call graph, but
  // they are in a global context and they infect all the places where they're
  // used independently of the call sites crossed during the exploration. For
  // this reason, if we accumulate all the call sites for the constants we may
  // end up in situation where on a given constant we have lots of call sites in
  // the map, but all with the same CSVOffsets. This is bad for two reasons:
  // 1) it increases the size of the map for no reason;
  // 2) it potentially propagates wrong call sites where the constants are used.
  // For this reason we used this workaround, to only insert null call sites for
  // constants.
  if (CrossedCallSites.empty() or isa<ConstantInt>(V)) {
    ValueCallSiteOffsets[V][nullptr] = Offset;
    revng_log(CSVAccessLog, "CallSite: nullptr\n    " << Offset);
  } else {
    // In all the other cases use the active set of crossed call sites
    for (const auto &Call : CrossedCallSites) {
      ValueCallSiteOffsets[V][Call] = Offset;
      revng_log(CSVAccessLog, "CallSite: " << Call << "\n    " << Offset);
    }
  }
}

OptCSVOffsets
CPUSAOA::getOffsetsOrExploreSrc(Value *V, WorkItem &Item, bool IsLoad) const {
  if (auto *Call = dyn_cast<CallInst>(V)) {
    revng_log(CSVAccessLog, "CALL: " << dumpToString(Call));
    Item = WorkItem(Call, IsLoad, Lazy, LoadMDKind, StoreMDKind);
  } else if (auto *Arg = dyn_cast<Argument>(V)) {
    revng_log(CSVAccessLog, "ARG: " << dumpToString(Arg));
    Item = WorkItem(Arg, ReachableFunctions, Lazy, LoadMDKind, StoreMDKind);
  } else if (auto *Instr = dyn_cast<Instruction>(V)) {
    revng_log(CSVAccessLog, "INST: " << dumpToString(Instr));
    const auto OpCode = Instr->getOpcode();
    switch (OpCode) {
    case Instruction::Load: {
      revng_log(CSVAccessLog, "LOAD");
      const auto *Load = cast<const LoadInst>(Instr);
      const Value *Ptr = Load->getPointerOperand()->stripPointerCasts();
      if (const auto *CSV = dyn_cast<const GlobalVariable>(Ptr)) {
        revng_log(CSVAccessLog, "GLOBAL");
        if (CSV == CPUStatePtr) {
          revng_log(CSVAccessLog, "ENV");
          return CSVOffsets(CSVOffsets::Kind::KnownInPtr, 0);
        } else {
          revng_log(CSVAccessLog, "NOT-ENV");
          return CSVOffsets(CSVOffsets::Kind::Unknown);
        }
      } else {
        revng_log(CSVAccessLog, "NOT-GLOBAL");
        return CSVOffsets(CSVOffsets::Kind::Unknown);
      }
    }
    case Instruction::Alloca:
      revng_log(CSVAccessLog, "ALLOCA");
      return CSVOffsets(CSVOffsets::Kind::Unknown);
    case Instruction::Or:
    case Instruction::And:
    case Instruction::ICmp:
      revng_log(CSVAccessLog, "CMP");
      return CSVOffsets(CSVOffsets::Kind::Unknown);
    case Instruction::Store:
      revng_abort();
    default:
      break;
    }
    // If we reach this point the CSVOffsets of this instruction are not known
    Item = WorkItem(Instr);
  } else if (const auto *IntConst = dyn_cast<const ConstantInt>(V)) {
    int64_t Offset = IntConst->getSExtValue();
    revng_log(CSVAccessLog, "CONST: " << Offset);
    return CSVOffsets(CSVOffsets::Kind::Numeric, Offset);
  } else {
    revng_abort();
  }
  return OptCSVOffsets();
}

bool CPUSAOA::exploreImmediateSources(Value *V, bool IsLoad) {
  // Try to get new unexplored sources for V.
  WorkItem NewItem;
  {
    auto ConstKnownOffsets = getOffsetsOrExploreSrc(V, NewItem, IsLoad);
    if (ConstKnownOffsets.has_value()) {
      revng_log(CSVAccessLog, "ConstantOffset");

      // If we reach this point, V only has a constant know CSVOffsets and does
      // not really have sources that must be explored. In this case we can just
      // insert the ConstKnownOffsets in the map and we're done.
      insertCallSiteOffset(V, std::move(*ConstKnownOffsets));
      return false;
    }
  }
  revng_log(CSVAccessLog, "New!: " << NewItem);

  Value *NewItemV = NewItem.val();
  if (isInExploration(NewItemV)) {
    revng_log(CSVAccessLog, "IS RECURSIVE");

    const ConstValuePtrSet &Tainted = TaintedAccesses.TaintedValues;

    if (const Argument *Arg = dyn_cast<Argument>(NewItemV)) {
      revng_assert(Tainted.contains(Arg));
      revng_assert(Arg->getArgNo() == 0);
      const Function *Fun = Arg->getParent();
      revng_assert(CpuLoop != nullptr);
      revng_assert(CpuLoop->arg_size());
      revng_assert(Arg->getType() == CpuLoop->arg_begin()->getType());

      auto WLIt = WorkList.cbegin();
      auto WLEnd = WorkList.cend();
      bool FoundRecursion = false;
      for (; WLIt != WLEnd; ++WLIt) {
        if (Arg != WLIt->val())
          continue;

        FoundRecursion = true;
        revng_log(CSVAccessLog, "Close recursion");

        Value *CurSrcVal = WLIt->currentSourceValue();
        revng_assert(CurSrcVal != Arg);
        CSVOffsets NewOffsets = CSVOffsets(CSVOffsets::Kind::KnownInPtr, 0);
        insertCallSiteOffset(CurSrcVal, std::move(NewOffsets));

        Value *NextSrcVal = WLIt->nextSourceValue();
        if (nullptr == NextSrcVal)
          break;

        revng_log(CSVAccessLog,
                  "Has unresolved source: " << dumpToString(NextSrcVal));
        revng_assert(NextSrcVal != Arg);

        WorkItem::size_type NextSrcId = WLIt->getSourceIndex() + 1;
        NewItem.setSourceIndex(NextSrcId);
        if (tryPush(std::move(NewItem)))
          return true;
      }
      revng_assert(FoundRecursion);
    } else {
      revng_assert(!Tainted.contains(NewItemV));
      for (const Use *U : NewItem.sources())
        insertCallSiteOffset(U->get(), CSVOffsets(CSVOffsets::Kind::Unknown));
    }

  } else {

    for (const auto &Group : llvm::enumerate(NewItem.sources())) {
      revng_log(CSVAccessLog, "Src: " << dumpToString(Group.value()->get()));
      revng_log(CSVAccessLog, "SrcId: " << Group.index());
      // Adjust the SourceIndex, to set the correct Source
      auto SrcId = Group.index();
      NewItem.setSourceIndex(SrcId);
      if (tryPush(std::move(NewItem)))
        return true;
    }
  }

  // If we reach this point the current Value V only has sources that were
  // already explored.
  if (NewItem.getNumSources()) {
    revng_log(CSVAccessLog, "DONE");
    computeOffsetsFromSources(NewItem, IsLoad);
  }
  return false;
}

static bool callsBuiltinMemcpy(const Instruction *TheCall) {
  const Function *Callee = getCallee(TheCall);
  return Callee != nullptr and Callee->getIntrinsicID() == Intrinsic::memcpy;
}

void CPUSAOA::analyzeAccess(Instruction *LoadOrStore, bool IsLoad) {

  // This analysis starts from the Instruction LoadOrStore and works in two
  // alternate steps:
  // 1) it iterates backward, looking for all the values that generate their
  // pointer operands;
  // 2) as soon as the backward exploration reaches the leaves (i.e. the initial
  // values that are used as building blocks for the computation) it starts
  // working forward. At each step of the forward propagation it tries to
  // constant-fold the sources of the current value. If all the offsets of all
  // the sources are known they are constant folded and the analysis keeps
  // working forward. Otherwise, if there is a source that it still unexplored
  // (i.e. it has no known offsets), the analysis starts working backward again,
  // until also the unexplored source is resolved and can be constant folded.

  // Initialization
  if (not callsBuiltinMemcpy(LoadOrStore))
    push(WorkItem(LoadOrStore));
  else
    push(WorkItem(cast<CallInst>(LoadOrStore),
                  IsLoad,
                  Lazy,
                  LoadMDKind,
                  StoreMDKind));

  while (not WorkList.empty()) {
    const auto Size = WorkList.size();
    Value *CurSrcVal = WorkList.back().currentSourceValue();
    if (CSVAccessLog.isEnabled()) {
      const auto *CurVal = WorkList.back().val();
      revng_log(CSVAccessLog,
                "Val   : " << CurVal << " : " << dumpToString(CurVal));
      revng_log(CSVAccessLog,
                "Src   : " << CurSrcVal << " : " << dumpToString(CurSrcVal));
    }

    // Explore CurSrcVal's immediate sources (going backward)
    // If we the exploration succeeded we have something new on the WorkList and
    // we want to keep exploring back
    if (exploreImmediateSources(CurSrcVal, IsLoad))
      continue;
    revng_log(CSVAccessLog, "not grown");

    // If we didn't push anything, we are done exploring backward the current
    // source and we want to explore backward the other sources of this value
    if (Size == WorkList.size())
      if (selectNextSource(WorkList.back()))
        continue;

    revng_log(CSVAccessLog, "Done");

    // If we reach this point we have finished exploring all the sources of
    // the item that is currently on top of the WorkList.
    // The backward propagation is complete for now.
    // We want to fold the results of the sources on the Item, store the
    // result in the OffsetMap, and then pop the Item
    do {
      const WorkItem &Item = WorkList.back();
      if (CSVAccessLog.isEnabled()) {
        const auto *Val = Item.val();
        revng_log(CSVAccessLog,
                  "TopItemVal: " << Val << " : " << dumpToString(Val));
        const auto *SrcVal = Item.currentSourceValue();
        revng_log(CSVAccessLog,
                  "CurSrc    : " << SrcVal << " : " << dumpToString(SrcVal));
      }

      // Constant fold the finished value and pop it.
      computeOffsetsFromSources(Item, IsLoad);
      pop();
    } while (not WorkList.empty()
             and WorkList.back().nextSourceValue() == nullptr);

    if (not WorkList.empty())
      selectNextSource(WorkList.back());
  }
}

template<bool IsLoad>
void CPUSAOA::computeAggregatedOffsets() {
  const InstrPtrSet &Tainted = IsLoad ? TaintedAccesses.TaintedLoads :
                                        TaintedAccesses.TaintedStores;
  ValueCallSiteOffsetMap &AccessCSOffsets = IsLoad ? LoadCallSiteOffsets :
                                                     StoreCallSiteOffsets;
  CallSiteOffsetMap &CallSiteOffsets = IsLoad ? CallSiteLoadOffsets :
                                                CallSiteStoreOffsets;
  AccessOffsetMap &AccessOffsets = IsLoad ? LoadOffsets : StoreOffsets;

  for (std::pair<Value *const, CallSiteOffsetMap> &ACSO : AccessCSOffsets) {
    // This is the load/store that actually accesses the CPU State
    Value *I = ACSO.first;

    auto DL = M.getDataLayout();

    bool IsInstr = isa<Instruction>(I);
    bool IsCorrectAccessType = IsLoad ? isa<LoadInst>(I) : isa<StoreInst>(I);
    bool IsCallToBuiltinMemcpy = callsBuiltinMemcpy(dyn_cast<Instruction>(I));
    revng_assert(IsInstr and (IsCorrectAccessType or IsCallToBuiltinMemcpy));
    auto *Instr = dyn_cast<Instruction>(I);
    revng_assert(Tainted.contains(Instr));

    int64_t AccessSize;
    if (IsCallToBuiltinMemcpy) {
      auto Call = cast<CallInst>(I);
      auto SizeParam = cast<ConstantInt>(Call->getArgOperand(2));
      AccessSize = SizeParam->getSExtValue();
    } else if (IsLoad) {
      auto *Load = cast<LoadInst>(Instr);
      AccessSize = DL.getTypeAllocSize(Load->getType());
    } else {
      auto Store = cast<StoreInst>(Instr);
      AccessSize = DL.getTypeAllocSize(Store->getValueOperand()->getType());
    }
    revng_assert(AccessSize != 0);

    CallSiteOffsetMap &CallSiteMap = ACSO.second;
    for (std::pair<CallInst *const, CSVOffsets> &CSO : CallSiteMap) {
      CallInst *const Call = CSO.first;
      CSVOffsets &O = CSO.second;
      revng_assert(O.isPtr());

      bool Inserted;

      // Compute AccessOffsets, i.e. the set of offsets accessed by each access
      {
        AccessOffsetMap::iterator AccessOffsetIt;
        auto Offset = std::make_pair(Instr, O);
        std::tie(AccessOffsetIt, Inserted) = AccessOffsets.insert(Offset);
        if (not Inserted)
          AccessOffsetIt->second.combine(O);
      }

      // Compute CallSiteOffsets, i.e. the set of offsets that might be accessed
      // from a given call in root.
      // This is a little more tricky than computing AccessOffsets, since here,
      // when we collapse on the call site, we lose the information on the
      // specific instruction that caused a given offset to be computed, hence
      // also losing the size of the access. For this reason here we have to
      // take into account the sizes of all the accesses.
      {
        OptCSVOffsets New;
        if (not O.hasOffsetSet()) {
          New = O;
        } else {
          revng_assert(O.size());
          std::set<int64_t> FineGrainedOffsets;
          // Now compute the fine-grained offsets
          for (const int64_t Coarse : O) {
            int64_t Refined = Coarse;
            int64_t End = Coarse + AccessSize;
            while (Refined < End) {
              unsigned InternalOffset = 0;
              GlobalVariable *AccessedVar = nullptr;
              std::tie(AccessedVar,
                       InternalOffset) = Variables->getByEnvOffset(Refined);
              int64_t SizeAtOffset = 0;
              if (AccessedVar != nullptr) {
                Type *AccessedTy = AccessedVar->getValueType();
                SizeAtOffset = DL.getTypeAllocSize(AccessedTy) - InternalOffset;
                revng_assert(SizeAtOffset > 0);
                FineGrainedOffsets.insert(Refined - InternalOffset);
                CSVAccessLog << "Value: " << I << DoLog;
                CSVAccessLog << "Insert Refined: " << Refined << DoLog;
              } else {
                // Skip padding one byte at a time, without adding offsets
                SizeAtOffset = 1;
              }
              revng_assert(SizeAtOffset != 0);
              Refined += SizeAtOffset;
            }
          }
          New = CSVOffsets(O.getKind(), FineGrainedOffsets);
        }
        // Finally insert them or combine them
        CallSiteOffsetMap::iterator CallOffsetIt;
        std::tie(CallOffsetIt,
                 Inserted) = CallSiteOffsets.insert({ Call, *New });
        if (not Inserted)
          CallOffsetIt->second.combine(*New);
      }
    }
  }
}

bool CPUSAOA::run() {

  // Analyze load and store
  for (Instruction *I : TaintedAccesses.TaintedLoads)
    analyzeAccess(I, true);
  for (Instruction *I : TaintedAccesses.TaintedStores)
    analyzeAccess(I, false);

  if (CSVAccessLog.isEnabled()) {
    TaintLog << "== ACCESS ANALYSIS RESULTS ==\n";
    TaintLog << "== Loads ==\n";
    for (Instruction *LoadOrStore : TaintedAccesses.TaintedLoads) {
      TaintLog << "INSTRUCTION: " << LoadOrStore << DoLog;
      TaintLog << dumpToString(LoadOrStore) << DoLog;
      TaintLog.indent(4);
      for (const auto &CSO : LoadCallSiteOffsets.at(LoadOrStore)) {
        TaintLog << "CallSite: " << CSO.first << '\n';
        if (CSO.first != nullptr)
          TaintLog << dumpToString(CSO.first);
        TaintLog << DoLog;
        TaintLog << CSO.second << '\n';
      }
      TaintLog.unindent(4);
      TaintLog << DoLog;
    }
    TaintLog << "== Stores ==\n";
    for (Instruction *LoadOrStore : TaintedAccesses.TaintedStores) {
      TaintLog << "INSTRUCTION: " << LoadOrStore << DoLog;
      TaintLog << dumpToString(LoadOrStore) << DoLog;
      TaintLog.indent(4);
      for (const auto &CSO : StoreCallSiteOffsets.at(LoadOrStore)) {
        TaintLog << "CallSite: " << CSO.first << '\n';
        if (CSO.first != nullptr)
          TaintLog << dumpToString(CSO.first);
        TaintLog << DoLog;
        TaintLog << CSO.second << '\n';
      }
      TaintLog.unindent(4);
      TaintLog << DoLog;
    }
    TaintLog << DoLog;
  }

  // ValueCallSiteOffset is not needed anymore here, because it's only used
  // across different calls to analyzeAccess to optimize the runtime avoiding
  // multiple iterations on the same Values.
  ValueCallSiteOffsets = {};

  // Aggregate the results:
  // - from LoadCallSiteOffsets to CallSiteLoadOffsets and LoadOffsets
  // - from StoreCallSiteOffsets to CallSiteStoreOffsets and StoreOffsets
  computeAggregatedOffsets</* IsLoad = */ true>();
  computeAggregatedOffsets</* IsLoad = */ false>();

  cleanup();

  return not TaintedAccesses.empty();
}

class CPUStateAccessFixer {

private:
  using Pair = AccessOffsetMap::value_type;

private:
  // A reference to the analyzed Module
  const Module &M;

  VariableManager *Variables = nullptr;

  // References to the maps that were filled by CPUStateAccessAnalysis.
  // Every map maps an Instruction to the CSVOffset representing all the
  // possible offsets that are accessed by that Instruction, being it either a
  // load, a store or a call to Intrinsic::memcpy.
  // We hold two separate maps, one for loads and one for stores, so that calls
  // to memcpy can be in both maps, with different associated offsets.
  const AccessOffsetMap &CSVLoadOffsetMap;
  const AccessOffsetMap &CSVStoreOffsetMap;

  std::vector<Instruction *> InstructionsToRemove;

public:
  CPUStateAccessFixer(const Module &Mod,
                      VariableManager *V,
                      const AccessOffsetMap &L,
                      const AccessOffsetMap &S) :
    M(Mod),
    Variables(V),
    CSVLoadOffsetMap(L),
    CSVStoreOffsetMap(S),
    DL(Mod.getDataLayout()),
    EnvStructSize(DL.getTypeAllocSize(V->getCPUStateType())),
    Builder(Mod.getContext()),
    Int64Ty(IntegerType::getInt64Ty(M.getContext())),
    SizeOfEnv(ConstantInt::get(Int64Ty, APInt(64, EnvStructSize, true))),
    Zero(ConstantInt::get(Int64Ty, APInt(64, 0, true))),
    CPUStatePtr(Mod.getGlobalVariable("env")) {}

public:
  bool run();

private:
  template<bool IsLoad>
  std::tuple<Instruction *, Type *, Value *>
  setupOutEnvAccess(Instruction *AccessToFix);

  template<bool IsLoad>
  void correctCPUStateAccesses();

  void setupLoadInEnv(Instruction *LoadToFix,
                      int64_t EnvOffset,
                      SwitchInst *Switch,
                      BasicBlock *NextBB,
                      PHINode *Phi);

  void setupStoreInEnv(Instruction *LoadToFix,
                       int64_t EnvOffset,
                       SwitchInst *Switch,
                       BasicBlock *NextBB);

  template<bool IsLoad>
  void fixAccess(const Pair &IOff);

private:
  const DataLayout &DL;
  int64_t EnvStructSize;
  IRBuilder<> Builder;
  Type *Int64Ty = nullptr;
  Constant *SizeOfEnv = nullptr;
  Constant *Zero = nullptr;
  Value *CPUStatePtr = nullptr;
};

static Value *getLoadAddressValue(Instruction *I) {
  auto OpCode = I->getOpcode();
  Value *Address = nullptr;
  switch (OpCode) {
  case Instruction::Load: {
    Address = cast<LoadInst>(I)->getPointerOperand();
  } break;
  case Instruction::Call: {
    auto *Call = cast<CallInst>(I);
    Function *Callee = getCallee(Call);
    // We only support memcpys where the last parameter is constant
    revng_assert(Callee != nullptr
                 and (Callee->getIntrinsicID() == Intrinsic::memcpy
                      and isa<ConstantInt>(Call->getArgOperand(2))));
    Address = Call->getArgOperand(1);
  } break;
  default:
    revng_abort();
  }
  return Address;
}

static Type *getLoadedType(Instruction *I) {
  if (auto Load = dyn_cast<LoadInst>(I)) {
    return Load->getType();
  }
  return nullptr;
}

static Value *getStoreAddressValue(Instruction *I) {
  auto OpCode = I->getOpcode();
  Value *Address = nullptr;
  switch (OpCode) {
  case Instruction::Store: {
    Address = cast<StoreInst>(I)->getPointerOperand();
  } break;
  case Instruction::Call: {
    auto *Call = cast<CallInst>(I);
    Function *Callee = getCallee(Call);
    // We only support memcpys where the last parameter is constant
    revng_assert(Callee != nullptr
                 and (Callee->getIntrinsicID() == Intrinsic::memcpy
                      and isa<ConstantInt>(Call->getArgOperand(2))));
    Address = Call->getArgOperand(0);
  } break;
  default:
    revng_abort();
  }
  return Address;
}

static Type *getStoredType(Instruction *I) {
  if (auto *Store = dyn_cast<StoreInst>(I)) {
    return Store->getValueOperand()->getType();
  }
  return nullptr;
}

static ConstantInt *getConstantOffset(Type *Int64Ty, int64_t O) {
  auto *EnvOffsetConst = ConstantInt::get(Int64Ty, APInt(64, O, true));
  return cast<ConstantInt>(EnvOffsetConst);
}

template<bool IsLoad>
std::tuple<Instruction *, Type *, Value *>
CPUStateAccessFixer::setupOutEnvAccess(Instruction *AccessToFix) {
  LLVMContext &Context = M.getContext();
  BasicBlock *AccessToFixBB = AccessToFix->getParent();
  Function *F = AccessToFixBB->getParent();
  auto InstrIt = AccessToFix->getIterator();
  revng_assert(InstrIt != AccessToFixBB->end());
  revng_assert(std::next(InstrIt) != AccessToFixBB->end());
  // Create a new block NextBB and move there all the instructions after
  // the access
  BasicBlock *NextBB = AccessToFixBB->splitBasicBlock(InstrIt);
  AccessToFixBB->getTerminator()->eraseFromParent();
  revng_assert(not NextBB->empty());

  // Create a new block OutAccessBB only for accesses outside env, and
  // clone the accessing instruction in there. This clone of the
  // accessing instruction in the OutAccessBB will be leaved untouched
  // by the substitution performed later.
  BasicBlock *OutAccessBB = BasicBlock::Create(Context, "OutAccess", F);
  Builder.SetInsertPoint(OutAccessBB);
  BranchInst *OutToNextBranchInst = Builder.CreateBr(NextBB);
  Instruction *OutAccess = AccessToFix->clone();
  OutAccess->insertBefore(OutToNextBranchInst);

  // Create a new block InAccessBB only for accesses inside env (if any).
  // The accessing instruction is cloned into the new InAccessBB
  // and we insert a branch instruction to the NextBB.
  BasicBlock *InAccessBB = BasicBlock::Create(Context, "InAccess", F);
  Builder.SetInsertPoint(InAccessBB);
  BranchInst *InToNextBranchInst = Builder.CreateBr(NextBB);
  Instruction *InAccessToFix = AccessToFix->clone();
  InAccessToFix->insertBefore(InToNextBranchInst);

  // Create a conditional branch to jump to InAccessBB if the access is
  // going to be in env, or to OutAccessBB if the access is going to be
  // out of env
  Value *Address = IsLoad ? getLoadAddressValue(AccessToFix) :
                            getStoreAddressValue(AccessToFix);

  Builder.SetInsertPoint(AccessToFixBB);
  Value *OffsetValue = Builder.CreatePtrToInt(Address, Int64Ty);
  Value *GEZero = Builder.CreateICmpSGE(OffsetValue, Zero);
  Value *LTSizeOf = Builder.CreateICmpSLT(OffsetValue, SizeOfEnv);
  Value *IsInCSV = Builder.CreateOr(GEZero, LTSizeOf);
  Builder.CreateCondBr(IsInCSV, InAccessBB, OutAccessBB);

  if (IsLoad) {
    Type *LoadedType = getLoadedType(AccessToFix);
    if (LoadedType != nullptr) {
      Builder.SetInsertPoint(&NextBB->front());
      PHINode *PN = Builder.CreatePHI(LoadedType, 2);
      AccessToFix->replaceAllUsesWith(PN);
      PN->addIncoming(OutAccess, OutAccessBB);
      PN->addIncoming(InAccessToFix, InAccessBB);
    }
    return std::make_tuple(InAccessToFix, LoadedType, OffsetValue);
  }
  return std::make_tuple(InAccessToFix, nullptr, OffsetValue);
}

void CPUStateAccessFixer::setupLoadInEnv(Instruction *LoadToFix,
                                         int64_t EnvOffset,
                                         SwitchInst *Switch,
                                         BasicBlock *NextBB,
                                         PHINode *Phi) {
  LLVMContext &Context = M.getContext();
  Function *F = LoadToFix->getFunction();
  auto *OffsetConstInt = getConstantOffset(Int64Ty, EnvOffset);
  BasicBlock *CaseBlock = BasicBlock::Create(Context, "CaseInLoad", F);
  Builder.SetInsertPoint(CaseBlock);
  BranchInst *Break = Builder.CreateBr(NextBB);

  Instruction *Clone = LoadToFix->clone();
  Clone->insertBefore(Break);

  bool Ok = false;
  Builder.SetInsertPoint(Clone);
  switch (LoadToFix->getOpcode()) {

  case Instruction::Load: {
    Type *OriginalLoadedType = Clone->getType();
    unsigned Size = DL.getTypeAllocSize(OriginalLoadedType);
    revng_assert(Size != 0);
    auto *Loaded = Variables->loadFromEnvOffset(Builder, Size, EnvOffset);
    Ok = Loaded != nullptr;
    if (Ok) {
      Type *LoadedType = Loaded->getType();
      if (LoadedType != OriginalLoadedType) {
        unsigned LoadedSize = DL.getTypeAllocSize(LoadedType);
        revng_assert(LoadedSize == Size);
        Loaded = Builder.CreateIntToPtr(Loaded, OriginalLoadedType);
      }
      Switch->addCase(OffsetConstInt, CaseBlock);
      // Add an incoming edge for the PHI after the switch if necessary.
      if (Phi != nullptr)
        Phi->addIncoming(Loaded, CaseBlock);
      Clone->replaceAllUsesWith(Loaded);
      InstructionsToRemove.push_back(Clone);
    } else {
      eraseFromParent(CaseBlock);
      CaseBlock = nullptr; // Prevent this from being used
    }
  } break;

  case Instruction::Call: {
    CallInst *Call = cast<CallInst>(Clone);
    Ok = Variables->memcpyAtEnvOffset(Builder, Call, EnvOffset, true);
    if (Ok) {
      InstructionsToRemove.push_back(Clone);
      Switch->addCase(OffsetConstInt, CaseBlock);
    } else {
      eraseFromParent(CaseBlock);
      CaseBlock = nullptr; // Prevent this from being used
    }
  } break;

  default:
    revng_abort();
  }
}

void CPUStateAccessFixer::setupStoreInEnv(Instruction *StoreToFix,
                                          int64_t EnvOffset,
                                          SwitchInst *Switch,
                                          BasicBlock *NextBB) {
  LLVMContext &Context = M.getContext();
  Function *F = StoreToFix->getFunction();
  auto *OffsetConstInt = getConstantOffset(Int64Ty, EnvOffset);
  BasicBlock *CaseBlock = BasicBlock::Create(Context, "CaseInStore");
  CaseBlock->insertInto(F);
  Builder.SetInsertPoint(CaseBlock);
  BranchInst *Break = Builder.CreateBr(NextBB);

  Instruction *Clone = StoreToFix->clone();
  Clone->insertBefore(Break);

  bool Ok = false;
  Builder.SetInsertPoint(Clone);
  switch (StoreToFix->getOpcode()) {

  case Instruction::Store: {
    auto *Store = cast<StoreInst>(Clone);
    auto *V = Store->getValueOperand();
    unsigned Size = DL.getTypeAllocSize(V->getType());
    revng_assert(Size != 0);
    Ok = Variables->storeToEnvOffset(Builder, Size, EnvOffset, V).has_value();
  } break;

  case Instruction::Call: {
    CallInst *Call = cast<CallInst>(Clone);
    Ok = Variables->memcpyAtEnvOffset(Builder, Call, EnvOffset, false);
  } break;

  default:
    revng_abort();
  }

  if (Ok) {
    InstructionsToRemove.push_back(Clone);
    Switch->addCase(OffsetConstInt, CaseBlock);
  } else {
    eraseFromParent(CaseBlock);
  }
}

template<bool IsLoad>
void CPUStateAccessFixer::correctCPUStateAccesses() {
  auto &CSVAccessOffsetMap = IsLoad ? CSVLoadOffsetMap : CSVStoreOffsetMap;
  auto &OtherCSVAccessOffsetMap = IsLoad ? CSVStoreOffsetMap : CSVLoadOffsetMap;

  if (IsLoad)
    FixAccessLog << "######## Fixing Loads ########" << DoLog;
  else
    FixAccessLog << "######## Fixing Stores ########" << DoLog;

  Type *CharTy = IntegerType::getInt8Ty(M.getContext());
  for (const Pair &IOff : CSVAccessOffsetMap) {
    Instruction *const Instr = IOff.first;
    auto It = OtherCSVAccessOffsetMap.find(Instr);
    if (It != OtherCSVAccessOffsetMap.end()) {
      revng_log(FixAccessLog, "Is memcpy");
      if (IsLoad) {
        revng_log(FixAccessLog, "Must be fixed NOW!");
        // Decompose memcpy from env to env into two separate memcpy, the first
        // to do the load, the second to do the store
        auto *Call = cast<CallInst>(Instr);
        Function *Memcpy = getCallee(Call);
        revng_assert(Memcpy != nullptr
                     and (Memcpy->getIntrinsicID() == Intrinsic::memcpy
                          and isa<ConstantInt>(Call->getArgOperand(2))));

        Value *MemcpySize = Call->getArgOperand(2);
        Value *MemcpySrc = Call->getArgOperand(1);
        Value *MemcpyDst = Call->getArgOperand(0);

        Function *F = Instr->getFunction();
        Builder.SetInsertPoint(&*F->getEntryBlock().begin());
        AllocaInst *TmpBuffer = Builder.CreateAlloca(CharTy, MemcpySize);
        revng_log(FixAccessLog,
                  "Created ALLOCA: " << TmpBuffer << " : "
                                     << dumpToString(TmpBuffer));

        Builder.SetInsertPoint(Instr);
        auto TmpAlign = MaybeAlign(TmpBuffer->getAlign());
        auto AlignOne = MaybeAlign(1);
        CallInst *MemcpyLoad = Builder.CreateMemCpy(TmpBuffer,
                                                    TmpAlign,
                                                    MemcpySrc,
                                                    AlignOne,
                                                    MemcpySize);
        revng_log(FixAccessLog,
                  "Created LOAD: " << MemcpyLoad << " : "
                                   << dumpToString(MemcpyLoad));

        CallInst *MemcpyStore = Builder.CreateMemCpy(MemcpyDst,
                                                     AlignOne,
                                                     TmpBuffer,
                                                     TmpAlign,
                                                     MemcpySize);
        revng_log(FixAccessLog,
                  "Created STORE: " << MemcpyStore << " : "
                                    << dumpToString(MemcpyStore));

        fixAccess</* IsLoad = */ true>({ MemcpyLoad, IOff.second });
        fixAccess</* IsLoad = */ false>({ MemcpyStore, It->second });

        revng_log(FixAccessLog,
                  "Queuing for erasure memcpy: " << Instr << " : "
                                                 << dumpToString(Instr));
        InstructionsToRemove.push_back(Instr);
      } // else do nothing because we fix it only once with loads
      continue;
    }
    fixAccess<IsLoad>(IOff);
  }
}

template<bool IsLoad>
void CPUStateAccessFixer::fixAccess(const Pair &IOff) {
  Instruction *const Instr = IOff.first;
  const CSVOffsets &Offsets = IOff.second;
  CSVOffsets::Kind OKind = Offsets.getKind();
  FixAccessLog << "Fixing access: " << Instr
               << "\nCSVOffsets Kind: " << CSVOffsets::toString(OKind) << DoLog;
  revng_assert(CSVOffsets::isPtr(OKind));
  Function *F = Instr->getFunction();
  Instruction *AccessToFix = Instr;

  switch (OKind) {
  default:
  case CSVOffsets::Kind::Unknown:
  case CSVOffsets::Kind::Numeric:
    revng_abort();
  case CSVOffsets::Kind::OutAndKnownInPtr:
  case CSVOffsets::Kind::UnknownInPtr:
  case CSVOffsets::Kind::OutAndUnknownInPtr:
  case CSVOffsets::Kind::KnownInPtr: {

    revng_log(FixAccessLog, "Before: " << dumpToString(F));

    // This is necessary to get the correct debug info.
    // Setting the insert point to an Instruction also updates the Builder
    // to use its debug info until the insert point is set to a new
    // instruction.
    // Given that in the rest of the code we mostly use
    // SetInsertPoint(BasicBlock *), which does not reset the debug info, we
    // want to do it now, otherwise we might end up using the wrong debug
    // info from an instruction of a previous iteration of this loop.
    Builder.SetInsertPoint(Instr);

    Value *Address = nullptr;
    Type *LoadedType = nullptr; // This is not used if IsLoad is false
    Type *StoredType = nullptr; // This is not used if IsLoad is true
    if (IsLoad) {
      Address = getLoadAddressValue(Instr);
      LoadedType = getLoadedType(Instr);
    } else {
      Address = getStoreAddressValue(Instr);
      StoredType = getStoredType(Instr);
    }

    Value *OffsetValue = nullptr;
    if (CSVOffsets::isInOutPtr(OKind))
      std::tie(AccessToFix,
               LoadedType,
               OffsetValue) = setupOutEnvAccess<IsLoad>(Instr);

    revng_assert(AccessToFix != nullptr);
    LLVMContext &Context = M.getContext();
    QuickMetadata QMD(Context);

    if (IsLoad) {
      if (LoadedType != nullptr and LoadedType->isPointerTy()) {
        FixAccessLog << "REPLACE!" << DoLog;
        Constant *NullPtr = Constant::getNullValue(LoadedType);
        AccessToFix->replaceAllUsesWith(NullPtr);
        break; // out from the big switch to the verify and cleanup code
      }
      // TODO: Handle memcpy, not necessary for now
    } else {
      if (StoredType != nullptr and StoredType->isPointerTy()) {
        FixAccessLog << "REPLACE!" << DoLog;
        break; // out from the big switch to the verify and cleanup code
      }
      // TODO: Handle memcpy, not necessary for now
    }

    // filter out cases where a switch is not necessary
    if (Offsets.size() == 1) {
      int64_t Offset = *Offsets.begin();

      Instruction *Clone = AccessToFix->clone();
      Clone->insertAfter(AccessToFix);
      AccessToFix->replaceAllUsesWith(Clone);

      Builder.SetInsertPoint(Clone);
      bool Ok = false;
      switch (AccessToFix->getOpcode()) {

      case Instruction::Load: {
        if (IsLoad) {
          Type *OriginalLoadedType = Clone->getType();
          unsigned Size = DL.getTypeAllocSize(OriginalLoadedType);
          revng_assert(Size != 0);
          auto *Loaded = Variables->loadFromEnvOffset(Builder, Size, Offset);
          Ok = Loaded != nullptr;
          if (Ok) {
            Type *LoadedType = Loaded->getType();
            if (LoadedType != OriginalLoadedType) {
              unsigned LoadedSize = DL.getTypeAllocSize(LoadedType);
              revng_assert(LoadedSize == Size);
              Loaded = Builder.CreateIntToPtr(Loaded, OriginalLoadedType);
            }
            Clone->replaceAllUsesWith(Loaded);
          }
        } else {
          revng_abort();
        }
      } break;

      case Instruction::Store: {
        if (not IsLoad) {
          auto *Store = cast<StoreInst>(Clone);
          auto *V = Store->getValueOperand();
          unsigned Size = DL.getTypeAllocSize(V->getType());
          revng_assert(Size != 0);
          Ok = Variables->storeToEnvOffset(Builder, Size, Offset, V)
                 .has_value();
        } else {
          revng_abort();
        }
      } break;

      case Instruction::Call: {
        CallInst *Call = cast<CallInst>(Clone);
        Ok = Variables->memcpyAtEnvOffset(Builder, Call, Offset, IsLoad);
      } break;

      default:
        revng_abort();
      }

      if (not Ok) {
        Builder.SetInsertPoint(Clone);
        CallInst *CallAbort = Builder.CreateCall(M.getFunction("abort"));
        auto InvalidMDKind = Context.getMDKindID("revng.csaa.invalid.unique.in."
                                                 "access");
        CallAbort->setMetadata(InvalidMDKind, QMD.tuple((uint32_t) 0));
      } else {
        InstructionsToRemove.push_back(Clone);
      }

      break; // out of the big switch to the verify and cleanup code
    }

    // Create a new block NextBB, after the Switch, and move there all the
    // instructions after the access
    BasicBlock *AccessToFixBB = AccessToFix->getParent();
    auto InstrIt = AccessToFix->getIterator();
    revng_assert(InstrIt != AccessToFixBB->end());
    revng_assert(std::next(InstrIt) != AccessToFixBB->end());
    BasicBlock *NextBB = AccessToFixBB->splitBasicBlock(std::next(InstrIt));
    AccessToFixBB->getTerminator()->eraseFromParent();
    revng_assert(not NextBB->empty());
    revng_assert(std::next(InstrIt) == AccessToFixBB->end());
    // If we're processing loads, add a PHI in NextBB if necessary
    PHINode *Phi = nullptr;
    if (IsLoad) {
      if (LoadedType != nullptr) {
        Builder.SetInsertPoint(&NextBB->front());
        Phi = Builder.CreatePHI(LoadedType, Offsets.size());
        AccessToFix->replaceAllUsesWith(Phi);
      }
    }

    // Create the default BB for the switch, calling revng_abort()
    BasicBlock *Default = BasicBlock::Create(Context, "DefaultInAccess", F);
    Builder.SetInsertPoint(Default);
    CallInst *CallAbort = Builder.CreateCall(M.getFunction("abort"));
    auto UnexpectedInMDKind = Context.getMDKindID("revng.csaa.unexpected.in."
                                                  "access");
    CallAbort->setMetadata(UnexpectedInMDKind, QMD.tuple((uint32_t) 0));
    Builder.CreateUnreachable();

    // Create the offset value to use as a variable for the switch if
    // necessary
    Builder.SetInsertPoint(AccessToFixBB);
    if (OffsetValue == nullptr)
      OffsetValue = Builder.CreatePtrToInt(Address, Int64Ty);
    revng_assert(OffsetValue != nullptr);

    if (CSVOffsets::isUnknownInPtr(OKind)) {

      if (FixAccessLog.isEnabled()) {
        ++NumUnknown;

        std::string Name = F->getName().str();
        FunToNumUnknown[Name]++;
        FunToUnknowns[Name].insert(dumpToString(AccessToFix));
      }

      SwitchInst *SwitchOffset = Builder.CreateSwitch(OffsetValue,
                                                      Default,
                                                      EnvStructSize);
      for (int64_t CurrEnvOff = 0; CurrEnvOff < EnvStructSize; ++CurrEnvOff) {
        if (IsLoad)
          setupLoadInEnv(AccessToFix, CurrEnvOff, SwitchOffset, NextBB, Phi);
        else
          setupStoreInEnv(AccessToFix, CurrEnvOff, SwitchOffset, NextBB);
      }
      revng_assert(SwitchOffset->getNumCases() > 0);
      break; // out from the switch, to the verify and cleanup code
    }

    revng_assert(Offsets.size() > 0);
    SwitchInst *SwitchOffset = Builder.CreateSwitch(OffsetValue,
                                                    Default,
                                                    Offsets.size());
    for (const int64_t CurrEnvOff : Offsets) {
      if (IsLoad)
        setupLoadInEnv(AccessToFix, CurrEnvOff, SwitchOffset, NextBB, Phi);
      else
        setupStoreInEnv(AccessToFix, CurrEnvOff, SwitchOffset, NextBB);
    }

    if (IsLoad) {
      if (Phi != nullptr and Phi->getNumIncomingValues() == 0) {
        Builder.SetInsertPoint(Phi);

        CallInst *CallAbort = Builder.CreateCall(M.getFunction("abort"));
        auto NeverValidInMDKind = Context.getMDKindID("revng.csaa.never.valid."
                                                      "in.load");
        CallAbort->setMetadata(NeverValidInMDKind, QMD.tuple((uint32_t) 0));

        Instruction *DisabledInLoad = AccessToFix->clone();
        DisabledInLoad->insertBefore(Phi);
        Phi->replaceAllUsesWith(DisabledInLoad);
        InstructionsToRemove.push_back(Phi);
      }
    }
  } break;
  }

  // Verify the transformation and cleanup the access to fix
  revng_log(FixAccessLog, "After: " << dumpToString(F));
  if (Instr != AccessToFix) {
    if (FixAccessLog.isEnabled()) {
      FixAccessLog << "Queuing for erasure AccessToFix: " << AccessToFix
                   << DoLog;
      FixAccessLog << dumpToString(AccessToFix) << DoLog;
    }
    InstructionsToRemove.push_back(AccessToFix);
  }
  if (FixAccessLog.isEnabled()) {
    FixAccessLog << "Queuing for erasure Instr: " << Instr << DoLog;
    FixAccessLog << dumpToString(Instr) << DoLog;
  }
  InstructionsToRemove.push_back(Instr);
}

bool CPUStateAccessFixer::run() {
  if (CPUStatePtr == nullptr)
    return false;

  // Fix loads
  correctCPUStateAccesses</* IsLoad = */ true>();
  revng::verify(&M);

  // Fix stores
  correctCPUStateAccesses</* IsLoad = */ false>();
  revng::verify(&M);

  // Remove fixed accesses
  for (Instruction *Instr : InstructionsToRemove)
    eraseFromParent(Instr);
  InstructionsToRemove.clear();

  if (FixAccessLog.isEnabled()) {
    FixAccessLog << "Num Unknowns: " << NumUnknown << DoLog;

    for (const auto &Fun2Num : FunToNumUnknown)
      FixAccessLog << Fun2Num.first << ": " << Fun2Num.second << DoLog;

    for (const auto &Fun2Unknowns : FunToUnknowns)
      for (const auto &U : Fun2Unknowns.second)
        FixAccessLog << Fun2Unknowns.first << ": " << U << DoLog;
  }
  return true;
}

class CPUStateAccessAnalysis {

private:
  const bool Lazy;
  // A reference to the analyzed Module
  const Module &M;

  // A reference to the associated VariableManager
  VariableManager *Variables = nullptr;

  // References to the maps that will be filled by this analysis.
  // Every map maps an Instruction to the CSVOffset representing all the
  // possible offsets that are accessed by that Instruction, being it either a
  // load, a store or a call to Intrinsic::memcpy.
  // We hold two separate maps, one for loads and one for stores, so that calls
  // to memcpy can be in both maps, with different associated offsets.
  AccessOffsetMap CSVLoadOffsetMap;
  AccessOffsetMap CSVStoreOffsetMap;

  const unsigned LoadMDKind;
  const unsigned StoreMDKind;

  // Helpers
  const DataLayout &DL;
  Type *Int64Ty = nullptr;
  Value *CPUStatePtr = nullptr;

public:
  CPUStateAccessAnalysis(const Module &Mod,
                         VariableManager *V,
                         const bool IsLazy) :
    Lazy(IsLazy),
    M(Mod),
    Variables(V),
    CSVLoadOffsetMap(),
    CSVStoreOffsetMap(),
    LoadMDKind(Mod.getContext().getMDKindID("revng.csvaccess.offsets.load")),
    StoreMDKind(Mod.getContext().getMDKindID("revng.csvaccess.offsets.store")),
    DL(Mod.getDataLayout()),
    Int64Ty(llvm::IntegerType::getInt64Ty(Mod.getContext())),
    CPUStatePtr(Mod.getGlobalVariable("env")) {}

public:
  bool run();

private:
  void forceEmptyMetadata(Function *RootFunction) const;
};

static void addAccessMetadata(const CallSiteOffsetMap &OffsetMap,
                              VariableManager *Variables,
                              QuickMetadata &QMD,
                              unsigned MDKind) {
  for (auto &AccessOffsets : OffsetMap) {
    CallInst *const CallSite = AccessOffsets.first;
    if (CallSite == nullptr)
      continue;
    const CSVOffsets &Offsets = AccessOffsets.second;
    revng_assert(Offsets.isPtr());

    ConstantAsMetadata *UnknownAccess = nullptr;
    MDTuple *AccessedVariablesTuple = nullptr;
    SmallVector<Metadata *, 10> OffsetMetadata;
    OffsetMetadata.reserve(Offsets.size());
    std::set<GlobalVariable *> AccessedVars;
    if (Offsets.isUnknownInPtr()) {
      CSVAccessLog << "Unknown access to CSV" << DoLog;
      UnknownAccess = QMD.get((uint32_t) 1);
      AccessedVariablesTuple = QMD.tuple(OffsetMetadata);
    } else {
      UnknownAccess = QMD.get((uint32_t) 0);
      for (const int64_t O : Offsets) {
        CSVAccessLog << "CallSite: " << CallSite << DoLog;
        CSVAccessLog << "Refined: " << O << DoLog;
        GlobalVariable *AccessedVar = Variables->getByEnvOffset(O).first;
        bool NewlyInserted = AccessedVars.insert(AccessedVar).second;
        if (NewlyInserted) {
          OffsetMetadata.push_back(QMD.get(AccessedVar));
        }
        CSVAccessLog << "Accessed Var: " << AccessedVar
                     << " Offset: " << Variables->getByEnvOffset(O).second
                     << DoLog;
      }
      AccessedVariablesTuple = QMD.tuple(OffsetMetadata);
    }
    SmallVector<Metadata *, 2> AccessMetadata = { UnknownAccess,
                                                  AccessedVariablesTuple };
    CallSite->setMetadata(MDKind, QMD.tuple(AccessMetadata));
  }
}

void CPUStateAccessAnalysis::forceEmptyMetadata(Function *RootFunction) const {
  LLVMContext &Context = M.getContext();
  QuickMetadata QMD(Context);
  for (BasicBlock &BB : *RootFunction) {
    for (Instruction &I : BB) {
      if (isCallToHelper(&I)) {
        if (I.getMetadata(LoadMDKind) == nullptr)
          I.setMetadata(LoadMDKind,
                        MDTuple::get(Context,
                                     { QMD.get((uint32_t) 0),
                                       MDTuple::get(Context, {}) }));
        if (I.getMetadata(StoreMDKind) == nullptr)
          I.setMetadata(StoreMDKind,
                        MDTuple::get(Context,
                                     { QMD.get((uint32_t) 0),
                                       MDTuple::get(Context, {}) }));
      }
    }
  }
}

bool CPUStateAccessAnalysis::run() {

  if (CPUStatePtr == nullptr)
    return false;

  // Get the root Function
  Function *RootFunction = M.getFunction("root");
  revng_assert(RootFunction);

  // Preprocessing: detect all the functions that are directly reachable from
  // the RootFunction
  auto ReachedFunctions = computeDirectlyReachableFunctions(RootFunction,
                                                            LoadMDKind,
                                                            StoreMDKind,
                                                            Lazy);
  if (CSVAccessLog.isEnabled()) {
    CSVAccessLog << "====== Reachable Functions ======";
    for (const Function *F : ReachedFunctions)
      CSVAccessLog << "\n" << F;
    CSVAccessLog << DoLog;
  }

  // Start with a forward taint analysis, to detect all the tainted Values,
  // and all the tainted loads and stores.
  CSVAccessLog << "Before Taint Analysis" << DoLog;
  const auto TaintResults = forwardTaintAnalysis(&M,
                                                 CPUStatePtr,
                                                 ReachedFunctions,
                                                 LoadMDKind,
                                                 StoreMDKind,
                                                 Lazy);
  CSVAccessLog << "After Taint Analysis" << DoLog;

  // If there are no tainted loads and stores we don't need to run the CPUSAOA.
  if (TaintResults.TaintedLoads.empty()
      and TaintResults.TaintedStores.empty()) {
    forceEmptyMetadata(RootFunction);
    return TaintResults.IllegalCalls.size();
  }

  if (TaintLog.isEnabled()) {
    revng_log(TaintLog, "==== Tainted Loads ====");
    for (const Instruction *I : TaintResults.TaintedLoads) {
      revng_log(TaintLog, "In Function: " << I->getFunction()->getName());
      revng_log(TaintLog, I << " : " << dumpToString(I));
    }
    revng_log(TaintLog, "==== Tainted Stores ====");
    for (const Instruction *I : TaintResults.TaintedStores) {
      revng_log(TaintLog, "In Function: " << I->getFunction()->getName());
      revng_log(TaintLog, I << " : " << dumpToString(I));
    }
    revng_log(TaintLog, "==== Illegal Calls ====");
    for (const Instruction *I : TaintResults.IllegalCalls) {
      revng_log(TaintLog, "In Function: " << I->getFunction()->getName());
      revng_log(TaintLog, I << " : " << dumpToString(I));
    }
    revng_log(TaintLog, "=======================");
  }

  CallSiteOffsetMap CallSiteLoadOffset;
  CallSiteOffsetMap CallSiteStoreOffset;
  auto AccessOffsetAnalysis = CPUSAOA(M,
                                      CPUStatePtr,
                                      RootFunction,
                                      ReachedFunctions,
                                      TaintResults,
                                      Lazy,
                                      LoadMDKind,
                                      StoreMDKind,
                                      Variables,
                                      CSVLoadOffsetMap,
                                      CSVStoreOffsetMap,
                                      CallSiteLoadOffset,
                                      CallSiteStoreOffset);
  bool Found = AccessOffsetAnalysis.run();

  if (Found) {
    QuickMetadata QMD(M.getContext());
    addAccessMetadata(CallSiteLoadOffset, Variables, QMD, LoadMDKind);
    addAccessMetadata(CallSiteStoreOffset, Variables, QMD, StoreMDKind);
  }

  if (not Lazy) {
    CPUStateAccessFixer CSVAccessFixer(M,
                                       Variables,
                                       CSVLoadOffsetMap,
                                       CSVStoreOffsetMap);
    CSVAccessFixer.run();
  }

  forceEmptyMetadata(RootFunction);
  return Found;
}

bool CPUStateAccessAnalysisPass::runOnModule(Module &Mod) {
  CPUStateAccessAnalysis AccessAnalysis(Mod, Variables, Lazy);
  return AccessAnalysis.run();
}

char CPUStateAccessAnalysisPass::ID = 0;

using RegisterCPUSAAP = RegisterPass<CPUStateAccessAnalysisPass>;
static RegisterCPUSAAP
  X("cpustate-access-analysis", "CPUState Access Analysis Pass", false, false);
