aboutsummaryrefslogtreecommitdiff
path: root/backend/unittests/CodeGen
diff options
context:
space:
mode:
authorRichard-Qin-X <richard.qin.001@gmail.com>2026-08-07 22:06:28 +0800
committerRichard-Qin-X <richard.qin.001@gmail.com>2026-08-07 22:06:28 +0800
commitf99ef4322a7a2c09380fff61f88ab9440f05a92c (patch)
tree69538d9ac3acb4a4a680dbce143f39c7b576035c /backend/unittests/CodeGen
parent3e15bfd7b075209ec3b65d30d0611e060f6fbb77 (diff)
downloadrcc-project-f99ef4322a7a2c09380fff61f88ab9440f05a92c.tar.gz
rcc-project-f99ef4322a7a2c09380fff61f88ab9440f05a92c.tar.xz
[CodeGen] Complete machine pass and ABI lowering infrastructure
Diffstat (limited to 'backend/unittests/CodeGen')
-rw-r--r--backend/unittests/CodeGen/CMakeLists.txt48
-rw-r--r--backend/unittests/CodeGen/GenericOpcodesTest.cpp701
-rw-r--r--backend/unittests/CodeGen/MachineAnalysesTest.cpp477
-rw-r--r--backend/unittests/CodeGen/MachinePassTest.cpp918
-rw-r--r--backend/unittests/CodeGen/TargetCallLoweringTest.cpp329
5 files changed, 2473 insertions, 0 deletions
diff --git a/backend/unittests/CodeGen/CMakeLists.txt b/backend/unittests/CodeGen/CMakeLists.txt
index ef96358..deb7a75 100644
--- a/backend/unittests/CodeGen/CMakeLists.txt
+++ b/backend/unittests/CodeGen/CMakeLists.txt
@@ -4,6 +4,7 @@ set(
RCCBackendCodeGenTestSources
CompilationUnitTest.cpp
CoreTypesTest.cpp
+ GenericOpcodesTest.cpp
InstructionDescriptorTest.cpp
MachineCompilationStatusTest.cpp
MachineConstantPoolTest.cpp
@@ -14,13 +15,16 @@ set(
MachineIRAttributesTest.cpp
MachineIRBuilderTest.cpp
MachineIRCoreTest.cpp
+ MachineAnalysesTest.cpp
MachineJumpTableInfoTest.cpp
MachineMetadataTest.cpp
MachineOperandTest.cpp
+ MachinePassTest.cpp
MachineVerifierTest.cpp
MachineVerifierTargetTest.cpp
MachineValueTypesTest.cpp
TargetConstantTest.cpp
+ TargetCallLoweringTest.cpp
TargetFunctionStateTest.cpp
TargetMIRTextInfoTest.cpp
TargetRegisterInfoTest.cpp
@@ -41,6 +45,12 @@ target_link_libraries(RCCBackendCodeGenCoreTypesTests PRIVATE RCCBackend::CodeGe
add_test(NAME RCCBackend.CodeGen.CoreTypes COMMAND RCCBackendCodeGenCoreTypesTests)
+add_executable(RCCBackendCodeGenGenericOpcodesTests GenericOpcodesTest.cpp)
+rcc_backend_configure_target(RCCBackendCodeGenGenericOpcodesTests)
+target_link_libraries(RCCBackendCodeGenGenericOpcodesTests PRIVATE RCCBackend::CodeGen)
+
+add_test(NAME RCCBackend.CodeGen.GenericOpcodes COMMAND RCCBackendCodeGenGenericOpcodesTests)
+
add_executable(
RCCBackendCodeGenInstructionDescriptorTests
InstructionDescriptorTest.cpp
@@ -233,6 +243,24 @@ add_test(
COMMAND RCCBackendCodeGenMachineOperandTests
)
+add_executable(RCCBackendCodeGenMachinePassTests MachinePassTest.cpp)
+rcc_backend_configure_target(RCCBackendCodeGenMachinePassTests)
+target_link_libraries(
+ RCCBackendCodeGenMachinePassTests
+ PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget
+)
+
+add_test(NAME RCCBackend.CodeGen.MachinePass COMMAND RCCBackendCodeGenMachinePassTests)
+
+add_executable(RCCBackendCodeGenMachineAnalysesTests MachineAnalysesTest.cpp)
+rcc_backend_configure_target(RCCBackendCodeGenMachineAnalysesTests)
+target_link_libraries(
+ RCCBackendCodeGenMachineAnalysesTests
+ PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget
+)
+
+add_test(NAME RCCBackend.CodeGen.MachineAnalyses COMMAND RCCBackendCodeGenMachineAnalysesTests)
+
add_executable(RCCBackendCodeGenTargetConstantTests TargetConstantTest.cpp)
rcc_backend_configure_target(RCCBackendCodeGenTargetConstantTests)
target_link_libraries(RCCBackendCodeGenTargetConstantTests PRIVATE RCCBackend::CodeGen)
@@ -242,6 +270,18 @@ add_test(
COMMAND RCCBackendCodeGenTargetConstantTests
)
+add_executable(RCCBackendCodeGenTargetCallLoweringTests TargetCallLoweringTest.cpp)
+rcc_backend_configure_target(RCCBackendCodeGenTargetCallLoweringTests)
+target_link_libraries(
+ RCCBackendCodeGenTargetCallLoweringTests
+ PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget
+)
+
+add_test(
+ NAME RCCBackend.CodeGen.TargetCallLowering
+ COMMAND RCCBackendCodeGenTargetCallLoweringTests
+)
+
add_executable(RCCBackendCodeGenTargetFunctionStateTests TargetFunctionStateTest.cpp)
rcc_backend_configure_target(RCCBackendCodeGenTargetFunctionStateTests)
target_link_libraries(
@@ -281,6 +321,7 @@ add_test(
rcc_backend_register_format_files(
"${CMAKE_CURRENT_SOURCE_DIR}/CompilationUnitTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/CoreTypesTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/GenericOpcodesTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/InstructionDescriptorTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineCompilationStatusTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineConstantPoolTest.cpp"
@@ -291,13 +332,16 @@ rcc_backend_register_format_files(
"${CMAKE_CURRENT_SOURCE_DIR}/MachineIRAttributesTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineIRBuilderTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineIRCoreTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/MachineAnalysesTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineJumpTableInfoTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineMetadataTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineOperandTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/MachinePassTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineVerifierTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineVerifierTargetTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineValueTypesTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/TargetConstantTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/TargetCallLoweringTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/TargetFunctionStateTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/TargetMIRTextInfoTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/TargetRegisterInfoTest.cpp"
@@ -305,6 +349,7 @@ rcc_backend_register_format_files(
rcc_backend_register_tidy_files(
"${CMAKE_CURRENT_SOURCE_DIR}/CompilationUnitTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/CoreTypesTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/GenericOpcodesTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/InstructionDescriptorTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineCompilationStatusTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineConstantPoolTest.cpp"
@@ -315,13 +360,16 @@ rcc_backend_register_tidy_files(
"${CMAKE_CURRENT_SOURCE_DIR}/MachineIRAttributesTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineIRBuilderTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineIRCoreTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/MachineAnalysesTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineJumpTableInfoTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineMetadataTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineOperandTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/MachinePassTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineVerifierTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineVerifierTargetTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineValueTypesTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/TargetConstantTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/TargetCallLoweringTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/TargetFunctionStateTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/TargetMIRTextInfoTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/TargetRegisterInfoTest.cpp"
diff --git a/backend/unittests/CodeGen/GenericOpcodesTest.cpp b/backend/unittests/CodeGen/GenericOpcodesTest.cpp
new file mode 100644
index 0000000..36cc6ca
--- /dev/null
+++ b/backend/unittests/CodeGen/GenericOpcodesTest.cpp
@@ -0,0 +1,701 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/CodeGen/GenericOpcodes.h"
+
+#include "rcc/CodeGen/MachineBasicBlock.h"
+#include "rcc/CodeGen/MachineFunction.h"
+#include "rcc/CodeGen/MachineInstr.h"
+#include "rcc/CodeGen/MachineMemOperand.h"
+
+#include <array>
+#include <bit>
+#include <cstddef>
+#include <cstdint>
+#include <iostream>
+#include <ranges>
+#include <span>
+#include <string>
+#include <string_view>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+namespace {
+
+class TestContext final {
+public:
+ void expect(bool Condition, std::string_view Message) {
+ if (Condition)
+ return;
+ std::cerr << Message << '\n';
+ ++Failures;
+ }
+
+ [[nodiscard]] int result() const noexcept {
+ return Failures == 0 ? 0 : 1;
+ }
+
+private:
+ int Failures = 0;
+};
+
+[[nodiscard]] std::vector<rcc::InstructionDescriptor> copyDescriptors() {
+ const auto Source = rcc::genericInstructionDescriptors();
+ return {Source.begin(), Source.end()};
+}
+
+[[nodiscard]] rcc::MachineOperand registerOperand(rcc::Register Reg, bool IsDef) {
+ rcc::RegisterOperandFlags Flags;
+ Flags.set(IsDef ? rcc::RegisterOperandFlag::Define : rcc::RegisterOperandFlag::Use);
+ auto Operand = rcc::MachineOperand::registerOperand(Reg, Flags);
+ if (!Operand)
+ std::terminate();
+ return std::move(*Operand);
+}
+
+[[nodiscard]] rcc::Register createVirtualRegister(rcc::MachineFunction &Function, rcc::LLT Type) {
+ auto Reg = Function.registerInfo().createVirtualRegister(Type);
+ if (!Reg)
+ std::terminate();
+ return *Reg;
+}
+
+[[nodiscard]] rcc::MachineInstr &createInstruction(rcc::MachineFunction &Function,
+ rcc::GenericOpcode OpcodeValue,
+ std::vector<rcc::MachineOperand> Operands) {
+ auto Instruction = Function.createInstruction(rcc::opcode(OpcodeValue));
+ if (!Instruction)
+ std::terminate();
+ for (rcc::MachineOperand &Operand : Operands) {
+ if (!(*Instruction)->addOperand(std::move(Operand)))
+ std::terminate();
+ }
+ return **Instruction;
+}
+
+[[nodiscard]] rcc::MachineMemOperandDesc
+memoryDescription(rcc::MemoryAccessFlags Accesses, std::uint64_t Bytes,
+ std::uint32_t AddressSpace = 0,
+ rcc::AtomicOperation AtomicKind = rcc::AtomicOperation::None) {
+ rcc::MachineMemOperandDesc Description;
+ Description.Accesses = Accesses;
+ Description.AtomicKind = AtomicKind;
+ if (AtomicKind == rcc::AtomicOperation::ReadModifyWrite) {
+ Description.SuccessOrdering = rcc::AtomicOrdering::AcquireRelease;
+ } else if (AtomicKind == rcc::AtomicOperation::CompareExchange) {
+ Description.SuccessOrdering = rcc::AtomicOrdering::AcquireRelease;
+ Description.FailureOrdering = rcc::AtomicOrdering::Acquire;
+ }
+ Description.Size = *rcc::MemorySize::fixed(Bytes);
+ Description.Align = *rcc::Alignment::create(std::bit_floor(Bytes));
+ Description.AddressSpace = AddressSpace;
+ Description.Base = rcc::MemoryBase::stack();
+ return Description;
+}
+
+void addMemOperand(rcc::MachineFunction &Function, rcc::MachineInstr &Instruction,
+ rcc::MachineMemOperandDesc Description) {
+ auto Memory = Function.createMemOperand(std::move(Description));
+ if (!Memory || !Instruction.addMemOperand(**Memory))
+ std::terminate();
+}
+
+void testCompleteRegistry(TestContext &Context) {
+ static_assert(rcc::CorePseudoOpcodeCount == 52);
+ static_assert(rcc::GenericOpcodeCount == 278);
+
+ const auto Descriptors = rcc::genericInstructionDescriptors();
+ Context.expect(Descriptors.size() == 330,
+ "the target-independent registry must contain 52 core and 278 generic opcodes");
+
+ std::unordered_set<std::string> Names;
+ std::unordered_set<rcc::Opcode> Opcodes;
+ bool ClassesAreCorrect = true;
+ for (const rcc::InstructionDescriptor &Descriptor : Descriptors) {
+ ClassesAreCorrect &= Descriptor.InstructionOpcode.isValid() && !Descriptor.Name.empty() &&
+ Names.insert(Descriptor.Name).second &&
+ Opcodes.insert(Descriptor.InstructionOpcode).second;
+ if (rcc::isGenericOpcode(Descriptor.InstructionOpcode)) {
+ ClassesAreCorrect &= Descriptor.Flags.has(rcc::InstructionFlag::Generic) &&
+ !Descriptor.Flags.has(rcc::InstructionFlag::Pseudo);
+ } else {
+ ClassesAreCorrect &= rcc::isCorePseudoOpcode(Descriptor.InstructionOpcode) &&
+ Descriptor.Flags.has(rcc::InstructionFlag::Pseudo) &&
+ !Descriptor.Flags.has(rcc::InstructionFlag::Generic);
+ }
+ }
+ Context.expect(ClassesAreCorrect,
+ "every target-independent opcode must have one valid name, value, and class");
+ Context.expect(rcc::getGenericOpcodeName(rcc::GenericOpcode::PHI) == "PHI" &&
+ rcc::getGenericOpcodeName(rcc::GenericOpcode::G_ASSERT_SEXT) ==
+ "G_ASSERT_SEXT" &&
+ rcc::getGenericOpcodeName(rcc::GenericOpcode::G_UBFX) == "G_UBFX" &&
+ rcc::getGenericOpcodeName(rcc::GenericOpcode::Invalid).empty(),
+ "opcode names must cover both ranges without assigning marker names");
+
+ auto Set = rcc::InstructionSet::create(copyDescriptors());
+ Context.expect(Set.has_value(), "the complete target-independent descriptor set must validate");
+}
+
+void testCoreOpcodeContracts(TestContext &Context) {
+ struct Contract final {
+ rcc::GenericOpcode OpcodeValue;
+ std::size_t DescriptorOperands;
+ std::size_t ExplicitDefs;
+ bool Variadic;
+ std::size_t MinimumRepetitions;
+ bool Meta;
+ bool TerminalEmission;
+ rcc::PseudoOwner Owner;
+ rcc::PipelineStage Deadline;
+ };
+
+#define RCC_META(Name, Operands, Defs, Variadic, Minimum) \
+ Contract{rcc::GenericOpcode::Name, \
+ Operands, \
+ Defs, \
+ Variadic, \
+ Minimum, \
+ true, \
+ false, \
+ rcc::PseudoOwner::None, \
+ rcc::PipelineStage::Emission}
+#define RCC_PSEUDO(Name, Operands, Defs, Variadic, Minimum, Owner, Deadline) \
+ Contract{rcc::GenericOpcode::Name, \
+ Operands, \
+ Defs, \
+ Variadic, \
+ Minimum, \
+ false, \
+ false, \
+ rcc::PseudoOwner::Owner, \
+ rcc::PipelineStage::Deadline}
+#define RCC_TERMINAL(Name, Operands, Defs, Variadic, Minimum) \
+ Contract { \
+ rcc::GenericOpcode::Name, Operands, Defs, Variadic, Minimum, false, true, \
+ rcc::PseudoOwner::AsmPrinter, rcc::PipelineStage::Emission \
+ }
+
+ const std::array Contracts{
+ RCC_PSEUDO(PHI, 3, 1, true, 1, PhiElimination, SSAEliminated),
+ RCC_TERMINAL(INLINEASM, 1, 0, true, 0),
+ RCC_TERMINAL(INLINEASM_BR, 1, 0, true, 0),
+ RCC_META(CFI_INSTRUCTION, 1, 0, false, 0),
+ RCC_META(EH_LABEL, 1, 0, false, 0),
+ RCC_META(GC_LABEL, 1, 0, false, 0),
+ RCC_META(ANNOTATION_LABEL, 1, 0, false, 0),
+ RCC_META(KILL, 1, 0, true, 0),
+ RCC_PSEUDO(EXTRACT_SUBREG, 3, 1, false, 0, PostRAExpansion, PostRAFinalized),
+ RCC_PSEUDO(INSERT_SUBREG, 4, 1, false, 0, TwoAddressRewrite, TiedOpsRewritten),
+ RCC_META(IMPLICIT_DEF, 1, 1, false, 0),
+ RCC_PSEUDO(INIT_UNDEF, 1, 1, false, 0, RegisterAllocator, RegisterAllocated),
+ RCC_PSEUDO(SUBREG_TO_REG, 3, 1, false, 0, PostRAExpansion, PostRAFinalized),
+ RCC_PSEUDO(COPY_TO_REGCLASS, 3, 1, false, 0, RegisterAllocator, RegisterAllocated),
+ RCC_META(DBG_VALUE, 1, 0, true, 0),
+ RCC_META(DBG_VALUE_LIST, 1, 0, true, 0),
+ RCC_META(DBG_INSTR_REF, 1, 0, true, 0),
+ RCC_META(DBG_PHI, 1, 0, true, 0),
+ RCC_META(DBG_LABEL, 1, 0, false, 0),
+ RCC_PSEUDO(REG_SEQUENCE, 3, 1, true, 1, TwoAddressRewrite, TiedOpsRewritten),
+ RCC_PSEUDO(COPY, 2, 1, false, 0, PostRAExpansion, PostRAFinalized),
+ RCC_PSEUDO(COPY_LANEMASK, 3, 1, false, 0, PostRAExpansion, PostRAFinalized),
+ RCC_META(BUNDLE, 1, 0, true, 0),
+ RCC_META(LIFETIME_START, 1, 0, false, 0),
+ RCC_META(LIFETIME_END, 1, 0, false, 0),
+ RCC_META(PSEUDO_PROBE, 4, 0, false, 0),
+ RCC_META(ARITH_FENCE, 2, 1, false, 0),
+ RCC_TERMINAL(STACKMAP, 3, 0, true, 0),
+ RCC_TERMINAL(FENTRY_CALL, 0, 0, false, 0),
+ RCC_TERMINAL(PATCHPOINT, 7, 1, true, 0),
+ RCC_PSEUDO(LOAD_STACK_GUARD, 1, 1, false, 0, InstructionSelect, Selected),
+ RCC_PSEUDO(PREALLOCATED_SETUP, 1, 0, false, 0, InstructionSelect, Selected),
+ RCC_PSEUDO(PREALLOCATED_ARG, 3, 1, false, 0, InstructionSelect, Selected),
+ RCC_TERMINAL(STATEPOINT, 1, 0, true, 0),
+ RCC_META(LOCAL_ESCAPE, 2, 0, false, 0),
+ RCC_TERMINAL(FAULTING_OP, 2, 1, true, 0),
+ RCC_TERMINAL(PATCHABLE_OP, 1, 0, true, 0),
+ RCC_TERMINAL(PATCHABLE_FUNCTION_ENTER, 0, 0, false, 0),
+ RCC_TERMINAL(PATCHABLE_RET, 1, 0, true, 0),
+ RCC_TERMINAL(PATCHABLE_FUNCTION_EXIT, 0, 0, false, 0),
+ RCC_TERMINAL(PATCHABLE_TAIL_CALL, 1, 0, true, 0),
+ RCC_TERMINAL(PATCHABLE_EVENT_CALL, 2, 0, false, 0),
+ RCC_TERMINAL(PATCHABLE_TYPED_EVENT_CALL, 3, 0, false, 0),
+ RCC_PSEUDO(ICALL_BRANCH_FUNNEL, 1, 0, true, 0, PostRAExpansion, PostRAFinalized),
+ RCC_META(FAKE_USE, 1, 0, true, 0),
+ RCC_META(MEMBARRIER, 0, 0, false, 0),
+ RCC_META(JUMP_TABLE_DEBUG_INFO, 1, 0, false, 0),
+ RCC_META(RELOC_NONE, 1, 0, false, 0),
+ RCC_META(CONVERGENCECTRL_ENTRY, 1, 1, false, 0),
+ RCC_META(CONVERGENCECTRL_ANCHOR, 1, 1, false, 0),
+ RCC_META(CONVERGENCECTRL_LOOP, 2, 1, false, 0),
+ RCC_META(CONVERGENCECTRL_GLUE, 1, 0, false, 0),
+ };
+#undef RCC_TERMINAL
+#undef RCC_PSEUDO
+#undef RCC_META
+
+ static_assert(Contracts.size() == rcc::CorePseudoOpcodeCount);
+ const auto Descriptors = rcc::genericInstructionDescriptors();
+ bool Matches = true;
+ for (const Contract &Expected : Contracts) {
+ const auto Found = std::ranges::find_if(
+ Descriptors, [&Expected](const rcc::InstructionDescriptor &Descriptor) {
+ return Descriptor.InstructionOpcode == rcc::opcode(Expected.OpcodeValue);
+ });
+ if (Found == Descriptors.end()) {
+ Matches = false;
+ continue;
+ }
+ Matches &=
+ Found->Operands.size() == Expected.DescriptorOperands &&
+ Found->NumExplicitDefs == Expected.ExplicitDefs && Found->Variadic == Expected.Variadic &&
+ Found->MinimumVariadicRepetitions == Expected.MinimumRepetitions &&
+ Found->Flags.has(rcc::InstructionFlag::Meta) == Expected.Meta &&
+ Found->Flags.has(rcc::InstructionFlag::TerminalEmission) == Expected.TerminalEmission &&
+ Found->Owner == Expected.Owner && Found->Deadline == Expected.Deadline;
+ }
+ Context.expect(Matches,
+ "all 52 core opcodes must retain their reviewed shape and lifecycle contracts");
+
+ const auto InlineAsm = std::ranges::find_if(Descriptors, [](const auto &Descriptor) {
+ return Descriptor.InstructionOpcode == rcc::opcode(rcc::GenericOpcode::INLINEASM);
+ });
+ const auto InlineAsmBr = std::ranges::find_if(Descriptors, [](const auto &Descriptor) {
+ return Descriptor.InstructionOpcode == rcc::opcode(rcc::GenericOpcode::INLINEASM_BR);
+ });
+ Context.expect(InlineAsm != Descriptors.end() && InlineAsmBr != Descriptors.end() &&
+ !InlineAsm->Flags.has(rcc::InstructionFlag::HasSideEffects) &&
+ InlineAsmBr->Flags.has(rcc::InstructionFlag::HasSideEffects),
+ "INLINEASM_BR must be intrinsically side-effecting while INLINEASM uses its "
+ "operand flags");
+}
+
+void testShapesAndTypes(TestContext &Context) {
+ auto Set = rcc::InstructionSet::create(copyDescriptors());
+ auto Function =
+ rcc::MachineFunction::create("generic", rcc::MachineFunctionStableID::fromValue(1));
+ Context.expect(Set.has_value() && Function.has_value(),
+ "generic opcode tests require a descriptor set and machine function");
+ if (!Set || !Function)
+ return;
+
+ auto I32 = rcc::LLT::makeInteger(32);
+ auto I64 = rcc::LLT::makeInteger(64);
+ auto I1 = rcc::LLT::makeInteger(1);
+ auto F32 = rcc::LLT::makeFloat(rcc::FloatSemantics::IEEESingle);
+ auto F64 = rcc::LLT::makeFloat(rcc::FloatSemantics::IEEEDouble);
+ auto P64 = rcc::LLT::makePointer(0, 64);
+ auto V2I32 = I32 ? rcc::LLT::makeFixedVector(2, *I32) : rcc::Expected<rcc::LLT>{};
+ auto V4I32 = I32 ? rcc::LLT::makeFixedVector(4, *I32) : rcc::Expected<rcc::LLT>{};
+ auto V2P64 = P64 ? rcc::LLT::makeFixedVector(2, *P64) : rcc::Expected<rcc::LLT>{};
+ if (!I1 || !I32 || !I64 || !F32 || !F64 || !P64 || !V2I32 || !V4I32 || !V2P64)
+ std::terminate();
+ auto Dst32 = (*Function)->registerInfo().createVirtualRegister(*I32);
+ auto Src32A = (*Function)->registerInfo().createVirtualRegister(*I32);
+ auto Src32B = (*Function)->registerInfo().createVirtualRegister(*I32);
+ auto Wide = (*Function)->registerInfo().createVirtualRegister(*I64);
+ auto Address = (*Function)->registerInfo().createVirtualRegister(*P64);
+ auto NewAddress = (*Function)->registerInfo().createVirtualRegister(*P64);
+ const rcc::Register Cond = createVirtualRegister(**Function, *I1);
+ const rcc::Register Float32 = createVirtualRegister(**Function, *F32);
+ const rcc::Register Float64 = createVirtualRegister(**Function, *F64);
+ const rcc::Register Vec2A = createVirtualRegister(**Function, *V2I32);
+ const rcc::Register Vec2B = createVirtualRegister(**Function, *V2I32);
+ const rcc::Register Vec4 = createVirtualRegister(**Function, *V4I32);
+ const rcc::Register PointerVector = createVirtualRegister(**Function, *V2P64);
+ if (!Dst32 || !Src32A || !Src32B || !Wide || !Address || !NewAddress)
+ std::terminate();
+
+ std::vector<rcc::MachineOperand> AddOperands;
+ AddOperands.push_back(registerOperand(*Dst32, true));
+ AddOperands.push_back(registerOperand(*Src32A, false));
+ AddOperands.push_back(registerOperand(*Src32B, false));
+ Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ADD), AddOperands,
+ (*Function)->registerInfo())
+ .has_value(),
+ "same-typed integer operands must satisfy G_ADD");
+ AddOperands.back() = registerOperand(*Wide, false);
+ Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ADD), AddOperands,
+ (*Function)->registerInfo()),
+ "G_ADD must reject a source from a different LLT equality group");
+
+ auto Block0 = (*Function)->createBasicBlock();
+ auto Block1 = (*Function)->createBasicBlock();
+ if (!Block0 || !Block1 || !(*Function)->appendBlock(**Block0) ||
+ !(*Function)->appendBlock(**Block1)) {
+ std::terminate();
+ }
+ std::vector<rcc::MachineOperand> PhiOperands;
+ PhiOperands.push_back(registerOperand(*Dst32, true));
+ Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_PHI), PhiOperands,
+ (*Function)->registerInfo()),
+ "G_PHI must require at least one value and predecessor pair");
+ PhiOperands.push_back(registerOperand(*Src32A, false));
+ PhiOperands.push_back(rcc::MachineOperand::basicBlock(**Block0));
+ PhiOperands.push_back(registerOperand(*Src32B, false));
+ PhiOperands.push_back(rcc::MachineOperand::basicBlock(**Block1));
+ Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_PHI), PhiOperands,
+ (*Function)->registerInfo())
+ .has_value(),
+ "G_PHI must accept repeated value and predecessor pairs");
+ PhiOperands.pop_back();
+ Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_PHI), PhiOperands,
+ (*Function)->registerInfo()),
+ "G_PHI must reject an incomplete variadic operand pair");
+
+ std::vector<rcc::MachineOperand> IndexedLoad;
+ IndexedLoad.push_back(registerOperand(*Dst32, true));
+ IndexedLoad.push_back(registerOperand(*NewAddress, true));
+ IndexedLoad.push_back(registerOperand(*Address, false));
+ IndexedLoad.push_back(registerOperand(*Wide, false));
+ IndexedLoad.push_back(rcc::MachineOperand::immediate(1));
+ Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_INDEXED_LOAD),
+ IndexedLoad, (*Function)->registerInfo())
+ .has_value(),
+ "indexed loads must keep address and integer offset type groups independent");
+
+ std::vector<rcc::MachineOperand> UnmergeOperands;
+ UnmergeOperands.push_back(registerOperand(*Dst32, true));
+ UnmergeOperands.push_back(registerOperand(*Wide, false));
+ Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_UNMERGE_VALUES),
+ UnmergeOperands, (*Function)->registerInfo()),
+ "G_UNMERGE_VALUES must require at least two definitions");
+ UnmergeOperands.insert(UnmergeOperands.begin() + 1, registerOperand(*Src32B, true));
+ Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_UNMERGE_VALUES),
+ UnmergeOperands, (*Function)->registerInfo())
+ .has_value(),
+ "two homogeneous halves must exactly cover a G_UNMERGE_VALUES source");
+
+ std::vector<rcc::MachineOperand> MergeOperands;
+ MergeOperands.push_back(registerOperand(*Wide, true));
+ MergeOperands.push_back(registerOperand(*Src32A, false));
+ Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_MERGE_VALUES),
+ MergeOperands, (*Function)->registerInfo()),
+ "G_MERGE_VALUES must require at least two sources");
+ MergeOperands.push_back(registerOperand(*Src32B, false));
+ Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_MERGE_VALUES),
+ MergeOperands, (*Function)->registerInfo())
+ .has_value(),
+ "two homogeneous sources must exactly fill a G_MERGE_VALUES result");
+
+ std::vector<rcc::MachineOperand> ConcatOperands;
+ ConcatOperands.push_back(registerOperand(Vec4, true));
+ ConcatOperands.push_back(registerOperand(Vec2A, false));
+ Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_CONCAT_VECTORS),
+ ConcatOperands, (*Function)->registerInfo()),
+ "G_CONCAT_VECTORS must require at least two source vectors");
+ ConcatOperands.push_back(registerOperand(Vec2B, false));
+ Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_CONCAT_VECTORS),
+ ConcatOperands, (*Function)->registerInfo())
+ .has_value(),
+ "two vector sources must exactly tile a G_CONCAT_VECTORS result");
+
+ std::vector<rcc::MachineOperand> FrameIndexOperands;
+ FrameIndexOperands.push_back(registerOperand(PointerVector, true));
+ FrameIndexOperands.push_back(rcc::MachineOperand::frameIndex(rcc::FrameIndex::fromValue(0)));
+ Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_FRAME_INDEX),
+ FrameIndexOperands, (*Function)->registerInfo()),
+ "G_FRAME_INDEX must reject a vector-of-pointers result");
+
+ std::vector<rcc::MachineOperand> PtrAuthOperands;
+ PtrAuthOperands.push_back(registerOperand(*NewAddress, true));
+ PtrAuthOperands.push_back(registerOperand(*Address, false));
+ PtrAuthOperands.push_back(rcc::MachineOperand::immediate(0));
+ PtrAuthOperands.push_back(registerOperand(*Wide, false));
+ PtrAuthOperands.push_back(rcc::MachineOperand::immediate(7));
+ Context.expect(
+ Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_PTRAUTH_GLOBAL_VALUE),
+ PtrAuthOperands, (*Function)->registerInfo())
+ .has_value(),
+ "G_PTRAUTH_GLOBAL_VALUE must accept pointer and discriminator registers");
+ auto Symbol = rcc::MachineOperand::globalSymbol("global");
+ if (!Symbol)
+ std::terminate();
+ PtrAuthOperands[1] = std::move(*Symbol);
+ Context.expect(
+ !Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_PTRAUTH_GLOBAL_VALUE),
+ PtrAuthOperands, (*Function)->registerInfo()),
+ "G_PTRAUTH_GLOBAL_VALUE must reject a symbol in place of its address register");
+
+ std::vector<rcc::MachineOperand> ExtensionOperands;
+ ExtensionOperands.push_back(registerOperand(*Wide, true));
+ ExtensionOperands.push_back(registerOperand(*Src32A, false));
+ Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ZEXT),
+ ExtensionOperands, (*Function)->registerInfo())
+ .has_value(),
+ "G_ZEXT must accept a wider destination with matching shape");
+ ExtensionOperands[0] = registerOperand(*Dst32, true);
+ ExtensionOperands[1] = registerOperand(*Wide, false);
+ Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ZEXT),
+ ExtensionOperands, (*Function)->registerInfo()),
+ "G_ZEXT must reject a narrower destination");
+
+ auto Predicate = rcc::MachineOperand::integerPredicate(rcc::IntegerPredicate::Equal);
+ if (!Predicate)
+ std::terminate();
+ std::vector<rcc::MachineOperand> CompareOperands;
+ CompareOperands.push_back(registerOperand(Cond, true));
+ CompareOperands.push_back(std::move(*Predicate));
+ CompareOperands.push_back(registerOperand(*Src32A, false));
+ CompareOperands.push_back(registerOperand(*Src32B, false));
+ Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ICMP),
+ CompareOperands, (*Function)->registerInfo())
+ .has_value(),
+ "G_ICMP must accept an i1 result and homogeneous integer sources");
+ CompareOperands[0] = registerOperand(*Dst32, true);
+ Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ICMP),
+ CompareOperands, (*Function)->registerInfo()),
+ "G_ICMP must reject a non-i1 result");
+
+ std::vector<rcc::MachineOperand> FloatingLoad;
+ FloatingLoad.push_back(registerOperand(Float64, true));
+ FloatingLoad.push_back(registerOperand(*Address, false));
+ Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_FPEXTLOAD),
+ FloatingLoad, (*Function)->registerInfo())
+ .has_value(),
+ "G_FPEXTLOAD must accept a scalar floating result and pointer address");
+ FloatingLoad[0] = registerOperand(*Wide, true);
+ Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_FPEXTLOAD),
+ FloatingLoad, (*Function)->registerInfo()),
+ "G_FPEXTLOAD must reject an integer result");
+
+ const std::array<std::uint64_t, 1> FloatWords{0};
+ auto FloatBits = rcc::FloatingPointBits::create(rcc::FloatSemantics::IEEESingle, FloatWords);
+ if (!FloatBits)
+ std::terminate();
+ std::vector<rcc::MachineOperand> FConstantOperands;
+ FConstantOperands.push_back(registerOperand(Float32, true));
+ FConstantOperands.push_back(rcc::MachineOperand::floatingImmediate(*FloatBits));
+ Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_FCONSTANT),
+ FConstantOperands, (*Function)->registerInfo())
+ .has_value(),
+ "G_FCONSTANT payload semantics must match its scalar destination");
+ FConstantOperands[0] = registerOperand(Float64, true);
+ Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_FCONSTANT),
+ FConstantOperands, (*Function)->registerInfo()),
+ "G_FCONSTANT must reject mismatched floating semantics");
+}
+
+void testInstructionMemorySemantics(TestContext &Context) {
+ auto Set = rcc::InstructionSet::create(copyDescriptors());
+ auto Function =
+ rcc::MachineFunction::create("generic_memory", rcc::MachineFunctionStableID::fromValue(2));
+ Context.expect(Set && Function, "generic memory tests require an instruction set and function");
+ if (!Set || !Function)
+ return;
+
+ const rcc::LLT I1 = *rcc::LLT::makeInteger(1);
+ const rcc::LLT I32 = *rcc::LLT::makeInteger(32);
+ const rcc::LLT I64 = *rcc::LLT::makeInteger(64);
+ const rcc::LLT F64 = *rcc::LLT::makeFloat(rcc::FloatSemantics::IEEEDouble);
+ const rcc::LLT P0 = *rcc::LLT::makePointer(0, 64);
+ const rcc::LLT P1 = *rcc::LLT::makePointer(1, 64);
+ const rcc::Register Dst32 = createVirtualRegister(**Function, I32);
+ const rcc::Register Dst64 = createVirtualRegister(**Function, I64);
+ const rcc::Register Src64 = createVirtualRegister(**Function, I64);
+ const rcc::Register Float64 = createVirtualRegister(**Function, F64);
+ const rcc::Register Status = createVirtualRegister(**Function, I1);
+ const rcc::Register Address0 = createVirtualRegister(**Function, P0);
+ const rcc::Register Address1 = createVirtualRegister(**Function, P1);
+
+ std::vector<rcc::MachineOperand> LoadOperands;
+ LoadOperands.push_back(registerOperand(Dst64, true));
+ LoadOperands.push_back(registerOperand(Address0, false));
+ rcc::MachineInstr &Load =
+ createInstruction(**Function, rcc::GenericOpcode::G_LOAD, std::move(LoadOperands));
+ addMemOperand(**Function, Load, memoryDescription({rcc::MemoryAccessFlag::Load}, 8));
+ Context.expect(Set->validateInstruction(Load, (*Function)->registerInfo()).has_value(),
+ "G_LOAD must accept one matching load MMO");
+
+ std::vector<rcc::MachineOperand> OversizedLoadOperands;
+ OversizedLoadOperands.push_back(registerOperand(Dst32, true));
+ OversizedLoadOperands.push_back(registerOperand(Address0, false));
+ rcc::MachineInstr &OversizedLoad =
+ createInstruction(**Function, rcc::GenericOpcode::G_LOAD, std::move(OversizedLoadOperands));
+ addMemOperand(**Function, OversizedLoad, memoryDescription({rcc::MemoryAccessFlag::Load}, 8));
+ Context.expect(!Set->validateInstruction(OversizedLoad, (*Function)->registerInfo()),
+ "G_LOAD must reject an MMO larger than its result LLT");
+
+ std::vector<rcc::MachineOperand> WrongAddressSpaceOperands;
+ WrongAddressSpaceOperands.push_back(registerOperand(Dst64, true));
+ WrongAddressSpaceOperands.push_back(registerOperand(Address1, false));
+ rcc::MachineInstr &WrongAddressSpace = createInstruction(**Function, rcc::GenericOpcode::G_LOAD,
+ std::move(WrongAddressSpaceOperands));
+ addMemOperand(**Function, WrongAddressSpace,
+ memoryDescription({rcc::MemoryAccessFlag::Load}, 8, 0));
+ Context.expect(!Set->validateInstruction(WrongAddressSpace, (*Function)->registerInfo()),
+ "generic memory access must match its pointer and MMO address spaces");
+
+ std::vector<rcc::MachineOperand> ExtLoadOperands;
+ ExtLoadOperands.push_back(registerOperand(Dst64, true));
+ ExtLoadOperands.push_back(registerOperand(Address0, false));
+ rcc::MachineInstr &ExtLoad =
+ createInstruction(**Function, rcc::GenericOpcode::G_ZEXTLOAD, std::move(ExtLoadOperands));
+ addMemOperand(**Function, ExtLoad, memoryDescription({rcc::MemoryAccessFlag::Load}, 4));
+ Context.expect(Set->validateInstruction(ExtLoad, (*Function)->registerInfo()).has_value(),
+ "G_ZEXTLOAD must accept a strictly narrower memory access");
+
+ std::vector<rcc::MachineOperand> NonExtendingLoadOperands;
+ NonExtendingLoadOperands.push_back(registerOperand(Dst64, true));
+ NonExtendingLoadOperands.push_back(registerOperand(Address0, false));
+ rcc::MachineInstr &NonExtendingLoad = createInstruction(
+ **Function, rcc::GenericOpcode::G_ZEXTLOAD, std::move(NonExtendingLoadOperands));
+ addMemOperand(**Function, NonExtendingLoad, memoryDescription({rcc::MemoryAccessFlag::Load}, 8));
+ Context.expect(!Set->validateInstruction(NonExtendingLoad, (*Function)->registerInfo()),
+ "G_ZEXTLOAD must reject a non-narrowing MMO");
+
+ std::vector<rcc::MachineOperand> CompareExchangeOperands;
+ CompareExchangeOperands.push_back(registerOperand(Dst64, true));
+ CompareExchangeOperands.push_back(registerOperand(Status, true));
+ CompareExchangeOperands.push_back(registerOperand(Address0, false));
+ CompareExchangeOperands.push_back(registerOperand(Src64, false));
+ CompareExchangeOperands.push_back(registerOperand(Src64, false));
+ rcc::MachineInstr &CompareExchange =
+ createInstruction(**Function, rcc::GenericOpcode::G_ATOMIC_CMPXCHG_WITH_SUCCESS,
+ std::move(CompareExchangeOperands));
+ addMemOperand(**Function, CompareExchange,
+ memoryDescription({rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Store}, 8, 0,
+ rcc::AtomicOperation::CompareExchange));
+ Context.expect(Set->validateInstruction(CompareExchange, (*Function)->registerInfo()).has_value(),
+ "generic compare-exchange must accept a matching atomic MMO");
+
+ std::vector<rcc::MachineOperand> WrongAtomicOperands;
+ WrongAtomicOperands.push_back(registerOperand(Dst64, true));
+ WrongAtomicOperands.push_back(registerOperand(Address0, false));
+ WrongAtomicOperands.push_back(registerOperand(Src64, false));
+ rcc::MachineInstr &WrongAtomic = createInstruction(
+ **Function, rcc::GenericOpcode::G_ATOMICRMW_ADD, std::move(WrongAtomicOperands));
+ addMemOperand(**Function, WrongAtomic,
+ memoryDescription({rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Store}, 8, 0,
+ rcc::AtomicOperation::CompareExchange));
+ Context.expect(!Set->validateInstruction(WrongAtomic, (*Function)->registerInfo()),
+ "generic atomic RMW must reject a compare-exchange MMO");
+
+ std::vector<rcc::MachineOperand> FloatingAtomicOperands;
+ FloatingAtomicOperands.push_back(registerOperand(Float64, true));
+ FloatingAtomicOperands.push_back(registerOperand(Address0, false));
+ FloatingAtomicOperands.push_back(registerOperand(Float64, false));
+ rcc::MachineInstr &FloatingAtomic = createInstruction(
+ **Function, rcc::GenericOpcode::G_ATOMICRMW_FADD, std::move(FloatingAtomicOperands));
+ addMemOperand(**Function, FloatingAtomic,
+ memoryDescription({rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Store}, 8, 0,
+ rcc::AtomicOperation::ReadModifyWrite));
+ Context.expect(Set->validateInstruction(FloatingAtomic, (*Function)->registerInfo()).has_value(),
+ "floating atomic RMW must accept a scalar floating value and RMW MMO");
+
+ std::vector<rcc::MachineOperand> WrongFloatingAtomicOperands;
+ WrongFloatingAtomicOperands.push_back(registerOperand(Dst64, true));
+ WrongFloatingAtomicOperands.push_back(registerOperand(Address0, false));
+ WrongFloatingAtomicOperands.push_back(registerOperand(Src64, false));
+ rcc::MachineInstr &WrongFloatingAtomic = createInstruction(
+ **Function, rcc::GenericOpcode::G_ATOMICRMW_FADD, std::move(WrongFloatingAtomicOperands));
+ addMemOperand(**Function, WrongFloatingAtomic,
+ memoryDescription({rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Store}, 8, 0,
+ rcc::AtomicOperation::ReadModifyWrite));
+ Context.expect(!Set->validateInstruction(WrongFloatingAtomic, (*Function)->registerInfo()),
+ "floating atomic RMW must reject integer value registers");
+
+ std::vector<rcc::MachineOperand> PrefetchOperands;
+ PrefetchOperands.push_back(registerOperand(Address0, false));
+ PrefetchOperands.push_back(rcc::MachineOperand::immediate(0));
+ PrefetchOperands.push_back(rcc::MachineOperand::immediate(3));
+ PrefetchOperands.push_back(rcc::MachineOperand::immediate(1));
+ rcc::MachineInstr &Prefetch =
+ createInstruction(**Function, rcc::GenericOpcode::G_PREFETCH, std::move(PrefetchOperands));
+ Context.expect(!Set->validateInstruction(Prefetch, (*Function)->registerInfo()),
+ "G_PREFETCH must require an MMO");
+ addMemOperand(**Function, Prefetch, memoryDescription({rcc::MemoryAccessFlag::Load}, 1));
+ Context.expect(Set->validateInstruction(Prefetch, (*Function)->registerInfo()).has_value(),
+ "read-prefetch selector must match a load-only MMO");
+
+ std::vector<rcc::MachineOperand> VarArgOperands;
+ VarArgOperands.push_back(registerOperand(Dst64, true));
+ VarArgOperands.push_back(registerOperand(Address0, false));
+ VarArgOperands.push_back(rcc::MachineOperand::immediate(8));
+ rcc::MachineInstr &VarArg =
+ createInstruction(**Function, rcc::GenericOpcode::G_VAARG, std::move(VarArgOperands));
+ addMemOperand(**Function, VarArg, memoryDescription({rcc::MemoryAccessFlag::Load}, 8));
+ addMemOperand(**Function, VarArg, memoryDescription({rcc::MemoryAccessFlag::Store}, 8));
+ Context.expect(Set->validateInstruction(VarArg, (*Function)->registerInfo()).has_value(),
+ "G_VAARG must accept separate value-load and list-update MMOs");
+
+ std::vector<rcc::MachineOperand> CombinedVarArgOperands;
+ CombinedVarArgOperands.push_back(registerOperand(Dst64, true));
+ CombinedVarArgOperands.push_back(registerOperand(Address0, false));
+ CombinedVarArgOperands.push_back(rcc::MachineOperand::immediate(8));
+ rcc::MachineInstr &CombinedVarArg =
+ createInstruction(**Function, rcc::GenericOpcode::G_VAARG, std::move(CombinedVarArgOperands));
+ addMemOperand(**Function, CombinedVarArg,
+ memoryDescription({rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Store}, 8));
+ Context.expect(!Set->validateInstruction(CombinedVarArg, (*Function)->registerInfo()),
+ "G_VAARG must reject a combined read-write MMO in place of two accesses");
+
+ std::vector<rcc::MachineOperand> MemoryTransferOperands;
+ MemoryTransferOperands.push_back(registerOperand(Address0, false));
+ MemoryTransferOperands.push_back(registerOperand(Address1, false));
+ MemoryTransferOperands.push_back(registerOperand(Src64, false));
+ MemoryTransferOperands.push_back(rcc::MachineOperand::immediate(0));
+ rcc::MachineInstr &MemoryTransfer = createInstruction(**Function, rcc::GenericOpcode::G_MEMCPY,
+ std::move(MemoryTransferOperands));
+ addMemOperand(**Function, MemoryTransfer,
+ memoryDescription({rcc::MemoryAccessFlag::Store}, 8, 0));
+ addMemOperand(**Function, MemoryTransfer, memoryDescription({rcc::MemoryAccessFlag::Load}, 8, 1));
+ Context.expect(Set->validateInstruction(MemoryTransfer, (*Function)->registerInfo()).has_value(),
+ "G_MEMCPY must accept equal-sized store/load MMOs in canonical order");
+
+ std::vector<rcc::MachineOperand> ReversedTransferOperands;
+ ReversedTransferOperands.push_back(registerOperand(Address0, false));
+ ReversedTransferOperands.push_back(registerOperand(Address1, false));
+ ReversedTransferOperands.push_back(registerOperand(Src64, false));
+ ReversedTransferOperands.push_back(rcc::MachineOperand::immediate(0));
+ rcc::MachineInstr &ReversedTransfer = createInstruction(**Function, rcc::GenericOpcode::G_MEMCPY,
+ std::move(ReversedTransferOperands));
+ addMemOperand(**Function, ReversedTransfer,
+ memoryDescription({rcc::MemoryAccessFlag::Load}, 8, 0));
+ addMemOperand(**Function, ReversedTransfer,
+ memoryDescription({rcc::MemoryAccessFlag::Store}, 8, 1));
+ Context.expect(!Set->validateInstruction(ReversedTransfer, (*Function)->registerInfo()),
+ "G_MEMCPY must reject reversed MMO access directions");
+}
+
+void testSemanticFlags(TestContext &Context) {
+ const auto Descriptors = rcc::genericInstructionDescriptors();
+ const auto Find = [Descriptors](rcc::GenericOpcode OpcodeValue) {
+ const rcc::Opcode Value = rcc::opcode(OpcodeValue);
+ const auto Position =
+ std::ranges::find_if(Descriptors, [Value](const rcc::InstructionDescriptor &Descriptor) {
+ return Descriptor.InstructionOpcode == Value;
+ });
+ return Position == Descriptors.end() ? nullptr : &*Position;
+ };
+
+ const auto *Load = Find(rcc::GenericOpcode::G_LOAD);
+ const auto *Atomic = Find(rcc::GenericOpcode::G_ATOMIC_CMPXCHG);
+ const auto *Branch = Find(rcc::GenericOpcode::G_BRINDIRECT);
+ const auto *Strict = Find(rcc::GenericOpcode::G_STRICT_FMA);
+ const auto *InlineAsm = Find(rcc::GenericOpcode::INLINEASM);
+ Context.expect(
+ Load != nullptr && Load->Flags.has(rcc::InstructionFlag::MayLoad) &&
+ Load->RequiresMemoryOperands && Atomic != nullptr &&
+ Atomic->Flags.has(rcc::InstructionFlag::MayLoad) &&
+ Atomic->Flags.has(rcc::InstructionFlag::MayStore) && Atomic->RequiresMemoryOperands &&
+ Branch != nullptr && Branch->Flags.has(rcc::InstructionFlag::Terminator) &&
+ Branch->Flags.has(rcc::InstructionFlag::IndirectBranch) &&
+ Branch->Flags.has(rcc::InstructionFlag::Barrier) && Strict != nullptr &&
+ Strict->Flags.has(rcc::InstructionFlag::HasSideEffects) && InlineAsm != nullptr &&
+ InlineAsm->Flags.has(rcc::InstructionFlag::TerminalEmission) &&
+ InlineAsm->Owner == rcc::PseudoOwner::AsmPrinter,
+ "memory, control-flow, constrained-FP, and terminal flags must be explicit");
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ testCompleteRegistry(Context);
+ testCoreOpcodeContracts(Context);
+ testShapesAndTypes(Context);
+ testInstructionMemorySemantics(Context);
+ testSemanticFlags(Context);
+ return Context.result();
+}
diff --git a/backend/unittests/CodeGen/MachineAnalysesTest.cpp b/backend/unittests/CodeGen/MachineAnalysesTest.cpp
new file mode 100644
index 0000000..23c5e19
--- /dev/null
+++ b/backend/unittests/CodeGen/MachineAnalysesTest.cpp
@@ -0,0 +1,477 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/CodeGen/MachineAnalyses.h"
+
+#include "Target/TestTarget.h"
+#include "rcc/CodeGen/GenericOpcodes.h"
+#include "rcc/CodeGen/MachineCFGUpdater.h"
+#include "rcc/CodeGen/MachineFunction.h"
+#include "rcc/CodeGen/MachineIRBuilder.h"
+#include "rcc/CodeGen/MachineInstr.h"
+#include "rcc/CodeGen/MachineMemOperand.h"
+#include "rcc/CodeGen/MachineVerifier.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <iostream>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace {
+
+class TestContext final {
+public:
+ void expect(bool Condition, std::string_view Message) {
+ if (Condition)
+ return;
+ std::cerr << Message << '\n';
+ ++Failures;
+ }
+
+ [[nodiscard]] int result() const noexcept {
+ return Failures == 0 ? 0 : 1;
+ }
+
+private:
+ int Failures = 0;
+};
+
+[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() {
+ std::vector<rcc::InstructionDescriptor> Descriptors;
+ std::vector<rcc::ImmediatePredicate> Predicates;
+ auto Appended = rcc::appendGenericInstructionInfo(Descriptors, Predicates);
+ if (!Appended)
+ return std::unexpected(Appended.error());
+ auto Instructions = rcc::InstructionSet::create(std::move(Descriptors), std::move(Predicates));
+ if (!Instructions)
+ return std::unexpected(Instructions.error());
+ return rcc::test::createTestTargetDescription(std::move(*Instructions));
+}
+
+[[nodiscard]] rcc::Expected<rcc::MachineBasicBlock *> appendBlock(rcc::MachineFunction &Function) {
+ auto Block = Function.createBasicBlock();
+ if (!Block)
+ return std::unexpected(Block.error());
+ auto Appended = Function.appendBlock(**Block);
+ if (!Appended)
+ return std::unexpected(Appended.error());
+ return *Block;
+}
+
+void testCFGAnalyses(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Function = rcc::MachineFunction::create("cfg", rcc::MachineFunctionStableID::fromValue(1));
+ Context.expect(Function.has_value(), "CFG analysis function creation must succeed");
+ if (!Function)
+ return;
+
+ auto Entry = appendBlock(**Function);
+ auto Header = appendBlock(**Function);
+ auto Body = appendBlock(**Function);
+ auto Exit = appendBlock(**Function);
+ auto Dead = appendBlock(**Function);
+ Context.expect(Entry && Header && Body && Exit && Dead,
+ "CFG analysis blocks must be appended successfully");
+ if (!Entry || !Header || !Body || !Exit || !Dead)
+ return;
+
+ auto ThreeQuarters = rcc::BranchProbability::fromFraction(3, 4);
+ auto OneQuarter = rcc::BranchProbability::fromFraction(1, 4);
+ auto Updater = (*Function)->cfgUpdater();
+ Context.expect(ThreeQuarters && OneQuarter &&
+ Updater.addSuccessor(**Entry, **Header, rcc::BranchProbability::one()) &&
+ Updater.addSuccessor(**Header, **Body, *ThreeQuarters) &&
+ Updater.addSuccessor(**Header, **Exit, *OneQuarter) &&
+ Updater.addSuccessor(**Body, **Header, rcc::BranchProbability::one()),
+ "reducible loop CFG construction must succeed");
+
+ rcc::MachineAnalysisManager Analyses;
+ Context.expect(rcc::registerStage8MachineAnalyses(Analyses).has_value(),
+ "standard Stage 8 analyses must register atomically");
+ auto Dominators = Analyses.getResult<rcc::MachineDominatorTreeAnalysis>(**Function, Target);
+ auto Loops = Analyses.getResult<rcc::MachineLoopAnalysis>(**Function, Target);
+ auto Probabilities =
+ Analyses.getResult<rcc::MachineBranchProbabilityAnalysis>(**Function, Target);
+ auto Frequencies = Analyses.getResult<rcc::MachineBlockFrequencyAnalysis>(**Function, Target);
+ Context.expect(Dominators && Loops && Probabilities && Frequencies,
+ "CFG analyses and their dependencies must materialize");
+ if (!Dominators || !Loops || !Probabilities || !Frequencies)
+ return;
+
+ Context.expect((*Dominators)->dominates(**Entry, **Exit) &&
+ (*Dominators)->dominates(**Header, **Body) &&
+ (*Dominators)->immediateDominator(**Body) == *Header &&
+ (*Dominators)->nearestCommonDominator(**Body, **Exit) == *Header &&
+ !(*Dominators)->isReachable(**Dead),
+ "dominator analysis must model reachability, IDom, and common dominators");
+ Context.expect((*Loops)->loops().size() == 1 && (*Loops)->loopFor(**Header) != nullptr &&
+ (*Loops)->loopFor(**Body) != nullptr && (*Loops)->loopDepth(**Body) == 1,
+ "loop analysis must recover the natural loop and loop depth");
+ Context.expect((*Probabilities)->probability(**Header, **Body) == *ThreeQuarters &&
+ (*Probabilities)->probability(**Header, **Exit) == *OneQuarter,
+ "branch-probability analysis must retain exact integer edge probabilities");
+ Context.expect((*Frequencies)->frequency(**Entry) != 0 &&
+ (*Frequencies)->frequency(**Header) > (*Frequencies)->frequency(**Entry) &&
+ (*Frequencies)->frequency(**Dead) == 0,
+ "block-frequency analysis must distinguish loop and unreachable blocks");
+
+ auto Alias = Analyses.getResult<rcc::MachineAliasAnalysis>(**Function, Target);
+ auto MemoryDependence =
+ Analyses.getResult<rcc::MachineMemoryDependenceAnalysis>(**Function, Target);
+ Context.expect(Alias && MemoryDependence && Analyses.cachedResultCount(**Function) == 6,
+ "the complete Stage 8 analysis set must materialize into one function cache");
+ Analyses.invalidate(**Function, rcc::PreservedAnalyses::none(),
+ {rcc::MachinePassChange::Registers});
+ Context.expect(Analyses.cachedResultCount(**Function) == 6,
+ "register-only changes must preserve CFG and memory analyses");
+ Analyses.invalidate(**Function, rcc::PreservedAnalyses::none(), {rcc::MachinePassChange::CFG});
+ Context.expect(
+ Analyses.cachedResultCount(**Function) == 1 &&
+ Analyses.hasCached(**Function,
+ rcc::MachineAnalysisManager::id<rcc::MachineAliasAnalysis>()),
+ "CFG changes must invalidate CFG dependents and memory dependence but preserve aliasing");
+
+ auto RefreshedDominators =
+ Analyses.getResult<rcc::MachineDominatorTreeAnalysis>(**Function, Target);
+ Context.expect(
+ RefreshedDominators &&
+ Updater.addSuccessor(**Entry, **Exit, rcc::BranchProbability::zero()).has_value() &&
+ (*RefreshedDominators)->updateAfterCFGChange(**Function).has_value() &&
+ (*RefreshedDominators)->immediateDominator(**Exit) == *Entry,
+ "dominator result must support exact updates after CFG edge changes");
+}
+
+void testIrreducibleCFG(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Function =
+ rcc::MachineFunction::create("irreducible", rcc::MachineFunctionStableID::fromValue(2));
+ if (!Function) {
+ Context.expect(false, "irreducible CFG function creation must succeed");
+ return;
+ }
+ auto Entry = appendBlock(**Function);
+ auto Left = appendBlock(**Function);
+ auto Right = appendBlock(**Function);
+ auto Exit = appendBlock(**Function);
+ if (!Entry || !Left || !Right || !Exit) {
+ Context.expect(false, "irreducible CFG blocks must be appended");
+ return;
+ }
+ auto Half = rcc::BranchProbability::fromFraction(1, 2);
+ auto Updater = (*Function)->cfgUpdater();
+ Context.expect(Half && Updater.addSuccessor(**Entry, **Left, *Half) &&
+ Updater.addSuccessor(**Entry, **Right, *Half) &&
+ Updater.addSuccessor(**Left, **Right, *Half) &&
+ Updater.addSuccessor(**Left, **Exit, *Half) &&
+ Updater.addSuccessor(**Right, **Left, *Half) &&
+ Updater.addSuccessor(**Right, **Exit, *Half),
+ "irreducible CFG construction must succeed");
+
+ rcc::MachineAnalysisManager Analyses;
+ auto Registered = rcc::registerStage8MachineAnalyses(Analyses);
+ auto Loops =
+ Registered
+ ? Analyses.getResult<rcc::MachineLoopAnalysis>(**Function, Target)
+ : rcc::Expected<rcc::MachineLoopAnalysis::Result *>{std::unexpected(Registered.error())};
+ Context.expect(Loops && (*Loops)->irreducibleRegions().size() == 1 &&
+ (*Loops)->irreducibleRegions().front().Entries.size() == 2 &&
+ (*Loops)->isIrreducible(**Left) && (*Loops)->isIrreducible(**Right),
+ "loop analysis must explicitly identify multi-entry irreducible regions");
+}
+
+[[nodiscard]] rcc::MachineMemOperandDesc
+memoryDescription(rcc::MemoryBase Base, std::int64_t Offset, rcc::MemoryAccessFlag Access) {
+ rcc::MachineMemOperandDesc Description;
+ Description.Accesses = {Access};
+ Description.Size = *rcc::MemorySize::fixed(8);
+ Description.Align = *rcc::Alignment::create(8);
+ Description.Base = std::move(Base);
+ Description.Offset = Offset;
+ return Description;
+}
+
+void testAliasAnalysis(TestContext &Context) {
+ auto First = rcc::MachineMemOperand::create(memoryDescription(
+ rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)), 0, rcc::MemoryAccessFlag::Load));
+ auto Same = rcc::MachineMemOperand::create(memoryDescription(
+ rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)), 0, rcc::MemoryAccessFlag::Store));
+ auto Partial = rcc::MachineMemOperand::create(memoryDescription(
+ rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)), 4, rcc::MemoryAccessFlag::Store));
+ auto DisjointRange = rcc::MachineMemOperand::create(
+ memoryDescription(rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)), 16,
+ rcc::MemoryAccessFlag::Store));
+ auto DistinctObject = rcc::MachineMemOperand::create(memoryDescription(
+ rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(1)), 0, rcc::MemoryAccessFlag::Store));
+ auto DifferentObjectKind = rcc::MachineMemOperand::create(
+ memoryDescription(rcc::MemoryBase::constantPool(rcc::ConstantPoolIndex::fromValue(0)), 0,
+ rcc::MemoryAccessFlag::Store));
+ Context.expect(First && Same && Partial && DisjointRange && DistinctObject && DifferentObjectKind,
+ "alias-analysis memory operands must be valid");
+ if (!First || !Same || !Partial || !DisjointRange || !DistinctObject || !DifferentObjectKind)
+ return;
+
+ rcc::MachineAliasAnalysis::Result Alias;
+ Context.expect(Alias.alias(*First, *Same) == rcc::MachineAliasResult::MustAlias &&
+ Alias.alias(*First, *Partial) == rcc::MachineAliasResult::PartialAlias &&
+ Alias.alias(*First, *DisjointRange) == rcc::MachineAliasResult::NoAlias &&
+ Alias.alias(*First, *DistinctObject) == rcc::MachineAliasResult::NoAlias &&
+ Alias.alias(*First, *DifferentObjectKind) == rcc::MachineAliasResult::NoAlias,
+ "alias analysis must distinguish exact, partial, range, and object aliasing");
+}
+
+void testMemoryDependence(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Function =
+ rcc::MachineFunction::create("memory", rcc::MachineFunctionStableID::fromValue(3));
+ if (!Function) {
+ Context.expect(false, "memory-dependence function creation must succeed");
+ return;
+ }
+ auto Block = appendBlock(**Function);
+ auto OtherBlock = appendBlock(**Function);
+ auto I64 = rcc::LLT::makeInteger(64);
+ auto Pointer = rcc::LLT::makePointer(0, 64);
+ auto Value = I64 ? (*Function)->registerInfo().createVirtualRegister(*I64)
+ : rcc::Expected<rcc::Register>{std::unexpected(I64.error())};
+ auto Address = Pointer ? (*Function)->registerInfo().createVirtualRegister(*Pointer)
+ : rcc::Expected<rcc::Register>{std::unexpected(Pointer.error())};
+ if (!Block || !OtherBlock || !Value || !Address) {
+ Context.expect(false, "memory-dependence MachineIR setup must succeed");
+ return;
+ }
+
+ auto StoreMemory = (*Function)->createMemOperand(
+ memoryDescription(rcc::MemoryBase::stack(), 0, rcc::MemoryAccessFlag::Store));
+ auto AliasingLoadMemory = (*Function)->createMemOperand(
+ memoryDescription(rcc::MemoryBase::stack(), 0, rcc::MemoryAccessFlag::Load));
+ auto DisjointLoadMemory = (*Function)->createMemOperand(
+ memoryDescription(rcc::MemoryBase::stack(), 32, rcc::MemoryAccessFlag::Load));
+ auto VolatileStoreDescription =
+ memoryDescription(rcc::MemoryBase::stack(), 64, rcc::MemoryAccessFlag::Store);
+ VolatileStoreDescription.Accesses.set(rcc::MemoryAccessFlag::Volatile);
+ auto VolatileLoadDescription =
+ memoryDescription(rcc::MemoryBase::stack(), 96, rcc::MemoryAccessFlag::Load);
+ VolatileLoadDescription.Accesses.set(rcc::MemoryAccessFlag::Volatile);
+ auto VolatileStoreMemory = (*Function)->createMemOperand(std::move(VolatileStoreDescription));
+ auto VolatileLoadMemory = (*Function)->createMemOperand(std::move(VolatileLoadDescription));
+ auto OtherBlockLoadMemory = (*Function)->createMemOperand(
+ memoryDescription(rcc::MemoryBase::stack(), 0, rcc::MemoryAccessFlag::Load));
+ rcc::MachineIRBuilder Builder(**Function, Target);
+ Context.expect(StoreMemory && AliasingLoadMemory && DisjointLoadMemory && VolatileStoreMemory &&
+ VolatileLoadMemory && OtherBlockLoadMemory && Builder.setInsertPoint(**Block),
+ "memory-dependence builder setup must succeed");
+ if (!StoreMemory || !AliasingLoadMemory || !DisjointLoadMemory || !VolatileStoreMemory ||
+ !VolatileLoadMemory || !OtherBlockLoadMemory)
+ return;
+
+ auto Store = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_STORE));
+ if (!Store || !Store->addUse(*Value) || !Store->addUse(*Address) ||
+ !Store->addMemOperand(**StoreMemory)) {
+ Context.expect(false, "generic store construction must succeed");
+ return;
+ }
+ auto StoreInstruction = Store->build();
+ auto Load = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_LOAD));
+ if (!Load || !Load->addDef(*Value) || !Load->addUse(*Address) ||
+ !Load->addMemOperand(**AliasingLoadMemory)) {
+ Context.expect(false, "aliasing generic load construction must succeed");
+ return;
+ }
+ auto LoadInstruction = Load->build();
+ auto DisjointLoad = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_LOAD));
+ if (!DisjointLoad || !DisjointLoad->addDef(*Value) || !DisjointLoad->addUse(*Address) ||
+ !DisjointLoad->addMemOperand(**DisjointLoadMemory)) {
+ Context.expect(false, "disjoint generic load construction must succeed");
+ return;
+ }
+ auto DisjointLoadInstruction = DisjointLoad->build();
+ auto VolatileStore = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_STORE));
+ if (!VolatileStore || !VolatileStore->addUse(*Value) || !VolatileStore->addUse(*Address) ||
+ !VolatileStore->addMemOperand(**VolatileStoreMemory)) {
+ Context.expect(false, "volatile store construction must succeed");
+ return;
+ }
+ auto VolatileStoreInstruction = VolatileStore->build();
+ auto VolatileLoad = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_LOAD));
+ if (!VolatileLoad || !VolatileLoad->addDef(*Value) || !VolatileLoad->addUse(*Address) ||
+ !VolatileLoad->addMemOperand(**VolatileLoadMemory)) {
+ Context.expect(false, "volatile load construction must succeed");
+ return;
+ }
+ auto VolatileLoadInstruction = VolatileLoad->build();
+ if (!Builder.setInsertPoint(**OtherBlock)) {
+ Context.expect(false, "second memory region insertion point must be valid");
+ return;
+ }
+ auto OtherBlockLoad = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_LOAD));
+ if (!OtherBlockLoad || !OtherBlockLoad->addDef(*Value) || !OtherBlockLoad->addUse(*Address) ||
+ !OtherBlockLoad->addMemOperand(**OtherBlockLoadMemory)) {
+ Context.expect(false, "second-region generic load construction must succeed");
+ return;
+ }
+ auto OtherBlockLoadInstruction = OtherBlockLoad->build();
+ Context.expect(StoreInstruction && LoadInstruction && DisjointLoadInstruction &&
+ VolatileStoreInstruction && VolatileLoadInstruction &&
+ OtherBlockLoadInstruction,
+ "memory instructions must be published transactionally");
+ if (!StoreInstruction || !LoadInstruction || !DisjointLoadInstruction ||
+ !VolatileStoreInstruction || !VolatileLoadInstruction || !OtherBlockLoadInstruction)
+ return;
+
+ rcc::MachineAnalysisManager Analyses;
+ auto Registered = rcc::registerStage8MachineAnalyses(Analyses);
+ auto Dependencies =
+ Registered ? Analyses.getResult<rcc::MachineMemoryDependenceAnalysis>(**Function, Target)
+ : rcc::Expected<rcc::MachineMemoryDependenceAnalysis::Result *>{
+ std::unexpected(Registered.error())};
+ Context.expect(Dependencies && (*Dependencies)->dependencies(**LoadInstruction).size() == 1 &&
+ (*Dependencies)->dependencies(**LoadInstruction).front().Source ==
+ (*StoreInstruction)->stableID() &&
+ (*Dependencies)->dependencies(**DisjointLoadInstruction).empty(),
+ "memory dependence must add flow edges only for potentially aliasing accesses");
+ Context.expect(
+ Dependencies && (*Dependencies)->regions().size() == 2 &&
+ (*Dependencies)->regionFor(**Block) != nullptr &&
+ (*Dependencies)->regionFor(**Block)->Instructions.size() == 5 &&
+ (*Dependencies)->regionFor(**OtherBlock) != nullptr &&
+ (*Dependencies)->regionFor(**OtherBlock)->Instructions.size() == 1 &&
+ (*Dependencies)->dependencies(**OtherBlockLoadInstruction).empty() &&
+ (*Dependencies)->dependencies(**OtherBlock).empty(),
+ "memory dependence must expose deterministic block regions without cross-CFG edges");
+ bool HasOrderedVolatileDependency = false;
+ if (Dependencies) {
+ HasOrderedVolatileDependency =
+ std::ranges::any_of((*Dependencies)->dependencies(**VolatileLoadInstruction),
+ [&](const rcc::MachineMemoryDependency &Dependency) {
+ return Dependency.Source == (*VolatileStoreInstruction)->stableID();
+ });
+ }
+ Context.expect(HasOrderedVolatileDependency,
+ "ordered memory accesses must retain dependencies across disjoint addresses");
+}
+
+class ConflictingAnalysis final {
+public:
+ struct Result final {};
+ [[nodiscard]] static rcc::Expected<Result>
+ run(rcc::MachineFunction &, const rcc::TargetDescription &, rcc::MachineAnalysisManager &) {
+ return Result{};
+ }
+};
+
+void testAtomicStandardRegistration(TestContext &Context) {
+ rcc::MachineAnalysisManager Analyses;
+ Context.expect(Analyses
+ .registerAnalysis<ConflictingAnalysis>({
+ .Name = "MachineLoopInfo",
+ .Dependencies = {},
+ })
+ .has_value(),
+ "analysis-name conflict setup must succeed");
+ Context.expect(!rcc::registerStage8MachineAnalyses(Analyses) &&
+ !Analyses.isRegistered(
+ rcc::MachineAnalysisManager::id<rcc::MachineDominatorTreeAnalysis>()),
+ "failed standard-analysis registration must not publish a partial set");
+}
+
+void testCFGUpdaterPHIs(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Function =
+ rcc::MachineFunction::create("cfg_phi", rcc::MachineFunctionStableID::fromValue(4));
+ if (!Function) {
+ Context.expect(false, "CFG PHI function creation must succeed");
+ return;
+ }
+ auto Source = appendBlock(**Function);
+ auto Other = appendBlock(**Function);
+ auto Merge = appendBlock(**Function);
+ auto Unconnected = appendBlock(**Function);
+ if (!Source || !Other || !Merge || !Unconnected) {
+ Context.expect(false, "CFG PHI blocks must be appended");
+ return;
+ }
+ auto Updater = (*Function)->cfgUpdater();
+ if (!Updater.addSuccessor(**Source, **Merge, rcc::BranchProbability::one()) ||
+ !Updater.addSuccessor(**Other, **Merge, rcc::BranchProbability::one())) {
+ Context.expect(false, "CFG PHI predecessor edges must be created");
+ return;
+ }
+
+ auto I64 = rcc::LLT::makeInteger(64);
+ auto Destination = I64 ? (*Function)->registerInfo().createVirtualRegister(*I64)
+ : rcc::Expected<rcc::Register>{std::unexpected(I64.error())};
+ auto SourceValue = I64 ? (*Function)->registerInfo().createVirtualRegister(*I64)
+ : rcc::Expected<rcc::Register>{std::unexpected(I64.error())};
+ auto OtherValue = I64 ? (*Function)->registerInfo().createVirtualRegister(*I64)
+ : rcc::Expected<rcc::Register>{std::unexpected(I64.error())};
+ if (!Destination || !SourceValue || !OtherValue) {
+ Context.expect(false, "CFG PHI registers must be created");
+ return;
+ }
+
+ rcc::MachineIRBuilder Builder(**Function, Target);
+ if (!Builder.setInsertPoint(**Source))
+ return;
+ auto SourceDefinition = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_IMPLICIT_DEF));
+ if (!SourceDefinition || !SourceDefinition->addDef(*SourceValue) || !SourceDefinition->build()) {
+ Context.expect(false, "CFG PHI source definition must build");
+ return;
+ }
+ auto Branch = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_BR));
+ if (!Branch || !Branch->addBasicBlock(**Merge)) {
+ Context.expect(false, "CFG PHI branch must be assembled");
+ return;
+ }
+ auto BranchInstruction = Branch->build();
+ if (!BranchInstruction || !Builder.setInsertPoint(**Merge)) {
+ Context.expect(false, "CFG PHI branch must publish");
+ return;
+ }
+ auto Phi = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_PHI));
+ if (!Phi || !Phi->addDef(*Destination) || !Phi->addUse(*SourceValue) ||
+ !Phi->addBasicBlock(**Source) || !Phi->addUse(*OtherValue) || !Phi->addBasicBlock(**Other)) {
+ Context.expect(false, "CFG PHI instruction must be assembled");
+ return;
+ }
+ auto PhiInstruction = Phi->build();
+ Context.expect(PhiInstruction && rcc::MachineVerifier::verify(**Function, Target),
+ "initial CFG PHI function must verify");
+ if (!PhiInstruction)
+ return;
+
+ auto Split = Updater.splitBlock(**Source, **BranchInstruction);
+ Context.expect(
+ Split && (*Split)->predecessors().size() == 1 &&
+ (*Split)->predecessors().front() == *Source && (*Split)->successors().size() == 1 &&
+ (*PhiInstruction)->operandAt(2)->getIf<rcc::BasicBlockOperand>()->Block == *Split &&
+ rcc::MachineVerifier::verify(**Function, Target),
+ "block split must redirect CFG edges and PHI predecessor identity");
+ if (!Split)
+ return;
+
+ Context.expect(Updater.removeSuccessor(**Other, **Merge) &&
+ (*PhiInstruction)->operandCount() == 3 &&
+ rcc::MachineVerifier::verify(**Function, Target),
+ "edge deletion must remove the corresponding PHI incoming pair");
+ Context.expect(!Updater.addSuccessor(**Unconnected, **Merge, rcc::BranchProbability::one()),
+ "new PHI predecessors without incoming values must be rejected");
+ Context.expect(Updater.deleteBlock(**Other) && !(*Other)->isLinked() &&
+ rcc::MachineVerifier::verify(**Function, Target),
+ "CFG block deletion must detach a disconnected block cleanly");
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ auto Target = createTarget();
+ Context.expect(Target.has_value(), "machine analysis tests require a valid target");
+ if (!Target)
+ return Context.result();
+
+ testCFGAnalyses(Context, *Target);
+ testIrreducibleCFG(Context, *Target);
+ testAliasAnalysis(Context);
+ testMemoryDependence(Context, *Target);
+ testCFGUpdaterPHIs(Context, *Target);
+ testAtomicStandardRegistration(Context);
+ return Context.result();
+}
diff --git a/backend/unittests/CodeGen/MachinePassTest.cpp b/backend/unittests/CodeGen/MachinePassTest.cpp
new file mode 100644
index 0000000..fb125a4
--- /dev/null
+++ b/backend/unittests/CodeGen/MachinePassTest.cpp
@@ -0,0 +1,918 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/CodeGen/MachinePass.h"
+
+#include "Target/TestTarget.h"
+#include "rcc/CodeGen/CompilationUnit.h"
+#include "rcc/CodeGen/GenericOpcodes.h"
+#include "rcc/CodeGen/MachineFunction.h"
+
+#include <algorithm>
+#include <atomic>
+#include <chrono>
+#include <cstddef>
+#include <cstdint>
+#include <iostream>
+#include <memory>
+#include <ranges>
+#include <string>
+#include <string_view>
+#include <thread>
+#include <utility>
+#include <vector>
+
+namespace {
+
+class TestContext final {
+public:
+ void expect(bool Condition, std::string_view Message) {
+ if (Condition)
+ return;
+ std::cerr << Message << '\n';
+ ++Failures;
+ }
+
+ [[nodiscard]] int result() const noexcept {
+ return Failures == 0 ? 0 : 1;
+ }
+
+private:
+ int Failures = 0;
+};
+
+[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() {
+ std::vector<rcc::InstructionDescriptor> Descriptors;
+ std::vector<rcc::ImmediatePredicate> Predicates;
+ auto Appended = rcc::appendGenericInstructionInfo(Descriptors, Predicates);
+ if (!Appended)
+ return std::unexpected(Appended.error());
+ auto Instructions = rcc::InstructionSet::create(std::move(Descriptors), std::move(Predicates));
+ if (!Instructions)
+ return std::unexpected(Instructions.error());
+ return rcc::test::createTestTargetDescription(std::move(*Instructions));
+}
+
+class BlockCountAnalysis final {
+public:
+ struct Result final {
+ std::size_t Count = 0;
+ };
+
+ [[nodiscard]] static constexpr std::string_view name() noexcept {
+ return "BlockCount";
+ }
+
+ [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &,
+ rcc::MachineAnalysisManager &) {
+ ++Runs;
+ return Result{Function.blockCount()};
+ }
+
+ static inline std::atomic<std::uint32_t> Runs = 0;
+};
+
+class DependentAnalysis final {
+public:
+ struct Result final {
+ std::size_t Count = 0;
+ };
+
+ [[nodiscard]] static constexpr std::string_view name() noexcept {
+ return "Dependent";
+ }
+
+ [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target,
+ rcc::MachineAnalysisManager &Analyses) {
+ auto Blocks = Analyses.getResult<BlockCountAnalysis>(Function, Target);
+ if (!Blocks)
+ return std::unexpected(Blocks.error());
+ return Result{(*Blocks)->Count};
+ }
+};
+
+class NonThreadSafeAnalysis final {
+public:
+ struct Result final {};
+
+ [[nodiscard]] static constexpr std::string_view name() noexcept {
+ return "NonThreadSafe";
+ }
+
+ [[nodiscard]] static rcc::Expected<Result>
+ run(rcc::MachineFunction &, const rcc::TargetDescription &, rcc::MachineAnalysisManager &) {
+ return Result{};
+ }
+};
+
+class FailingAnalysis final {
+public:
+ struct Result final {};
+
+ [[nodiscard]] static constexpr std::string_view name() noexcept {
+ return "Failing";
+ }
+
+ [[nodiscard]] static rcc::Expected<Result>
+ run(rcc::MachineFunction &, const rcc::TargetDescription &, rcc::MachineAnalysisManager &) {
+ return rcc::makeUnexpected(rcc::ErrorCode::VerificationError,
+ "intentional machine analysis failure");
+ }
+};
+
+class StickyAnalysis final {
+public:
+ struct Result final {
+ [[nodiscard]] bool invalidate(const rcc::MachineFunction &,
+ const rcc::PreservedAnalyses &) noexcept {
+ return false;
+ }
+ };
+
+ [[nodiscard]] static constexpr std::string_view name() noexcept {
+ return "Sticky";
+ }
+ [[nodiscard]] static rcc::Expected<Result>
+ run(rcc::MachineFunction &, const rcc::TargetDescription &, rcc::MachineAnalysisManager &) {
+ return Result{};
+ }
+};
+
+class ExpiringAnalysis final {
+public:
+ struct Result final {
+ [[nodiscard]] bool invalidate(const rcc::MachineFunction &,
+ const rcc::PreservedAnalyses &) noexcept {
+ return true;
+ }
+ };
+
+ [[nodiscard]] static constexpr std::string_view name() noexcept {
+ return "Expiring";
+ }
+ [[nodiscard]] static rcc::Expected<Result>
+ run(rcc::MachineFunction &, const rcc::TargetDescription &, rcc::MachineAnalysisManager &) {
+ return Result{};
+ }
+};
+
+class CycleA final {
+public:
+ struct Result final {};
+ [[nodiscard]] static constexpr std::string_view name() noexcept {
+ return "CycleA";
+ }
+ [[nodiscard]] static rcc::Expected<Result>
+ run(rcc::MachineFunction &, const rcc::TargetDescription &, rcc::MachineAnalysisManager &) {
+ return Result{};
+ }
+};
+
+class CycleB final {
+public:
+ struct Result final {};
+ [[nodiscard]] static constexpr std::string_view name() noexcept {
+ return "CycleB";
+ }
+ [[nodiscard]] static rcc::Expected<Result>
+ run(rcc::MachineFunction &, const rcc::TargetDescription &, rcc::MachineAnalysisManager &) {
+ return Result{};
+ }
+};
+
+class RequiredAnalysisPass final : public rcc::MachineFunctionPass {
+public:
+ [[nodiscard]] const rcc::MachinePassContract &contract() const noexcept override {
+ static const rcc::MachinePassContract Contract{
+ .Name = "RequiredAnalysis",
+ .Requires = {},
+ .Sets = {},
+ .Clears = {},
+ .RequiredAnalyses = {rcc::MachineAnalysisManager::id<DependentAnalysis>()},
+ .VerificationStage = std::nullopt,
+ };
+ return Contract;
+ }
+
+ [[nodiscard]] rcc::Expected<rcc::MachinePassResult>
+ run(rcc::MachineFunction &Function, const rcc::TargetDescription &Target,
+ rcc::MachineAnalysisManager &Analyses) override {
+ auto Dependent = Analyses.getResult<DependentAnalysis>(Function, Target);
+ if (!Dependent)
+ return std::unexpected(Dependent.error());
+ auto FirstAction = Analyses.requestAction("first", Function);
+ auto SecondAction = Analyses.requestAction("second", Function);
+ if (!FirstAction || !SecondAction || !*FirstAction || *SecondAction) {
+ return rcc::makeUnexpected(rcc::ErrorCode::VerificationError,
+ "per-function action fuel was not enforced");
+ }
+ rcc::MachinePassResult Result;
+ Result.Preserved = rcc::PreservedAnalyses::all();
+ return Result;
+ }
+};
+
+class UnreportedChangePass final : public rcc::MachineFunctionPass {
+public:
+ [[nodiscard]] const rcc::MachinePassContract &contract() const noexcept override {
+ static const rcc::MachinePassContract Contract{
+ .Name = "UnreportedChange",
+ .Requires = {},
+ .Sets = {},
+ .Clears = {},
+ .RequiredAnalyses = {},
+ .VerificationStage = std::nullopt,
+ };
+ return Contract;
+ }
+
+ [[nodiscard]] rcc::Expected<rcc::MachinePassResult> run(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &,
+ rcc::MachineAnalysisManager &) override {
+ auto Block = Function.createBasicBlock();
+ if (!Block)
+ return std::unexpected(Block.error());
+ auto Appended = Function.appendBlock(**Block);
+ if (!Appended)
+ return std::unexpected(Appended.error());
+ return rcc::MachinePassResult{};
+ }
+};
+
+class RequiresFailingAnalysisPass final : public rcc::MachineFunctionPass {
+public:
+ [[nodiscard]] const rcc::MachinePassContract &contract() const noexcept override {
+ static const rcc::MachinePassContract Contract{
+ .Name = "RequiresFailingAnalysis",
+ .Requires = {},
+ .Sets = {},
+ .Clears = {},
+ .RequiredAnalyses = {rcc::MachineAnalysisManager::id<FailingAnalysis>()},
+ .VerificationStage = std::nullopt,
+ };
+ return Contract;
+ }
+
+ [[nodiscard]] rcc::Expected<rcc::MachinePassResult> run(rcc::MachineFunction &,
+ const rcc::TargetDescription &,
+ rcc::MachineAnalysisManager &) override {
+ return rcc::MachinePassResult{};
+ }
+};
+
+class MutatingFailurePass final : public rcc::MachineFunctionPass {
+public:
+ [[nodiscard]] const rcc::MachinePassContract &contract() const noexcept override {
+ static const rcc::MachinePassContract Contract{
+ .Name = "MutatingFailure",
+ .Requires = {},
+ .Sets = {},
+ .Clears = {},
+ .RequiredAnalyses = {rcc::MachineAnalysisManager::id<BlockCountAnalysis>()},
+ .VerificationStage = std::nullopt,
+ };
+ return Contract;
+ }
+
+ [[nodiscard]] rcc::Expected<rcc::MachinePassResult> run(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &,
+ rcc::MachineAnalysisManager &) override {
+ auto Block = Function.createBasicBlock();
+ if (!Block)
+ return std::unexpected(Block.error());
+ auto Appended = Function.appendBlock(**Block);
+ if (!Appended)
+ return std::unexpected(Appended.error());
+ return rcc::makeUnexpected(rcc::ErrorCode::VerificationError,
+ "intentional mutating pass failure");
+ }
+};
+
+class ParallelCountingPass final : public rcc::MachineFunctionPass {
+public:
+ explicit ParallelCountingPass(std::atomic<std::uint32_t> &Count) : Count(&Count) {}
+
+ [[nodiscard]] const rcc::MachinePassContract &contract() const noexcept override {
+ static const rcc::MachinePassContract Contract = [] {
+ rcc::MachinePassContract Value{
+ .Name = "ParallelCounting",
+ .Requires = {},
+ .Sets = {},
+ .Clears = {},
+ .RequiredAnalyses = {},
+ .VerificationStage = std::nullopt,
+ };
+ Value.ThreadSafety = rcc::PassThreadSafety::ThreadSafe;
+ return Value;
+ }();
+ return Contract;
+ }
+
+ [[nodiscard]] rcc::Expected<rcc::MachinePassResult> run(rcc::MachineFunction &,
+ const rcc::TargetDescription &,
+ rcc::MachineAnalysisManager &) override {
+ Count->fetch_add(1, std::memory_order_relaxed);
+ return rcc::MachinePassResult{};
+ }
+
+private:
+ std::atomic<std::uint32_t> *Count;
+};
+
+class InstrumentedParallelPass final : public rcc::MachineFunctionPass {
+public:
+ InstrumentedParallelPass(std::atomic<std::uint32_t> &Runs, std::atomic<std::uint32_t> &Active,
+ std::atomic<std::uint32_t> &MaximumActive)
+ : Runs(&Runs), Active(&Active), MaximumActive(&MaximumActive) {}
+
+ [[nodiscard]] const rcc::MachinePassContract &contract() const noexcept override {
+ static const rcc::MachinePassContract Contract = [] {
+ rcc::MachinePassContract Value{
+ .Name = "InstrumentedParallel",
+ .Requires = {},
+ .Sets = {},
+ .Clears = {},
+ .RequiredAnalyses = {},
+ .VerificationStage = std::nullopt,
+ };
+ Value.ThreadSafety = rcc::PassThreadSafety::ThreadSafe;
+ Value.DeclaredChanges = {rcc::MachinePassChange::BlockLayout};
+ return Value;
+ }();
+ return Contract;
+ }
+
+ [[nodiscard]] rcc::Expected<rcc::MachinePassResult> run(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &,
+ rcc::MachineAnalysisManager &) override {
+ const std::uint32_t Current = Active->fetch_add(1, std::memory_order_acq_rel) + 1U;
+ std::uint32_t Maximum = MaximumActive->load(std::memory_order_relaxed);
+ while (Maximum < Current &&
+ !MaximumActive->compare_exchange_weak(Maximum, Current, std::memory_order_relaxed)) {
+ }
+ std::this_thread::sleep_for(std::chrono::milliseconds(2));
+ auto Block = Function.createBasicBlock();
+ if (!Block) {
+ Active->fetch_sub(1, std::memory_order_acq_rel);
+ return std::unexpected(Block.error());
+ }
+ auto Appended = Function.appendBlock(**Block);
+ Active->fetch_sub(1, std::memory_order_acq_rel);
+ if (!Appended)
+ return std::unexpected(Appended.error());
+ Runs->fetch_add(1, std::memory_order_relaxed);
+ rcc::MachinePassResult Result;
+ Result.Changed = true;
+ Result.Changes.add(rcc::MachinePassChange::BlockLayout);
+ return Result;
+ }
+
+private:
+ std::atomic<std::uint32_t> *Runs;
+ std::atomic<std::uint32_t> *Active;
+ std::atomic<std::uint32_t> *MaximumActive;
+};
+
+class RequiresNonThreadSafeAnalysisPass final : public rcc::MachineFunctionPass {
+public:
+ [[nodiscard]] const rcc::MachinePassContract &contract() const noexcept override {
+ static const rcc::MachinePassContract Contract = [] {
+ rcc::MachinePassContract Value{
+ .Name = "RequiresNonThreadSafe",
+ .Requires = {},
+ .Sets = {},
+ .Clears = {},
+ .RequiredAnalyses =
+ {
+ rcc::MachineAnalysisManager::id<NonThreadSafeAnalysis>(),
+ },
+ .VerificationStage = std::nullopt,
+ };
+ Value.ThreadSafety = rcc::PassThreadSafety::ThreadSafe;
+ return Value;
+ }();
+ return Contract;
+ }
+
+ [[nodiscard]] rcc::Expected<rcc::MachinePassResult> run(rcc::MachineFunction &,
+ const rcc::TargetDescription &,
+ rcc::MachineAnalysisManager &) override {
+ return rcc::MachinePassResult{};
+ }
+};
+
+class UnitFunctionCountAnalysis final {
+public:
+ struct Result final {
+ std::size_t Count = 0;
+ };
+ [[nodiscard]] static constexpr std::string_view name() noexcept {
+ return "UnitFunctionCount";
+ }
+ [[nodiscard]] static rcc::Expected<Result> run(rcc::CompilationUnit &Unit,
+ const rcc::TargetDescription &,
+ rcc::CompilationUnitAnalysisManager &) {
+ return Result{Unit.functionCount()};
+ }
+};
+
+class FailingUnitAnalysis final {
+public:
+ struct Result final {};
+
+ [[nodiscard]] static constexpr std::string_view name() noexcept {
+ return "FailingUnit";
+ }
+
+ [[nodiscard]] static rcc::Expected<Result> run(rcc::CompilationUnit &,
+ const rcc::TargetDescription &,
+ rcc::CompilationUnitAnalysisManager &) {
+ return rcc::makeUnexpected(rcc::ErrorCode::VerificationError,
+ "intentional compilation-unit analysis failure");
+ }
+};
+
+class RequiresFailingUnitAnalysisPass final : public rcc::CompilationUnitPass {
+public:
+ [[nodiscard]] const rcc::CompilationUnitPassContract &contract() const noexcept override {
+ static const rcc::CompilationUnitPassContract Contract{
+ .Name = "RequiresFailingUnitAnalysis",
+ .RequiredAnalyses =
+ {
+ rcc::CompilationUnitAnalysisManager::id<FailingUnitAnalysis>(),
+ },
+ .Parameters = {},
+ .DeclaredChanges = {},
+ };
+ return Contract;
+ }
+
+ [[nodiscard]] rcc::Expected<rcc::CompilationUnitPassResult>
+ run(rcc::CompilationUnit &, const rcc::TargetDescription &, rcc::CompilationUnitAnalysisManager &,
+ rcc::MachineAnalysisManager &) override {
+ return rcc::CompilationUnitPassResult{};
+ }
+};
+
+class ObserveUnitPass final : public rcc::CompilationUnitPass {
+public:
+ [[nodiscard]] const rcc::CompilationUnitPassContract &contract() const noexcept override {
+ static const rcc::CompilationUnitPassContract Contract{
+ .Name = "ObserveUnit",
+ .RequiredAnalyses = {rcc::CompilationUnitAnalysisManager::id<UnitFunctionCountAnalysis>()},
+ .Parameters = {},
+ .DeclaredChanges = {},
+ };
+ return Contract;
+ }
+
+ [[nodiscard]] rcc::Expected<rcc::CompilationUnitPassResult>
+ run(rcc::CompilationUnit &Unit, const rcc::TargetDescription &Target,
+ rcc::CompilationUnitAnalysisManager &Analyses, rcc::MachineAnalysisManager &) override {
+ auto Count = Analyses.getResult<UnitFunctionCountAnalysis>(Unit, Target);
+ if (!Count)
+ return std::unexpected(Count.error());
+ if ((*Count)->Count != Unit.functionCount()) {
+ return rcc::makeUnexpected(rcc::ErrorCode::VerificationError,
+ "unit analysis returned a stale function count");
+ }
+ return rcc::CompilationUnitPassResult{};
+ }
+};
+
+class MutatingUnitFailurePass final : public rcc::CompilationUnitPass {
+public:
+ [[nodiscard]] const rcc::CompilationUnitPassContract &contract() const noexcept override {
+ static const rcc::CompilationUnitPassContract Contract{
+ .Name = "MutatingUnitFailure",
+ .RequiredAnalyses = {rcc::CompilationUnitAnalysisManager::id<UnitFunctionCountAnalysis>()},
+ .Parameters = {},
+ .DeclaredChanges = {rcc::MachinePassChange::BlockLayout},
+ };
+ return Contract;
+ }
+
+ [[nodiscard]] rcc::Expected<rcc::CompilationUnitPassResult>
+ run(rcc::CompilationUnit &Unit, const rcc::TargetDescription &,
+ rcc::CompilationUnitAnalysisManager &, rcc::MachineAnalysisManager &) override {
+ auto Created = Unit.createMachineFunction("added-before-failure");
+ if (!Created)
+ return std::unexpected(Created.error());
+ return rcc::makeUnexpected(rcc::ErrorCode::VerificationError,
+ "intentional compilation-unit pass failure");
+ }
+};
+
+void testAnalysisInfrastructure(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Function =
+ rcc::MachineFunction::create("analysis", rcc::MachineFunctionStableID::fromValue(1));
+ Context.expect(Function.has_value(), "analysis function creation must succeed");
+ if (!Function)
+ return;
+
+ rcc::MachineAnalysisManager Analyses;
+ Context.expect(Analyses
+ .registerAnalysis<BlockCountAnalysis>({
+ .Name = "BlockCount",
+ .Dependencies = {},
+ .InvalidatedBy = {rcc::MachinePassChange::CFG},
+ })
+ .has_value(),
+ "base analysis registration must succeed");
+ Context.expect(Analyses
+ .registerAnalysis<DependentAnalysis>({
+ .Name = "Dependent",
+ .Dependencies = {rcc::MachineAnalysisManager::id<BlockCountAnalysis>()},
+ .InvalidatedBy = {rcc::MachinePassChange::Operands},
+ })
+ .has_value(),
+ "dependent analysis registration must succeed");
+
+ BlockCountAnalysis::Runs = 0;
+ auto First = Analyses.getResult<DependentAnalysis>(**Function, Target);
+ auto Second = Analyses.getResult<DependentAnalysis>(**Function, Target);
+ Context.expect(First && Second && *First == *Second && BlockCountAnalysis::Runs == 1,
+ "analysis dependencies must materialize once and then use the cache");
+
+ Analyses.invalidate(**Function, rcc::PreservedAnalyses::none(),
+ {rcc::MachinePassChange::Registers});
+ Context.expect(Analyses.cachedResultCount(**Function) == 2,
+ "unrelated changes must preserve cached analyses precisely");
+ Analyses.invalidate(**Function, rcc::PreservedAnalyses::none(), {rcc::MachinePassChange::CFG});
+ Context.expect(Analyses.cachedResultCount(**Function) == 0,
+ "invalidating a dependency must invalidate its dependent analysis");
+
+ rcc::MachineAnalysisManager Cyclic;
+ Context.expect(Cyclic
+ .registerAnalysis<CycleA>({
+ .Name = "CycleA",
+ .Dependencies = {rcc::MachineAnalysisManager::id<CycleB>()},
+ })
+ .has_value() &&
+ Cyclic
+ .registerAnalysis<CycleB>({
+ .Name = "CycleB",
+ .Dependencies = {rcc::MachineAnalysisManager::id<CycleA>()},
+ })
+ .has_value(),
+ "cyclic analysis registrations must be accepted until requested");
+ Context.expect(!Cyclic.getResult<CycleA>(**Function, Target),
+ "analysis dependency cycles must produce a structured error");
+
+ rcc::MachineAnalysisManager CustomInvalidation;
+ Context.expect(CustomInvalidation
+ .registerAnalysis<StickyAnalysis>({
+ .Name = "Sticky",
+ .Dependencies = {},
+ .InvalidatedBy = {rcc::MachinePassChange::CFG},
+ })
+ .has_value() &&
+ CustomInvalidation
+ .registerAnalysis<ExpiringAnalysis>({
+ .Name = "Expiring",
+ .Dependencies = {},
+ .InvalidatedBy = {rcc::MachinePassChange::CFG},
+ })
+ .has_value() &&
+ CustomInvalidation.getResult<StickyAnalysis>(**Function, Target) &&
+ CustomInvalidation.getResult<ExpiringAnalysis>(**Function, Target),
+ "custom invalidation analyses must materialize");
+ CustomInvalidation.invalidate(**Function, rcc::PreservedAnalyses::none(),
+ {rcc::MachinePassChange::CFG});
+ Context.expect(
+ CustomInvalidation.hasCached(**Function, rcc::MachineAnalysisManager::id<StickyAnalysis>()) &&
+ !CustomInvalidation.hasCached(**Function,
+ rcc::MachineAnalysisManager::id<ExpiringAnalysis>()),
+ "analysis invalidate() must return false to preserve and true to invalidate");
+}
+
+void testPassAndInstrumentation(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Function = rcc::MachineFunction::create("pass", rcc::MachineFunctionStableID::fromValue(2));
+ Context.expect(Function.has_value(), "pass function creation must succeed");
+ if (!Function)
+ return;
+
+ rcc::MachineAnalysisManager Analyses;
+ Context.expect(
+ Analyses
+ .registerAnalysis<BlockCountAnalysis>({
+ .Name = "BlockCount",
+ .Dependencies = {},
+ .InvalidatedBy = rcc::MachinePassChanges::all(),
+ .ThreadSafety = rcc::PassThreadSafety::ThreadSafe,
+ })
+ .has_value() &&
+ Analyses
+ .registerAnalysis<DependentAnalysis>({
+ .Name = "Dependent",
+ .Dependencies = {rcc::MachineAnalysisManager::id<BlockCountAnalysis>()},
+ })
+ .has_value(),
+ "pass analysis registrations must succeed");
+
+ rcc::MachinePassInstrumentation Instrumentation;
+ Instrumentation.setPerFunctionFuel(1);
+ std::uint32_t BeforePass = 0;
+ std::uint32_t AfterPass = 0;
+ std::uint32_t BeforeAnalysis = 0;
+ std::uint32_t AfterAnalysis = 0;
+ std::uint32_t AnalysisCacheHits = 0;
+ std::uint32_t Printed = 0;
+ std::uint32_t ChangeEvents = 0;
+ std::uint32_t FailureEvents = 0;
+ Instrumentation.addBeforeCallback(
+ [&BeforePass](std::string_view, const rcc::MachineFunction &) { ++BeforePass; });
+ Instrumentation.addAfterCallback([&AfterPass](std::string_view, const rcc::MachineFunction &,
+ const rcc::MachinePassResult &) { ++AfterPass; });
+ Instrumentation.addBeforeAnalysisCallback(
+ [&BeforeAnalysis](std::string_view, const rcc::MachineFunction &) { ++BeforeAnalysis; });
+ Instrumentation.addAfterAnalysisCallback(
+ [&AfterAnalysis, &AnalysisCacheHits](std::string_view, const rcc::MachineFunction &,
+ bool CacheHit, std::uint64_t) {
+ ++AfterAnalysis;
+ AnalysisCacheHits += CacheHit ? 1U : 0U;
+ });
+ Instrumentation.addIRPrintCallback([&Printed](rcc::InstrumentationPhase, std::string_view,
+ const rcc::MachineFunction &) { ++Printed; });
+ Instrumentation.addChangeCallback([&ChangeEvents](std::string_view, const rcc::MachineFunction &,
+ rcc::MachinePassChanges Changes,
+ bool FingerprintChanged) {
+ if (Changes.empty() && !FingerprintChanged)
+ ++ChangeEvents;
+ });
+ Instrumentation.addFailureCallback([&FailureEvents](std::string_view,
+ const rcc::MachineFunction &,
+ const rcc::Error &) { ++FailureEvents; });
+
+ rcc::MachinePassManager Pipeline;
+ Context.expect(Pipeline.addPass(std::make_unique<RequiredAnalysisPass>()).has_value(),
+ "a valid pass must be accepted");
+ auto Run = Pipeline.run(**Function, Target, Analyses, &Instrumentation);
+ Context.expect(Run.has_value(), "a pass with a materialized required analysis must run");
+ Context.expect(BeforePass == 1 && AfterPass == 1 && BeforeAnalysis == 2 && AfterAnalysis == 4 &&
+ AnalysisCacheHits == 2 && Printed == 2 && ChangeEvents == 1,
+ "pass, analysis, cache-hit, IR-print, and change callbacks must all run");
+ const auto Statistics = Instrumentation.statistics();
+ Context.expect(Statistics.size() == 3,
+ "instrumentation must collect one pass and two analysis statistics");
+
+ auto ChangedFunction =
+ rcc::MachineFunction::create("unreported", rcc::MachineFunctionStableID::fromValue(3));
+ rcc::MachineAnalysisManager EmptyAnalyses;
+ rcc::MachinePassManager Detecting(
+ {.VerifyBefore = true, .VerifyEach = true, .DetectUnreportedChanges = true});
+ Context.expect(ChangedFunction &&
+ Detecting.addPass(std::make_unique<UnreportedChangePass>()).has_value() &&
+ !Detecting.run(**ChangedFunction, Target, EmptyAnalyses, &Instrumentation) &&
+ FailureEvents == 1,
+ "change detection must reject an unreported MachineIR mutation");
+
+ auto FailedFunction =
+ rcc::MachineFunction::create("failed", rcc::MachineFunctionStableID::fromValue(4));
+ rcc::MachineAnalysisManager FailureAnalyses;
+ Context.expect(FailedFunction && FailureAnalyses
+ .registerAnalysis<BlockCountAnalysis>({
+ .Name = "BlockCount",
+ .Dependencies = {},
+ .InvalidatedBy = rcc::MachinePassChanges::all(),
+ })
+ .has_value(),
+ "mutating failure analysis fixtures must be valid");
+ rcc::MachinePassManager FailingPipeline;
+ Context.expect(FailedFunction &&
+ FailingPipeline.addPass(std::make_unique<MutatingFailurePass>()).has_value() &&
+ !FailingPipeline.run(**FailedFunction, Target, FailureAnalyses) &&
+ !FailureAnalyses.hasCached(
+ **FailedFunction, rcc::MachineAnalysisManager::id<BlockCountAnalysis>()),
+ "a pass that mutates and fails must not leave stale function analyses cached");
+
+ rcc::MachineAnalysisManager FailingAnalyses;
+ Context.expect(FailingAnalyses
+ .registerAnalysis<FailingAnalysis>({
+ .Name = "Failing",
+ .Dependencies = {},
+ })
+ .has_value(),
+ "failing analysis registration must succeed");
+ rcc::MachinePassInstrumentation FailureInstrumentation;
+ std::uint32_t BeforeFailingAnalysis = 0;
+ std::uint32_t FailingAnalysisEvents = 0;
+ std::uint32_t RequiredAnalysisPassFailures = 0;
+ FailureInstrumentation.addBeforeAnalysisCallback(
+ [&BeforeFailingAnalysis](std::string_view, const rcc::MachineFunction &) {
+ ++BeforeFailingAnalysis;
+ });
+ FailureInstrumentation.addAnalysisFailureCallback(
+ [&FailingAnalysisEvents](std::string_view, const rcc::MachineFunction &, const rcc::Error &) {
+ ++FailingAnalysisEvents;
+ });
+ FailureInstrumentation.addFailureCallback(
+ [&RequiredAnalysisPassFailures](std::string_view, const rcc::MachineFunction &,
+ const rcc::Error &) { ++RequiredAnalysisPassFailures; });
+ rcc::MachinePassManager RequiredAnalysisFailurePipeline;
+ Context.expect(
+ RequiredAnalysisFailurePipeline.addPass(std::make_unique<RequiresFailingAnalysisPass>())
+ .has_value() &&
+ !RequiredAnalysisFailurePipeline.run(**Function, Target, FailingAnalyses,
+ &FailureInstrumentation) &&
+ BeforeFailingAnalysis == 1 && FailingAnalysisEvents == 1 &&
+ RequiredAnalysisPassFailures == 1,
+ "analysis and pass failure instrumentation must report required-analysis failures");
+}
+
+void testCompilationUnitAndParallelism(TestContext &Context, const rcc::TargetDescription &Target) {
+ rcc::CompilationUnit Unit;
+ for (std::size_t Index = 0; Index < 8; ++Index) {
+ auto Function = Unit.createMachineFunction("f" + std::to_string(Index));
+ Context.expect(Function.has_value(), "parallel test function creation must succeed");
+ }
+
+ std::atomic<std::uint32_t> Runs = 0;
+ rcc::MachinePassManager Parallel;
+ Context.expect(Parallel.addPass(std::make_unique<ParallelCountingPass>(Runs)).has_value(),
+ "thread-safe pass registration must succeed");
+ rcc::MachineAnalysisManager Prototype;
+ auto ParallelRun = Parallel.runFunctions(Unit, Target, Prototype, 4);
+ Context.expect(ParallelRun.has_value() && Runs == Unit.functionCount(),
+ "parallel function execution must visit every function exactly once");
+
+ std::atomic<std::uint32_t> DuplicateRuns = 0;
+ rcc::MachinePassManager Repeated;
+ Context.expect(
+ Repeated.addPass(std::make_unique<ParallelCountingPass>(DuplicateRuns)).has_value() &&
+ Repeated.addPass(std::make_unique<ParallelCountingPass>(DuplicateRuns)).has_value() &&
+ Repeated.runFunctions(Unit, Target, Prototype, 4).has_value() &&
+ DuplicateRuns == Unit.functionCount() * 2,
+ "a pipeline must allow the same named pass to run more than once");
+
+ rcc::MachinePassInstrumentation Instrumentation;
+ std::vector<std::string> ObservedFunctions;
+ std::vector<std::size_t> BeforeBlockCounts;
+ std::vector<std::size_t> AfterBlockCounts;
+ Instrumentation.addBeforeCallback([&ObservedFunctions, &BeforeBlockCounts](
+ std::string_view, const rcc::MachineFunction &Function) {
+ ObservedFunctions.emplace_back(Function.name());
+ BeforeBlockCounts.push_back(Function.blockCount());
+ });
+ Instrumentation.addAfterCallback([&AfterBlockCounts](std::string_view,
+ const rcc::MachineFunction &Function,
+ const rcc::MachinePassResult &) {
+ AfterBlockCounts.push_back(Function.blockCount());
+ });
+ std::atomic<std::uint32_t> InstrumentedRuns = 0;
+ std::atomic<std::uint32_t> ActiveRuns = 0;
+ std::atomic<std::uint32_t> MaximumActiveRuns = 0;
+ rcc::MachinePassManager Instrumented;
+ Context.expect(
+ Instrumented
+ .addPass(std::make_unique<InstrumentedParallelPass>(InstrumentedRuns, ActiveRuns,
+ MaximumActiveRuns))
+ .has_value() &&
+ Instrumented.runFunctions(Unit, Target, Prototype, 4, &Instrumentation).has_value() &&
+ InstrumentedRuns == Unit.functionCount() && MaximumActiveRuns > 1,
+ "instrumented function pipelines must execute concurrently with a parallel request");
+ bool StableCallbackOrder = ObservedFunctions.size() == Unit.functionCount();
+ for (std::size_t Index = 0; StableCallbackOrder && Index < ObservedFunctions.size(); ++Index)
+ StableCallbackOrder = ObservedFunctions[Index] == "f" + std::to_string(Index);
+ Context.expect(StableCallbackOrder,
+ "instrumentation callbacks must commit in stable compilation-unit order");
+ Context.expect(
+ BeforeBlockCounts.size() == Unit.functionCount() &&
+ AfterBlockCounts.size() == Unit.functionCount() &&
+ std::ranges::all_of(BeforeBlockCounts, [](std::size_t Count) { return Count == 0; }) &&
+ std::ranges::all_of(AfterBlockCounts, [](std::size_t Count) { return Count == 1; }),
+ "buffered instrumentation must retain distinct before/after IR snapshots");
+
+ rcc::MachineAnalysisManager UnsafePrototype;
+ Context.expect(UnsafePrototype
+ .registerAnalysis<NonThreadSafeAnalysis>({
+ .Name = "NonThreadSafe",
+ .Dependencies = {},
+ .InvalidatedBy = rcc::MachinePassChanges::all(),
+ .ThreadSafety = rcc::PassThreadSafety::NotThreadSafe,
+ })
+ .has_value(),
+ "non-thread-safe analysis registration must be explicit");
+ rcc::MachinePassManager UnsafeParallel;
+ Context.expect(
+ UnsafeParallel.addPass(std::make_unique<RequiresNonThreadSafeAnalysisPass>()).has_value() &&
+ !UnsafeParallel.runFunctions(Unit, Target, UnsafePrototype, 4),
+ "parallel pipelines must reject a required analysis not marked thread-safe");
+
+ rcc::CompilationUnitAnalysisManager UnitAnalyses;
+ Context.expect(UnitAnalyses
+ .registerAnalysis<UnitFunctionCountAnalysis>({
+ .Name = "UnitFunctionCount",
+ .Dependencies = {},
+ .InvalidatedBy = rcc::MachinePassChanges::all(),
+ })
+ .has_value(),
+ "compilation-unit analysis registration must succeed");
+ rcc::MachineAnalysisManager FunctionAnalyses;
+ rcc::CompilationUnitPassManager UnitPipeline;
+ Context.expect(UnitPipeline.addPass(std::make_unique<ObserveUnitPass>()).has_value(),
+ "compilation-unit pass registration must succeed");
+ rcc::MachinePassInstrumentation UnitInstrumentation;
+ std::uint32_t BeforeUnit = 0;
+ std::uint32_t AfterUnit = 0;
+ std::uint32_t BeforeUnitAnalysis = 0;
+ std::uint32_t AfterUnitAnalysis = 0;
+ std::uint32_t UnitAnalysisCacheHits = 0;
+ UnitInstrumentation.addBeforeUnitCallback(
+ [&BeforeUnit](std::string_view, const rcc::CompilationUnit &) { ++BeforeUnit; });
+ UnitInstrumentation.addAfterUnitCallback(
+ [&AfterUnit](std::string_view, const rcc::CompilationUnit &,
+ const rcc::CompilationUnitPassResult &) { ++AfterUnit; });
+ UnitInstrumentation.addBeforeUnitAnalysisCallback(
+ [&BeforeUnitAnalysis](std::string_view, const rcc::CompilationUnit &) {
+ ++BeforeUnitAnalysis;
+ });
+ UnitInstrumentation.addAfterUnitAnalysisCallback(
+ [&AfterUnitAnalysis, &UnitAnalysisCacheHits](std::string_view, const rcc::CompilationUnit &,
+ bool CacheHit, std::uint64_t) {
+ ++AfterUnitAnalysis;
+ UnitAnalysisCacheHits += CacheHit ? 1U : 0U;
+ });
+ Context.expect(
+ UnitPipeline.run(Unit, Target, UnitAnalyses, FunctionAnalyses, &UnitInstrumentation)
+ .has_value(),
+ "compilation-unit pass and analysis must run through their own manager");
+ Context.expect(BeforeUnit == 1 && AfterUnit == 1 && BeforeUnitAnalysis == 1 &&
+ AfterUnitAnalysis == 2 && UnitAnalysisCacheHits == 1,
+ "compilation-unit instrumentation must report pass and analysis cache events");
+
+ rcc::CompilationUnitAnalysisManager FailingUnitAnalyses;
+ Context.expect(FailingUnitAnalyses
+ .registerAnalysis<FailingUnitAnalysis>({
+ .Name = "FailingUnit",
+ .Dependencies = {},
+ })
+ .has_value(),
+ "failing compilation-unit analysis registration must succeed");
+ rcc::CompilationUnitPassManager FailingUnitAnalysisPipeline;
+ Context.expect(
+ FailingUnitAnalysisPipeline.addPass(std::make_unique<RequiresFailingUnitAnalysisPass>())
+ .has_value(),
+ "a pass requiring a failing unit analysis must register");
+ rcc::MachinePassInstrumentation FailingUnitInstrumentation;
+ std::uint32_t BeforeFailingUnitAnalysis = 0;
+ std::uint32_t FailingUnitAnalysisEvents = 0;
+ std::uint32_t RequiredUnitAnalysisPassFailures = 0;
+ FailingUnitInstrumentation.addBeforeUnitAnalysisCallback(
+ [&BeforeFailingUnitAnalysis](std::string_view, const rcc::CompilationUnit &) {
+ ++BeforeFailingUnitAnalysis;
+ });
+ FailingUnitInstrumentation.addUnitAnalysisFailureCallback(
+ [&FailingUnitAnalysisEvents](std::string_view, const rcc::CompilationUnit &,
+ const rcc::Error &) { ++FailingUnitAnalysisEvents; });
+ FailingUnitInstrumentation.addUnitFailureCallback(
+ [&RequiredUnitAnalysisPassFailures](std::string_view, const rcc::CompilationUnit &,
+ const rcc::Error &) {
+ ++RequiredUnitAnalysisPassFailures;
+ });
+ Context.expect(
+ !FailingUnitAnalysisPipeline.run(Unit, Target, FailingUnitAnalyses, FunctionAnalyses,
+ &FailingUnitInstrumentation) &&
+ BeforeFailingUnitAnalysis == 1 && FailingUnitAnalysisEvents == 1 &&
+ RequiredUnitAnalysisPassFailures == 1,
+ "unit analysis and pass failure instrumentation must report required-analysis failures");
+
+ rcc::MachineFunction *FirstFunction = Unit.functionAt(0);
+ Context.expect(
+ FirstFunction != nullptr &&
+ FunctionAnalyses
+ .registerAnalysis<BlockCountAnalysis>({
+ .Name = "BlockCount",
+ .Dependencies = {},
+ .InvalidatedBy = rcc::MachinePassChanges::all(),
+ })
+ .has_value() &&
+ FunctionAnalyses.getResult<BlockCountAnalysis>(*FirstFunction, Target).has_value(),
+ "compilation-unit failure cache fixtures must materialize");
+ rcc::CompilationUnitPassManager FailingUnitPipeline;
+ Context.expect(
+ FirstFunction != nullptr &&
+ FailingUnitPipeline.addPass(std::make_unique<MutatingUnitFailurePass>()).has_value() &&
+ !FailingUnitPipeline.run(Unit, Target, UnitAnalyses, FunctionAnalyses) &&
+ !UnitAnalyses.hasCached(
+ Unit, rcc::CompilationUnitAnalysisManager::id<UnitFunctionCountAnalysis>()) &&
+ !FunctionAnalyses.hasCached(*FirstFunction,
+ rcc::MachineAnalysisManager::id<BlockCountAnalysis>()),
+ "a mutating compilation-unit failure must clear unit and function analysis caches");
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ auto Target = createTarget();
+ Context.expect(Target.has_value(), "machine pass tests require a valid target description");
+ if (!Target)
+ return Context.result();
+
+ testAnalysisInfrastructure(Context, *Target);
+ testPassAndInstrumentation(Context, *Target);
+ testCompilationUnitAndParallelism(Context, *Target);
+ return Context.result();
+}
diff --git a/backend/unittests/CodeGen/TargetCallLoweringTest.cpp b/backend/unittests/CodeGen/TargetCallLoweringTest.cpp
new file mode 100644
index 0000000..f826285
--- /dev/null
+++ b/backend/unittests/CodeGen/TargetCallLoweringTest.cpp
@@ -0,0 +1,329 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/CodeGen/TargetCallLowering.h"
+
+#include "Target/TestTarget.h"
+#include "rcc/CodeGen/InstructionDescriptor.h"
+#include "rcc/CodeGen/MachineFunction.h"
+#include "rcc/CodeGen/MachineIRBuilder.h"
+
+#include <concepts>
+#include <cstdint>
+#include <iostream>
+#include <memory>
+#include <span>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace {
+
+class TestContext final {
+public:
+ void expect(bool Condition, std::string_view Message) {
+ if (Condition)
+ return;
+ std::cerr << Message << '\n';
+ ++FailureCount;
+ }
+
+ [[nodiscard]] int result() const noexcept {
+ return FailureCount == 0 ? 0 : 1;
+ }
+
+private:
+ int FailureCount = 0;
+};
+
+[[nodiscard]] rcc::Alignment align(std::uint64_t Bytes) {
+ const auto Value = rcc::Alignment::create(Bytes);
+ if (!Value)
+ std::terminate();
+ return *Value;
+}
+
+[[nodiscard]] rcc::LLT integer(std::uint32_t Bits) {
+ const auto Type = rcc::LLT::makeInteger(Bits);
+ if (!Type)
+ std::terminate();
+ return *Type;
+}
+
+[[nodiscard]] rcc::PhysicalRegister physical(std::uint32_t ID) {
+ const auto Register = rcc::PhysicalRegister::create(ID);
+ if (!Register)
+ std::terminate();
+ return *Register;
+}
+
+[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() {
+ rcc::InstructionDescriptor Nop;
+ Nop.InstructionOpcode = rcc::Opcode::fromValue(1);
+ Nop.Name = "TEST_NOP";
+ Nop.Flags = {rcc::InstructionFlag::ProducesCode};
+ std::vector<rcc::InstructionDescriptor> Descriptors;
+ Descriptors.push_back(std::move(Nop));
+ auto Instructions = rcc::InstructionSet::create(std::move(Descriptors));
+ if (!Instructions)
+ return std::unexpected(Instructions.error());
+ return rcc::test::createTestTargetDescription(std::move(*Instructions));
+}
+
+[[nodiscard]] rcc::CallLoweringPlanDesc emptyPlanDescription(rcc::CallLoweringPlanKind Kind) {
+ rcc::CallLoweringPlanDesc Description;
+ Description.Kind = Kind;
+ Description.Target = "test-target:test-abi";
+ Description.Convention = rcc::CallingConventionID::fromValue(1);
+ Description.StackArguments.Align = align(8);
+ Description.CallFrame.Align = align(8);
+ if (Kind == rcc::CallLoweringPlanKind::Call) {
+ Description.CallFrame.EmitSetup = true;
+ Description.CallFrame.EmitDestroy = true;
+ Description.PreservedMask = rcc::test::createTestCallPreservedMask();
+ Description.Callee = *rcc::MachineCallTarget::externalSymbol("callee");
+ Description.CallSiteID = 1;
+ }
+ return Description;
+}
+
+[[nodiscard]] rcc::Expected<rcc::CallLoweringPlan> emptyPlan(rcc::CallLoweringPlanKind Kind) {
+ return rcc::CallLoweringPlan::create(emptyPlanDescription(Kind));
+}
+
+[[nodiscard]] rcc::CCAssignment registerAssignment() {
+ return {
+ .ValueIndex = 0,
+ .OriginalValue = rcc::Register::fromPhysical(physical(2)),
+ .OriginalValueIndex = 0,
+ .FragmentIndex = 0,
+ .FragmentCount = 1,
+ .ValueType = integer(64),
+ .LocationType = integer(64),
+ .LocationKind = rcc::CCLocationKind::Register,
+ .LocationRegister = physical(1),
+ .StackOffsetBytes = std::nullopt,
+ .LocationSizeBytes = 8,
+ .StackSlotSizeBytes = 8,
+ .LocationAlignment = align(8),
+ .Extension = rcc::ABIExtensionKind::None,
+ .CustomFlags = {},
+ };
+}
+
+void testPlanValidation(TestContext &Context) {
+ static_assert(
+ std::same_as<decltype(std::declval<const rcc::CallLoweringPlan &>().argumentAssignments()),
+ std::span<const rcc::CCAssignment>>);
+ static_assert(std::same_as<decltype(std::declval<const rcc::CallLoweringPlan &>().byValCopies()),
+ std::span<const rcc::ByValCopyAssignment>>);
+
+ auto Valid = emptyPlan(rcc::CallLoweringPlanKind::FormalArguments);
+ Context.expect(Valid && Valid->target() == "test-target:test-abi" &&
+ Valid->argumentAssignments().empty(),
+ "a valid immutable formal-argument plan must be accepted");
+
+ auto MissingRegister = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments);
+ auto Assignment = registerAssignment();
+ Assignment.OriginalValue = {};
+ MissingRegister.ArgumentAssignments.push_back(Assignment);
+ Context.expect(!rcc::CallLoweringPlan::create(std::move(MissingRegister)),
+ "an assignment without a machine value must be rejected");
+
+ auto BadStack = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments);
+ BadStack.StackArguments.SizeBytes = 8;
+ auto StackAssignment = registerAssignment();
+ StackAssignment.LocationKind = rcc::CCLocationKind::Stack;
+ StackAssignment.LocationRegister.reset();
+ StackAssignment.StackOffsetBytes = 8;
+ BadStack.ArgumentAssignments.push_back(StackAssignment);
+ Context.expect(!rcc::CallLoweringPlan::create(std::move(BadStack)),
+ "an assignment outside the declared stack area must be rejected");
+
+ auto Overlap = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments);
+ Overlap.StackArguments.SizeBytes = 16;
+ StackAssignment.StackOffsetBytes = 0;
+ Overlap.ArgumentAssignments.push_back(StackAssignment);
+ StackAssignment.OriginalValue = rcc::Register::fromPhysical(physical(3));
+ StackAssignment.StackOffsetBytes = 4;
+ Overlap.ArgumentAssignments.push_back(StackAssignment);
+ Context.expect(!rcc::CallLoweringPlan::create(std::move(Overlap)),
+ "overlapping stack assignments must be rejected");
+
+ auto MisalignedStack = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments);
+ MisalignedStack.StackArguments.SizeBytes = 16;
+ StackAssignment.StackOffsetBytes = 4;
+ MisalignedStack.ArgumentAssignments.push_back(StackAssignment);
+ Context.expect(!rcc::CallLoweringPlan::create(std::move(MisalignedStack)),
+ "a stack assignment must satisfy its declared alignment");
+
+ auto OversizedLocation = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments);
+ auto OversizedAssignment = registerAssignment();
+ OversizedAssignment.LocationSizeBytes = 16;
+ OversizedLocation.ArgumentAssignments.push_back(OversizedAssignment);
+ Context.expect(!rcc::CallLoweringPlan::create(std::move(OversizedLocation)),
+ "an ABI location cannot exceed its transport slot");
+
+ auto UnknownLocation = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments);
+ auto UnknownAssignment = registerAssignment();
+ UnknownAssignment.LocationKind =
+ static_cast<rcc::CCLocationKind>(255); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange)
+ UnknownLocation.ArgumentAssignments.push_back(UnknownAssignment);
+ Context.expect(!rcc::CallLoweringPlan::create(std::move(UnknownLocation)),
+ "an unknown ABI location kind must be rejected");
+
+ auto BadFrame = emptyPlanDescription(rcc::CallLoweringPlanKind::Call);
+ BadFrame.CallFrame.EmitDestroy = false;
+ Context.expect(!rcc::CallLoweringPlan::create(std::move(BadFrame)),
+ "unpaired call-frame setup and destroy requirements must be rejected");
+
+ auto SmallFrame = emptyPlanDescription(rcc::CallLoweringPlanKind::Call);
+ SmallFrame.StackArguments.SizeBytes = 16;
+ SmallFrame.CallFrame.SizeBytes = 8;
+ StackAssignment.StackOffsetBytes = 0;
+ SmallFrame.ArgumentAssignments.push_back(StackAssignment);
+ Context.expect(!rcc::CallLoweringPlan::create(std::move(SmallFrame)),
+ "a call frame must contain its complete stack-argument area");
+
+ auto BadTail = emptyPlanDescription(rcc::CallLoweringPlanKind::Call);
+ BadTail.CallFrame.EmitSetup = false;
+ BadTail.CallFrame.EmitDestroy = false;
+ BadTail.CallFrame.ReusesIncomingArea = true;
+ BadTail.TailDecision = rcc::TailCallKind::Tail;
+ BadTail.TailRejectionReason = "inconsistent accepted tail call";
+ Context.expect(!rcc::CallLoweringPlan::create(std::move(BadTail)),
+ "an accepted tail call cannot retain a rejection reason");
+
+ auto UnknownTail = emptyPlanDescription(rcc::CallLoweringPlanKind::Call);
+ UnknownTail.TailDecision =
+ static_cast<rcc::TailCallKind>(255); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange)
+ Context.expect(!rcc::CallLoweringPlan::create(std::move(UnknownTail)),
+ "an unknown tail-call decision must be rejected");
+
+ auto BadVarArgs = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments);
+ BadVarArgs.VarArgs.IsVariadic = true;
+ BadVarArgs.VarArgs.RegisterSaveAreaSizeBytes = 8;
+ BadVarArgs.VarArgs.RegistersToSave.push_back({physical(1), 8});
+ Context.expect(!rcc::CallLoweringPlan::create(std::move(BadVarArgs)),
+ "a variadic register save outside its area must be rejected");
+
+ auto BadByVal = emptyPlanDescription(rcc::CallLoweringPlanKind::Call);
+ auto ByValAssignment = registerAssignment();
+ ByValAssignment.ValueType = *rcc::LLT::makePointer(0, 64);
+ ByValAssignment.LocationType = ByValAssignment.ValueType;
+ ByValAssignment.CustomFlags.set(rcc::CCAssignmentFlag::ByVal);
+ BadByVal.ArgumentAssignments.push_back(ByValAssignment);
+ BadByVal.ByValCopies.push_back(
+ {0, ByValAssignment.OriginalValue, ByValAssignment.ValueType, 0, 16, align(8)});
+ Context.expect(!rcc::CallLoweringPlan::create(std::move(BadByVal)),
+ "a byval copy outside the call frame must be rejected");
+
+ auto OverlappingByVal = emptyPlanDescription(rcc::CallLoweringPlanKind::Call);
+ OverlappingByVal.CallFrame.SizeBytes = 32;
+ auto FirstByVal = registerAssignment();
+ FirstByVal.ValueType = *rcc::LLT::makePointer(0, 64);
+ FirstByVal.LocationType = FirstByVal.ValueType;
+ FirstByVal.CustomFlags.set(rcc::CCAssignmentFlag::ByVal);
+ auto SecondByVal = FirstByVal;
+ SecondByVal.ValueIndex = 1;
+ SecondByVal.OriginalValue = rcc::Register::fromPhysical(physical(3));
+ SecondByVal.OriginalValueIndex = 1;
+ SecondByVal.LocationRegister = physical(2);
+ OverlappingByVal.ArgumentAssignments = {FirstByVal, SecondByVal};
+ OverlappingByVal.ByValCopies = {
+ {0, FirstByVal.OriginalValue, FirstByVal.ValueType, 0, 16, align(8)},
+ {1, SecondByVal.OriginalValue, SecondByVal.ValueType, 8, 16, align(8)},
+ };
+ Context.expect(!rcc::CallLoweringPlan::create(std::move(OverlappingByVal)),
+ "overlapping byval copy ranges must be rejected");
+
+ Context.expect(!rcc::MachineCallTarget::globalSymbol("") && !rcc::MachineCallTarget::indirect({}),
+ "call-target factories must reject missing targets");
+}
+
+class RecordingCallLowering final : public rcc::TargetCallLowering {
+public:
+ enum class Behavior : std::uint8_t {
+ Valid,
+ PlannerFailure,
+ WrongKind,
+ };
+
+ explicit RecordingCallLowering(Behavior CurrentBehavior) : CurrentBehavior(CurrentBehavior) {}
+
+ [[nodiscard]] rcc::Expected<rcc::CallLoweringPlan>
+ planFormalArguments(const rcc::FormalArgumentRequest &) const override {
+ return plan(rcc::CallLoweringPlanKind::FormalArguments);
+ }
+
+ [[nodiscard]] rcc::Expected<rcc::CallLoweringPlan>
+ planCall(const rcc::CallLoweringRequest &) const override {
+ return plan(rcc::CallLoweringPlanKind::Call);
+ }
+
+ [[nodiscard]] rcc::Expected<rcc::CallLoweringPlan>
+ planReturn(const rcc::ReturnLoweringRequest &) const override {
+ return plan(rcc::CallLoweringPlanKind::Return);
+ }
+
+ [[nodiscard]] rcc::Expected<void> emitPlan(rcc::MachineIRBuilder &,
+ const rcc::CallLoweringPlan &) const override {
+ ++EmissionCount;
+ return {};
+ }
+
+ [[nodiscard]] std::uint32_t emissions() const noexcept {
+ return EmissionCount;
+ }
+
+private:
+ [[nodiscard]] rcc::Expected<rcc::CallLoweringPlan>
+ plan(rcc::CallLoweringPlanKind RequestedKind) const {
+ if (CurrentBehavior == Behavior::PlannerFailure) {
+ return rcc::makeUnexpected(rcc::ErrorCode::Unsupported,
+ "intentional call-lowering planner failure");
+ }
+ if (CurrentBehavior == Behavior::WrongKind)
+ RequestedKind = RequestedKind == rcc::CallLoweringPlanKind::Call
+ ? rcc::CallLoweringPlanKind::Return
+ : rcc::CallLoweringPlanKind::Call;
+ return emptyPlan(RequestedKind);
+ }
+
+ Behavior CurrentBehavior;
+ mutable std::uint32_t EmissionCount = 0;
+};
+
+void testPlanThenEmitBoundary(TestContext &Context) {
+ auto Target = createTarget();
+ auto Function =
+ rcc::MachineFunction::create("plan_then_emit", rcc::MachineFunctionStableID::fromValue(1));
+ Context.expect(Target && Function, "call-lowering workflow fixtures must be valid");
+ if (!Target || !Function)
+ return;
+ rcc::MachineIRBuilder Builder(**Function, *Target);
+ rcc::CallLoweringRequest Request;
+
+ RecordingCallLowering Failing(RecordingCallLowering::Behavior::PlannerFailure);
+ auto Failure = Failing.lowerCall(Builder, Request);
+ Context.expect(!Failure && Failing.emissions() == 0 && (*Function)->allInstructions().empty(),
+ "planner failure must not invoke emission or mutate MachineIR");
+
+ RecordingCallLowering WrongKind(RecordingCallLowering::Behavior::WrongKind);
+ auto Mismatch = WrongKind.lowerCall(Builder, Request);
+ Context.expect(!Mismatch && Mismatch.error().code() == rcc::ErrorCode::InternalError &&
+ WrongKind.emissions() == 0,
+ "a target returning the wrong plan kind must be rejected before emission");
+
+ RecordingCallLowering Valid(RecordingCallLowering::Behavior::Valid);
+ Context.expect(Valid.lowerCall(Builder, Request) && Valid.emissions() == 1,
+ "a valid plan must be emitted exactly once");
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ testPlanValidation(Context);
+ testPlanThenEmitBoundary(Context);
+ return Context.result();
+}