From f99ef4322a7a2c09380fff61f88ab9440f05a92c Mon Sep 17 00:00:00 2001 From: Richard-Qin-X Date: Fri, 7 Aug 2026 22:06:28 +0800 Subject: [CodeGen] Complete machine pass and ABI lowering infrastructure --- backend/unittests/CodeGen/CMakeLists.txt | 48 ++ backend/unittests/CodeGen/GenericOpcodesTest.cpp | 701 ++++++++++++++++ backend/unittests/CodeGen/MachineAnalysesTest.cpp | 477 +++++++++++ backend/unittests/CodeGen/MachinePassTest.cpp | 918 +++++++++++++++++++++ .../unittests/CodeGen/TargetCallLoweringTest.cpp | 329 ++++++++ 5 files changed, 2473 insertions(+) create mode 100644 backend/unittests/CodeGen/GenericOpcodesTest.cpp create mode 100644 backend/unittests/CodeGen/MachineAnalysesTest.cpp create mode 100644 backend/unittests/CodeGen/MachinePassTest.cpp create mode 100644 backend/unittests/CodeGen/TargetCallLoweringTest.cpp (limited to 'backend/unittests/CodeGen') 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 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 Names; + std::unordered_set 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{}; + auto V4I32 = I32 ? rcc::LLT::makeFixedVector(4, *I32) : rcc::Expected{}; + auto V2P64 = P64 ? rcc::LLT::makeFixedVector(2, *P64) : rcc::Expected{}; + 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 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 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 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 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 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 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 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 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 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 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 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 FloatWords{0}; + auto FloatBits = rcc::FloatingPointBits::create(rcc::FloatSemantics::IEEESingle, FloatWords); + if (!FloatBits) + std::terminate(); + std::vector 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 +#include +#include +#include +#include +#include +#include + +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 createTarget() { + std::vector Descriptors; + std::vector 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 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(**Function, Target); + auto Loops = Analyses.getResult(**Function, Target); + auto Probabilities = + Analyses.getResult(**Function, Target); + auto Frequencies = Analyses.getResult(**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(**Function, Target); + auto MemoryDependence = + Analyses.getResult(**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()), + "CFG changes must invalidate CFG dependents and memory dependence but preserve aliasing"); + + auto RefreshedDominators = + Analyses.getResult(**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(**Function, Target) + : rcc::Expected{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{std::unexpected(I64.error())}; + auto Address = Pointer ? (*Function)->registerInfo().createVirtualRegister(*Pointer) + : rcc::Expected{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(**Function, Target) + : rcc::Expected{ + 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 + run(rcc::MachineFunction &, const rcc::TargetDescription &, rcc::MachineAnalysisManager &) { + return Result{}; + } +}; + +void testAtomicStandardRegistration(TestContext &Context) { + rcc::MachineAnalysisManager Analyses; + Context.expect(Analyses + .registerAnalysis({ + .Name = "MachineLoopInfo", + .Dependencies = {}, + }) + .has_value(), + "analysis-name conflict setup must succeed"); + Context.expect(!rcc::registerStage8MachineAnalyses(Analyses) && + !Analyses.isRegistered( + rcc::MachineAnalysisManager::id()), + "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{std::unexpected(I64.error())}; + auto SourceValue = I64 ? (*Function)->registerInfo().createVirtualRegister(*I64) + : rcc::Expected{std::unexpected(I64.error())}; + auto OtherValue = I64 ? (*Function)->registerInfo().createVirtualRegister(*I64) + : rcc::Expected{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()->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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 createTarget() { + std::vector Descriptors; + std::vector 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 run(rcc::MachineFunction &Function, + const rcc::TargetDescription &, + rcc::MachineAnalysisManager &) { + ++Runs; + return Result{Function.blockCount()}; + } + + static inline std::atomic 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 run(rcc::MachineFunction &Function, + const rcc::TargetDescription &Target, + rcc::MachineAnalysisManager &Analyses) { + auto Blocks = Analyses.getResult(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 + 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 + 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 + 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 + 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 + 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 + 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()}, + .VerificationStage = std::nullopt, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected + run(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineAnalysisManager &Analyses) override { + auto Dependent = Analyses.getResult(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 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()}, + .VerificationStage = std::nullopt, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected 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()}, + .VerificationStage = std::nullopt, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected 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 &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 run(rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::MachineAnalysisManager &) override { + Count->fetch_add(1, std::memory_order_relaxed); + return rcc::MachinePassResult{}; + } + +private: + std::atomic *Count; +}; + +class InstrumentedParallelPass final : public rcc::MachineFunctionPass { +public: + InstrumentedParallelPass(std::atomic &Runs, std::atomic &Active, + std::atomic &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 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 *Runs; + std::atomic *Active; + std::atomic *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(), + }, + .VerificationStage = std::nullopt, + }; + Value.ThreadSafety = rcc::PassThreadSafety::ThreadSafe; + return Value; + }(); + return Contract; + } + + [[nodiscard]] rcc::Expected 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 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 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(), + }, + .Parameters = {}, + .DeclaredChanges = {}, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected + 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()}, + .Parameters = {}, + .DeclaredChanges = {}, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected + run(rcc::CompilationUnit &Unit, const rcc::TargetDescription &Target, + rcc::CompilationUnitAnalysisManager &Analyses, rcc::MachineAnalysisManager &) override { + auto Count = Analyses.getResult(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()}, + .Parameters = {}, + .DeclaredChanges = {rcc::MachinePassChange::BlockLayout}, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected + 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({ + .Name = "BlockCount", + .Dependencies = {}, + .InvalidatedBy = {rcc::MachinePassChange::CFG}, + }) + .has_value(), + "base analysis registration must succeed"); + Context.expect(Analyses + .registerAnalysis({ + .Name = "Dependent", + .Dependencies = {rcc::MachineAnalysisManager::id()}, + .InvalidatedBy = {rcc::MachinePassChange::Operands}, + }) + .has_value(), + "dependent analysis registration must succeed"); + + BlockCountAnalysis::Runs = 0; + auto First = Analyses.getResult(**Function, Target); + auto Second = Analyses.getResult(**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({ + .Name = "CycleA", + .Dependencies = {rcc::MachineAnalysisManager::id()}, + }) + .has_value() && + Cyclic + .registerAnalysis({ + .Name = "CycleB", + .Dependencies = {rcc::MachineAnalysisManager::id()}, + }) + .has_value(), + "cyclic analysis registrations must be accepted until requested"); + Context.expect(!Cyclic.getResult(**Function, Target), + "analysis dependency cycles must produce a structured error"); + + rcc::MachineAnalysisManager CustomInvalidation; + Context.expect(CustomInvalidation + .registerAnalysis({ + .Name = "Sticky", + .Dependencies = {}, + .InvalidatedBy = {rcc::MachinePassChange::CFG}, + }) + .has_value() && + CustomInvalidation + .registerAnalysis({ + .Name = "Expiring", + .Dependencies = {}, + .InvalidatedBy = {rcc::MachinePassChange::CFG}, + }) + .has_value() && + CustomInvalidation.getResult(**Function, Target) && + CustomInvalidation.getResult(**Function, Target), + "custom invalidation analyses must materialize"); + CustomInvalidation.invalidate(**Function, rcc::PreservedAnalyses::none(), + {rcc::MachinePassChange::CFG}); + Context.expect( + CustomInvalidation.hasCached(**Function, rcc::MachineAnalysisManager::id()) && + !CustomInvalidation.hasCached(**Function, + rcc::MachineAnalysisManager::id()), + "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({ + .Name = "BlockCount", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + .ThreadSafety = rcc::PassThreadSafety::ThreadSafe, + }) + .has_value() && + Analyses + .registerAnalysis({ + .Name = "Dependent", + .Dependencies = {rcc::MachineAnalysisManager::id()}, + }) + .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()).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()).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({ + .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()).has_value() && + !FailingPipeline.run(**FailedFunction, Target, FailureAnalyses) && + !FailureAnalyses.hasCached( + **FailedFunction, rcc::MachineAnalysisManager::id()), + "a pass that mutates and fails must not leave stale function analyses cached"); + + rcc::MachineAnalysisManager FailingAnalyses; + Context.expect(FailingAnalyses + .registerAnalysis({ + .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()) + .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 Runs = 0; + rcc::MachinePassManager Parallel; + Context.expect(Parallel.addPass(std::make_unique(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 DuplicateRuns = 0; + rcc::MachinePassManager Repeated; + Context.expect( + Repeated.addPass(std::make_unique(DuplicateRuns)).has_value() && + Repeated.addPass(std::make_unique(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 ObservedFunctions; + std::vector BeforeBlockCounts; + std::vector 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 InstrumentedRuns = 0; + std::atomic ActiveRuns = 0; + std::atomic MaximumActiveRuns = 0; + rcc::MachinePassManager Instrumented; + Context.expect( + Instrumented + .addPass(std::make_unique(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({ + .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()).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({ + .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()).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({ + .Name = "FailingUnit", + .Dependencies = {}, + }) + .has_value(), + "failing compilation-unit analysis registration must succeed"); + rcc::CompilationUnitPassManager FailingUnitAnalysisPipeline; + Context.expect( + FailingUnitAnalysisPipeline.addPass(std::make_unique()) + .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({ + .Name = "BlockCount", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + }) + .has_value() && + FunctionAnalyses.getResult(*FirstFunction, Target).has_value(), + "compilation-unit failure cache fixtures must materialize"); + rcc::CompilationUnitPassManager FailingUnitPipeline; + Context.expect( + FirstFunction != nullptr && + FailingUnitPipeline.addPass(std::make_unique()).has_value() && + !FailingUnitPipeline.run(Unit, Target, UnitAnalyses, FunctionAnalyses) && + !UnitAnalyses.hasCached( + Unit, rcc::CompilationUnitAnalysisManager::id()) && + !FunctionAnalyses.hasCached(*FirstFunction, + rcc::MachineAnalysisManager::id()), + "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 +#include +#include +#include +#include +#include +#include +#include + +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 createTarget() { + rcc::InstructionDescriptor Nop; + Nop.InstructionOpcode = rcc::Opcode::fromValue(1); + Nop.Name = "TEST_NOP"; + Nop.Flags = {rcc::InstructionFlag::ProducesCode}; + std::vector 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 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().argumentAssignments()), + std::span>); + static_assert(std::same_as().byValCopies()), + std::span>); + + 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(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(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 + planFormalArguments(const rcc::FormalArgumentRequest &) const override { + return plan(rcc::CallLoweringPlanKind::FormalArguments); + } + + [[nodiscard]] rcc::Expected + planCall(const rcc::CallLoweringRequest &) const override { + return plan(rcc::CallLoweringPlanKind::Call); + } + + [[nodiscard]] rcc::Expected + planReturn(const rcc::ReturnLoweringRequest &) const override { + return plan(rcc::CallLoweringPlanKind::Return); + } + + [[nodiscard]] rcc::Expected emitPlan(rcc::MachineIRBuilder &, + const rcc::CallLoweringPlan &) const override { + ++EmissionCount; + return {}; + } + + [[nodiscard]] std::uint32_t emissions() const noexcept { + return EmissionCount; + } + +private: + [[nodiscard]] rcc::Expected + 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(); +} -- cgit v1.2.3