[llvm] [MIR] Save internal VirtRegMap state in MIR (PR #197361)
Quentin Colombet via llvm-commits
llvm-commits at lists.llvm.org
Wed May 13 21:34:01 PDT 2026
https://github.com/qcolombet updated https://github.com/llvm/llvm-project/pull/197361
>From ad8352f17b241058debca6d11d2a9fab4698f331 Mon Sep 17 00:00:00 2001
From: Quentin Colombet <quentin.colombet at gmail.com>
Date: Fri, 24 Apr 2026 15:37:47 -0700
Subject: [PATCH] [MIR] Save internal VirtRegMap state in MIR
Adds two optional fields to the per-vreg YAML record so MIR tests can
express VirtRegMap state that previously had no representation:
registers:
- { id: 1, class: vgpr_32, split-from: '%0', assigned-phys: '$vgpr5' }
Testing passes that consume sibling-register information (e.g.
InlineSpiller) requires constructing a VirtRegMap with split
relationships from a MIR test, which implies triggering live-range
splitting at minimum and make reproducers unnecessarily complicated.
So this change introduces a mechanism to serialize/deserialize the state
of the VirtRegMap pass.
Mechanism:
- For serialization:
- MIRPrinter emits the new fields only when the VirtRegMap is available.
- For deserialization:
- MIRParser stashes parsed entries on the generic MachineFunctionInfo
base (PendingVRegMappings), using the existing
parseVirtualRegisterReference / parseRegisterReference helpers.
- VirtRegMap::init() drains the stash via assignVirt2Phys and
setIsSplitFromReg, then clears it.
Validation at parse time:
- 'assigned-phys' must be a physical register.
- 'split-from' must reference a different vreg than 'id'.
- 'split-from' physregs / named-vreg references already rejected
by parseStandaloneVirtualRegister.
I'm not super happy about stashing the VRM info in the MFI, but that's
a small price to pay.
---
llvm/include/llvm/CodeGen/MIRPrinter.h | 11 ++++-
llvm/include/llvm/CodeGen/MIRYamlMapping.h | 18 ++++++-
llvm/include/llvm/CodeGen/MachineFunction.h | 10 ++++
llvm/lib/CodeGen/MIRParser/MIRParser.cpp | 48 +++++++++++++++++++
llvm/lib/CodeGen/MIRPrinter.cpp | 33 +++++++++----
llvm/lib/CodeGen/MIRPrintingPass.cpp | 11 ++++-
llvm/lib/CodeGen/VirtRegMap.cpp | 15 ++++++
.../MIR/AMDGPU/virtregmap-stash-bad-phys.mir | 18 +++++++
.../MIR/AMDGPU/virtregmap-stash-bad-split.mir | 16 +++++++
.../AMDGPU/virtregmap-stash-self-split.mir | 17 +++++++
.../CodeGen/MIR/AMDGPU/virtregmap-stash.mir | 32 +++++++++++++
11 files changed, 216 insertions(+), 13 deletions(-)
create mode 100644 llvm/test/CodeGen/MIR/AMDGPU/virtregmap-stash-bad-phys.mir
create mode 100644 llvm/test/CodeGen/MIR/AMDGPU/virtregmap-stash-bad-split.mir
create mode 100644 llvm/test/CodeGen/MIR/AMDGPU/virtregmap-stash-self-split.mir
create mode 100644 llvm/test/CodeGen/MIR/AMDGPU/virtregmap-stash.mir
diff --git a/llvm/include/llvm/CodeGen/MIRPrinter.h b/llvm/include/llvm/CodeGen/MIRPrinter.h
index 829d16823d721..7956193c556a4 100644
--- a/llvm/include/llvm/CodeGen/MIRPrinter.h
+++ b/llvm/include/llvm/CodeGen/MIRPrinter.h
@@ -24,6 +24,7 @@ class MachineBasicBlock;
class MachineFunction;
class MachineModuleInfo;
class Module;
+class VirtRegMap;
template <typename T> class SmallVectorImpl;
class PrintMIRPreparePass : public RequiredPassInfoMixin<PrintMIRPreparePass> {
@@ -47,12 +48,18 @@ class PrintMIRPass : public RequiredPassInfoMixin<PrintMIRPass> {
LLVM_ABI void printMIR(raw_ostream &OS, const Module &M);
/// Print MIR using Legacy Pass Manager (uses MachineModuleInfo).
+/// If \p VRM is non-null and -mir-emit-vrm is enabled, the printer will
+/// serialize VirtRegMap state (split-from, assigned-phys).
LLVM_ABI void printMIR(raw_ostream &OS, const MachineModuleInfo &MMI,
- const MachineFunction &MF);
+ const MachineFunction &MF,
+ const VirtRegMap *VRM = nullptr);
/// Print MIR using New Pass Manager (uses FunctionAnalysisManager).
+/// If \p VRM is non-null and -mir-emit-vrm is enabled, the printer will
+/// serialize VirtRegMap state (split-from, assigned-phys).
LLVM_ABI void printMIR(raw_ostream &OS, FunctionAnalysisManager &FAM,
- const MachineFunction &MF);
+ const MachineFunction &MF,
+ const VirtRegMap *VRM = nullptr);
/// Determine a possible list of successors of a basic block based on the
/// basic block machine operand being used inside the block. This should give
diff --git a/llvm/include/llvm/CodeGen/MIRYamlMapping.h b/llvm/include/llvm/CodeGen/MIRYamlMapping.h
index 295ee296f78ab..de495f1d6ef82 100644
--- a/llvm/include/llvm/CodeGen/MIRYamlMapping.h
+++ b/llvm/include/llvm/CodeGen/MIRYamlMapping.h
@@ -203,12 +203,20 @@ struct VirtualRegisterDefinition {
StringValue Class;
StringValue PreferredRegister;
std::vector<FlowStringValue> RegisterFlags;
+ // VirtRegMap state.
+ // SplitFrom: id-form virtual register only (e.g. '%0'); physregs and named
+ // vregs are rejected by the parser.
+ // AssignedPhys: physical register only (e.g. '$r5'); virtregs are rejected.
+ StringValue SplitFrom;
+ StringValue AssignedPhys;
// TODO: Serialize the target specific register hints.
bool operator==(const VirtualRegisterDefinition &Other) const {
return ID == Other.ID && Class == Other.Class &&
- PreferredRegister == Other.PreferredRegister;
+ PreferredRegister == Other.PreferredRegister &&
+ SplitFrom == Other.SplitFrom &&
+ AssignedPhys == Other.AssignedPhys;
}
};
@@ -220,6 +228,14 @@ template <> struct MappingTraits<VirtualRegisterDefinition> {
StringValue()); // Don't print out when it's empty.
YamlIO.mapOptional("flags", Reg.RegisterFlags,
std::vector<FlowStringValue>());
+ // MIRPrinter sets WriteDefaultValues=true unless -simplify-mir is passed,
+ // so a plain mapOptional with an empty default would still emit the keys
+ // and change every existing test's output.
+ // Skip the call on output when empty to keep them off entirely.
+ if (!YamlIO.outputting() || !Reg.SplitFrom.Value.empty())
+ YamlIO.mapOptional("split-from", Reg.SplitFrom, StringValue());
+ if (!YamlIO.outputting() || !Reg.AssignedPhys.Value.empty())
+ YamlIO.mapOptional("assigned-phys", Reg.AssignedPhys, StringValue());
}
static const bool flow = true;
diff --git a/llvm/include/llvm/CodeGen/MachineFunction.h b/llvm/include/llvm/CodeGen/MachineFunction.h
index d620ae5137eba..ff6024c1e9b8e 100644
--- a/llvm/include/llvm/CodeGen/MachineFunction.h
+++ b/llvm/include/llvm/CodeGen/MachineFunction.h
@@ -129,6 +129,16 @@ struct LLVM_ABI MachineFunctionInfo {
const {
return nullptr;
}
+
+ // Placeholder for deserialization of the VirtRegMap object.
+ // This gets populuated by the MIRParser when the information is present
+ // and is used in VirtRegMap::init() to initialize its state.
+ struct PendingVRegMapping {
+ Register VReg;
+ Register SplitFrom; // NoReg if absent.
+ MCRegister AssignedPhys; // NoReg if absent.
+ };
+ SmallVector<PendingVRegMapping, 0> PendingVRegMappings;
};
/// Properties which a MachineFunction may have at a given point in time.
diff --git a/llvm/lib/CodeGen/MIRParser/MIRParser.cpp b/llvm/lib/CodeGen/MIRParser/MIRParser.cpp
index 00967e1de43cc..140de1dc7ef56 100644
--- a/llvm/lib/CodeGen/MIRParser/MIRParser.cpp
+++ b/llvm/lib/CodeGen/MIRParser/MIRParser.cpp
@@ -804,6 +804,54 @@ bool MIRParserImpl::parseRegisterInfo(PerFunctionMIParsingState &PFS,
RegInfo.setCalleeSavedRegs(CalleeSavedRegisters);
}
+ // Stash any VirtRegMap state on the generic MachineFunctionInfo base.
+ // VirtRegMap::init() will use that information to get pre-populated
+ // on the first analysis run.
+ for (const auto &VReg : YamlMF.VirtualRegisters) {
+ if (VReg.SplitFrom.Value.empty() && VReg.AssignedPhys.Value.empty())
+ continue;
+
+ auto *MFI = MF.getInfo<MachineFunctionInfo>();
+ if (!MFI)
+ return error(VReg.SplitFrom.Value.empty() ? VReg.AssignedPhys.SourceRange
+ .Start
+ : VReg.SplitFrom.SourceRange
+ .Start,
+ "'split-from' / 'assigned-phys' require the target to "
+ "implement TargetMachine::createMachineFunctionInfo");
+
+ auto It = PFS.VRegInfos.find(VReg.ID.Value);
+ if (It == PFS.VRegInfos.end())
+ continue;
+ Register ChildReg = It->second->VReg;
+
+ MachineFunctionInfo::PendingVRegMapping Pending;
+ Pending.VReg = ChildReg;
+
+ if (!VReg.SplitFrom.Value.empty()) {
+ VRegInfo *Parent = nullptr;
+ if (parseVirtualRegisterReference(PFS, Parent, VReg.SplitFrom.Value,
+ Error))
+ return error(Error, VReg.SplitFrom.SourceRange);
+ if (Parent->VReg == ChildReg)
+ return error(VReg.SplitFrom.SourceRange.Start,
+ Twine("'split-from' references the same vreg as 'id' (%") +
+ Twine(VReg.ID.Value) + ")");
+ Pending.SplitFrom = Parent->VReg;
+ }
+ if (!VReg.AssignedPhys.Value.empty()) {
+ Register Phys;
+ if (parseRegisterReference(PFS, Phys, VReg.AssignedPhys.Value, Error))
+ return error(Error, VReg.AssignedPhys.SourceRange);
+ if (!Phys.isPhysical())
+ return error(VReg.AssignedPhys.SourceRange.Start,
+ Twine("'assigned-phys' must be a physical register, got '") +
+ VReg.AssignedPhys.Value + "'");
+ Pending.AssignedPhys = Phys.asMCReg();
+ }
+ MFI->PendingVRegMappings.push_back(Pending);
+ }
+
return false;
}
diff --git a/llvm/lib/CodeGen/MIRPrinter.cpp b/llvm/lib/CodeGen/MIRPrinter.cpp
index 679304c43af02..f51c0f241a443 100644
--- a/llvm/lib/CodeGen/MIRPrinter.cpp
+++ b/llvm/lib/CodeGen/MIRPrinter.cpp
@@ -37,6 +37,7 @@
#include "llvm/CodeGen/TargetInstrInfo.h"
#include "llvm/CodeGen/TargetRegisterInfo.h"
#include "llvm/CodeGen/TargetSubtargetInfo.h"
+#include "llvm/CodeGen/VirtRegMap.h"
#include "llvm/CodeGenTypes/LowLevelType.h"
#include "llvm/IR/DebugInfoMetadata.h"
#include "llvm/IR/DebugLoc.h"
@@ -143,7 +144,8 @@ static void printMBB(raw_ostream &OS, MFPrintState &State,
const MachineBasicBlock &MBB);
static void convertMRI(yaml::MachineFunction &YamlMF, const MachineFunction &MF,
const MachineRegisterInfo &RegInfo,
- const TargetRegisterInfo *TRI);
+ const TargetRegisterInfo *TRI,
+ const VirtRegMap *VRM);
static void convertMCP(yaml::MachineFunction &MF,
const MachineConstantPool &ConstantPool);
static void convertMJTI(ModuleSlotTracker &MST, yaml::MachineJumpTable &YamlJTI,
@@ -175,7 +177,7 @@ static void convertPrefetchTargets(yaml::MachineFunction &YMF,
const MachineFunction &MF);
static void printMF(raw_ostream &OS, MFGetterFnT Fn,
- const MachineFunction &MF) {
+ const MachineFunction &MF, const VirtRegMap *VRM) {
MFPrintState State(std::move(Fn), MF);
State.RegisterMaskIds = initRegisterMaskIds(MF);
@@ -206,7 +208,8 @@ static void printMF(raw_ostream &OS, MFGetterFnT Fn,
YamlMF.IsSSA = Props.hasIsSSA();
YamlMF.NoVRegs = Props.hasNoVRegs();
- convertMRI(YamlMF, MF, MF.getRegInfo(), MF.getSubtarget().getRegisterInfo());
+ convertMRI(YamlMF, MF, MF.getRegInfo(), MF.getSubtarget().getRegisterInfo(),
+ VRM);
MachineModuleSlotTracker &MST = State.MST;
MST.incorporateFunction(MF.getFunction());
convertMFI(MST, YamlMF.FrameInfo, MF.getFrameInfo(),
@@ -306,7 +309,8 @@ static void printRegFlags(Register Reg,
static void convertMRI(yaml::MachineFunction &YamlMF, const MachineFunction &MF,
const MachineRegisterInfo &RegInfo,
- const TargetRegisterInfo *TRI) {
+ const TargetRegisterInfo *TRI,
+ const VirtRegMap *VRM) {
YamlMF.TracksRegLiveness = RegInfo.tracksLiveness();
// Print the virtual register definitions.
@@ -321,6 +325,17 @@ static void convertMRI(yaml::MachineFunction &YamlMF, const MachineFunction &MF,
if (PreferredReg)
printRegMIR(PreferredReg, VReg.PreferredRegister, TRI);
printRegFlags(Reg, VReg.RegisterFlags, MF, TRI);
+ if (VRM) {
+ Register Orig = VRM->getPreSplitReg(Reg);
+ if (Orig && Orig != Reg) {
+ raw_string_ostream OS(VReg.SplitFrom.Value);
+ OS << printReg(Orig, TRI);
+ }
+ if (VRM->hasPhys(Reg)) {
+ raw_string_ostream OS(VReg.AssignedPhys.Value);
+ OS << printReg(VRM->getPhys(Reg), TRI);
+ }
+ }
YamlMF.VirtualRegisters.push_back(std::move(VReg));
}
@@ -1070,12 +1085,14 @@ void llvm::printMIR(raw_ostream &OS, const Module &M) {
}
void llvm::printMIR(raw_ostream &OS, const MachineModuleInfo &MMI,
- const MachineFunction &MF) {
- printMF(OS, [&](const Function &F) { return MMI.getMachineFunction(F); }, MF);
+ const MachineFunction &MF, const VirtRegMap *VRM) {
+ printMF(
+ OS, [&](const Function &F) { return MMI.getMachineFunction(F); }, MF,
+ VRM);
}
void llvm::printMIR(raw_ostream &OS, FunctionAnalysisManager &FAM,
- const MachineFunction &MF) {
+ const MachineFunction &MF, const VirtRegMap *VRM) {
printMF(
OS,
[&](const Function &F) {
@@ -1083,5 +1100,5 @@ void llvm::printMIR(raw_ostream &OS, FunctionAnalysisManager &FAM,
const_cast<Function &>(F))
.getMF();
},
- MF);
+ MF, VRM);
}
diff --git a/llvm/lib/CodeGen/MIRPrintingPass.cpp b/llvm/lib/CodeGen/MIRPrintingPass.cpp
index a6fdaa6ab9c50..f5e455a520151 100644
--- a/llvm/lib/CodeGen/MIRPrintingPass.cpp
+++ b/llvm/lib/CodeGen/MIRPrintingPass.cpp
@@ -15,6 +15,7 @@
#include "llvm/CodeGen/MachineFunctionPass.h"
#include "llvm/CodeGen/MachineModuleInfo.h"
#include "llvm/CodeGen/Passes.h"
+#include "llvm/CodeGen/VirtRegMap.h"
#include "llvm/IR/Function.h"
#include "llvm/InitializePasses.h"
@@ -30,7 +31,8 @@ PreservedAnalyses PrintMIRPass::run(MachineFunction &MF,
auto &FAM = MFAM.getResult<FunctionAnalysisManagerMachineFunctionProxy>(MF)
.getManager();
- printMIR(OS, FAM, MF);
+ const VirtRegMap *VRM = MFAM.getCachedResult<VirtRegMapAnalysis>(MF);
+ printMIR(OS, FAM, MF, VRM);
return PreservedAnalyses::all();
}
@@ -50,6 +52,7 @@ struct MIRPrintingPass : public MachineFunctionPass {
void getAnalysisUsage(AnalysisUsage &AU) const override {
AU.setPreservesAll();
+ AU.addUsedIfAvailable<VirtRegMapWrapperLegacy>();
MachineFunctionPass::getAnalysisUsage(AU);
}
@@ -60,7 +63,11 @@ struct MIRPrintingPass : public MachineFunctionPass {
MachineModuleInfo *MMI =
&getAnalysis<MachineModuleInfoWrapperPass>().getMMI();
- printMIR(StrOS, *MMI, MF);
+ const VirtRegMap *VRM = nullptr;
+ if (auto *W = getAnalysisIfAvailable<VirtRegMapWrapperLegacy>())
+ VRM = &W->getVRM();
+
+ printMIR(StrOS, *MMI, MF, VRM);
MachineFunctions.append(Str);
return false;
}
diff --git a/llvm/lib/CodeGen/VirtRegMap.cpp b/llvm/lib/CodeGen/VirtRegMap.cpp
index 972bd8f550e8b..3d625e8032111 100644
--- a/llvm/lib/CodeGen/VirtRegMap.cpp
+++ b/llvm/lib/CodeGen/VirtRegMap.cpp
@@ -74,6 +74,21 @@ void VirtRegMap::init(MachineFunction &mf) {
Virt2ShapeMap.clear();
grow();
+
+ // Drain any VirtRegMap state stashed by MIRParser on the generic
+ // MachineFunctionInfo base. Must run after grow() so that Virt2*Map have
+ // capacity for every vreg referenced by the stash.
+ if (auto *MFI = mf.getInfo<MachineFunctionInfo>()) {
+ for (const auto &P : MFI->PendingVRegMappings) {
+ if (P.AssignedPhys)
+ assignVirt2Phys(P.VReg, P.AssignedPhys);
+ // Guard against self-reference; the parser also rejects this, but a
+ // belt-and-braces check keeps Virt2SplitMap meaningful.
+ if (P.SplitFrom && P.SplitFrom != P.VReg)
+ setIsSplitFromReg(P.VReg, P.SplitFrom);
+ }
+ MFI->PendingVRegMappings.clear();
+ }
}
void VirtRegMap::grow() {
diff --git a/llvm/test/CodeGen/MIR/AMDGPU/virtregmap-stash-bad-phys.mir b/llvm/test/CodeGen/MIR/AMDGPU/virtregmap-stash-bad-phys.mir
new file mode 100644
index 0000000000000..9dddfbf4fb6ca
--- /dev/null
+++ b/llvm/test/CodeGen/MIR/AMDGPU/virtregmap-stash-bad-phys.mir
@@ -0,0 +1,18 @@
+# RUN: not llc -mtriple=amdgcn -mcpu=gfx1100 -run-pass=virtregmap \
+# RUN: -filetype=null %s 2>&1 | FileCheck %s
+#
+# 'assigned-phys' must be a physical register; passing a vreg should error.
+
+# CHECK: 'assigned-phys' must be a physical register, got '%0'
+---
+name: bad_phys
+tracksRegLiveness: true
+registers:
+ - { id: 0, class: vgpr_32 }
+ - { id: 1, class: vgpr_32, assigned-phys: '%0' }
+body: |
+ bb.0:
+ %0:vgpr_32 = V_MOV_B32_e32 0, implicit $exec
+ %1:vgpr_32 = V_MOV_B32_e32 1, implicit $exec
+ S_ENDPGM 0, implicit %0, implicit %1
+...
diff --git a/llvm/test/CodeGen/MIR/AMDGPU/virtregmap-stash-bad-split.mir b/llvm/test/CodeGen/MIR/AMDGPU/virtregmap-stash-bad-split.mir
new file mode 100644
index 0000000000000..494677f23f7d1
--- /dev/null
+++ b/llvm/test/CodeGen/MIR/AMDGPU/virtregmap-stash-bad-split.mir
@@ -0,0 +1,16 @@
+# RUN: not llc -mtriple=amdgcn -mcpu=gfx1100 -run-pass=virtregmap \
+# RUN: -filetype=null %s 2>&1 | FileCheck %s
+#
+# 'split-from' must reference a virtual register; physregs are rejected.
+
+# CHECK: expected a virtual register
+---
+name: bad_split
+tracksRegLiveness: true
+registers:
+ - { id: 0, class: vgpr_32, split-from: '$vgpr5' }
+body: |
+ bb.0:
+ %0:vgpr_32 = V_MOV_B32_e32 0, implicit $exec
+ S_ENDPGM 0, implicit %0
+...
diff --git a/llvm/test/CodeGen/MIR/AMDGPU/virtregmap-stash-self-split.mir b/llvm/test/CodeGen/MIR/AMDGPU/virtregmap-stash-self-split.mir
new file mode 100644
index 0000000000000..a14c37e01b2e7
--- /dev/null
+++ b/llvm/test/CodeGen/MIR/AMDGPU/virtregmap-stash-self-split.mir
@@ -0,0 +1,17 @@
+# RUN: not llc -mtriple=amdgcn -mcpu=gfx1100 -run-pass=virtregmap \
+# RUN: -filetype=null %s 2>&1 | FileCheck %s
+#
+# 'split-from' must reference a different vreg than 'id'.
+# I.e., disallow self-references
+
+# CHECK: 'split-from' references the same vreg as 'id' (%0)
+---
+name: self_split
+tracksRegLiveness: true
+registers:
+ - { id: 0, class: vgpr_32, split-from: '%0' }
+body: |
+ bb.0:
+ %0:vgpr_32 = V_MOV_B32_e32 0, implicit $exec
+ S_ENDPGM 0, implicit %0
+...
diff --git a/llvm/test/CodeGen/MIR/AMDGPU/virtregmap-stash.mir b/llvm/test/CodeGen/MIR/AMDGPU/virtregmap-stash.mir
new file mode 100644
index 0000000000000..a9272c92775a6
--- /dev/null
+++ b/llvm/test/CodeGen/MIR/AMDGPU/virtregmap-stash.mir
@@ -0,0 +1,32 @@
+# RUN: llc -mtriple=amdgcn -mcpu=gfx1100 -run-pass=virtregmap \
+# RUN: -o - %s | FileCheck %s
+#
+# Verify that VirtRegMap state (split-from, assigned-phys) round-trips through
+# MIR.
+
+# CHECK-LABEL: name: test_vrm_roundtrip
+# CHECK: registers:
+# CHECK: id: 0,
+# CHECK-NOT: split-from:
+# CHECK: assigned-phys: '$vgpr5'
+# CHECK: id: 1,
+# CHECK: split-from: '%0'
+# CHECK: assigned-phys: '$vgpr6'
+# CHECK: id: 2,
+# CHECK: split-from: '%0'
+# CHECK: assigned-phys: '$vgpr7'
+---
+name: test_vrm_roundtrip
+tracksRegLiveness: true
+registers:
+ - { id: 0, class: vgpr_32, assigned-phys: '$vgpr5' }
+ - { id: 1, class: vgpr_32, assigned-phys: '$vgpr6', split-from: '%0' }
+ - { id: 2, class: vgpr_32, assigned-phys: '$vgpr7', split-from: '%0' }
+body: |
+ bb.0:
+ liveins: $vgpr0
+ %0:vgpr_32 = COPY $vgpr0
+ %1:vgpr_32 = V_ADD_U32_e32 1, %0:vgpr_32, implicit $exec
+ %2:vgpr_32 = V_ADD_U32_e32 2, %0:vgpr_32, implicit $exec
+ S_ENDPGM 0, implicit %0, implicit %1, implicit %2
+...
More information about the llvm-commits
mailing list