[llvm] [Passes] Add IR tracker for source-indexed pass snapshots (PR #191852)
Juan Manuel Martinez Caamaño via llvm-commits
llvm-commits at lists.llvm.org
Thu May 14 05:53:14 PDT 2026
================
@@ -0,0 +1,1185 @@
+//===- IRTrackerInstrumentation.cpp - IR tracker recorder -----------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/Passes/IRTrackerInstrumentation.h"
+
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/DenseSet.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StableHashing.h"
+#include "llvm/Analysis/LazyCallGraph.h"
+#include "llvm/Analysis/LoopInfo.h"
+#include "llvm/IR/BasicBlock.h"
+#include "llvm/IR/Constants.h"
+#include "llvm/IR/DIBuilder.h"
+#include "llvm/IR/DebugInfoMetadata.h"
+#include "llvm/IR/Function.h"
+#include "llvm/IR/Instruction.h"
+#include "llvm/IR/Instructions.h"
+#include "llvm/IR/Module.h"
+#include "llvm/IR/ModuleSlotTracker.h"
+#include "llvm/IR/PassInstrumentation.h"
+#include "llvm/IR/PassManager.h"
+#include "llvm/IR/PrintPasses.h"
+#include "llvm/IR/StructuralHash.h"
+#include "llvm/Support/CommandLine.h"
+#include "llvm/Support/ErrorHandling.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/Path.h"
+#include "llvm/Support/raw_ostream.h"
+
+using namespace llvm;
+
+//===----------------------------------------------------------------------===//
+// CLI option (the recorder owns its own flag so this TU is self-contained).
+//===----------------------------------------------------------------------===//
+
+static cl::opt<std::string> IRTrackerOutput(
+ "ir-tracker-output",
+ cl::desc("IR tracker: per-pass IR snapshot output path (TSV row format)"),
+ cl::value_desc("file"), cl::init(""), cl::Hidden);
+
+namespace {
+
+//===----------------------------------------------------------------------===//
+// Local copies of small helpers shared with StandardInstrumentations.cpp.
+// Duplicated here so this TU is self-contained.
+//===----------------------------------------------------------------------===//
+
+template <typename IRUnitT> static const IRUnitT *unwrapIR(Any IR) {
+ const IRUnitT **IRPtr = llvm::any_cast<const IRUnitT *>(&IR);
+ return IRPtr ? *IRPtr : nullptr;
+}
+
+static std::string getIRName(Any IR) {
+ if (unwrapIR<Module>(IR))
+ return "[module]";
+ if (const auto *F = unwrapIR<Function>(IR))
+ return F->getName().str();
+ if (const auto *C = unwrapIR<LazyCallGraph::SCC>(IR))
+ return C->getName();
+ if (const auto *L = unwrapIR<Loop>(IR))
+ return "loop %" + L->getName().str() + " in function " +
+ L->getHeader()->getParent()->getName().str();
+ // Unknown IR-unit type. Mirrors the closed-set fallthrough in
+ // shouldPrintIR: degrade gracefully (empty name -> P row carries an
+ // empty ir_unit field) rather than aborting opt. The recorder is not
+ // load-bearing for compilation correctness, so a hard crash here is
+ // the wrong tradeoff if the new pass manager later grows a fifth IR
+ // unit type.
+ return {};
+}
+
+static bool moduleContainsFilterPrintFunc(const Module &M) {
+ return any_of(M.functions(),
+ [](const Function &F) {
+ return isFunctionInPrintList(F.getName());
+ }) ||
+ isFunctionInPrintList("*");
+}
+
+static bool sccContainsFilterPrintFunc(const LazyCallGraph::SCC &C) {
+ return any_of(C,
+ [](const LazyCallGraph::Node &N) {
+ return isFunctionInPrintList(N.getName());
+ }) ||
+ isFunctionInPrintList("*");
+}
+
+static bool shouldPrintIR(Any IR) {
+ if (const auto *M = unwrapIR<Module>(IR))
+ return moduleContainsFilterPrintFunc(*M);
+ if (const auto *F = unwrapIR<Function>(IR))
+ return isFunctionInPrintList(F->getName());
+ if (const auto *C = unwrapIR<LazyCallGraph::SCC>(IR))
+ return sccContainsFilterPrintFunc(*C);
+ if (const auto *L = unwrapIR<Loop>(IR))
+ return isFunctionInPrintList(L->getHeader()->getParent()->getName());
+ return false;
+}
+
+static bool isIgnored(StringRef PassID) {
+ return isSpecialPass(PassID,
+ {"PassManager", "PassAdaptor", "AnalysisManagerProxy",
+ "DevirtSCCRepeatedPass", "ModuleInlinerWrapperPass",
+ "VerifierPass", "PrintModulePass", "PrintMIRPass",
+ "PrintMIRPreparePass"});
+}
+
+//===----------------------------------------------------------------------===//
+// Auto-synthesis of missing DILocations.
+//
+// Purpose: give every instruction a stable identifier that survives
+// pass-driven cloning, moving, and rewriting, so the recorder can
+// associate instructions that originate from the same source point across
+// passes. The recorder uses an instruction's DILocation as that
+// identifier -- when a later pass copies or rewrites an instruction, the
+// resulting instruction's DILocation tells us which original instruction
+// it descends from. Inputs without -g (bitcode, clang JIT, lld embedding,
+// hand-written .ll) have no DILocations on most instructions, so the
+// recorder loses identity across passes. This function fills the gaps
+// with synthesized ordinal IDs on first sight per module.
+//
+// What it adds: for each function that lacks a DISubprogram, one synthetic
+// DISubprogram whose file is "<ir-tracker-synthetic>"; and for each
+// instruction that lacks a DebugLoc, a DILocation(line=N, column=0,
+// scope=that DISubprogram) where N is a module-wide running counter.
+// Real DI is never overwritten -- functions that already have a
+// DISubprogram and instructions that already have a DebugLoc are skipped.
+//
+// Example. Input without debug info:
+//
+// define i32 @add(i32 %a, i32 %b) {
+// %s = add i32 %a, %b
+// ret i32 %s
+// }
+//
+// After synthesizeMissingInstructionLocs:
+//
+// define i32 @add(i32 %a, i32 %b) !dbg !2 {
+// %s = add i32 %a, %b, !dbg !3
+// ret i32 %s, !dbg !4
+// }
+// !1 = !DIFile(filename: "<ir-tracker-synthetic>", directory: ".")
+// !2 = distinct !DISubprogram(name: "add", scope: !0, file: !1, line: 1,
+// ...)
+// !3 = !DILocation(line: 1, column: 0, scope: !2)
+// !4 = !DILocation(line: 2, column: 0, scope: !2)
+//
+// The line numbers are arbitrary running ordinals; they do not refer to
+// anything in the original source. They only need to be stable across
+// passes so the recorder can match a later pass's DILocation against an
+// earlier one and conclude "same logical instruction".
+//
+// No-op when the module already has llvm.dbg.cu (a real -g build), or
+// when the module has no functions.
+//===----------------------------------------------------------------------===//
+
+static void synthesizeMissingInstructionLocs(Module &M) {
+ // Skip if the module already has real debug info; we never want to
+ // override real DI.
+ if (M.getNamedMetadata("llvm.dbg.cu"))
+ return;
+ // Skip empty modules (no functions to attach locs to).
+ if (M.empty())
+ return;
+
+ if (!M.getModuleFlag("Debug Info Version"))
+ M.addModuleFlag(Module::Warning, "Debug Info Version",
+ DEBUG_METADATA_VERSION);
+
+ DIBuilder DIB(M);
+ LLVMContext &Ctx = M.getContext();
+ DIFile *File = DIB.createFile("<ir-tracker-synthetic>", ".");
+ DICompileUnit *CU =
+ DIB.createCompileUnit(dwarf::DW_LANG_C, File, "ir-tracker",
+ /*isOptimized=*/true, "", /*RV=*/0,
+ /*SplitName=*/"", DICompileUnit::FullDebug);
+ auto SPType = DIB.createSubroutineType(DIB.getOrCreateTypeArray({}));
+ unsigned NextOrdinal = 1;
+
+ for (Function &F : M) {
+ if (F.isDeclaration())
+ continue;
+ if (F.getSubprogram())
+ continue;
+ unsigned FuncLine = NextOrdinal;
+ DISubprogram::DISPFlags SPFlags =
+ DISubprogram::SPFlagDefinition | DISubprogram::SPFlagOptimized;
+ if (F.hasPrivateLinkage() || F.hasInternalLinkage())
+ SPFlags |= DISubprogram::SPFlagLocalToUnit;
+ DISubprogram *SP =
+ DIB.createFunction(CU, F.getName(), F.getName(), File, FuncLine, SPType,
+ FuncLine, DINode::FlagZero, SPFlags);
+ F.setSubprogram(SP);
+
+ for (BasicBlock &BB : F) {
+ for (Instruction &I : BB) {
+ if (I.getDebugLoc())
+ continue;
+ I.setDebugLoc(DILocation::get(Ctx, NextOrdinal, 0, SP));
+ ++NextOrdinal;
+ }
+ }
+ }
+ DIB.finalize();
+}
+
+//===----------------------------------------------------------------------===//
+// Recorder implementation.
+//===----------------------------------------------------------------------===//
+
+static std::string getIRTrackerFilePath(const DILocation *Loc) {
+ if (!Loc)
+ return {};
+
+ StringRef Dir = Loc->getDirectory();
+ StringRef File = Loc->getFilename();
+ if (File.empty())
+ return {};
+ if (Dir.empty())
+ return File.str();
+
+ SmallString<256> Path(Dir);
+ sys::path::append(Path, File);
+ return std::string(Path);
+}
+
+static stable_hash hashTrackerIdentity(const DILocation *Loc);
+
+static stable_hash hashValueIdentity(const Value &V) {
+ if (const auto *Arg = dyn_cast<Argument>(&V))
+ return stable_hash_combine(static_cast<stable_hash>(1), Arg->getArgNo());
+ if (const auto *GV = dyn_cast<GlobalValue>(&V))
+ return stable_hash_combine(
+ static_cast<stable_hash>(2),
+ static_cast<stable_hash>(hash_value(GV->getName())));
+ if (const auto *BB = dyn_cast<BasicBlock>(&V)) {
+ if (BB->hasName())
+ return stable_hash_combine(
+ static_cast<stable_hash>(3),
+ static_cast<stable_hash>(hash_value(BB->getName())));
+ return 0;
+ }
+ if (const auto *I = dyn_cast<Instruction>(&V)) {
+ if (const DILocation *Loc =
+ I->getDebugLoc() ? I->getDebugLoc().get() : nullptr)
+ return stable_hash_combine(static_cast<stable_hash>(4),
+ hashTrackerIdentity(Loc));
+ if (I->hasName())
+ return stable_hash_combine(
+ static_cast<stable_hash>(5),
+ static_cast<stable_hash>(hash_value(I->getName())));
+ return 0;
+ }
+ if (V.hasName())
+ return stable_hash_combine(
+ static_cast<stable_hash>(6),
+ static_cast<stable_hash>(hash_value(V.getName())));
+ return 0;
+}
+
+/// Compute a stable structural fingerprint of an instruction.
+///
+/// Used by the per-instruction change-detection step: at each pass, the
+/// recorder rehashes every changed-block instruction and compares against
+/// the previous pass's hash for the same tracker ID. Equal hash → "did
+/// not change" → skip emission.
+///
+/// Captures: opcode, result type, operand count, per-operand type, the
+/// per-operand kind bits (is-Constant, is-Argument), a stable identity for
+/// non-constant operands when one is available (argument number, global name,
+/// basic-block name, instruction source-point identity), the commutative flag,
+/// the CmpInst predicate, and the value of any ConstantInt operand.
+///
+/// The important property is that operand rewrites which change the rendered
+/// instruction text should usually perturb the hash as well:
+///
+/// %a = add i32 %x, %y hash X
+/// %b = add i32 %x, %z hash Y (different source-point identity)
+/// %c = add i32 %x, 1 hash Y (kind bit changed)
+/// %d = add i32 %x, 2 hash Z (ConstantInt value changed)
+/// %e = sub i32 %x, %y hash W (opcode changed)
+/// %f = icmp eq i32 %x, %y hash V
+/// %g = icmp ne i32 %x, %y hash U (predicate changed)
+///
+/// Still missed: zero-loc unnamed instruction operands that have no stable
+/// identity source, ConstantFP value changes, ConstantExpr changes, and
+/// attached metadata changes. Metadata tracking is opt-in via separate flags
+/// (deferred to a follow-up PR).
+static stable_hash hashInstruction(const Instruction &I) {
+ stable_hash H = stable_hash_combine(I.getOpcode(), I.getType()->getTypeID(),
+ I.getNumOperands());
+ for (const Use &U : I.operands()) {
+ Value *V = U.get();
+ H = stable_hash_combine(
+ H,
+ stable_hash_combine(static_cast<stable_hash>(V->getType()->getTypeID()),
+ static_cast<stable_hash>(isa<Constant>(V) ? 1 : 0),
+ static_cast<stable_hash>(isa<Argument>(V) ? 1 : 0),
+ hashValueIdentity(*V)));
+ if (auto *C = dyn_cast<ConstantInt>(V))
+ H = stable_hash_combine(
+ H, static_cast<stable_hash>(hash_value(C->getValue())));
+ }
+ if (I.isCommutative())
+ H = stable_hash_combine(H, 1);
+ if (auto *CI = dyn_cast<CmpInst>(&I))
+ H = stable_hash_combine(H, CI->getPredicate());
+ return H;
+}
+
+/// Compute the hash that identifies "this source point" for tracker-ID
+/// interning.
+///
+/// The recorder assigns one compact integer ID to each unique source
+/// point and references that ID in instruction rows so the source
+/// location appears once (in a metadata row) rather than per
+/// instruction row. Two DILocations should map to the same ID iff
+/// they refer to the same source point.
+///
+/// Combines: source file, line, column, and the declared line of the
+/// enclosing DISubprogram. The file participates in the key because
+/// mixed-file modules can legitimately contain the same (line, col,
+/// scope-line) tuple in multiple translation units; without file
+/// identity those instructions alias to the same tracker ID and share
+/// change-detection state incorrectly. The subprogram's declared line
+/// still disambiguates instructions at the same (line, col) coming
+/// from different functions in the same file:
+///
+/// file.c:1 in function foo (foo is declared at line 1)
+/// hash = combine("file.c", 1, 0, 1)
+/// file.c:1 in function bar (bar is declared at line 3)
+/// hash = combine("file.c", 1, 0, 3) // distinct
+/// other.c:1 in function foo (foo is declared at line 1)
+/// hash = combine("other.c", 1, 0, 1) // distinct
+///
+/// Without the scope-line component, inlined or template-instantiated
+/// instructions at the same (line, col) would alias and be treated as
+/// the same source point. Including the DISubprogram pointer directly
+/// would make the hash run-unstable (pointer addresses are not stable
+/// across LLVM invocations); using the file path plus the subprogram's
+/// declared line gives us the needed disambiguation while keeping the
+/// hash deterministic.
+///
+/// Returns 0 for a null DILocation. The recorder uses 0 as a sentinel
+/// "no real source point" and routes such instructions through a
+/// per-block temp-ID fallback path (synthesized phi nodes, etc.).
+static stable_hash hashTrackerIdentity(const DILocation *Loc) {
+ if (!Loc)
+ return 0;
+ stable_hash FileKey = stable_hash_combine(
+ static_cast<stable_hash>(hash_value(Loc->getDirectory())),
+ static_cast<stable_hash>(hash_value(Loc->getFilename())));
+ unsigned ScopeLine = 0;
+ if (DISubprogram *SP = Loc->getScope()->getSubprogram())
+ ScopeLine = SP->getLine();
+ return stable_hash_combine(FileKey, Loc->getLine(), Loc->getColumn(),
+ ScopeLine);
+}
+
+static void printAPIntValue(raw_ostream &OS, const APInt &V) {
+ SmallString<32> Tmp;
+ if (V.isNegative())
+ V.toStringSigned(Tmp, 10);
+ else
+ V.toStringUnsigned(Tmp, 10);
----------------
jmmartinez wrote:
Isn't the printed value for positive numbers the same signed or unsigned ?
https://github.com/llvm/llvm-project/pull/191852
More information about the llvm-commits
mailing list