diff options
Diffstat (limited to 'backend/unittests/CodeGen')
55 files changed, 17708 insertions, 0 deletions
diff --git a/backend/unittests/CodeGen/CMakeLists.txt b/backend/unittests/CodeGen/CMakeLists.txt new file mode 100644 index 0000000..62a5025 --- /dev/null +++ b/backend/unittests/CodeGen/CMakeLists.txt @@ -0,0 +1,545 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +set( + RCCBackendCodeGenTestSources + CompilationUnitTest.cpp + CoreTypesTest.cpp + GenericOpcodesTest.cpp + InstructionDescriptorTest.cpp + MachineCompilationStatusTest.cpp + MachineConstantPoolTest.cpp + MachineDebugInfoTest.cpp + MachineFrameInfoTest.cpp + MachineFunctionClonerTest.cpp + MachineFunctionRecordsTest.cpp + MachineIRAttributesTest.cpp + MachineIRBuilderTest.cpp + MachineIRCoreTest.cpp + MachineAnalysesTest.cpp + MachineJumpTableInfoTest.cpp + SlotIndexesTest.cpp + LiveVariablesTest.cpp + LiveIntervalTest.cpp + LiveIntervalsTest.cpp + LivePhysRegsTest.cpp + LiveRegUnitsTest.cpp + LiveTestSupport.h + MachineMetadataTest.cpp + MachineOperandTest.cpp + MachineOptimizationTest.cpp + PassManagerTest.cpp + PassInstrumentationTest.cpp + MachinePassManagerTest.cpp + MachineSchedulingTest.cpp + PBQPTest.cpp + RegisterAllocationTest.cpp + PHIEliminationTest.cpp + TwoAddressInstructionPassTest.cpp + MachineVerifierTest.cpp + MachineVerifierTargetTest.cpp + MachineValueTypesTest.cpp + TargetConstantTest.cpp + TargetCallLoweringTest.cpp + TargetFunctionStateTest.cpp + TargetMIRTextInfoTest.cpp + TargetRegisterInfoTest.cpp +) + +add_executable(RCCBackendCodeGenCompilationUnitTests CompilationUnitTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenCompilationUnitTests) +target_link_libraries(RCCBackendCodeGenCompilationUnitTests PRIVATE RCCBackend::CodeGen) + +add_test( + NAME RCCBackend.CodeGen.CompilationUnit + COMMAND RCCBackendCodeGenCompilationUnitTests +) + +add_executable(RCCBackendCodeGenCoreTypesTests CoreTypesTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenCoreTypesTests) +target_link_libraries(RCCBackendCodeGenCoreTypesTests PRIVATE RCCBackend::CodeGen) + +add_test(NAME RCCBackend.CodeGen.CoreTypes COMMAND RCCBackendCodeGenCoreTypesTests) + +add_executable(RCCBackendCodeGenPBQPTests PBQPTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenPBQPTests) +target_link_libraries(RCCBackendCodeGenPBQPTests PRIVATE RCCBackend::CodeGen) + +add_test(NAME RCCBackend.CodeGen.PBQP COMMAND RCCBackendCodeGenPBQPTests) + +add_executable(RCCBackendCodeGenRegisterAllocationTests RegisterAllocationTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenRegisterAllocationTests) +target_link_libraries( + RCCBackendCodeGenRegisterAllocationTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.RegisterAllocation + COMMAND RCCBackendCodeGenRegisterAllocationTests +) + +add_executable(RCCBackendCodeGenLiveRegMatrixTests LiveRegMatrixTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenLiveRegMatrixTests) +target_link_libraries( + RCCBackendCodeGenLiveRegMatrixTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test(NAME RCCBackend.CodeGen.LiveRegMatrix COMMAND RCCBackendCodeGenLiveRegMatrixTests) + +add_executable(RCCBackendCodeGenVirtRegMapTests VirtRegMapTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenVirtRegMapTests) +target_link_libraries( + RCCBackendCodeGenVirtRegMapTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test(NAME RCCBackend.CodeGen.VirtRegMap COMMAND RCCBackendCodeGenVirtRegMapTests) + +add_executable(RCCBackendCodeGenSpillerTests SpillerTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenSpillerTests) +target_link_libraries( + RCCBackendCodeGenSpillerTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test(NAME RCCBackend.CodeGen.Spiller COMMAND RCCBackendCodeGenSpillerTests) + +add_executable(RCCBackendCodeGenBasicAllocatorTests RegAllocBasicTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenBasicAllocatorTests) +target_link_libraries( + RCCBackendCodeGenBasicAllocatorTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test(NAME RCCBackend.CodeGen.BasicAllocator COMMAND RCCBackendCodeGenBasicAllocatorTests) + +add_executable(RCCBackendCodeGenFastAllocatorTests RegAllocFastTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenFastAllocatorTests) +target_link_libraries( + RCCBackendCodeGenFastAllocatorTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test(NAME RCCBackend.CodeGen.FastAllocator COMMAND RCCBackendCodeGenFastAllocatorTests) + +add_executable(RCCBackendCodeGenPBQPAllocatorTests RegAllocPBQPTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenPBQPAllocatorTests) +target_link_libraries( + RCCBackendCodeGenPBQPAllocatorTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test(NAME RCCBackend.CodeGen.PBQPAllocator COMMAND RCCBackendCodeGenPBQPAllocatorTests) + +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 +) +rcc_backend_configure_target(RCCBackendCodeGenInstructionDescriptorTests) +target_link_libraries( + RCCBackendCodeGenInstructionDescriptorTests + PRIVATE RCCBackend::CodeGen +) + +add_test( + NAME RCCBackend.CodeGen.InstructionDescriptor + COMMAND RCCBackendCodeGenInstructionDescriptorTests +) + +add_executable( + RCCBackendCodeGenMachineCompilationStatusTests + MachineCompilationStatusTest.cpp +) +rcc_backend_configure_target(RCCBackendCodeGenMachineCompilationStatusTests) +target_link_libraries( + RCCBackendCodeGenMachineCompilationStatusTests + PRIVATE RCCBackend::CodeGen +) + +add_test( + NAME RCCBackend.CodeGen.MachineCompilationStatus + COMMAND RCCBackendCodeGenMachineCompilationStatusTests +) + +add_executable(RCCBackendCodeGenMachineConstantPoolTests MachineConstantPoolTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineConstantPoolTests) +target_link_libraries(RCCBackendCodeGenMachineConstantPoolTests PRIVATE RCCBackend::CodeGen) + +add_test( + NAME RCCBackend.CodeGen.MachineConstantPool + COMMAND RCCBackendCodeGenMachineConstantPoolTests +) + +add_executable(RCCBackendCodeGenMachineDebugInfoTests MachineDebugInfoTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineDebugInfoTests) +target_link_libraries( + RCCBackendCodeGenMachineDebugInfoTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.MachineDebugInfo + COMMAND RCCBackendCodeGenMachineDebugInfoTests +) + +add_executable(RCCBackendCodeGenMachineFrameInfoTests MachineFrameInfoTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineFrameInfoTests) +target_link_libraries(RCCBackendCodeGenMachineFrameInfoTests PRIVATE RCCBackend::CodeGen) + +add_test( + NAME RCCBackend.CodeGen.MachineFrameInfo + COMMAND RCCBackendCodeGenMachineFrameInfoTests +) + +add_executable( + RCCBackendCodeGenMachineFunctionClonerTests + MachineFunctionClonerTest.cpp +) +rcc_backend_configure_target(RCCBackendCodeGenMachineFunctionClonerTests) +target_link_libraries( + RCCBackendCodeGenMachineFunctionClonerTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.MachineFunctionCloner + COMMAND RCCBackendCodeGenMachineFunctionClonerTests +) + +add_executable( + RCCBackendCodeGenMachineFunctionRecordsTests + MachineFunctionRecordsTest.cpp +) +rcc_backend_configure_target(RCCBackendCodeGenMachineFunctionRecordsTests) +target_link_libraries( + RCCBackendCodeGenMachineFunctionRecordsTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.MachineFunctionRecords + COMMAND RCCBackendCodeGenMachineFunctionRecordsTests +) + +add_executable(RCCBackendCodeGenMachineIRBuilderTests MachineIRBuilderTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineIRBuilderTests) +target_link_libraries( + RCCBackendCodeGenMachineIRBuilderTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.MachineIRBuilder + COMMAND RCCBackendCodeGenMachineIRBuilderTests +) + +add_executable(RCCBackendCodeGenMachineIRAttributesTests MachineIRAttributesTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineIRAttributesTests) +target_link_libraries( + RCCBackendCodeGenMachineIRAttributesTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.MachineIRAttributes + COMMAND RCCBackendCodeGenMachineIRAttributesTests +) + +add_executable(RCCBackendCodeGenMachineVerifierTests MachineVerifierTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineVerifierTests) +target_link_libraries( + RCCBackendCodeGenMachineVerifierTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.MachineVerifier + COMMAND RCCBackendCodeGenMachineVerifierTests +) + +add_executable( + RCCBackendCodeGenMachineVerifierTargetTests + MachineVerifierTargetTest.cpp +) +rcc_backend_configure_target(RCCBackendCodeGenMachineVerifierTargetTests) +target_link_libraries( + RCCBackendCodeGenMachineVerifierTargetTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.MachineVerifierTarget + COMMAND RCCBackendCodeGenMachineVerifierTargetTests +) + +add_executable(RCCBackendCodeGenMachineValueTypesTests MachineValueTypesTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineValueTypesTests) +target_link_libraries(RCCBackendCodeGenMachineValueTypesTests PRIVATE RCCBackend::CodeGen) + +add_test( + NAME RCCBackend.CodeGen.MachineValueTypes + COMMAND RCCBackendCodeGenMachineValueTypesTests +) + +add_executable(RCCBackendCodeGenMachineIRCoreTests MachineIRCoreTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineIRCoreTests) +target_link_libraries(RCCBackendCodeGenMachineIRCoreTests PRIVATE RCCBackend::CodeGen) + +add_test( + NAME RCCBackend.CodeGen.MachineIRCore + COMMAND RCCBackendCodeGenMachineIRCoreTests +) + +add_executable(RCCBackendCodeGenMachineJumpTableInfoTests MachineJumpTableInfoTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineJumpTableInfoTests) +target_link_libraries(RCCBackendCodeGenMachineJumpTableInfoTests PRIVATE RCCBackend::CodeGen) + +add_test( + NAME RCCBackend.CodeGen.MachineJumpTableInfo + COMMAND RCCBackendCodeGenMachineJumpTableInfoTests +) + +foreach( + Component + SlotIndexes + LiveVariables + LiveInterval + LiveIntervals + LivePhysRegs + LiveRegUnits +) + add_executable(RCCBackendCodeGen${Component}Tests ${Component}Test.cpp) + rcc_backend_configure_target(RCCBackendCodeGen${Component}Tests) + target_link_libraries( + RCCBackendCodeGen${Component}Tests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget RCCBackend::RISCV + ) + add_test( + NAME RCCBackend.CodeGen.${Component} + COMMAND RCCBackendCodeGen${Component}Tests + ) +endforeach() + +add_executable(RCCBackendCodeGenMachineMetadataTests MachineMetadataTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineMetadataTests) +target_link_libraries( + RCCBackendCodeGenMachineMetadataTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.MachineMetadata + COMMAND RCCBackendCodeGenMachineMetadataTests +) + +add_executable(RCCBackendCodeGenMachineOperandTests MachineOperandTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineOperandTests) +target_link_libraries( + RCCBackendCodeGenMachineOperandTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.MachineOperand + COMMAND RCCBackendCodeGenMachineOperandTests +) + +add_executable(RCCBackendCodeGenPassManagerTests PassManagerTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenPassManagerTests) +target_link_libraries( + RCCBackendCodeGenPassManagerTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test(NAME RCCBackend.CodeGen.PassManager COMMAND RCCBackendCodeGenPassManagerTests) + +add_executable(RCCBackendCodeGenPassInstrumentationTests PassInstrumentationTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenPassInstrumentationTests) +target_link_libraries( + RCCBackendCodeGenPassInstrumentationTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test( + NAME RCCBackend.CodeGen.PassInstrumentation + COMMAND RCCBackendCodeGenPassInstrumentationTests +) + +add_executable(RCCBackendCodeGenMachinePassManagerTests MachinePassManagerTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachinePassManagerTests) +target_link_libraries( + RCCBackendCodeGenMachinePassManagerTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test( + NAME RCCBackend.CodeGen.MachinePassManager + COMMAND RCCBackendCodeGenMachinePassManagerTests +) + +add_subdirectory(GlobalISel) + +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(RCCBackendCodeGenMachineOptimizationTests MachineOptimizationTest.cpp) +target_include_directories( + RCCBackendCodeGenMachineOptimizationTests + PRIVATE "${RCC_BACKEND_SOURCE_DIR}/lib/Target/RISCV" +) +rcc_backend_configure_target(RCCBackendCodeGenMachineOptimizationTests) +target_link_libraries( + RCCBackendCodeGenMachineOptimizationTests + PRIVATE RCCBackend::CodeGen RCCBackend::RISCV +) +add_test(NAME RCCBackend.CodeGen.MachineOptimization COMMAND RCCBackendCodeGenMachineOptimizationTests) + +add_executable(RCCBackendCodeGenMachineSchedulingTests MachineSchedulingTest.cpp) +target_include_directories( + RCCBackendCodeGenMachineSchedulingTests + PRIVATE "${RCC_BACKEND_SOURCE_DIR}/lib/Target/RISCV" +) +rcc_backend_configure_target(RCCBackendCodeGenMachineSchedulingTests) +target_link_libraries( + RCCBackendCodeGenMachineSchedulingTests + PRIVATE RCCBackend::CodeGen RCCBackend::RISCV +) +add_test(NAME RCCBackend.CodeGen.MachineScheduling COMMAND RCCBackendCodeGenMachineSchedulingTests) + +add_executable(RCCBackendCodeGenPHIEliminationTests PHIEliminationTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenPHIEliminationTests) +target_link_libraries( + RCCBackendCodeGenPHIEliminationTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test( + NAME RCCBackend.CodeGen.PHIElimination + COMMAND RCCBackendCodeGenPHIEliminationTests +) + +add_executable( + RCCBackendCodeGenTwoAddressInstructionPassTests + TwoAddressInstructionPassTest.cpp +) +rcc_backend_configure_target(RCCBackendCodeGenTwoAddressInstructionPassTests) +target_link_libraries( + RCCBackendCodeGenTwoAddressInstructionPassTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test( + NAME RCCBackend.CodeGen.TwoAddressInstructionPass + COMMAND RCCBackendCodeGenTwoAddressInstructionPassTests +) + +add_executable(RCCBackendCodeGenTargetConstantTests TargetConstantTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenTargetConstantTests) +target_link_libraries(RCCBackendCodeGenTargetConstantTests PRIVATE RCCBackend::CodeGen) + +add_test( + NAME RCCBackend.CodeGen.TargetConstant + 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( + RCCBackendCodeGenTargetFunctionStateTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.TargetFunctionState + COMMAND RCCBackendCodeGenTargetFunctionStateTests +) + +add_executable(RCCBackendCodeGenTargetMIRTextInfoTests TargetMIRTextInfoTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenTargetMIRTextInfoTests) +target_link_libraries( + RCCBackendCodeGenTargetMIRTextInfoTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.TargetMIRTextInfo + COMMAND RCCBackendCodeGenTargetMIRTextInfoTests +) + +add_executable(RCCBackendCodeGenTargetRegisterInfoTests TargetRegisterInfoTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenTargetRegisterInfoTests) +target_link_libraries( + RCCBackendCodeGenTargetRegisterInfoTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.TargetRegisterInfo + COMMAND RCCBackendCodeGenTargetRegisterInfoTests +) + +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" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineDebugInfoTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineFrameInfoTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineFunctionClonerTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineFunctionRecordsTest.cpp" + "${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}/SlotIndexesTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/LiveVariablesTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/LiveIntervalTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/LiveIntervalsTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/LivePhysRegsTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/LiveRegUnitsTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/LiveTestSupport.h" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineMetadataTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineOperandTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineOptimizationTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/PassManagerTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/PassInstrumentationTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachinePassManagerTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineSchedulingTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/PBQPTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/RegisterAllocationTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/PHIEliminationTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/SSAExitTestSupport.h" + "${CMAKE_CURRENT_SOURCE_DIR}/TwoAddressInstructionPassTest.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" + "${CMAKE_CURRENT_SOURCE_DIR}/RegisterAllocationTestSupport.h" + "${CMAKE_CURRENT_SOURCE_DIR}/LiveRegMatrixTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/VirtRegMapTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/SpillerTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/RegAllocBasicTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/RegAllocFastTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/RegAllocPBQPTest.cpp" +) diff --git a/backend/unittests/CodeGen/CompilationUnitTest.cpp b/backend/unittests/CodeGen/CompilationUnitTest.cpp new file mode 100644 index 0000000..141d6db --- /dev/null +++ b/backend/unittests/CodeGen/CompilationUnitTest.cpp @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/CompilationUnit.h" + +#include "rcc/CodeGen/MachineFunction.h" + +#include <cstddef> +#include <iostream> +#include <utility> + +namespace { + +int fail(const char *Message) { + std::cerr << Message << '\n'; + return 1; +} + +} // namespace + +int main() { + rcc::CompilationUnit Unit; + auto FirstAnonymous = Unit.createMachineFunction(""); + auto SecondAnonymous = Unit.createMachineFunction(""); + if (!FirstAnonymous || !SecondAnonymous || Unit.functionCount() != 2 || + (*FirstAnonymous)->stableID().value() != 1 || (*SecondAnonymous)->stableID().value() != 2 || + Unit.function("") != nullptr) + return fail("CompilationUnit must retain anonymous functions by order and StableID"); + + auto First = Unit.createMachineFunction("first"); + if (!First || (*First)->stableID().value() != 3) + return fail("CompilationUnit must allocate the first stable function identity"); + + auto Duplicate = Unit.createMachineFunction("first"); + if (Duplicate || Unit.functionCount() != 3) + return fail("CompilationUnit must reject duplicate function names atomically"); + + auto Second = Unit.createMachineFunction("second"); + if (!Second || (*Second)->stableID().value() != 4) + return fail("Rejected function creation must not consume a StableID"); + if (Unit.functionCount() != 4 || Unit.functionAt(0) != *FirstAnonymous || + Unit.functionAt(1) != *SecondAnonymous || Unit.functionAt(2) != *First || + Unit.functionAt(3) != *Second) + return fail("CompilationUnit must preserve function document order"); + if (Unit.function("first") != *First || Unit.function("missing") != nullptr) + return fail("CompilationUnit must provide exact name lookup"); + if (Unit.function((*Second)->stableID()) != *Second || + Unit.function(rcc::MachineFunctionStableID::fromValue(99)) != nullptr) + return fail("CompilationUnit must provide exact StableID lookup"); + if (Unit.functionAt(4) != nullptr) + return fail("CompilationUnit must reject an out-of-range ordered lookup"); + + auto Zero = rcc::GlobalInitializer::zeroFill(4); + if (!Zero) + return fail("Global initializer creation must succeed"); + rcc::GlobalObjectDesc Object; + Object.Symbol = "global"; + Object.ObjectType = rcc::GenericConstantType::byte(); + Object.Size = 4; + Object.Initializer.emplace(std::move(*Zero)); + auto Global = Unit.addGlobalObject(std::move(Object)); + if (!Global || Unit.globalObjectCount() != 1 || Unit.globalObject("global") != *Global || + Unit.globalObjectAt(0) != *Global || Unit.globalObjectAt(1) != nullptr) + return fail("CompilationUnit must own and index global objects"); + + auto ConflictingFunction = Unit.createMachineFunction("global"); + if (ConflictingFunction || Unit.functionCount() != 4) + return fail("CompilationUnit must reject function/global symbol conflicts atomically"); + + rcc::GlobalObjectDesc ConflictingGlobal; + ConflictingGlobal.Symbol = "first"; + ConflictingGlobal.ObjectType = rcc::GenericConstantType::byte(); + ConflictingGlobal.Size = 1; + auto Byte = rcc::GlobalInitializer::bytes({std::byte{1}}); + if (!Byte) + return fail("Byte initializer creation must succeed"); + ConflictingGlobal.Initializer.emplace(std::move(*Byte)); + if (Unit.addGlobalObject(std::move(ConflictingGlobal)) || Unit.globalObjectCount() != 1) + return fail("CompilationUnit must reject global/function symbol conflicts atomically"); + + auto MismatchedBytes = rcc::GlobalInitializer::bytes({std::byte{1}, std::byte{2}}); + if (!MismatchedBytes) + return fail("Mismatched initializer construction must succeed before object verification"); + rcc::GlobalObjectDesc MismatchedObject; + MismatchedObject.Symbol = "mismatched"; + MismatchedObject.ObjectType = rcc::GenericConstantType::byte(); + MismatchedObject.Size = 1; + MismatchedObject.Initializer.emplace(std::move(*MismatchedBytes)); + if (Unit.addGlobalObject(std::move(MismatchedObject)) || Unit.globalObjectCount() != 1) + return fail("CompilationUnit must reject mismatched global initializer sizes atomically"); + + auto Verification = Unit.verify(); + if (!Verification) + return fail("A valid CompilationUnit must pass verification"); + + rcc::CompilationUnit Moved = std::move(Unit); + if (Moved.functionCount() != 4 || Moved.functionAt(0) != *FirstAnonymous || + Moved.functionAt(1) != *SecondAnonymous || Moved.function("first") != *First || + Moved.function("second") != *Second || Moved.globalObject("global") != *Global) + return fail("Moving a CompilationUnit must preserve owned object addresses and indices"); + Verification = Moved.verify(); + if (!Verification) + return fail("A moved CompilationUnit must pass verification"); + return 0; +} diff --git a/backend/unittests/CodeGen/CoreTypesTest.cpp b/backend/unittests/CodeGen/CoreTypesTest.cpp new file mode 100644 index 0000000..8ad0ce6 --- /dev/null +++ b/backend/unittests/CodeGen/CoreTypesTest.cpp @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/LowLevelType.h" +#include "rcc/CodeGen/MachineFunctionProperties.h" +#include "rcc/CodeGen/Register.h" +#include "rcc/CodeGen/StableID.h" + +#include <cstdint> +#include <iostream> +#include <string_view> +#include <type_traits> + +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; +}; + +void testLowLevelTypes(TestContext &Context) { + const rcc::LowLevelType Invalid; + Context.expect(!Invalid.isValid(), "default LowLevelType must be invalid"); + Context.expect(Invalid.toString() == "LLT_invalid", + "invalid LowLevelType spelling must be stable"); + + const rcc::LowLevelType Token = rcc::LowLevelType::token(); + Context.expect(Token.isToken(), "token LowLevelType must preserve its kind"); + Context.expect(Token.scalarBitWidth() == 0, "token LowLevelType must have no bit width"); + Context.expect(Token.toString() == "s0", "token LowLevelType must use the MIR token spelling"); + + auto Scalar = rcc::LowLevelType::makeCategoryNeutralScalar(3); + auto SameScalar = rcc::LowLevelType::makeCategoryNeutralScalar(3); + Context.expect(Scalar && SameScalar, "arbitrary positive scalar widths must be accepted"); + if (Scalar && SameScalar) { + Context.expect(Scalar->isCategoryNeutralScalar(), + "scalar LowLevelType must preserve its category-neutral kind"); + Context.expect(*Scalar == *SameScalar, + "equal-width scalars must have one canonical low-level type"); + Context.expect(Scalar->toString() == "s3", + "scalar LowLevelType spelling must include its width"); + } + + Context.expect(!rcc::LowLevelType::makeCategoryNeutralScalar(0), + "zero-width scalar LowLevelType must be rejected"); + Context.expect(!rcc::LowLevelType::makeCategoryNeutralScalar(std::uint64_t{1} << 16), + "scalar LowLevelType wider than the encoding must be rejected"); + + auto Pointer = rcc::LowLevelType::makePointer(rcc::LowLevelType::MaxAddressSpace, 64); + Context.expect(Pointer.has_value(), "maximum encodable pointer address space must be accepted"); + if (Pointer) { + Context.expect(Pointer->isPointer(), "pointer LowLevelType must preserve its kind"); + Context.expect(Pointer->addressSpace() == rcc::LowLevelType::MaxAddressSpace, + "pointer LowLevelType must preserve its address space"); + Context.expect(Pointer->toString() == "p16777215:64", + "pointer LowLevelType spelling must preserve address space and width"); + } + Context.expect(!rcc::LowLevelType::makePointer(std::uint64_t{1} << 24, 64), + "pointer address spaces wider than the encoding must be rejected"); + + auto I33 = rcc::LowLevelType::makeCategoryNeutralScalar(33); + if (!I33) { + Context.expect(false, "i33 construction must succeed before vector tests"); + return; + } + + auto FixedVector = rcc::LowLevelType::makeFixedVector(4, *I33); + Context.expect(FixedVector.has_value(), "fixed integer vector LowLevelType must be accepted"); + if (FixedVector) { + Context.expect(FixedVector->isVector(), "fixed vector LowLevelType must preserve vector kind"); + Context.expect(!FixedVector->isScalableVector(), + "fixed vector LowLevelType must not be marked scalable"); + Context.expect(FixedVector->minimumElementCount() == 4, + "fixed vector LowLevelType must preserve element count"); + Context.expect(FixedVector->scalarElementType() == I33, + "fixed vector LowLevelType must preserve its scalar element type"); + Context.expect(FixedVector->toString() == "<4 x s33>", + "fixed vector LowLevelType spelling must be stable"); + } + + auto S16 = rcc::LowLevelType::makeCategoryNeutralScalar(16); + if (!S16) { + Context.expect(false, "s16 construction must succeed before vector tests"); + return; + } + auto ScalableVector = rcc::LowLevelType::makeScalableVector(1, *S16); + Context.expect(ScalableVector.has_value(), "scalable vector with one minimum lane must work"); + if (ScalableVector) { + Context.expect(ScalableVector->isScalableVector(), + "scalable vector LowLevelType must preserve scalability"); + Context.expect(ScalableVector->isVectorValue(), + "category-neutral vectors must preserve their value kind"); + Context.expect(ScalableVector->scalarElementType() == S16, + "scalable vectors must preserve their element type"); + Context.expect(ScalableVector->toString() == "<vscale x 1 x s16>", + "scalable vector spelling must preserve its shape"); + } + + Context.expect(!rcc::LowLevelType::makeFixedVector(1, *I33), + "single-lane fixed vectors must be rejected"); + Context.expect(!rcc::LowLevelType::makeScalableVector(0, *I33), + "zero-lane scalable vectors must be rejected"); + Context.expect(!rcc::LowLevelType::makeFixedVector(4, Token), + "token vector elements must be rejected"); + if (FixedVector) { + Context.expect(!rcc::LowLevelType::makeFixedVector(4, *FixedVector), + "nested LowLevelType vectors must be rejected"); + } +} + +void testRegisters(TestContext &Context) { + const rcc::Register NoRegister{0}; + Context.expect(NoRegister.isNoRegister(), "Register{0} must represent NoRegister"); + Context.expect(NoRegister.toString() == "noreg", "NoRegister spelling must be stable"); + + auto PhysReg = rcc::PhysicalRegister::create(7); + Context.expect(PhysReg.has_value(), "positive physical register numbers must be accepted"); + if (PhysReg) { + const rcc::Register Reg = rcc::Register::fromPhysical(*PhysReg); + Context.expect(Reg.isPhysical(), "physical Register must preserve its category"); + Context.expect(Reg.physicalRegister() == PhysReg, + "physical Register must round-trip through PhysicalRegister"); + Context.expect(Reg.toString() == "$physreg7", + "target-independent physical register spelling must be stable"); + } + + Context.expect(!rcc::PhysicalRegister::create(0), "physical register zero must remain reserved"); + Context.expect(!rcc::Register::makePhysical(std::uint64_t{1} << 31), + "out-of-range physical registers must be rejected"); + + auto FirstVirtual = rcc::Register::makeVirtual(0); + auto LastVirtual = rcc::Register::makeVirtual((std::uint64_t{1} << 31) - 1); + Context.expect(FirstVirtual && LastVirtual, + "the complete virtual register index domain must be accepted"); + if (FirstVirtual) { + Context.expect(FirstVirtual->isVirtual(), "virtual Register must preserve its category"); + Context.expect(FirstVirtual->virtualIndex() == 0, "virtual Register must preserve index zero"); + Context.expect(FirstVirtual->toString() == "%0", + "virtual Register spelling must match MIR numbering"); + } + Context.expect(!rcc::Register::makeVirtual(std::uint64_t{1} << 31), + "out-of-range virtual register indices must be rejected"); +} + +void testStableIDs(TestContext &Context) { + static_assert(!std::is_same_v<rcc::MachineFunctionStableID, rcc::MachineBlockStableID>); + static_assert(!std::is_same_v<rcc::MachineBlockStableID, rcc::MachineInstrStableID>); + static_assert(!std::is_copy_constructible_v<rcc::StableIDGenerator<rcc::MachineInstrStableID>>); + + const rcc::MachineInstrStableID Invalid; + Context.expect(!Invalid.isValid(), "zero StableID must be invalid"); + + rcc::StableIDGenerator<rcc::MachineInstrStableID> Generator; + auto First = Generator.allocate(); + auto Second = Generator.allocate(); + Context.expect(First && Second, "StableID generator must allocate IDs"); + if (First && Second) { + Context.expect(First->value() == 1 && Second->value() == 2, + "StableID generator must allocate monotonically"); + Context.expect(*First != *Second, "StableID generator must not reuse live IDs"); + } +} + +void testMachineFunctionProperties(TestContext &Context) { + using enum rcc::MachineFunctionProperty; + + rcc::MachineFunctionProperties Properties{IsSSA, TracksLiveness}; + Context.expect(Properties.count() == 2, "property set must count active properties"); + Context.expect(Properties.has(IsSSA), "property set must retain IsSSA"); + Context.expect(Properties.toString() == "IsSSA, TracksLiveness", + "property spelling must have deterministic enum order"); + + const auto Original = Properties; + auto MissingRequired = + Properties.applyTransition(rcc::MachineFunctionProperties{Legalized}, + rcc::MachineFunctionProperties{RegBankSelected}, {}); + Context.expect(!MissingRequired, "property transition must reject missing requirements"); + Context.expect(Properties == Original, "failed property transition must be atomic"); + if (!MissingRequired) { + Context.expect(MissingRequired.error().code() == rcc::ErrorCode::VerificationError, + "missing property requirements must be verification errors"); + } + + auto ConflictingTransition = Properties.applyTransition( + {}, rcc::MachineFunctionProperties{Selected}, rcc::MachineFunctionProperties{Selected}); + Context.expect(!ConflictingTransition, + "property transition must reject overlapping sets and clears"); + Context.expect(Properties == Original, "conflicting property transition must be atomic"); + + auto ValidTransition = Properties.applyTransition(rcc::MachineFunctionProperties{IsSSA}, + rcc::MachineFunctionProperties{NoPHIs}, + rcc::MachineFunctionProperties{IsSSA}); + Context.expect(ValidTransition.has_value(), "valid property transition must succeed"); + Context.expect(Properties.has(NoPHIs) && !Properties.has(IsSSA), + "valid property transition must apply sets and clears together"); + + auto RawRoundTrip = rcc::MachineFunctionProperties::fromRaw(Properties.rawValue()); + Context.expect(RawRoundTrip && *RawRoundTrip == Properties, + "known property masks must round-trip"); + Context.expect(!rcc::MachineFunctionProperties::fromRaw(std::uint64_t{1} << 63), + "unknown property bits must be rejected"); +} + +} // namespace + +int main() { + TestContext Context; + testLowLevelTypes(Context); + testRegisters(Context); + testStableIDs(Context); + testMachineFunctionProperties(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/GenericOpcodesTest.cpp b/backend/unittests/CodeGen/GenericOpcodesTest.cpp new file mode 100644 index 0000000..f946c90 --- /dev/null +++ b/backend/unittests/CodeGen/GenericOpcodesTest.cpp @@ -0,0 +1,748 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/GenericOpcodes.h" + +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineMemOperand.h" + +#include <array> +#include <bit> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <ranges> +#include <span> +#include <string> +#include <string_view> +#include <unordered_set> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +[[nodiscard]] std::vector<rcc::InstructionDescriptor> copyDescriptors() { + const auto Source = rcc::genericInstructionDescriptors(); + return {Source.begin(), Source.end()}; +} + +[[nodiscard]] rcc::MachineOperand registerOperand(rcc::Register Reg, bool IsDef) { + rcc::RegisterOperandFlags Flags; + Flags.set(IsDef ? rcc::RegisterOperandFlag::Define : rcc::RegisterOperandFlag::Use); + auto Operand = rcc::MachineOperand::registerOperand(Reg, Flags); + if (!Operand) + std::terminate(); + return std::move(*Operand); +} + +[[nodiscard]] rcc::Register createVirtualRegister(rcc::MachineFunction &Function, + rcc::LowLevelType Type) { + auto Reg = Function.registerInfo().createVirtualRegister(Type); + if (!Reg) + std::terminate(); + return *Reg; +} + +[[nodiscard]] rcc::MachineInstr &createInstruction(rcc::MachineFunction &Function, + rcc::GenericOpcode OpcodeValue, + std::vector<rcc::MachineOperand> Operands) { + auto Instruction = Function.createInstruction(rcc::opcode(OpcodeValue)); + if (!Instruction) + std::terminate(); + for (rcc::MachineOperand &Operand : Operands) { + if (!(*Instruction)->addOperand(std::move(Operand))) + std::terminate(); + } + return **Instruction; +} + +[[nodiscard]] rcc::MachineMemOperandDesc +memoryDescription(rcc::MemoryAccessFlags Accesses, std::uint64_t Bytes, + std::uint32_t AddressSpace = 0, + rcc::AtomicOperation AtomicKind = rcc::AtomicOperation::None) { + rcc::MachineMemOperandDesc Description; + Description.Accesses = Accesses; + Description.AtomicKind = AtomicKind; + if (AtomicKind == rcc::AtomicOperation::ReadModifyWrite) { + Description.SuccessOrdering = rcc::AtomicOrdering::AcquireRelease; + } else if (AtomicKind == rcc::AtomicOperation::CompareExchange) { + Description.SuccessOrdering = rcc::AtomicOrdering::AcquireRelease; + Description.FailureOrdering = rcc::AtomicOrdering::Acquire; + } + Description.Size = *rcc::MemorySize::fixed(Bytes); + Description.Align = *rcc::Alignment::create(std::bit_floor(Bytes)); + Description.AddressSpace = AddressSpace; + Description.Base = rcc::MemoryBase::stack(); + return Description; +} + +void addMemOperand(rcc::MachineFunction &Function, rcc::MachineInstr &Instruction, + rcc::MachineMemOperandDesc Description) { + auto Memory = Function.createMemOperand(std::move(Description)); + if (!Memory || !Instruction.addMemOperand(**Memory)) + std::terminate(); +} + +void testCompleteRegistry(TestContext &Context) { + static_assert(rcc::CorePseudoOpcodeCount == 52); + static_assert(rcc::GenericOpcodeCount == 278); + + const auto Descriptors = rcc::genericInstructionDescriptors(); + Context.expect(Descriptors.size() == 330, + "the target-independent registry must contain 52 core and 278 generic opcodes"); + + std::unordered_set<std::string> Names; + std::unordered_set<rcc::Opcode> Opcodes; + bool ClassesAreCorrect = true; + for (const rcc::InstructionDescriptor &Descriptor : Descriptors) { + ClassesAreCorrect &= Descriptor.InstructionOpcode.isValid() && !Descriptor.Name.empty() && + Names.insert(Descriptor.Name).second && + Opcodes.insert(Descriptor.InstructionOpcode).second; + if (rcc::isGenericOpcode(Descriptor.InstructionOpcode)) { + ClassesAreCorrect &= Descriptor.Flags.has(rcc::InstructionFlag::Generic) && + !Descriptor.Flags.has(rcc::InstructionFlag::Pseudo); + } else { + ClassesAreCorrect &= rcc::isCorePseudoOpcode(Descriptor.InstructionOpcode) && + Descriptor.Flags.has(rcc::InstructionFlag::Pseudo) && + !Descriptor.Flags.has(rcc::InstructionFlag::Generic); + } + } + Context.expect(ClassesAreCorrect, + "every target-independent opcode must have one valid name, value, and class"); + Context.expect(rcc::getGenericOpcodeName(rcc::GenericOpcode::PHI) == "PHI" && + rcc::getGenericOpcodeName(rcc::GenericOpcode::G_ASSERT_SEXT) == + "G_ASSERT_SEXT" && + rcc::getGenericOpcodeName(rcc::GenericOpcode::G_UBFX) == "G_UBFX" && + rcc::getGenericOpcodeName(rcc::GenericOpcode::Invalid).empty(), + "opcode names must cover both ranges without assigning marker names"); + + auto Set = rcc::InstructionSet::create(copyDescriptors()); + Context.expect(Set.has_value(), "the complete target-independent descriptor set must validate"); +} + +void testCoreOpcodeContracts(TestContext &Context) { + struct Contract final { + rcc::GenericOpcode OpcodeValue; + std::size_t DescriptorOperands; + std::size_t ExplicitDefs; + bool Variadic; + std::size_t MinimumRepetitions; + bool Meta; + bool TerminalEmission; + rcc::PseudoOwner Owner; + rcc::PipelineStage Deadline; + }; + +#define RCC_META(Name, Operands, Defs, Variadic, Minimum) \ + Contract{rcc::GenericOpcode::Name, \ + Operands, \ + Defs, \ + Variadic, \ + Minimum, \ + true, \ + false, \ + rcc::PseudoOwner::None, \ + rcc::PipelineStage::Emission} +#define RCC_PSEUDO(Name, Operands, Defs, Variadic, Minimum, Owner, Deadline) \ + Contract{rcc::GenericOpcode::Name, \ + Operands, \ + Defs, \ + Variadic, \ + Minimum, \ + false, \ + false, \ + rcc::PseudoOwner::Owner, \ + rcc::PipelineStage::Deadline} +#define RCC_TERMINAL(Name, Operands, Defs, Variadic, Minimum) \ + Contract { \ + rcc::GenericOpcode::Name, Operands, Defs, Variadic, Minimum, false, true, \ + rcc::PseudoOwner::AsmPrinter, rcc::PipelineStage::Emission \ + } + + const std::array Contracts{ + RCC_PSEUDO(PHI, 3, 1, true, 1, PhiElimination, SSAEliminated), + RCC_TERMINAL(INLINEASM, 1, 0, true, 0), + RCC_TERMINAL(INLINEASM_BR, 1, 0, true, 0), + RCC_META(CFI_INSTRUCTION, 1, 0, false, 0), + RCC_META(EH_LABEL, 1, 0, false, 0), + RCC_META(GC_LABEL, 1, 0, false, 0), + RCC_META(ANNOTATION_LABEL, 1, 0, false, 0), + RCC_META(KILL, 1, 0, true, 0), + RCC_PSEUDO(EXTRACT_SUBREG, 3, 1, false, 0, PostRAExpansion, PostRAFinalized), + RCC_PSEUDO(INSERT_SUBREG, 4, 1, false, 0, TwoAddressRewrite, TiedOpsRewritten), + RCC_META(IMPLICIT_DEF, 1, 1, false, 0), + RCC_PSEUDO(INIT_UNDEF, 1, 1, false, 0, RegisterAllocator, RegisterAllocated), + RCC_PSEUDO(SUBREG_TO_REG, 3, 1, false, 0, PostRAExpansion, PostRAFinalized), + RCC_PSEUDO(COPY_TO_REGCLASS, 3, 1, false, 0, RegisterAllocator, RegisterAllocated), + RCC_META(DBG_VALUE, 1, 0, true, 0), + RCC_META(DBG_VALUE_LIST, 1, 0, true, 0), + RCC_META(DBG_INSTR_REF, 1, 0, true, 0), + RCC_META(DBG_PHI, 1, 0, true, 0), + RCC_META(DBG_LABEL, 1, 0, false, 0), + RCC_PSEUDO(REG_SEQUENCE, 3, 1, true, 1, TwoAddressRewrite, TiedOpsRewritten), + RCC_PSEUDO(COPY, 2, 1, false, 0, PostRAExpansion, PostRAFinalized), + RCC_PSEUDO(COPY_LANEMASK, 3, 1, false, 0, PostRAExpansion, PostRAFinalized), + RCC_META(BUNDLE, 1, 0, true, 0), + RCC_META(LIFETIME_START, 1, 0, false, 0), + RCC_META(LIFETIME_END, 1, 0, false, 0), + RCC_META(PSEUDO_PROBE, 4, 0, false, 0), + RCC_META(ARITH_FENCE, 2, 1, false, 0), + RCC_TERMINAL(STACKMAP, 3, 0, true, 0), + RCC_TERMINAL(FENTRY_CALL, 0, 0, false, 0), + RCC_TERMINAL(PATCHPOINT, 7, 1, true, 0), + RCC_PSEUDO(LOAD_STACK_GUARD, 1, 1, false, 0, InstructionSelect, Selected), + RCC_PSEUDO(PREALLOCATED_SETUP, 1, 0, false, 0, InstructionSelect, Selected), + RCC_PSEUDO(PREALLOCATED_ARG, 3, 1, false, 0, InstructionSelect, Selected), + RCC_TERMINAL(STATEPOINT, 1, 0, true, 0), + RCC_META(LOCAL_ESCAPE, 2, 0, false, 0), + RCC_TERMINAL(FAULTING_OP, 2, 1, true, 0), + RCC_TERMINAL(PATCHABLE_OP, 1, 0, true, 0), + RCC_TERMINAL(PATCHABLE_FUNCTION_ENTER, 0, 0, false, 0), + RCC_TERMINAL(PATCHABLE_RET, 1, 0, true, 0), + RCC_TERMINAL(PATCHABLE_FUNCTION_EXIT, 0, 0, false, 0), + RCC_TERMINAL(PATCHABLE_TAIL_CALL, 1, 0, true, 0), + RCC_TERMINAL(PATCHABLE_EVENT_CALL, 2, 0, false, 0), + RCC_TERMINAL(PATCHABLE_TYPED_EVENT_CALL, 3, 0, false, 0), + RCC_PSEUDO(ICALL_BRANCH_FUNNEL, 1, 0, true, 0, PostRAExpansion, PostRAFinalized), + RCC_META(FAKE_USE, 1, 0, true, 0), + RCC_META(MEMBARRIER, 0, 0, false, 0), + RCC_META(JUMP_TABLE_DEBUG_INFO, 1, 0, false, 0), + RCC_META(RELOC_NONE, 1, 0, false, 0), + RCC_META(CONVERGENCECTRL_ENTRY, 1, 1, false, 0), + RCC_META(CONVERGENCECTRL_ANCHOR, 1, 1, false, 0), + RCC_META(CONVERGENCECTRL_LOOP, 2, 1, false, 0), + RCC_META(CONVERGENCECTRL_GLUE, 1, 0, false, 0), + }; +#undef RCC_TERMINAL +#undef RCC_PSEUDO +#undef RCC_META + + static_assert(Contracts.size() == rcc::CorePseudoOpcodeCount); + const auto Descriptors = rcc::genericInstructionDescriptors(); + bool Matches = true; + for (const Contract &Expected : Contracts) { + const auto Found = std::ranges::find_if( + Descriptors, [&Expected](const rcc::InstructionDescriptor &Descriptor) { + return Descriptor.InstructionOpcode == rcc::opcode(Expected.OpcodeValue); + }); + if (Found == Descriptors.end()) { + Matches = false; + continue; + } + Matches &= + Found->Operands.size() == Expected.DescriptorOperands && + Found->NumExplicitDefs == Expected.ExplicitDefs && Found->Variadic == Expected.Variadic && + Found->MinimumVariadicRepetitions == Expected.MinimumRepetitions && + Found->Flags.has(rcc::InstructionFlag::Meta) == Expected.Meta && + Found->Flags.has(rcc::InstructionFlag::TerminalEmission) == Expected.TerminalEmission && + Found->Owner == Expected.Owner && Found->Deadline == Expected.Deadline; + } + Context.expect(Matches, + "all 52 core opcodes must retain their reviewed shape and lifecycle contracts"); + + const auto InlineAsm = std::ranges::find_if(Descriptors, [](const auto &Descriptor) { + return Descriptor.InstructionOpcode == rcc::opcode(rcc::GenericOpcode::INLINEASM); + }); + const auto InlineAsmBr = std::ranges::find_if(Descriptors, [](const auto &Descriptor) { + return Descriptor.InstructionOpcode == rcc::opcode(rcc::GenericOpcode::INLINEASM_BR); + }); + const auto Copy = std::ranges::find_if(Descriptors, [](const auto &Descriptor) { + return Descriptor.InstructionOpcode == rcc::opcode(rcc::GenericOpcode::COPY); + }); + 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"); + Context.expect(Copy != Descriptors.end() && Copy->AllowExtraImplicitOperands, + "COPY must allow dynamic implicit super-register operands"); +} + +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::LowLevelType::makeCategoryNeutralScalar(32); + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto I1 = rcc::LowLevelType::makeCategoryNeutralScalar(1); + auto S16 = rcc::LowLevelType::makeCategoryNeutralScalar(16); + auto F32 = rcc::LowLevelType::makeCategoryNeutralScalar(32); + auto F64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto P64 = rcc::LowLevelType::makePointer(0, 64); + auto V2I32 = + I32 ? rcc::LowLevelType::makeFixedVector(2, *I32) : rcc::Expected<rcc::LowLevelType>{}; + auto V4I32 = + I32 ? rcc::LowLevelType::makeFixedVector(4, *I32) : rcc::Expected<rcc::LowLevelType>{}; + auto V2P64 = + P64 ? rcc::LowLevelType::makeFixedVector(2, *P64) : rcc::Expected<rcc::LowLevelType>{}; + if (!I1 || !I32 || !I64 || !S16 || !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 Scalar16 = createVirtualRegister(**Function, *S16); + const rcc::Register Vec2A = createVirtualRegister(**Function, *V2I32); + const rcc::Register Vec2B = createVirtualRegister(**Function, *V2I32); + const rcc::Register Vec4 = createVirtualRegister(**Function, *V4I32); + const rcc::Register PointerVector = createVirtualRegister(**Function, *V2P64); + if (!Dst32 || !Src32A || !Src32B || !Wide || !Address || !NewAddress) + std::terminate(); + + std::vector<rcc::MachineOperand> AddOperands; + AddOperands.push_back(registerOperand(*Dst32, true)); + AddOperands.push_back(registerOperand(*Src32A, false)); + AddOperands.push_back(registerOperand(*Src32B, false)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ADD), AddOperands, + (*Function)->registerInfo()) + .has_value(), + "same-typed integer operands must satisfy G_ADD"); + AddOperands.back() = registerOperand(*Wide, false); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ADD), AddOperands, + (*Function)->registerInfo()), + "G_ADD must reject a source from a different LowLevelType equality group"); + + auto ZeroImmediate = rcc::APInt::fromUnsigned(64, 0); + std::vector<rcc::MachineOperand> PointerConstant; + PointerConstant.push_back(registerOperand(*Address, true)); + if (ZeroImmediate) + PointerConstant.push_back(rcc::MachineOperand::integerImmediate(std::move(*ZeroImmediate))); + Context.expect(PointerConstant.size() == 2 && + Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_CONSTANT), + PointerConstant, (*Function)->registerInfo()) + .has_value(), + "G_CONSTANT must accept a scalar pointer destination"); + + auto Block0 = (*Function)->createBasicBlock(); + auto Block1 = (*Function)->createBasicBlock(); + if (!Block0 || !Block1 || !(*Function)->appendBlock(**Block0) || + !(*Function)->appendBlock(**Block1)) { + std::terminate(); + } + std::vector<rcc::MachineOperand> PhiOperands; + PhiOperands.push_back(registerOperand(*Dst32, true)); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_PHI), PhiOperands, + (*Function)->registerInfo()), + "G_PHI must require at least one value and predecessor pair"); + PhiOperands.push_back(registerOperand(*Src32A, false)); + PhiOperands.push_back(rcc::MachineOperand::basicBlock(**Block0)); + PhiOperands.push_back(registerOperand(*Src32B, false)); + PhiOperands.push_back(rcc::MachineOperand::basicBlock(**Block1)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_PHI), PhiOperands, + (*Function)->registerInfo()) + .has_value(), + "G_PHI must accept repeated value and predecessor pairs"); + PhiOperands.pop_back(); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_PHI), PhiOperands, + (*Function)->registerInfo()), + "G_PHI must reject an incomplete variadic operand pair"); + + std::vector<rcc::MachineOperand> IndexedLoad; + IndexedLoad.push_back(registerOperand(*Dst32, true)); + IndexedLoad.push_back(registerOperand(*NewAddress, true)); + IndexedLoad.push_back(registerOperand(*Address, false)); + IndexedLoad.push_back(registerOperand(*Wide, false)); + IndexedLoad.push_back(rcc::MachineOperand::immediate(1)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_INDEXED_LOAD), + IndexedLoad, (*Function)->registerInfo()) + .has_value(), + "indexed loads must keep address and integer offset type groups independent"); + + std::vector<rcc::MachineOperand> UnmergeOperands; + UnmergeOperands.push_back(registerOperand(*Dst32, true)); + UnmergeOperands.push_back(registerOperand(*Wide, false)); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_UNMERGE_VALUES), + UnmergeOperands, (*Function)->registerInfo()), + "G_UNMERGE_VALUES must require at least two definitions"); + UnmergeOperands.insert(UnmergeOperands.begin() + 1, registerOperand(*Src32B, true)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_UNMERGE_VALUES), + UnmergeOperands, (*Function)->registerInfo()) + .has_value(), + "two homogeneous halves must exactly cover a G_UNMERGE_VALUES source"); + + std::vector<rcc::MachineOperand> MergeOperands; + MergeOperands.push_back(registerOperand(*Wide, true)); + MergeOperands.push_back(registerOperand(*Src32A, false)); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_MERGE_VALUES), + MergeOperands, (*Function)->registerInfo()), + "G_MERGE_VALUES must require at least two sources"); + MergeOperands.push_back(registerOperand(*Src32B, false)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_MERGE_VALUES), + MergeOperands, (*Function)->registerInfo()) + .has_value(), + "two homogeneous sources must exactly fill a G_MERGE_VALUES result"); + + std::vector<rcc::MachineOperand> ConcatOperands; + ConcatOperands.push_back(registerOperand(Vec4, true)); + ConcatOperands.push_back(registerOperand(Vec2A, false)); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_CONCAT_VECTORS), + ConcatOperands, (*Function)->registerInfo()), + "G_CONCAT_VECTORS must require at least two source vectors"); + ConcatOperands.push_back(registerOperand(Vec2B, false)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_CONCAT_VECTORS), + ConcatOperands, (*Function)->registerInfo()) + .has_value(), + "two vector sources must exactly tile a G_CONCAT_VECTORS result"); + + std::vector<rcc::MachineOperand> FrameIndexOperands; + FrameIndexOperands.push_back(registerOperand(PointerVector, true)); + FrameIndexOperands.push_back(rcc::MachineOperand::frameIndex(rcc::FrameIndex::fromValue(0))); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_FRAME_INDEX), + FrameIndexOperands, (*Function)->registerInfo()), + "G_FRAME_INDEX must reject a vector-of-pointers result"); + + std::vector<rcc::MachineOperand> PtrAuthOperands; + PtrAuthOperands.push_back(registerOperand(*NewAddress, true)); + PtrAuthOperands.push_back(registerOperand(*Address, false)); + PtrAuthOperands.push_back(rcc::MachineOperand::immediate(0)); + PtrAuthOperands.push_back(registerOperand(*Wide, false)); + PtrAuthOperands.push_back(rcc::MachineOperand::immediate(7)); + Context.expect( + Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_PTRAUTH_GLOBAL_VALUE), + PtrAuthOperands, (*Function)->registerInfo()) + .has_value(), + "G_PTRAUTH_GLOBAL_VALUE must accept pointer and discriminator registers"); + auto Symbol = rcc::MachineOperand::globalSymbol("global"); + if (!Symbol) + std::terminate(); + PtrAuthOperands[1] = std::move(*Symbol); + Context.expect( + !Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_PTRAUTH_GLOBAL_VALUE), + PtrAuthOperands, (*Function)->registerInfo()), + "G_PTRAUTH_GLOBAL_VALUE must reject a symbol in place of its address register"); + + std::vector<rcc::MachineOperand> ExtensionOperands; + ExtensionOperands.push_back(registerOperand(*Wide, true)); + ExtensionOperands.push_back(registerOperand(*Src32A, false)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ZEXT), + ExtensionOperands, (*Function)->registerInfo()) + .has_value(), + "G_ZEXT must accept a wider destination with matching shape"); + ExtensionOperands[0] = registerOperand(*Dst32, true); + ExtensionOperands[1] = registerOperand(*Wide, false); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ZEXT), + ExtensionOperands, (*Function)->registerInfo()), + "G_ZEXT must reject a narrower destination"); + + auto Predicate = rcc::MachineOperand::integerPredicate(rcc::IntegerPredicate::Equal); + if (!Predicate) + std::terminate(); + std::vector<rcc::MachineOperand> CompareOperands; + CompareOperands.push_back(registerOperand(Cond, true)); + CompareOperands.push_back(std::move(*Predicate)); + CompareOperands.push_back(registerOperand(*Src32A, false)); + CompareOperands.push_back(registerOperand(*Src32B, false)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ICMP), + CompareOperands, (*Function)->registerInfo()) + .has_value(), + "G_ICMP must accept an i1 result and homogeneous integer sources"); + CompareOperands[0] = registerOperand(*Dst32, true); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ICMP), + CompareOperands, (*Function)->registerInfo()) + .has_value(), + "G_ICMP must accept a legalized integer result container"); + CompareOperands[2] = registerOperand(*Address, false); + CompareOperands[3] = registerOperand(*NewAddress, false); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ICMP), + CompareOperands, (*Function)->registerInfo()) + .has_value(), + "G_ICMP must accept homogeneous pointer sources"); + CompareOperands[3] = registerOperand(*Src32B, false); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ICMP), + CompareOperands, (*Function)->registerInfo()), + "G_ICMP must reject mixed pointer and integer sources"); + + std::vector<rcc::MachineOperand> FloatingLoad; + FloatingLoad.push_back(registerOperand(Float64, true)); + FloatingLoad.push_back(registerOperand(*Address, false)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_FPEXTLOAD), + FloatingLoad, (*Function)->registerInfo()) + .has_value(), + "G_FPEXTLOAD must accept a scalar floating result and pointer address"); + + const std::array<std::uint64_t, 1> FloatWords{0}; + auto FloatBits = rcc::APFloat::create(rcc::FloatSemantics::IEEESingle, FloatWords); + if (!FloatBits) + std::terminate(); + std::vector<rcc::MachineOperand> FConstantOperands; + FConstantOperands.push_back(registerOperand(Float32, true)); + FConstantOperands.push_back(rcc::MachineOperand::floatingImmediate(*FloatBits)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_FCONSTANT), + FConstantOperands, (*Function)->registerInfo()) + .has_value(), + "G_FCONSTANT payload semantics must match its scalar destination"); + FConstantOperands[0] = registerOperand(Float64, true); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_FCONSTANT), + FConstantOperands, (*Function)->registerInfo()), + "G_FCONSTANT must reject mismatched floating semantics"); + + const std::array<std::uint64_t, 1> BFloatWords{0}; + auto BFloatBits = rcc::APFloat::create(rcc::FloatSemantics::BFloat, BFloatWords); + if (!BFloatBits) + std::terminate(); + FConstantOperands[0] = registerOperand(Scalar16, true); + FConstantOperands[1] = rcc::MachineOperand::floatingImmediate(*BFloatBits); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_FCONSTANT), + FConstantOperands, (*Function)->registerInfo()) + .has_value(), + "G_FCONSTANT must accept a bit-width-compatible LLVM scalar LowLevelType"); +} + +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::LowLevelType I1 = *rcc::LowLevelType::makeCategoryNeutralScalar(1); + const rcc::LowLevelType I32 = *rcc::LowLevelType::makeCategoryNeutralScalar(32); + const rcc::LowLevelType I64 = *rcc::LowLevelType::makeCategoryNeutralScalar(64); + const rcc::LowLevelType F64 = *rcc::LowLevelType::makeCategoryNeutralScalar(64); + const rcc::LowLevelType P0 = *rcc::LowLevelType::makePointer(0, 64); + const rcc::LowLevelType P1 = *rcc::LowLevelType::makePointer(1, 64); + const rcc::Register Dst32 = createVirtualRegister(**Function, I32); + const rcc::Register Dst64 = createVirtualRegister(**Function, I64); + const rcc::Register Src32 = createVirtualRegister(**Function, I32); + const rcc::Register Src64 = createVirtualRegister(**Function, I64); + const rcc::Register Float64 = createVirtualRegister(**Function, F64); + const rcc::Register Status = createVirtualRegister(**Function, I1); + const rcc::Register Address0 = createVirtualRegister(**Function, P0); + const rcc::Register Address1 = createVirtualRegister(**Function, P1); + + std::vector<rcc::MachineOperand> LoadOperands; + LoadOperands.push_back(registerOperand(Dst64, true)); + LoadOperands.push_back(registerOperand(Address0, false)); + rcc::MachineInstr &Load = + createInstruction(**Function, rcc::GenericOpcode::G_LOAD, std::move(LoadOperands)); + addMemOperand(**Function, Load, memoryDescription({rcc::MemoryAccessFlag::Load}, 8)); + Context.expect(Set->validateInstruction(Load, (*Function)->registerInfo()).has_value(), + "G_LOAD must accept one matching load MMO"); + + std::vector<rcc::MachineOperand> OversizedLoadOperands; + OversizedLoadOperands.push_back(registerOperand(Dst32, true)); + OversizedLoadOperands.push_back(registerOperand(Address0, false)); + rcc::MachineInstr &OversizedLoad = + createInstruction(**Function, rcc::GenericOpcode::G_LOAD, std::move(OversizedLoadOperands)); + addMemOperand(**Function, OversizedLoad, memoryDescription({rcc::MemoryAccessFlag::Load}, 8)); + Context.expect(!Set->validateInstruction(OversizedLoad, (*Function)->registerInfo()), + "G_LOAD must reject an MMO larger than its result LowLevelType"); + + std::vector<rcc::MachineOperand> WrongAddressSpaceOperands; + WrongAddressSpaceOperands.push_back(registerOperand(Dst64, true)); + WrongAddressSpaceOperands.push_back(registerOperand(Address1, false)); + rcc::MachineInstr &WrongAddressSpace = createInstruction(**Function, rcc::GenericOpcode::G_LOAD, + std::move(WrongAddressSpaceOperands)); + addMemOperand(**Function, WrongAddressSpace, + memoryDescription({rcc::MemoryAccessFlag::Load}, 8, 0)); + Context.expect(!Set->validateInstruction(WrongAddressSpace, (*Function)->registerInfo()), + "generic memory access must match its pointer and MMO address spaces"); + + std::vector<rcc::MachineOperand> ExtLoadOperands; + ExtLoadOperands.push_back(registerOperand(Dst64, true)); + ExtLoadOperands.push_back(registerOperand(Address0, false)); + rcc::MachineInstr &ExtLoad = + createInstruction(**Function, rcc::GenericOpcode::G_ZEXTLOAD, std::move(ExtLoadOperands)); + addMemOperand(**Function, ExtLoad, memoryDescription({rcc::MemoryAccessFlag::Load}, 4)); + Context.expect(Set->validateInstruction(ExtLoad, (*Function)->registerInfo()).has_value(), + "G_ZEXTLOAD must accept a strictly narrower memory access"); + + std::vector<rcc::MachineOperand> NonExtendingLoadOperands; + NonExtendingLoadOperands.push_back(registerOperand(Dst64, true)); + NonExtendingLoadOperands.push_back(registerOperand(Address0, false)); + rcc::MachineInstr &NonExtendingLoad = createInstruction( + **Function, rcc::GenericOpcode::G_ZEXTLOAD, std::move(NonExtendingLoadOperands)); + addMemOperand(**Function, NonExtendingLoad, memoryDescription({rcc::MemoryAccessFlag::Load}, 8)); + Context.expect(!Set->validateInstruction(NonExtendingLoad, (*Function)->registerInfo()), + "G_ZEXTLOAD must reject a non-narrowing MMO"); + + std::vector<rcc::MachineOperand> CompareExchangeOperands; + CompareExchangeOperands.push_back(registerOperand(Dst64, true)); + CompareExchangeOperands.push_back(registerOperand(Status, true)); + CompareExchangeOperands.push_back(registerOperand(Address0, false)); + CompareExchangeOperands.push_back(registerOperand(Src64, false)); + CompareExchangeOperands.push_back(registerOperand(Src64, false)); + rcc::MachineInstr &CompareExchange = + createInstruction(**Function, rcc::GenericOpcode::G_ATOMIC_CMPXCHG_WITH_SUCCESS, + std::move(CompareExchangeOperands)); + addMemOperand(**Function, CompareExchange, + memoryDescription({rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Store}, 8, 0, + rcc::AtomicOperation::CompareExchange)); + Context.expect(Set->validateInstruction(CompareExchange, (*Function)->registerInfo()).has_value(), + "generic compare-exchange must accept a matching atomic MMO"); + + std::vector<rcc::MachineOperand> WrongAtomicOperands; + WrongAtomicOperands.push_back(registerOperand(Dst64, true)); + WrongAtomicOperands.push_back(registerOperand(Address0, false)); + WrongAtomicOperands.push_back(registerOperand(Src64, false)); + rcc::MachineInstr &WrongAtomic = createInstruction( + **Function, rcc::GenericOpcode::G_ATOMICRMW_ADD, std::move(WrongAtomicOperands)); + addMemOperand(**Function, WrongAtomic, + memoryDescription({rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Store}, 8, 0, + rcc::AtomicOperation::CompareExchange)); + Context.expect(!Set->validateInstruction(WrongAtomic, (*Function)->registerInfo()), + "generic atomic RMW must reject a compare-exchange MMO"); + + std::vector<rcc::MachineOperand> NarrowAtomicOperands; + NarrowAtomicOperands.push_back(registerOperand(Dst64, true)); + NarrowAtomicOperands.push_back(registerOperand(Address0, false)); + NarrowAtomicOperands.push_back(registerOperand(Src64, false)); + rcc::MachineInstr &NarrowAtomic = createInstruction( + **Function, rcc::GenericOpcode::G_ATOMICRMW_ADD, std::move(NarrowAtomicOperands)); + addMemOperand(**Function, NarrowAtomic, + memoryDescription({rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Store}, 1, 0, + rcc::AtomicOperation::ReadModifyWrite)); + Context.expect(Set->validateInstruction(NarrowAtomic, (*Function)->registerInfo()).has_value(), + "generic atomic RMW must accept an MMO narrower than its register container"); + + std::vector<rcc::MachineOperand> OversizedAtomicOperands; + OversizedAtomicOperands.push_back(registerOperand(Dst32, true)); + OversizedAtomicOperands.push_back(registerOperand(Address0, false)); + OversizedAtomicOperands.push_back(registerOperand(Src32, false)); + rcc::MachineInstr &OversizedAtomic = createInstruction( + **Function, rcc::GenericOpcode::G_ATOMICRMW_ADD, std::move(OversizedAtomicOperands)); + addMemOperand(**Function, OversizedAtomic, + memoryDescription({rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Store}, 8, 0, + rcc::AtomicOperation::ReadModifyWrite)); + Context.expect(!Set->validateInstruction(OversizedAtomic, (*Function)->registerInfo()), + "generic atomic RMW must reject an MMO wider than its register container"); + + std::vector<rcc::MachineOperand> FloatingAtomicOperands; + FloatingAtomicOperands.push_back(registerOperand(Float64, true)); + FloatingAtomicOperands.push_back(registerOperand(Address0, false)); + FloatingAtomicOperands.push_back(registerOperand(Float64, false)); + rcc::MachineInstr &FloatingAtomic = createInstruction( + **Function, rcc::GenericOpcode::G_ATOMICRMW_FADD, std::move(FloatingAtomicOperands)); + addMemOperand(**Function, FloatingAtomic, + memoryDescription({rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Store}, 8, 0, + rcc::AtomicOperation::ReadModifyWrite)); + Context.expect(Set->validateInstruction(FloatingAtomic, (*Function)->registerInfo()).has_value(), + "floating atomic RMW must accept a scalar floating value and RMW MMO"); + + std::vector<rcc::MachineOperand> PrefetchOperands; + PrefetchOperands.push_back(registerOperand(Address0, false)); + PrefetchOperands.push_back(rcc::MachineOperand::immediate(0)); + PrefetchOperands.push_back(rcc::MachineOperand::immediate(3)); + PrefetchOperands.push_back(rcc::MachineOperand::immediate(1)); + rcc::MachineInstr &Prefetch = + createInstruction(**Function, rcc::GenericOpcode::G_PREFETCH, std::move(PrefetchOperands)); + Context.expect(!Set->validateInstruction(Prefetch, (*Function)->registerInfo()), + "G_PREFETCH must require an MMO"); + addMemOperand(**Function, Prefetch, memoryDescription({rcc::MemoryAccessFlag::Load}, 1)); + Context.expect(Set->validateInstruction(Prefetch, (*Function)->registerInfo()).has_value(), + "read-prefetch selector must match a load-only MMO"); + + std::vector<rcc::MachineOperand> VarArgOperands; + VarArgOperands.push_back(registerOperand(Dst64, true)); + VarArgOperands.push_back(registerOperand(Address0, false)); + VarArgOperands.push_back(rcc::MachineOperand::immediate(8)); + rcc::MachineInstr &VarArg = + createInstruction(**Function, rcc::GenericOpcode::G_VAARG, std::move(VarArgOperands)); + Context.expect(Set->validateInstruction(VarArg, (*Function)->registerInfo()).has_value(), + "G_VAARG must accept LLVM's canonical form without MMOs"); + addMemOperand(**Function, VarArg, memoryDescription({rcc::MemoryAccessFlag::Load}, 8)); + addMemOperand(**Function, VarArg, memoryDescription({rcc::MemoryAccessFlag::Store}, 8)); + Context.expect(!Set->validateInstruction(VarArg, (*Function)->registerInfo()), + "G_VAARG must reject MMOs before legalization"); + + std::vector<rcc::MachineOperand> MemoryTransferOperands; + MemoryTransferOperands.push_back(registerOperand(Address0, false)); + MemoryTransferOperands.push_back(registerOperand(Address1, false)); + MemoryTransferOperands.push_back(registerOperand(Src64, false)); + MemoryTransferOperands.push_back(rcc::MachineOperand::immediate(0)); + rcc::MachineInstr &MemoryTransfer = createInstruction(**Function, rcc::GenericOpcode::G_MEMCPY, + std::move(MemoryTransferOperands)); + addMemOperand(**Function, MemoryTransfer, + memoryDescription({rcc::MemoryAccessFlag::Store}, 8, 0)); + addMemOperand(**Function, MemoryTransfer, memoryDescription({rcc::MemoryAccessFlag::Load}, 8, 1)); + Context.expect(Set->validateInstruction(MemoryTransfer, (*Function)->registerInfo()).has_value(), + "G_MEMCPY must accept equal-sized store/load MMOs in canonical order"); + + std::vector<rcc::MachineOperand> ReversedTransferOperands; + ReversedTransferOperands.push_back(registerOperand(Address0, false)); + ReversedTransferOperands.push_back(registerOperand(Address1, false)); + ReversedTransferOperands.push_back(registerOperand(Src64, false)); + ReversedTransferOperands.push_back(rcc::MachineOperand::immediate(0)); + rcc::MachineInstr &ReversedTransfer = createInstruction(**Function, rcc::GenericOpcode::G_MEMCPY, + std::move(ReversedTransferOperands)); + addMemOperand(**Function, ReversedTransfer, + memoryDescription({rcc::MemoryAccessFlag::Load}, 8, 0)); + addMemOperand(**Function, ReversedTransfer, + memoryDescription({rcc::MemoryAccessFlag::Store}, 8, 1)); + Context.expect(!Set->validateInstruction(ReversedTransfer, (*Function)->registerInfo()), + "G_MEMCPY must reject reversed MMO access directions"); +} + +void testSemanticFlags(TestContext &Context) { + const auto Descriptors = rcc::genericInstructionDescriptors(); + const auto Find = [Descriptors](rcc::GenericOpcode OpcodeValue) { + const rcc::Opcode Value = rcc::opcode(OpcodeValue); + const auto Position = + std::ranges::find_if(Descriptors, [Value](const rcc::InstructionDescriptor &Descriptor) { + return Descriptor.InstructionOpcode == Value; + }); + return Position == Descriptors.end() ? nullptr : &*Position; + }; + + const auto *Load = Find(rcc::GenericOpcode::G_LOAD); + const auto *Atomic = Find(rcc::GenericOpcode::G_ATOMIC_CMPXCHG); + const auto *VarArg = Find(rcc::GenericOpcode::G_VAARG); + 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 && + VarArg != nullptr && VarArg->Flags.has(rcc::InstructionFlag::MayLoad) && + VarArg->Flags.has(rcc::InstructionFlag::MayStore) && !VarArg->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/GlobalISel/CMakeLists.txt b/backend/unittests/CodeGen/GlobalISel/CMakeLists.txt new file mode 100644 index 0000000..ba38de1 --- /dev/null +++ b/backend/unittests/CodeGen/GlobalISel/CMakeLists.txt @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +include_directories("${RCC_BACKEND_SOURCE_DIR}/lib/Target/RISCV") + +add_executable(RCCBackendCodeGenLegalizerTests LegalizerTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenLegalizerTests) +target_link_libraries( + RCCBackendCodeGenLegalizerTests + PRIVATE RCCBackend::CodeGen RCCBackend::RISCV RCCBackend::TestTarget +) +add_test(NAME RCCBackend.CodeGen.GlobalISel.Legalizer COMMAND RCCBackendCodeGenLegalizerTests) + +add_executable(RCCBackendCodeGenRegBankSelectTests RegBankSelectTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenRegBankSelectTests) +target_link_libraries( + RCCBackendCodeGenRegBankSelectTests + PRIVATE RCCBackend::CodeGen RCCBackend::RISCV RCCBackend::TestTarget +) +add_test( + NAME RCCBackend.CodeGen.GlobalISel.RegBankSelect + COMMAND RCCBackendCodeGenRegBankSelectTests +) + +add_executable(RCCBackendCodeGenInstructionSelectTests InstructionSelectTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenInstructionSelectTests) +target_link_libraries( + RCCBackendCodeGenInstructionSelectTests + PRIVATE RCCBackend::CodeGen RCCBackend::RISCV RCCBackend::TestTarget +) +add_test( + NAME RCCBackend.CodeGen.GlobalISel.InstructionSelect + COMMAND RCCBackendCodeGenInstructionSelectTests +) + +rcc_backend_register_format_files( + "${CMAKE_CURRENT_SOURCE_DIR}/GISelMITest.h" + "${CMAKE_CURRENT_SOURCE_DIR}/InstructionSelectTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/LegalizerTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/RegBankSelectTest.cpp" +) diff --git a/backend/unittests/CodeGen/GlobalISel/GISelMITest.h b/backend/unittests/CodeGen/GlobalISel/GISelMITest.h new file mode 100644 index 0000000..20e8854 --- /dev/null +++ b/backend/unittests/CodeGen/GlobalISel/GISelMITest.h @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RCC_UNITTESTS_CODEGEN_GLOBALISEL_GISELMITEST_H +#define RCC_UNITTESTS_CODEGEN_GLOBALISEL_GISELMITEST_H + +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <string> +#include <string_view> +#include <utility> + +namespace rcc::test { + +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; +}; + +struct FunctionFixture final { + std::unique_ptr<rcc::MachineFunction> Function; + rcc::MachineBasicBlock *Entry = nullptr; +}; + +[[nodiscard]] inline FunctionFixture createFixture(TestContext &Context, std::string_view Name, + std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::string(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "machine pipeline function creation must succeed"); + if (!Function) + return {}; + auto Entry = (*Function)->createBasicBlock(); + Context.expect(Entry.has_value(), "machine pipeline block creation must succeed"); + if (!Entry || !(*Function)->appendBlock(**Entry)) + return {}; + return {std::move(*Function), *Entry}; +} + +[[nodiscard]] inline rcc::Register +createRegister(TestContext &Context, rcc::MachineFunction &Function, std::uint32_t Bits) { + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(Bits); + Context.expect(Type.has_value(), "integer LowLevelType construction must succeed"); + if (!Type) + return {}; + auto Reg = Function.registerInfo().createVirtualRegister(*Type); + Context.expect(Reg.has_value(), "virtual register construction must succeed"); + return Reg ? *Reg : rcc::Register{}; +} + +[[nodiscard]] inline rcc::Register createFloatRegister(TestContext &Context, + rcc::MachineFunction &Function) { + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + Context.expect(Type.has_value(), "floating LowLevelType construction must succeed"); + if (!Type) + return {}; + auto Reg = Function.registerInfo().createVirtualRegister(*Type); + Context.expect(Reg.has_value(), "floating virtual register construction must succeed"); + return Reg ? *Reg : rcc::Register{}; +} + +[[nodiscard]] inline bool assignGPR(rcc::MachineFunction &Function, rcc::Register Reg) { + const auto Bank = + rcc::RegisterBankID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterBank::GPR)); + const auto RegClass = + rcc::RegisterClassID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterClass::GPR)); + return Function.registerInfo().setRegisterBank(Reg, Bank).has_value() && + Function.registerInfo().setRegisterClass(Reg, RegClass).has_value(); +} + +[[nodiscard]] inline bool buildConstant(TestContext &Context, rcc::MachineIRBuilder &Builder, + rcc::Register Destination, std::int64_t Value) { + auto Pending = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_CONSTANT)); + if (!Pending || !Pending->addDef(Destination) || !Pending->addImmediate(Value) || + !Pending->build()) { + Context.expect(false, "generic constant construction must succeed"); + return false; + } + return true; +} + +[[nodiscard]] inline bool buildAdd(TestContext &Context, rcc::MachineIRBuilder &Builder, + rcc::Register Destination, rcc::Register Left, + rcc::Register Right) { + auto Pending = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_ADD)); + if (!Pending || !Pending->addDef(Destination) || !Pending->addUse(Left) || + !Pending->addUse(Right) || !Pending->build()) { + Context.expect(false, "generic add construction must succeed"); + return false; + } + return true; +} + +} // namespace rcc::test + +#endif // RCC_UNITTESTS_CODEGEN_GLOBALISEL_GISELMITEST_H diff --git a/backend/unittests/CodeGen/GlobalISel/InstructionSelectTest.cpp b/backend/unittests/CodeGen/GlobalISel/InstructionSelectTest.cpp new file mode 100644 index 0000000..9f3f0b5 --- /dev/null +++ b/backend/unittests/CodeGen/GlobalISel/InstructionSelectTest.cpp @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/GlobalISel/InstructionSelect.h" + +#include "GISel/RISCVRegisterBankInfo.h" +#include "GISelMITest.h" +#include "rcc/CodeGen/GlobalISel/Legalizer.h" +#include "rcc/CodeGen/GlobalISel/RegBankSelect.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/Target/RISCV/RISCVSubtarget.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +#include <algorithm> +#include <array> +#include <cstdint> +#include <memory> +#include <ranges> +#include <vector> + +namespace { + +using rcc::test::assignGPR; +using rcc::test::buildAdd; +using rcc::test::buildConstant; +using rcc::test::createFixture; +using rcc::test::createRegister; +using rcc::test::TestContext; + +class MutatingFailingSelector final : public rcc::InstructionSelector { +public: + [[nodiscard]] rcc::Expected<void> canSelect(const rcc::MachineInstr &, + const rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return {}; + } + + [[nodiscard]] rcc::Expected<void> select(rcc::MachineInstr &Instruction, rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + auto Mutation = Instruction.setOpcode(rcc::opcode(rcc::GenericOpcode::G_SUB)); + if (!Mutation) + return std::unexpected(Mutation.error()); + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, "test selector rejected its renderer"); + } +}; + +class RecordingSelector final : public rcc::InstructionSelector { +public: + explicit RecordingSelector(const rcc::InstructionSelector &Delegate) : Delegate(&Delegate) {} + + [[nodiscard]] rcc::Expected<void> canSelect(const rcc::MachineInstr &Instruction, + const rcc::MachineFunction &Function, + const rcc::TargetDescription &Target) const override { + return Delegate->canSelect(Instruction, Function, Target); + } + + [[nodiscard]] rcc::Expected<void> select(rcc::MachineInstr &Instruction, + rcc::MachineFunction &Function, + const rcc::TargetDescription &Target) const override { + SelectedIDs.push_back(Instruction.stableID().value()); + return Delegate->select(Instruction, Function, Target); + } + + [[nodiscard]] const std::vector<std::uint64_t> &selectedIDs() const noexcept { + return SelectedIDs; + } + +private: + const rcc::InstructionSelector *Delegate; + mutable std::vector<std::uint64_t> SelectedIDs; +}; + +class TwoStepSelector final : public rcc::InstructionSelector { +public: + [[nodiscard]] rcc::Expected<void> canSelect(const rcc::MachineInstr &, + const rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return {}; + } + + [[nodiscard]] rcc::Expected<void> select(rcc::MachineInstr &Instruction, rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + ++Selections; + if (Instruction.opcode() == rcc::opcode(rcc::GenericOpcode::G_ADD)) + return Instruction.setOpcode(rcc::opcode(rcc::GenericOpcode::G_SUB)); + if (Instruction.opcode() == rcc::opcode(rcc::GenericOpcode::G_SUB)) { + return Instruction.setOpcode( + rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::ADD))); + } + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, + "two-step selector received an unexpected opcode"); + } + + [[nodiscard]] std::size_t selections() const noexcept { + return Selections; + } + +private: + mutable std::size_t Selections = 0; +}; + +void testPipeline(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFixture(Context, "pipeline_success", 2); + Context.expect(Target.has_value() && Fixture.Function != nullptr && Fixture.Entry != nullptr, + "pipeline success fixture must be complete"); + if (!Target || !Fixture.Function || Fixture.Entry == nullptr) + return; + + const rcc::Register Left = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Right = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Result = createRegister(Context, *Fixture.Function, 64); + rcc::MachineIRBuilder Builder(*Fixture.Function, *Target); + Context.expect(Builder.setInsertPoint(*Fixture.Entry).has_value(), + "pipeline insertion point must be valid"); + if (Left.isNoRegister() || Right.isNoRegister() || Result.isNoRegister() || + !buildConstant(Context, Builder, Left, 42) || !buildConstant(Context, Builder, Right, 1) || + !buildAdd(Context, Builder, Result, Left, Right)) + return; + + Fixture.Function->properties() = {rcc::MachineFunctionProperty::IsSSA, + rcc::MachineFunctionProperty::TracksLiveness}; + const rcc::riscv::RISCVSubtarget Subtarget(rcc::riscv::RISCVSubtargetFeatures::rv64gc()); + rcc::MachineFunctionPassManager Pipeline; + Context.expect(Pipeline + .addPass(std::make_unique<rcc::Legalizer>(Subtarget.getLegalizerInfo(), + rcc::OptimizationLevel::O2)) + .has_value(), + "legalizer must be accepted by the pass manager"); + Context.expect(Pipeline.addPass(std::make_unique<rcc::RegBankSelect>(Subtarget.getRegBankInfo())) + .has_value(), + "register-bank selector must be accepted by the pass manager"); + Context.expect( + Pipeline.addPass(std::make_unique<rcc::InstructionSelect>(Subtarget.getInstructionSelector())) + .has_value(), + "instruction selector must be accepted by the pass manager"); + rcc::MachineFunctionAnalysisManager Analyses; + auto Run = Pipeline.run(*Fixture.Function, *Target, Analyses); + Context.expect(Run.has_value(), "RV64GC generic pipeline must complete"); + Context.expect(Fixture.Function->properties().has(rcc::MachineFunctionProperty::Selected), + "successful instruction selection must publish Selected"); + Context.expect(Fixture.Function->firstBlock()->firstInstruction() != nullptr, + "selected instructions must remain attached to their block"); + for (const rcc::MachineInstr *Instruction : Fixture.Function->allInstructions()) + Context.expect(!rcc::isGenericOpcode(Instruction->opcode()), + "successful instruction selection must remove generic opcodes"); +} + +void testInstructionSelectionTransaction(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFixture(Context, "pipeline_selection_transaction", 8); + if (!Target || !Fixture.Function || Fixture.Entry == nullptr) + return; + const rcc::Register Left = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Right = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Result = createRegister(Context, *Fixture.Function, 64); + rcc::MachineIRBuilder Builder(*Fixture.Function, *Target); + if (Left.isNoRegister() || Right.isNoRegister() || Result.isNoRegister() || + !Builder.setInsertPoint(*Fixture.Entry) || !buildAdd(Context, Builder, Result, Left, Right) || + !assignGPR(*Fixture.Function, Left) || !assignGPR(*Fixture.Function, Right) || + !assignGPR(*Fixture.Function, Result)) { + return; + } + rcc::MachineInstr *Original = Fixture.Entry->firstInstruction(); + if (Original == nullptr) + return; + const rcc::Opcode OriginalOpcode = Original->opcode(); + const std::size_t OriginalOperandCount = Original->operandCount(); + Fixture.Function->properties() = {rcc::MachineFunctionProperty::Legalized, + rcc::MachineFunctionProperty::RegBankSelected}; + MutatingFailingSelector Selector; + rcc::InstructionSelect Pass(Selector); + rcc::MachineFunctionAnalysisManager Analyses; + auto Run = Pass.run(*Fixture.Function, *Target, Analyses); + Context.expect(!Run && Run.error().code() == rcc::ErrorCode::Unsupported, + "a renderer failure must be reported before publication"); + Context.expect(Original->opcode() == OriginalOpcode && + Original->operandCount() == OriginalOperandCount && + Fixture.Entry->instructionCount() == 1, + "a renderer failure must leave the original MachineIR unchanged"); +} + +void testInstructionSelectionOrder(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFixture(Context, "pipeline_selection_order", 9); + if (!Target || !Fixture.Function || Fixture.Entry == nullptr) + return; + const rcc::Register Left = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Right = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Result = createRegister(Context, *Fixture.Function, 64); + rcc::MachineIRBuilder Builder(*Fixture.Function, *Target); + if (Left.isNoRegister() || Right.isNoRegister() || Result.isNoRegister() || + !Builder.setInsertPoint(*Fixture.Entry) || !buildConstant(Context, Builder, Left, 6) || + !buildConstant(Context, Builder, Right, 7) || + !buildAdd(Context, Builder, Result, Left, Right) || !assignGPR(*Fixture.Function, Left) || + !assignGPR(*Fixture.Function, Right) || !assignGPR(*Fixture.Function, Result)) { + return; + } + std::vector<std::uint64_t> LayoutIDs; + for (const rcc::MachineInstr *Instruction = Fixture.Entry->firstInstruction(); + Instruction != nullptr; Instruction = Instruction->next()) { + LayoutIDs.push_back(Instruction->stableID().value()); + } + Fixture.Function->properties() = {rcc::MachineFunctionProperty::Legalized, + rcc::MachineFunctionProperty::RegBankSelected}; + const rcc::riscv::RISCVSubtarget Subtarget(rcc::riscv::RISCVSubtargetFeatures::rv64gc()); + RecordingSelector Selector(*Subtarget.getInstructionSelector()); + rcc::InstructionSelect Pass(Selector); + rcc::MachineFunctionAnalysisManager Analyses; + auto Run = Pass.run(*Fixture.Function, *Target, Analyses); + Context.expect(Run.has_value(), "recorded instruction selection must complete"); + std::ranges::reverse(LayoutIDs); + std::vector<std::uint64_t> Expected = LayoutIDs; + Expected.insert(Expected.end(), LayoutIDs.begin(), LayoutIDs.end()); + Context.expect( + Selector.selectedIDs() == Expected, + "instruction selection must use stable block-postorder and reverse instruction order"); +} + +void testInstructionSelectionWorklist(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFixture(Context, "pipeline_selection_worklist", 10); + if (!Target || !Fixture.Function || Fixture.Entry == nullptr) + return; + const rcc::Register Left = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Right = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Result = createRegister(Context, *Fixture.Function, 64); + rcc::MachineIRBuilder Builder(*Fixture.Function, *Target); + if (Left.isNoRegister() || Right.isNoRegister() || Result.isNoRegister() || + !Builder.setInsertPoint(*Fixture.Entry) || !buildAdd(Context, Builder, Result, Left, Right) || + !assignGPR(*Fixture.Function, Left) || !assignGPR(*Fixture.Function, Right) || + !assignGPR(*Fixture.Function, Result)) { + return; + } + Fixture.Function->properties() = {rcc::MachineFunctionProperty::Legalized, + rcc::MachineFunctionProperty::RegBankSelected}; + TwoStepSelector Selector; + rcc::InstructionSelect Pass(Selector); + rcc::MachineFunctionAnalysisManager Analyses; + auto Run = Pass.run(*Fixture.Function, *Target, Analyses); + Context.expect(Run.has_value(), "a progressing multi-step selection must complete"); + Context.expect(Selector.selections() == 4, + "transaction preflight and publication must each select both generic states"); + Context.expect(Fixture.Entry->firstInstruction() != nullptr && + Fixture.Entry->firstInstruction()->opcode() == + rcc::Opcode::fromValue( + static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::ADD)), + "the selection worklist must revisit a progressing generic root"); +} + +void testExtendedRV64GCPipeline(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFixture(Context, "pipeline_extended_rv64gc", 7); + Context.expect(Target.has_value() && Fixture.Function != nullptr && Fixture.Entry != nullptr, + "extended pipeline fixture must be complete"); + if (!Target || !Fixture.Function || Fixture.Entry == nullptr) + return; + auto I1 = rcc::LowLevelType::makeCategoryNeutralScalar(1); + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Pointer = rcc::LowLevelType::makePointer(0, 64); + if (!I1 || !I64 || !Pointer) + return; + const rcc::Register Left = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Right = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Condition = + Fixture.Function->registerInfo().createVirtualRegister(*I1).value_or(rcc::Register{}); + const rcc::Register Selected = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Sum = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Overflow = + Fixture.Function->registerInfo().createVirtualRegister(*I1).value_or(rcc::Register{}); + const rcc::Register Quotient = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Remainder = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Address = + Fixture.Function->registerInfo().createVirtualRegister(*Pointer).value_or(rcc::Register{}); + const rcc::Register AtomicResult = createRegister(Context, *Fixture.Function, 64); + const std::array Registers{Left, Right, Condition, Selected, Sum, + Overflow, Quotient, Remainder, Address, AtomicResult}; + if (std::ranges::any_of(Registers, [](rcc::Register Reg) { return Reg.isNoRegister(); })) + return; + rcc::MachineIRBuilder Builder(*Fixture.Function, *Target); + if (!Builder.setInsertPoint(*Fixture.Entry) || !buildConstant(Context, Builder, Left, 41) || + !buildConstant(Context, Builder, Right, 7)) { + return; + } + auto Compare = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_ICMP)); + if (!Compare || !Compare->addDef(Condition) || + !Compare->addIntegerPredicate(rcc::IntegerPredicate::UnsignedGreaterThan) || + !Compare->addUse(Left) || !Compare->addUse(Right) || !Compare->build()) { + return; + } + auto Select = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_SELECT)); + if (!Select || !Select->addDef(Selected) || !Select->addUse(Condition) || !Select->addUse(Left) || + !Select->addUse(Right) || !Select->build()) { + return; + } + auto AddOverflow = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_UADDO)); + if (!AddOverflow || !AddOverflow->addDef(Sum) || !AddOverflow->addDef(Overflow) || + !AddOverflow->addUse(Left) || !AddOverflow->addUse(Right) || !AddOverflow->build()) { + return; + } + auto DivRem = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_SDIVREM)); + if (!DivRem || !DivRem->addDef(Quotient) || !DivRem->addDef(Remainder) || !DivRem->addUse(Left) || + !DivRem->addUse(Right) || !DivRem->build()) { + return; + } + auto AddressDefinition = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_IMPLICIT_DEF)); + if (!AddressDefinition || !AddressDefinition->addDef(Address) || !AddressDefinition->build()) + return; + auto Size = rcc::MemorySize::fixed(8); + auto Align = rcc::Alignment::create(8); + if (!Size || !Align) + return; + rcc::MachineMemOperandDesc Description; + Description.Accesses = {rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Store}; + Description.AtomicKind = rcc::AtomicOperation::ReadModifyWrite; + Description.SuccessOrdering = rcc::AtomicOrdering::SequentiallyConsistent; + Description.Size = *Size; + Description.Align = *Align; + Description.Base = rcc::MemoryBase::stack(); + auto Memory = Fixture.Function->createMemOperand(std::move(Description)); + if (!Memory) + return; + auto Atomic = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_ATOMICRMW_ADD)); + if (!Atomic || !Atomic->addDef(AtomicResult) || !Atomic->addUse(Address) || + !Atomic->addUse(Selected) || !Atomic->addMemOperand(**Memory) || !Atomic->build()) { + return; + } + + Fixture.Function->properties() = {rcc::MachineFunctionProperty::IsSSA, + rcc::MachineFunctionProperty::TracksLiveness}; + const rcc::riscv::RISCVSubtarget Subtarget(rcc::riscv::RISCVSubtargetFeatures::rv64gc()); + rcc::MachineFunctionPassManager Pipeline; + if (!Pipeline.addPass(std::make_unique<rcc::Legalizer>(Subtarget.getLegalizerInfo(), + rcc::OptimizationLevel::O2)) || + !Pipeline.addPass(std::make_unique<rcc::RegBankSelect>(Subtarget.getRegBankInfo())) || + !Pipeline.addPass( + std::make_unique<rcc::InstructionSelect>(Subtarget.getInstructionSelector()))) { + Context.expect(false, "extended RV64GC pipeline construction must succeed"); + return; + } + rcc::MachineFunctionAnalysisManager Analyses; + auto Run = Pipeline.run(*Fixture.Function, *Target, Analyses); + Context.expect(Run.has_value(), "extended RV64GC pipeline must complete"); + Context.expect(Run && Fixture.Function->properties().has(rcc::MachineFunctionProperty::Selected), + "extended pipeline must publish Selected"); + for (const rcc::MachineInstr *Instruction : Fixture.Function->allInstructions()) { + Context.expect(!rcc::isGenericOpcode(Instruction->opcode()), + "extended pipeline must eliminate every generic opcode"); + Context.expect( + Target->validateInstruction(*Instruction, Fixture.Function->registerInfo()).has_value(), + "extended pipeline output must satisfy target descriptors"); + } +} + +} // namespace + +int main() { + TestContext Context; + testPipeline(Context); + testInstructionSelectionTransaction(Context); + testInstructionSelectionOrder(Context); + testInstructionSelectionWorklist(Context); + testExtendedRV64GCPipeline(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/GlobalISel/LegalizerTest.cpp b/backend/unittests/CodeGen/GlobalISel/LegalizerTest.cpp new file mode 100644 index 0000000..eaf3741 --- /dev/null +++ b/backend/unittests/CodeGen/GlobalISel/LegalizerTest.cpp @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/GlobalISel/Legalizer.h" + +#include "GISelMITest.h" +#include "rcc/CodeGen/GlobalISel/GISelChangeObserver.h" +#include "rcc/CodeGen/GlobalISel/LegalizerHelper.h" +#include "rcc/Target/RISCV/RISCVRegisters.h" +#include "rcc/Target/RISCV/RISCVSubtarget.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +#include <cstdint> +#include <memory> +#include <string> + +namespace { + +using rcc::test::buildAdd; +using rcc::test::createFixture; +using rcc::test::createRegister; +using rcc::test::TestContext; + +class BackwardInvalidatingLegalizer final : public rcc::LegalizerInfo { +public: + BackwardInvalidatingLegalizer() { + getActionDefinitionsBuilder(rcc::opcode(rcc::GenericOpcode::G_ADD)).alwaysLegal(); + getActionDefinitionsBuilder(rcc::opcode(rcc::GenericOpcode::G_SUB)).custom(); + } + + [[nodiscard]] rcc::Expected<void> + legalizeCustom(rcc::LegalizerHelper &Helper, rcc::MachineInstr &Instruction, + const rcc::LegalizationDecision &) const override { + rcc::MachineFunction &Function = Helper.MIRBuilder.function(); + for (rcc::MachineInstr *Candidate : Function.allInstructions()) { + if (Candidate != &Instruction && + Candidate->opcode() == rcc::opcode(rcc::GenericOpcode::G_ADD)) { + Helper.Observer.changingInstr(*Candidate); + auto Invalidated = Candidate->setOpcode(rcc::opcode(rcc::GenericOpcode::G_SUB)); + if (!Invalidated) + return std::unexpected(Invalidated.error()); + Helper.Observer.changedInstr(*Candidate); + break; + } + } + Helper.Observer.changingInstr(Instruction); + auto Changed = Instruction.setOpcode( + rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::ADD))); + if (!Changed) + return std::unexpected(Changed.error()); + Helper.Observer.changedInstr(Instruction); + return {}; + } +}; + +void testNullAndMissingContracts(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFixture(Context, "pipeline_contracts", 1); + Context.expect(Target.has_value() && Fixture.Function != nullptr && Fixture.Entry != nullptr, + "pipeline contract fixture must be complete"); + if (!Target || !Fixture.Function) + return; + + rcc::Legalizer NullPass(nullptr, rcc::OptimizationLevel::O2); + rcc::MachineFunctionAnalysisManager Analyses; + auto NullResult = NullPass.run(*Fixture.Function, *Target, Analyses); + Context.expect(!NullResult && NullResult.error().code() == rcc::ErrorCode::InvalidArgument, + "a null legalizer interface must be rejected"); + Context.expect(Fixture.Function->properties().empty(), + "a rejected pass must not publish function properties"); + + const rcc::riscv::RISCVSubtarget Subtarget(rcc::riscv::RISCVSubtargetFeatures::rv64gc()); + rcc::Legalizer ValidPass(Subtarget.getLegalizerInfo(), rcc::OptimizationLevel::O2); + auto MissingResult = ValidPass.run(*Fixture.Function, *Target, Analyses); + Context.expect(!MissingResult && + MissingResult.error().code() == rcc::ErrorCode::VerificationError, + "a pass with missing prerequisites must be rejected"); +} + +void testUnsupportedRollback(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + if (!Target) + return; + auto Unsupported = createFixture(Context, "pipeline_unsupported", 3); + Context.expect(Unsupported.Function != nullptr && Unsupported.Entry != nullptr, + "unsupported pipeline fixture must be complete"); + if (!Unsupported.Function || Unsupported.Entry == nullptr) + return; + const rcc::Register UnsupportedReg = createRegister(Context, *Unsupported.Function, 64); + rcc::MachineIRBuilder UnsupportedBuilder(*Unsupported.Function, *Target); + Context.expect(UnsupportedBuilder.setInsertPoint(*Unsupported.Entry).has_value(), + "unsupported pipeline insertion point must be valid"); + if (UnsupportedReg.isNoRegister()) + return; + auto Pending = UnsupportedBuilder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_GET_FPENV)); + Context.expect(Pending.has_value(), "unsupported generic instruction must be constructible"); + if (!Pending) + return; + auto Built = Pending->addDef(UnsupportedReg); + Context.expect(Built.has_value(), "unsupported generic definition must be accepted"); + if (!Built) + return; + Context.expect(Pending->build().has_value(), + "unsupported floating-point environment operation must be constructible"); + Unsupported.Function->properties() = {rcc::MachineFunctionProperty::IsSSA, + rcc::MachineFunctionProperty::TracksLiveness}; + const rcc::riscv::RISCVSubtarget Subtarget(rcc::riscv::RISCVSubtargetFeatures::rv64gc()); + rcc::MachineFunctionPassManager OnlyLegalizer; + Context.expect(OnlyLegalizer + .addPass(std::make_unique<rcc::Legalizer>(Subtarget.getLegalizerInfo(), + rcc::OptimizationLevel::O2)) + .has_value(), + "standalone legalizer must be accepted"); + rcc::MachineFunctionAnalysisManager UnsupportedAnalyses; + auto Failure = OnlyLegalizer.run(*Unsupported.Function, *Target, UnsupportedAnalyses); + Context.expect(!Failure && Failure.error().code() == rcc::ErrorCode::Unsupported, + "unsupported generic instructions must produce a target diagnostic"); + Context.expect(!Failure && Failure.error().message().find("G_GET_FPENV") != std::string::npos, + "unsupported legalization diagnostics must name the rejected opcode"); + Context.expect( + Unsupported.Function->properties().has(rcc::MachineFunctionProperty::IsSSA) && + !Unsupported.Function->properties().has(rcc::MachineFunctionProperty::Legalized), + "failed legalizer execution must not publish Legalized"); +} + +void testLegalizerRevisitsInvalidatedInstructions(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFixture(Context, "pipeline_legalizer_revisit", 7); + if (!Target || !Fixture.Function || Fixture.Entry == nullptr) + return; + const rcc::Register Left = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Right = createRegister(Context, *Fixture.Function, 64); + const rcc::Register FirstResult = createRegister(Context, *Fixture.Function, 64); + const rcc::Register SecondResult = createRegister(Context, *Fixture.Function, 64); + rcc::MachineIRBuilder Builder(*Fixture.Function, *Target); + if (Left.isNoRegister() || Right.isNoRegister() || FirstResult.isNoRegister() || + SecondResult.isNoRegister() || !Builder.setInsertPoint(*Fixture.Entry) || + !buildAdd(Context, Builder, FirstResult, Left, Right) || + !buildAdd(Context, Builder, SecondResult, Left, Right)) + return; + rcc::MachineInstr *First = Fixture.Entry->firstInstruction(); + rcc::MachineInstr *Second = First == nullptr ? nullptr : First->next(); + if (Second == nullptr || !Second->setOpcode(rcc::opcode(rcc::GenericOpcode::G_SUB))) + return; + auto X10 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X10); + auto X11 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X11); + auto FirstCopy = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::COPY)); + auto SecondCopy = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::COPY)); + if (!X10 || !X11 || !FirstCopy || !FirstCopy->addDef(rcc::Register::fromPhysical(*X10)) || + !FirstCopy->addUse(FirstResult) || !FirstCopy->build() || !SecondCopy || + !SecondCopy->addDef(rcc::Register::fromPhysical(*X11)) || !SecondCopy->addUse(SecondResult) || + !SecondCopy->build()) + return; + + Fixture.Function->properties() = {rcc::MachineFunctionProperty::IsSSA, + rcc::MachineFunctionProperty::TracksLiveness}; + BackwardInvalidatingLegalizer Legalizer; + rcc::Legalizer Pass(Legalizer, rcc::OptimizationLevel::O2); + rcc::MachineFunctionAnalysisManager Analyses; + auto Run = Pass.run(*Fixture.Function, *Target, Analyses); + const rcc::Opcode LegalOpcode = + rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::ADD)); + Context.expect(Run && First->opcode() == LegalOpcode && Second->opcode() == LegalOpcode, + "legalizer must revisit an earlier instruction invalidated by a later lowering"); +} + +} // namespace + +int main() { + TestContext Context; + testNullAndMissingContracts(Context); + testUnsupportedRollback(Context); + testLegalizerRevisitsInvalidatedInstructions(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/GlobalISel/RegBankSelectTest.cpp b/backend/unittests/CodeGen/GlobalISel/RegBankSelectTest.cpp new file mode 100644 index 0000000..785844f --- /dev/null +++ b/backend/unittests/CodeGen/GlobalISel/RegBankSelectTest.cpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/GlobalISel/RegBankSelect.h" + +#include "GISel/RISCVRegisterBankInfo.h" +#include "GISelMITest.h" +#include "rcc/Target/RISCV/RISCVSubtarget.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +#include <cstdint> + +namespace { + +using rcc::test::buildConstant; +using rcc::test::createFixture; +using rcc::test::createRegister; +using rcc::test::TestContext; + +void testBankConflict(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFixture(Context, "pipeline_bank_conflict", 4); + Context.expect(Target.has_value() && Fixture.Function != nullptr && Fixture.Entry != nullptr, + "bank conflict fixture must be complete"); + if (!Target || !Fixture.Function || Fixture.Entry == nullptr) + return; + const rcc::Register Value = createRegister(Context, *Fixture.Function, 64); + Context.expect( + Fixture.Function->registerInfo() + .setRegisterBank(Value, rcc::RegisterBankID::fromValue( + static_cast<std::uint32_t>(rcc::riscv::RegisterBank::FPR))) + .has_value(), + "conflicting bank assignment must be constructible"); + Fixture.Function->properties() = {rcc::MachineFunctionProperty::Legalized}; + const rcc::riscv::RISCVSubtarget Subtarget(rcc::riscv::RISCVSubtargetFeatures::rv64gc()); + rcc::RegBankSelect Pass(Subtarget.getRegBankInfo()); + rcc::MachineFunctionAnalysisManager Analyses; + auto Result = Pass.run(*Fixture.Function, *Target, Analyses); + Context.expect(!Result && Result.error().code() == rcc::ErrorCode::VerificationError, + "bank selection must reject a conflicting pre-existing bank"); + auto Info = Fixture.Function->registerInfo().virtualRegisterInfo(Value); + Context.expect(Info && Info->get().Bank, "failed bank selection must preserve a bank assignment"); + if (!Info || !Info->get().Bank) + return; + const rcc::RegisterBankID Bank = Info->get().Bank.value_or(rcc::RegisterBankID{}); + Context.expect(Bank == rcc::RegisterBankID::fromValue( + static_cast<std::uint32_t>(rcc::riscv::RegisterBank::FPR)), + "failed bank selection must preserve the original bank assignment"); +} + +void testBankSelectionDoesNotAssignAClass(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFixture(Context, "pipeline_bank_class", 5); + Context.expect(Target.has_value() && Fixture.Function != nullptr && Fixture.Entry != nullptr, + "bank class fixture must be complete"); + if (!Target || !Fixture.Function || Fixture.Entry == nullptr) + return; + const rcc::Register Value = createRegister(Context, *Fixture.Function, 64); + rcc::MachineIRBuilder Builder(*Fixture.Function, *Target); + if (Value.isNoRegister() || !Builder.setInsertPoint(*Fixture.Entry) || + !buildConstant(Context, Builder, Value, 7)) + return; + const auto GPR = + rcc::RegisterBankID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterBank::GPR)); + Context.expect(Fixture.Function->registerInfo().setRegisterBank(Value, GPR).has_value(), + "pre-existing GPR bank must be assignable"); + Fixture.Function->properties() = {rcc::MachineFunctionProperty::Legalized}; + const rcc::riscv::RISCVSubtarget Subtarget(rcc::riscv::RISCVSubtargetFeatures::rv64gc()); + rcc::RegBankSelect Pass(Subtarget.getRegBankInfo()); + rcc::MachineFunctionAnalysisManager Analyses; + auto Result = Pass.run(*Fixture.Function, *Target, Analyses); + Context.expect(Result.has_value() && !Result->Changed, + "bank selection must preserve an existing bank-only mapping"); + auto Info = Fixture.Function->registerInfo().virtualRegisterInfo(Value); + Context.expect(Info && Info->get().Bank && !Info->get().RegClass, + "bank selection must not assign a register class"); + if (!Info || !Info->get().Bank) + return; + Context.expect(Info->get().Bank.value_or(rcc::RegisterBankID{}) == GPR, + "bank selection must preserve the GPR bank"); +} + +} // namespace + +int main() { + TestContext Context; + testBankConflict(Context); + testBankSelectionDoesNotAssignAClass(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/InstructionDescriptorTest.cpp b/backend/unittests/CodeGen/InstructionDescriptorTest.cpp new file mode 100644 index 0000000..d263725 --- /dev/null +++ b/backend/unittests/CodeGen/InstructionDescriptorTest.cpp @@ -0,0 +1,368 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/InstructionDescriptor.h" + +#include "rcc/CodeGen/LowLevelType.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/CodeGen/MachineOperand.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::OperandDescriptor registerDescriptor(rcc::RegisterOperandRole Role) { + rcc::OperandDescriptor Descriptor; + Descriptor.Kinds = {rcc::MachineOperand::Kind::Register}; + Descriptor.RegisterRole = Role; + return Descriptor; +} + +[[nodiscard]] rcc::InstructionDescriptor minimalDescriptor(std::uint32_t OpcodeValue, + std::string Name) { + rcc::InstructionDescriptor Descriptor; + Descriptor.InstructionOpcode = rcc::Opcode::fromValue(OpcodeValue); + Descriptor.Name = std::move(Name); + Descriptor.Flags = {rcc::InstructionFlag::ProducesCode}; + return Descriptor; +} + +[[nodiscard]] rcc::Expected<rcc::MachineOperand> registerOperand(rcc::Register Reg, + rcc::RegisterOperandFlags Flags, + rcc::SubRegIndex SubReg = {}, + std::uint32_t TargetFlags = 0) { + return rcc::MachineOperand::registerOperand(Reg, Flags, SubReg, TargetFlags); +} + +[[nodiscard]] rcc::MachineMemOperandDesc loadDescription() { + rcc::MachineMemOperandDesc Description; + Description.Accesses = {rcc::MemoryAccessFlag::Load}; + Description.Size = *rcc::MemorySize::fixed(8); + Description.Align = *rcc::Alignment::create(8); + Description.Base = rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)); + return Description; +} + +[[nodiscard]] rcc::Expected<rcc::InstructionSet> createValidInstructionSet() { + constexpr std::uint32_t AddOpcode = 1; + constexpr std::uint32_t TwoAddressOpcode = 2; + constexpr std::uint32_t LoadOpcode = 3; + + rcc::InstructionDescriptor Add = minimalDescriptor(AddOpcode, "TEST_ADDri"); + auto Def = registerDescriptor(rcc::RegisterOperandRole::Def); + Def.RegBank = rcc::RegisterBankID::fromValue(7); + Def.RegClass = rcc::RegisterClassID::fromValue(8); + Def.Name = rcc::NamedOperandID::fromValue(1); + + auto Source = registerDescriptor(rcc::RegisterOperandRole::Use); + Source.Name = rcc::NamedOperandID::fromValue(2); + + rcc::OperandDescriptor RegisterOrImmediate; + RegisterOrImmediate.Kinds = { + rcc::MachineOperand::Kind::Register, + rcc::MachineOperand::Kind::Immediate, + rcc::MachineOperand::Kind::GlobalSymbol, + }; + RegisterOrImmediate.RegisterRole = rcc::RegisterOperandRole::Use; + RegisterOrImmediate.ImmediatePredicate = rcc::ImmediatePredicateID::fromValue(1); + RegisterOrImmediate.Name = rcc::NamedOperandID::fromValue(3); + RegisterOrImmediate.AllowedTargetFlags = 0x3; + + Add.Operands.push_back(Def); + Add.Operands.push_back(Source); + Add.Operands.push_back(RegisterOrImmediate); + Add.NumExplicitDefs = 1; + Add.ImplicitDefs.push_back( + rcc::ImplicitRegisterDescriptor{*rcc::PhysicalRegister::create(1), false, true}); + Add.ImplicitUses.push_back(rcc::ImplicitRegisterDescriptor{*rcc::PhysicalRegister::create(2)}); + + rcc::InstructionDescriptor TwoAddress = minimalDescriptor(TwoAddressOpcode, "TEST_TWOADDR"); + auto TiedDef = registerDescriptor(rcc::RegisterOperandRole::Def); + auto TiedUse = registerDescriptor(rcc::RegisterOperandRole::Use); + TiedDef.DefaultTiedOperand = 1; + TiedUse.DefaultTiedOperand = 0; + TwoAddress.Operands.push_back(TiedDef); + TwoAddress.Operands.push_back(TiedUse); + TwoAddress.NumExplicitDefs = 1; + + rcc::InstructionDescriptor Load = minimalDescriptor(LoadOpcode, "TEST_LOAD"); + Load.Flags.set(rcc::InstructionFlag::MayLoad); + Load.RequiresMemoryOperands = true; + + rcc::ImmediatePredicate Signed12; + Signed12.ID = rcc::ImmediatePredicateID::fromValue(1); + Signed12.Name = "register-or-simm12"; + Signed12.Predicate = [](const rcc::MachineOperand &Operand) { + if (Operand.isRegister()) + return true; + const auto *Value = Operand.getIf<std::int64_t>(); + return Value != nullptr && *Value >= -2048 && *Value <= 2047; + }; + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Add)); + Descriptors.push_back(std::move(TwoAddress)); + Descriptors.push_back(std::move(Load)); + std::vector<rcc::ImmediatePredicate> Predicates; + Predicates.push_back(std::move(Signed12)); + return rcc::InstructionSet::create(std::move(Descriptors), std::move(Predicates)); +} + +void testDescriptorStructure(TestContext &Context) { + Context.expect(!rcc::InstructionSet::create({}), "an empty instruction set must be rejected"); + + auto DefAfterUses = minimalDescriptor(10, "BAD_DEF_ORDER"); + DefAfterUses.Operands.push_back(registerDescriptor(rcc::RegisterOperandRole::Def)); + Context.expect(!rcc::InstructionSet::create({DefAfterUses}), + "a definition outside the explicit def prefix must be rejected"); + + auto AmbiguousVariadic = minimalDescriptor(11, "BAD_VARIADIC"); + auto Optional = registerDescriptor(rcc::RegisterOperandRole::Use); + Optional.Optional = true; + auto Variadic = registerDescriptor(rcc::RegisterOperandRole::Use); + Variadic.VariadicGroup = true; + AmbiguousVariadic.Operands.push_back(Optional); + AmbiguousVariadic.Operands.push_back(Variadic); + AmbiguousVariadic.Variadic = true; + Context.expect(!rcc::InstructionSet::create({AmbiguousVariadic}), + "optional and variadic operand tails must not be combined"); + + auto BadTie = minimalDescriptor(12, "BAD_TIE"); + auto Def = registerDescriptor(rcc::RegisterOperandRole::Def); + auto Use = registerDescriptor(rcc::RegisterOperandRole::Use); + Def.DefaultTiedOperand = 1; + BadTie.Operands.push_back(Def); + BadTie.Operands.push_back(Use); + BadTie.NumExplicitDefs = 1; + Context.expect(!rcc::InstructionSet::create({BadTie}), "default operand ties must be reciprocal"); + + auto DuplicateNames = minimalDescriptor(13, "BAD_NAMES"); + auto FirstUse = registerDescriptor(rcc::RegisterOperandRole::Use); + auto SecondUse = registerDescriptor(rcc::RegisterOperandRole::Use); + FirstUse.Name = rcc::NamedOperandID::fromValue(9); + SecondUse.Name = rcc::NamedOperandID::fromValue(9); + DuplicateNames.Operands.push_back(FirstUse); + DuplicateNames.Operands.push_back(SecondUse); + Context.expect(!rcc::InstructionSet::create({DuplicateNames}), + "named operand IDs must be unique within an instruction"); + + auto BadBranch = minimalDescriptor(14, "BAD_BRANCH"); + BadBranch.Flags.set(rcc::InstructionFlag::Branch); + Context.expect(!rcc::InstructionSet::create({BadBranch}), + "branch descriptors must also be terminators"); + + auto UnknownPredicate = minimalDescriptor(15, "BAD_PREDICATE"); + rcc::OperandDescriptor Immediate; + Immediate.Kinds = {rcc::MachineOperand::Kind::Immediate}; + Immediate.ImmediatePredicate = rcc::ImmediatePredicateID::fromValue(99); + UnknownPredicate.Operands.push_back(Immediate); + Context.expect(!rcc::InstructionSet::create({UnknownPredicate}), + "operand descriptors must not reference unknown predicates"); +} + +void testOperandValidation(TestContext &Context) { + auto InstructionSet = createValidInstructionSet(); + Context.expect(InstructionSet.has_value(), "the complete test instruction set must be valid"); + if (!InstructionSet) + return; + + auto Function = rcc::MachineFunction::create("descriptor_operands", + rcc::MachineFunctionStableID::fromValue(1)); + Context.expect(Function.has_value(), "descriptor test function must be valid"); + if (!Function) + return; + + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Destination = (*Function)->registerInfo().createVirtualRegister(*I64); + auto Source = (*Function)->registerInfo().createVirtualRegister(*I64); + Context.expect(Destination && Source, "descriptor test virtual registers must be allocated"); + if (!Destination || !Source) + return; + + auto &RegInfo = (*Function)->registerInfo(); + Context.expect( + RegInfo.setRegisterBank(*Destination, rcc::RegisterBankID::fromValue(7)).has_value() && + RegInfo.setRegisterClass(*Destination, rcc::RegisterClassID::fromValue(8)).has_value(), + "descriptor test destination constraints must be recorded"); + + std::vector<rcc::MachineOperand> ValidOperands; + ValidOperands.push_back(*registerOperand(*Destination, {rcc::RegisterOperandFlag::Define})); + ValidOperands.push_back(*registerOperand(*Source, {rcc::RegisterOperandFlag::Use})); + ValidOperands.push_back(rcc::MachineOperand::immediate(42, 0x1)); + Context.expect( + InstructionSet->validateExplicitOperands(rcc::Opcode::fromValue(1), ValidOperands, RegInfo) + .has_value(), + "matching roles, constraints, flags, and predicate must validate"); + + std::vector<rcc::MachineOperand> SymbolOperands; + SymbolOperands.push_back(*registerOperand(*Destination, {rcc::RegisterOperandFlag::Define})); + SymbolOperands.push_back(*registerOperand(*Source, {rcc::RegisterOperandFlag::Use})); + SymbolOperands.push_back(*rcc::MachineOperand::globalSymbol("object", 0, 0x1)); + Context.expect( + InstructionSet->validateExplicitOperands(rcc::Opcode::fromValue(1), SymbolOperands, RegInfo) + .has_value(), + "an immediate predicate must not reject another allowed symbolic operand kind"); + + std::vector<rcc::MachineOperand> WrongRole; + WrongRole.push_back(*registerOperand(*Destination, {rcc::RegisterOperandFlag::Use})); + WrongRole.push_back(*registerOperand(*Source, {rcc::RegisterOperandFlag::Use})); + WrongRole.push_back(rcc::MachineOperand::immediate(1)); + Context.expect( + !InstructionSet->validateExplicitOperands(rcc::Opcode::fromValue(1), WrongRole, RegInfo), + "a register role mismatch must be rejected"); + + std::vector<rcc::MachineOperand> BadImmediate; + BadImmediate.push_back(*registerOperand(*Destination, {rcc::RegisterOperandFlag::Define})); + BadImmediate.push_back(*registerOperand(*Source, {rcc::RegisterOperandFlag::Use})); + BadImmediate.push_back(rcc::MachineOperand::immediate(4096)); + Context.expect( + !InstructionSet->validateExplicitOperands(rcc::Opcode::fromValue(1), BadImmediate, RegInfo), + "an operand outside its immediate predicate must be rejected"); + + std::vector<rcc::MachineOperand> BadTargetFlags; + BadTargetFlags.push_back(*registerOperand(*Destination, {rcc::RegisterOperandFlag::Define})); + BadTargetFlags.push_back(*registerOperand(*Source, {rcc::RegisterOperandFlag::Use})); + BadTargetFlags.push_back(rcc::MachineOperand::immediate(1, 0x4)); + Context.expect( + !InstructionSet->validateExplicitOperands(rcc::Opcode::fromValue(1), BadTargetFlags, RegInfo), + "unregistered target operand flags must be rejected"); +} + +void testInstructionValidation(TestContext &Context) { + auto InstructionSet = createValidInstructionSet(); + auto Function = rcc::MachineFunction::create("descriptor_instructions", + rcc::MachineFunctionStableID::fromValue(2)); + Context.expect(InstructionSet && Function, "instruction validation setup must succeed"); + if (!InstructionSet || !Function) + return; + + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Reg = (*Function)->registerInfo().createVirtualRegister(*I64); + Context.expect(Reg.has_value(), "instruction validation register must be allocated"); + if (!Reg) + return; + + auto Tied = (*Function)->createInstruction(rcc::Opcode::fromValue(2)); + Context.expect(Tied.has_value(), "tied instruction must be allocated"); + if (!Tied) + return; + Context.expect( + (*Tied)->addOperand(*registerOperand(*Reg, {rcc::RegisterOperandFlag::Define})).has_value() && + (*Tied) + ->addOperand(*registerOperand(*Reg, {rcc::RegisterOperandFlag::Use})) + .has_value() && + (*Tied)->tieOperands(0, 1).has_value(), + "tied instruction operands must be materialized"); + Context.expect( + InstructionSet->validateInstruction(**Tied, (*Function)->registerInfo()).has_value(), + "a reciprocal descriptor-required tie must validate"); + + auto Untied = (*Function)->createInstruction(rcc::Opcode::fromValue(2)); + Context.expect(Untied.has_value(), "untied instruction must be allocated"); + if (Untied) { + Context.expect((*Untied) + ->addOperand(*registerOperand(*Reg, {rcc::RegisterOperandFlag::Define})) + .has_value() && + (*Untied) + ->addOperand(*registerOperand(*Reg, {rcc::RegisterOperandFlag::Use})) + .has_value(), + "untied instruction operands must be added"); + Context.expect(!InstructionSet->validateInstruction(**Untied, (*Function)->registerInfo()), + "a missing descriptor-required tie must be rejected"); + } + + auto Destination = (*Function)->registerInfo().createVirtualRegister(*I64); + auto Source = (*Function)->registerInfo().createVirtualRegister(*I64); + Context.expect(Destination && Source, "implicit operand validation registers must be allocated"); + if (!Destination || !Source) + return; + auto &RegInfo = (*Function)->registerInfo(); + Context.expect( + RegInfo.setRegisterBank(*Destination, rcc::RegisterBankID::fromValue(7)).has_value() && + RegInfo.setRegisterClass(*Destination, rcc::RegisterClassID::fromValue(8)).has_value(), + "implicit validation destination constraints must be recorded"); + + auto WithImplicit = (*Function)->createInstruction(rcc::Opcode::fromValue(1)); + const rcc::InstructionDescriptor *AddDescriptor = + InstructionSet->descriptor(rcc::Opcode::fromValue(1)); + Context.expect(WithImplicit && AddDescriptor != nullptr, + "implicit instruction setup must find its descriptor"); + if (!WithImplicit || AddDescriptor == nullptr) + return; + Context.expect( + (*WithImplicit) + ->addOperand(*registerOperand(*Destination, {rcc::RegisterOperandFlag::Define})) + .has_value() && + (*WithImplicit) + ->addOperand(*registerOperand(*Source, {rcc::RegisterOperandFlag::Use})) + .has_value() && + (*WithImplicit)->addOperand(rcc::MachineOperand::immediate(7)).has_value(), + "explicit operands for implicit validation must be added"); + auto DefaultImplicit = InstructionSet->createDefaultImplicitOperands(*AddDescriptor); + Context.expect(DefaultImplicit.has_value(), "default implicit operands must be constructible"); + if (!DefaultImplicit) + return; + for (rcc::MachineOperand &Operand : *DefaultImplicit) { + Context.expect((*WithImplicit)->addOperand(std::move(Operand)).has_value(), + "default implicit operand insertion must succeed"); + } + Context.expect(InstructionSet->validateInstruction(**WithImplicit, RegInfo).has_value(), + "exact default implicit operands must validate"); + + rcc::MachineOperand *ImplicitDef = (*WithImplicit)->operandAt(3); + Context.expect(ImplicitDef != nullptr && + ImplicitDef + ->setRegisterFlags( + {rcc::RegisterOperandFlag::Define, rcc::RegisterOperandFlag::Implicit}) + .has_value(), + "implicit definition flags must be mutable for a negative test"); + Context.expect(!InstructionSet->validateInstruction(**WithImplicit, RegInfo), + "implicit renamable state must match the descriptor exactly"); + + auto Load = (*Function)->createInstruction(rcc::Opcode::fromValue(3)); + Context.expect(Load.has_value(), "load instruction must be allocated"); + if (!Load) + return; + Context.expect(!InstructionSet->validateInstruction(**Load, RegInfo), + "an MMO-required load without an MMO must be rejected"); + auto MemOperand = (*Function)->createMemOperand(loadDescription()); + Context.expect(MemOperand.has_value() && (*Load)->addMemOperand(**MemOperand).has_value(), + "a function-owned load MMO must attach"); + Context.expect(InstructionSet->validateInstruction(**Load, RegInfo).has_value(), + "an MMO matching mayLoad must validate"); +} + +} // namespace + +int main() { + TestContext Context; + testDescriptorStructure(Context); + testOperandValidation(Context); + testInstructionValidation(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/LiveIntervalTest.cpp b/backend/unittests/CodeGen/LiveIntervalTest.cpp new file mode 100644 index 0000000..34ebd76 --- /dev/null +++ b/backend/unittests/CodeGen/LiveIntervalTest.cpp @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/LiveInterval.h" + +#include "LiveTestSupport.h" +#include "rcc/CodeGen/LiveIntervals.h" +#include "rcc/CodeGen/LiveVariables.h" + +namespace { + +using namespace rcc::test::liveness; + +void testLiveRange(TestContext &Context) { + const rcc::SlotIndex S16 = rcc::SlotIndex::fromValue(16).registerSlot(); + const rcc::SlotIndex S24 = rcc::SlotIndex::fromValue(24).registerSlot(); + const rcc::SlotIndex S32 = rcc::SlotIndex::fromValue(32).registerSlot(); + const rcc::SlotIndex S40 = rcc::SlotIndex::fromValue(40).registerSlot(); + const rcc::SlotIndex S48 = rcc::SlotIndex::fromValue(48).registerSlot(); + + rcc::LiveRange Range; + auto First = Range.createValue(S16); + Context.expect(First.has_value(), "live value creation must succeed"); + if (!First) + return; + Context.expect(Range.addSegment({S16, S32, *First}).has_value() && + Range.addSegment({S32, S48, *First}).has_value() && + Range.segments().size() == 1, + "adjacent same-value live segments must merge"); + auto Second = Range.createValue(S40, rcc::LiveValueState::Redefined); + Context.expect(Second.has_value() && !Range.addSegment({S24, S40, *Second}), + "overlapping different live values must be rejected"); + Context.expect(Range.valueAt(S24) != nullptr && Range.liveAt(S40) && + Range.remove(S24, S40).has_value() && Range.segments().size() == 2 && + Range.verify().has_value(), + "live-range lookup and removal must preserve ordered half-open segments"); + + rcc::LiveRange Shrunk; + auto Value = Shrunk.createValue(S16); + if (!Value) { + Context.expect(false, "shrink test value creation must succeed"); + return; + } + const std::array Uses{S32}; + Context.expect(Shrunk.addSegment({S16, S48.deadSlot(), *Value}).has_value() && + Shrunk.shrinkToUses(Uses).has_value() && Shrunk.segments().size() == 1 && + Shrunk.segments().front().End == S32.deadSlot(), + "shrinkToUses must retain exactly through the final use dead slot"); +} + +void testLaneSubrangesAndEarlyClobber(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction("lane-subranges", 3, 1); + if (!Fixture) { + Context.expect(false, "lane-subrange fixture creation must succeed"); + return; + } + auto Reg = createVReg(*Fixture->Function); + auto Early = createVReg(*Fixture->Function); + if (!Reg || !Early) { + Context.expect(false, "lane-subrange registers must be created"); + return; + } + auto LowDef = + buildDef(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg, rcc::test::TestSubReg::Low32); + auto LowUse = + buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg, rcc::test::TestSubReg::Low32); + auto HighDef = buildDef(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg, + rcc::test::TestSubReg::High32); + auto HighUse = buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg, + rcc::test::TestSubReg::High32); + auto EarlyDef = buildDef(*Fixture->Function, Target, *Fixture->Blocks[0], *Early, {}, true); + auto EarlyUse = buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], *Early); + if (!LowDef || !LowUse || !HighDef || !HighUse || !EarlyDef || !EarlyUse) { + Context.expect(false, "lane-subrange instructions must be built"); + return; + } + + rcc::MachineFunctionAnalysisManager Analyses; + auto Registered = rcc::registerStage11MachineAnalyses(Analyses); + auto Intervals = Registered + ? Analyses.getResult<rcc::LiveIntervalsAnalysis>(*Fixture->Function, Target) + : rcc::Expected<rcc::LiveIntervalsAnalysis::Result *>{ + std::unexpected(Registered.error())}; + if (!Intervals) { + Context.expect(false, "lane-subrange intervals must materialize"); + return; + } + const rcc::LiveInterval *Interval = (*Intervals)->interval(*Reg); + const rcc::LiveInterval *EarlyInterval = (*Intervals)->interval(*Early); + const bool HasLow = Interval != nullptr && + std::ranges::any_of(Interval->SubRanges, [](const rcc::LiveSubRange &Range) { + return Range.Lanes == rcc::LaneMask::fromRaw(0x3); + }); + const bool HasHigh = Interval != nullptr && + std::ranges::any_of(Interval->SubRanges, [](const rcc::LiveSubRange &Range) { + return Range.Lanes == rcc::LaneMask::fromRaw(0xC); + }); + Context.expect(Interval != nullptr && Interval->SubRanges.size() == 2 && HasLow && HasHigh, + "partial virtual-register accesses must form disjoint lane subranges"); + Context.expect(EarlyInterval != nullptr && !EarlyInterval->MainRange.values().empty() && + EarlyInterval->MainRange.values().front().Definition.kind() == + rcc::SlotIndexKind::EarlyClobber, + "early-clobber definitions must use the early-clobber slot"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "liveness TestTarget must be created"); + if (!Target) + return Context.result(); + testLiveRange(Context); + testLaneSubrangesAndEarlyClobber(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/LiveIntervalsTest.cpp b/backend/unittests/CodeGen/LiveIntervalsTest.cpp new file mode 100644 index 0000000..a080e4d --- /dev/null +++ b/backend/unittests/CodeGen/LiveIntervalsTest.cpp @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/LiveIntervals.h" + +#include "LiveTestSupport.h" +#include "rcc/CodeGen/LiveRegUnits.h" +#include "rcc/CodeGen/LiveVariables.h" +#include "rcc/Target/RISCV/RISCVInstructions.h" +#include "rcc/Target/RISCV/RISCVRegisters.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +namespace { + +using namespace rcc::test::liveness; + +void testCFGAndValueNumbers(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction("cfg-liveness", 2, 8); + if (!Fixture) { + Context.expect(false, "CFG liveness fixture creation must succeed"); + return; + } + auto &Function = *Fixture->Function; + auto &Entry = *Fixture->Blocks[0]; + auto &Left = *Fixture->Blocks[1]; + auto &Right = *Fixture->Blocks[2]; + auto &Join = *Fixture->Blocks[3]; + auto &Header = *Fixture->Blocks[4]; + auto &Body = *Fixture->Blocks[5]; + auto &Exit = *Fixture->Blocks[6]; + auto &Dead = *Fixture->Blocks[7]; + auto Half = rcc::BranchProbability::fromFraction(1, 2); + auto Updater = Function.cfgUpdater(); + const bool CFGValid = Half && Updater.addSuccessor(Entry, Left, *Half) && + Updater.addSuccessor(Entry, Right, *Half) && + Updater.addSuccessor(Left, Join, rcc::BranchProbability::one()) && + Updater.addSuccessor(Right, Join, rcc::BranchProbability::one()) && + Updater.addSuccessor(Join, Header, rcc::BranchProbability::one()) && + Updater.addSuccessor(Header, Body, *Half) && + Updater.addSuccessor(Header, Exit, *Half) && + Updater.addSuccessor(Body, Header, rcc::BranchProbability::one()); + Context.expect(CFGValid, "diamond and loop CFG construction must succeed"); + if (!CFGValid) + return; + + auto Across = createVReg(Function); + auto Merged = createVReg(Function); + auto Unreachable = createVReg(Function); + auto Redefined = createVReg(Function); + if (!Across || !Merged || !Unreachable || !Redefined) { + Context.expect(false, "CFG liveness virtual registers must be created"); + return; + } + auto AcrossDef = buildDef(Function, Target, Entry, *Across); + auto LeftDef = buildDef(Function, Target, Left, *Merged); + auto RightDef = buildDef(Function, Target, Right, *Merged); + auto MergedUse = buildUse(Function, Target, Join, *Merged); + auto LoopUse = buildUse(Function, Target, Body, *Across); + auto DeadUse = buildUse(Function, Target, Dead, *Unreachable); + auto FirstRedef = buildDef(Function, Target, Exit, *Redefined); + auto SecondRedef = buildDef(Function, Target, Exit, *Redefined); + if (!AcrossDef || !LeftDef || !RightDef || !MergedUse || !LoopUse || !DeadUse || !FirstRedef || + !SecondRedef) { + Context.expect(false, "CFG liveness instructions must be built"); + return; + } + + rcc::MachineFunctionAnalysisManager Analyses; + auto Registered = rcc::registerStage11MachineAnalyses(Analyses); + auto Variables = Registered ? Analyses.getResult<rcc::LiveVariablesAnalysis>(Function, Target) + : rcc::Expected<rcc::LiveVariablesAnalysis::Result *>{ + std::unexpected(Registered.error())}; + auto Intervals = Registered ? Analyses.getResult<rcc::LiveIntervalsAnalysis>(Function, Target) + : rcc::Expected<rcc::LiveIntervalsAnalysis::Result *>{ + std::unexpected(Registered.error())}; + if (!Variables || !Intervals) { + Context.expect(false, "CFG liveness analyses must materialize"); + return; + } + Context.expect((*Variables)->isLiveOut(Entry, *Across) && (*Variables)->isLiveIn(Left, *Across) && + (*Variables)->isLiveIn(Right, *Across) && + (*Variables)->isLiveIn(Header, *Across) && + (*Variables)->isLiveOut(Body, *Across), + "CFG fixed point must propagate a value through a diamond and loop backedge"); + Context.expect( + (*Variables)->isLiveOut(Left, *Merged) && (*Variables)->isLiveOut(Right, *Merged) && + (*Variables)->isLiveIn(Join, *Merged) && (*Variables)->isLiveIn(Dead, *Unreachable), + "liveness must merge predecessor definitions and retain unreachable uses"); + + const rcc::LiveInterval *MergedInterval = (*Intervals)->interval(*Merged); + const rcc::LiveInterval *AcrossInterval = (*Intervals)->interval(*Across); + const rcc::LiveInterval *RedefinedInterval = (*Intervals)->interval(*Redefined); + const bool HasPhi = MergedInterval != nullptr && + std::ranges::any_of(MergedInterval->MainRange.values(), + [](const rcc::LiveValueNumber &Value) { + return Value.State == rcc::LiveValueState::Phi; + }); + const bool HasRedefined = + RedefinedInterval != nullptr && + std::ranges::any_of(RedefinedInterval->MainRange.values(), + [](const rcc::LiveValueNumber &Value) { + return Value.State == rcc::LiveValueState::Redefined; + }); + Context.expect(MergedInterval != nullptr && MergedInterval->MainRange.values().size() >= 3 && + HasPhi && HasRedefined, + "non-SSA intervals must retain multiple definitions, PHI, and redefined values"); + Context.expect(AcrossInterval != nullptr && (*Intervals)->interferes(*Across, *Merged), + "live intervals must expose CFG-derived interference"); +} + +void testPhysicalUnitsAndRegMasks(TestContext &Context, const rcc::TargetDescription &Target) { + const rcc::RegisterUnitID MultiLaneUnit = rcc::RegisterUnitID::fromValue(9); + rcc::LiveRegUnits PartialLanes; + PartialLanes.add(MultiLaneUnit, rcc::LaneMask::fromRaw(0x3)); + rcc::LiveRegUnits RemovedLanes; + RemovedLanes.add(MultiLaneUnit, rcc::LaneMask::fromRaw(0x2)); + PartialLanes.subtract(RemovedLanes); + Context.expect(PartialLanes.lanes(MultiLaneUnit) == rcc::LaneMask::fromRaw(0x1) && + PartialLanes.contains(MultiLaneUnit, rcc::LaneMask::fromRaw(0x1)) && + !PartialLanes.intersects(MultiLaneUnit, rcc::LaneMask::fromRaw(0x2)), + "RegisterUnit lane sets must preserve partial union and subtraction"); + + auto AliasFixture = createFunction("physical-alias", 4, 1); + if (!AliasFixture) { + Context.expect(false, "physical-alias fixture creation must succeed"); + return; + } + const rcc::Register G0 = rcc::Register::fromPhysical(physical(rcc::test::TestRegister::G0)); + const rcc::Register G0High = + rcc::Register::fromPhysical(physical(rcc::test::TestRegister::G0High32)); + auto G0Def = buildDef(*AliasFixture->Function, Target, *AliasFixture->Blocks[0], G0); + auto G0HighUse = buildUse(*AliasFixture->Function, Target, *AliasFixture->Blocks[0], G0High); + if (!G0Def || !G0HighUse) { + Context.expect(false, "physical-alias instructions must be built"); + return; + } + rcc::MachineFunctionAnalysisManager AliasAnalyses; + auto AliasRegistered = rcc::registerStage11MachineAnalyses(AliasAnalyses); + auto AliasIntervals = + AliasRegistered + ? AliasAnalyses.getResult<rcc::LiveIntervalsAnalysis>(*AliasFixture->Function, Target) + : rcc::Expected<rcc::LiveIntervalsAnalysis::Result *>{ + std::unexpected(AliasRegistered.error())}; + const rcc::RegisterUnitID HighUnit = rcc::RegisterUnitID::fromValue(3); + Context.expect(AliasIntervals && (*AliasIntervals)->regUnitRange(HighUnit) != nullptr && + (*AliasIntervals)->regUnitRange(HighUnit)->segments().size() == 1, + "shared RegisterUnits must connect super-register defs to subregister uses"); + + auto MaskFixture = createFunction("regmask", 5, 1); + if (!MaskFixture) { + Context.expect(false, "RegMask fixture creation must succeed"); + return; + } + auto Across = createVReg(*MaskFixture->Function); + auto Output = createVReg(*MaskFixture->Function); + if (!Across || !Output) { + Context.expect(false, "RegMask virtual registers must be created"); + return; + } + const auto Mask = rcc::test::createTestCallPreservedMask(); + auto AcrossDef = buildDef(*MaskFixture->Function, Target, *MaskFixture->Blocks[0], *Across); + auto FirstMask = buildMask(*MaskFixture->Function, Target, *MaskFixture->Blocks[0], Mask); + auto AcrossUse = buildUse(*MaskFixture->Function, Target, *MaskFixture->Blocks[0], *Across); + auto OutputDef = + buildMaskedDef(*MaskFixture->Function, Target, *MaskFixture->Blocks[0], *Output, Mask); + auto OutputUse = buildUse(*MaskFixture->Function, Target, *MaskFixture->Blocks[0], *Output); + if (!AcrossDef || !FirstMask || !AcrossUse || !OutputDef || !OutputUse) { + Context.expect(false, "RegMask instructions must be built"); + return; + } + rcc::MachineFunctionAnalysisManager MaskAnalyses; + auto MaskRegistered = rcc::registerStage11MachineAnalyses(MaskAnalyses); + auto MaskIntervals = + MaskRegistered + ? MaskAnalyses.getResult<rcc::LiveIntervalsAnalysis>(*MaskFixture->Function, Target) + : rcc::Expected<rcc::LiveIntervalsAnalysis::Result *>{ + std::unexpected(MaskRegistered.error())}; + if (!MaskIntervals) { + Context.expect(false, "RegMask intervals must materialize"); + return; + } + const rcc::PhysicalRegister Candidate = physical(rcc::test::TestRegister::G0); + const rcc::LiveInterval *AcrossInterval = (*MaskIntervals)->interval(*Across); + const rcc::LiveInterval *OutputInterval = (*MaskIntervals)->interval(*Output); + Context.expect((*MaskIntervals)->regMaskPositions().size() == 2 && + std::ranges::all_of((*MaskIntervals)->regMaskPositions(), + [](const rcc::RegMaskPosition &Position) { + return Position.Position.kind() == + rcc::SlotIndexKind::EarlyClobber; + }), + "RegMask interference positions must use early-clobber slots"); + Context.expect(AcrossInterval != nullptr && OutputInterval != nullptr && + (*MaskIntervals)->regMaskClobbers(*AcrossInterval, Candidate) && + !(*MaskIntervals)->regMaskClobbers(*OutputInterval, Candidate), + "RegMask queries must reject live-through values but accept same-call outputs"); + Context.expect((*MaskIntervals)->regUnitRange(rcc::RegisterUnitID::fromValue(1)) == nullptr, + "unused RegMask clobbers must not expand into physical live ranges"); +} + +void testRISCVLiveness(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFunction("riscv-liveness", 8, 1); + if (!Target || !Fixture) { + Context.expect(false, "RV64GC liveness fixture creation must succeed"); + return; + } + auto X5 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X5); + auto X6 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X6); + auto X7 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X7); + if (!X5 || !X6 || !X7) { + Context.expect(false, "RV64GC physical registers must resolve"); + return; + } + auto BuildADDI = [&](rcc::PhysicalRegister Destination, rcc::PhysicalRegister Source, + std::int64_t Immediate) -> rcc::Expected<rcc::MachineInstr *> { + rcc::MachineIRBuilder Builder(*Fixture->Function, *Target); + auto Point = Builder.setInsertPoint(*Fixture->Blocks[0]); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr( + rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::ADDI))); + if (!Pending) + return std::unexpected(Pending.error()); + auto Def = Pending->addDef(rcc::Register::fromPhysical(Destination)); + if (!Def) + return std::unexpected(Def.error()); + auto Use = Pending->addUse(rcc::Register::fromPhysical(Source)); + if (!Use) + return std::unexpected(Use.error()); + auto Imm = Pending->addImmediate(Immediate); + if (!Imm) + return std::unexpected(Imm.error()); + return Pending->build(); + }; + auto First = BuildADDI(*X6, *X5, 1); + auto Second = BuildADDI(*X7, *X6, 2); + if (!First || !Second) { + Context.expect(false, "RV64GC ADDI instructions must be built"); + return; + } + rcc::MachineFunctionAnalysisManager Analyses; + auto Registered = rcc::registerStage11MachineAnalyses(Analyses); + auto Intervals = Registered + ? Analyses.getResult<rcc::LiveIntervalsAnalysis>(*Fixture->Function, *Target) + : rcc::Expected<rcc::LiveIntervalsAnalysis::Result *>{ + std::unexpected(Registered.error())}; + const auto Unit = + rcc::RegisterUnitID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterUnit::U_X6)); + Context.expect(Intervals && (*Intervals)->regUnitRange(Unit) != nullptr && + (*Intervals)->regUnitRange(Unit)->segments().size() == 1, + "production RV64GC register units must participate in LiveIntervals"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "liveness TestTarget must be created"); + if (!Target) + return Context.result(); + testCFGAndValueNumbers(Context, *Target); + testPhysicalUnitsAndRegMasks(Context, *Target); + testRISCVLiveness(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/LivePhysRegsTest.cpp b/backend/unittests/CodeGen/LivePhysRegsTest.cpp new file mode 100644 index 0000000..6eea796 --- /dev/null +++ b/backend/unittests/CodeGen/LivePhysRegsTest.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/LivePhysRegs.h" + +#include "LiveTestSupport.h" +#include "rcc/CodeGen/LiveVariables.h" + +namespace { + +using namespace rcc::test::liveness; + +void testPostRALivenessRepair(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction("post-ra-repair", 12, 1); + if (!Fixture) { + Context.expect(false, "post-RA repair fixture creation must succeed"); + return; + } + const rcc::Register G0 = rcc::Register::fromPhysical(physical(rcc::test::TestRegister::G0)); + auto Def = buildDef(*Fixture->Function, Target, *Fixture->Blocks[0], G0); + auto FirstUse = buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], G0); + auto LastUse = buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], G0); + if (!Def || !FirstUse || !LastUse) { + Context.expect(false, "post-RA repair instructions must be built"); + return; + } + + rcc::MachineFunctionAnalysisManager Analyses; + auto Registered = rcc::registerStage11MachineAnalyses(Analyses); + auto Liveness = Registered + ? Analyses.getResult<rcc::LiveVariablesAnalysis>(*Fixture->Function, Target) + : rcc::Expected<rcc::LiveVariablesAnalysis::Result *>{ + std::unexpected(Registered.error())}; + if (!Liveness) { + Context.expect(false, "post-RA repair liveness must materialize"); + return; + } + const std::array Blocks{Fixture->Blocks[0]}; + auto Repaired = rcc::repairPostRAMachineBlockLiveness(Blocks, Target, **Liveness); + Context.expect(Repaired && *Repaired, "post-RA repair must update stale kill/dead flags"); + Context.expect(!hasFlag(**Def, 0, rcc::RegisterOperandFlag::Dead) && + !hasFlag(**FirstUse, 0, rcc::RegisterOperandFlag::Kill) && + hasFlag(**LastUse, 0, rcc::RegisterOperandFlag::Kill), + "post-RA repair must derive precise physical kill/dead flags"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "liveness TestTarget must be created"); + if (!Target) + return Context.result(); + testPostRALivenessRepair(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/LiveRegMatrixTest.cpp b/backend/unittests/CodeGen/LiveRegMatrixTest.cpp new file mode 100644 index 0000000..423da50 --- /dev/null +++ b/backend/unittests/CodeGen/LiveRegMatrixTest.cpp @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/LiveRegMatrix.h" + +#include "CodeGen/RegisterAllocationTestSupport.h" + +#include <algorithm> +#include <iostream> + +namespace { + +using namespace rcc; +using namespace rcc::test; +using namespace rcc::test::ra; + +void testLaneAndAssignment(TestContext &Context, const TargetDescription &Target) { + auto Fixture = createFunction("matrix-lanes", 1); + if (!Fixture) + return; + auto Low = createVReg(*Fixture->Function); + auto High = createVReg(*Fixture->Function); + if (!Low || !High || + !buildDef(*Fixture->Function, Target, *Fixture->Block, *Low, 1, TestSubReg::Low32) || + !buildDef(*Fixture->Function, Target, *Fixture->Block, *High, 2, TestSubReg::High32) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *High, TestSubReg::High32) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Low, TestSubReg::Low32)) + return; + + MachineFunctionAnalysisManager Analyses; + if (!registerStage11MachineAnalyses(Analyses)) + return; + auto Intervals = Analyses.getResult<LiveIntervalsAnalysis>(*Fixture->Function, Target); + if (!Intervals) + return; + const PhysicalRegister G0 = physical(TestRegister::G0); + LiveRegMatrix Matrix(*Fixture->Function, Target, **Intervals); + Context.expect(Matrix.assign(*Low, G0).has_value() && + Matrix.checkInterference(*High, G0).Kind == RegisterInterferenceKind::Free && + Matrix.assign(*High, G0).has_value() && Matrix.assignment(*Low) == G0, + "LiveRegMatrix must allow disjoint lanes and publish assignments"); + Context.expect(Matrix.unassign(*Low).has_value() && !Matrix.assignment(*Low), + "LiveRegMatrix must remove an assignment without affecting other lanes"); +} + +void testFixedAndMaskInterference(TestContext &Context, const TargetDescription &Target) { + auto Fixture = createFunction("matrix-fixed", 2); + if (!Fixture) + return; + auto Value = createVReg(*Fixture->Function); + const PhysicalRegister G0 = physical(TestRegister::G0); + const PhysicalRegister G1 = physical(TestRegister::G1); + if (!Value || !buildDef(*Fixture->Function, Target, *Fixture->Block, *Value, 7) || + !buildMask(*Fixture->Function, Target, *Fixture->Block, createTestCallPreservedMask()) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, Register::fromPhysical(G0)) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value)) + return; + + MachineFunctionAnalysisManager Analyses; + if (!registerStage11MachineAnalyses(Analyses)) + return; + auto Intervals = Analyses.getResult<LiveIntervalsAnalysis>(*Fixture->Function, Target); + if (!Intervals) + return; + LiveRegMatrix Matrix(*Fixture->Function, Target, **Intervals); + const auto G0Interferences = Matrix.collectInterferences(*Value, G0); + const auto G1Interferences = Matrix.collectInterferences(*Value, G1); + const auto HasKind = [](std::span<const RegisterInterference> Values, + RegisterInterferenceKind Kind) { + return std::ranges::any_of( + Values, [Kind](const RegisterInterference &Value) { return Value.Kind == Kind; }); + }; + Context.expect(HasKind(G0Interferences, RegisterInterferenceKind::FixedRegister) && + HasKind(G0Interferences, RegisterInterferenceKind::RegisterMask) && + !HasKind(G1Interferences, RegisterInterferenceKind::FixedRegister) && + Matrix.assign(*Value, G1).has_value(), + "LiveRegMatrix must distinguish fixed-register and RegMask interference"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "LiveRegMatrix test target must be constructible"); + if (!Target) { + std::cerr << Target.error().message() << '\n'; + return Context.result(); + } + testLaneAndAssignment(Context, *Target); + testFixedAndMaskInterference(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/LiveRegUnitsTest.cpp b/backend/unittests/CodeGen/LiveRegUnitsTest.cpp new file mode 100644 index 0000000..7f224ac --- /dev/null +++ b/backend/unittests/CodeGen/LiveRegUnitsTest.cpp @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/LiveRegUnits.h" + +#include "LiveTestSupport.h" + +namespace { + +using namespace rcc::test::liveness; + +void testLiveRegUnits(TestContext &Context) { + const rcc::RegisterUnitID Unit = rcc::RegisterUnitID::fromValue(9); + rcc::LiveRegUnits Present; + rcc::LiveRegUnits Removed; + Present.add(Unit, rcc::LaneMask::fromRaw(0x3)); + Removed.add(Unit, rcc::LaneMask::fromRaw(0x2)); + Present.subtract(Removed); + Context.expect(Present.lanes(Unit) == rcc::LaneMask::fromRaw(0x1) && + Present.contains(Unit, rcc::LaneMask::fromRaw(0x1)) && + !Present.intersects(Unit, rcc::LaneMask::fromRaw(0x2)), + "LiveRegUnits must preserve partial union and subtraction"); + Present.unite(Removed); + Context.expect(Present.contains(Unit, rcc::LaneMask::fromRaw(0x3)), + "LiveRegUnits union must restore all selected lanes"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "liveness TestTarget must be created"); + if (!Target) + return Context.result(); + testLiveRegUnits(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/LiveTestSupport.h b/backend/unittests/CodeGen/LiveTestSupport.h new file mode 100644 index 0000000..f6a3566 --- /dev/null +++ b/backend/unittests/CodeGen/LiveTestSupport.h @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RCC_BACKEND_LIVE_TEST_SUPPORT_H +#define RCC_BACKEND_LIVE_TEST_SUPPORT_H + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineCFGUpdater.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/MachineVerifier.h" +#include "rcc/CodeGen/TargetDescription.h" + +#include <algorithm> +#include <array> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <memory> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace rcc::test::liveness { + +constexpr std::uint32_t DefOpcode = 2048; +constexpr std::uint32_t UseOpcode = 2049; +constexpr std::uint32_t EarlyDefOpcode = 2050; +constexpr std::uint32_t MaskOpcode = 2051; +constexpr std::uint32_t MaskedDefOpcode = 2052; +constexpr std::uint32_t LiveOutMaskOpcode = 2053; + +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]] inline rcc::Opcode testOpcode(std::uint32_t Value) { + return rcc::Opcode::fromValue(Value); +} + +[[nodiscard]] inline rcc::OperandDescriptor registerOperand(rcc::RegisterOperandRole Role, + bool EarlyClobber = false) { + rcc::OperandDescriptor Result; + Result.Kinds = {rcc::MachineOperand::Kind::Register}; + Result.RegisterRole = Role; + Result.EarlyClobber = EarlyClobber; + Result.AllowSubReg = true; + return Result; +} + +[[nodiscard]] inline rcc::OperandDescriptor plainOperand(rcc::MachineOperand::Kind Kind) { + rcc::OperandDescriptor Result; + Result.Kinds = {Kind}; + return Result; +} + +[[nodiscard]] inline rcc::InstructionDescriptor instruction(std::uint32_t Value, std::string Name) { + rcc::InstructionDescriptor Result; + Result.InstructionOpcode = testOpcode(Value); + Result.Name = std::move(Name); + Result.Flags = {rcc::InstructionFlag::ProducesCode}; + return Result; +} + +[[nodiscard]] inline rcc::Expected<rcc::TargetDescription> createTarget() { + std::vector<rcc::InstructionDescriptor> Descriptors; + std::vector<rcc::ImmediatePredicate> Predicates; + auto Generic = rcc::appendGenericInstructionInfo(Descriptors, Predicates); + if (!Generic) + return std::unexpected(Generic.error()); + + auto Def = instruction(DefOpcode, "LIVENESS_DEF"); + Def.Operands = {registerOperand(rcc::RegisterOperandRole::Def)}; + Def.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Def)); + + auto Use = instruction(UseOpcode, "LIVENESS_USE"); + Use.Operands = {registerOperand(rcc::RegisterOperandRole::Use)}; + Descriptors.push_back(std::move(Use)); + + auto EarlyDef = instruction(EarlyDefOpcode, "LIVENESS_EARLY_DEF"); + EarlyDef.Operands = {registerOperand(rcc::RegisterOperandRole::Def, true)}; + EarlyDef.NumExplicitDefs = 1; + Descriptors.push_back(std::move(EarlyDef)); + + auto Mask = instruction(MaskOpcode, "LIVENESS_MASK"); + Mask.Operands = {plainOperand(rcc::MachineOperand::Kind::RegisterMask)}; + Mask.Flags.set(rcc::InstructionFlag::Call); + Mask.Flags.set(rcc::InstructionFlag::HasSideEffects); + Descriptors.push_back(std::move(Mask)); + + auto MaskedDef = instruction(MaskedDefOpcode, "LIVENESS_MASKED_DEF"); + MaskedDef.Operands = {registerOperand(rcc::RegisterOperandRole::Def), + plainOperand(rcc::MachineOperand::Kind::RegisterMask)}; + MaskedDef.NumExplicitDefs = 1; + MaskedDef.Flags.set(rcc::InstructionFlag::Call); + MaskedDef.Flags.set(rcc::InstructionFlag::HasSideEffects); + Descriptors.push_back(std::move(MaskedDef)); + + auto LiveOutMask = instruction(LiveOutMaskOpcode, "LIVENESS_LIVEOUT_MASK"); + LiveOutMask.Operands = {plainOperand(rcc::MachineOperand::Kind::LiveOutRegisterMask)}; + Descriptors.push_back(std::move(LiveOutMask)); + + 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)); +} + +struct FunctionFixture final { + std::unique_ptr<rcc::MachineFunction> Function; + std::vector<rcc::MachineBasicBlock *> Blocks; +}; + +[[nodiscard]] inline rcc::Expected<FunctionFixture> +createFunction(std::string Name, std::uint64_t ID, std::size_t BlockCount) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + if (!Function) + return std::unexpected(Function.error()); + FunctionFixture Result{std::move(*Function), {}}; + Result.Blocks.reserve(BlockCount); + for (std::size_t Index = 0; Index < BlockCount; ++Index) { + auto Block = Result.Function->createBasicBlock(); + if (!Block) + return std::unexpected(Block.error()); + auto Appended = Result.Function->appendBlock(**Block); + if (!Appended) + return std::unexpected(Appended.error()); + Result.Blocks.push_back(*Block); + } + return Result; +} + +[[nodiscard]] inline rcc::Expected<rcc::Register> createVReg(rcc::MachineFunction &Function) { + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + if (!Type) + return std::unexpected(Type.error()); + auto Reg = Function.registerInfo().createVirtualRegister(*Type); + if (!Reg) + return std::unexpected(Reg.error()); + auto Bank = Function.registerInfo().setRegisterBank(*Reg, rcc::test::TestRegisterBank::GPR); + if (!Bank) + return std::unexpected(Bank.error()); + auto Class = Function.registerInfo().setRegisterClass(*Reg, rcc::test::TestRegisterClass::GPR64); + if (!Class) + return std::unexpected(Class.error()); + return *Reg; +} + +[[nodiscard]] inline rcc::Expected<rcc::MachineInstr *> +buildDef(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Register Reg, rcc::SubRegIndex SubReg = {}, + bool EarlyClobber = false) { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = Builder.setInsertPoint(Block); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(testOpcode(EarlyClobber ? EarlyDefOpcode : DefOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + rcc::RegisterOperandFlags Flags; + if (EarlyClobber) + Flags.set(rcc::RegisterOperandFlag::EarlyClobber); + auto Added = Pending->addDef(Reg, Flags, SubReg); + if (!Added) + return std::unexpected(Added.error()); + return Pending->build(); +} + +[[nodiscard]] inline rcc::Expected<rcc::MachineInstr *> +buildUse(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Register Reg, rcc::SubRegIndex SubReg = {}, + rcc::MachineInstr *Before = nullptr) { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = + Before == nullptr ? Builder.setInsertPoint(Block) : Builder.setInsertPointBefore(*Before); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(testOpcode(UseOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + auto Added = Pending->addUse(Reg, {}, SubReg); + if (!Added) + return std::unexpected(Added.error()); + return Pending->build(); +} + +[[nodiscard]] inline rcc::Expected<rcc::MachineInstr *> +buildMask(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, std::shared_ptr<const rcc::RegisterMask> Mask) { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = Builder.setInsertPoint(Block); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(testOpcode(MaskOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + auto Added = Pending->addRegisterMask(std::move(Mask)); + if (!Added) + return std::unexpected(Added.error()); + return Pending->build(); +} + +[[nodiscard]] inline rcc::Expected<rcc::MachineInstr *> +buildMaskedDef(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Register Reg, + std::shared_ptr<const rcc::RegisterMask> Mask) { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = Builder.setInsertPoint(Block); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(testOpcode(MaskedDefOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + auto Def = Pending->addDef(Reg); + if (!Def) + return std::unexpected(Def.error()); + auto MaskResult = Pending->addRegisterMask(std::move(Mask)); + if (!MaskResult) + return std::unexpected(MaskResult.error()); + return Pending->build(); +} + +[[nodiscard]] inline rcc::PhysicalRegister physical(std::uint32_t Number) { + return *rcc::PhysicalRegister::create(Number); +} + +[[nodiscard]] inline bool hasFlag(const rcc::MachineInstr &Instruction, std::size_t OperandIndex, + rcc::RegisterOperandFlag Flag) { + const rcc::MachineOperand *Operand = Instruction.operandAt(OperandIndex); + return Operand != nullptr && Operand->registerFlags().has(Flag); +} + +inline void setRebuildProperties(rcc::MachineFunction &Function) { + Function.properties().set(rcc::MachineFunctionProperty::Legalized); + Function.properties().set(rcc::MachineFunctionProperty::RegBankSelected); + Function.properties().set(rcc::MachineFunctionProperty::Selected); + Function.properties().set(rcc::MachineFunctionProperty::NoPHIs); + Function.properties().set(rcc::MachineFunctionProperty::TiedOpsRewritten); +} + +} // namespace rcc::test::liveness + +#endif // RCC_BACKEND_LIVE_TEST_SUPPORT_H diff --git a/backend/unittests/CodeGen/LiveVariablesTest.cpp b/backend/unittests/CodeGen/LiveVariablesTest.cpp new file mode 100644 index 0000000..1cc4e91 --- /dev/null +++ b/backend/unittests/CodeGen/LiveVariablesTest.cpp @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/LiveVariables.h" + +#include "LiveTestSupport.h" +#include "rcc/CodeGen/LiveIntervals.h" +#include "rcc/Target/RISCV/RISCVInstructions.h" +#include "rcc/Target/RISCV/RISCVRegisters.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +namespace { + +using namespace rcc::test::liveness; + +void testLivenessRebuildAndVerifier(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction("rebuild", 6, 2); + if (!Fixture) { + Context.expect(false, "liveness-rebuild fixture creation must succeed"); + return; + } + auto &Function = *Fixture->Function; + auto &Entry = *Fixture->Blocks[0]; + auto &Exit = *Fixture->Blocks[1]; + auto Edge = Function.cfgUpdater().addSuccessor(Entry, Exit, rcc::BranchProbability::one()); + const rcc::Register G0 = rcc::Register::fromPhysical(physical(rcc::test::TestRegister::G0)); + const rcc::Register G1 = rcc::Register::fromPhysical(physical(rcc::test::TestRegister::G1)); + auto LiveDef = buildDef(Function, Target, Entry, G0); + auto FirstUse = buildUse(Function, Target, Exit, G0); + auto LastUse = buildUse(Function, Target, Exit, G0); + auto DeadDef = buildDef(Function, Target, Exit, G1); + if (!Edge || !LiveDef || !FirstUse || !LastUse || !DeadDef) { + Context.expect(false, "liveness-rebuild MachineIR must be constructed"); + return; + } + auto StaleLiveIn = Entry.addLiveIn(physical(rcc::test::TestRegister::F0)); + Context.expect(StaleLiveIn.has_value(), "stale live-in setup must succeed"); + setRebuildProperties(Function); + + rcc::MachineFunctionPassManager Pipeline; + auto Added = Pipeline.addPass(std::make_unique<rcc::RebuildMachineLivenessPass>()); + rcc::MachineFunctionAnalysisManager Analyses; + auto Run = Added ? Pipeline.run(Function, Target, Analyses) + : rcc::Expected<void>{std::unexpected(Added.error())}; + if (!Run) + std::cerr << "liveness rebuild pipeline error: " << Run.error().message() << '\n'; + Context.expect(Run.has_value() && + Function.properties().has(rcc::MachineFunctionProperty::TracksLiveness), + "RebuildMachineLiveness must publish TracksLiveness through the pass manager"); + if (!Run) + return; + Context.expect( + Entry.liveIns().empty() && Exit.liveIns().size() == 1 && + Exit.liveIns().front().PhysReg == physical(rcc::test::TestRegister::G0) && + Exit.liveIns().front().Lanes == rcc::LaneMask::all(), + "liveness rebuild must replace stale live-ins with canonical whole-register input"); + Context.expect(!hasFlag(**LiveDef, 0, rcc::RegisterOperandFlag::Dead) && + !hasFlag(**FirstUse, 0, rcc::RegisterOperandFlag::Kill) && + hasFlag(**LastUse, 0, rcc::RegisterOperandFlag::Kill) && + hasFlag(**DeadDef, 0, rcc::RegisterOperandFlag::Dead), + "reverse reconstruction must derive conservative kill and dead flags"); + Context.expect(rcc::MachineVerifier::verify(Function, Target).has_value(), + "rebuilt tracked liveness must pass the MachineVerifier"); + + auto ConservativeLiveIn = Entry.addLiveIn(physical(rcc::test::TestRegister::F0)); + Context.expect(ConservativeLiveIn.has_value() && + rcc::MachineVerifier::verify(Function, Target).has_value(), + "MachineVerifier must accept conservative physical live-ins"); + Entry.clearLiveIns(); + + rcc::MachineOperand *FirstUseOperand = (*FirstUse)->operandAt(0); + if (FirstUseOperand == nullptr) { + Context.expect(false, "first use operand must exist"); + return; + } + rcc::RegisterOperandFlags StaleFlags = FirstUseOperand->registerFlags(); + StaleFlags.set(rcc::RegisterOperandFlag::Kill); + auto StaleKill = FirstUseOperand->setRegisterFlags(StaleFlags); + Context.expect(StaleKill.has_value() && !rcc::MachineVerifier::verify(Function, Target), + "MachineVerifier must reject an overstrong tracked kill flag"); + StaleFlags.clear(rcc::RegisterOperandFlag::Kill); + auto ClearedKill = FirstUseOperand->setRegisterFlags(StaleFlags); + Context.expect(ClearedKill.has_value(), "stale kill flag cleanup must succeed"); + Exit.clearLiveIns(); + Context.expect(!rcc::MachineVerifier::verify(Function, Target), + "MachineVerifier must reject stale tracked physical live-ins"); + + rcc::RebuildMachineLivenessPass Rebuild; + auto Repaired = Rebuild.run(Function, Target, Analyses); + Context.expect(Repaired && rcc::MachineVerifier::verify(Function, Target), + "a second liveness rebuild must repair stale derived state"); +} + +void testTrackedLiveThroughPhysicalRegister(TestContext &Context, + const rcc::TargetDescription &Target) { + auto Fixture = createFunction("tracked-live-through", 11, 2); + if (!Fixture) { + Context.expect(false, "tracked live-through fixture creation must succeed"); + return; + } + auto &Function = *Fixture->Function; + auto &Predecessor = *Fixture->Blocks[0]; + auto &Successor = *Fixture->Blocks[1]; + const rcc::Register G0 = rcc::Register::fromPhysical(physical(rcc::test::TestRegister::G0)); + auto Edge = + Function.cfgUpdater().addSuccessor(Predecessor, Successor, rcc::BranchProbability::one()); + auto Use = buildUse(Function, Target, Successor, G0); + auto LiveIn = Successor.addLiveIn(physical(rcc::test::TestRegister::G0)); + if (!Edge || !Use || !LiveIn) { + Context.expect(false, "tracked live-through MachineIR must be constructed"); + return; + } + + Function.properties().set(rcc::MachineFunctionProperty::TracksLiveness); + Context.expect(rcc::verifyTrackedMachineLiveness(Function, Target).has_value() && + Predecessor.liveIns().empty(), + "a predecessor must not declare a physical register used only by its successor"); + Successor.clearLiveIns(); + Context.expect(!rcc::verifyTrackedMachineLiveness(Function, Target), + "a block must declare a physical register that it reads before defining"); +} + +void testFailedRebuildIsAtomic(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction("failed-rebuild", 9, 2); + if (!Fixture) { + Context.expect(false, "failed-rebuild fixture creation must succeed"); + return; + } + auto Destination = createVReg(*Fixture->Function); + auto Source = createVReg(*Fixture->Function); + if (!Destination || !Source) { + Context.expect(false, "failed-rebuild virtual registers must be created"); + return; + } + + auto &Predecessor = *Fixture->Blocks[0]; + auto &PhiBlock = *Fixture->Blocks[1]; + const auto LiveIn = + PhiBlock.addLiveIn(physical(rcc::test::TestRegister::G0), rcc::LaneMask::fromRaw(0x1)); + rcc::RegisterOperandFlags KillFlags; + KillFlags.set(rcc::RegisterOperandFlag::Kill); + rcc::MachineIRBuilder Builder(*Fixture->Function, Target); + auto Point = Builder.setInsertPoint(PhiBlock); + auto Pending = Point ? Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::PHI)) + : rcc::Expected<rcc::MachineInstrBuilder>{std::unexpected(Point.error())}; + auto Def = Pending ? Pending->addDef(*Destination) + : rcc::Expected<void>{std::unexpected(Pending.error())}; + auto Use = + Def ? Pending->addUse(*Source, KillFlags) : rcc::Expected<void>{std::unexpected(Def.error())}; + auto Incoming = + Use ? Pending->addBasicBlock(Predecessor) : rcc::Expected<void>{std::unexpected(Use.error())}; + auto Phi = Incoming ? Pending->build() + : rcc::Expected<rcc::MachineInstr *>{std::unexpected(Incoming.error())}; + if (!LiveIn || !Phi) { + Context.expect(false, "malformed-edge PHI setup must succeed"); + return; + } + + const std::vector<rcc::MachineBasicBlockLiveIn> OriginalLiveIns(PhiBlock.liveIns().begin(), + PhiBlock.liveIns().end()); + const rcc::RegisterOperandFlags OriginalFlags = (*Phi)->operandAt(1)->registerFlags(); + auto Rebuilt = rcc::rebuildMachineLiveness(*Fixture->Function, Target); + Context.expect(!Rebuilt && Rebuilt.error().code() == rcc::ErrorCode::VerificationError && + std::ranges::equal(PhiBlock.liveIns(), OriginalLiveIns) && + (*Phi)->operandAt(1)->registerFlags() == OriginalFlags, + "malformed PHI edges must fail without changing live-ins or operand flags"); +} + +class DummyAnalysis final { +public: + struct Result final {}; + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + return Result{}; + } +}; + +void testAnalysisRegistrationAndInvalidation(TestContext &Context, + const rcc::TargetDescription &Target) { + auto Fixture = createFunction("analysis-invalidation", 7, 1); + if (!Fixture) { + Context.expect(false, "analysis-invalidation fixture creation must succeed"); + return; + } + auto Reg = createVReg(*Fixture->Function); + auto Def = Reg ? buildDef(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg) + : rcc::Expected<rcc::MachineInstr *>{std::unexpected(Reg.error())}; + auto Use = Reg ? buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg) + : rcc::Expected<rcc::MachineInstr *>{std::unexpected(Reg.error())}; + if (!Reg || !Def || !Use) { + Context.expect(false, "analysis-invalidation instructions must be built"); + return; + } + + rcc::MachineFunctionAnalysisManager Analyses; + auto FirstRegistration = rcc::registerStage11MachineAnalyses(Analyses); + auto SecondRegistration = rcc::registerStage11MachineAnalyses(Analyses); + auto Intervals = FirstRegistration + ? Analyses.getResult<rcc::LiveIntervalsAnalysis>(*Fixture->Function, Target) + : rcc::Expected<rcc::LiveIntervalsAnalysis::Result *>{ + std::unexpected(FirstRegistration.error())}; + Context.expect(FirstRegistration && SecondRegistration && Intervals && + Analyses.cachedResultCount(*Fixture->Function) == 3, + "Stage 11 analyses must register idempotently and materialize dependencies"); + Analyses.invalidate(*Fixture->Function, rcc::PreservedAnalyses::none(), + {rcc::MachinePassChange::Frame}); + Context.expect(Analyses.cachedResultCount(*Fixture->Function) == 3, + "debug-only changes must preserve all liveness analyses"); + Analyses.invalidate(*Fixture->Function, rcc::PreservedAnalyses::none(), + {rcc::MachinePassChange::Operands}); + Context.expect( + Analyses.cachedResultCount(*Fixture->Function) == 1 && + Analyses.hasCached(*Fixture->Function, + rcc::MachineFunctionAnalysisManager::id<rcc::SlotIndexesAnalysis>()), + "operand changes must retain SlotIndexes while invalidating liveness dependents"); + auto Recomputed = Analyses.getResult<rcc::LiveIntervalsAnalysis>(*Fixture->Function, Target); + Context.expect(Recomputed && Analyses.cachedResultCount(*Fixture->Function) == 3, + "invalidated liveness dependencies must recompute on demand"); + Analyses.invalidate(*Fixture->Function, rcc::PreservedAnalyses::none(), + {rcc::MachinePassChange::BlockLayout}); + Context.expect( + Analyses.cachedResultCount(*Fixture->Function) == 1 && + Analyses.hasCached(*Fixture->Function, + rcc::MachineFunctionAnalysisManager::id<rcc::LiveVariablesAnalysis>()), + "layout changes must invalidate SlotIndexes and LiveIntervals only"); + + rcc::MachineFunctionAnalysisManager Atomic; + rcc::MachineAnalysisRegistration DummyRegistration; + DummyRegistration.Name = "SlotIndexes"; + auto Dummy = Atomic.registerAnalysis<DummyAnalysis>(std::move(DummyRegistration)); + auto Rejected = rcc::registerStage11MachineAnalyses(Atomic); + Context.expect( + Dummy && !Rejected && + Atomic.isRegistered(rcc::MachineFunctionAnalysisManager::id<DummyAnalysis>()) && + !Atomic.isRegistered( + rcc::MachineFunctionAnalysisManager::id<rcc::SlotIndexesAnalysis>()) && + !Atomic.isRegistered( + rcc::MachineFunctionAnalysisManager::id<rcc::LiveVariablesAnalysis>()), + "failed standard-analysis registration must leave the manager unchanged"); +} + +void testRISCVReservedRegisterLiveness(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFunction("riscv-reserved-liveness", 10, 1); + if (!Target || !Fixture) { + Context.expect(false, "RV64GC reserved-register liveness fixture creation must succeed"); + return; + } + auto X0 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X0); + auto X5 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X5); + if (!X0 || !X5) { + Context.expect(false, "RV64GC reserved-register fixture registers must resolve"); + return; + } + + rcc::MachineIRBuilder Builder(*Fixture->Function, *Target); + auto Point = Builder.setInsertPoint(*Fixture->Blocks[0]); + auto Pending = Point ? Builder.buildInstr(rcc::Opcode::fromValue( + static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::ADDI))) + : rcc::Expected<rcc::MachineInstrBuilder>{std::unexpected(Point.error())}; + auto Def = Pending ? Pending->addDef(rcc::Register::fromPhysical(*X5)) + : rcc::Expected<void>{std::unexpected(Pending.error())}; + auto Use = Def ? Pending->addUse(rcc::Register::fromPhysical(*X0)) + : rcc::Expected<void>{std::unexpected(Def.error())}; + auto Immediate = + Use ? Pending->addImmediate(1) : rcc::Expected<void>{std::unexpected(Use.error())}; + auto Instruction = Immediate + ? Pending->build() + : rcc::Expected<rcc::MachineInstr *>{std::unexpected(Immediate.error())}; + auto DeadDef = + Instruction ? Builder.buildInstr(rcc::Opcode::fromValue( + static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::ADDI))) + : rcc::Expected<rcc::MachineInstrBuilder>{std::unexpected(Instruction.error())}; + auto DeadDefDestination = DeadDef ? DeadDef->addDef(rcc::Register::fromPhysical(*X0)) + : rcc::Expected<void>{std::unexpected(DeadDef.error())}; + auto DeadDefSource = DeadDefDestination + ? DeadDef->addUse(rcc::Register::fromPhysical(*X0)) + : rcc::Expected<void>{std::unexpected(DeadDefDestination.error())}; + auto DeadDefImmediate = DeadDefSource + ? DeadDef->addImmediate(0) + : rcc::Expected<void>{std::unexpected(DeadDefSource.error())}; + auto DeadDefInstruction = + DeadDefImmediate + ? DeadDef->build() + : rcc::Expected<rcc::MachineInstr *>{std::unexpected(DeadDefImmediate.error())}; + if (!Instruction || !DeadDefInstruction) { + Context.expect(false, "RV64GC reserved-register liveness instruction must be built"); + return; + } + + auto Rebuilt = rcc::rebuildMachineLiveness(*Fixture->Function, *Target); + const rcc::MachineOperand *ZeroUse = (*Instruction)->operandAt(1); + const rcc::MachineOperand *ZeroDef = (*DeadDefInstruction)->operandAt(0); + const rcc::MachineOperand *DeadDefZeroUse = (*DeadDefInstruction)->operandAt(1); + Context.expect(Rebuilt && Fixture->Blocks[0]->liveIns().empty() && ZeroUse != nullptr && + !ZeroUse->registerFlags().has(rcc::RegisterOperandFlag::Kill) && + ZeroDef != nullptr && + ZeroDef->registerFlags().has(rcc::RegisterOperandFlag::Dead) && + DeadDefZeroUse != nullptr && + !DeadDefZeroUse->registerFlags().has(rcc::RegisterOperandFlag::Kill), + "reserved uses must remain available while dead defs retain precise liveness"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "liveness TestTarget must be created"); + if (!Target) + return Context.result(); + + testLivenessRebuildAndVerifier(Context, *Target); + testTrackedLiveThroughPhysicalRegister(Context, *Target); + testFailedRebuildIsAtomic(Context, *Target); + testAnalysisRegistrationAndInvalidation(Context, *Target); + testRISCVReservedRegisterLiveness(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineAnalysesTest.cpp b/backend/unittests/CodeGen/MachineAnalysesTest.cpp new file mode 100644 index 0000000..53cd847 --- /dev/null +++ b/backend/unittests/CodeGen/MachineAnalysesTest.cpp @@ -0,0 +1,582 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineAnalyses.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/MachineCFGUpdater.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/CodeGen/MachineVerifier.h" + +#include <algorithm> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + std::vector<rcc::InstructionDescriptor> Descriptors; + std::vector<rcc::ImmediatePredicate> Predicates; + auto Appended = rcc::appendGenericInstructionInfo(Descriptors, Predicates); + if (!Appended) + return std::unexpected(Appended.error()); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors), std::move(Predicates)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] rcc::Expected<rcc::MachineBasicBlock *> appendBlock(rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + if (!Block) + return std::unexpected(Block.error()); + auto Appended = Function.appendBlock(**Block); + if (!Appended) + return std::unexpected(Appended.error()); + return *Block; +} + +void testCFGAnalyses(TestContext &Context, const rcc::TargetDescription &Target) { + auto Function = rcc::MachineFunction::create("cfg", rcc::MachineFunctionStableID::fromValue(1)); + Context.expect(Function.has_value(), "CFG analysis function creation must succeed"); + if (!Function) + return; + + auto Entry = appendBlock(**Function); + auto Header = appendBlock(**Function); + auto Body = appendBlock(**Function); + auto Exit = appendBlock(**Function); + auto Dead = appendBlock(**Function); + Context.expect(Entry && Header && Body && Exit && Dead, + "CFG analysis blocks must be appended successfully"); + if (!Entry || !Header || !Body || !Exit || !Dead) + return; + + auto ThreeQuarters = rcc::BranchProbability::fromFraction(3, 4); + auto OneQuarter = rcc::BranchProbability::fromFraction(1, 4); + auto Updater = (*Function)->cfgUpdater(); + Context.expect(ThreeQuarters && OneQuarter && + Updater.addSuccessor(**Entry, **Header, rcc::BranchProbability::one()) && + Updater.addSuccessor(**Header, **Body, *ThreeQuarters) && + Updater.addSuccessor(**Header, **Exit, *OneQuarter) && + Updater.addSuccessor(**Body, **Header, rcc::BranchProbability::one()), + "reducible loop CFG construction must succeed"); + + rcc::MachineFunctionAnalysisManager Analyses; + Context.expect(rcc::registerStage8MachineAnalyses(Analyses).has_value(), + "standard Stage 8 analyses must register atomically"); + auto Dominators = Analyses.getResult<rcc::MachineDominatorTreeAnalysis>(**Function, Target); + auto Loops = Analyses.getResult<rcc::MachineLoopAnalysis>(**Function, Target); + auto Probabilities = + Analyses.getResult<rcc::MachineBranchProbabilityAnalysis>(**Function, Target); + auto Frequencies = Analyses.getResult<rcc::MachineBlockFrequencyAnalysis>(**Function, Target); + Context.expect(Dominators && Loops && Probabilities && Frequencies, + "CFG analyses and their dependencies must materialize"); + if (!Dominators || !Loops || !Probabilities || !Frequencies) + return; + + Context.expect((*Dominators)->dominates(**Entry, **Exit) && + (*Dominators)->dominates(**Header, **Body) && + (*Dominators)->immediateDominator(**Body) == *Header && + (*Dominators)->nearestCommonDominator(**Body, **Exit) == *Header && + !(*Dominators)->isReachable(**Dead), + "dominator analysis must model reachability, IDom, and common dominators"); + Context.expect((*Loops)->loops().size() == 1 && (*Loops)->loopFor(**Header) != nullptr && + (*Loops)->loopFor(**Body) != nullptr && (*Loops)->loopDepth(**Body) == 1, + "loop analysis must recover the natural loop and loop depth"); + Context.expect((*Probabilities)->probability(**Header, **Body) == *ThreeQuarters && + (*Probabilities)->probability(**Header, **Exit) == *OneQuarter, + "branch-probability analysis must retain exact integer edge probabilities"); + Context.expect((*Frequencies)->frequency(**Entry) != 0 && + (*Frequencies)->frequency(**Header) > (*Frequencies)->frequency(**Entry) && + (*Frequencies)->frequency(**Dead) == 0, + "block-frequency analysis must distinguish loop and unreachable blocks"); + + auto Alias = Analyses.getResult<rcc::MachineAliasAnalysis>(**Function, Target); + auto MemoryDependence = + Analyses.getResult<rcc::MachineMemoryDependenceAnalysis>(**Function, Target); + Context.expect(Alias && MemoryDependence && Analyses.cachedResultCount(**Function) == 6, + "the complete Stage 8 analysis set must materialize into one function cache"); + Analyses.invalidate(**Function, rcc::PreservedAnalyses::none(), + {rcc::MachinePassChange::Registers}); + Context.expect(Analyses.cachedResultCount(**Function) == 6, + "register-only changes must preserve CFG and memory analyses"); + Analyses.invalidate(**Function, rcc::PreservedAnalyses::none(), {rcc::MachinePassChange::CFG}); + Context.expect( + Analyses.cachedResultCount(**Function) == 1 && + Analyses.hasCached(**Function, + rcc::MachineFunctionAnalysisManager::id<rcc::MachineAliasAnalysis>()), + "CFG changes must invalidate CFG dependents and memory dependence but preserve aliasing"); + + auto RefreshedDominators = + Analyses.getResult<rcc::MachineDominatorTreeAnalysis>(**Function, Target); + Context.expect( + RefreshedDominators && + Updater.addSuccessor(**Entry, **Exit, rcc::BranchProbability::zero()).has_value() && + (*RefreshedDominators)->updateAfterCFGChange(**Function).has_value() && + (*RefreshedDominators)->immediateDominator(**Exit) == *Entry, + "dominator result must support exact updates after CFG edge changes"); +} + +void testIrreducibleCFG(TestContext &Context, const rcc::TargetDescription &Target) { + auto Function = + rcc::MachineFunction::create("irreducible", rcc::MachineFunctionStableID::fromValue(2)); + if (!Function) { + Context.expect(false, "irreducible CFG function creation must succeed"); + return; + } + auto Entry = appendBlock(**Function); + auto Left = appendBlock(**Function); + auto Right = appendBlock(**Function); + auto Exit = appendBlock(**Function); + if (!Entry || !Left || !Right || !Exit) { + Context.expect(false, "irreducible CFG blocks must be appended"); + return; + } + auto Half = rcc::BranchProbability::fromFraction(1, 2); + auto Updater = (*Function)->cfgUpdater(); + Context.expect(Half && Updater.addSuccessor(**Entry, **Left, *Half) && + Updater.addSuccessor(**Entry, **Right, *Half) && + Updater.addSuccessor(**Left, **Right, *Half) && + Updater.addSuccessor(**Left, **Exit, *Half) && + Updater.addSuccessor(**Right, **Left, *Half) && + Updater.addSuccessor(**Right, **Exit, *Half), + "irreducible CFG construction must succeed"); + + rcc::MachineFunctionAnalysisManager Analyses; + auto Registered = rcc::registerStage8MachineAnalyses(Analyses); + auto Loops = + Registered + ? Analyses.getResult<rcc::MachineLoopAnalysis>(**Function, Target) + : rcc::Expected<rcc::MachineLoopAnalysis::Result *>{std::unexpected(Registered.error())}; + Context.expect(Loops && (*Loops)->irreducibleRegions().size() == 1 && + (*Loops)->irreducibleRegions().front().Entries.size() == 2 && + (*Loops)->isIrreducible(**Left) && (*Loops)->isIrreducible(**Right), + "loop analysis must explicitly identify multi-entry irreducible regions"); +} + +[[nodiscard]] rcc::MachineMemOperandDesc +memoryDescription(rcc::MemoryBase Base, std::int64_t Offset, rcc::MemoryAccessFlag Access) { + rcc::MachineMemOperandDesc Description; + Description.Accesses = {Access}; + Description.Size = *rcc::MemorySize::fixed(8); + Description.Align = *rcc::Alignment::create(8); + Description.Base = std::move(Base); + Description.Offset = Offset; + return Description; +} + +void testAliasAnalysis(TestContext &Context) { + auto First = rcc::MachineMemOperand::create(memoryDescription( + rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)), 0, rcc::MemoryAccessFlag::Load)); + auto Same = rcc::MachineMemOperand::create(memoryDescription( + rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)), 0, rcc::MemoryAccessFlag::Store)); + auto Partial = rcc::MachineMemOperand::create(memoryDescription( + rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)), 4, rcc::MemoryAccessFlag::Store)); + auto DisjointRange = rcc::MachineMemOperand::create( + memoryDescription(rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)), 16, + rcc::MemoryAccessFlag::Store)); + auto DistinctObject = rcc::MachineMemOperand::create(memoryDescription( + rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(1)), 0, rcc::MemoryAccessFlag::Store)); + auto DifferentObjectKind = rcc::MachineMemOperand::create( + memoryDescription(rcc::MemoryBase::constantPool(rcc::ConstantPoolIndex::fromValue(0)), 0, + rcc::MemoryAccessFlag::Store)); + Context.expect(First && Same && Partial && DisjointRange && DistinctObject && DifferentObjectKind, + "alias-analysis memory operands must be valid"); + if (!First || !Same || !Partial || !DisjointRange || !DistinctObject || !DifferentObjectKind) + return; + + rcc::MachineAliasAnalysis::Result Alias; + Context.expect(Alias.alias(*First, *Same) == rcc::MachineAliasResult::MustAlias && + Alias.alias(*First, *Partial) == rcc::MachineAliasResult::PartialAlias && + Alias.alias(*First, *DisjointRange) == rcc::MachineAliasResult::NoAlias && + Alias.alias(*First, *DistinctObject) == rcc::MachineAliasResult::NoAlias && + Alias.alias(*First, *DifferentObjectKind) == rcc::MachineAliasResult::NoAlias, + "alias analysis must distinguish exact, partial, range, and object aliasing"); +} + +void testMemoryDependence(TestContext &Context, const rcc::TargetDescription &Target) { + auto Function = + rcc::MachineFunction::create("memory", rcc::MachineFunctionStableID::fromValue(3)); + if (!Function) { + Context.expect(false, "memory-dependence function creation must succeed"); + return; + } + auto Block = appendBlock(**Function); + auto OtherBlock = appendBlock(**Function); + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Pointer = rcc::LowLevelType::makePointer(0, 64); + auto Value = I64 ? (*Function)->registerInfo().createVirtualRegister(*I64) + : rcc::Expected<rcc::Register>{std::unexpected(I64.error())}; + auto Address = Pointer ? (*Function)->registerInfo().createVirtualRegister(*Pointer) + : rcc::Expected<rcc::Register>{std::unexpected(Pointer.error())}; + if (!Block || !OtherBlock || !Value || !Address) { + Context.expect(false, "memory-dependence MachineIR setup must succeed"); + return; + } + + auto StoreMemory = (*Function)->createMemOperand( + memoryDescription(rcc::MemoryBase::stack(), 0, rcc::MemoryAccessFlag::Store)); + auto AliasingLoadMemory = (*Function)->createMemOperand( + memoryDescription(rcc::MemoryBase::stack(), 0, rcc::MemoryAccessFlag::Load)); + auto DisjointLoadMemory = (*Function)->createMemOperand( + memoryDescription(rcc::MemoryBase::stack(), 32, rcc::MemoryAccessFlag::Load)); + auto VolatileStoreDescription = + memoryDescription(rcc::MemoryBase::stack(), 64, rcc::MemoryAccessFlag::Store); + VolatileStoreDescription.Accesses.set(rcc::MemoryAccessFlag::Volatile); + auto VolatileLoadDescription = + memoryDescription(rcc::MemoryBase::stack(), 96, rcc::MemoryAccessFlag::Load); + VolatileLoadDescription.Accesses.set(rcc::MemoryAccessFlag::Volatile); + auto VolatileStoreMemory = (*Function)->createMemOperand(std::move(VolatileStoreDescription)); + auto VolatileLoadMemory = (*Function)->createMemOperand(std::move(VolatileLoadDescription)); + auto OtherBlockLoadMemory = (*Function)->createMemOperand( + memoryDescription(rcc::MemoryBase::stack(), 0, rcc::MemoryAccessFlag::Load)); + rcc::MachineIRBuilder Builder(**Function, Target); + Context.expect(StoreMemory && AliasingLoadMemory && DisjointLoadMemory && VolatileStoreMemory && + VolatileLoadMemory && OtherBlockLoadMemory && Builder.setInsertPoint(**Block), + "memory-dependence builder setup must succeed"); + if (!StoreMemory || !AliasingLoadMemory || !DisjointLoadMemory || !VolatileStoreMemory || + !VolatileLoadMemory || !OtherBlockLoadMemory) + return; + + auto Store = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_STORE)); + if (!Store || !Store->addUse(*Value) || !Store->addUse(*Address) || + !Store->addMemOperand(**StoreMemory)) { + Context.expect(false, "generic store construction must succeed"); + return; + } + auto StoreInstruction = Store->build(); + auto Load = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_LOAD)); + if (!Load || !Load->addDef(*Value) || !Load->addUse(*Address) || + !Load->addMemOperand(**AliasingLoadMemory)) { + Context.expect(false, "aliasing generic load construction must succeed"); + return; + } + auto LoadInstruction = Load->build(); + auto DisjointLoad = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_LOAD)); + if (!DisjointLoad || !DisjointLoad->addDef(*Value) || !DisjointLoad->addUse(*Address) || + !DisjointLoad->addMemOperand(**DisjointLoadMemory)) { + Context.expect(false, "disjoint generic load construction must succeed"); + return; + } + auto DisjointLoadInstruction = DisjointLoad->build(); + auto VolatileStore = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_STORE)); + if (!VolatileStore || !VolatileStore->addUse(*Value) || !VolatileStore->addUse(*Address) || + !VolatileStore->addMemOperand(**VolatileStoreMemory)) { + Context.expect(false, "volatile store construction must succeed"); + return; + } + auto VolatileStoreInstruction = VolatileStore->build(); + auto VolatileLoad = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_LOAD)); + if (!VolatileLoad || !VolatileLoad->addDef(*Value) || !VolatileLoad->addUse(*Address) || + !VolatileLoad->addMemOperand(**VolatileLoadMemory)) { + Context.expect(false, "volatile load construction must succeed"); + return; + } + auto VolatileLoadInstruction = VolatileLoad->build(); + if (!Builder.setInsertPoint(**OtherBlock)) { + Context.expect(false, "second memory region insertion point must be valid"); + return; + } + auto OtherBlockLoad = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_LOAD)); + if (!OtherBlockLoad || !OtherBlockLoad->addDef(*Value) || !OtherBlockLoad->addUse(*Address) || + !OtherBlockLoad->addMemOperand(**OtherBlockLoadMemory)) { + Context.expect(false, "second-region generic load construction must succeed"); + return; + } + auto OtherBlockLoadInstruction = OtherBlockLoad->build(); + Context.expect(StoreInstruction && LoadInstruction && DisjointLoadInstruction && + VolatileStoreInstruction && VolatileLoadInstruction && + OtherBlockLoadInstruction, + "memory instructions must be published transactionally"); + if (!StoreInstruction || !LoadInstruction || !DisjointLoadInstruction || + !VolatileStoreInstruction || !VolatileLoadInstruction || !OtherBlockLoadInstruction) + return; + + rcc::MachineFunctionAnalysisManager Analyses; + auto Registered = rcc::registerStage8MachineAnalyses(Analyses); + auto Dependencies = + Registered ? Analyses.getResult<rcc::MachineMemoryDependenceAnalysis>(**Function, Target) + : rcc::Expected<rcc::MachineMemoryDependenceAnalysis::Result *>{ + std::unexpected(Registered.error())}; + Context.expect(Dependencies && (*Dependencies)->dependencies(**LoadInstruction).size() == 1 && + (*Dependencies)->dependencies(**LoadInstruction).front().Source == + (*StoreInstruction)->stableID() && + (*Dependencies)->dependencies(**DisjointLoadInstruction).empty(), + "memory dependence must add flow edges only for potentially aliasing accesses"); + Context.expect( + Dependencies && (*Dependencies)->regions().size() == 2 && + (*Dependencies)->regionFor(**Block) != nullptr && + (*Dependencies)->regionFor(**Block)->Instructions.size() == 5 && + (*Dependencies)->regionFor(**OtherBlock) != nullptr && + (*Dependencies)->regionFor(**OtherBlock)->Instructions.size() == 1 && + (*Dependencies)->dependencies(**OtherBlockLoadInstruction).empty() && + (*Dependencies)->dependencies(**OtherBlock).empty(), + "memory dependence must expose deterministic block regions without cross-CFG edges"); + bool HasOrderedVolatileDependency = false; + if (Dependencies) { + HasOrderedVolatileDependency = + std::ranges::any_of((*Dependencies)->dependencies(**VolatileLoadInstruction), + [&](const rcc::MachineMemoryDependency &Dependency) { + return Dependency.Source == (*VolatileStoreInstruction)->stableID(); + }); + } + Context.expect(HasOrderedVolatileDependency, + "ordered memory accesses must retain dependencies across disjoint addresses"); +} + +class ConflictingAnalysis final { +public: + struct Result final {}; + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + return Result{}; + } +}; + +void testAtomicStandardRegistration(TestContext &Context) { + rcc::MachineFunctionAnalysisManager Analyses; + Context.expect(Analyses + .registerAnalysis<ConflictingAnalysis>({ + .Name = "MachineLoopInfo", + .Dependencies = {}, + }) + .has_value(), + "analysis-name conflict setup must succeed"); + Context.expect( + !rcc::registerStage8MachineAnalyses(Analyses) && + !Analyses.isRegistered( + rcc::MachineFunctionAnalysisManager::id<rcc::MachineDominatorTreeAnalysis>()), + "failed standard-analysis registration must not publish a partial set"); +} + +void testCFGUpdaterPHIs(TestContext &Context, const rcc::TargetDescription &Target) { + auto Function = + rcc::MachineFunction::create("cfg_phi", rcc::MachineFunctionStableID::fromValue(4)); + if (!Function) { + Context.expect(false, "CFG PHI function creation must succeed"); + return; + } + auto Source = appendBlock(**Function); + auto Other = appendBlock(**Function); + auto Merge = appendBlock(**Function); + auto Unconnected = appendBlock(**Function); + if (!Source || !Other || !Merge || !Unconnected) { + Context.expect(false, "CFG PHI blocks must be appended"); + return; + } + auto Updater = (*Function)->cfgUpdater(); + if (!Updater.addSuccessor(**Source, **Merge, rcc::BranchProbability::one()) || + !Updater.addSuccessor(**Other, **Merge, rcc::BranchProbability::one())) { + Context.expect(false, "CFG PHI predecessor edges must be created"); + return; + } + + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Destination = I64 ? (*Function)->registerInfo().createVirtualRegister(*I64) + : rcc::Expected<rcc::Register>{std::unexpected(I64.error())}; + auto SourceValue = I64 ? (*Function)->registerInfo().createVirtualRegister(*I64) + : rcc::Expected<rcc::Register>{std::unexpected(I64.error())}; + auto OtherValue = I64 ? (*Function)->registerInfo().createVirtualRegister(*I64) + : rcc::Expected<rcc::Register>{std::unexpected(I64.error())}; + if (!Destination || !SourceValue || !OtherValue) { + Context.expect(false, "CFG PHI registers must be created"); + return; + } + + rcc::MachineIRBuilder Builder(**Function, Target); + if (!Builder.setInsertPoint(**Source)) + return; + auto SourceDefinition = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_IMPLICIT_DEF)); + if (!SourceDefinition || !SourceDefinition->addDef(*SourceValue) || !SourceDefinition->build()) { + Context.expect(false, "CFG PHI source definition must build"); + return; + } + auto Branch = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_BR)); + if (!Branch || !Branch->addBasicBlock(**Merge)) { + Context.expect(false, "CFG PHI branch must be assembled"); + return; + } + auto BranchInstruction = Branch->build(); + if (!BranchInstruction || !Builder.setInsertPoint(**Merge)) { + Context.expect(false, "CFG PHI branch must publish"); + return; + } + auto Phi = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_PHI)); + if (!Phi || !Phi->addDef(*Destination) || !Phi->addUse(*SourceValue) || + !Phi->addBasicBlock(**Source) || !Phi->addUse(*OtherValue) || !Phi->addBasicBlock(**Other)) { + Context.expect(false, "CFG PHI instruction must be assembled"); + return; + } + auto PhiInstruction = Phi->build(); + Context.expect(PhiInstruction && rcc::MachineVerifier::verify(**Function, Target), + "initial CFG PHI function must verify"); + if (!PhiInstruction) + return; + + auto Split = Updater.splitBlock(**Source, **BranchInstruction); + Context.expect( + Split && (*Split)->predecessors().size() == 1 && + (*Split)->predecessors().front() == *Source && (*Split)->successors().size() == 1 && + (*PhiInstruction)->operandAt(2)->getIf<rcc::BasicBlockOperand>()->Block == *Split && + rcc::MachineVerifier::verify(**Function, Target), + "block split must redirect CFG edges and PHI predecessor identity"); + if (!Split) + return; + + Context.expect(Updater.removeSuccessor(**Other, **Merge) && + (*PhiInstruction)->operandCount() == 3 && + rcc::MachineVerifier::verify(**Function, Target), + "edge deletion must remove the corresponding PHI incoming pair"); + Context.expect(!Updater.addSuccessor(**Unconnected, **Merge, rcc::BranchProbability::one()), + "new PHI predecessors without incoming values must be rejected"); + Context.expect(Updater.deleteBlock(**Other) && !(*Other)->isLinked() && + rcc::MachineVerifier::verify(**Function, Target), + "CFG block deletion must detach a disconnected block cleanly"); +} + +void testCFGUpdaterFailureAtomicity(TestContext &Context, const rcc::TargetDescription &Target) { + auto SplitFunction = + rcc::MachineFunction::create("cfg_split_atomic", rcc::MachineFunctionStableID::fromValue(5)); + if (!SplitFunction) { + Context.expect(false, "CFG split atomicity function creation must succeed"); + return; + } + auto Source = appendBlock(**SplitFunction); + auto Merge = appendBlock(**SplitFunction); + if (!Source || !Merge) + return; + auto SplitUpdater = (*SplitFunction)->cfgUpdater(); + if (!SplitUpdater.addSuccessor(**Source, **Merge, rcc::BranchProbability::one())) + return; + + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Destination = I64 ? (*SplitFunction)->registerInfo().createVirtualRegister(*I64) + : rcc::Expected<rcc::Register>{std::unexpected(I64.error())}; + auto Value = I64 ? (*SplitFunction)->registerInfo().createVirtualRegister(*I64) + : rcc::Expected<rcc::Register>{std::unexpected(I64.error())}; + if (!Destination || !Value) + return; + + rcc::MachineIRBuilder Builder(**SplitFunction, Target); + if (!Builder.setInsertPoint(**Source)) + return; + auto Branch = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_BR)); + auto BranchInstruction = + Branch && Branch->addBasicBlock(**Merge) + ? Branch->build() + : rcc::Expected<rcc::MachineInstr *>{rcc::makeUnexpected( + rcc::ErrorCode::InternalError, "failed to build CFG split branch")}; + if (!BranchInstruction || !Builder.setInsertPoint(**Merge)) + return; + auto Phi = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_PHI)); + auto PhiInstruction = + Phi && Phi->addDef(*Destination) && Phi->addUse(*Value) && Phi->addBasicBlock(**Source) + ? Phi->build() + : rcc::Expected<rcc::MachineInstr *>{rcc::makeUnexpected( + rcc::ErrorCode::InternalError, "failed to build CFG split PHI")}; + if (!PhiInstruction || !(*PhiInstruction)->eraseOperand(2)) + return; + + const std::size_t LinkedBlocksBefore = (*SplitFunction)->blockCount(); + const std::size_t AllocatedBlocksBefore = (*SplitFunction)->allBlocks().size(); + auto FailedSplit = SplitUpdater.splitBlock(**Source, **BranchInstruction); + Context.expect( + !FailedSplit && (*SplitFunction)->blockCount() == LinkedBlocksBefore && + (*SplitFunction)->allBlocks().size() == AllocatedBlocksBefore && + (*BranchInstruction)->parent() == *Source && (*Source)->successors().size() == 1 && + (*Source)->successors().front().Block == *Merge && (*Merge)->predecessors().size() == 1 && + (*Merge)->predecessors().front() == *Source, + "failed block split must not mutate layout, instructions, or CFG edges"); + + auto FailedDelete = SplitUpdater.deleteBlock(**Source); + Context.expect(!FailedDelete && (*Source)->isLinked() && + (*SplitFunction)->blockCount() == LinkedBlocksBefore && + (*Source)->successors().size() == 1 && (*Merge)->predecessors().size() == 1, + "failed block deletion must not partially disconnect CFG edges"); + + auto TransferFunction = rcc::MachineFunction::create("cfg_transfer_atomic", + rcc::MachineFunctionStableID::fromValue(6)); + if (!TransferFunction) + return; + auto From = appendBlock(**TransferFunction); + auto To = appendBlock(**TransferFunction); + auto Successor = appendBlock(**TransferFunction); + if (!From || !To || !Successor) + return; + auto TransferUpdater = (*TransferFunction)->cfgUpdater(); + if (!TransferUpdater.addSuccessor(**From, **Successor, rcc::BranchProbability::one()) || + !TransferUpdater.addSuccessor(**To, **Successor, rcc::BranchProbability::one())) { + return; + } + auto TransferDestination = I64 ? (*TransferFunction)->registerInfo().createVirtualRegister(*I64) + : rcc::Expected<rcc::Register>{std::unexpected(I64.error())}; + auto TransferValue = I64 ? (*TransferFunction)->registerInfo().createVirtualRegister(*I64) + : rcc::Expected<rcc::Register>{std::unexpected(I64.error())}; + if (!TransferDestination || !TransferValue) + return; + rcc::MachineIRBuilder TransferBuilder(**TransferFunction, Target); + if (!TransferBuilder.setInsertPoint(**Successor)) + return; + auto TransferPhi = TransferBuilder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_PHI)); + auto TransferPhiInstruction = + TransferPhi && TransferPhi->addDef(*TransferDestination) && + TransferPhi->addUse(*TransferValue) && TransferPhi->addBasicBlock(**From) + ? TransferPhi->build() + : rcc::Expected<rcc::MachineInstr *>{rcc::makeUnexpected( + rcc::ErrorCode::InternalError, "failed to build CFG transfer PHI")}; + if (!TransferPhiInstruction) + return; + + auto FailedTransfer = TransferUpdater.transferSuccessors(**From, **To); + Context.expect( + !FailedTransfer && (*From)->successors().size() == 1 && (*To)->successors().size() == 1 && + (*Successor)->predecessors().size() == 2 && + (*TransferPhiInstruction)->operandCount() == 3 && + (*TransferPhiInstruction)->operandAt(2)->getIf<rcc::BasicBlockOperand>()->Block == *From, + "failed successor transfer must preserve both CFG edges and PHI operands"); +} + +} // 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); + testCFGUpdaterFailureAtomicity(Context, *Target); + testAtomicStandardRegistration(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineCompilationStatusTest.cpp b/backend/unittests/CodeGen/MachineCompilationStatusTest.cpp new file mode 100644 index 0000000..84cada0 --- /dev/null +++ b/backend/unittests/CodeGen/MachineCompilationStatusTest.cpp @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineCompilationStatus.h" + +#include <array> +#include <cstdint> +#include <iostream> +#include <string> +#include <string_view> +#include <utility> + +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::MachineCompilationFailure +makeFailure(std::string Message = "selection did not match") { + rcc::MachineCompilationFailure Failure; + Failure.Diagnostic.Severity = rcc::MachineCompilationDiagnosticSeverity::Error; + Failure.Diagnostic.Code = rcc::ErrorCode::Unsupported; + Failure.Diagnostic.Message = std::move(Message); + Failure.Diagnostic.Location = rcc::SourceLocation{"input.mir", 7, 3, 0}; + Failure.FailurePass = "instruction-select"; + Failure.FailureInstruction = rcc::MachineInstrStableID::fromValue(17); + Failure.FailureStage = rcc::PipelineStage::Selected; + Failure.FailureRule = "rv64-test-pattern"; + Failure.Target = "riscv64-unknown-linux-gnu"; + Failure.Subtarget = "rv64gc"; + return Failure; +} + +void testStateNames(TestContext &Context) { + constexpr std::array States{ + rcc::MachineCompilationState::Success, + rcc::MachineCompilationState::Unsupported, + rcc::MachineCompilationState::MIRParseFailed, + rcc::MachineCompilationState::LegalizationFailed, + rcc::MachineCompilationState::SelectionFailed, + rcc::MachineCompilationState::RegisterAllocationFailed, + rcc::MachineCompilationState::CallLoweringFailed, + rcc::MachineCompilationState::FrameLayoutFailed, + rcc::MachineCompilationState::FinalizationFailed, + rcc::MachineCompilationState::AssemblyLoweringFailed, + rcc::MachineCompilationState::InternalError, + }; + for (const rcc::MachineCompilationState State : States) { + Context.expect(rcc::isKnownMachineCompilationState(State) && + rcc::getMachineCompilationStateName(State) != "Unknown", + "every public compilation state must have a stable name"); + } + Context.expect(!rcc::isKnownMachineCompilationState(rcc::MachineCompilationState::Count) && + rcc::getMachineCompilationStateName(rcc::MachineCompilationState::Count) == + "Unknown", + "compilation state sentinel must not be accepted as a public state"); +} + +void testSuccessfulAndFailedStatus(TestContext &Context) { + rcc::MachineCompilationStatus Status; + Context.expect(Status.succeeded() && Status.state() == rcc::MachineCompilationState::Success && + Status.failure() == nullptr && Status.verify().has_value(), + "default compilation status must represent verified success"); + + Context.expect( + Status.fail(rcc::MachineCompilationState::SelectionFailed, makeFailure()).has_value(), + "valid structured failure must atomically replace success"); + const rcc::MachineCompilationFailure *Failure = Status.failure(); + Context.expect(!Status.succeeded() && + Status.state() == rcc::MachineCompilationState::SelectionFailed && + Failure != nullptr && + Failure->FailureInstruction == rcc::MachineInstrStableID::fromValue(17) && + Failure->FailureStage == rcc::PipelineStage::Selected && + Failure->Target == "riscv64-unknown-linux-gnu" && Status.verify().has_value(), + "failed compilation status must preserve all structured context"); + + Context.expect( + !Status.fail(rcc::MachineCompilationState::InternalError, makeFailure("second failure")), + "first compilation failure must remain sticky"); + Context.expect(Status.state() == rcc::MachineCompilationState::SelectionFailed && + Status.failure() != nullptr && + Status.failure()->Diagnostic.Message == "selection did not match", + "a rejected later failure must not mutate the original status"); +} + +void testInvalidFailures(TestContext &Context) { + rcc::MachineCompilationStatus SuccessState; + Context.expect(!SuccessState.fail(rcc::MachineCompilationState::Success, makeFailure()), + "failure API must reject the success state"); + + rcc::MachineCompilationStatus UnknownState; + Context.expect(!UnknownState.fail(rcc::MachineCompilationState::Count, makeFailure()), + "failure API must reject an unknown state"); + + rcc::MachineCompilationStatus EmptyDiagnostic; + Context.expect( + !EmptyDiagnostic.fail(rcc::MachineCompilationState::SelectionFailed, makeFailure({})), + "failure API must reject an empty diagnostic message"); + + auto InvalidLocationFailure = makeFailure(); + InvalidLocationFailure.Diagnostic.Location = rcc::SourceLocation{"", 0, 0, 0}; + rcc::MachineCompilationStatus InvalidLocation; + Context.expect(!InvalidLocation.fail(rcc::MachineCompilationState::SelectionFailed, + std::move(InvalidLocationFailure)), + "failure API must reject an invalid diagnostic location"); + + auto InvalidInstructionFailure = makeFailure(); + InvalidInstructionFailure.FailureInstruction = rcc::MachineInstrStableID{}; + rcc::MachineCompilationStatus InvalidInstruction; + Context.expect(!InvalidInstruction.fail(rcc::MachineCompilationState::SelectionFailed, + std::move(InvalidInstructionFailure)), + "failure API must reject an invalid instruction StableID"); + + auto InvalidStageFailure = makeFailure(); + InvalidStageFailure.FailureStage = static_cast<rcc::PipelineStage>(255); + rcc::MachineCompilationStatus InvalidStage; + Context.expect(!InvalidStage.fail(rcc::MachineCompilationState::SelectionFailed, + std::move(InvalidStageFailure)), + "failure API must reject an unknown pipeline stage"); + + auto EmptyPassFailure = makeFailure(); + EmptyPassFailure.FailurePass = std::string{}; + rcc::MachineCompilationStatus EmptyPass; + Context.expect( + !EmptyPass.fail(rcc::MachineCompilationState::SelectionFailed, std::move(EmptyPassFailure)), + "failure API must reject empty optional text fields"); + + auto InvalidSeverityFailure = makeFailure(); + InvalidSeverityFailure.Diagnostic.Severity = rcc::MachineCompilationDiagnosticSeverity::Count; + rcc::MachineCompilationStatus InvalidSeverity; + Context.expect(!InvalidSeverity.fail(rcc::MachineCompilationState::SelectionFailed, + std::move(InvalidSeverityFailure)), + "failure API must reject an unknown diagnostic severity"); + + auto InvalidCodeFailure = makeFailure(); + InvalidCodeFailure.Diagnostic.Code = static_cast<rcc::ErrorCode>(255); + rcc::MachineCompilationStatus InvalidCode; + Context.expect(!InvalidCode.fail(rcc::MachineCompilationState::SelectionFailed, + std::move(InvalidCodeFailure)), + "failure API must reject an unknown diagnostic code"); +} + +} // namespace + +int main() { + TestContext Context; + testStateNames(Context); + testSuccessfulAndFailedStatus(Context); + testInvalidFailures(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineConstantPoolTest.cpp b/backend/unittests/CodeGen/MachineConstantPoolTest.cpp new file mode 100644 index 0000000..39323d5 --- /dev/null +++ b/backend/unittests/CodeGen/MachineConstantPoolTest.cpp @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineConstantPool.h" + +#include <bit> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <optional> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +struct PoolTargetConstant final { + std::uint64_t Value = 0; + std::string Symbol; + + [[nodiscard]] static constexpr rcc::TargetConstantTypeID stableTypeID() noexcept { + return rcc::TargetConstantTypeID::fromValue(0x5253434350430001ULL); + } + + [[nodiscard]] static constexpr std::uint32_t schemaVersion() noexcept { + return 1; + } + + [[nodiscard]] rcc::Expected<void> verify() const { + if (Value == 0) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "pool target constant value must be nonzero"); + } + return {}; + } + + [[nodiscard]] rcc::Expected<PoolTargetConstant> clone() const { + return PoolTargetConstant{Value, Symbol}; + } + + [[nodiscard]] std::uint64_t stableHash() const noexcept { + std::uint64_t Hash = Value; + for (const char Character : Symbol) + Hash = (Hash * 16777619U) ^ static_cast<unsigned char>(Character); + return Hash; + } + + [[nodiscard]] std::string print() const { + return Symbol.empty() ? std::to_string(Value) : Symbol + "+" + std::to_string(Value); + } + + [[nodiscard]] std::vector<std::byte> bytes() const { + std::vector<std::byte> Bytes(8); + for (std::size_t Index = 0; Index < Bytes.size(); ++Index) + Bytes[Index] = static_cast<std::byte>((Value >> (Index * 8U)) & 0xffU); + return Bytes; + } + + [[nodiscard]] rcc::Expected<std::vector<std::byte>> encode() const { + std::vector<std::byte> Payload = bytes(); + for (const char Character : Symbol) + Payload.push_back(static_cast<std::byte>(static_cast<unsigned char>(Character))); + return Payload; + } + + [[nodiscard]] rcc::Expected<rcc::TargetConstantEmission> emit() const { + rcc::TargetConstantEmission Emission; + Emission.Bytes = bytes(); + if (!Symbol.empty()) { + Emission.Fixups.push_back({0, rcc::TargetRelocationKindID::fromValue(3), Symbol, + std::bit_cast<std::int64_t>(Value)}); + } + return Emission; + } + + [[nodiscard]] rcc::Alignment requiredAlignment() const noexcept { + return *rcc::Alignment::create(8); + } + + [[nodiscard]] std::optional<rcc::TargetRelocationKindID> relocationKind() const noexcept { + if (Symbol.empty()) + return std::nullopt; + return rcc::TargetRelocationKindID::fromValue(3); + } + + [[nodiscard]] static rcc::Expected<PoolTargetConstant> parse(std::string_view Text) { + if (Text.empty()) + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, "pool constant text is empty"); + return PoolTargetConstant{1, std::string(Text)}; + } + + [[nodiscard]] static rcc::Expected<PoolTargetConstant> + decode(std::uint32_t Version, std::span<const std::byte> Payload) { + if (Version != schemaVersion() || Payload.size() < 8) { + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, + "pool target constant encoding is invalid"); + } + std::uint64_t Value = 0; + for (std::size_t Index = 0; Index < 8; ++Index) + Value |= std::to_integer<std::uint64_t>(Payload[Index]) << (Index * 8U); + std::string Symbol; + for (const std::byte Character : Payload.subspan(8)) + Symbol.push_back(static_cast<char>(std::to_integer<unsigned char>(Character))); + return PoolTargetConstant{Value, std::move(Symbol)}; + } + + friend bool operator==(const PoolTargetConstant &Left, const PoolTargetConstant &Right) = default; +}; + +[[nodiscard]] rcc::MachineConstantPoolEntryDesc +genericDescription(rcc::ConstantPoolMergeKind Merge) { + rcc::MachineConstantPoolEntryDesc Description; + auto Integer = rcc::APInt::fromUnsigned(32, 0x04030201U); + Description.Value = std::move(*rcc::GenericConstantData::integer(std::move(*Integer))); + Description.Size = 4; + Description.Align = *rcc::Alignment::create(4); + Description.Merge = Merge; + if (Merge == rcc::ConstantPoolMergeKind::Exact) + Description.Placement.Section = rcc::ConstantPoolSectionKind::MergeableReadOnly; + return Description; +} + +[[nodiscard]] rcc::MachineConstantPoolEntryDesc targetDescription(bool Expression) { + rcc::MachineConstantPoolEntryDesc Description; + auto Constant = rcc::TargetConstant::create( + PoolTargetConstant{Expression ? 17U : 9U, Expression ? "symbol" : ""}); + if (Expression) + Description.Value = rcc::TargetExpressionConstant{std::move(*Constant)}; + else + Description.Value = std::move(*Constant); + Description.Size = 8; + Description.Align = *rcc::Alignment::create(8); + Description.Relocation = + Expression ? rcc::ConstantPoolRelocationMode::Target : rcc::ConstantPoolRelocationMode::None; + return Description; +} + +[[nodiscard]] rcc::MachineConstantPoolEntryDesc symbolDifferenceDescription() { + rcc::MachineConstantPoolEntryDesc Description; + Description.Value = rcc::SymbolDifferenceConstant{"upper", "lower", 4}; + Description.Size = 4; + Description.Align = *rcc::Alignment::create(4); + Description.Relocation = rcc::ConstantPoolRelocationMode::PositionIndependent; + return Description; +} + +[[nodiscard]] rcc::MachineConstantPoolEntryDesc blockAddressDescription() { + rcc::MachineConstantPoolEntryDesc Description; + Description.Value = rcc::BlockAddressConstant{"function", "block", 0}; + Description.Size = 8; + Description.Align = *rcc::Alignment::create(8); + Description.Relocation = rcc::ConstantPoolRelocationMode::Static; + return Description; +} + +void testPoolLifecycle(TestContext &Context) { + rcc::MachineConstantPool Pool; + + auto MergedA = Pool.addEntry(genericDescription(rcc::ConstantPoolMergeKind::Exact)); + auto MergedB = Pool.addEntry(genericDescription(rcc::ConstantPoolMergeKind::Exact)); + auto Unmerged = Pool.addEntry(genericDescription(rcc::ConstantPoolMergeKind::Never)); + Context.expect(MergedA && MergedB && Unmerged && *MergedA == *MergedB && MergedA->value() == 0 && + Unmerged->value() == 1 && Pool.entries().size() == 2 && + Pool.liveEntryCount() == 2, + "exact constants must merge while nonmergeable constants retain identity"); + if (!MergedA || !Unmerged) + return; + + Context.expect( + !Pool.updateEntry(*Unmerged, genericDescription(rcc::ConstantPoolMergeKind::Exact)), + "pool update must reject duplicate mergeable entries"); + Context.expect(Pool.eraseEntry(*Unmerged).has_value() && Pool.entry(*Unmerged) == nullptr, + "constant pool erase must leave a tombstone"); + + auto Target = Pool.addEntry(targetDescription(false)); + auto Difference = Pool.addEntry(symbolDifferenceDescription()); + auto Address = Pool.addEntry(blockAddressDescription()); + auto Expression = Pool.addEntry(targetDescription(true)); + Context.expect(Target && Difference && Address && Expression && Target->value() == 2 && + Expression->value() == 5 && Pool.liveEntryCount() == 5, + "all constant pool value kinds must receive monotonic indices"); + if (!Target || !Expression) + return; + + const rcc::MachineConstantPoolEntry *TargetEntry = Pool.entry(*Target); + auto ClonedDescription = + TargetEntry == nullptr ? rcc::Expected<rcc::MachineConstantPoolEntryDesc>(rcc::makeUnexpected( + rcc::ErrorCode::InternalError, "missing target entry")) + : TargetEntry->cloneDescription(); + Context.expect(ClonedDescription && + TargetEntry->description()->semanticallyEquals(*ClonedDescription), + "target constant pool descriptions must deep-clone"); + + Context.expect(Pool.verify().has_value() && Pool.freeze().has_value() && Pool.isFrozen(), + "valid constant pool must freeze after verification"); + Context.expect(!Pool.addEntry(blockAddressDescription()) && !Pool.eraseEntry(*Target) && + !Pool.updateEntry(*Expression, targetDescription(true)) && !Pool.freeze(), + "frozen constant pool must reject every mutation"); +} + +void testInvalidEntries(TestContext &Context) { + rcc::MachineConstantPool Pool; + + auto SizeMismatch = genericDescription(rcc::ConstantPoolMergeKind::Never); + SizeMismatch.Size = 8; + Context.expect(!Pool.addEntry(std::move(SizeMismatch)), + "generic constant byte size must match its entry size"); + + auto RelocatingBytes = genericDescription(rcc::ConstantPoolMergeKind::Never); + RelocatingBytes.Relocation = rcc::ConstantPoolRelocationMode::Static; + Context.expect(!Pool.addEntry(std::move(RelocatingBytes)), + "raw generic bytes must reject implicit relocation semantics"); + + auto WeakTargetAlignment = targetDescription(false); + WeakTargetAlignment.Align = *rcc::Alignment::create(4); + Context.expect(!Pool.addEntry(std::move(WeakTargetAlignment)), + "target constant must enforce its required alignment"); + + auto MissingTargetRelocation = targetDescription(true); + MissingTargetRelocation.Relocation = rcc::ConstantPoolRelocationMode::None; + Context.expect(!Pool.addEntry(std::move(MissingTargetRelocation)), + "target expression must agree with its relocation contract"); + + auto InvalidPlacement = blockAddressDescription(); + InvalidPlacement.Placement.Section = rcc::ConstantPoolSectionKind::Target; + Context.expect(!Pool.addEntry(std::move(InvalidPlacement)), + "target placement must carry target flags"); + + auto ZeroDistance = blockAddressDescription(); + ZeroDistance.Placement.MaximumDistance = 0; + Context.expect(!Pool.addEntry(std::move(ZeroDistance)), + "constant pool maximum distance must be nonzero"); +} + +} // namespace + +int main() { + TestContext Context; + testPoolLifecycle(Context); + testInvalidEntries(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineDebugInfoTest.cpp b/backend/unittests/CodeGen/MachineDebugInfoTest.cpp new file mode 100644 index 0000000..ae4d539 --- /dev/null +++ b/backend/unittests/CodeGen/MachineDebugInfoTest.cpp @@ -0,0 +1,518 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineDebugInfo.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineFrameInfo.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineFunctionAttributes.h" +#include "rcc/CodeGen/MachineFunctionCloner.h" +#include "rcc/CodeGen/MachineFunctionRecords.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineVerifier.h" +#include "rcc/CodeGen/TargetDescription.h" + +#include <cstdint> +#include <iostream> +#include <limits> +#include <memory> +#include <optional> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t ValueOpcode = 1024; +constexpr std::uint32_t MetaOpcode = 1025; + +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::Expected<rcc::TargetDescription> createTarget() { + rcc::InstructionDescriptor Value; + Value.InstructionOpcode = rcc::Opcode::fromValue(ValueOpcode); + Value.Name = "TEST_VALUE"; + Value.Flags = {rcc::InstructionFlag::ProducesCode}; + rcc::OperandDescriptor OptionalMetadata; + OptionalMetadata.Kinds = {rcc::MachineOperand::Kind::Metadata}; + OptionalMetadata.Optional = true; + Value.Operands.push_back(OptionalMetadata); + + rcc::InstructionDescriptor Meta; + Meta.InstructionOpcode = rcc::Opcode::fromValue(MetaOpcode); + Meta.Name = "TEST_META"; + Meta.Flags = {rcc::InstructionFlag::Meta}; + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Value)); + Descriptors.push_back(std::move(Meta)); + std::vector<rcc::ImmediatePredicate> Predicates; + auto Generic = rcc::appendGenericInstructionInfo(Descriptors, Predicates); + if (!Generic) + return std::unexpected(Generic.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]] std::unique_ptr<rcc::MachineFunction> +createFunction(TestContext &Context, std::uint64_t StableID, std::string Name) { + auto Function = rcc::MachineFunction::create(std::move(Name), + rcc::MachineFunctionStableID::fromValue(StableID)); + Context.expect(Function.has_value(), "machine function creation must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineBasicBlock *appendBlock(TestContext &Context, + rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + Context.expect(Block.has_value() && Function.appendBlock(**Block).has_value(), + "machine basic block creation and linking must succeed"); + return Block ? *Block : nullptr; +} + +[[nodiscard]] rcc::MachineInstr *appendInstruction(TestContext &Context, + rcc::MachineFunction &Function, + rcc::MachineBasicBlock &Block, + std::uint32_t OpcodeValue) { + auto Instruction = Function.createInstruction(rcc::Opcode::fromValue(OpcodeValue)); + Context.expect(Instruction.has_value() && Block.appendInstruction(**Instruction).has_value(), + "machine instruction creation and linking must succeed"); + return Instruction ? *Instruction : nullptr; +} + +void testMetadataValuesAndLocations(TestContext &Context) { + rcc::MachineMetadataInfo Metadata; + Context.expect(Metadata.addNode({.Slot = 0, .IsDistinct = false, .Elements = {}}).has_value(), + "function-local debug metadata setup must succeed"); + + const rcc::MachineDIExpression Expression( + std::vector<std::uint64_t>{0x1005, 0, 0x22, 0x1000, 0, 64}); + Context.expect(Expression.elements().size() == 6 && Expression.elements()[0] == 0x1005, + "inline DIExpression elements must preserve their exact unsigned encoding"); + auto ExpressionValue = rcc::MachineMetadataValue::expression(Expression); + Context.expect(ExpressionValue.verify(Metadata).has_value() && + ExpressionValue.expression() != nullptr && + *ExpressionValue.expression() == Expression, + "inline DIExpression metadata values must remain structured"); + Context.expect(!rcc::MachineMetadataValue{}.verify(Metadata), + "an unrepresented metadata value must be rejected"); + + Context.expect(!rcc::MachineDILocation::create({}), + "an inline DILocation must contain a root frame"); + Context.expect(!rcc::MachineDILocation::create({{}}), + "an inline DILocation frame must contain a valid scope"); + Context.expect(!rcc::MachineDILocation::create({ + { + .Line = 1, + .Column = 0, + .Scope = rcc::MachineMetadataReference::embeddedIR(1), + .IsImplicitCode = false, + .AtomGroup = 0, + .AtomRank = 8, + }, + }), + "an inline DILocation atom rank must fit the fixed three-bit field"); + Context.expect(!rcc::MachineDILocation::create({ + { + .Line = 1, + .Column = 0, + .Scope = rcc::MachineMetadataReference::embeddedIR(1), + .IsImplicitCode = false, + .AtomGroup = rcc::MachineDILocation::MaxAtomGroup + 1, + .AtomRank = 0, + }, + }), + "an inline DILocation atom group must fit the fixed 61-bit field"); + + auto InlineLocation = rcc::MachineDILocation::create( + { + { + .Line = 7, + .Column = 65'535, + .Scope = rcc::MachineMetadataReference::embeddedIR(3), + .IsImplicitCode = true, + .AtomGroup = rcc::MachineDILocation::MaxAtomGroup, + .AtomRank = 7, + }, + { + .Line = 9, + .Column = 4, + .Scope = rcc::MachineMetadataReference::embeddedIR(0), + .IsImplicitCode = false, + .AtomGroup = 0, + .AtomRank = 0, + }, + }, + rcc::MachineMetadataReference::embeddedIR(4)); + Context.expect(InlineLocation.has_value() && InlineLocation->verify(Metadata).has_value() && + InlineLocation->inlineChain().size() == 2 && + InlineLocation->terminalInlinedAt() == + rcc::MachineMetadataReference::embeddedIR(4), + "inline DILocation chains must preserve nested and terminal references"); + if (!InlineLocation) + return; + + auto DebugLocation = rcc::MachineDebugLocation::inlineLocation(*InlineLocation); + Context.expect(DebugLocation.verify(Metadata).has_value() && + DebugLocation.inlineLocation() != nullptr, + "a structured inline DILocation must form a debug-location attachment"); + rcc::MachineVariableDebugInfo VariableInfo{ + .Variable = rcc::MachineMetadataReference::embeddedIR(5), + .Expression = ExpressionValue, + .Location = DebugLocation, + }; + Context.expect(VariableInfo.verify(Metadata).has_value() && + !rcc::MachineVariableDebugInfo{}.verify(Metadata), + "variable debug records must carry at least one verified metadata component"); + + auto Undefined = + rcc::MachineDebugLocation::reference(rcc::MachineMetadataReference::machineFunction(99)); + Context.expect(!Undefined, "typed debug locations must not accept function-local tuple metadata"); +} + +void testInstructionMetadataAndClone(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 20, "debug-metadata"); + if (!Target || Function == nullptr) + return; + Context.expect( + Function->metadata().addNode({.Slot = 0, .IsDistinct = false, .Elements = {}}).has_value(), + "instruction metadata graph setup must succeed"); + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + rcc::MachineInstr *Instruction = appendInstruction(Context, *Function, *Block, ValueOpcode); + if (Instruction == nullptr) + return; + + auto MetadataValue = + rcc::MachineMetadataValue::reference(rcc::MachineMetadataReference::machineFunction(0)); + Context.expect(MetadataValue.has_value(), "instruction metadata reference must be valid"); + if (!MetadataValue) + return; + auto Operand = rcc::MachineOperand::metadata(std::move(*MetadataValue)); + auto InlineLocation = rcc::MachineDILocation::create({ + { + .Line = 12, + .Column = 8, + .Scope = rcc::MachineMetadataReference::embeddedIR(7), + .IsImplicitCode = false, + .AtomGroup = 0, + .AtomRank = 0, + }, + }); + Context.expect(Operand.has_value() && InlineLocation.has_value(), + "instruction metadata values must be constructible"); + if (!Operand || !InlineLocation) + return; + const rcc::MachineDebugLocation DebugLocation = + rcc::MachineDebugLocation::inlineLocation(*InlineLocation); + Context.expect(Instruction->addOperand(std::move(*Operand)).has_value() && + Instruction->setDebugLocation(DebugLocation).has_value(), + "instruction metadata operand and debug location must attach independently"); + Instruction->setSourceLocation({"debug-input.mir", 3, 5, 0}); + const rcc::MachineVariableDebugInfo StackDebugInfo{ + .Variable = rcc::MachineMetadataReference::embeddedIR(8), + .Expression = rcc::MachineMetadataValue::expression( + rcc::MachineDIExpression(std::vector<std::uint64_t>{0x1000, 0, 64})), + .Location = DebugLocation, + }; + rcc::StackObjectDesc StackDescription; + StackDescription.Size = 8; + StackDescription.Align = *rcc::Alignment::create(8); + StackDescription.DebugInfo = StackDebugInfo; + auto StackIndex = Function->frameInfo().createObject(std::move(StackDescription)); + Context.expect(StackIndex.has_value(), + "structured variable debug information must attach to a stack object"); + if (!StackIndex) + return; + Context.expect(rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "well-formed instruction metadata must pass unified verification"); + + auto Clone = + rcc::cloneMachineFunction(*Function, *Target, + { + .NewStableID = rcc::MachineFunctionStableID::fromValue(21), + .NewName = std::string("debug-metadata.clone"), + }); + Context.expect(Clone.has_value(), "instruction debug metadata must support deep cloning"); + if (!Clone) + return; + const rcc::MachineInstr *ClonedInstruction = + Clone->ClonedFunction->firstBlock()->firstInstruction(); + const auto ClonedStackIndex = Clone->FrameIndices.find(*StackIndex); + const rcc::StackObject *ClonedStack = + ClonedStackIndex == Clone->FrameIndices.end() + ? nullptr + : Clone->ClonedFunction->frameInfo().object(ClonedStackIndex->second); + Context.expect( + ClonedInstruction->debugLocation() == DebugLocation && + ClonedInstruction->sourceLocation() == rcc::SourceLocation{"debug-input.mir", 3, 5, 0} && + ClonedInstruction->operandAt(0)->getIf<rcc::MetadataOperand>()->Value.reference() != + nullptr && + ClonedStack != nullptr && ClonedStack->description() != nullptr && + ClonedStack->description()->DebugInfo == StackDebugInfo, + "clone must preserve distinct parser source and semantic debug information"); + + auto InvalidFunction = createFunction(Context, 22, "invalid-debug-metadata"); + if (InvalidFunction == nullptr) + return; + rcc::MachineBasicBlock *InvalidBlock = appendBlock(Context, *InvalidFunction); + if (InvalidBlock == nullptr) + return; + rcc::MachineInstr *InvalidInstruction = + appendInstruction(Context, *InvalidFunction, *InvalidBlock, ValueOpcode); + if (InvalidInstruction == nullptr) + return; + auto InvalidValue = + rcc::MachineMetadataValue::reference(rcc::MachineMetadataReference::machineFunction(9)); + Context.expect(InvalidValue.has_value(), "undefined scoped references remain representable"); + if (!InvalidValue) + return; + auto InvalidOperand = rcc::MachineOperand::metadata(std::move(*InvalidValue)); + Context.expect(InvalidOperand.has_value() && + InvalidInstruction->addOperand(std::move(*InvalidOperand)).has_value() && + !rcc::MachineVerifier::verify(*InvalidFunction, *Target), + "undefined instruction metadata references must fail unified verification"); + + auto InvalidFrameFunction = createFunction(Context, 23, "invalid-frame-debug-metadata"); + if (InvalidFrameFunction == nullptr) + return; + rcc::StackObjectDesc InvalidStackDescription; + InvalidStackDescription.Size = 8; + InvalidStackDescription.Align = *rcc::Alignment::create(8); + InvalidStackDescription.DebugInfo = rcc::MachineVariableDebugInfo{ + .Variable = rcc::MachineMetadataReference::machineFunction(9), + .Expression = std::nullopt, + .Location = std::nullopt, + }; + Context.expect( + !InvalidFrameFunction->frameInfo().createObject(std::move(InvalidStackDescription)), + "typed stack debug information must reject function-local tuple metadata"); +} + +void testInstructionNumbering(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 1, "debug-numbering"); + if (!Target || Function == nullptr) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + rcc::MachineInstr *First = appendInstruction(Context, *Function, *Block, ValueOpcode); + rcc::MachineInstr *Second = appendInstruction(Context, *Function, *Block, ValueOpcode); + rcc::MachineInstr *Third = appendInstruction(Context, *Function, *Block, ValueOpcode); + if (First == nullptr || Second == nullptr || Third == nullptr) + return; + + Context.expect(!First->setDebugInstrNumber(0) && + !First->setDebugInstrNumber( + static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max()) + 1), + "debug instruction numbers must fit in nonzero uint32"); + Context.expect(First->setDebugInstrNumber(7).has_value(), + "an explicit debug instruction number must be accepted"); + auto SecondNumber = Second->assignDebugInstrNumber(); + auto ExistingSecondNumber = Second->assignDebugInstrNumber(); + Context.expect(SecondNumber.has_value() && *SecondNumber == 8 && + ExistingSecondNumber.has_value() && *ExistingSecondNumber == *SecondNumber, + "generated debug numbers must continue after the largest observed number"); + Second->clearDebugInstrNumber(); + auto ReassignedSecondNumber = Second->assignDebugInstrNumber(); + Context.expect(ReassignedSecondNumber.has_value() && *ReassignedSecondNumber == 9, + "cleared debug numbers must not rewind the monotonic allocator"); + Context.expect(Third->setDebugInstrNumber(7).has_value() && + !rcc::MachineVerifier::verify(*Function, *Target), + "the unified verifier must reject duplicate live debug numbers"); + Context.expect(!Third->setDebugInstrNumber(10), + "an existing debug identity must be cleared before replacement"); + Third->clearDebugInstrNumber(); + Context.expect(Third->setDebugInstrNumber(10).has_value() && + rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "unique debug numbers must pass unified verification"); +} + +void testSubstitutionsAndClone(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 2, "debug-substitutions"); + if (!Target || Function == nullptr) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + rcc::MachineInstr *Instruction = appendInstruction(Context, *Function, *Block, ValueOpcode); + if (Instruction == nullptr) + return; + Context.expect(Instruction->setDebugInstrNumber(7).has_value(), + "tracked instruction numbering must succeed"); + + const rcc::MachineDebugValueSubstitution Substitution{ + .Source = {.InstructionNumber = 1, .OperandNumber = 0}, + .Destination = {.InstructionNumber = 7, .OperandNumber = 1}, + .Subregister = rcc::test::TestSubReg::Low32, + }; + Context.expect(Function->records().addDebugValueSubstitution(Substitution).has_value(), + "a structured debug value substitution must be stored"); + Context.expect(!Function->records().addDebugValueSubstitution(Substitution), + "a debug value source must have at most one replacement"); + Context.expect(!Function->records().addDebugValueSubstitution({ + .Source = + { + .InstructionNumber = 2, + .OperandNumber = rcc::MachineDebugMemoryOperandNumber, + }, + .Destination = {.InstructionNumber = 7, .OperandNumber = 0}, + .Subregister = {}, + }), + "a substitution cannot originate from the reserved memory operand"); + Context.expect(!Function->records().addDebugValueSubstitution({ + .Source = {.InstructionNumber = 3, .OperandNumber = 0}, + .Destination = {.InstructionNumber = 3, .OperandNumber = 1}, + .Subregister = {}, + }), + "a substitution cannot replace a value within one instruction"); + + Context.expect(!rcc::MachineVerifier::verify(*Function, *Target), + "substitutions must require the DebugInstrRef function policy"); + Function->attributes().set(rcc::MachineFunctionAttribute::DebugInstrRef); + Context.expect(rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "well-formed substitutions must pass unified verification"); + + auto Clone = + rcc::cloneMachineFunction(*Function, *Target, + { + .NewStableID = rcc::MachineFunctionStableID::fromValue(3), + .NewName = std::string("debug-substitutions.clone"), + }); + Context.expect(Clone.has_value(), "tracked debug state must support explicit deep cloning"); + if (!Clone) + return; + const rcc::MachineBasicBlock *ClonedBlock = Clone->ClonedFunction->firstBlock(); + const rcc::MachineInstr *ClonedInstruction = + ClonedBlock == nullptr ? nullptr : ClonedBlock->firstInstruction(); + Context.expect(ClonedInstruction != nullptr && ClonedInstruction->debugInstrNumber() == 7 && + Clone->ClonedFunction->records().debugValueSubstitutions().size() == 1 && + Clone->ClonedFunction->records().debugValueSubstitutions()[0] == Substitution, + "deep clone must preserve debug numbers and substitution identities"); +} + +void testVerifierFailures(TestContext &Context) { + auto Target = createTarget(); + if (!Target) + return; + + { + auto Function = createFunction(Context, 9, "missing-debug-location"); + if (Function == nullptr) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + auto Instruction = Function->createInstruction(rcc::opcode(rcc::GenericOpcode::DBG_VALUE)); + if (!Instruction) + return; + for (const std::int64_t Value : {1, 2, 3, 4}) { + if (!(*Instruction)->addOperand(rcc::MachineOperand::immediate(Value))) + return; + } + Context.expect(Block->appendInstruction(**Instruction).has_value(), + "four-operand DBG_VALUE must be linkable for verifier coverage"); + auto Verification = rcc::MachineVerifier::verify(*Function, *Target); + Context.expect(!Verification && + Verification.error().message().find( + "Bad machine code: Missing DebugLoc for debug instruction\n" + "- instruction: DBG_VALUE 1, 2, 3, 4") != std::string_view::npos, + "complete DBG_VALUE must require a DebugLoc with LLVM-compatible diagnostics"); + } + + { + auto Function = createFunction(Context, 10, "debug-meta"); + if (Function == nullptr) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + rcc::MachineInstr *Meta = appendInstruction(Context, *Function, *Block, MetaOpcode); + if (Meta == nullptr) + return; + Context.expect(Meta->setDebugInstrNumber(1).has_value() && + !rcc::MachineVerifier::verify(*Function, *Target), + "metadata instructions must not carry debug value tracking numbers"); + } + + { + auto Function = createFunction(Context, 11, "debug-subreg"); + if (Function == nullptr) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + rcc::MachineInstr *Instruction = appendInstruction(Context, *Function, *Block, ValueOpcode); + if (Instruction == nullptr) + return; + Function->attributes().set(rcc::MachineFunctionAttribute::DebugInstrRef); + Context.expect(Function->records() + .addDebugValueSubstitution({ + .Source = {.InstructionNumber = 1, .OperandNumber = 0}, + .Destination = {.InstructionNumber = 2, .OperandNumber = 0}, + .Subregister = rcc::SubRegIndex::fromValue(999), + }) + .has_value() && + !rcc::MachineVerifier::verify(*Function, *Target), + "debug substitutions must reject target-unknown subregister indices"); + } + + { + rcc::MachineFunctionRecords Records; + Context.expect(Records + .addDebugValueSubstitution({ + .Source = {.InstructionNumber = 1, .OperandNumber = 0}, + .Destination = {.InstructionNumber = 2, .OperandNumber = 0}, + .Subregister = {}, + }) + .has_value() && + Records + .addDebugValueSubstitution({ + .Source = {.InstructionNumber = 2, .OperandNumber = 0}, + .Destination = {.InstructionNumber = 1, .OperandNumber = 0}, + .Subregister = {}, + }) + .has_value() && + !Records.verify(), + "debug substitution chains must be acyclic"); + } +} + +} // namespace + +int main() { + TestContext Context; + testMetadataValuesAndLocations(Context); + testInstructionMetadataAndClone(Context); + testInstructionNumbering(Context); + testSubstitutionsAndClone(Context); + testVerifierFailures(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineFrameInfoTest.cpp b/backend/unittests/CodeGen/MachineFrameInfoTest.cpp new file mode 100644 index 0000000..b0464a6 --- /dev/null +++ b/backend/unittests/CodeGen/MachineFrameInfoTest.cpp @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineFrameInfo.h" + +#include "rcc/CodeGen/MachineDebugInfo.h" + +#include <cstdint> +#include <iostream> +#include <optional> +#include <string_view> + +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::StackObjectDesc fixedObject(std::uint64_t Size, std::uint64_t Align) { + rcc::StackObjectDesc Description; + Description.Size = Size; + Description.Align = *rcc::Alignment::create(Align); + return Description; +} + +void testObjectLifecycle(TestContext &Context) { + rcc::MachineFrameInfo Frame; + + auto LocalDescription = fixedObject(8, 8); + LocalDescription.IsAliased = true; + LocalDescription.AddressTaken = true; + LocalDescription.Lifetime.push_back( + {rcc::MachineInstrStableID::fromValue(3), rcc::MachineInstrStableID::fromValue(7)}); + auto Local = Frame.createObject(LocalDescription); + + auto FixedDescription = fixedObject(16, 16); + FixedDescription.Kind = rcc::StackObjectKind::FixedABI; + FixedDescription.Offset = -32; + auto Fixed = Frame.createObject(FixedDescription); + + auto SpillDescription = fixedObject(8, 8); + SpillDescription.Kind = rcc::StackObjectKind::SpillSlot; + auto Spill = Frame.createObject(SpillDescription); + + rcc::StackObjectDesc VariableDescription; + VariableDescription.Kind = rcc::StackObjectKind::VariableSized; + VariableDescription.Align = *rcc::Alignment::create(32); + VariableDescription.Offset = 0; + VariableDescription.LocalOffset = 0; + auto Variable = Frame.createObject(VariableDescription); + + Context.expect(Local && Fixed && Spill && Variable, "valid stack object kinds must be created"); + if (!Local || !Fixed || !Spill || !Variable) + return; + + Context.expect(Local->value() == 0 && Fixed->value() == 1 && Spill->value() == 2 && + Variable->value() == 3 && Frame.liveObjectCount() == 4, + "frame indices must be monotonic table identities"); + Context.expect(Frame.object(*Fixed)->isFixed() && Frame.object(*Spill)->isSpill() && + Frame.object(*Variable)->isVariableSized(), + "stack object semantic categories must be derived exactly"); + + Context.expect(Frame.setObjectOffset(*Local, -8).has_value() && + Frame.setObjectOffset(*Spill, -16).has_value() && + Frame.setObjectOffset(*Variable, -64).has_value() && + Frame.markObjectDead(*Spill).has_value(), + "live stack objects must support controlled pre-freeze updates"); + Context.expect(Frame.object(*Spill)->description()->IsDead, + "dead stack object state must remain explicit"); + Context.expect(Frame.object(*Variable)->description()->Offset == -64 && + Frame.object(*Variable)->description()->LocalOffset == 0, + "variable-sized stack objects must preserve LLVM MIR frame offsets"); + + Context.expect(Frame.eraseObject(*Fixed).has_value() && Frame.object(*Fixed) == nullptr && + Frame.liveObjectCount() == 3, + "erasing a stack object must leave an inaccessible tombstone"); + auto Replacement = Frame.createObject(fixedObject(4, 4)); + Context.expect(Replacement && Replacement->value() == 4, + "erased frame indices must never be reused"); + if (!Replacement) + return; + Context.expect(Frame.setObjectOffset(*Replacement, -24).has_value(), + "replacement stack object must accept a final offset"); + + Context.expect(Frame.setMaxCallFrameSize(24).has_value() && + Frame.freeze(64, *rcc::Alignment::create(16)).has_value() && + Frame.isFrozen() && Frame.staticStackSize() == 64 && + Frame.stackAlignment().bytes() == 16 && Frame.maxCallFrameSize() == 24 && + Frame.verify().has_value(), + "complete frame layout must freeze atomically"); + + Context.expect(!Frame.createObject(fixedObject(8, 8)) && !Frame.setObjectOffset(*Local, -40) && + !Frame.eraseObject(*Local) && !Frame.setMaxCallFrameSize(32) && + !Frame.freeze(64, *rcc::Alignment::create(16)), + "frozen frames must reject every semantic mutation"); +} + +void testInvalidDescriptions(TestContext &Context) { + rcc::MachineFrameInfo Frame; + + rcc::StackObjectDesc MissingSize; + Context.expect(!Frame.createObject(MissingSize), + "fixed-size stack object must reject a missing size"); + + auto ZeroSize = fixedObject(0, 1); + Context.expect(Frame.createObject(ZeroSize).has_value(), + "fixed-size stack objects must preserve LLVM MIR zero-sized objects"); + + rcc::StackObjectDesc InvalidVariable; + InvalidVariable.Kind = rcc::StackObjectKind::DynamicAlloca; + InvalidVariable.Size = 8; + Context.expect(!Frame.createObject(InvalidVariable), + "variable-sized stack object must reject a fixed size"); + + auto MissingFixedOffset = fixedObject(8, 8); + MissingFixedOffset.Kind = rcc::StackObjectKind::FixedABI; + Context.expect(!Frame.createObject(MissingFixedOffset), + "fixed ABI stack object must require an offset"); + + auto InvalidTarget = fixedObject(8, 8); + InvalidTarget.Kind = rcc::StackObjectKind::Target; + Context.expect(!Frame.createObject(InvalidTarget), + "target stack object must carry target identity"); + + auto InvalidLifetime = fixedObject(8, 8); + InvalidLifetime.Lifetime.push_back( + {rcc::MachineInstrStableID::fromValue(4), rcc::MachineInstrStableID::fromValue(4)}); + Context.expect(!Frame.createObject(InvalidLifetime), + "stack object lifetime must reject equal boundaries"); + + auto InvalidDebugInfo = fixedObject(8, 8); + InvalidDebugInfo.DebugInfo.emplace(); + Context.expect(!Frame.createObject(InvalidDebugInfo), + "stack object debug information must not be structurally empty"); + + auto DebugObject = fixedObject(8, 8); + DebugObject.DebugInfo = rcc::MachineVariableDebugInfo{ + .Variable = rcc::MachineMetadataReference::embeddedIR(1), + .Expression = rcc::MachineMetadataValue::expression(rcc::MachineDIExpression{}), + .Location = std::nullopt, + }; + auto DebugIndex = Frame.createObject(DebugObject); + Context.expect(DebugIndex.has_value() && + Frame.object(*DebugIndex)->description()->DebugInfo == DebugObject.DebugInfo, + "stack object debug information must remain structured"); + + Context.expect( + rcc::StackID::fromValue(0) && rcc::StackID::fromValue(4) && rcc::StackID::fromValue(255) && + !rcc::StackID::fromValue(5) && !rcc::StackID::fromValue(256) && + rcc::parseStackIDName("sgpr-spill") == rcc::StackID::sgprSpillStack() && + rcc::parseStackIDName("scalable-vector") == rcc::StackID::scalableVectorStack() && + rcc::parseStackIDName("scalable-predicate-vector") == + rcc::StackID::scalablePredicateVectorStack() && + rcc::parseStackIDName("wasm-local") == rcc::StackID::wasmLocalStack() && + rcc::parseStackIDName("noalloc") == rcc::StackID::noAllocStack() && + !rcc::parseStackIDName("safe") && + rcc::getStackIDName(rcc::StackID::noAllocStack()) == "noalloc", + "stack IDs and names must match the fixed LLVM MIR enumeration"); + + auto Target = fixedObject(8, 8); + Target.Kind = rcc::StackObjectKind::Target; + Target.Stack = rcc::StackID::sgprSpillStack(); + auto TargetIndex = Frame.createObject(Target); + Context.expect(TargetIndex.has_value(), "special stack identity must admit target stack objects"); + + auto Unresolved = Frame.createObject(fixedObject(8, 8)); + Context.expect(Unresolved && !Frame.freeze(16, *rcc::Alignment::create(8)) && !Frame.isFrozen() && + !Frame.staticStackSize() && Frame.verify().has_value(), + "failed frame freeze must not mutate frame state"); +} + +} // namespace + +int main() { + TestContext Context; + testObjectLifecycle(Context); + testInvalidDescriptions(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineFunctionClonerTest.cpp b/backend/unittests/CodeGen/MachineFunctionClonerTest.cpp new file mode 100644 index 0000000..82bcce1 --- /dev/null +++ b/backend/unittests/CodeGen/MachineFunctionClonerTest.cpp @@ -0,0 +1,846 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineFunctionCloner.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/LowLevelType.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineCFGUpdater.h" +#include "rcc/CodeGen/MachineCompilationStatus.h" +#include "rcc/CodeGen/MachineConstantPool.h" +#include "rcc/CodeGen/MachineFrameInfo.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineFunctionProperties.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineJumpTableInfo.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/MachineVerifier.h" +#include "rcc/CodeGen/TargetFunctionState.h" + +#include <algorithm> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t NopOpcode = 1; +constexpr std::uint32_t DefOpcode = 2; +constexpr std::uint32_t BranchOpcode = 3; +constexpr std::uint32_t CopyOpcode = 4; +constexpr std::uint32_t FrameOpcode = 5; +constexpr std::uint32_t BlockAddressOpcode = 6; +constexpr std::uint32_t LoadOpcode = 7; +constexpr std::uint32_t ConstantPoolOpcode = 8; +constexpr std::uint32_t JumpTableOpcode = 9; + +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; +}; + +void appendLittleEndian(std::vector<std::byte> &Bytes, std::uint64_t Value, std::size_t Width) { + for (std::size_t Index = 0; Index < Width; ++Index) + Bytes.push_back(static_cast<std::byte>((Value >> (Index * 8U)) & 0xffU)); +} + +[[nodiscard]] std::uint64_t readLittleEndian(std::span<const std::byte> Bytes, std::size_t Offset, + std::size_t Width) { + std::uint64_t Value = 0; + for (std::size_t Index = 0; Index < Width; ++Index) { + Value |= std::to_integer<std::uint64_t>(Bytes[Offset + Index]) << (Index * 8U); + } + return Value; +} + +struct CloneTargetState final { + rcc::MachineBlockStableID Block; + rcc::MachineInstrStableID Instruction; + rcc::Register TrackedRegister; + rcc::FrameIndex Frame; + rcc::ConstantPoolIndex Constant; + rcc::JumpTableIndex JumpTable; + + [[nodiscard]] static constexpr rcc::TargetFunctionStateTypeID stableTypeID() noexcept { + return rcc::TargetFunctionStateTypeID::fromValue(0x524343434c4f4e45ULL); + } + + [[nodiscard]] static constexpr std::uint32_t schemaVersion() noexcept { + return 1; + } + + [[nodiscard]] rcc::Expected<void> verify() const { + if (!Block.isValid() || !Instruction.isValid() || TrackedRegister.isNoRegister()) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "clone target state has an invalid identity"); + } + return {}; + } + + [[nodiscard]] rcc::Expected<CloneTargetState> + clone(const rcc::MachineFunctionCloneContext &Context) const { + auto MappedBlock = Context.mapBlockStableID(Block); + if (!MappedBlock) + return std::unexpected(MappedBlock.error()); + auto MappedInstruction = Context.mapInstructionStableID(Instruction); + if (!MappedInstruction) + return std::unexpected(MappedInstruction.error()); + auto MappedRegister = Context.mapRegister(TrackedRegister); + if (!MappedRegister) + return std::unexpected(MappedRegister.error()); + auto MappedFrame = Context.mapFrameIndex(Frame); + if (!MappedFrame) + return std::unexpected(MappedFrame.error()); + auto MappedConstant = Context.mapConstantPoolIndex(Constant); + if (!MappedConstant) + return std::unexpected(MappedConstant.error()); + auto MappedJumpTable = Context.mapJumpTableIndex(JumpTable); + if (!MappedJumpTable) + return std::unexpected(MappedJumpTable.error()); + return CloneTargetState{*MappedBlock, *MappedInstruction, *MappedRegister, + *MappedFrame, *MappedConstant, *MappedJumpTable}; + } + + [[nodiscard]] std::string print() const { + return "clone-target-state"; + } + + [[nodiscard]] rcc::Expected<std::vector<std::byte>> encode() const { + std::vector<std::byte> Payload; + Payload.reserve(32); + appendLittleEndian(Payload, Block.value(), 8); + appendLittleEndian(Payload, Instruction.value(), 8); + appendLittleEndian(Payload, TrackedRegister.opaqueValue(), 4); + appendLittleEndian(Payload, Frame.value(), 4); + appendLittleEndian(Payload, Constant.value(), 4); + appendLittleEndian(Payload, JumpTable.value(), 4); + return Payload; + } + + [[nodiscard]] static rcc::Expected<CloneTargetState> decode(std::uint32_t Version, + std::span<const std::byte> Payload) { + if (Version != schemaVersion() || Payload.size() != 32) { + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, + "clone target state encoding is invalid"); + } + return CloneTargetState{ + rcc::MachineBlockStableID::fromValue(readLittleEndian(Payload, 0, 8)), + rcc::MachineInstrStableID::fromValue(readLittleEndian(Payload, 8, 8)), + rcc::Register(static_cast<std::uint32_t>(readLittleEndian(Payload, 16, 4))), + rcc::FrameIndex::fromValue(static_cast<std::uint32_t>(readLittleEndian(Payload, 20, 4))), + rcc::ConstantPoolIndex::fromValue( + static_cast<std::uint32_t>(readLittleEndian(Payload, 24, 4))), + rcc::JumpTableIndex::fromValue( + static_cast<std::uint32_t>(readLittleEndian(Payload, 28, 4))), + }; + } + + friend bool operator==(const CloneTargetState &Left, const CloneTargetState &Right) = default; +}; + +[[nodiscard]] rcc::OperandDescriptor registerDescriptor(rcc::RegisterOperandRole Role) { + rcc::OperandDescriptor Descriptor; + Descriptor.Kinds = {rcc::MachineOperand::Kind::Register}; + Descriptor.RegisterRole = Role; + return Descriptor; +} + +[[nodiscard]] rcc::InstructionDescriptor instructionDescriptor(std::uint32_t OpcodeValue, + std::string Name) { + rcc::InstructionDescriptor Descriptor; + Descriptor.InstructionOpcode = rcc::Opcode::fromValue(OpcodeValue); + Descriptor.Name = std::move(Name); + Descriptor.Flags = {rcc::InstructionFlag::ProducesCode}; + return Descriptor; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + auto Nop = instructionDescriptor(NopOpcode, "TEST_NOP"); + + auto Def = instructionDescriptor(DefOpcode, "TEST_DEF"); + Def.Operands.push_back(registerDescriptor(rcc::RegisterOperandRole::Def)); + Def.NumExplicitDefs = 1; + + auto Branch = instructionDescriptor(BranchOpcode, "TEST_BRANCH"); + rcc::OperandDescriptor Block; + Block.Kinds = {rcc::MachineOperand::Kind::BasicBlock}; + Branch.Operands.push_back(Block); + Branch.Flags.set(rcc::InstructionFlag::Branch); + Branch.Flags.set(rcc::InstructionFlag::Terminator); + + auto Copy = instructionDescriptor(CopyOpcode, "TEST_COPY"); + auto CopyDef = registerDescriptor(rcc::RegisterOperandRole::Def); + auto CopyUse = registerDescriptor(rcc::RegisterOperandRole::Use); + CopyDef.DefaultTiedOperand = 1; + CopyUse.DefaultTiedOperand = 0; + Copy.Operands.push_back(CopyDef); + Copy.Operands.push_back(CopyUse); + Copy.NumExplicitDefs = 1; + + auto Frame = instructionDescriptor(FrameOpcode, "TEST_FRAME"); + rcc::OperandDescriptor FrameIndex; + FrameIndex.Kinds = {rcc::MachineOperand::Kind::FrameIndex}; + Frame.Operands.push_back(FrameIndex); + + auto BlockAddress = instructionDescriptor(BlockAddressOpcode, "TEST_BLOCK_ADDRESS"); + rcc::OperandDescriptor Address; + Address.Kinds = {rcc::MachineOperand::Kind::BlockAddress}; + BlockAddress.Operands.push_back(Address); + + auto Load = instructionDescriptor(LoadOpcode, "TEST_LOAD"); + Load.Flags.set(rcc::InstructionFlag::MayLoad); + Load.RequiresMemoryOperands = true; + + auto ConstantPool = instructionDescriptor(ConstantPoolOpcode, "TEST_CONSTANT_POOL"); + rcc::OperandDescriptor ConstantPoolIndex; + ConstantPoolIndex.Kinds = {rcc::MachineOperand::Kind::ConstantPoolIndex}; + ConstantPool.Operands.push_back(ConstantPoolIndex); + + auto JumpTable = instructionDescriptor(JumpTableOpcode, "TEST_JUMP_TABLE"); + rcc::OperandDescriptor JumpTableIndex; + JumpTableIndex.Kinds = {rcc::MachineOperand::Kind::JumpTableIndex}; + JumpTable.Operands.push_back(JumpTableIndex); + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Nop)); + Descriptors.push_back(std::move(Def)); + Descriptors.push_back(std::move(Branch)); + Descriptors.push_back(std::move(Copy)); + Descriptors.push_back(std::move(Frame)); + Descriptors.push_back(std::move(BlockAddress)); + Descriptors.push_back(std::move(Load)); + Descriptors.push_back(std::move(ConstantPool)); + Descriptors.push_back(std::move(JumpTable)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> +createFunction(TestContext &Context, std::string Name, std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "machine function creation must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineBasicBlock *createBlock(TestContext &Context, + rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + Context.expect(Block.has_value(), "machine basic block creation must succeed"); + if (!Block) + return nullptr; + Context.expect(Function.appendBlock(**Block).has_value(), + "machine basic block linking must succeed"); + return *Block; +} + +[[nodiscard]] rcc::MachineMemOperandDesc loadDescription(rcc::MemoryBase Base) { + rcc::MachineMemOperandDesc Description; + Description.Accesses = {rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Dereferenceable}; + Description.Size = *rcc::MemorySize::fixed(8); + Description.Align = *rcc::Alignment::create(8); + Description.BaseAlign = *rcc::Alignment::create(16); + Description.Base = std::move(Base); + Description.Offset = -8; + Description.TBAA = rcc::MachineMetadataReference::embeddedIR(10); + Description.AliasScope = rcc::MachineMetadataReference::embeddedIR(11); + Description.NoAlias = rcc::MachineMetadataReference::embeddedIR(12); + Description.NoAliasAddressSpace = rcc::MachineMetadataReference::embeddedIR(13); + Description.RangeMetadata = rcc::MachineMetadataReference::embeddedIR(14); + Description.TargetFlags = 0x3; + return Description; +} + +[[nodiscard]] rcc::MachineInstr *findInstruction(rcc::MachineBasicBlock &Block, + std::uint32_t OpcodeValue) { + for (rcc::MachineInstr *Instruction = Block.firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + if (Instruction->opcode() == rcc::Opcode::fromValue(OpcodeValue)) + return Instruction; + } + return nullptr; +} + +struct SourceFixture final { + rcc::MachineBasicBlock *Entry = nullptr; + rcc::MachineBasicBlock *Exit = nullptr; + rcc::Register Value; + rcc::Register CopyValue; + rcc::FrameIndex StackSlot; + rcc::ConstantPoolIndex Constant; + rcc::JumpTableIndex JumpTable; + std::vector<const rcc::MachineMemOperand *> LoadMMOs; +}; + +[[nodiscard]] bool populateSource(TestContext &Context, rcc::MachineFunction &Function, + const rcc::TargetDescription &Target, SourceFixture &Fixture) { + auto HistoricalBlock = Function.createBasicBlock(); + auto HistoricalInstruction = Function.createInstruction(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(HistoricalBlock && HistoricalInstruction, "source arena history must be created"); + + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Tombstone = Function.registerInfo().createVirtualRegister(*Type); + Context.expect(Tombstone && Function.registerInfo().eraseVirtualRegister(*Tombstone).has_value(), + "source vreg history must contain a tombstone"); + + rcc::StackObjectDesc HistoricalFrame; + HistoricalFrame.Size = 4; + HistoricalFrame.Align = *rcc::Alignment::create(4); + auto HistoricalFrameIndex = Function.frameInfo().createObject(HistoricalFrame); + Context.expect(HistoricalFrameIndex && + Function.frameInfo().eraseObject(*HistoricalFrameIndex).has_value(), + "source frame history must contain a tombstone"); + + rcc::StackObjectDesc StackSlot; + StackSlot.Kind = rcc::StackObjectKind::SpillSlot; + StackSlot.Size = 8; + StackSlot.Align = *rcc::Alignment::create(8); + StackSlot.Offset = -16; + auto StackSlotIndex = Function.frameInfo().createObject(StackSlot); + Context.expect(StackSlotIndex.has_value(), "source live stack object must be created"); + if (!StackSlotIndex) + return false; + Fixture.StackSlot = *StackSlotIndex; + + Fixture.Entry = createBlock(Context, Function); + Fixture.Exit = createBlock(Context, Function); + if (Fixture.Entry == nullptr || Fixture.Exit == nullptr) + return false; + + rcc::MachineConstantPoolEntryDesc HistoricalConstant; + auto HistoricalInteger = rcc::APInt::fromUnsigned(32, 0x01010101U); + HistoricalConstant.Value = + std::move(*rcc::GenericConstantData::integer(std::move(*HistoricalInteger))); + HistoricalConstant.Size = 4; + HistoricalConstant.Align = *rcc::Alignment::create(4); + auto HistoricalConstantIndex = Function.constantPool().addEntry(std::move(HistoricalConstant)); + Context.expect(HistoricalConstantIndex && + Function.constantPool().eraseEntry(*HistoricalConstantIndex).has_value(), + "source constant pool history must contain a tombstone"); + + rcc::MachineConstantPoolEntryDesc Constant; + Constant.Value = rcc::BlockAddressConstant{std::string(Function.name()), "exit", 5}; + Constant.Size = 8; + Constant.Align = *rcc::Alignment::create(8); + Constant.Relocation = rcc::ConstantPoolRelocationMode::Static; + auto ConstantIndex = Function.constantPool().addEntry(std::move(Constant)); + Context.expect(ConstantIndex && Function.constantPool().freeze().has_value(), + "source live constant pool entry must be frozen"); + if (!ConstantIndex) + return false; + Fixture.Constant = *ConstantIndex; + + rcc::MachineJumpTableDesc HistoricalJumpTable; + HistoricalJumpTable.EntrySize = 4; + HistoricalJumpTable.Align = *rcc::Alignment::create(4); + HistoricalJumpTable.Targets = {Fixture.Entry}; + auto HistoricalJumpTableIndex = + Function.jumpTableInfo().createTable(std::move(HistoricalJumpTable)); + Context.expect(HistoricalJumpTableIndex && + Function.jumpTableInfo().eraseTable(*HistoricalJumpTableIndex).has_value(), + "source jump table history must contain a tombstone"); + + rcc::MachineJumpTableDesc JumpTable; + JumpTable.Encoding = rcc::JumpTableEntryEncoding::GPRelative32BlockAddress; + JumpTable.EntrySize = 4; + JumpTable.Align = *rcc::Alignment::create(4); + JumpTable.Targets = {Fixture.Exit, Fixture.Entry, Fixture.Exit}; + auto JumpTableIndex = Function.jumpTableInfo().createTable(std::move(JumpTable)); + Context.expect(JumpTableIndex && Function.jumpTableInfo().freeze().has_value(), + "source live jump table must be frozen"); + if (!JumpTableIndex) + return false; + Fixture.JumpTable = *JumpTableIndex; + + auto Value = Function.registerInfo().createVirtualRegister(*Type); + auto CopyValue = Function.registerInfo().createVirtualRegister(*Type); + Context.expect(Value && CopyValue, "source live vregs must be created"); + if (!Value || !CopyValue) + return false; + Fixture.Value = *Value; + Fixture.CopyValue = *CopyValue; + + const auto Bank = rcc::RegisterBankID::fromValue(1); + const auto RegClass = rcc::RegisterClassID::fromValue(2); + for (const rcc::Register Reg : {Fixture.Value, Fixture.CopyValue}) { + Context.expect(Function.registerInfo().setRegisterBank(Reg, Bank).has_value() && + Function.registerInfo().setRegisterClass(Reg, RegClass).has_value() && + Function.registerInfo().setTargetRegisterFlags(Reg, 0x3).has_value(), + "source vreg constraints must be recorded"); + } + + const auto LiveIn = *rcc::PhysicalRegister::create(1); + const auto Reserved = *rcc::PhysicalRegister::create(2); + const auto Hint = *rcc::PhysicalRegister::create(5); + Context.expect(Function.registerInfo().addAllocationHint(Fixture.Value, Hint).has_value() && + Function.registerInfo().addLiveIn(LiveIn, Fixture.Value).has_value() && + Function.registerInfo().reservePhysicalRegister(Reserved).has_value() && + Fixture.Entry->addLiveIn(LiveIn).has_value(), + "source register configuration must be recorded"); + + rcc::MachineIRBuilder Builder(Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Entry).has_value(), + "source entry insertion point must be valid"); + auto Def = Builder.buildInstr(rcc::Opcode::fromValue(DefOpcode)); + Context.expect(Def && Def->addDef(Fixture.Value).has_value(), + "source definition must be assembled"); + if (!Def) + return false; + Context.expect(Def->setFlags({rcc::MachineInstrFlag::NoMerge}).has_value() && + Def->setSourceLocation({"source.mir", 9, 4, 2}).has_value() && + Def->setOrigin(rcc::OriginID::fromValue(77)).has_value() && Def->build(), + "source definition metadata must build"); + + auto Branch = Builder.buildInstr(rcc::Opcode::fromValue(BranchOpcode)); + Context.expect(Branch && Branch->addBasicBlock(*Fixture.Exit).has_value() && Branch->build(), + "source branch must build"); + Context.expect(Function.cfgUpdater() + .addSuccessor(*Fixture.Entry, *Fixture.Exit, rcc::BranchProbability::one()) + .has_value(), + "source CFG edge must build"); + + Context.expect(Builder.setInsertPoint(*Fixture.Exit).has_value(), + "source exit insertion point must be valid"); + auto Copy = Builder.buildInstr(rcc::Opcode::fromValue(CopyOpcode)); + Context.expect(Copy && Copy->addDef(Fixture.CopyValue).has_value() && + Copy->addUse(Fixture.Value).has_value() && Copy->build(), + "source tied copy must build"); + + auto Frame = Builder.buildInstr(rcc::Opcode::fromValue(FrameOpcode)); + Context.expect(Frame && Frame->addFrameIndex(Fixture.StackSlot, -8).has_value() && Frame->build(), + "source frame-index instruction must build"); + + auto ConstantPool = Builder.buildInstr(rcc::Opcode::fromValue(ConstantPoolOpcode)); + Context.expect(ConstantPool && ConstantPool->addConstantPool(Fixture.Constant, 8).has_value() && + ConstantPool->build(), + "source constant-pool instruction must build"); + + auto JumpTableInstruction = Builder.buildInstr(rcc::Opcode::fromValue(JumpTableOpcode)); + Context.expect(JumpTableInstruction && + JumpTableInstruction->addJumpTable(Fixture.JumpTable).has_value() && + JumpTableInstruction->build(), + "source jump-table instruction must build"); + + auto Address = Builder.buildInstr(rcc::Opcode::fromValue(BlockAddressOpcode)); + Context.expect( + Address && Address->addBlockAddress(std::string(Function.name()), "exit", 3).has_value() && + Address->build(), + "source self block-address instruction must build"); + + for (rcc::MemoryBase Base : {rcc::MemoryBase::frameIndex(Fixture.StackSlot), + rcc::MemoryBase::constantPool(Fixture.Constant), + rcc::MemoryBase::jumpTable(Fixture.JumpTable)}) { + auto MMO = Function.createMemOperand(loadDescription(std::move(Base))); + Context.expect(MMO.has_value(), "source MMO must be created"); + if (!MMO) + return false; + Fixture.LoadMMOs.push_back(*MMO); + } + auto Load = Builder.buildInstr(rcc::Opcode::fromValue(LoadOpcode)); + if (!Load) + return false; + bool AddedAllMMOs = true; + for (const rcc::MachineMemOperand *MMO : Fixture.LoadMMOs) + AddedAllMMOs &= Load->addMemOperand(*MMO).has_value(); + Context.expect(AddedAllMMOs && Load->build(), "source load with MMOs must build"); + + rcc::MachineInstr *Definition = findInstruction(*Fixture.Entry, DefOpcode); + rcc::MachineInstr *LoadInstruction = findInstruction(*Fixture.Exit, LoadOpcode); + const rcc::StackObject *Object = Function.frameInfo().object(Fixture.StackSlot); + if (Definition == nullptr || LoadInstruction == nullptr || Object == nullptr) + return false; + Context.expect( + Definition->setPreInstrSymbol("before").has_value() && + Definition->setPostInstrSymbol("after").has_value() && + Definition->setHeapAllocMarker(rcc::MachineMetadataReference::embeddedIR(20)) + .has_value() && + Definition->setPCSections(rcc::MachineMetadataReference::embeddedIR(21)).has_value() && + Definition->setMMRA(rcc::MachineMetadataReference::embeddedIR(22)).has_value() && + Definition->setCFIType(23).has_value() && + Definition->setDeactivationSymbol("guard").has_value(), + "source instruction attachments must be recorded"); + rcc::StackObjectDesc UpdatedStackSlot = *Object->description(); + UpdatedStackSlot.Lifetime.push_back({Definition->stableID(), LoadInstruction->stableID()}); + Context.expect(Function.frameInfo() + .updateObject(Fixture.StackSlot, std::move(UpdatedStackSlot)) + .has_value() && + Function.frameInfo().setMaxCallFrameSize(16).has_value() && + Function.frameInfo().freeze(32, *rcc::Alignment::create(16)).has_value(), + "source frame metadata must be finalized"); + + auto TargetState = rcc::TargetFunctionState::create( + CloneTargetState{Fixture.Entry->stableID(), LoadInstruction->stableID(), Fixture.Value, + Fixture.StackSlot, Fixture.Constant, Fixture.JumpTable}); + Context.expect(TargetState.has_value(), "source target function state must be created"); + if (!TargetState) + return false; + Context.expect(Function.setTargetState(std::move(*TargetState)).has_value(), + "source target function state must be installed"); + + rcc::MachineCompilationFailure Failure; + Failure.Diagnostic.Code = rcc::ErrorCode::Unsupported; + Failure.Diagnostic.Message = "test instruction selection failed"; + Failure.Diagnostic.Location = rcc::SourceLocation{"source.mir", 12, 5, 0}; + Failure.FailurePass = "test-instruction-select"; + Failure.FailureInstruction = LoadInstruction->stableID(); + Failure.FailureStage = rcc::PipelineStage::Selected; + Failure.FailureRule = "test-load-pattern"; + Failure.Target = "test64"; + Failure.Subtarget = "test64+feature"; + Context.expect(Function.compilationStatus() + .fail(rcc::MachineCompilationState::SelectionFailed, std::move(Failure)) + .has_value(), + "source structured compilation failure must be recorded"); + + Function.properties().set(rcc::MachineFunctionProperty::IsSSA); + Function.properties().set(rcc::MachineFunctionProperty::TracksLiveness); + Function.properties().set(rcc::MachineFunctionProperty::NoPHIs); + return true; +} + +void testDeepClone(TestContext &Context) { + auto Target = createTarget(); + auto Source = createFunction(Context, "source_function", 1); + SourceFixture Fixture; + if (!Target || !Source || !populateSource(Context, *Source, *Target, Fixture)) { + return; + } + Context.expect(rcc::MachineVerifier::verify(*Source, *Target).has_value(), + "source function must verify before cloning"); + + auto Clone = rcc::cloneMachineFunction( + *Source, *Target, + rcc::MachineFunctionCloneOptions{rcc::MachineFunctionStableID::fromValue(99), + std::string("cloned_function")}); + Context.expect(Clone.has_value(), "valid source function must deep-clone"); + if (!Clone) + return; + rcc::MachineFunction &Destination = *Clone->ClonedFunction; + + Context.expect(Destination.name() == "cloned_function" && + Destination.stableID() == rcc::MachineFunctionStableID::fromValue(99), + "clone must use its explicit function identity"); + Context.expect(Destination.properties() == Source->properties() && + rcc::MachineVerifier::verify(Destination, *Target).has_value() && + rcc::MachineVerifier::verify(*Source, *Target).has_value(), + "source and destination must independently preserve valid properties"); + Context.expect(Source->allBlocks().size() == 3 && Destination.allBlocks().size() == 2 && + Source->allInstructions().size() == 9 && + Destination.allInstructions().size() == 8 && + Source->frameInfo().objects().size() == 2 && + Destination.frameInfo().objects().size() == 1 && + Source->constantPool().entries().size() == 2 && + Destination.constantPool().entries().size() == 1 && + Source->jumpTableInfo().tables().size() == 2 && + Destination.jumpTableInfo().tables().size() == 1, + "clone must omit detached arena history"); + Context.expect(Clone->BlockStableIDs.size() == 2 && Clone->InstructionStableIDs.size() == 8 && + Clone->VirtualRegisters.size() == 2 && Clone->FrameIndices.size() == 1 && + Clone->ConstantPoolIndices.size() == 1 && Clone->JumpTableIndices.size() == 1, + "clone must publish complete live identity mappings"); + + rcc::MachineBasicBlock *DestinationEntry = Destination.firstBlock(); + rcc::MachineBasicBlock *DestinationExit = + DestinationEntry == nullptr ? nullptr : DestinationEntry->next(); + Context.expect(DestinationEntry != nullptr && DestinationExit != nullptr && + DestinationExit->next() == nullptr && DestinationEntry != Fixture.Entry && + DestinationExit != Fixture.Exit, + "clone blocks must be distinct and preserve layout"); + if (DestinationEntry == nullptr || DestinationExit == nullptr) + return; + + const auto EntryID = Clone->BlockStableIDs.find(Fixture.Entry->stableID()); + const auto ExitID = Clone->BlockStableIDs.find(Fixture.Exit->stableID()); + Context.expect(EntryID != Clone->BlockStableIDs.end() && ExitID != Clone->BlockStableIDs.end() && + EntryID->second == DestinationEntry->stableID() && + ExitID->second == DestinationExit->stableID(), + "block StableID mapping must identify cloned layout objects"); + Context.expect(DestinationEntry->successors().size() == 1 && + DestinationEntry->successors()[0].Block == DestinationExit && + DestinationExit->predecessors().size() == 1 && + DestinationExit->predecessors()[0] == DestinationEntry, + "clone must rebuild reciprocal CFG edges"); + Context.expect(std::ranges::equal(DestinationEntry->liveIns(), Fixture.Entry->liveIns()), + "clone must preserve block live-ins"); + + const auto ValuePosition = Clone->VirtualRegisters.find(Fixture.Value); + const auto CopyValuePosition = Clone->VirtualRegisters.find(Fixture.CopyValue); + Context.expect(ValuePosition != Clone->VirtualRegisters.end() && + CopyValuePosition != Clone->VirtualRegisters.end(), + "clone must map every live source vreg"); + if (ValuePosition == Clone->VirtualRegisters.end() || + CopyValuePosition == Clone->VirtualRegisters.end()) { + return; + } + const rcc::Register NewValue = ValuePosition->second; + const rcc::Register NewCopyValue = CopyValuePosition->second; + Context.expect(NewValue.virtualIndex() == 0 && NewCopyValue.virtualIndex() == 1, + "clone must deterministically compact source vreg tombstones"); + + auto SourceInfo = Source->registerInfo().virtualRegisterInfo(Fixture.Value); + auto DestinationInfo = Destination.registerInfo().virtualRegisterInfo(NewValue); + Context.expect(SourceInfo && DestinationInfo && + SourceInfo->get().Type == DestinationInfo->get().Type && + SourceInfo->get().Bank == DestinationInfo->get().Bank && + SourceInfo->get().RegClass == DestinationInfo->get().RegClass && + SourceInfo->get().AllocationHints == DestinationInfo->get().AllocationHints && + SourceInfo->get().TargetFlags == DestinationInfo->get().TargetFlags, + "clone must preserve vreg type and allocation constraints"); + Context.expect(Destination.registerInfo().liveIns().size() == 1 && + Destination.registerInfo().liveIns()[0].InitialVirtualRegister == NewValue && + std::ranges::equal(Destination.registerInfo().reservedRegisters(), + Source->registerInfo().reservedRegisters()), + "clone must remap function live-ins and preserve reserved registers"); + + rcc::MachineInstr *DestinationDef = findInstruction(*DestinationEntry, DefOpcode); + rcc::MachineInstr *DestinationBranch = findInstruction(*DestinationEntry, BranchOpcode); + rcc::MachineInstr *DestinationCopy = findInstruction(*DestinationExit, CopyOpcode); + rcc::MachineInstr *DestinationFrame = findInstruction(*DestinationExit, FrameOpcode); + rcc::MachineInstr *DestinationConstantPool = + findInstruction(*DestinationExit, ConstantPoolOpcode); + rcc::MachineInstr *DestinationJumpTable = findInstruction(*DestinationExit, JumpTableOpcode); + rcc::MachineInstr *DestinationAddress = findInstruction(*DestinationExit, BlockAddressOpcode); + rcc::MachineInstr *DestinationLoad = findInstruction(*DestinationExit, LoadOpcode); + Context.expect(DestinationDef && DestinationBranch && DestinationCopy && DestinationFrame && + DestinationConstantPool && DestinationJumpTable && DestinationAddress && + DestinationLoad, + "clone must preserve every live instruction"); + if (!DestinationDef || !DestinationBranch || !DestinationCopy || !DestinationFrame || + !DestinationConstantPool || !DestinationJumpTable || !DestinationAddress || + !DestinationLoad) { + return; + } + + Context.expect( + DestinationDef->operandAt(0)->registerValue() == NewValue && + DestinationDef->flags().has(rcc::MachineInstrFlag::NoMerge) && + DestinationDef->sourceLocation() == rcc::SourceLocation{"source.mir", 9, 4, 2} && + DestinationDef->origin() == rcc::OriginID::fromValue(77) && + DestinationDef->preInstrSymbol() == "before" && + DestinationDef->postInstrSymbol() == "after" && + DestinationDef->heapAllocMarker() == rcc::MachineMetadataReference::embeddedIR(20) && + DestinationDef->pcSections() == rcc::MachineMetadataReference::embeddedIR(21) && + DestinationDef->mmra() == rcc::MachineMetadataReference::embeddedIR(22) && + DestinationDef->cfiType() == 23 && DestinationDef->deactivationSymbol() == "guard", + "clone must remap register operands and preserve instruction metadata"); + Context.expect(DestinationBranch->operandAt(0)->getIf<rcc::BasicBlockOperand>()->Block == + DestinationExit, + "clone must remap basic-block operands"); + Context.expect(DestinationCopy->operandAt(0)->registerValue() == NewCopyValue && + DestinationCopy->operandAt(1)->registerValue() == NewValue && + DestinationCopy->operandAt(0)->tiedOperandIndex() == 1 && + DestinationCopy->operandAt(1)->tiedOperandIndex() == 0, + "clone must remap tied registers and rebuild ties"); + const auto FramePosition = Clone->FrameIndices.find(Fixture.StackSlot); + Context.expect(FramePosition != Clone->FrameIndices.end() && + FramePosition->second == rcc::FrameIndex::fromValue(0) && + DestinationFrame->operandAt(0)->getIf<rcc::FrameIndexOperand>()->Index == + FramePosition->second, + "clone must compact and remap frame indices"); + + const auto ConstantPosition = Clone->ConstantPoolIndices.find(Fixture.Constant); + const auto JumpTablePosition = Clone->JumpTableIndices.find(Fixture.JumpTable); + Context.expect( + ConstantPosition != Clone->ConstantPoolIndices.end() && + JumpTablePosition != Clone->JumpTableIndices.end() && + ConstantPosition->second == rcc::ConstantPoolIndex::fromValue(0) && + JumpTablePosition->second == rcc::JumpTableIndex::fromValue(0) && + DestinationConstantPool->operandAt(0)->getIf<rcc::ConstantPoolOperand>()->Index == + ConstantPosition->second && + DestinationJumpTable->operandAt(0)->getIf<rcc::JumpTableOperand>()->Index == + JumpTablePosition->second, + "clone must compact and remap constant-pool and jump-table indices"); + + const rcc::MachineConstantPoolEntry *DestinationConstantEntry = + ConstantPosition == Clone->ConstantPoolIndices.end() + ? nullptr + : Destination.constantPool().entry(ConstantPosition->second); + const auto *DestinationPoolAddress = + DestinationConstantEntry == nullptr + ? nullptr + : std::get_if<rcc::BlockAddressConstant>(&DestinationConstantEntry->description()->Value); + Context.expect(Destination.constantPool().isFrozen() && DestinationPoolAddress != nullptr && + DestinationPoolAddress->FunctionName == "cloned_function" && + DestinationPoolAddress->BlockName == "exit", + "clone must preserve frozen constant pool state and rewrite self addresses"); + + const rcc::MachineJumpTableEntry *DestinationJumpTableEntry = + JumpTablePosition == Clone->JumpTableIndices.end() + ? nullptr + : Destination.jumpTableInfo().table(JumpTablePosition->second); + const rcc::MachineJumpTableDesc *DestinationJumpDescription = + DestinationJumpTableEntry == nullptr ? nullptr : DestinationJumpTableEntry->description(); + Context.expect(Destination.jumpTableInfo().isFrozen() && DestinationJumpDescription != nullptr && + DestinationJumpDescription->Targets.size() == 3 && + DestinationJumpDescription->Targets[0] == DestinationExit && + DestinationJumpDescription->Targets[1] == DestinationEntry && + DestinationJumpDescription->Targets[2] == DestinationExit, + "clone must preserve jump-table order while remapping block targets"); + + Context.expect( + DestinationAddress->operandAt(0)->getIf<rcc::BlockAddressOperand>()->FunctionName == + "cloned_function", + "clone must rewrite self block-address function names"); + + bool MMOsMatch = DestinationLoad->memOperands().size() == Fixture.LoadMMOs.size(); + for (std::size_t Index = 0; MMOsMatch && Index < DestinationLoad->memOperands().size(); ++Index) { + rcc::MachineMemOperandDesc ExpectedMMO = Fixture.LoadMMOs[Index]->description(); + if (const auto Frame = ExpectedMMO.Base.frameIndex(); + Frame && FramePosition != Clone->FrameIndices.end()) { + ExpectedMMO.Base = rcc::MemoryBase::frameIndex(FramePosition->second); + } + if (const auto Constant = ExpectedMMO.Base.constantPoolIndex(); + Constant && ConstantPosition != Clone->ConstantPoolIndices.end()) { + ExpectedMMO.Base = rcc::MemoryBase::constantPool(ConstantPosition->second); + } + if (const auto JumpTable = ExpectedMMO.Base.jumpTableIndex(); + JumpTable && JumpTablePosition != Clone->JumpTableIndices.end()) { + ExpectedMMO.Base = rcc::MemoryBase::jumpTable(JumpTablePosition->second); + } + + const rcc::MachineMemOperand *DestinationMMO = DestinationLoad->memOperands()[Index]; + MMOsMatch = + DestinationMMO != Fixture.LoadMMOs[Index] && DestinationMMO->description() == ExpectedMMO && + Destination.ownsMemOperand(*DestinationMMO) && !Source->ownsMemOperand(*DestinationMMO); + } + Context.expect(MMOsMatch, "clone must deep-copy MMO ownership while remapping every table base"); + + const rcc::StackObject *DestinationStackSlot = + FramePosition == Clone->FrameIndices.end() + ? nullptr + : Destination.frameInfo().object(FramePosition->second); + Context.expect( + DestinationStackSlot != nullptr && Destination.frameInfo().isFrozen() && + Destination.frameInfo().staticStackSize() == 32 && + Destination.frameInfo().maxCallFrameSize() == 16 && + DestinationStackSlot->description()->Lifetime.size() == 1 && + DestinationStackSlot->description()->Lifetime[0].Begin == DestinationDef->stableID() && + DestinationStackSlot->description()->Lifetime[0].End == DestinationLoad->stableID(), + "clone must preserve frozen frame layout and remap instruction lifetimes"); + + rcc::MachineInstr *SourceDef = findInstruction(*Fixture.Entry, DefOpcode); + const auto InstructionID = SourceDef == nullptr + ? Clone->InstructionStableIDs.end() + : Clone->InstructionStableIDs.find(SourceDef->stableID()); + Context.expect(SourceDef != nullptr && InstructionID != Clone->InstructionStableIDs.end() && + InstructionID->second == DestinationDef->stableID(), + "instruction StableID mapping must identify cloned instructions"); + + rcc::MachineInstr *SourceLoad = findInstruction(*Fixture.Exit, LoadOpcode); + const auto LoadID = SourceLoad == nullptr + ? Clone->InstructionStableIDs.end() + : Clone->InstructionStableIDs.find(SourceLoad->stableID()); + const CloneTargetState *SourceTarget = Source->getTargetInfo<CloneTargetState>(); + const CloneTargetState *DestinationTarget = Destination.getTargetInfo<CloneTargetState>(); + Context.expect(SourceLoad != nullptr && LoadID != Clone->InstructionStableIDs.end() && + SourceTarget != nullptr && DestinationTarget != nullptr && + SourceTarget != DestinationTarget && EntryID != Clone->BlockStableIDs.end() && + FramePosition != Clone->FrameIndices.end() && + ConstantPosition != Clone->ConstantPoolIndices.end() && + JumpTablePosition != Clone->JumpTableIndices.end() && + DestinationTarget->Block == EntryID->second && + DestinationTarget->Instruction == LoadID->second && + DestinationTarget->TrackedRegister == NewValue && + DestinationTarget->Frame == FramePosition->second && + DestinationTarget->Constant == ConstantPosition->second && + DestinationTarget->JumpTable == JumpTablePosition->second, + "clone must strongly clone target state through every identity mapping"); + + const rcc::MachineCompilationFailure *SourceFailure = Source->compilationStatus().failure(); + const rcc::MachineCompilationFailure *DestinationFailure = + Destination.compilationStatus().failure(); + Context.expect(SourceFailure != nullptr && DestinationFailure != nullptr && + Destination.compilationStatus().state() == + rcc::MachineCompilationState::SelectionFailed && + DestinationFailure->FailureInstruction == DestinationLoad->stableID() && + DestinationFailure->Diagnostic == SourceFailure->Diagnostic && + DestinationFailure->FailurePass == SourceFailure->FailurePass && + DestinationFailure->FailureStage == SourceFailure->FailureStage && + DestinationFailure->FailureRule == SourceFailure->FailureRule && + DestinationFailure->Target == SourceFailure->Target && + DestinationFailure->Subtarget == SourceFailure->Subtarget, + "clone must preserve compilation failure details and remap its instruction"); +} + +void testCloneFailures(TestContext &Context) { + auto Target = createTarget(); + auto Source = createFunction(Context, "failure_source", 2); + if (!Target || !Source) + return; + + Context.expect( + !rcc::cloneMachineFunction(*Source, *Target, + rcc::MachineFunctionCloneOptions{ + rcc::MachineFunctionStableID::fromValue(0), std::nullopt}), + "clone must reject an invalid destination StableID"); + auto EmptyName = rcc::cloneMachineFunction( + *Source, *Target, + rcc::MachineFunctionCloneOptions{rcc::MachineFunctionStableID::fromValue(3), std::string()}); + Context.expect(EmptyName && EmptyName->ClonedFunction->name().empty(), + "clone must preserve an explicitly empty destination name"); + + auto Invalid = createFunction(Context, "invalid_source", 4); + rcc::MachineBasicBlock *Block = Invalid ? createBlock(Context, *Invalid) : nullptr; + if (Invalid && Block != nullptr) { + auto Unknown = Invalid->createInstruction(rcc::Opcode::fromValue(999)); + Context.expect(Unknown && Block->appendInstruction(**Unknown).has_value(), + "invalid clone source must contain an unknown opcode"); + Context.expect( + !rcc::cloneMachineFunction(*Invalid, *Target, + rcc::MachineFunctionCloneOptions{ + rcc::MachineFunctionStableID::fromValue(5), std::nullopt}), + "clone must verify and reject an invalid source function"); + } + + auto UnmappedState = createFunction(Context, "unmapped_target_state", 6); + auto Physical = rcc::Register::makePhysical(1); + if (UnmappedState && Physical) { + auto State = rcc::TargetFunctionState::create(CloneTargetState{ + rcc::MachineBlockStableID::fromValue(99), rcc::MachineInstrStableID::fromValue(99), + *Physical, rcc::FrameIndex::fromValue(0), rcc::ConstantPoolIndex::fromValue(0), + rcc::JumpTableIndex::fromValue(0)}); + Context.expect(State && UnmappedState->setTargetState(std::move(*State)).has_value(), + "unmapped target state negative test must be installed"); + Context.expect( + !rcc::cloneMachineFunction(*UnmappedState, *Target, + rcc::MachineFunctionCloneOptions{ + rcc::MachineFunctionStableID::fromValue(7), std::nullopt}), + "clone must reject target state that does not map its identities"); + } +} + +} // namespace + +int main() { + TestContext Context; + testDeepClone(Context); + testCloneFailures(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineFunctionRecordsTest.cpp b/backend/unittests/CodeGen/MachineFunctionRecordsTest.cpp new file mode 100644 index 0000000..621bd21 --- /dev/null +++ b/backend/unittests/CodeGen/MachineFunctionRecordsTest.cpp @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineFunctionRecords.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineDebugInfo.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineFunctionCloner.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineVerifier.h" +#include "rcc/CodeGen/TargetDescription.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t NopOpcode = 1; +constexpr std::uint32_t CallOpcode = 2; + +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::PhysicalRegister phys(std::uint32_t Number) { + auto Reg = rcc::PhysicalRegister::create(Number); + return Reg ? *Reg : rcc::PhysicalRegister{}; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + rcc::InstructionDescriptor Nop; + Nop.InstructionOpcode = rcc::Opcode::fromValue(NopOpcode); + Nop.Name = "TEST_NOP"; + Nop.Flags = {rcc::InstructionFlag::ProducesCode}; + + rcc::InstructionDescriptor Call; + Call.InstructionOpcode = rcc::Opcode::fromValue(CallOpcode); + Call.Name = "TEST_CALL"; + Call.Flags = {rcc::InstructionFlag::Call, rcc::InstructionFlag::HasSideEffects, + rcc::InstructionFlag::ProducesCode}; + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Nop)); + Descriptors.push_back(std::move(Call)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> +createFunction(TestContext &Context, std::uint64_t StableID, std::string Name) { + auto Function = rcc::MachineFunction::create(std::move(Name), + rcc::MachineFunctionStableID::fromValue(StableID)); + Context.expect(Function.has_value(), "machine function creation must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineBasicBlock *appendBlock(TestContext &Context, + rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + Context.expect(Block.has_value(), "machine basic block creation must succeed"); + if (!Block) + return nullptr; + Context.expect(Function.appendBlock(**Block).has_value(), "machine block linking must succeed"); + return *Block; +} + +[[nodiscard]] rcc::MachineInstr *appendInstruction(TestContext &Context, + rcc::MachineFunction &Function, + rcc::MachineBasicBlock &Block, + std::uint32_t OpcodeValue) { + auto Instruction = Function.createInstruction(rcc::Opcode::fromValue(OpcodeValue)); + Context.expect(Instruction.has_value(), "machine instruction creation must succeed"); + if (!Instruction) + return nullptr; + Context.expect(Block.appendInstruction(**Instruction).has_value(), + "machine instruction linking must succeed"); + return *Instruction; +} + +void testValueValidation(TestContext &Context) { + rcc::MachineFunctionRecords Records; + Context.expect(!Records.hasCalleeSavedRegisters() && Records.calleeSavedRegisters().empty(), + "an absent callee-saved list must remain distinguishable"); + Context.expect(Records.setCalleeSavedRegisters({}).has_value() && + Records.hasCalleeSavedRegisters() && Records.calleeSavedRegisters().empty(), + "an explicit empty callee-saved list must remain present"); + Records.clearCalleeSavedRegisters(); + + const rcc::PhysicalRegister G0 = phys(rcc::test::TestRegister::G0); + Context.expect(!Records.setCalleeSavedRegisters({G0, G0}), + "duplicate callee-saved registers must be rejected"); + Context.expect(!Records.addCallSite({}), + "a call-site record must require a valid instruction StableID"); + Context.expect(!Records.addCalledGlobal({ + .CallInstruction = rcc::MachineInstrStableID::fromValue(1), + .Callee = {}, + .Flags = 0, + }), + "a called-global record must require a nonempty callee"); + Context.expect(!Records.addEntryValue({}), + "an entry-value record must require a physical register and debug metadata"); + + const rcc::MachinePrefetchTarget Target{ + .Block = {.BaseID = 7, .CloneID = 3}, + .CallsiteIndex = 4, + }; + Context.expect(Records.addPrefetchTarget(Target).has_value() && + !Records.addPrefetchTarget(Target), + "duplicate structured prefetch targets must be rejected"); +} + +void testVerifierAndClone(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 1, "records"); + if (!Target || Function == nullptr) + return; + Context.expect( + Function->metadata().addNode({.Slot = 0, .IsDistinct = false, .Elements = {}}).has_value(), + "function record metadata setup must succeed"); + + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + Block->attributes().setProfileID({.BaseID = 19, .CloneID = 2}); + rcc::MachineInstr *Call = appendInstruction(Context, *Function, *Block, CallOpcode); + if (Call == nullptr) + return; + + const rcc::PhysicalRegister G0 = phys(rcc::test::TestRegister::G0); + const rcc::PhysicalRegister G1 = phys(rcc::test::TestRegister::G1); + Context.expect(Function->records().setCalleeSavedRegisters({G0, G1}).has_value(), + "callee-saved registers must be stored"); + Context.expect(Function->records() + .addCallSite({ + .CallInstruction = Call->stableID(), + .ForwardedArguments = + { + {.Reg = G0, .ArgumentNumber = 0}, + {.Reg = G1, .ArgumentNumber = 0}, + }, + .CalleeTypeIDs = {0x11, 0x22}, + }) + .has_value(), + "structured call-site information must be stored"); + Context.expect(Function->records() + .addCalledGlobal({ + .CallInstruction = Call->stableID(), + .Callee = "callee", + .Flags = 5, + }) + .has_value(), + "structured called-global information must be stored"); + Context.expect(Function->records() + .addPrefetchTarget({ + .Block = {.BaseID = 19, .CloneID = 2}, + .CallsiteIndex = 8, + }) + .has_value(), + "structured prefetch target information must be stored"); + auto EntryLocation = + rcc::MachineDebugLocation::reference(rcc::MachineMetadataReference::embeddedIR(12)); + Context.expect(EntryLocation.has_value(), "entry-value debug location must be representable"); + if (!EntryLocation) + return; + const rcc::MachineEntryValueInfo EntryValue{ + .Reg = G0, + .DebugInfo = + { + .Variable = rcc::MachineMetadataReference::embeddedIR(10), + .Expression = rcc::MachineMetadataValue::expression(rcc::MachineDIExpression{}), + .Location = *EntryLocation, + }, + }; + Context.expect(Function->records().addEntryValue(EntryValue).has_value(), + "structured entry-value debug information must be stored"); + Context.expect(rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "well-formed function records must pass unified verification"); + + auto Clone = + rcc::cloneMachineFunction(*Function, *Target, + { + .NewStableID = rcc::MachineFunctionStableID::fromValue(2), + .NewName = std::string("records.clone"), + }); + Context.expect(Clone.has_value(), "machine function records must support deep cloning"); + if (!Clone) + return; + + const auto InstructionID = Clone->InstructionStableIDs.find(Call->stableID()); + const rcc::MachineFunctionRecords &ClonedRecords = Clone->ClonedFunction->records(); + Context.expect( + InstructionID != Clone->InstructionStableIDs.end() && + ClonedRecords.hasCalleeSavedRegisters() && + ClonedRecords.calleeSavedRegisters().size() == 2 && + ClonedRecords.callSites().size() == 1 && + ClonedRecords.callSites()[0].CallInstruction == InstructionID->second && + ClonedRecords.callSites()[0].ForwardedArguments.size() == 2 && + ClonedRecords.callSites()[0].ForwardedArguments[1].ArgumentNumber == 0 && + ClonedRecords.callSites()[0].CalleeTypeIDs == std::vector<std::uint64_t>({0x11, 0x22}) && + ClonedRecords.calledGlobals().size() == 1 && + ClonedRecords.calledGlobals()[0].CallInstruction == InstructionID->second && + ClonedRecords.calledGlobals()[0].Callee == "callee" && + ClonedRecords.calledGlobals()[0].Flags == 5 && + ClonedRecords.prefetchTargets().size() == 1 && + ClonedRecords.prefetchTargets()[0].Block == + rcc::MachineBasicBlockProfileID{.BaseID = 19, .CloneID = 2} && + ClonedRecords.prefetchTargets()[0].CallsiteIndex == 8 && + ClonedRecords.entryValues().size() == 1 && ClonedRecords.entryValues()[0] == EntryValue, + "deep clone must preserve records and remap instruction StableIDs"); +} + +void testCrossReferenceFailures(TestContext &Context) { + auto Target = createTarget(); + if (!Target) + return; + + { + auto Function = createFunction(Context, 10, "noncall"); + if (Function == nullptr) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + rcc::MachineInstr *Nop = appendInstruction(Context, *Function, *Block, NopOpcode); + if (Nop == nullptr) + return; + Context.expect(Function->records() + .addCallSite({ + .CallInstruction = Nop->stableID(), + .ForwardedArguments = {}, + .CalleeTypeIDs = {}, + }) + .has_value() && + !rcc::MachineVerifier::verify(*Function, *Target), + "call-site records must reference instructions described as calls"); + } + + { + auto Function = createFunction(Context, 11, "detached"); + if (Function == nullptr) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + auto Detached = Function->createInstruction(rcc::Opcode::fromValue(CallOpcode)); + Context.expect(Detached.has_value() && + Function->records() + .addCalledGlobal({ + .CallInstruction = (*Detached)->stableID(), + .Callee = "callee", + }) + .has_value() && + !rcc::MachineVerifier::verify(*Function, *Target), + "function records must not reference detached instructions"); + } + + { + auto Function = createFunction(Context, 12, "unknown-register"); + if (Function == nullptr) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + rcc::MachineInstr *Call = appendInstruction(Context, *Function, *Block, CallOpcode); + if (Call == nullptr) + return; + Context.expect(Function->records() + .addCallSite({ + .CallInstruction = Call->stableID(), + .ForwardedArguments = {{.Reg = phys(999), .ArgumentNumber = 0}}, + .CalleeTypeIDs = {}, + }) + .has_value() && + !rcc::MachineVerifier::verify(*Function, *Target), + "call-site records must reject target-unknown physical registers"); + } + + { + auto Function = createFunction(Context, 13, "dangling-prefetch-target"); + if (Function == nullptr) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + Context.expect(Function->records() + .addPrefetchTarget({ + .Block = {.BaseID = 1, .CloneID = 0}, + .CallsiteIndex = 0, + }) + .has_value() && + rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "prefetch targets may reference a bb_id without a machine block"); + } + + { + auto Function = createFunction(Context, 14, "duplicate-bbid"); + if (Function == nullptr) + return; + rcc::MachineBasicBlock *First = appendBlock(Context, *Function); + rcc::MachineBasicBlock *Second = appendBlock(Context, *Function); + if (First == nullptr || Second == nullptr) + return; + First->attributes().setProfileID({.BaseID = 3, .CloneID = 1}); + Second->attributes().setProfileID({.BaseID = 3, .CloneID = 1}); + Context.expect(!rcc::MachineVerifier::verify(*Function, *Target), + "linked block bb_id values must be unique"); + } + + { + auto Function = createFunction(Context, 13, "entry-value-register"); + if (Function == nullptr) + return; + Context.expect(Function->records() + .addEntryValue({ + .Reg = phys(999), + .DebugInfo = + { + .Variable = rcc::MachineMetadataReference::embeddedIR(1), + .Expression = std::nullopt, + .Location = std::nullopt, + }, + }) + .has_value() && + !rcc::MachineVerifier::verify(*Function, *Target), + "entry-value records must reject target-unknown physical registers"); + } +} + +} // namespace + +int main() { + TestContext Context; + testValueValidation(Context); + testVerifierAndClone(Context); + testCrossReferenceFailures(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineIRAttributesTest.cpp b/backend/unittests/CodeGen/MachineIRAttributesTest.cpp new file mode 100644 index 0000000..5619210 --- /dev/null +++ b/backend/unittests/CodeGen/MachineIRAttributesTest.cpp @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineBasicBlockAttributes.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineFunctionAttributes.h" +#include "rcc/CodeGen/MachineFunctionCloner.h" +#include "rcc/CodeGen/MachineIRTypes.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineVerifier.h" +#include "rcc/CodeGen/TargetDescription.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t NopOpcode = 1; + +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::Expected<rcc::TargetDescription> createTarget() { + rcc::InstructionDescriptor Nop; + Nop.InstructionOpcode = rcc::Opcode::fromValue(NopOpcode); + Nop.Name = "TEST_NOP"; + Nop.Flags = {rcc::InstructionFlag::ProducesCode}; + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Nop)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> createFunction(TestContext &Context, + std::uint64_t StableID) { + auto Function = + rcc::MachineFunction::create("attributes", rcc::MachineFunctionStableID::fromValue(StableID)); + Context.expect(Function.has_value(), "machine function creation must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +void testAttributeValueTypes(TestContext &Context) { + rcc::MachineFunctionAttributes FunctionAttributes; + FunctionAttributes.set(rcc::MachineFunctionAttribute::ExposesReturnsTwice); + FunctionAttributes.set(rcc::MachineFunctionAttribute::TracksDebugUserValues); + Context.expect(FunctionAttributes.has(rcc::MachineFunctionAttribute::ExposesReturnsTwice) && + FunctionAttributes.has(rcc::MachineFunctionAttribute::TracksDebugUserValues) && + rcc::getMachineFunctionAttributeName( + rcc::MachineFunctionAttribute::TracksDebugUserValues) == + "TracksDebugUserValues", + "function attribute flags and names must be stable"); + + auto DecodedFunctionAttributes = + rcc::MachineFunctionAttributes::fromRawFlags(FunctionAttributes.rawFlags()); + Context.expect(DecodedFunctionAttributes.has_value() && + DecodedFunctionAttributes->rawFlags() == FunctionAttributes.rawFlags(), + "function attribute raw flags must round trip"); + Context.expect(!rcc::MachineFunctionAttributes::fromRawFlags(std::uint16_t{1} << 15U), + "unknown function attribute bits must be rejected"); + + rcc::MachineBasicBlockAttributes BlockAttributes; + Context.expect(!BlockAttributes.setName("") && !BlockAttributes.setIRBlockName(""), + "empty block and IR block names must be rejected"); + Context.expect(BlockAttributes.setName("entry").has_value() && + BlockAttributes.setIRBlockName("entry").has_value(), + "decoded block and IR block names must be accepted"); + BlockAttributes.set(rcc::MachineBasicBlockAttribute::IRBlockAddressTaken); + BlockAttributes.setCallFrameSize(0); + Context.expect(BlockAttributes.verify().has_value() && BlockAttributes.callFrameSize() == 0, + "zero call-frame size must be the semantic default"); + BlockAttributes.setSectionID(rcc::MachineBasicBlockSectionID::exception()); + BlockAttributes.setProfileID({.BaseID = 7, .CloneID = 2}); + Context.expect( + BlockAttributes.sectionID().kind() == rcc::MachineBasicBlockSectionKind::Exception && + BlockAttributes.sectionID().number() == 0 && + BlockAttributes.profileID() == rcc::MachineBasicBlockProfileID{.BaseID = 7, .CloneID = 2}, + "special section and fixed profile identities must remain structured"); + Context.expect(!rcc::MachineBasicBlockAttributes::fromRawFlags(std::uint8_t{1} << 7U), + "unknown block attribute bits must be rejected"); + + rcc::MachineInstrFlags Flags{ + rcc::MachineInstrFlag::NoConvergent, rcc::MachineInstrFlag::NonNeg, + rcc::MachineInstrFlag::Disjoint, rcc::MachineInstrFlag::NoUSWrap, + rcc::MachineInstrFlag::SameSign, rcc::MachineInstrFlag::InBounds, + }; + Context.expect( + Flags.has(rcc::MachineInstrFlag::NoConvergent) && Flags.has(rcc::MachineInstrFlag::NonNeg) && + Flags.has(rcc::MachineInstrFlag::Disjoint) && + Flags.has(rcc::MachineInstrFlag::NoUSWrap) && + Flags.has(rcc::MachineInstrFlag::SameSign) && Flags.has(rcc::MachineInstrFlag::InBounds), + "all fixed MIR instruction flags must have independent storage"); +} + +void testVerifierAndClone(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 1); + if (!Target || Function == nullptr) + return; + + auto FunctionAlignment = rcc::Alignment::create(32); + auto BlockAlignment = rcc::Alignment::create(16); + Context.expect(FunctionAlignment.has_value() && BlockAlignment.has_value(), + "power-of-two alignments must be accepted"); + if (!FunctionAlignment || !BlockAlignment) + return; + + Function->attributes().set(rcc::MachineFunctionAttribute::ExposesReturnsTwice); + Function->attributes().set(rcc::MachineFunctionAttribute::HasEHScopes); + Function->attributes().set(rcc::MachineFunctionAttribute::DebugInstrRef); + Function->attributes().setAlignment(*FunctionAlignment); + + auto BlockResult = Function->createBasicBlock(); + Context.expect(BlockResult.has_value(), "machine basic block creation must succeed"); + if (!BlockResult) + return; + rcc::MachineBasicBlock *Block = *BlockResult; + Context.expect(Function->appendBlock(*Block).has_value(), + "machine basic block linking must succeed"); + Context.expect(Block->attributes().setName("entry").has_value(), + "machine basic block name must be stored"); + Block->attributes().set(rcc::MachineBasicBlockAttribute::IRBlockAddressTaken); + Context.expect(!rcc::MachineVerifier::verify(*Function, *Target), + "IR-block-address-taken must require an IR block association"); + + Context.expect(Block->attributes().setIRBlockName("entry").has_value(), + "IR block association must be stored"); + Block->attributes().set(rcc::MachineBasicBlockAttribute::LandingPad); + Block->attributes().setAlignment(*BlockAlignment); + Block->attributes().setSectionID(rcc::MachineBasicBlockSectionID::numbered(3)); + Block->attributes().setProfileID({.BaseID = 11, .CloneID = 4}); + Block->attributes().setCallFrameSize(64); + + auto InstructionResult = Function->createInstruction(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(InstructionResult.has_value(), "machine instruction creation must succeed"); + if (!InstructionResult) + return; + rcc::MachineInstr *Instruction = *InstructionResult; + Instruction->setFlags({ + rcc::MachineInstrFlag::NoConvergent, + rcc::MachineInstrFlag::NonNeg, + rcc::MachineInstrFlag::Disjoint, + rcc::MachineInstrFlag::NoUSWrap, + rcc::MachineInstrFlag::SameSign, + rcc::MachineInstrFlag::InBounds, + }); + Context.expect(Block->appendInstruction(*Instruction).has_value(), + "machine instruction linking must succeed"); + Context.expect(rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "function and block MIR attributes must pass the unified verifier"); + + auto Clone = + rcc::cloneMachineFunction(*Function, *Target, + rcc::MachineFunctionCloneOptions{ + .NewStableID = rcc::MachineFunctionStableID::fromValue(2), + .NewName = std::string("attributes.clone"), + }); + Context.expect(Clone.has_value(), "attributed machine function clone must succeed"); + if (!Clone) + return; + + const rcc::MachineFunction &ClonedFunction = *Clone->ClonedFunction; + const rcc::MachineBasicBlock *ClonedBlock = ClonedFunction.firstBlock(); + const rcc::MachineInstr *ClonedInstruction = + ClonedBlock == nullptr ? nullptr : ClonedBlock->firstInstruction(); + Context.expect(ClonedFunction.attributes() == Function->attributes(), + "function MIR attributes must survive deep cloning"); + Context.expect(ClonedBlock != nullptr && ClonedBlock->attributes() == Block->attributes(), + "basic block MIR attributes must survive deep cloning"); + Context.expect(ClonedInstruction != nullptr && ClonedInstruction->flags() == Instruction->flags(), + "complete MIR instruction flags must survive deep cloning"); +} + +} // namespace + +int main() { + TestContext Context; + testAttributeValueTypes(Context); + testVerifierAndClone(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineIRBuilderTest.cpp b/backend/unittests/CodeGen/MachineIRBuilderTest.cpp new file mode 100644 index 0000000..17e9874 --- /dev/null +++ b/backend/unittests/CodeGen/MachineIRBuilderTest.cpp @@ -0,0 +1,556 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineIRBuilder.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/LowLevelType.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineConstantPool.h" +#include "rcc/CodeGen/MachineDebugInfo.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineJumpTableInfo.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" + +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <memory> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t CopyOpcode = 1; +constexpr std::uint32_t AddOpcode = 2; +constexpr std::uint32_t LoadOpcode = 3; +constexpr std::uint32_t NopOpcode = 4; +constexpr std::uint32_t BranchOpcode = 5; +constexpr std::uint32_t DynamicImplicitOpcode = 6; +constexpr std::uint32_t ConstantPoolOpcode = 7; +constexpr std::uint32_t JumpTableOpcode = 8; + +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::OperandDescriptor registerDescriptor(rcc::RegisterOperandRole Role) { + rcc::OperandDescriptor Descriptor; + Descriptor.Kinds = {rcc::MachineOperand::Kind::Register}; + Descriptor.RegisterRole = Role; + return Descriptor; +} + +[[nodiscard]] rcc::InstructionDescriptor instructionDescriptor(std::uint32_t OpcodeValue, + std::string Name) { + rcc::InstructionDescriptor Descriptor; + Descriptor.InstructionOpcode = rcc::Opcode::fromValue(OpcodeValue); + Descriptor.Name = std::move(Name); + Descriptor.Flags = {rcc::InstructionFlag::ProducesCode}; + return Descriptor; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + auto Copy = instructionDescriptor(CopyOpcode, "TEST_COPY"); + auto CopyDef = registerDescriptor(rcc::RegisterOperandRole::Def); + auto CopyUse = registerDescriptor(rcc::RegisterOperandRole::Use); + CopyDef.DefaultTiedOperand = 1; + CopyUse.DefaultTiedOperand = 0; + Copy.Operands.push_back(CopyDef); + Copy.Operands.push_back(CopyUse); + Copy.NumExplicitDefs = 1; + + auto Add = instructionDescriptor(AddOpcode, "TEST_ADD"); + Add.Operands.push_back(registerDescriptor(rcc::RegisterOperandRole::Def)); + Add.Operands.push_back(registerDescriptor(rcc::RegisterOperandRole::Use)); + rcc::OperandDescriptor RegisterOrImmediate; + RegisterOrImmediate.Kinds = { + rcc::MachineOperand::Kind::Register, + rcc::MachineOperand::Kind::Immediate, + }; + RegisterOrImmediate.RegisterRole = rcc::RegisterOperandRole::Use; + Add.Operands.push_back(RegisterOrImmediate); + Add.NumExplicitDefs = 1; + Add.ImplicitDefs.push_back( + rcc::ImplicitRegisterDescriptor{*rcc::PhysicalRegister::create(1), false, true}); + Add.ImplicitUses.push_back(rcc::ImplicitRegisterDescriptor{*rcc::PhysicalRegister::create(2)}); + + auto Load = instructionDescriptor(LoadOpcode, "TEST_LOAD"); + Load.Flags.set(rcc::InstructionFlag::MayLoad); + Load.RequiresMemoryOperands = true; + + auto Nop = instructionDescriptor(NopOpcode, "TEST_NOP"); + + auto Branch = instructionDescriptor(BranchOpcode, "TEST_BRANCH"); + rcc::OperandDescriptor Block; + Block.Kinds = {rcc::MachineOperand::Kind::BasicBlock}; + Branch.Operands.push_back(Block); + Branch.Flags.set(rcc::InstructionFlag::Branch); + Branch.Flags.set(rcc::InstructionFlag::Terminator); + + auto Dynamic = instructionDescriptor(DynamicImplicitOpcode, "TEST_DYNAMIC_IMPLICIT"); + Dynamic.AllowExtraImplicitOperands = true; + + auto ConstantPool = instructionDescriptor(ConstantPoolOpcode, "TEST_CONSTANT_POOL"); + rcc::OperandDescriptor ConstantPoolIndex; + ConstantPoolIndex.Kinds = {rcc::MachineOperand::Kind::ConstantPoolIndex}; + ConstantPool.Operands.push_back(ConstantPoolIndex); + + auto JumpTable = instructionDescriptor(JumpTableOpcode, "TEST_JUMP_TABLE"); + rcc::OperandDescriptor JumpTableIndex; + JumpTableIndex.Kinds = {rcc::MachineOperand::Kind::JumpTableIndex}; + JumpTable.Operands.push_back(JumpTableIndex); + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Copy)); + Descriptors.push_back(std::move(Add)); + Descriptors.push_back(std::move(Load)); + Descriptors.push_back(std::move(Nop)); + Descriptors.push_back(std::move(Branch)); + Descriptors.push_back(std::move(Dynamic)); + Descriptors.push_back(std::move(ConstantPool)); + Descriptors.push_back(std::move(JumpTable)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> +createFunction(TestContext &Context, std::string Name, std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "machine function creation must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineBasicBlock *createBlock(TestContext &Context, + rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + Context.expect(Block.has_value(), "machine basic block creation must succeed"); + if (!Block) + return nullptr; + Context.expect(Function.appendBlock(**Block).has_value(), + "machine basic block linking must succeed"); + return *Block; +} + +[[nodiscard]] rcc::MachineMemOperandDesc memoryDescription(rcc::MemoryAccessFlag Access) { + rcc::MachineMemOperandDesc Description; + Description.Accesses = {Access}; + Description.Size = *rcc::MemorySize::fixed(8); + Description.Align = *rcc::Alignment::create(8); + Description.Base = rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)); + return Description; +} + +[[nodiscard]] rcc::MachineInstr *buildNop(TestContext &Context, rcc::MachineIRBuilder &Builder) { + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(Pending.has_value(), "NOP builder creation must succeed"); + if (!Pending) + return nullptr; + auto Instruction = Pending->build(); + Context.expect(Instruction.has_value(), "NOP construction must succeed"); + if (!Instruction) + return nullptr; + return *Instruction; +} + +void testTransactionalBuild(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, "transactional", 1); + Context.expect(Target.has_value(), "builder target description must be valid"); + if (!Target || !Function) + return; + rcc::MachineBasicBlock *Block = createBlock(Context, *Function); + if (Block == nullptr) + return; + + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Destination = Function->registerInfo().createVirtualRegister(*Type); + auto Source = Function->registerInfo().createVirtualRegister(*Type); + Context.expect(Destination && Source, "builder virtual registers must be allocated"); + if (!Destination || !Source) + return; + + rcc::MachineIRBuilder Builder(*Function, *Target); + Context.expect(!Builder.buildInstr(rcc::Opcode::fromValue(CopyOpcode)), + "an unset insertion point must reject instruction construction"); + Context.expect(Function->allInstructions().empty(), + "rejected builder setup must not allocate an instruction"); + Context.expect(Builder.setInsertPoint(*Block).has_value(), + "a linked block must be a valid insertion point"); + Context.expect(!Builder.buildInstr(rcc::Opcode::fromValue(99)), + "an unregistered opcode must be rejected"); + + auto Incomplete = Builder.buildInstr(rcc::Opcode::fromValue(CopyOpcode)); + Context.expect(Incomplete.has_value() && Incomplete->addDef(*Destination).has_value(), + "incomplete instruction setup must accept its first operand"); + if (!Incomplete) + return; + Context.expect(!Incomplete->tieOperands(0, 1) && !Incomplete->setSourceLocation({"", 0, 0, 0}) && + !Incomplete->setDebugLocation({}) && + !Incomplete->setOrigin(rcc::OriginID::fromValue(0)), + "invalid pending metadata and tie indices must be rejected"); + Context.expect(!Incomplete->build(), "descriptor validation must reject a missing operand"); + Context.expect(Function->allInstructions().empty() && Block->instructionCount() == 0, + "failed validation must not allocate or publish an instruction"); + + auto Complete = Builder.buildInstr(rcc::Opcode::fromValue(CopyOpcode)); + Context.expect(Complete.has_value(), "complete instruction builder must be created"); + if (!Complete) + return; + Context.expect(Complete->addDef(*Destination).has_value() && + Complete->addUse(*Source).has_value(), + "complete instruction operands must be accepted"); + Context.expect(Complete->tieOperands(0, 1).has_value(), + "an explicit tie matching the descriptor default must be idempotent"); + Context.expect(Complete->setFlags({rcc::MachineInstrFlag::FrameSetup}).has_value(), + "valid instruction flags must be accepted"); + auto InlineDebugLocation = rcc::MachineDILocation::create({ + { + .Line = 11, + .Column = 2, + .Scope = rcc::MachineMetadataReference::embeddedIR(3), + .IsImplicitCode = false, + .AtomGroup = 0, + .AtomRank = 0, + }, + }); + Context.expect(InlineDebugLocation.has_value(), + "builder debug location setup must be structurally valid"); + if (!InlineDebugLocation) + return; + const rcc::MachineDebugLocation DebugLocation = + rcc::MachineDebugLocation::inlineLocation(*InlineDebugLocation); + Context.expect( + Complete->setSourceLocation(rcc::SourceLocation{"builder.mir", 7, 3, 1}).has_value() && + Complete->setDebugLocation(DebugLocation).has_value() && + Complete->setOrigin(rcc::OriginID::fromValue(19)).has_value(), + "valid parser source, debug location, and origin metadata must be accepted"); + auto Built = Complete->build(); + Context.expect(Built.has_value(), "valid pending instruction must build"); + if (!Built) + return; + + rcc::MachineInstr *Instruction = *Built; + Context.expect(Instruction->stableID().value() == 1, + "failed builds must not consume instruction StableIDs"); + Context.expect(Instruction->parent() == Block && Block->firstInstruction() == Instruction, + "successful build must publish at the insertion point"); + Context.expect(Instruction->operandAt(0)->tiedOperandIndex() == 1 && + Instruction->operandAt(1)->tiedOperandIndex() == 0, + "descriptor-default tie must be materialized bidirectionally"); + Context.expect(Instruction->flags().has(rcc::MachineInstrFlag::FrameSetup) && + Instruction->sourceLocation() == rcc::SourceLocation{"builder.mir", 7, 3, 1} && + Instruction->debugLocation() == DebugLocation && + Instruction->origin() == rcc::OriginID::fromValue(19), + "builder metadata must be preserved on the published instruction"); + Context.expect(Target->validateInstruction(*Instruction, Function->registerInfo()).has_value(), + "builder output must satisfy the shared instruction descriptor"); + Context.expect(!Complete->build(), "a successful pending builder must be single-use"); + Context.expect(!Complete->setFlags({}) && !Complete->clearSourceLocation() && + !Complete->clearDebugLocation() && !Complete->clearOrigin(), + "all pending-builder mutators must reject use after publication"); + Context.expect(Function->registerInfo().verifyDefUseChains().has_value(), + "published builder operands must enter exact def-use chains"); + + auto MoveSource = Builder.buildInstr(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(MoveSource.has_value(), "movable pending builder must be created"); + if (!MoveSource) + return; + rcc::MachineInstrBuilder MoveDestination(std::move(*MoveSource)); + Context.expect(!MoveSource->build(), "a moved-from pending builder must be consumed"); + auto MovedResult = MoveDestination.build(); + Context.expect(MovedResult && (*MovedResult)->stableID().value() == 2, + "a moved-to pending builder must retain its construction state"); +} + +void testInsertionOrdering(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, "ordering", 2); + if (!Target || !Function) + return; + rcc::MachineBasicBlock *Block = createBlock(Context, *Function); + if (Block == nullptr) + return; + + rcc::MachineIRBuilder Builder(*Function, *Target); + Context.expect(Builder.setInsertPoint(*Block).has_value(), + "ordering builder insertion point must be valid"); + rcc::MachineInstr *First = buildNop(Context, Builder); + rcc::MachineInstr *Tail = buildNop(Context, Builder); + if (First == nullptr || Tail == nullptr) + return; + + Context.expect(Builder.setInsertPointAfter(*First).has_value(), + "after-instruction insertion point must be accepted"); + rcc::MachineInstr *MiddleA = buildNop(Context, Builder); + rcc::MachineInstr *MiddleB = buildNop(Context, Builder); + Context.expect(MiddleA != nullptr && MiddleB != nullptr && First->next() == MiddleA && + MiddleA->next() == MiddleB && MiddleB->next() == Tail, + "repeated insertion after a cursor must preserve build order"); + + Context.expect(Builder.setInsertPointBefore(*First).has_value(), + "before-instruction insertion point must be accepted"); + rcc::MachineInstr *PrefixA = buildNop(Context, Builder); + rcc::MachineInstr *PrefixB = buildNop(Context, Builder); + Context.expect(PrefixA != nullptr && PrefixB != nullptr && Block->firstInstruction() == PrefixA && + PrefixA->next() == PrefixB && PrefixB->next() == First, + "repeated insertion before a cursor must preserve build order"); +} + +void testImplicitAndMemoryValidation(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, "implicit_memory", 3); + auto OtherFunction = createFunction(Context, "foreign", 4); + if (!Target || !Function || !OtherFunction) + return; + rcc::MachineBasicBlock *Block = createBlock(Context, *Function); + rcc::MachineBasicBlock *OtherBlock = createBlock(Context, *OtherFunction); + if (Block == nullptr || OtherBlock == nullptr) + return; + + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Destination = Function->registerInfo().createVirtualRegister(*Type); + auto Source = Function->registerInfo().createVirtualRegister(*Type); + Context.expect(Destination && Source, "implicit test virtual registers must be allocated"); + if (!Destination || !Source) + return; + + rcc::MachineIRBuilder Builder(*Function, *Target); + Context.expect(Builder.setInsertPoint(*Block).has_value(), + "implicit test insertion point must be accepted"); + + auto Add = Builder.buildInstr(rcc::Opcode::fromValue(AddOpcode)); + Context.expect(Add && Add->addDef(*Destination).has_value() && Add->addUse(*Source).has_value() && + Add->addImmediate(7).has_value(), + "ADD operands must be accepted by the pending builder"); + if (!Add) + return; + auto BuiltAdd = Add->build(); + Context.expect(BuiltAdd.has_value(), "ADD with default implicits must build"); + if (!BuiltAdd) + return; + Context.expect( + (*BuiltAdd)->operandCount() == 5 && (*BuiltAdd)->operandAt(3)->isImplicit() && + (*BuiltAdd)->operandAt(3)->registerFlags().has(rcc::RegisterOperandFlag::Renamable) && + (*BuiltAdd)->operandAt(4)->isImplicit(), + "default implicit defs and uses must follow explicit operands"); + Context.expect(Target->validateInstruction(**BuiltAdd, Function->registerInfo()).has_value(), + "default implicit builder output must pass shared validation"); + + const std::size_t BeforeRejectedImplicit = Function->allInstructions().size(); + auto ExtraImplicit = Builder.buildInstr(rcc::Opcode::fromValue(AddOpcode)); + Context.expect( + ExtraImplicit && ExtraImplicit->addDef(*Destination).has_value() && + ExtraImplicit->addUse(*Source).has_value() && + ExtraImplicit->addImmediate(1).has_value() && + ExtraImplicit + ->addImplicitUse(rcc::Register::fromPhysical(*rcc::PhysicalRegister::create(3))) + .has_value(), + "dynamic implicit negative test must be assembled"); + Context.expect(ExtraImplicit && !ExtraImplicit->build() && + Function->allInstructions().size() == BeforeRejectedImplicit, + "descriptor-rejected dynamic implicits must not allocate instructions"); + + auto Dynamic = Builder.buildInstr(rcc::Opcode::fromValue(DynamicImplicitOpcode)); + Context.expect( + Dynamic && + Dynamic->addImplicitUse(rcc::Register::fromPhysical(*rcc::PhysicalRegister::create(3))) + .has_value(), + "descriptor-permitted dynamic implicit operand must be accepted"); + if (!Dynamic) + return; + auto BuiltDynamic = Dynamic->build(); + Context.expect( + BuiltDynamic.has_value() && + Target->validateInstruction(**BuiltDynamic, Function->registerInfo()).has_value(), + "dynamic implicit builder output must validate"); + + const std::size_t BeforeMissingMemory = Function->allInstructions().size(); + auto MissingMemory = Builder.buildInstr(rcc::Opcode::fromValue(LoadOpcode)); + Context.expect(MissingMemory && !MissingMemory->build() && + Function->allInstructions().size() == BeforeMissingMemory, + "missing required MMO must fail before instruction allocation"); + + rcc::StackObjectDesc StackSlot; + StackSlot.Size = 8; + StackSlot.Align = *rcc::Alignment::create(8); + auto FrameIndex = Function->frameInfo().createObject(StackSlot); + auto ForeignFrameIndex = OtherFunction->frameInfo().createObject(StackSlot); + Context.expect(FrameIndex && ForeignFrameIndex, + "memory validation stack objects must be created"); + if (!FrameIndex || !ForeignFrameIndex) + return; + + auto LoadMemory = Function->createMemOperand(memoryDescription(rcc::MemoryAccessFlag::Load)); + auto StoreMemory = Function->createMemOperand(memoryDescription(rcc::MemoryAccessFlag::Store)); + auto ForeignMemory = + OtherFunction->createMemOperand(memoryDescription(rcc::MemoryAccessFlag::Load)); + Context.expect(LoadMemory && StoreMemory && ForeignMemory, + "memory validation operands must be created"); + if (!LoadMemory || !StoreMemory || !ForeignMemory) + return; + + const std::size_t BeforeUnknownTableReferences = Function->allInstructions().size(); + auto UnknownConstant = Builder.buildInstr(rcc::Opcode::fromValue(ConstantPoolOpcode)); + auto UnknownJumpTable = Builder.buildInstr(rcc::Opcode::fromValue(JumpTableOpcode)); + Context.expect( + UnknownConstant && + UnknownConstant->addConstantPool(rcc::ConstantPoolIndex::fromValue(99)).has_value() && + !UnknownConstant->build() && UnknownJumpTable && + UnknownJumpTable->addJumpTable(rcc::JumpTableIndex::fromValue(99)).has_value() && + !UnknownJumpTable->build() && + Function->allInstructions().size() == BeforeUnknownTableReferences, + "builder must reject unknown constant-pool and jump-table indices before allocation"); + + rcc::MachineConstantPoolEntryDesc ConstantDescription; + auto Integer = rcc::APInt::fromUnsigned(64, 0x2a2a2a2a2a2a2a2aULL); + ConstantDescription.Value = std::move(*rcc::GenericConstantData::integer(std::move(*Integer))); + ConstantDescription.Size = 8; + ConstantDescription.Align = *rcc::Alignment::create(8); + auto ConstantIndex = Function->constantPool().addEntry(std::move(ConstantDescription)); + rcc::MachineJumpTableDesc JumpDescription; + JumpDescription.Targets = {Block}; + auto JumpIndex = Function->jumpTableInfo().createTable(std::move(JumpDescription)); + Context.expect(ConstantIndex && JumpIndex, "builder table references must have owned entries"); + if (!ConstantIndex || !JumpIndex) + return; + + auto ConstantInstruction = Builder.buildInstr(rcc::Opcode::fromValue(ConstantPoolOpcode)); + auto JumpInstruction = Builder.buildInstr(rcc::Opcode::fromValue(JumpTableOpcode)); + Context.expect( + ConstantInstruction && ConstantInstruction->addConstantPool(*ConstantIndex, 4).has_value() && + ConstantInstruction->build() && JumpInstruction && + JumpInstruction->addJumpTable(*JumpIndex).has_value() && JumpInstruction->build(), + "builder must publish valid owned table references"); + + auto ConstantMemoryDescription = memoryDescription(rcc::MemoryAccessFlag::Load); + ConstantMemoryDescription.Base = rcc::MemoryBase::constantPool(*ConstantIndex); + auto JumpMemoryDescription = memoryDescription(rcc::MemoryAccessFlag::Load); + JumpMemoryDescription.Base = rcc::MemoryBase::jumpTable(*JumpIndex); + auto ConstantMemory = Function->createMemOperand(std::move(ConstantMemoryDescription)); + auto JumpMemory = Function->createMemOperand(std::move(JumpMemoryDescription)); + auto ConstantLoad = Builder.buildInstr(rcc::Opcode::fromValue(LoadOpcode)); + auto JumpLoad = Builder.buildInstr(rcc::Opcode::fromValue(LoadOpcode)); + Context.expect(ConstantMemory && JumpMemory && ConstantLoad && + ConstantLoad->addMemOperand(**ConstantMemory).has_value() && + ConstantLoad->build() && JumpLoad && + JumpLoad->addMemOperand(**JumpMemory).has_value() && JumpLoad->build(), + "builder must validate constant-pool and jump-table MMO bases"); + + auto Load = Builder.buildInstr(rcc::Opcode::fromValue(LoadOpcode)); + Context.expect(Load && Load->addMemOperand(**LoadMemory).has_value(), + "function-owned load MMO must be accepted"); + if (!Load) + return; + auto BuiltLoad = Load->build(); + Context.expect(BuiltLoad.has_value() && + Target->validateInstruction(**BuiltLoad, Function->registerInfo()).has_value(), + "matching load MMO must build and validate"); + + const std::size_t BeforeStoreMismatch = Function->allInstructions().size(); + auto StoreMismatch = Builder.buildInstr(rcc::Opcode::fromValue(LoadOpcode)); + Context.expect(StoreMismatch && StoreMismatch->addMemOperand(**StoreMemory).has_value() && + !StoreMismatch->build() && + Function->allInstructions().size() == BeforeStoreMismatch, + "MMO access mismatch must fail before instruction allocation"); + auto ForeignMMO = Builder.buildInstr(rcc::Opcode::fromValue(LoadOpcode)); + Context.expect(ForeignMMO && !ForeignMMO->addMemOperand(**ForeignMemory), + "foreign-function MMO must be rejected immediately"); + + const std::size_t BeforeForeignBlock = Function->allInstructions().size(); + auto ForeignBranch = Builder.buildInstr(rcc::Opcode::fromValue(BranchOpcode)); + Context.expect(ForeignBranch && ForeignBranch->addBasicBlock(*OtherBlock).has_value() && + !ForeignBranch->build() && + Function->allInstructions().size() == BeforeForeignBlock, + "foreign basic block operand must fail before instruction allocation"); +} + +void testStaleCursorsAndOwnedOperands(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, "ownership", 5); + if (!Target || !Function) + return; + rcc::MachineBasicBlock *Block = createBlock(Context, *Function); + if (Block == nullptr) + return; + + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Destination = Function->registerInfo().createVirtualRegister(*Type); + auto Source = Function->registerInfo().createVirtualRegister(*Type); + if (!Destination || !Source) + return; + + rcc::MachineIRBuilder Builder(*Function, *Target); + Context.expect(Builder.setInsertPoint(*Block).has_value(), + "ownership test insertion point must be valid"); + auto Copy = Builder.buildInstr(rcc::Opcode::fromValue(CopyOpcode)); + Context.expect(Copy && Copy->addDef(*Destination).has_value() && + Copy->addUse(*Source).has_value(), + "ownership source instruction must be assembled"); + if (!Copy) + return; + auto BuiltCopy = Copy->build(); + if (!BuiltCopy) + return; + + auto DetachedTarget = Function->createInstruction(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(DetachedTarget.has_value(), "detached target instruction must be allocated"); + if (!DetachedTarget) + return; + rcc::MachineOperand *OwnedOperand = (*BuiltCopy)->operandAt(0); + Context.expect( + OwnedOperand != nullptr && !(*DetachedTarget)->addOperand(std::move(*OwnedOperand)) && + OwnedOperand->parent() == *BuiltCopy && OwnedOperand->registerValue() == *Destination, + "controlled instruction API must reject an owned operand without moving it"); + + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(Pending && OwnedOperand != nullptr && + !Pending->addOperand(std::move(*OwnedOperand)) && + OwnedOperand->parent() == *BuiltCopy, + "controlled builder API must reject an owned operand without moving it"); + + rcc::MachineInstr *Cursor = buildNop(Context, Builder); + if (Cursor == nullptr) + return; + Context.expect(Builder.setInsertPointBefore(*Cursor).has_value(), "cursor setup must succeed"); + auto Stale = Builder.buildInstr(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(Stale.has_value(), "pending stale-cursor test must be created"); + const std::size_t BeforeStaleBuild = Function->allInstructions().size(); + Context.expect(Block->removeInstruction(*Cursor).has_value(), "cursor removal must succeed"); + Context.expect(Stale && !Stale->build() && Function->allInstructions().size() == BeforeStaleBuild, + "a stale cursor must fail before instruction allocation"); +} + +} // namespace + +int main() { + TestContext Context; + testTransactionalBuild(Context); + testInsertionOrdering(Context); + testImplicitAndMemoryValidation(Context); + testStaleCursorsAndOwnedOperands(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineIRCoreTest.cpp b/backend/unittests/CodeGen/MachineIRCoreTest.cpp new file mode 100644 index 0000000..875eeb8 --- /dev/null +++ b/backend/unittests/CodeGen/MachineIRCoreTest.cpp @@ -0,0 +1,484 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/LowLevelType.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineCFGUpdater.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/RegisterMask.h" + +#include <array> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <memory> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> +createFunction(TestContext &Context, std::string Name, std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "valid machine function construction must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineBasicBlock *createLinkedBlock(TestContext &Context, + rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + Context.expect(Block.has_value(), "basic block allocation must succeed"); + if (!Block) + return nullptr; + auto LinkResult = Function.appendBlock(**Block); + Context.expect(LinkResult.has_value(), "basic block insertion must succeed"); + if (!LinkResult) + return nullptr; + return *Block; +} + +[[nodiscard]] bool addRegisterOperand(TestContext &Context, rcc::MachineInstr &Instruction, + rcc::Register Reg, rcc::RegisterOperandFlags Flags) { + auto Operand = rcc::MachineOperand::registerOperand(Reg, Flags); + Context.expect(Operand.has_value(), "valid register operand construction must succeed"); + if (!Operand) + return false; + auto AddResult = Instruction.addOperand(std::move(*Operand)); + Context.expect(AddResult.has_value(), "register operand insertion must succeed"); + return AddResult.has_value(); +} + +[[nodiscard]] std::size_t countOperands(rcc::RegOperandRange Range) { + std::size_t Count = 0; + for ([[maybe_unused]] const rcc::MachineOperand &Operand : Range) + ++Count; + return Count; +} + +rcc::MachineMemOperandDesc makeLoadDescription() { + rcc::MachineMemOperandDesc Description; + Description.Accesses = rcc::MemoryAccessFlags{ + rcc::MemoryAccessFlag::Load, + rcc::MemoryAccessFlag::Dereferenceable, + }; + Description.Size = *rcc::MemorySize::fixed(8); + Description.Align = *rcc::Alignment::create(8); + Description.Base = rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)); + return Description; +} + +void testOperandKinds(TestContext &Context) { + auto Function = createFunction(Context, "operands", 1); + if (!Function) + return; + auto Block = Function->createBasicBlock(); + Context.expect(Block.has_value(), "operand test block allocation must succeed"); + if (!Block) + return; + + const std::array<std::uint64_t, 1> HalfWords{0x3c00}; + auto HalfBits = rcc::APFloat::create(rcc::FloatSemantics::IEEEHalf, HalfWords); + Context.expect(HalfBits.has_value(), "well-sized floating immediate must be accepted"); + if (!HalfBits) + return; + Context.expect(HalfBits->bitWidth() == 16 && HalfBits->words()[0] == 0x3c00, + "floating immediate must preserve semantics and raw bits"); + + const std::array<std::uint64_t, 1> OverflowHalfWords{0x10000}; + Context.expect(!rcc::APFloat::create(rcc::FloatSemantics::IEEEHalf, OverflowHalfWords), + "floating immediate bits above semantics width must be rejected"); + const std::array<std::uint64_t, 2> TooManyHalfWords{0, 0}; + Context.expect(!rcc::APFloat::create(rcc::FloatSemantics::IEEEHalf, TooManyHalfWords), + "floating immediate word-count mismatch must be rejected"); + + auto Virtual = Function->registerInfo().createVirtualRegister( + *rcc::LowLevelType::makeCategoryNeutralScalar(32)); + Context.expect(Virtual.has_value(), "operand test virtual register must be created"); + if (!Virtual) + return; + + auto RegOperand = rcc::MachineOperand::registerOperand( + *Virtual, rcc::RegisterOperandFlags{rcc::RegisterOperandFlag::Use}); + Context.expect(RegOperand && RegOperand->kind() == rcc::MachineOperand::Kind::Register, + "register operand must preserve its kind"); + Context.expect(!rcc::MachineOperand::registerOperand(*Virtual, {}), + "register operand without def/use role must be rejected"); + Context.expect(!rcc::MachineOperand::registerOperand( + *Virtual, rcc::RegisterOperandFlags{rcc::RegisterOperandFlag::Define, + rcc::RegisterOperandFlag::Kill}), + "kill flag on register definition must be rejected"); + + auto Immediate = rcc::MachineOperand::immediate(-9, 17); + Context.expect(Immediate.kind() == rcc::MachineOperand::Kind::Immediate && + *Immediate.getIf<std::int64_t>() == -9 && Immediate.targetFlags() == 17, + "immediate operand must preserve value and target flags"); + + auto Floating = rcc::MachineOperand::floatingImmediate(*HalfBits); + Context.expect(Floating.kind() == rcc::MachineOperand::Kind::FloatingImmediate, + "floating operand must preserve its kind"); + auto BlockRef = rcc::MachineOperand::basicBlock(**Block); + Context.expect(BlockRef.getIf<rcc::BasicBlockOperand>()->Block == *Block, + "basic block operand must preserve its target"); + auto Frame = rcc::MachineOperand::frameIndex(rcc::FrameIndex::fromValue(4), -8); + Context.expect(Frame.getIf<rcc::FrameIndexOperand>()->Offset == -8, + "frame index operand must preserve its offset"); + auto Constant = rcc::MachineOperand::constantPool(rcc::ConstantPoolIndex::fromValue(2), 3); + Context.expect(Constant.kind() == rcc::MachineOperand::Kind::ConstantPoolIndex, + "constant pool operand must preserve its kind"); + auto Jump = rcc::MachineOperand::jumpTable(rcc::JumpTableIndex::fromValue(5)); + Context.expect(Jump.kind() == rcc::MachineOperand::Kind::JumpTableIndex, + "jump table operand must preserve its kind"); + + auto Global = rcc::MachineOperand::globalSymbol("global", 12); + auto External = rcc::MachineOperand::externalSymbol("external", -4); + auto BlockAddress = rcc::MachineOperand::blockAddress("function", "block", 2); + Context.expect(Global && External && BlockAddress, "valid symbol operands must be accepted"); + Context.expect(!rcc::MachineOperand::globalSymbol(""), + "empty global symbol operand must be rejected"); + Context.expect(!rcc::MachineOperand::blockAddress("", "block"), + "incomplete block address operand must be rejected"); + + auto Mask = std::make_shared<const rcc::RegisterMask>(std::vector<std::uint64_t>{0xffff}); + auto MaskOperand = rcc::MachineOperand::registerMask(Mask); + Context.expect(MaskOperand && MaskOperand->kind() == rcc::MachineOperand::Kind::RegisterMask, + "register mask operand must preserve shared immutable mask"); + Context.expect(!rcc::MachineOperand::registerMask(nullptr), + "null register mask operand must be rejected"); + + auto MetadataReference = + rcc::MachineMetadataValue::reference(rcc::MachineMetadataReference::embeddedIR(0)); + Context.expect(MetadataReference.has_value(), + "a valid scoped metadata reference must form an operand value"); + if (!MetadataReference) + return; + auto Metadata = rcc::MachineOperand::metadata(std::move(*MetadataReference)); + Context.expect(Metadata && Metadata->kind() == rcc::MachineOperand::Kind::Metadata && + Metadata->getIf<rcc::MetadataOperand>()->Value.reference() != nullptr, + "metadata operand must preserve its scoped semantic value"); + Context.expect(!rcc::MachineOperand::metadata({}), + "an unrepresented metadata operand value must be rejected"); + + auto Target = rcc::MachineOperand::targetIndex(rcc::TargetIndex::fromValue(8), 9); + Context.expect(Target.getIf<rcc::TargetIndexOperand>()->Offset == 9, + "target index operand must preserve its offset"); + auto MCSymbol = rcc::MachineOperand::mcSymbol(".Ltmp0", 1); + Context.expect(MCSymbol && MCSymbol->kind() == rcc::MachineOperand::Kind::MCSymbol, + "MC symbol precursor must preserve its kind"); + + const auto Clone = Immediate.cloneDetached(); + Context.expect(Clone.parent() == nullptr && + *Clone.getIf<std::int64_t>() == *Immediate.getIf<std::int64_t>(), + "detached operand clone must preserve payload without ownership"); + Context.expect(!Immediate.setRegister(*Virtual), + "register mutation on non-register operand must be rejected"); +} + +void testRegisterInfoAndOperandMovement(TestContext &Context) { + auto Function = createFunction(Context, "def_use", 2); + if (!Function) + return; + rcc::MachineBasicBlock *Block = createLinkedBlock(Context, *Function); + if (Block == nullptr) + return; + + auto I32 = rcc::LowLevelType::makeCategoryNeutralScalar(32); + Context.expect(I32.has_value(), "i32 construction for MRI test must succeed"); + if (!I32) + return; + auto V0 = Function->registerInfo().createVirtualRegister(*I32); + auto V1 = Function->registerInfo().createVirtualRegister(*I32); + Context.expect(V0 && V1, "MRI must allocate monotonic virtual registers"); + if (!V0 || !V1) + return; + Context.expect(V0->virtualIndex() == 0 && V1->virtualIndex() == 1, + "MRI virtual register indices must start at zero and increase"); + + auto PhysReg = rcc::PhysicalRegister::create(7); + Context.expect(PhysReg.has_value(), "MRI test physical register must be valid"); + if (!PhysReg) + return; + const rcc::Register Physical = rcc::Register::fromPhysical(*PhysReg); + + auto Instruction = Function->createInstruction(rcc::Opcode::fromValue(1)); + Context.expect(Instruction.has_value(), "machine instruction allocation must succeed"); + if (!Instruction) + return; + rcc::MachineInstr *Instr = *Instruction; + + if (!addRegisterOperand(Context, *Instr, *V0, + rcc::RegisterOperandFlags{rcc::RegisterOperandFlag::Define}) || + !addRegisterOperand(Context, *Instr, *V0, + rcc::RegisterOperandFlags{rcc::RegisterOperandFlag::Use}) || + !addRegisterOperand(Context, *Instr, *V0, + rcc::RegisterOperandFlags{rcc::RegisterOperandFlag::Use, + rcc::RegisterOperandFlag::Debug}) || + !addRegisterOperand(Context, *Instr, Physical, + rcc::RegisterOperandFlags{rcc::RegisterOperandFlag::Use})) { + return; + } + + Context.expect(Function->registerInfo().useEmpty(*V0), + "detached instruction operands must not enter def-use chains"); + auto LinkInstruction = Block->appendInstruction(*Instr); + Context.expect(LinkInstruction.has_value(), "instruction insertion must succeed"); + if (!LinkInstruction) + return; + + Context.expect(countOperands(Function->registerInfo().regOperands(*V0)) == 3, + "def-use chain must include definition, use, and debug use"); + Context.expect(countOperands(Function->registerInfo().useOperands(*V0)) == 2, + "use iterator must include ordinary and debug uses"); + Context.expect(countOperands(Function->registerInfo().useOperands(*V0, false)) == 1, + "nodbg use iterator must filter debug operands"); + Context.expect(Function->registerInfo().getUniqueVRegDef(*V0) == Instr->operandAt(0), + "unique virtual register definition must be discoverable"); + Context.expect(countOperands(Function->registerInfo().regOperands(Physical)) == 1, + "physical registers must participate in exact def-use chains"); + + rcc::MachineOperand *const OldOperandAddress = Instr->operandAt(0); + Instr->reserveOperands(128); + Context.expect(Instr->operandAt(0) != nullptr && Instr->operandAt(0)->parent() == Instr, + "operand relocation must preserve instruction ownership"); + Context.expect(Function->registerInfo().verifyDefUseChains().has_value(), + "operand relocation must repair intrusive def-use links"); + Context.expect(OldOperandAddress != Instr->operandAt(0) || + Function->registerInfo().getUniqueVRegDef(*V0) == Instr->operandAt(0), + "def-use head must follow an operand when its address changes"); + + auto TieResult = Instr->tieOperands(0, 1); + Context.expect(TieResult.has_value(), "valid def-use operand tie must succeed"); + const auto DetachedTieClone = Instr->operandAt(0)->cloneDetached(); + Context.expect(!DetachedTieClone.tiedOperandIndex(), + "single detached operand clone must not retain an instruction-local tie"); + auto InsertResult = Instr->insertOperand(0, rcc::MachineOperand::immediate(5)); + Context.expect(InsertResult.has_value(), "operand insertion before a tie must succeed"); + Context.expect(Instr->operandAt(1)->tiedOperandIndex() == 2 && + Instr->operandAt(2)->tiedOperandIndex() == 1, + "operand insertion must rewrite both tie indices"); + auto EraseResult = Instr->eraseOperand(0); + Context.expect(EraseResult.has_value(), "temporary operand erasure must succeed"); + Context.expect(Instr->operandAt(0)->tiedOperandIndex() == 1 && + Instr->operandAt(1)->tiedOperandIndex() == 0, + "operand erasure must rewrite both tie indices"); + Context.expect(Instr->untieOperand(0).has_value(), "reciprocal operand tie must be removable"); + + auto SetRegisterResult = Instr->operandAt(1)->setRegister(*V1); + Context.expect(SetRegisterResult.has_value(), "register operand rebinding must succeed"); + Context.expect(Function->registerInfo().useEmpty(*V0, false), + "register rebinding must unlink the old ordinary use"); + Context.expect(Function->registerInfo().hasOneUse(*V1), + "register rebinding must link the new use"); + + auto ReplaceResult = Function->registerInfo().replaceRegWith(*V1, *V0); + Context.expect(ReplaceResult && *ReplaceResult == 1, + "replaceRegWith must rewrite every linked operand exactly once"); + Context.expect(Function->registerInfo().verifyDefUseChains().has_value(), + "register replacement must preserve exact def-use chains"); + + for (std::int64_t Value = 0; Value < 64; ++Value) { + auto Result = Instr->addOperand(rcc::MachineOperand::immediate(Value)); + Context.expect(Result.has_value(), "variadic immediate operand insertion must succeed"); + } + Context.expect(Function->registerInfo().verifyDefUseChains().has_value(), + "repeated operand growth must preserve def-use links"); + + auto &MRI = Function->registerInfo(); + Context.expect(MRI.setRegisterBank(*V0, rcc::RegisterBankID::fromValue(3)).has_value(), + "MRI must record a register bank"); + Context.expect(MRI.setRegisterClass(*V0, rcc::RegisterClassID::fromValue(4)).has_value(), + "MRI must record a register class"); + Context.expect(MRI.addAllocationHint(*V0, *PhysReg).has_value(), + "MRI must record a physical allocation hint"); + auto V0Info = MRI.virtualRegisterInfo(*V0); + Context.expect(V0Info && V0Info->get().Type == I32 && + V0Info->get().Bank == rcc::RegisterBankID::fromValue(3) && + V0Info->get().RegClass == rcc::RegisterClassID::fromValue(4), + "MRI virtual register properties must evolve without losing LowLevelType"); + + Context.expect(MRI.addLiveIn(*PhysReg, *V0).has_value(), + "MRI must record function physical live-ins"); + Context.expect(!MRI.addLiveIn(*PhysReg, *V0), + "duplicate function physical live-ins must be rejected"); + Context.expect(MRI.reservePhysicalRegister(*PhysReg).has_value(), + "MRI must record reserved physical registers"); + Context.expect(MRI.isReserved(*PhysReg), + "MRI reserved-register query must find recorded register"); + + auto DeadVReg = MRI.createVirtualRegister(*I32); + Context.expect(DeadVReg && MRI.eraseVirtualRegister(*DeadVReg).has_value(), + "unused virtual register must become a tombstone"); + auto LaterVReg = MRI.createVirtualRegister(*I32); + Context.expect(LaterVReg && LaterVReg->virtualIndex() != DeadVReg->virtualIndex(), + "MRI must not reuse tombstoned virtual register indices"); + Context.expect(!MRI.eraseVirtualRegister(*V0), + "virtual register with linked operands must not be erased"); + + auto ZombieVReg = MRI.createVirtualRegister(*I32); + auto ZombieInstr = Function->createInstruction(rcc::Opcode::fromValue(2)); + Context.expect(ZombieVReg && ZombieInstr, + "detached tombstone validation setup must allocate objects"); + if (ZombieVReg && ZombieInstr) { + Context.expect(addRegisterOperand(Context, **ZombieInstr, *ZombieVReg, + rcc::RegisterOperandFlags{rcc::RegisterOperandFlag::Use}), + "detached instruction must initially accept its live virtual register"); + Context.expect(MRI.eraseVirtualRegister(*ZombieVReg).has_value(), + "detached operand must not make an unpublished vreg live"); + const std::size_t OriginalInstructionCount = Block->instructionCount(); + Context.expect(!Block->appendInstruction(**ZombieInstr), + "instruction insertion must reject a tombstoned virtual register"); + Context.expect((*ZombieInstr)->parent() == nullptr && + Block->instructionCount() == OriginalInstructionCount, + "failed instruction insertion must not mutate block layout"); + Context.expect(!(*ZombieInstr)->operandAt(0)->setRegister(*ZombieVReg), + "detached register mutation must reject a tombstoned vreg"); + } + + auto UnlinkResult = Block->removeInstruction(*Instr); + Context.expect(UnlinkResult.has_value(), "instruction removal must succeed"); + Context.expect(MRI.useEmpty(*V0) && MRI.getUniqueVRegDef(*V0) == nullptr, + "instruction removal must unlink every register operand"); + Context.expect(MRI.verifyDefUseChains().has_value(), + "empty def-use lists after removal must remain structurally valid"); + Context.expect(Block->appendInstruction(*Instr).has_value(), + "detached instruction must be reinsertable"); + Context.expect(MRI.getUniqueVRegDef(*V0) == Instr->operandAt(0), + "instruction reinsertion must rebuild register links"); +} + +void testOwnershipCFGAndMemory(TestContext &Context) { + auto Function = createFunction(Context, "cfg", 3); + auto OtherFunction = createFunction(Context, "other", 4); + if (!Function || !OtherFunction) + return; + + rcc::MachineBasicBlock *Entry = createLinkedBlock(Context, *Function); + rcc::MachineBasicBlock *Left = createLinkedBlock(Context, *Function); + rcc::MachineBasicBlock *Right = createLinkedBlock(Context, *Function); + rcc::MachineBasicBlock *Foreign = createLinkedBlock(Context, *OtherFunction); + if (Entry == nullptr || Left == nullptr || Right == nullptr || Foreign == nullptr) + return; + + Context.expect(Function->blockCount() == 3, + "machine function block list must preserve layout count"); + Context.expect(Entry->layoutIndex() == 0 && Left->layoutIndex() == 1 && Right->layoutIndex() == 2, + "block layout indices must follow intrusive layout order"); + + auto Half = rcc::BranchProbability::fromFraction(1, 2); + Context.expect(Half.has_value(), "CFG test probability must be valid"); + if (!Half) + return; + auto CFG = Function->cfgUpdater(); + Context.expect(CFG.addSuccessor(*Entry, *Left, *Half).has_value() && + CFG.addSuccessor(*Entry, *Left, *Half).has_value(), + "duplicate semantic branches must merge into one CFG successor"); + Context.expect(Entry->successors().size() == 1 && Left->predecessors().size() == 1, + "CFG must not contain duplicate successor or predecessor entries"); + Context.expect(CFG.addSuccessor(*Entry, *Right, *Half).has_value(), + "second CFG successor must be insertable"); + Context.expect(!CFG.addSuccessor(*Entry, *Foreign, *Half), + "cross-function CFG edge must be rejected"); + Context.expect(!CFG.replaceSuccessor(*Entry, *Left, *Foreign) && + Entry->successors().size() == 2 && Left->predecessors().size() == 1, + "cross-function CFG replacement must fail without mutating existing edges"); + + Context.expect(CFG.replaceSuccessor(*Entry, *Left, *Right).has_value(), + "CFG successor replacement must succeed"); + Context.expect(Entry->successors().size() == 1 && Left->predecessors().empty() && + Right->predecessors().size() == 1, + "CFG replacement must merge duplicate target and repair predecessors"); + Context.expect(CFG.transferSuccessors(*Entry, *Left).has_value(), + "CFG successor transfer must succeed"); + Context.expect(Entry->successors().empty() && Left->successors().size() == 1 && + Right->predecessors()[0] == Left, + "CFG successor transfer must update both directions"); + Context.expect(CFG.disconnectBlock(*Left).has_value(), + "CFG block disconnection must remove incoming and outgoing edges"); + Context.expect(Left->successors().empty() && Right->predecessors().empty(), + "CFG disconnection must leave no stale reciprocal edges"); + + auto Instr = Function->createInstruction(rcc::Opcode::fromValue(9)); + auto NextInstr = Function->createInstruction(rcc::Opcode::fromValue(10)); + Context.expect(Instr && NextInstr, "instruction arena allocation must succeed"); + if (!Instr || !NextInstr) + return; + Context.expect(Entry->appendInstruction(**Instr).has_value(), + "first block instruction must be inserted"); + Context.expect(Entry->insertInstructionBefore(**Instr, **NextInstr).has_value(), + "instruction insertion before a stable node must succeed"); + Context.expect(Entry->firstInstruction() == *NextInstr && Entry->lastInstruction() == *Instr && + Entry->instructionCount() == 2, + "intrusive instruction list must preserve order and endpoints"); + + auto CrossBlockOperand = rcc::MachineOperand::basicBlock(*Foreign); + Context.expect(!(*Instr)->addOperand(std::move(CrossBlockOperand)), + "instruction must reject a block operand from another function"); + + auto MemOperand = Function->createMemOperand(makeLoadDescription()); + auto ReplacementMemOperand = Function->createMemOperand(makeLoadDescription()); + auto ForeignMemOperand = OtherFunction->createMemOperand(makeLoadDescription()); + Context.expect(MemOperand && ReplacementMemOperand && ForeignMemOperand, + "function arena must allocate immutable memory operands"); + if (MemOperand && ReplacementMemOperand && ForeignMemOperand) { + Context.expect((*Instr)->addMemOperand(**MemOperand).has_value(), + "instruction must accept a memory operand owned by its function"); + Context.expect((*Instr)->replaceMemOperand(0, **ReplacementMemOperand).has_value() && + (*Instr)->memOperands().front() == *ReplacementMemOperand, + "instruction must replace an owned memory operand in place"); + Context.expect(!(*Instr)->replaceMemOperand(1, **MemOperand) && + !(*Instr)->replaceMemOperand(0, **ForeignMemOperand) && + (*Instr)->memOperands().front() == *ReplacementMemOperand, + "invalid memory replacement must fail without mutation"); + Context.expect(!(*Instr)->addMemOperand(**ForeignMemOperand), + "instruction must reject a memory operand from another function"); + } + + const rcc::MachineBasicBlock *StableEntryAddress = Entry; + const rcc::MachineInstr *StableInstrAddress = *Instr; + for (std::uint32_t Index = 0; Index < 128; ++Index) { + auto NewBlock = Function->createBasicBlock(); + auto NewInstr = Function->createInstruction(rcc::Opcode::fromValue(Index + 100)); + Context.expect(NewBlock && NewInstr, "arena growth must continue to allocate objects"); + } + Context.expect(StableEntryAddress == Entry && StableInstrAddress == *Instr, + "arena growth must preserve block and instruction addresses"); + + Context.expect(Function->removeBlock(*Right).has_value(), + "disconnected empty block must be removable from layout"); + Context.expect(!Right->isLinked() && Function->blockCount() == 2, + "removed block must leave layout while retaining stable storage"); +} + +} // namespace + +int main() { + TestContext Context; + testOperandKinds(Context); + testRegisterInfoAndOperandMovement(Context); + testOwnershipCFGAndMemory(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineJumpTableInfoTest.cpp b/backend/unittests/CodeGen/MachineJumpTableInfoTest.cpp new file mode 100644 index 0000000..106a2a6 --- /dev/null +++ b/backend/unittests/CodeGen/MachineJumpTableInfoTest.cpp @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineJumpTableInfo.h" + +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineFunction.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <string> +#include <string_view> + +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]] std::unique_ptr<rcc::MachineFunction> +createFunction(TestContext &Context, std::string Name, std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "machine function creation must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineBasicBlock *createBlock(TestContext &Context, + rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + Context.expect(Block.has_value() && Function.appendBlock(**Block).has_value(), + "jump table test block creation must succeed"); + return Block ? *Block : nullptr; +} + +[[nodiscard]] rcc::MachineJumpTableDesc tableDescription(rcc::MachineBasicBlock &First, + rcc::MachineBasicBlock &Second) { + rcc::MachineJumpTableDesc Description; + Description.Encoding = rcc::JumpTableEntryEncoding::GPRelative32BlockAddress; + Description.EntrySize = 4; + Description.Align = *rcc::Alignment::create(4); + Description.Targets = {&First, &Second, &Second}; + Description.Placement.MaximumDistance = 4096; + return Description; +} + +void testTableLifecycle(TestContext &Context) { + auto Function = createFunction(Context, "jump_tables", 1); + if (!Function) + return; + rcc::MachineBasicBlock *Entry = createBlock(Context, *Function); + rcc::MachineBasicBlock *Exit = createBlock(Context, *Function); + if (Entry == nullptr || Exit == nullptr) + return; + + auto First = Function->jumpTableInfo().createTable(tableDescription(*Entry, *Exit)); + auto TargetDescription = tableDescription(*Exit, *Entry); + TargetDescription.Encoding = rcc::JumpTableEntryEncoding::Target; + TargetDescription.TargetEncoding = 19; + TargetDescription.Placement.TargetFlags = 7; + auto Target = Function->jumpTableInfo().createTable(std::move(TargetDescription)); + Context.expect(First && Target && First->value() == 0 && Target->value() == 1 && + Function->jumpTableInfo().liveTableCount() == 2, + "valid jump tables must receive monotonic indices"); + if (!First || !Target) + return; + + const rcc::MachineJumpTableEntry *FirstEntry = Function->jumpTableInfo().table(*First); + Context.expect(FirstEntry != nullptr && FirstEntry->description()->Targets.size() == 3 && + FirstEntry->description()->Targets[1] == Exit && + FirstEntry->description()->Targets[2] == Exit, + "jump table target order and duplicate cases must be preserved"); + + Context.expect(Function->jumpTableInfo().eraseTable(*First).has_value() && + Function->jumpTableInfo().table(*First) == nullptr, + "erasing a jump table must leave a tombstone"); + auto CompressedDescription = tableDescription(*Entry, *Exit); + CompressedDescription.Encoding = rcc::JumpTableEntryEncoding::Custom32; + CompressedDescription.EntrySize = 4; + auto Compressed = Function->jumpTableInfo().createTable(std::move(CompressedDescription)); + Context.expect(Compressed && Compressed->value() == 2, + "jump table tombstone indices must never be reused"); + + auto Updated = tableDescription(*Entry, *Exit); + Updated.Encoding = rcc::JumpTableEntryEncoding::LabelDifference64; + Updated.EntrySize = 8; + Context.expect(Function->jumpTableInfo().updateTable(*Target, std::move(Updated)).has_value() && + Function->jumpTableInfo().verify().has_value() && + Function->jumpTableInfo().freeze().has_value() && + Function->jumpTableInfo().isFrozen(), + "valid jump table updates must verify and freeze"); + Context.expect( + !Function->jumpTableInfo().eraseTable(*Target) && + !Function->jumpTableInfo().updateTable(*Target, tableDescription(*Entry, *Exit)) && + !Function->jumpTableInfo().createTable(tableDescription(*Entry, *Exit)) && + !Function->jumpTableInfo().freeze(), + "frozen jump table info must reject every mutation"); +} + +void testInvalidTables(TestContext &Context) { + auto Function = createFunction(Context, "invalid_tables", 2); + auto Foreign = createFunction(Context, "foreign_tables", 3); + if (!Function || !Foreign) + return; + rcc::MachineBasicBlock *Block = createBlock(Context, *Function); + rcc::MachineBasicBlock *ForeignBlock = createBlock(Context, *Foreign); + if (Block == nullptr || ForeignBlock == nullptr) + return; + + rcc::MachineJumpTableDesc Empty; + Context.expect(!Function->jumpTableInfo().createTable(std::move(Empty)), + "jump table must reject an empty target list"); + + auto ForeignTarget = tableDescription(*Block, *ForeignBlock); + Context.expect(!Function->jumpTableInfo().createTable(std::move(ForeignTarget)), + "jump table must reject a foreign target block"); + + auto InvalidSize = tableDescription(*Block, *Block); + InvalidSize.EntrySize = 3; + Context.expect(!Function->jumpTableInfo().createTable(std::move(InvalidSize)), + "jump table entry size must be a bounded power of two"); + + auto MissingTargetEncoding = tableDescription(*Block, *Block); + MissingTargetEncoding.Encoding = rcc::JumpTableEntryEncoding::Target; + Context.expect(!Function->jumpTableInfo().createTable(std::move(MissingTargetEncoding)), + "target jump table must carry a target encoding ID"); + + auto UnexpectedTargetEncoding = tableDescription(*Block, *Block); + UnexpectedTargetEncoding.TargetEncoding = 1; + Context.expect(!Function->jumpTableInfo().createTable(std::move(UnexpectedTargetEncoding)), + "generic jump table encoding must reject target encoding IDs"); + + auto OversizedCompressed = tableDescription(*Block, *Block); + OversizedCompressed.Encoding = rcc::JumpTableEntryEncoding::Custom32; + OversizedCompressed.EntrySize = 8; + OversizedCompressed.EntrySize = 8; + Context.expect(!Function->jumpTableInfo().createTable(std::move(OversizedCompressed)), + "compressed jump table must reject oversized entries"); + + auto ZeroDistance = tableDescription(*Block, *Block); + ZeroDistance.Placement.MaximumDistance = 0; + Context.expect(!Function->jumpTableInfo().createTable(std::move(ZeroDistance)), + "jump table maximum distance must be nonzero"); + + auto LiveTable = Function->jumpTableInfo().createTable(tableDescription(*Block, *Block)); + Context.expect(LiveTable && Function->removeBlock(*Block).has_value() && + !Function->jumpTableInfo().verify(), + "jump table verification must detect a detached target"); +} + +} // namespace + +int main() { + TestContext Context; + testTableLifecycle(Context); + testInvalidTables(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineMetadataTest.cpp b/backend/unittests/CodeGen/MachineMetadataTest.cpp new file mode 100644 index 0000000..6669e2a --- /dev/null +++ b/backend/unittests/CodeGen/MachineMetadataTest.cpp @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineMetadata.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineFunctionCloner.h" +#include "rcc/CodeGen/MachineVerifier.h" +#include "rcc/CodeGen/TargetDescription.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t NopOpcode = 1; + +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::Expected<rcc::TargetDescription> createTarget() { + rcc::InstructionDescriptor Nop; + Nop.InstructionOpcode = rcc::Opcode::fromValue(NopOpcode); + Nop.Name = "TEST_NOP"; + Nop.Flags = {rcc::InstructionFlag::ProducesCode}; + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Nop)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> +createFunction(TestContext &Context, std::uint64_t StableID, std::string Name) { + auto Function = rcc::MachineFunction::create(std::move(Name), + rcc::MachineFunctionStableID::fromValue(StableID)); + Context.expect(Function.has_value(), "machine function creation must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineMetadataElement +referenceElement(TestContext &Context, rcc::MachineMetadataReference Reference) { + auto Element = rcc::MachineMetadataElement::reference(Reference); + Context.expect(Element.has_value(), "valid machine metadata references must form elements"); + return Element ? std::move(*Element) : rcc::MachineMetadataElement{}; +} + +void testReferenceIdentity(TestContext &Context) { + const rcc::MachineMetadataReference Invalid; + const rcc::MachineMetadataReference EmbeddedZero = rcc::MachineMetadataReference::embeddedIR(0); + const rcc::MachineMetadataReference FunctionZero = + rcc::MachineMetadataReference::machineFunction(0); + + Context.expect(!Invalid.isValid() && EmbeddedZero.isValid() && FunctionZero.isValid(), + "metadata slot zero must be valid in both explicit reference scopes"); + Context.expect(EmbeddedZero != FunctionZero && + EmbeddedZero.scope() == rcc::MachineMetadataScope::EmbeddedIR && + FunctionZero.scope() == rcc::MachineMetadataScope::MachineFunction && + EmbeddedZero.slot() == 0 && FunctionZero.slot() == 0, + "embedded and function-local metadata slots must have distinct identities"); + Context.expect(!rcc::MachineMetadataElement::reference(Invalid), + "an invalid metadata reference must not form an element"); +} + +void testOrderedGraphValidation(TestContext &Context) { + rcc::MachineMetadataInfo Metadata; + rcc::MachineMetadataNode First{ + .Slot = 0, + .IsDistinct = true, + .Elements = + { + referenceElement(Context, rcc::MachineMetadataReference::embeddedIR(0)), + rcc::MachineMetadataElement::string(""), + referenceElement(Context, rcc::MachineMetadataReference::machineFunction(1)), + }, + }; + Context.expect(Metadata.addNode(std::move(First)).has_value() && !Metadata.verify(), + "forward references must be accepted before graph closure is verified"); + + rcc::MachineMetadataNode Second{ + .Slot = 1, + .IsDistinct = false, + .Elements = + { + referenceElement(Context, rcc::MachineMetadataReference::machineFunction(0)), + rcc::MachineMetadataElement::string("payload"), + }, + }; + Context.expect(Metadata.addNode(std::move(Second)).has_value() && Metadata.verify().has_value(), + "closed cyclic metadata tuple graphs must be valid"); + Context.expect(Metadata.nodes().size() == 2 && Metadata.nodes()[0].Slot == 0 && + Metadata.nodes()[0].IsDistinct && Metadata.nodes()[1].Slot == 1 && + Metadata.node(0) == &Metadata.nodes()[0] && + Metadata.node(1) == &Metadata.nodes()[1] && Metadata.node(2) == nullptr, + "metadata definitions and lookup must preserve deterministic source order"); + Context.expect(Metadata.nodes()[0].Elements[1].string() != nullptr && + Metadata.nodes()[0].Elements[1].string()->empty(), + "empty metadata strings must remain valid tuple elements"); + + const std::size_t OriginalSize = Metadata.nodes().size(); + Context.expect(!Metadata.addNode({.Slot = 0, .IsDistinct = false, .Elements = {}}) && + Metadata.nodes().size() == OriginalSize, + "duplicate metadata definitions must be rejected without changing the graph"); + + auto Clone = Metadata.clone(); + Context.expect(Clone.has_value() && *Clone == Metadata && + Clone->nodes().data() != Metadata.nodes().data(), + "metadata graphs must support independent deterministic deep cloning"); + + rcc::MachineMetadataInfo InvalidMetadata; + rcc::MachineMetadataNode InvalidNode; + InvalidNode.Elements.emplace_back(); + Context.expect(!InvalidMetadata.addNode(std::move(InvalidNode)) && + InvalidMetadata.nodes().empty(), + "invalid tuple elements must be rejected before publication"); +} + +void testFunctionVerifierAndClone(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 1, "metadata"); + if (!Target || Function == nullptr) + return; + + Context.expect( + Function->metadata() + .addNode({ + .Slot = 7, + .IsDistinct = false, + .Elements = + { + referenceElement(Context, rcc::MachineMetadataReference::embeddedIR(3)), + rcc::MachineMetadataElement::string("root"), + }, + }) + .has_value() && + Function->metadata() + .addNode({ + .Slot = 2, + .IsDistinct = true, + .Elements = + { + referenceElement(Context, + rcc::MachineMetadataReference::machineFunction(7)), + }, + }) + .has_value() && + rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "well-formed machine metadata must pass unified function verification"); + + auto Clone = + rcc::cloneMachineFunction(*Function, *Target, + { + .NewStableID = rcc::MachineFunctionStableID::fromValue(2), + .NewName = std::string("metadata.clone"), + }); + Context.expect(Clone.has_value() && Clone->ClonedFunction->metadata() == Function->metadata() && + Clone->ClonedFunction->metadata().nodes().data() != + Function->metadata().nodes().data(), + "function cloning must preserve an independent metadata graph"); + + auto InvalidFunction = createFunction(Context, 3, "invalid-metadata"); + if (InvalidFunction == nullptr) + return; + Context.expect( + InvalidFunction->metadata() + .addNode({ + .Slot = 0, + .Elements = + { + referenceElement(Context, + rcc::MachineMetadataReference::machineFunction(99)), + }, + }) + .has_value() && + !rcc::MachineVerifier::verify(*InvalidFunction, *Target), + "the unified verifier must reject undefined function-local metadata references"); +} + +} // namespace + +int main() { + TestContext Context; + testReferenceIdentity(Context); + testOrderedGraphValidation(Context); + testFunctionVerifierAndClone(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineOperandTest.cpp b/backend/unittests/CodeGen/MachineOperandTest.cpp new file mode 100644 index 0000000..96f1f19 --- /dev/null +++ b/backend/unittests/CodeGen/MachineOperandTest.cpp @@ -0,0 +1,524 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineOperand.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineCFIInfo.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineFunctionAttributes.h" +#include "rcc/CodeGen/MachineFunctionCloner.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineVerifier.h" +#include "rcc/CodeGen/RegisterMask.h" +#include "rcc/CodeGen/TargetDescription.h" + +#include <array> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <limits> +#include <memory> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t OperandOpcode = 1; + +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::OperandDescriptor operandDescriptor(rcc::MachineOperand::Kind Kind) { + rcc::OperandDescriptor Descriptor; + Descriptor.Kinds = {Kind}; + return Descriptor; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + rcc::InstructionDescriptor Descriptor; + Descriptor.InstructionOpcode = rcc::Opcode::fromValue(OperandOpcode); + Descriptor.Name = "TEST_MIR_OPERANDS"; + Descriptor.Flags = {rcc::InstructionFlag::ProducesCode}; + Descriptor.Operands = { + operandDescriptor(rcc::MachineOperand::Kind::IntegerImmediate), + operandDescriptor(rcc::MachineOperand::Kind::LiveOutRegisterMask), + operandDescriptor(rcc::MachineOperand::Kind::CFIIndex), + operandDescriptor(rcc::MachineOperand::Kind::Intrinsic), + operandDescriptor(rcc::MachineOperand::Kind::Predicate), + operandDescriptor(rcc::MachineOperand::Kind::Predicate), + operandDescriptor(rcc::MachineOperand::Kind::ShuffleMask), + operandDescriptor(rcc::MachineOperand::Kind::DebugInstrRef), + operandDescriptor(rcc::MachineOperand::Kind::LaneMask), + }; + + auto Instructions = rcc::InstructionSet::create({std::move(Descriptor)}); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> +createFunction(TestContext &Context, std::uint64_t ID, std::string Name) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "machine function construction must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineBasicBlock *appendBlock(TestContext &Context, + rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + Context.expect(Block.has_value(), "machine basic block construction must succeed"); + if (!Block) + return nullptr; + auto AppendResult = Function.appendBlock(**Block); + Context.expect(AppendResult.has_value(), "machine basic block insertion must succeed"); + if (!AppendResult) + return nullptr; + return *Block; +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> +buildOperandInstruction(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::CFIInstructionIndex CFIIndex, + std::shared_ptr<const rcc::RegisterMask> LiveOutMask) { + const std::array<std::uint64_t, 2> Words{ + 0x0123456789abcdefULL, + 0xfedcba9876543210ULL, + }; + auto Integer = rcc::APInt::create(128, Words); + if (!Integer) + return std::unexpected(Integer.error()); + + rcc::MachineIRBuilder Builder(Function, Target); + auto InsertResult = Builder.setInsertPoint(Block); + if (!InsertResult) + return std::unexpected(InsertResult.error()); + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(OperandOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addIntegerImmediate(std::move(*Integer)); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addLiveOutRegisterMask(std::move(LiveOutMask)); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addCFIInstruction(CFIIndex); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addIntrinsic(rcc::IntrinsicID::fromValue(7)); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addIntegerPredicate(rcc::IntegerPredicate::SignedLessThan); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addFloatingPredicate(rcc::FloatingPredicate::UnorderedGreaterEqual); + !Result) { + return std::unexpected(Result.error()); + } + if (auto Result = Pending->addShuffleMask({1, rcc::ShuffleMaskOperand::Undef, 0}); !Result) { + return std::unexpected(Result.error()); + } + if (auto Result = Pending->addDebugInstrRef(9, 3); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addLaneMask(rcc::LaneMask::fromRaw(0x8000000000000001ULL)); !Result) { + return std::unexpected(Result.error()); + } + return Pending->build(); +} + +void testIntegerImmediate(TestContext &Context) { + const std::array<std::uint64_t, 2> Words{ + 0x0123456789abcdefULL, + 0x0000000000000001ULL, + }; + auto Integer = rcc::APInt::create(65, Words); + Context.expect(Integer.has_value() && Integer->bitWidth() == 65 && Integer->words().size() == 2 && + Integer->words()[0] == Words[0] && Integer->words()[1] == Words[1] && + Integer->isNegative() && !Integer->isZero(), + "arbitrary-width integer must preserve exact little-endian words"); + + Context.expect( + !rcc::APInt::create(0, std::span<const std::uint64_t>{}) && + !rcc::APInt::create(rcc::APInt::MaxBitWidth + 1, std::span<const std::uint64_t>{}), + "integer immediate must reject unsupported bit widths"); + const std::array<std::uint64_t, 2> TwoWords{1, 0}; + const std::array<std::uint64_t, 1> HighBits{0x100}; + Context.expect(!rcc::APInt::create(64, TwoWords) && !rcc::APInt::create(8, HighBits), + "integer immediate must reject word-count and high-bit mismatches"); + + auto UnsignedMaximum = rcc::APInt::fromUnsigned(8, 255); + auto UnsignedOverflow = rcc::APInt::fromUnsigned(8, 256); + auto SignedMinimum = rcc::APInt::fromSigned(8, -128); + auto SignedMaximum = rcc::APInt::fromSigned(8, 127); + auto SignedUnderflow = rcc::APInt::fromSigned(8, -129); + auto SignedOverflow = rcc::APInt::fromSigned(8, 128); + Context.expect(UnsignedMaximum && UnsignedMaximum->words()[0] == 255 && !UnsignedOverflow && + SignedMinimum && SignedMinimum->words()[0] == 128 && SignedMaximum && + SignedMaximum->words()[0] == 127 && !SignedUnderflow && !SignedOverflow, + "integer immediate scalar factories must enforce signedness and width"); + + auto SignExtended = rcc::APInt::fromSigned(128, -1); + auto Zero = rcc::APInt::fromUnsigned(128, 0); + Context.expect(SignExtended && SignExtended->isNegative() && SignExtended->words().size() == 2 && + SignExtended->words()[0] == std::numeric_limits<std::uint64_t>::max() && + SignExtended->words()[1] == std::numeric_limits<std::uint64_t>::max() && + Zero && Zero->isZero() && !Zero->isNegative(), + "wide scalar factories must sign-extend and zero-extend exactly"); + + if (Integer) { + auto Operand = rcc::MachineOperand::integerImmediate(std::move(*Integer), 5); + const auto *Payload = Operand.getIf<rcc::IntegerImmediateOperand>(); + Context.expect(Operand.kind() == rcc::MachineOperand::Kind::IntegerImmediate && + Operand.targetFlags() == 5 && Payload != nullptr && + Payload->Value.bitWidth() == 65, + "typed integer operand must retain its distinct kind and target flags"); + } +} + +void testOperandFactories(TestContext &Context) { + auto Mask = rcc::test::createTestCallPreservedMask(); + auto LiveOut = rcc::MachineOperand::liveOutRegisterMask(Mask, 3); + Context.expect(LiveOut && LiveOut->kind() == rcc::MachineOperand::Kind::LiveOutRegisterMask && + LiveOut->targetFlags() == 3 && + LiveOut->getIf<rcc::LiveOutRegisterMaskOperand>()->Mask == Mask && + !rcc::MachineOperand::liveOutRegisterMask(nullptr), + "live-out mask must be distinct from a call-preserved register mask"); + + auto CFI = rcc::MachineOperand::cfiIndex(rcc::CFIInstructionIndex::fromValue(0), 4); + Context.expect(CFI.kind() == rcc::MachineOperand::Kind::CFIIndex && + CFI.getIf<rcc::CFIIndexOperand>()->Index.value() == 0 && + CFI.targetFlags() == 4, + "CFI operand must preserve index zero and target flags"); + + auto Intrinsic = rcc::MachineOperand::intrinsic(rcc::IntrinsicID::fromValue(17), 6); + Context.expect(Intrinsic && Intrinsic->kind() == rcc::MachineOperand::Kind::Intrinsic && + Intrinsic->getIf<rcc::IntrinsicOperand>()->ID.value() == 17 && + Intrinsic->targetFlags() == 6 && + !rcc::MachineOperand::intrinsic(rcc::IntrinsicID{}), + "intrinsic operand must use a nonzero strong ID"); + + auto IntegerPredicate = + rcc::MachineOperand::integerPredicate(rcc::IntegerPredicate::UnsignedGreaterEqual); + auto FloatingPredicate = + rcc::MachineOperand::floatingPredicate(rcc::FloatingPredicate::OrderedNotEqual); + Context.expect( + IntegerPredicate && FloatingPredicate && + IntegerPredicate->getIf<rcc::PredicateOperand>()->integerPredicate() != nullptr && + FloatingPredicate->getIf<rcc::PredicateOperand>()->floatingPredicate() != nullptr && + !rcc::MachineOperand::integerPredicate(rcc::IntegerPredicate::Count) && + !rcc::MachineOperand::floatingPredicate(rcc::FloatingPredicate::Count), + "integer and floating predicates must share one tagged operand without invalid sentinels"); + + auto Shuffle = rcc::MachineOperand::shuffleMask({1, rcc::ShuffleMaskOperand::Undef, -2, 0}, 7); + Context.expect(Shuffle && Shuffle->kind() == rcc::MachineOperand::Kind::ShuffleMask && + Shuffle->getIf<rcc::ShuffleMaskOperand>()->Elements == + std::vector<std::int32_t>({1, -1, -2, 0}) && + Shuffle->targetFlags() == 7 && !rcc::MachineOperand::shuffleMask({0}), + "shuffle mask must preserve int32 elements and the canonical undef sentinel"); + + auto Reference = rcc::MachineOperand::debugInstrRef(9, 1'000'000, 8); + auto ZeroReference = rcc::MachineOperand::debugInstrRef(0, 0); + Context.expect( + Reference && Reference->kind() == rcc::MachineOperand::Kind::DebugInstrRef && + Reference->getIf<rcc::DebugInstrRefOperand>()->InstructionNumber == 9 && + Reference->getIf<rcc::DebugInstrRefOperand>()->OperandNumber == 1'000'000 && + Reference->targetFlags() == 8 && ZeroReference && + ZeroReference->getIf<rcc::DebugInstrRefOperand>()->InstructionNumber == 0 && + !rcc::MachineOperand::debugInstrRef( + static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max()) + 1, 0) && + !rcc::MachineOperand::debugInstrRef( + 1, static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max()) + 1), + "debug instruction reference must preserve zero and enforce its two uint32 domains"); + + auto LaneMask = rcc::MachineOperand::laneMask(rcc::LaneMask::fromRaw(0xf00000000000000fULL), 9); + Context.expect(LaneMask.kind() == rcc::MachineOperand::Kind::LaneMask && + LaneMask.getIf<rcc::LaneMaskOperand>()->Mask.rawValue() == + 0xf00000000000000fULL && + LaneMask.targetFlags() == 9, + "lane-mask operand must preserve all 64 bits"); + + auto Clone = LaneMask.cloneDetached(); + Context.expect(Clone.parent() == nullptr && Clone.kind() == LaneMask.kind() && + Clone.targetFlags() == LaneMask.targetFlags() && + Clone.getIf<rcc::LaneMaskOperand>()->Mask == + LaneMask.getIf<rcc::LaneMaskOperand>()->Mask, + "detached operand cloning must preserve the new payload kinds"); +} + +void testFrameIndexRewrite(TestContext &Context) { + auto Function = createFunction(Context, 5, "frame-index-rewrite"); + if (!Function) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + auto Align = rcc::Alignment::create(8); + if (Block == nullptr || !Align) + return; + + rcc::StackObjectDesc Description; + Description.Kind = rcc::StackObjectKind::SpillSlot; + Description.Size = 8; + Description.Align = *Align; + auto First = Function->frameInfo().createObject(Description); + auto Second = Function->frameInfo().createObject(Description); + Context.expect(First.has_value() && Second.has_value(), + "frame-index rewrite objects must be created"); + if (!First || !Second) + return; + + auto Instruction = Function->createInstruction(rcc::Opcode::fromValue(OperandOpcode)); + Context.expect(Instruction.has_value() && + (*Instruction)->addOperand(rcc::MachineOperand::frameIndex(*First, 4)) && + Block->appendInstruction(**Instruction), + "frame-index rewrite operand must be linked to its function"); + if (!Instruction || (*Instruction)->operandCount() == 0) + return; + rcc::MachineOperand *Operand = (*Instruction)->operandAt(0); + Context.expect(Operand != nullptr && Operand->setFrameIndex(*Second).has_value() && + Operand->getIf<rcc::FrameIndexOperand>()->Index == *Second && + Operand->getIf<rcc::FrameIndexOperand>()->Offset == 4, + "setFrameIndex must rewrite only the object identity"); + + const rcc::FrameIndex Unknown = rcc::FrameIndex::fromValue(99); + Context.expect(Operand != nullptr && !Operand->setFrameIndex(Unknown) && + Operand->getIf<rcc::FrameIndexOperand>()->Index == *Second, + "setFrameIndex must reject an unknown attached object atomically"); + auto Immediate = rcc::MachineOperand::immediate(0); + Context.expect(!Immediate.setFrameIndex(*First), + "setFrameIndex must reject non-frame-index operands"); +} + +void testCFIInfo(TestContext &Context) { + const auto R1 = rcc::DwarfRegisterNumber::fromValue(1); + const auto R2 = rcc::DwarfRegisterNumber::fromValue(2); + const auto R3 = rcc::DwarfRegisterNumber::fromValue(3); + rcc::MachineCFIInfo Info; + + auto Add = [&Context, &Info](rcc::MachineCFIInstruction Instruction) { + auto Index = Info.addInstruction(std::move(Instruction)); + Context.expect(Index.has_value(), "valid CFI instruction insertion must succeed"); + }; + auto AddExpected = [&Context, &Info](rcc::Expected<rcc::MachineCFIInstruction> Instruction) { + Context.expect(Instruction.has_value(), "valid structured CFI factory must succeed"); + if (!Instruction) + return; + auto Index = Info.addInstruction(std::move(*Instruction)); + Context.expect(Index.has_value(), "valid structured CFI insertion must succeed"); + }; + + Add(rcc::MachineCFIInstruction::sameValue(R1)); + Add(rcc::MachineCFIInstruction::rememberState()); + Add(rcc::MachineCFIInstruction::restoreState()); + Add(rcc::MachineCFIInstruction::offset(R1, -8)); + Add(rcc::MachineCFIInstruction::llvmDefAddressSpaceCFA(R1, 16, 5)); + Add(rcc::MachineCFIInstruction::defCFARegister(R2)); + Add(rcc::MachineCFIInstruction::defCFAOffset(32)); + Add(rcc::MachineCFIInstruction::defCFA(R2, 24)); + Add(rcc::MachineCFIInstruction::relativeOffset(R3, -16)); + Add(rcc::MachineCFIInstruction::adjustCFAOffset(8)); + AddExpected(rcc::MachineCFIInstruction::escape({0x0c, 0x07, 0x08})); + Add(rcc::MachineCFIInstruction::restore(R1)); + Add(rcc::MachineCFIInstruction::undefined(R2)); + Add(rcc::MachineCFIInstruction::registerRule(R1, R3)); + Add(rcc::MachineCFIInstruction::windowSave()); + Add(rcc::MachineCFIInstruction::negateReturnAddressState()); + Add(rcc::MachineCFIInstruction::negateReturnAddressStateWithPC()); + Add(rcc::MachineCFIInstruction::gnuArgsSize(48)); + AddExpected(rcc::MachineCFIInstruction::label("cfi.label")); + Add(rcc::MachineCFIInstruction::valueOffset(R3, 64)); + Add(rcc::MachineCFIInstruction::llvmRegisterPair({ + .Register = R1, + .FirstRegister = R2, + .FirstSizeInBits = 32, + .SecondRegister = R3, + .SecondSizeInBits = 32, + })); + AddExpected(rcc::MachineCFIInstruction::llvmVectorRegisters({ + .Register = R1, + .VectorRegisters = {{R2, 0, 64}, {R3, 1, 64}}, + })); + Add(rcc::MachineCFIInstruction::llvmVectorOffset({ + .Register = R1, + .RegisterSizeInBits = 64, + .MaskRegister = R2, + .MaskRegisterSizeInBits = 8, + .Offset = -32, + })); + Add(rcc::MachineCFIInstruction::llvmVectorRegisterMask({ + .Register = R1, + .SpillRegister = R2, + .SpillRegisterLaneSizeInBits = 64, + .MaskRegister = R3, + .MaskRegisterSizeInBits = 8, + })); + + Context.expect(Info.instructions().size() == 24 && Info.verify().has_value() && + Info.instruction(rcc::CFIInstructionIndex::fromValue(0))->operation() == + rcc::MachineCFIOperation::SameValue && + Info.instruction(rcc::CFIInstructionIndex::fromValue(23))->operation() == + rcc::MachineCFIOperation::LLVMVectorRegisterMask && + Info.instruction(rcc::CFIInstructionIndex::fromValue(24)) == nullptr, + "CFI table must preserve deterministic zero-based definition order"); + + Context.expect(!rcc::MachineCFIInstruction::escape({}) && + !rcc::MachineCFIInstruction::label("") && + !rcc::MachineCFIInstruction::llvmVectorRegisters({ + .Register = R1, + .VectorRegisters = {}, + }), + "CFI factories must reject structurally incomplete payloads"); + + auto ZeroWidthVectorRegisters = rcc::MachineCFIInstruction::llvmVectorRegisters({ + .Register = R1, + .VectorRegisters = {{R2, 0, 0}}, + }); + Context.expect( + rcc::MachineCFIInstruction::llvmRegisterPair({ + .Register = R1, + .FirstRegister = R2, + .FirstSizeInBits = 0, + .SecondRegister = R3, + .SecondSizeInBits = 0, + }) + .verify() && + ZeroWidthVectorRegisters && ZeroWidthVectorRegisters->verify() && + rcc::MachineCFIInstruction::llvmVectorOffset({ + .Register = R1, + .RegisterSizeInBits = 0, + .MaskRegister = R2, + .MaskRegisterSizeInBits = 0, + .Offset = 0, + }) + .verify() && + rcc::MachineCFIInstruction::llvmVectorRegisterMask({ + .Register = R1, + .SpillRegister = R2, + .SpillRegisterLaneSizeInBits = 0, + .MaskRegister = R3, + .MaskRegisterSizeInBits = 0, + }) + .verify(), + "CFI unsigned size fields must preserve every value accepted by the fixed MIR grammar"); + + auto Clone = Info.clone(); + Context.expect(Clone && *Clone == Info, "CFI information clone must preserve every rule"); + if (Clone) { + auto Extra = Clone->addInstruction(rcc::MachineCFIInstruction::rememberState()); + Context.expect(Extra && Clone->instructions().size() == 25 && Info.instructions().size() == 24, + "CFI information clone must own independent storage"); + } +} + +void testBuilderVerifierAndClone(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "operand test target construction must succeed"); + auto Function = createFunction(Context, 1, "operand-integration"); + if (!Target || !Function) + return; + Function->attributes().set(rcc::MachineFunctionAttribute::DebugInstrRef); + auto CFIIndex = Function->cfiInfo().addInstruction(rcc::MachineCFIInstruction::defCFAOffset(16)); + Context.expect(CFIIndex.has_value(), "function CFI instruction insertion must succeed"); + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (!CFIIndex || Block == nullptr) + return; + + auto Instruction = buildOperandInstruction(*Function, *Target, *Block, *CFIIndex, + rcc::test::createTestCallPreservedMask()); + Context.expect(Instruction.has_value() && + rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "builder and verifier must accept every newly modeled operand kind"); + if (!Instruction) + return; + + const auto Operands = (*Instruction)->operands(); + Context.expect( + Operands.size() == 9 && + Operands[0].getIf<rcc::IntegerImmediateOperand>()->Value.bitWidth() == 128 && + Operands[1].kind() == rcc::MachineOperand::Kind::LiveOutRegisterMask && + Operands[2].getIf<rcc::CFIIndexOperand>()->Index == *CFIIndex && + Operands[3].getIf<rcc::IntrinsicOperand>()->ID.value() == 7 && + *Operands[4].getIf<rcc::PredicateOperand>()->integerPredicate() == + rcc::IntegerPredicate::SignedLessThan && + *Operands[5].getIf<rcc::PredicateOperand>()->floatingPredicate() == + rcc::FloatingPredicate::UnorderedGreaterEqual && + Operands[6].getIf<rcc::ShuffleMaskOperand>()->Elements[1] == + rcc::ShuffleMaskOperand::Undef && + Operands[7].getIf<rcc::DebugInstrRefOperand>()->InstructionNumber == 9 && + Operands[8].getIf<rcc::LaneMaskOperand>()->Mask.rawValue() == 0x8000000000000001ULL, + "built instruction must expose all new payloads without string or immediate overloading"); + + auto Clone = + rcc::cloneMachineFunction(*Function, *Target, + { + .NewStableID = rcc::MachineFunctionStableID::fromValue(2), + .NewName = std::string("operand-integration.clone"), + }); + Context.expect(Clone.has_value(), "operand-rich function must support explicit deep cloning"); + if (Clone) { + const rcc::MachineInstr *ClonedInstruction = + Clone->ClonedFunction->firstBlock()->firstInstruction(); + Context.expect( + Clone->ClonedFunction->cfiInfo() == Function->cfiInfo() && ClonedInstruction != nullptr && + ClonedInstruction->operands().size() == 9 && + ClonedInstruction->operands()[0].getIf<rcc::IntegerImmediateOperand>()->Value == + Operands[0].getIf<rcc::IntegerImmediateOperand>()->Value && + ClonedInstruction->operands()[6].getIf<rcc::ShuffleMaskOperand>()->Elements == + Operands[6].getIf<rcc::ShuffleMaskOperand>()->Elements, + "deep clone must preserve CFI indices and owned variable-size operand data"); + } + + auto MissingCFIFunction = createFunction(Context, 3, "missing-cfi"); + if (!MissingCFIFunction) + return; + rcc::MachineBasicBlock *MissingCFIBlock = appendBlock(Context, *MissingCFIFunction); + if (MissingCFIBlock == nullptr) + return; + auto MissingCFI = buildOperandInstruction(*MissingCFIFunction, *Target, *MissingCFIBlock, + rcc::CFIInstructionIndex::fromValue(0), + rcc::test::createTestCallPreservedMask()); + Context.expect(!MissingCFI, "builder must reject an operand that references an unknown CFI rule"); + + auto InvalidMaskFunction = createFunction(Context, 4, "invalid-liveout"); + if (!InvalidMaskFunction) + return; + auto ValidCFI = + InvalidMaskFunction->cfiInfo().addInstruction(rcc::MachineCFIInstruction::rememberState()); + rcc::MachineBasicBlock *InvalidMaskBlock = appendBlock(Context, *InvalidMaskFunction); + if (!ValidCFI || InvalidMaskBlock == nullptr) + return; + auto InvalidMask = std::make_shared<const rcc::RegisterMask>( + std::vector<std::uint64_t>{std::uint64_t{1} << 63U}); + auto InvalidLiveOut = buildOperandInstruction(*InvalidMaskFunction, *Target, *InvalidMaskBlock, + *ValidCFI, std::move(InvalidMask)); + Context.expect(!InvalidLiveOut, + "target validation must reject a live-out mask with unknown physical registers"); +} + +} // namespace + +int main() { + TestContext Context; + testIntegerImmediate(Context); + testOperandFactories(Context); + testFrameIndexRewrite(Context); + testCFIInfo(Context); + testBuilderVerifierAndClone(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineOptimizationTest.cpp b/backend/unittests/CodeGen/MachineOptimizationTest.cpp new file mode 100644 index 0000000..cffd4e1 --- /dev/null +++ b/backend/unittests/CodeGen/MachineOptimizationTest.cpp @@ -0,0 +1,798 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "RISCV.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/LiveVariables.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineCFGUpdater.h" +#include "rcc/CodeGen/MachineCSE.h" +#include "rcc/CodeGen/MachineCombiner.h" +#include "rcc/CodeGen/MachineDCE.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineLICM.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/CodeGen/MachineMemoryFolding.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/MachineSink.h" +#include "rcc/CodeGen/TargetMemoryFolding.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#include <string> +#include <string_view> + +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; +}; + +struct Fixture final { + std::unique_ptr<rcc::MachineFunction> Function; + rcc::MachineBasicBlock *Entry = nullptr; +}; + +[[nodiscard]] rcc::Opcode opcode(rcc::riscv::InstructionOpcode Value) noexcept; + +class TestMemoryFoldingInfo final : public rcc::TargetMemoryFoldingInfo { +public: + explicit TestMemoryFoldingInfo(bool DropMemory = false) noexcept : DropMemory(DropMemory) {} + + [[nodiscard]] rcc::Expected<bool> + canFoldMemoryOperand(const rcc::MachineInstr &Consumer, std::size_t OperandIndex, + const rcc::MachineInstr &MemoryInstruction, const rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return Consumer.opcode() == opcode(rcc::riscv::InstructionOpcode::ADDI) && OperandIndex == 1 && + MemoryInstruction.opcode() == opcode(rcc::riscv::InstructionOpcode::LD); + } + + [[nodiscard]] rcc::Expected<rcc::MachineInstr *> + foldMemoryOperand(rcc::MachineInstr &Consumer, std::size_t, rcc::MachineInstr &MemoryInstruction, + rcc::MachineFunction &Function, + const rcc::TargetDescription &Target) const override { + const rcc::MachineOperand *DestinationOperand = Consumer.operandAt(0); + const rcc::MachineOperand *BaseOperand = MemoryInstruction.operandAt(1); + const rcc::MachineOperand *OffsetOperand = MemoryInstruction.operandAt(2); + const rcc::Register *Destination = + DestinationOperand == nullptr ? nullptr : DestinationOperand->getIf<rcc::Register>(); + const rcc::Register *Base = + BaseOperand == nullptr ? nullptr : BaseOperand->getIf<rcc::Register>(); + const std::int64_t *Offset = + OffsetOperand == nullptr ? nullptr : OffsetOperand->getIf<std::int64_t>(); + rcc::MachineBasicBlock *Block = Consumer.parent(); + if (Destination == nullptr || Base == nullptr || Offset == nullptr || Block == nullptr || + MemoryInstruction.parent() != Block) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "test memory fold received an invalid candidate"); + } + + rcc::MachineIRBuilder Builder(Function, Target); + if (auto Point = Builder.setInsertPointBefore(Consumer); !Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::LD)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Added = Pending->addDef(*Destination); !Added) + return std::unexpected(Added.error()); + if (auto Added = Pending->addUse(*Base); !Added) + return std::unexpected(Added.error()); + if (auto Added = Pending->addImmediate(*Offset); !Added) + return std::unexpected(Added.error()); + for (const rcc::MachineMemOperand *Memory : MemoryInstruction.memOperands()) { + if (Memory == nullptr) + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "test memory fold received a null MMO"); + if (auto Added = Pending->addMemOperand(*Memory); !Added) + return std::unexpected(Added.error()); + } + auto Folded = Pending->build(); + if (!Folded) + return std::unexpected(Folded.error()); + if (DropMemory) { + while (!(*Folded)->memOperands().empty()) { + if (auto Erased = (*Folded)->eraseMemOperand(0); !Erased) + return std::unexpected(Erased.error()); + } + } + if (auto Removed = Block->removeInstruction(MemoryInstruction); !Removed) + return std::unexpected(Removed.error()); + if (auto Removed = Block->removeInstruction(Consumer); !Removed) + return std::unexpected(Removed.error()); + return *Folded; + } + + [[nodiscard]] rcc::Expected<bool> + unfoldMemoryOperand(rcc::MachineInstr &, rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return false; + } + + [[nodiscard]] rcc::Expected<std::optional<rcc::MemorySize>> + getFoldedMemorySize(const rcc::MachineInstr &, const rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + auto Size = rcc::MemorySize::fixed(8); + if (!Size) + return std::unexpected(Size.error()); + return std::optional<rcc::MemorySize>(*Size); + } + +private: + bool DropMemory; +}; + +[[nodiscard]] rcc::Opcode opcode(rcc::riscv::InstructionOpcode Value) noexcept { + return rcc::Opcode::fromValue(static_cast<std::uint32_t>(Value)); +} + +[[nodiscard]] Fixture makeFixture(TestContext &Context, std::string Name, std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "optimization fixture function creation must succeed"); + if (!Function) + return {}; + auto Entry = (*Function)->createBasicBlock(); + Context.expect(Entry.has_value(), "optimization fixture block creation must succeed"); + if (!Entry || !(*Function)->appendBlock(**Entry)) + return {}; + (*Function)->properties().set(rcc::MachineFunctionProperty::IsSSA); + (*Function)->properties().set(rcc::MachineFunctionProperty::Legalized); + (*Function)->properties().set(rcc::MachineFunctionProperty::RegBankSelected); + (*Function)->properties().set(rcc::MachineFunctionProperty::Selected); + return {std::move(*Function), *Entry}; +} + +void setPostRAProperties(rcc::MachineFunction &Function) { + Function.properties().clear(rcc::MachineFunctionProperty::IsSSA); + Function.properties().set(rcc::MachineFunctionProperty::NoPHIs); + Function.properties().set(rcc::MachineFunctionProperty::TiedOpsRewritten); + Function.properties().set(rcc::MachineFunctionProperty::NoVRegs); + Function.properties().set(rcc::MachineFunctionProperty::TracksLiveness); +} + +[[nodiscard]] rcc::Register makeGPR(TestContext &Context, rcc::MachineFunction &Function) { + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Reg = Type ? Function.registerInfo().createVirtualRegister(*Type) + : rcc::Expected<rcc::Register>(std::unexpected(rcc::Error( + rcc::ErrorCode::InternalError, "test LowLevelType creation failed"))); + Context.expect(Reg.has_value(), "optimization virtual register creation must succeed"); + if (!Reg) + return {}; + const auto Bank = + rcc::RegisterBankID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterBank::GPR)); + const auto Class = + rcc::RegisterClassID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterClass::GPR)); + Context.expect(Function.registerInfo().setRegisterBank(*Reg, Bank).has_value() && + Function.registerInfo().setRegisterClass(*Reg, Class).has_value(), + "optimization register constraints must be assigned"); + return *Reg; +} + +[[nodiscard]] rcc::PhysicalRegister physical(TestContext &Context, + rcc::riscv::PhysicalRegisterID ID) { + auto Reg = rcc::riscv::physicalRegister(ID); + Context.expect(Reg.has_value(), "optimization physical register lookup must succeed"); + return Reg.value_or(rcc::PhysicalRegister{}); +} + +[[nodiscard]] rcc::MachineInstr *buildADDI(TestContext &Context, rcc::MachineIRBuilder &Builder, + rcc::Register Destination, rcc::Register Source, + std::int64_t Immediate) { + auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::ADDI)); + if (!Pending || !Pending->addDef(Destination) || !Pending->addUse(Source) || + !Pending->addImmediate(Immediate)) { + Context.expect(false, "ADDI construction must succeed"); + return nullptr; + } + auto Built = Pending->build(); + Context.expect(Built.has_value(), "ADDI insertion must succeed"); + return Built.value_or(nullptr); +} + +[[nodiscard]] rcc::MachineInstr *buildADD(TestContext &Context, rcc::MachineIRBuilder &Builder, + rcc::Register Destination, rcc::Register Left, + rcc::Register Right) { + auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::ADD)); + if (!Pending || !Pending->addDef(Destination) || !Pending->addUse(Left) || + !Pending->addUse(Right)) { + Context.expect(false, "ADD construction must succeed"); + return nullptr; + } + auto Built = Pending->build(); + Context.expect(Built.has_value(), "ADD insertion must succeed"); + return Built.value_or(nullptr); +} + +[[nodiscard]] const rcc::MachineMemOperand * +makeMemory(TestContext &Context, rcc::MachineFunction &Function, bool Store, bool Invariant) { + auto Size = rcc::MemorySize::fixed(8); + auto Align = rcc::Alignment::create(8); + Context.expect(Size.has_value() && Align.has_value(), "memory shape creation must succeed"); + if (!Size || !Align) + return nullptr; + rcc::MemoryAccessFlags Flags{Store ? rcc::MemoryAccessFlag::Store : rcc::MemoryAccessFlag::Load}; + if (Invariant) { + Flags.set(rcc::MemoryAccessFlag::Invariant); + Flags.set(rcc::MemoryAccessFlag::Dereferenceable); + } + rcc::MachineMemOperandDesc Description; + Description.Accesses = Flags; + Description.Size = *Size; + Description.Align = *Align; + Description.Base = Invariant ? rcc::MemoryBase::constantPoolPseudo() : rcc::MemoryBase::stack(); + auto Memory = Function.createMemOperand(std::move(Description)); + Context.expect(Memory.has_value(), "memory operand creation must succeed"); + return Memory.value_or(nullptr); +} + +[[nodiscard]] rcc::MachineInstr *buildStore(TestContext &Context, rcc::MachineIRBuilder &Builder, + rcc::Register Value, rcc::PhysicalRegister StackPointer, + const rcc::MachineMemOperand &Memory) { + auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::SD)); + if (!Pending || !Pending->addUse(Value) || + !Pending->addUse(rcc::Register::fromPhysical(StackPointer)) || !Pending->addImmediate(0) || + !Pending->addMemOperand(Memory)) { + Context.expect(false, "store construction must succeed"); + return nullptr; + } + auto Built = Pending->build(); + Context.expect(Built.has_value(), "store insertion must succeed"); + return Built.value_or(nullptr); +} + +[[nodiscard]] rcc::MachineInstr *buildLoad(TestContext &Context, rcc::MachineIRBuilder &Builder, + rcc::Register Destination, rcc::Register Address, + const rcc::MachineMemOperand &Memory) { + auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::LD)); + if (!Pending || !Pending->addDef(Destination) || !Pending->addUse(Address) || + !Pending->addImmediate(0) || !Pending->addMemOperand(Memory)) { + Context.expect(false, "load construction must succeed"); + return nullptr; + } + auto Built = Pending->build(); + Context.expect(Built.has_value(), "load insertion must succeed"); + return Built.value_or(nullptr); +} + +void testDCEFixedPointAndDebugUses(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "dce", 1); + if (!Fixture.Function || Fixture.Entry == nullptr) + return; + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Entry).has_value(), + "DCE insert point must be established"); + const rcc::PhysicalRegister X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + const rcc::PhysicalRegister X2 = physical(Context, rcc::riscv::PhysicalRegisterID::X2); + + const rcc::Register LiveRoot = makeGPR(Context, *Fixture.Function); + const rcc::Register LiveValue = makeGPR(Context, *Fixture.Function); + const rcc::Register DeadRoot = makeGPR(Context, *Fixture.Function); + const rcc::Register DeadValue = makeGPR(Context, *Fixture.Function); + (void)buildADDI(Context, Builder, LiveRoot, rcc::Register::fromPhysical(X0), 1); + (void)buildADDI(Context, Builder, LiveValue, LiveRoot, 2); + (void)buildADDI(Context, Builder, DeadRoot, rcc::Register::fromPhysical(X0), 3); + (void)buildADDI(Context, Builder, DeadValue, DeadRoot, 4); + + auto Debug = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::DBG_VALUE)); + Context.expect(Debug.has_value() && + Debug->addUse(DeadValue, {rcc::RegisterOperandFlag::Debug}).has_value() && + Debug->build().has_value(), + "debug-only use construction must succeed"); + const rcc::MachineMemOperand *Memory = makeMemory(Context, *Fixture.Function, true, false); + if (Memory == nullptr) + return; + rcc::MachineInstr *Store = buildStore(Context, Builder, LiveValue, X2, *Memory); + rcc::MachineInstr *FrameSetup = buildADDI(Context, Builder, rcc::Register::fromPhysical(X2), + rcc::Register::fromPhysical(X2), -16); + rcc::MachineInstr *FrameDestroy = buildADDI(Context, Builder, rcc::Register::fromPhysical(X2), + rcc::Register::fromPhysical(X2), 16); + Context.expect(FrameSetup != nullptr && FrameDestroy != nullptr, + "DCE frame instruction construction must succeed"); + if (FrameSetup == nullptr || FrameDestroy == nullptr) + return; + FrameSetup->setFlags({rcc::MachineInstrFlag::FrameSetup}); + FrameDestroy->setFlags({rcc::MachineInstrFlag::FrameDestroy}); + for (rcc::MachineInstr *Instruction : {FrameSetup, FrameDestroy}) { + rcc::MachineOperand *Definition = Instruction->operandAt(0); + if (Definition == nullptr) + return; + rcc::RegisterOperandFlags Flags = Definition->registerFlags(); + Flags.set(rcc::RegisterOperandFlag::Dead); + Context.expect(Definition->setRegisterFlags(Flags).has_value(), + "DCE frame definition must accept a dead marker"); + } + const std::size_t Before = Fixture.Entry->instructionCount(); + + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineDCEPass Pass; + auto Result = Pass.run(*Fixture.Function, Target, Analyses); + if (!Result) + std::cerr << "machine DCE failure: " << Result.error().message() << '\n'; + Context.expect(Result.has_value() && Result->Changed, + "machine DCE must remove the debug-only dead chain"); + Context.expect(Fixture.Entry->instructionCount() + 2 == Before, + "machine DCE must reach a fixed point across the dead chain"); + Context.expect(Store != nullptr && Store->isLinked(), "machine DCE must preserve a store"); + Context.expect(FrameSetup->isLinked() && FrameDestroy->isLinked(), + "machine DCE must preserve frame setup and destroy instructions"); + Context.expect(!Fixture.Function->registerInfo().isKnownVirtualRegister(DeadRoot) && + !Fixture.Function->registerInfo().isKnownVirtualRegister(DeadValue), + "machine DCE must retire removed virtual registers"); + rcc::MachineInstr *DebugInstruction = nullptr; + for (rcc::MachineInstr *Instruction = Fixture.Entry->firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + if (Instruction->opcode() == rcc::opcode(rcc::GenericOpcode::DBG_VALUE)) + DebugInstruction = Instruction; + } + const rcc::MachineOperand *DebugOperand = + DebugInstruction == nullptr ? nullptr : DebugInstruction->operandAt(0); + Context.expect(DebugOperand != nullptr && DebugOperand->registerValue().has_value() && + DebugOperand->registerValue()->isNoRegister(), + "machine DCE must clear a dangling debug register use"); +} + +void testMachineCombinerIdentityRules(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "combiner", 6); + if (!Fixture.Function || Fixture.Entry == nullptr) + return; + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Entry).has_value(), + "combiner insert point must be established"); + const rcc::PhysicalRegister X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + const rcc::PhysicalRegister X2 = physical(Context, rcc::riscv::PhysicalRegisterID::X2); + const rcc::Register Seed = makeGPR(Context, *Fixture.Function); + const rcc::Register Identity = makeGPR(Context, *Fixture.Function); + const rcc::Register Consumer = makeGPR(Context, *Fixture.Function); + (void)buildADDI(Context, Builder, Seed, rcc::Register::fromPhysical(X0), 7); + rcc::MachineInstr *IdentityInstruction = buildADDI(Context, Builder, Identity, Seed, 0); + rcc::MachineInstr *ConsumerInstruction = buildADD(Context, Builder, Consumer, Identity, Seed); + const rcc::MachineMemOperand *Memory = makeMemory(Context, *Fixture.Function, true, false); + if (Memory == nullptr) + return; + (void)buildStore(Context, Builder, Consumer, X2, *Memory); + + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineCombinerPass Pass(rcc::riscv::machineCombinerInfo(), + rcc::MachineCombinerStage::SelectedSSA); + auto Result = Pass.run(*Fixture.Function, Target, Analyses); + if (!Result) + std::cerr << "machine combiner failure: " << Result.error().message() << '\n'; + Context.expect(Result.has_value() && Result->Changed, + "machine combiner must apply a registered RV64GC identity rule"); + Context.expect(IdentityInstruction != nullptr && !IdentityInstruction->isLinked() && + !Fixture.Function->registerInfo().isKnownVirtualRegister(Identity), + "machine combiner must erase the identity and retire its result"); + const rcc::MachineOperand *ConsumerUse = + ConsumerInstruction == nullptr ? nullptr : ConsumerInstruction->operandAt(1); + Context.expect(ConsumerUse != nullptr && ConsumerUse->registerValue() == Seed, + "machine combiner executor must rewrite all result uses"); +} + +void testPostRAMachineCombinerIdentity(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "post_ra_combiner", 7); + if (!Fixture.Function || Fixture.Entry == nullptr) + return; + Fixture.Function->properties().clear(rcc::MachineFunctionProperty::IsSSA); + Fixture.Function->properties().set(rcc::MachineFunctionProperty::NoPHIs); + Fixture.Function->properties().set(rcc::MachineFunctionProperty::TiedOpsRewritten); + Fixture.Function->properties().set(rcc::MachineFunctionProperty::NoVRegs); + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Entry).has_value(), + "post-RA combiner insert point must be established"); + const rcc::PhysicalRegister X10 = physical(Context, rcc::riscv::PhysicalRegisterID::X10); + rcc::MachineInstr *Identity = buildADDI(Context, Builder, rcc::Register::fromPhysical(X10), + rcc::Register::fromPhysical(X10), 0); + rcc::MachineInstr *FrameIdentity = buildADDI(Context, Builder, rcc::Register::fromPhysical(X10), + rcc::Register::fromPhysical(X10), 0); + if (FrameIdentity != nullptr) + FrameIdentity->setFlags({rcc::MachineInstrFlag::FrameDestroy}); + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineCombinerPass Pass(rcc::riscv::machineCombinerInfo(), + rcc::MachineCombinerStage::PostRegisterAllocation); + auto Result = Pass.run(*Fixture.Function, Target, Analyses); + Context.expect( + Result.has_value() && Result->Changed && Identity != nullptr && !Identity->isLinked(), + "post-RA combiner must erase a physical-register no-op only when identities match"); + Context.expect(FrameIdentity != nullptr && FrameIdentity->isLinked(), + "post-frame semantics must protect an otherwise removable identity"); +} + +void testCSERegisterAndMemoryRules(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "cse", 2); + if (!Fixture.Function || Fixture.Entry == nullptr) + return; + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Entry).has_value(), + "CSE insert point must be established"); + const rcc::PhysicalRegister X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + + const rcc::Register Seed = makeGPR(Context, *Fixture.Function); + const rcc::Register First = makeGPR(Context, *Fixture.Function); + const rcc::Register Redundant = makeGPR(Context, *Fixture.Function); + const rcc::Register Consumer = makeGPR(Context, *Fixture.Function); + (void)buildADDI(Context, Builder, Seed, rcc::Register::fromPhysical(X0), 9); + rcc::MachineInstr *FirstInstruction = buildADDI(Context, Builder, First, Seed, 7); + rcc::MachineInstr *RedundantInstruction = buildADDI(Context, Builder, Redundant, Seed, 7); + rcc::MachineInstr *ConsumerInstruction = buildADD(Context, Builder, Consumer, Redundant, Seed); + + const rcc::Register MutableLoadOne = makeGPR(Context, *Fixture.Function); + const rcc::Register MutableLoadTwo = makeGPR(Context, *Fixture.Function); + const rcc::Register InvariantLoadOne = makeGPR(Context, *Fixture.Function); + const rcc::Register InvariantLoadTwo = makeGPR(Context, *Fixture.Function); + const rcc::MachineMemOperand *MutableMemory = + makeMemory(Context, *Fixture.Function, false, false); + const rcc::MachineMemOperand *InvariantMemory = + makeMemory(Context, *Fixture.Function, false, true); + if (MutableMemory == nullptr || InvariantMemory == nullptr) + return; + rcc::MachineInstr *MutableOne = buildLoad(Context, Builder, MutableLoadOne, Seed, *MutableMemory); + rcc::MachineInstr *MutableTwo = buildLoad(Context, Builder, MutableLoadTwo, Seed, *MutableMemory); + rcc::MachineInstr *InvariantOne = + buildLoad(Context, Builder, InvariantLoadOne, Seed, *InvariantMemory); + rcc::MachineInstr *InvariantTwo = + buildLoad(Context, Builder, InvariantLoadTwo, Seed, *InvariantMemory); + + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineCSEPass Pass; + auto Result = Pass.run(*Fixture.Function, Target, Analyses); + if (!Result) + std::cerr << "machine CSE failure: " << Result.error().message() << '\n'; + Context.expect(Result.has_value() && Result->Changed, + "machine CSE must eliminate safe redundant expressions"); + Context.expect(FirstInstruction != nullptr && FirstInstruction->isLinked() && + RedundantInstruction != nullptr && !RedundantInstruction->isLinked(), + "machine CSE must keep the dominating expression only"); + const rcc::MachineOperand *ConsumerUse = + ConsumerInstruction == nullptr ? nullptr : ConsumerInstruction->operandAt(1); + Context.expect(ConsumerUse != nullptr && ConsumerUse->registerValue() == First, + "machine CSE must rewrite uses to the dominating result"); + Context.expect(MutableOne != nullptr && MutableOne->isLinked() && MutableTwo != nullptr && + MutableTwo->isLinked(), + "machine CSE must not merge mutable loads"); + Context.expect(InvariantOne != nullptr && InvariantOne->isLinked() && InvariantTwo != nullptr && + !InvariantTwo->isLinked(), + "machine CSE must merge invariant dereferenceable loads"); + Context.expect(!Fixture.Function->registerInfo().isKnownVirtualRegister(Redundant) && + !Fixture.Function->registerInfo().isKnownVirtualRegister(InvariantLoadTwo), + "machine CSE must retire redundant result registers"); +} + +void testCSEBudgetPreservesLegalMIR(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "cse_budget", 3); + if (!Fixture.Function || Fixture.Entry == nullptr) + return; + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Entry).has_value(), + "CSE budget insert point must be established"); + const rcc::PhysicalRegister X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + const rcc::Register Seed = makeGPR(Context, *Fixture.Function); + const rcc::Register First = makeGPR(Context, *Fixture.Function); + const rcc::Register Second = makeGPR(Context, *Fixture.Function); + (void)buildADDI(Context, Builder, Seed, rcc::Register::fromPhysical(X0), 1); + (void)buildADDI(Context, Builder, First, Seed, 2); + rcc::MachineInstr *SecondInstruction = buildADDI(Context, Builder, Second, Seed, 2); + + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineCSEPass Pass({.MaximumComparisons = 0}); + auto Result = Pass.run(*Fixture.Function, Target, Analyses); + if (!Result) + std::cerr << "machine CSE budget failure: " << Result.error().message() << '\n'; + Context.expect(Result.has_value() && !Result->Changed && SecondInstruction != nullptr && + SecondInstruction->isLinked(), + "machine CSE budget exhaustion must stop without rejecting or mutating MIR"); +} + +void testLICMHoistsOnlyInvariantInstructions(TestContext &Context, + const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "licm", 4); + if (!Fixture.Function || Fixture.Entry == nullptr) + return; + auto Header = Fixture.Function->createBasicBlock(); + auto Body = Fixture.Function->createBasicBlock(); + auto Exit = Fixture.Function->createBasicBlock(); + Context.expect(Header.has_value() && Body.has_value() && Exit.has_value() && + Fixture.Function->appendBlock(**Header).has_value() && + Fixture.Function->appendBlock(**Body).has_value() && + Fixture.Function->appendBlock(**Exit).has_value(), + "LICM loop blocks must be created"); + if (!Header || !Body || !Exit) + return; + auto Half = rcc::BranchProbability::fromFraction(1, 2); + Context.expect(Half.has_value(), "LICM branch probability must be created"); + if (!Half) + return; + auto Updater = Fixture.Function->cfgUpdater(); + Context.expect( + Updater.addSuccessor(*Fixture.Entry, **Header, rcc::BranchProbability::one()).has_value() && + Updater.addSuccessor(**Header, **Body, *Half).has_value() && + Updater.addSuccessor(**Header, **Exit, *Half).has_value() && + Updater.addSuccessor(**Body, **Header, rcc::BranchProbability::one()).has_value(), + "LICM loop CFG must be connected"); + + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Entry).has_value(), + "LICM preheader insertion point must be established"); + const rcc::PhysicalRegister X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + const rcc::Register Seed = makeGPR(Context, *Fixture.Function); + (void)buildADDI(Context, Builder, Seed, rcc::Register::fromPhysical(X0), 1); + + Context.expect(Builder.setInsertPoint(**Body).has_value(), + "LICM body insertion point must be established"); + const rcc::Register HoistedValue = makeGPR(Context, *Fixture.Function); + const rcc::Register DependentValue = makeGPR(Context, *Fixture.Function); + rcc::MachineInstr *Hoisted = buildADDI(Context, Builder, HoistedValue, Seed, 8); + rcc::MachineInstr *Dependent = buildADDI(Context, Builder, DependentValue, HoistedValue, 9); + const rcc::Register MutableLoad = makeGPR(Context, *Fixture.Function); + const rcc::MachineMemOperand *Memory = makeMemory(Context, *Fixture.Function, false, false); + rcc::MachineInstr *Load = + Memory == nullptr ? nullptr : buildLoad(Context, Builder, MutableLoad, Seed, *Memory); + + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineLICMPass Pass; + auto Result = Pass.run(*Fixture.Function, Target, Analyses); + if (!Result) + std::cerr << "machine LICM failure: " << Result.error().message() << '\n'; + Context.expect(Result.has_value() && Result->Changed, + "machine LICM must hoist loop-invariant instructions"); + Context.expect(Hoisted != nullptr && Hoisted->parent() == Fixture.Entry && Dependent != nullptr && + Dependent->parent() == Fixture.Entry, + "machine LICM must reach a fixed point for dependent invariants"); + Context.expect(Load != nullptr && Load->parent() == *Body, + "machine LICM must keep mutable loads in the loop"); +} + +void testMachineSinkUsesColdUniqueSuccessor(TestContext &Context, + const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "sink", 5); + if (!Fixture.Function || Fixture.Entry == nullptr) + return; + auto Cold = Fixture.Function->createBasicBlock(); + auto Hot = Fixture.Function->createBasicBlock(); + Context.expect(Cold.has_value() && Hot.has_value() && + Fixture.Function->appendBlock(**Cold).has_value() && + Fixture.Function->appendBlock(**Hot).has_value(), + "machine sink successor blocks must be created"); + if (!Cold || !Hot) + return; + auto ColdProbability = rcc::BranchProbability::fromFraction(1, 10); + auto HotProbability = rcc::BranchProbability::fromFraction(9, 10); + Context.expect(ColdProbability.has_value() && HotProbability.has_value(), + "machine sink probabilities must be created"); + if (!ColdProbability || !HotProbability) + return; + auto Updater = Fixture.Function->cfgUpdater(); + Context.expect(Updater.addSuccessor(*Fixture.Entry, **Cold, *ColdProbability).has_value() && + Updater.addSuccessor(*Fixture.Entry, **Hot, *HotProbability).has_value(), + "machine sink CFG must be connected"); + + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Entry).has_value(), + "machine sink source insertion point must be established"); + const rcc::PhysicalRegister X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + const rcc::Register Seed = makeGPR(Context, *Fixture.Function); + const rcc::Register SunkValue = makeGPR(Context, *Fixture.Function); + (void)buildADDI(Context, Builder, Seed, rcc::Register::fromPhysical(X0), 1); + rcc::MachineInstr *Sunk = buildADDI(Context, Builder, SunkValue, Seed, 4); + + Context.expect(Builder.setInsertPoint(**Cold).has_value(), + "machine sink destination insertion point must be established"); + const rcc::Register Consumer = makeGPR(Context, *Fixture.Function); + rcc::MachineInstr *Use = buildADD(Context, Builder, Consumer, SunkValue, Seed); + + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineSinkPass Pass; + auto Result = Pass.run(*Fixture.Function, Target, Analyses); + if (!Result) + std::cerr << "machine sink failure: " << Result.error().message() << '\n'; + Context.expect(Result.has_value() && Result->Changed, + "machine sink must move a cold-path-only expression"); + Context.expect(Sunk != nullptr && Sunk->parent() == *Cold && Use != nullptr && + Sunk->next() == Use, + "machine sink must place the definition immediately before its first use"); +} + +void testPostRACodeMotion(TestContext &Context, const rcc::TargetDescription &Target) { + auto LICMFixture = makeFixture(Context, "post-ra-licm", 10); + if (!LICMFixture.Function || LICMFixture.Entry == nullptr) + return; + auto Header = LICMFixture.Function->createBasicBlock(); + auto Body = LICMFixture.Function->createBasicBlock(); + auto Exit = LICMFixture.Function->createBasicBlock(); + if (!Header || !Body || !Exit || !LICMFixture.Function->appendBlock(**Header) || + !LICMFixture.Function->appendBlock(**Body) || !LICMFixture.Function->appendBlock(**Exit)) { + Context.expect(false, "post-RA LICM blocks must be created"); + return; + } + auto Half = rcc::BranchProbability::fromFraction(1, 2); + if (!Half) + return; + auto LICMUpdater = LICMFixture.Function->cfgUpdater(); + if (!LICMUpdater.addSuccessor(*LICMFixture.Entry, **Header, rcc::BranchProbability::one()) || + !LICMUpdater.addSuccessor(**Header, **Body, *Half) || + !LICMUpdater.addSuccessor(**Header, **Exit, *Half) || + !LICMUpdater.addSuccessor(**Body, **Header, rcc::BranchProbability::one())) { + Context.expect(false, "post-RA LICM CFG must be connected"); + return; + } + rcc::MachineIRBuilder LICMBuilder(*LICMFixture.Function, Target); + if (!LICMBuilder.setInsertPoint(*LICMFixture.Entry)) + return; + const rcc::Register X0 = + rcc::Register::fromPhysical(physical(Context, rcc::riscv::PhysicalRegisterID::X0)); + const rcc::PhysicalRegister X2Physical = physical(Context, rcc::riscv::PhysicalRegisterID::X2); + const rcc::Register X10 = + rcc::Register::fromPhysical(physical(Context, rcc::riscv::PhysicalRegisterID::X10)); + const rcc::Register X12 = + rcc::Register::fromPhysical(physical(Context, rcc::riscv::PhysicalRegisterID::X12)); + const rcc::Register X13 = + rcc::Register::fromPhysical(physical(Context, rcc::riscv::PhysicalRegisterID::X13)); + (void)buildADDI(Context, LICMBuilder, X10, X0, 1); + if (!LICMBuilder.setInsertPoint(**Header)) + return; + rcc::MachineInstr *Hoisted = buildADDI(Context, LICMBuilder, X12, X10, 4); + if (Hoisted == nullptr) + return; + Hoisted->setFlags({rcc::MachineInstrFlag::NoFPExcept}); + if (!LICMBuilder.setInsertPoint(**Body)) + return; + (void)buildADDI(Context, LICMBuilder, X13, X12, 1); + if (!LICMBuilder.setInsertPoint(**Exit)) + return; + const rcc::MachineMemOperand *LICMMemory = + makeMemory(Context, *LICMFixture.Function, true, false); + if (LICMMemory == nullptr) + return; + (void)buildStore(Context, LICMBuilder, X12, X2Physical, *LICMMemory); + setPostRAProperties(*LICMFixture.Function); + Context.expect(rcc::rebuildMachineLiveness(*LICMFixture.Function, Target).has_value(), + "post-RA LICM fixture liveness must be built"); + rcc::MachineFunctionAnalysisManager LICMAnalyses; + rcc::MachineLICMPass PostRALICM(rcc::MachineCodeMotionStage::PostRegisterAllocation); + auto LICMResult = PostRALICM.run(*LICMFixture.Function, Target, LICMAnalyses); + Context.expect(LICMResult && LICMResult->Changed && Hoisted->parent() == LICMFixture.Entry && + rcc::verifyTrackedMachineLiveness(*LICMFixture.Function, Target).has_value(), + "post-RA LICM must hoist a safe physical-register invariant and repair liveness"); + + auto SinkFixture = makeFixture(Context, "post-ra-sink", 11); + if (!SinkFixture.Function || SinkFixture.Entry == nullptr) + return; + auto Cold = SinkFixture.Function->createBasicBlock(); + auto Hot = SinkFixture.Function->createBasicBlock(); + if (!Cold || !Hot || !SinkFixture.Function->appendBlock(**Cold) || + !SinkFixture.Function->appendBlock(**Hot)) { + return; + } + auto ColdProbability = rcc::BranchProbability::fromFraction(1, 10); + auto HotProbability = rcc::BranchProbability::fromFraction(9, 10); + if (!ColdProbability || !HotProbability) + return; + auto SinkUpdater = SinkFixture.Function->cfgUpdater(); + if (!SinkUpdater.addSuccessor(*SinkFixture.Entry, **Cold, *ColdProbability) || + !SinkUpdater.addSuccessor(*SinkFixture.Entry, **Hot, *HotProbability)) { + return; + } + rcc::MachineIRBuilder SinkBuilder(*SinkFixture.Function, Target); + if (!SinkBuilder.setInsertPoint(*SinkFixture.Entry)) + return; + (void)buildADDI(Context, SinkBuilder, X10, X0, 2); + rcc::MachineInstr *Sunk = buildADDI(Context, SinkBuilder, X12, X10, 8); + if (Sunk == nullptr) + return; + Sunk->setFlags({rcc::MachineInstrFlag::NoFPExcept}); + if (!SinkBuilder.setInsertPoint(**Cold)) + return; + rcc::MachineInstr *ColdUse = buildADDI(Context, SinkBuilder, X13, X12, 1); + setPostRAProperties(*SinkFixture.Function); + Context.expect(rcc::rebuildMachineLiveness(*SinkFixture.Function, Target).has_value(), + "post-RA sink fixture liveness must be built"); + rcc::MachineFunctionAnalysisManager SinkAnalyses; + rcc::MachineSinkPass PostRASink(rcc::MachineCodeMotionStage::PostRegisterAllocation); + auto SinkResult = PostRASink.run(*SinkFixture.Function, Target, SinkAnalyses); + Context.expect(SinkResult && SinkResult->Changed && Sunk->parent() == *Cold && + Sunk->next() == ColdUse && + rcc::verifyTrackedMachineLiveness(*SinkFixture.Function, Target).has_value(), + "post-RA sinking must move a physical definition to its sole cold use"); +} + +void testMemoryFoldingContract(TestContext &Context, const rcc::TargetDescription &Target) { + auto BuildCandidate = [&](std::string Name, std::uint64_t ID) { + Fixture Candidate = makeFixture(Context, std::move(Name), ID); + if (!Candidate.Function || Candidate.Entry == nullptr) + return Candidate; + rcc::MachineIRBuilder Builder(*Candidate.Function, Target); + if (!Builder.setInsertPoint(*Candidate.Entry)) + return Candidate; + const rcc::PhysicalRegister X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + const rcc::PhysicalRegister X2 = physical(Context, rcc::riscv::PhysicalRegisterID::X2); + const rcc::Register Address = makeGPR(Context, *Candidate.Function); + const rcc::Register Loaded = makeGPR(Context, *Candidate.Function); + const rcc::Register FoldedResult = makeGPR(Context, *Candidate.Function); + (void)buildADDI(Context, Builder, Address, rcc::Register::fromPhysical(X0), 0); + const rcc::MachineMemOperand *LoadMemory = + makeMemory(Context, *Candidate.Function, false, false); + const rcc::MachineMemOperand *StoreMemory = + makeMemory(Context, *Candidate.Function, true, false); + if (LoadMemory == nullptr || StoreMemory == nullptr) + return Candidate; + (void)buildLoad(Context, Builder, Loaded, Address, *LoadMemory); + (void)buildADDI(Context, Builder, FoldedResult, Loaded, 0); + (void)buildStore(Context, Builder, FoldedResult, X2, *StoreMemory); + return Candidate; + }; + + Fixture Folded = BuildCandidate("memory-fold", 8); + if (!Folded.Function || Folded.Entry == nullptr) + return; + TestMemoryFoldingInfo GoodInfo; + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineMemoryFoldingPass Pass(GoodInfo); + auto Result = Pass.run(*Folded.Function, Target, Analyses); + if (!Result) + std::cerr << "machine memory folding failure: " << Result.error().message() << '\n'; + std::size_t LoadCount = 0; + rcc::MachineInstr *FoldedLoad = nullptr; + for (rcc::MachineInstr *Instruction = Folded.Entry->firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + if (Instruction->opcode() == opcode(rcc::riscv::InstructionOpcode::LD)) { + ++LoadCount; + FoldedLoad = Instruction; + } + } + Context.expect(Result && Result->Changed && LoadCount == 1 && FoldedLoad != nullptr && + FoldedLoad->memOperands().size() == 1, + "memory folding must replace an adjacent load-use pair and migrate its MMO"); + + Fixture Rejected = BuildCandidate("memory-fold-missing-mmo", 9); + if (!Rejected.Function || Rejected.Entry == nullptr) + return; + rcc::MachineInstr *OriginalLoad = Rejected.Entry->firstInstruction(); + OriginalLoad = OriginalLoad == nullptr ? nullptr : OriginalLoad->next(); + rcc::MachineInstr *OriginalConsumer = OriginalLoad == nullptr ? nullptr : OriginalLoad->next(); + const std::size_t OriginalCount = Rejected.Entry->instructionCount(); + TestMemoryFoldingInfo BadInfo(true); + rcc::MachineMemoryFoldingPass BadPass(BadInfo); + auto Failure = BadPass.run(*Rejected.Function, Target, Analyses); + Context.expect(!Failure && Failure.error().code() == rcc::ErrorCode::VerificationError && + Rejected.Entry->instructionCount() == OriginalCount && + OriginalLoad != nullptr && OriginalLoad->isLinked() && + OriginalConsumer != nullptr && OriginalConsumer->isLinked(), + "memory folding must reject missing MMO migration without mutating the function"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = rcc::riscv::createTargetDescription(); + Context.expect(Target.has_value(), "optimization target construction must succeed"); + if (!Target) + return Context.result(); + testDCEFixedPointAndDebugUses(Context, *Target); + testMachineCombinerIdentityRules(Context, *Target); + testPostRAMachineCombinerIdentity(Context, *Target); + testCSERegisterAndMemoryRules(Context, *Target); + testCSEBudgetPreservesLegalMIR(Context, *Target); + testLICMHoistsOnlyInvariantInstructions(Context, *Target); + testMachineSinkUsesColdUniqueSuccessor(Context, *Target); + testPostRACodeMotion(Context, *Target); + testMemoryFoldingContract(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachinePassManagerTest.cpp b/backend/unittests/CodeGen/MachinePassManagerTest.cpp new file mode 100644 index 0000000..6e6c44d --- /dev/null +++ b/backend/unittests/CodeGen/MachinePassManagerTest.cpp @@ -0,0 +1,426 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachinePassManager.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/CompilationUnit.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/MachineFunction.h" + +#include <atomic> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <memory> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + std::vector<rcc::InstructionDescriptor> Descriptors; + std::vector<rcc::ImmediatePredicate> Predicates; + auto Appended = rcc::appendGenericInstructionInfo(Descriptors, Predicates); + if (!Appended) + return std::unexpected(Appended.error()); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors), std::move(Predicates)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +class BlockCountAnalysis final { +public: + struct Result final { + std::size_t Count = 0; + }; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "BlockCount"; + } + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &Function, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + return Result{Function.blockCount()}; + } +}; + +class NonThreadSafeAnalysis final { +public: + struct Result final {}; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "NonThreadSafe"; + } + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + 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::MachineFunctionAnalysisManager::id<BlockCountAnalysis>()}, + .VerificationStage = std::nullopt, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected<rcc::MachinePassResult> + run(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineFunctionAnalysisManager &Analyses) override { + auto Blocks = Analyses.getResult<BlockCountAnalysis>(Function, Target); + if (!Blocks) + return std::unexpected(Blocks.error()); + rcc::MachinePassResult Result; + Result.Preserved = rcc::PreservedAnalyses::all(); + return Result; + } +}; + +class UnreportedChangePass final : public rcc::MachineFunctionPass { +public: + [[nodiscard]] const rcc::MachinePassContract &contract() const noexcept override { + static const rcc::MachinePassContract Contract{ + .Name = "UnreportedChange", + .Requires = {}, + .Sets = {}, + .Clears = {}, + .RequiredAnalyses = {}, + .VerificationStage = std::nullopt, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected<rcc::MachinePassResult> + run(rcc::MachineFunction &Function, const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) 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 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::MachineFunctionAnalysisManager::id<BlockCountAnalysis>()}, + .VerificationStage = std::nullopt, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected<rcc::MachinePassResult> + run(rcc::MachineFunction &Function, const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) override { + auto Block = Function.createBasicBlock(); + if (!Block) + return std::unexpected(Block.error()); + auto Appended = Function.appendBlock(**Block); + if (!Appended) + return std::unexpected(Appended.error()); + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "intentional mutating pass failure"); + } +}; + +class ParallelCountingPass final : public rcc::MachineFunctionPass { +public: + explicit ParallelCountingPass(std::atomic<std::uint32_t> &Count) : Count(&Count) {} + + [[nodiscard]] const rcc::MachinePassContract &contract() const noexcept override { + static const rcc::MachinePassContract Contract = [] { + rcc::MachinePassContract Value{ + .Name = "ParallelCounting", + .Requires = {}, + .Sets = {}, + .Clears = {}, + .RequiredAnalyses = {}, + .VerificationStage = std::nullopt, + }; + Value.ThreadSafety = rcc::PassThreadSafety::ThreadSafe; + return Value; + }(); + return Contract; + } + + [[nodiscard]] rcc::Expected<rcc::MachinePassResult> + run(rcc::MachineFunction &, const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) override { + Count->fetch_add(1, std::memory_order_relaxed); + return rcc::MachinePassResult{}; + } + +private: + std::atomic<std::uint32_t> *Count; +}; + +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::MachineFunctionAnalysisManager::id<NonThreadSafeAnalysis>(), + }, + .VerificationStage = std::nullopt, + }; + Value.ThreadSafety = rcc::PassThreadSafety::ThreadSafe; + return Value; + }(); + return Contract; + } + + [[nodiscard]] rcc::Expected<rcc::MachinePassResult> + run(rcc::MachineFunction &, const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) override { + return rcc::MachinePassResult{}; + } +}; + +class UnitFunctionCountAnalysis final { +public: + struct Result final { + std::size_t Count = 0; + }; + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "UnitFunctionCount"; + } + [[nodiscard]] static rcc::Expected<Result> run(rcc::CompilationUnit &Unit, + const rcc::TargetDescription &, + rcc::CompilationUnitAnalysisManager &) { + return Result{Unit.functionCount()}; + } +}; + +class ObserveUnitPass final : public rcc::CompilationUnitPass { +public: + [[nodiscard]] const rcc::CompilationUnitPassContract &contract() const noexcept override { + static const rcc::CompilationUnitPassContract Contract{ + .Name = "ObserveUnit", + .RequiredAnalyses = {rcc::CompilationUnitAnalysisManager::id<UnitFunctionCountAnalysis>()}, + .Parameters = {}, + .DeclaredChanges = {}, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected<rcc::CompilationUnitPassResult> + run(rcc::CompilationUnit &Unit, const rcc::TargetDescription &Target, + rcc::CompilationUnitAnalysisManager &Analyses, + rcc::MachineFunctionAnalysisManager &) override { + auto Count = Analyses.getResult<UnitFunctionCountAnalysis>(Unit, Target); + if (!Count) + return std::unexpected(Count.error()); + if ((*Count)->Count != Unit.functionCount()) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "unit analysis returned a stale function count"); + } + return rcc::CompilationUnitPassResult{}; + } +}; + +class MutatingUnitFailurePass final : public rcc::CompilationUnitPass { +public: + [[nodiscard]] const rcc::CompilationUnitPassContract &contract() const noexcept override { + static const rcc::CompilationUnitPassContract Contract{ + .Name = "MutatingUnitFailure", + .RequiredAnalyses = {rcc::CompilationUnitAnalysisManager::id<UnitFunctionCountAnalysis>()}, + .Parameters = {}, + .DeclaredChanges = {rcc::MachinePassChange::BlockLayout}, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected<rcc::CompilationUnitPassResult> + run(rcc::CompilationUnit &Unit, const rcc::TargetDescription &, + rcc::CompilationUnitAnalysisManager &, rcc::MachineFunctionAnalysisManager &) 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 testMachinePassManager(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::MachineFunctionAnalysisManager Analyses; + Context.expect(Analyses + .registerAnalysis<BlockCountAnalysis>({ + .Name = "BlockCount", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + .ThreadSafety = rcc::PassThreadSafety::ThreadSafe, + }) + .has_value(), + "pass analysis registration must succeed"); + + rcc::MachineFunctionPassManager Pipeline; + Context.expect(Pipeline.addPass(std::make_unique<RequiredAnalysisPass>()).has_value(), + "a valid pass must be accepted"); + auto Run = Pipeline.run(**Function, Target, Analyses); + Context.expect(Run.has_value(), "a pass with a materialized required analysis must run"); + + auto ChangedFunction = + rcc::MachineFunction::create("unreported", rcc::MachineFunctionStableID::fromValue(3)); + rcc::MachineFunctionAnalysisManager EmptyAnalyses; + rcc::MachineFunctionPassManager Detecting( + {.VerifyBefore = true, .VerifyEach = true, .DetectUnreportedChanges = true}); + Context.expect(ChangedFunction && + Detecting.addPass(std::make_unique<UnreportedChangePass>()).has_value() && + !Detecting.run(**ChangedFunction, Target, EmptyAnalyses), + "change detection must reject an unreported MachineIR mutation"); + + auto FailedFunction = + rcc::MachineFunction::create("failed", rcc::MachineFunctionStableID::fromValue(4)); + rcc::MachineFunctionAnalysisManager FailureAnalyses; + Context.expect(FailedFunction && FailureAnalyses + .registerAnalysis<BlockCountAnalysis>({ + .Name = "BlockCount", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + }) + .has_value(), + "mutating failure analysis fixtures must be valid"); + rcc::MachineFunctionPassManager FailingPipeline; + Context.expect( + FailedFunction && + FailingPipeline.addPass(std::make_unique<MutatingFailurePass>()).has_value() && + !FailingPipeline.run(**FailedFunction, Target, FailureAnalyses) && + !FailureAnalyses.hasCached(**FailedFunction, + rcc::MachineFunctionAnalysisManager::id<BlockCountAnalysis>()), + "a pass that mutates and fails must not leave stale function analyses cached"); +} + +void testCompilationUnitAndParallelism(TestContext &Context, const rcc::TargetDescription &Target) { + rcc::CompilationUnit Unit; + for (std::size_t Index = 0; Index < 8; ++Index) { + auto Function = Unit.createMachineFunction("f" + std::to_string(Index)); + Context.expect(Function.has_value(), "parallel test function creation must succeed"); + } + + std::atomic<std::uint32_t> Runs = 0; + rcc::MachineFunctionPassManager Parallel; + Context.expect(Parallel.addPass(std::make_unique<ParallelCountingPass>(Runs)).has_value(), + "thread-safe pass registration must succeed"); + rcc::MachineFunctionAnalysisManager Prototype; + auto ParallelRun = Parallel.runFunctions(Unit, Target, Prototype, std::size_t{4}); + Context.expect(ParallelRun.has_value() && Runs == Unit.functionCount(), + "parallel function execution must visit every function exactly once"); + + rcc::MachineFunctionAnalysisManager UnsafePrototype; + Context.expect(UnsafePrototype + .registerAnalysis<NonThreadSafeAnalysis>({ + .Name = "NonThreadSafe", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + .ThreadSafety = rcc::PassThreadSafety::NotThreadSafe, + }) + .has_value(), + "non-thread-safe analysis registration must be explicit"); + rcc::MachineFunctionPassManager UnsafeParallel; + Context.expect( + UnsafeParallel.addPass(std::make_unique<RequiresNonThreadSafeAnalysisPass>()).has_value() && + !UnsafeParallel.runFunctions(Unit, Target, UnsafePrototype, std::size_t{4}), + "parallel pipelines must reject a required analysis not marked thread-safe"); + + rcc::CompilationUnitAnalysisManager UnitAnalyses; + Context.expect(UnitAnalyses + .registerAnalysis<UnitFunctionCountAnalysis>({ + .Name = "UnitFunctionCount", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + }) + .has_value(), + "compilation-unit analysis registration must succeed"); + rcc::MachineFunctionAnalysisManager FunctionAnalyses; + rcc::CompilationUnitPassManager UnitPipeline; + Context.expect(UnitPipeline.addPass(std::make_unique<ObserveUnitPass>()).has_value(), + "compilation-unit pass registration must succeed"); + Context.expect(UnitPipeline.run(Unit, Target, UnitAnalyses, FunctionAnalyses).has_value(), + "compilation-unit pass and analysis must run through their own manager"); + + rcc::MachineFunction *FirstFunction = Unit.functionAt(0); + Context.expect( + FirstFunction != nullptr && + FunctionAnalyses + .registerAnalysis<BlockCountAnalysis>({ + .Name = "BlockCount", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + }) + .has_value() && + FunctionAnalyses.getResult<BlockCountAnalysis>(*FirstFunction, Target).has_value(), + "compilation-unit failure cache fixtures must materialize"); + rcc::CompilationUnitPassManager FailingUnitPipeline; + Context.expect( + FirstFunction != nullptr && + FailingUnitPipeline.addPass(std::make_unique<MutatingUnitFailurePass>()).has_value() && + !FailingUnitPipeline.run(Unit, Target, UnitAnalyses, FunctionAnalyses) && + !UnitAnalyses.hasCached( + Unit, rcc::CompilationUnitAnalysisManager::id<UnitFunctionCountAnalysis>()) && + !FunctionAnalyses.hasCached( + *FirstFunction, rcc::MachineFunctionAnalysisManager::id<BlockCountAnalysis>()), + "a mutating compilation-unit failure must clear unit and function analysis caches"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "machine pass tests require a valid target description"); + if (!Target) + return Context.result(); + + testMachinePassManager(Context, *Target); + testCompilationUnitAndParallelism(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineSchedulingTest.cpp b/backend/unittests/CodeGen/MachineSchedulingTest.cpp new file mode 100644 index 0000000..a3d1de9 --- /dev/null +++ b/backend/unittests/CodeGen/MachineSchedulingTest.cpp @@ -0,0 +1,518 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "RISCV.h" +#include "rcc/CodeGen/LiveVariables.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/MachineScheduler.h" +#include "rcc/CodeGen/PostRAScheduler.h" +#include "rcc/CodeGen/ScheduleDAG.h" +#include "rcc/CodeGen/ScheduleHazardRecognizer.h" +#include "rcc/CodeGen/TargetSchedulingModel.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +#include <array> +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#include <string> +#include <string_view> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +struct Fixture final { + std::unique_ptr<rcc::MachineFunction> Function; + rcc::MachineBasicBlock *Block = nullptr; +}; + +class OnePerCycleHazard final : public rcc::TargetScheduleHazardRecognizer { +public: + [[nodiscard]] std::unique_ptr<rcc::TargetScheduleHazardRecognizer> clone() const override { + return std::make_unique<OnePerCycleHazard>(*this); + } + + [[nodiscard]] rcc::Expected<rcc::ScheduleHazardType> + getHazardType(const rcc::ScheduleUnit &) const override { + return Emitted ? rcc::ScheduleHazardType::Hazard : rcc::ScheduleHazardType::NoHazard; + } + + [[nodiscard]] rcc::Expected<void> emitInstruction(const rcc::ScheduleUnit &) override { + Emitted = true; + return {}; + } + + [[nodiscard]] rcc::Expected<void> advanceCycle() override { + Emitted = false; + return {}; + } + + [[nodiscard]] rcc::Expected<void> recedeCycle() override { + Emitted = false; + return {}; + } + + [[nodiscard]] rcc::Expected<void> reset() override { + Emitted = false; + return {}; + } + +private: + bool Emitted = false; +}; + +class TestSchedulingHooks final : public rcc::TargetSchedulingHooks { +public: + bool MakeADDIBoundary = false; + bool AddMutation = false; + bool PreferSecondUnit = false; + bool EnableDynamicHazards = false; + + [[nodiscard]] bool isSchedulingBoundary(const rcc::MachineInstr &Instruction, + const rcc::TargetDescription &) const noexcept override { + return MakeADDIBoundary && + Instruction.opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>( + rcc::riscv::InstructionOpcode::ADDI)); + } + + [[nodiscard]] rcc::Expected<void> mutateScheduleDAG(rcc::ScheduleDAG &DAG, + const rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::ScheduleDAGMode) const override { + if (!AddMutation || DAG.units().size() < 2) + return {}; + return DAG.addArtificialDependency(0, 1, 2); + } + + [[nodiscard]] std::int64_t candidatePreference(const rcc::ScheduleUnit &Unit, + rcc::ScheduleDirection, + const rcc::MachineBasicBlock &, + std::uint64_t) const noexcept override { + return PreferSecondUnit && Unit.ID == 1 ? 100 : 0; + } + + [[nodiscard]] std::unique_ptr<rcc::TargetScheduleHazardRecognizer> + createHazardRecognizer(rcc::ScheduleDirection) const override { + return EnableDynamicHazards ? std::make_unique<OnePerCycleHazard>() : nullptr; + } +}; + +[[nodiscard]] rcc::Opcode opcode(rcc::riscv::InstructionOpcode Value) noexcept { + return rcc::Opcode::fromValue(static_cast<std::uint32_t>(Value)); +} + +[[nodiscard]] rcc::Expected<rcc::TargetSchedulingModel> +makeSchedulingModel(const rcc::TargetSchedulingHooks *Hooks = nullptr) { + rcc::TargetSchedulingModelDesc Description; + Description.IssueWidth = 2; + Description.DispatchLimit = 2; + for (const rcc::riscv::SchedulingClassInfo &Class : rcc::riscv::schedulingClasses()) { + Description.SchedulingClasses.push_back({ + .ID = Class.ID, + .Name = std::string(Class.Name), + .MicroOps = Class.MicroOps, + .Latency = Class.Latency, + .Resources = {}, + .WriteLatencies = {{0, Class.Latency}}, + .ReadAdvances = {}, + }); + } + Description.PressureSets.push_back({ + .ID = rcc::RegisterPressureSetID::fromValue(1), + .Name = "gpr", + .RegisterClasses = + { + rcc::RegisterClassID::fromValue( + static_cast<std::uint32_t>(rcc::riscv::RegisterClass::GPR)), + }, + .Limit = 27, + }); + Description.Hooks = Hooks; + return rcc::TargetSchedulingModel::create(std::move(Description)); +} + +[[nodiscard]] rcc::Register physical(TestContext &Context, rcc::riscv::PhysicalRegisterID ID) { + auto Value = rcc::riscv::physicalRegister(ID); + Context.expect(Value.has_value(), "physical register creation must succeed"); + return Value ? rcc::Register::fromPhysical(*Value) : rcc::Register{}; +} + +[[nodiscard]] Fixture makeFixture(TestContext &Context, std::string Name, std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "scheduler function creation must succeed"); + if (!Function) + return {}; + auto Block = (*Function)->createBasicBlock(); + Context.expect(Block.has_value() && (*Function)->appendBlock(**Block).has_value(), + "scheduler block creation must succeed"); + if (!Block) + return {}; + (*Function)->properties().set(rcc::MachineFunctionProperty::Legalized); + (*Function)->properties().set(rcc::MachineFunctionProperty::RegBankSelected); + (*Function)->properties().set(rcc::MachineFunctionProperty::Selected); + (*Function)->properties().set(rcc::MachineFunctionProperty::NoPHIs); + (*Function)->properties().set(rcc::MachineFunctionProperty::TiedOpsRewritten); + return {std::move(*Function), *Block}; +} + +[[nodiscard]] rcc::Register makeGPR(TestContext &Context, rcc::MachineFunction &Function) { + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Reg = Type ? Function.registerInfo().createVirtualRegister(*Type) + : rcc::Expected<rcc::Register>(std::unexpected(rcc::Error( + rcc::ErrorCode::InternalError, "test LowLevelType creation failed"))); + Context.expect(Reg.has_value(), "scheduler virtual register creation must succeed"); + if (!Reg) + return {}; + const auto Bank = + rcc::RegisterBankID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterBank::GPR)); + const auto Class = + rcc::RegisterClassID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterClass::GPR)); + Context.expect(Function.registerInfo().setRegisterBank(*Reg, Bank).has_value() && + Function.registerInfo().setRegisterClass(*Reg, Class).has_value(), + "scheduler virtual register constraints must be assigned"); + return *Reg; +} + +[[nodiscard]] rcc::MachineInstr *buildADDI(TestContext &Context, rcc::MachineIRBuilder &Builder, + rcc::Register Destination, rcc::Register Source, + std::int64_t Immediate) { + auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::ADDI)); + if (!Pending || !Pending->addDef(Destination) || !Pending->addUse(Source) || + !Pending->addImmediate(Immediate)) { + Context.expect(false, "scheduler ADDI construction must succeed"); + return nullptr; + } + auto Built = Pending->build(); + Context.expect(Built.has_value(), "scheduler ADDI insertion must succeed"); + return Built.value_or(nullptr); +} + +[[nodiscard]] rcc::MachineInstr *buildBinary(TestContext &Context, rcc::MachineIRBuilder &Builder, + rcc::riscv::InstructionOpcode Opcode, + rcc::Register Destination, rcc::Register Left, + rcc::Register Right) { + auto Pending = Builder.buildInstr(opcode(Opcode)); + if (!Pending || !Pending->addDef(Destination) || !Pending->addUse(Left) || + !Pending->addUse(Right)) { + Context.expect(false, "scheduler binary instruction construction must succeed"); + return nullptr; + } + auto Built = Pending->build(); + Context.expect(Built.has_value(), "scheduler binary instruction insertion must succeed"); + return Built.value_or(nullptr); +} + +[[nodiscard]] bool hasDependency(const rcc::ScheduleUnit &Unit, std::size_t Destination, + rcc::ScheduleDependencyKind Kind) { + for (const rcc::ScheduleDependency &Dependency : Unit.Successors) { + if (Dependency.Destination == Destination && Dependency.Kind == Kind) + return true; + } + return false; +} + +void testDAGRegisterDependencies(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "schedule_dag", 1); + if (!Fixture.Function || Fixture.Block == nullptr) + return; + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Block).has_value(), + "schedule DAG insertion point must be established"); + const rcc::Register X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + const rcc::Register X10 = physical(Context, rcc::riscv::PhysicalRegisterID::X10); + const rcc::Register Value = makeGPR(Context, *Fixture.Function); + const rcc::Register Consumer = makeGPR(Context, *Fixture.Function); + rcc::MachineInstr *Definition = buildADDI(Context, Builder, Value, X0, 1); + rcc::MachineInstr *Use = + buildBinary(Context, Builder, rcc::riscv::InstructionOpcode::ADD, Consumer, Value, X10); + rcc::MachineInstr *Redefinition = buildADDI(Context, Builder, Value, X0, 2); + std::array<rcc::MachineInstr *, 3> Instructions{Definition, Use, Redefinition}; + auto DAG = rcc::ScheduleDAG::build(*Fixture.Block, Instructions, *Fixture.Function, Target, + rcc::riscv::schedulingModel(), + rcc::ScheduleDAGMode::PreRegisterAllocation); + if (!DAG) + std::cerr << "schedule DAG failure: " << DAG.error().message() << '\n'; + Context.expect(DAG.has_value() && DAG->units().size() == 3, + "schedule DAG must create one unit per instruction"); + if (!DAG) + return; + Context.expect(hasDependency(DAG->units()[0], 1, rcc::ScheduleDependencyKind::Data), + "schedule DAG must model virtual-register data dependencies"); + Context.expect(hasDependency(DAG->units()[0], 2, rcc::ScheduleDependencyKind::Output), + "schedule DAG must model virtual-register output dependencies"); + Context.expect(hasDependency(DAG->units()[1], 2, rcc::ScheduleDependencyKind::Anti), + "schedule DAG must model virtual-register anti dependencies"); + Context.expect(DAG->units()[0].Latency != 0 && DAG->units()[0].MicroOps != 0, + "schedule DAG must attach target latency and resource facts"); +} + +void testHazardScoreboard(TestContext &Context) { + auto Hazards = rcc::ScheduleHazardRecognizer::create(rcc::riscv::schedulingModel()); + Context.expect(Hazards.has_value(), "hazard recognizer construction must succeed"); + if (!Hazards) + return; + const auto Memory = rcc::ProcessorResourceID::fromValue(3); + rcc::ScheduleUnit Load; + Load.MicroOps = 1; + Load.Resources = {{Memory, 1}}; + Context.expect(Hazards->issue(Load, 0).has_value(), + "first memory operation must issue in cycle zero"); + auto SecondCycle = Hazards->earliestIssueCycle(Load, 0); + Context.expect(SecondCycle.has_value() && *SecondCycle == 1, + "single-unit memory resource must serialize operations"); + + rcc::ScheduleUnit Wide; + Wide.MicroOps = 5; + auto Fresh = rcc::ScheduleHazardRecognizer::create(rcc::riscv::schedulingModel()); + Context.expect(Fresh.has_value() && Fresh->issue(Wide, 0).has_value(), + "multi-cycle dispatch must accept instructions wider than issue width"); + auto AfterWide = Fresh ? Fresh->earliestIssueCycle(Load, 0) + : rcc::Expected<std::uint64_t>(std::unexpected( + rcc::Error(rcc::ErrorCode::InternalError, "no recognizer"))); + Context.expect(AfterWide.has_value() && *AfterWide == 3, + "multi-cycle dispatch must block the complete dispatch interval"); +} + +void testOperandLatencyAndModelValidation(TestContext &Context) { + const rcc::SchedulingClassID ALU = rcc::SchedulingClassID::fromValue( + static_cast<std::uint32_t>(rcc::riscv::SchedulingClass::ALU)); + const rcc::SchedulingClassID Load = rcc::SchedulingClassID::fromValue( + static_cast<std::uint32_t>(rcc::riscv::SchedulingClass::LOAD)); + auto Forwarded = rcc::riscv::schedulingModel().operandLatency(Load, 0, ALU, 1); + auto Unforwarded = rcc::riscv::schedulingModel().operandLatency(Load, 0, ALU, 0); + auto ALUChain = rcc::riscv::schedulingModel().operandLatency(ALU, 0, ALU, 1); + Context.expect(Forwarded.has_value() && *Forwarded == 2 && Unforwarded.has_value() && + *Unforwarded == 3 && ALUChain.has_value() && *ALUChain == 1, + "operand latency must combine write latency with exact read advance"); + + rcc::TargetSchedulingModelDesc Invalid; + Invalid.SchedulingClasses.push_back({ + .ID = ALU, + .Name = "invalid", + .MicroOps = 1, + .Latency = 1, + .Resources = {}, + .WriteLatencies = {{0, 1}, {0, 2}}, + .ReadAdvances = {}, + }); + auto Rejected = rcc::TargetSchedulingModel::create(std::move(Invalid)); + Context.expect(!Rejected && Rejected.error().code() == rcc::ErrorCode::InvalidArgument, + "scheduling model must reject duplicate operand latency facts"); +} + +void testDynamicTargetHazards(TestContext &Context) { + TestSchedulingHooks Hooks; + Hooks.EnableDynamicHazards = true; + auto Model = makeSchedulingModel(&Hooks); + Context.expect(Model.has_value(), "dynamic-hazard scheduling model construction must succeed"); + if (!Model) + return; + + rcc::ScheduleUnit First; + First.ID = 1; + First.MicroOps = 1; + rcc::ScheduleUnit Second; + Second.ID = 2; + Second.MicroOps = 1; + auto Top = rcc::ScheduleHazardRecognizer::create(*Model, rcc::ScheduleDirection::TopDown); + Context.expect(Top.has_value() && Top->issue(First, 0).has_value(), + "target dynamic hazard must accept the first top-down issue"); + auto TopCycle = Top ? Top->earliestIssueCycle(Second, 0) + : rcc::Expected<std::uint64_t>(std::unexpected( + rcc::Error(rcc::ErrorCode::InternalError, "no top-down recognizer"))); + Context.expect(TopCycle.has_value() && *TopCycle == 1, + "target dynamic hazard must advance the top-down cycle"); + + auto Bottom = rcc::ScheduleHazardRecognizer::create(*Model, rcc::ScheduleDirection::BottomUp); + Context.expect(Bottom.has_value() && Bottom->issue(First, 0).has_value(), + "target dynamic hazard must accept the first bottom-up issue"); + auto BottomCycle = Bottom ? Bottom->earliestIssueCycle(Second, 0) + : rcc::Expected<std::uint64_t>(std::unexpected(rcc::Error( + rcc::ErrorCode::InternalError, "no bottom-up recognizer"))); + Context.expect(BottomCycle.has_value() && *BottomCycle == 1, + "target dynamic hazard must recede the bottom-up cycle"); +} + +void testTargetSchedulingHooks(TestContext &Context, const rcc::TargetDescription &Target) { + TestSchedulingHooks MutationHooks; + MutationHooks.AddMutation = true; + auto MutationModel = makeSchedulingModel(&MutationHooks); + auto Fixture = makeFixture(Context, "schedule_mutation", 4); + if (!MutationModel || !Fixture.Function || Fixture.Block == nullptr) { + Context.expect(false, "target schedule mutation fixture must be created"); + return; + } + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Block).has_value(), + "target schedule mutation insertion point must be established"); + const rcc::Register X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + rcc::MachineInstr *First = + buildADDI(Context, Builder, makeGPR(Context, *Fixture.Function), X0, 1); + rcc::MachineInstr *Second = + buildADDI(Context, Builder, makeGPR(Context, *Fixture.Function), X0, 2); + std::array<rcc::MachineInstr *, 2> Instructions{First, Second}; + auto DAG = rcc::ScheduleDAG::build(*Fixture.Block, Instructions, *Fixture.Function, Target, + *MutationModel, rcc::ScheduleDAGMode::PreRegisterAllocation); + Context.expect(DAG.has_value() && + hasDependency(DAG->units()[0], 1, rcc::ScheduleDependencyKind::Artificial), + "target schedule mutation must add a checked artificial dependency"); + + TestSchedulingHooks PreferenceHooks; + PreferenceHooks.PreferSecondUnit = true; + auto PreferenceModel = makeSchedulingModel(&PreferenceHooks); + if (!PreferenceModel) { + Context.expect(false, "target scheduling preference model must be created"); + return; + } + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineSchedulerPass PreferencePass( + *PreferenceModel, {.MaximumRegionSize = 8, .EnableBidirectionalScheduling = false}); + auto Preferred = PreferencePass.run(*Fixture.Function, Target, Analyses); + Context.expect(Preferred.has_value() && Preferred->Changed && + Fixture.Block->firstInstruction() == Second, + "target candidate preference must affect the production scheduler"); + + auto BoundaryFixture = makeFixture(Context, "schedule_boundary", 5); + TestSchedulingHooks BoundaryHooks; + BoundaryHooks.MakeADDIBoundary = true; + auto BoundaryModel = makeSchedulingModel(&BoundaryHooks); + if (!BoundaryFixture.Function || BoundaryFixture.Block == nullptr || !BoundaryModel) { + Context.expect(false, "target schedule boundary fixture must be created"); + return; + } + rcc::MachineIRBuilder BoundaryBuilder(*BoundaryFixture.Function, Target); + Context.expect(BoundaryBuilder.setInsertPoint(*BoundaryFixture.Block).has_value(), + "target schedule boundary insertion point must be established"); + rcc::MachineInstr *BoundaryFirst = + buildADDI(Context, BoundaryBuilder, makeGPR(Context, *BoundaryFixture.Function), X0, 1); + (void)buildADDI(Context, BoundaryBuilder, makeGPR(Context, *BoundaryFixture.Function), X0, 2); + rcc::MachineFunctionAnalysisManager BoundaryAnalyses; + rcc::MachineSchedulerPass BoundaryPass( + *BoundaryModel, {.MaximumRegionSize = 8, .EnableBidirectionalScheduling = false}); + auto BoundaryResult = BoundaryPass.run(*BoundaryFixture.Function, Target, BoundaryAnalyses); + Context.expect(BoundaryResult.has_value() && !BoundaryResult->Changed && + BoundaryFixture.Block->firstInstruction() == BoundaryFirst, + "target scheduling boundary must split production schedule regions"); +} + +void buildSchedulingChain(TestContext &Context, Fixture &Fixture, + const rcc::TargetDescription &Target, bool PhysicalRegisters, + rcc::MachineInstr *&Short, rcc::MachineInstr *&Long) { + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Block).has_value(), + "scheduler chain insertion point must be established"); + const rcc::Register X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + const rcc::Register X10 = physical(Context, rcc::riscv::PhysicalRegisterID::X10); + const rcc::Register X11 = physical(Context, rcc::riscv::PhysicalRegisterID::X11); + const rcc::Register ShortValue = PhysicalRegisters + ? physical(Context, rcc::riscv::PhysicalRegisterID::X12) + : makeGPR(Context, *Fixture.Function); + const rcc::Register LongValue = PhysicalRegisters + ? physical(Context, rcc::riscv::PhysicalRegisterID::X13) + : makeGPR(Context, *Fixture.Function); + const rcc::Register LongResult = PhysicalRegisters + ? physical(Context, rcc::riscv::PhysicalRegisterID::X14) + : makeGPR(Context, *Fixture.Function); + const rcc::Register ShortResult = PhysicalRegisters + ? physical(Context, rcc::riscv::PhysicalRegisterID::X15) + : makeGPR(Context, *Fixture.Function); + Short = buildADDI(Context, Builder, ShortValue, X0, 1); + Long = buildBinary(Context, Builder, rcc::riscv::InstructionOpcode::DIV, LongValue, X10, X11); + (void)buildBinary(Context, Builder, rcc::riscv::InstructionOpcode::ADD, LongResult, LongValue, + X10); + (void)buildBinary(Context, Builder, rcc::riscv::InstructionOpcode::ADD, ShortResult, ShortValue, + X11); +} + +void testPreRAScheduler(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "pre_ra_schedule", 2); + if (!Fixture.Function || Fixture.Block == nullptr) + return; + rcc::MachineInstr *Short = nullptr; + rcc::MachineInstr *Long = nullptr; + buildSchedulingChain(Context, Fixture, Target, false, Short, Long); + Context.expect(Fixture.Block->firstInstruction() == Short, + "pre-RA fixture must begin in intentionally suboptimal order"); + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineSchedulerPass Pass(rcc::riscv::schedulingModel(), + {.MaximumRegionSize = 64, .EnableBidirectionalScheduling = false}); + auto Result = Pass.run(*Fixture.Function, Target, Analyses); + if (!Result) + std::cerr << "pre-RA scheduler failure: " << Result.error().message() << '\n'; + Context.expect(Result.has_value() && Result->Changed, + "pre-RA scheduler must reorder the latency-critical chain"); + Context.expect(Fixture.Block->firstInstruction() == Long, + "pre-RA scheduler must prioritize the long critical path"); +} + +void testPostRASchedulerAndLivenessRepair(TestContext &Context, + const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "post_ra_schedule", 3); + if (!Fixture.Function || Fixture.Block == nullptr) + return; + Fixture.Function->properties().set(rcc::MachineFunctionProperty::NoVRegs); + Fixture.Function->properties().set(rcc::MachineFunctionProperty::TracksLiveness); + rcc::MachineInstr *Short = nullptr; + rcc::MachineInstr *Long = nullptr; + buildSchedulingChain(Context, Fixture, Target, true, Short, Long); + auto Rebuilt = rcc::rebuildMachineLiveness(*Fixture.Function, Target); + Context.expect(Rebuilt.has_value(), "post-RA fixture liveness rebuild must succeed"); + if (!Rebuilt) + return; + + rcc::MachineFunctionAnalysisManager Analyses; + rcc::PostRASchedulerPass Pass(rcc::riscv::schedulingModel(), + {.MaximumRegionSize = 64, .EnableBidirectionalScheduling = false}); + auto Result = Pass.run(*Fixture.Function, Target, Analyses); + if (!Result) + std::cerr << "post-RA scheduler failure: " << Result.error().message() << '\n'; + Context.expect(Result.has_value() && Result->Changed, + "post-RA scheduler must reorder the latency-critical chain"); + Context.expect(Fixture.Block->firstInstruction() == Long, + "post-RA scheduler must prioritize the long critical path"); + auto Verified = rcc::verifyTrackedMachineLiveness(*Fixture.Function, Target); + if (!Verified) + std::cerr << "post-RA liveness verification failure: " << Verified.error().message() << '\n'; + Context.expect(Verified.has_value(), + "post-RA scheduler must locally repair exact kill/dead state"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = rcc::riscv::createTargetDescription(); + Context.expect(Target.has_value(), "scheduler target creation must succeed"); + if (!Target) + return Context.result(); + testDAGRegisterDependencies(Context, *Target); + testHazardScoreboard(Context); + testOperandLatencyAndModelValidation(Context); + testDynamicTargetHazards(Context); + testTargetSchedulingHooks(Context, *Target); + testPreRAScheduler(Context, *Target); + testPostRASchedulerAndLivenessRepair(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineValueTypesTest.cpp b/backend/unittests/CodeGen/MachineValueTypesTest.cpp new file mode 100644 index 0000000..315d56e --- /dev/null +++ b/backend/unittests/CodeGen/MachineValueTypesTest.cpp @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineIRTypes.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/CodeGen/RegisterMask.h" + +#include <cstdint> +#include <iostream> +#include <string_view> +#include <type_traits> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +void testStrongValues(TestContext &Context) { + static_assert(!std::is_same_v<rcc::FrameIndex, rcc::ConstantPoolIndex>); + static_assert(!std::is_same_v<rcc::ConstantPoolIndex, rcc::JumpTableIndex>); + static_assert(!std::is_same_v<rcc::RegisterBankID, rcc::RegisterClassID>); + + const auto Frame = rcc::FrameIndex::fromValue(7); + const auto Constant = rcc::ConstantPoolIndex::fromValue(7); + Context.expect(Frame.value() == 7 && Constant.value() == 7, + "strong machine indices must preserve their values"); + + const auto Opcode = rcc::Opcode::fromValue(19); + Context.expect(Opcode.isValid() && Opcode.value() == 19, "Opcode must preserve its opaque value"); + Context.expect(!rcc::Opcode{}.isValid(), "Opcode zero must remain invalid"); + + const auto LowLanes = rcc::LaneMask::fromRaw(0x3); + const auto HighLanes = rcc::LaneMask::fromRaw(0x6); + Context.expect(LowLanes.intersects(HighLanes), "overlapping lane masks must intersect"); + Context.expect((LowLanes | HighLanes).rawValue() == 0x7, + "lane mask union must preserve all lanes"); +} + +void testBranchProbability(TestContext &Context) { + auto Half = rcc::BranchProbability::fromFraction(1, 2); + Context.expect(Half.has_value(), "valid branch probability fraction must be accepted"); + if (Half) { + const auto Sum = Half->saturatingAdd(*Half); + const auto RawDistance = rcc::BranchProbability::one().rawValue() - Sum.rawValue(); + Context.expect(RawDistance <= 1, + "two rounded half probabilities must sum to one within one unit"); + Context.expect(Half->complement().rawValue() == Half->rawValue() || + Half->complement().rawValue() + 1 == Half->rawValue(), + "half probability complement must preserve fixed-point rounding"); + } + + Context.expect(!rcc::BranchProbability::fromFraction(1, 0), + "zero branch probability denominator must be rejected"); + Context.expect(!rcc::BranchProbability::fromFraction(3, 2), + "branch probability greater than one must be rejected"); +} + +void testRegisterMask(TestContext &Context) { + std::vector<std::uint64_t> Words(2); + Words[0] = std::uint64_t{1} << 7; + Words[1] = std::uint64_t{1} << 6; + const rcc::RegisterMask Mask(std::move(Words)); + + auto Reg7 = rcc::PhysicalRegister::create(7); + auto Reg70 = rcc::PhysicalRegister::create(70); + auto Reg71 = rcc::PhysicalRegister::create(71); + Context.expect(Reg7 && Reg70 && Reg71, "physical registers for mask test must be valid"); + if (Reg7 && Reg70 && Reg71) { + Context.expect(Mask.preserves(*Reg7), "preserved register mask bit must be one"); + Context.expect(Mask.preserves(*Reg70), "preserved register mask must cross word boundaries"); + Context.expect(Mask.clobbers(*Reg71), "clear register mask bit must mean clobbered"); + } +} + +rcc::MachineMemOperandDesc makeBasicLoadDescription() { + rcc::MachineMemOperandDesc Description; + Description.Accesses = rcc::MemoryAccessFlags{ + rcc::MemoryAccessFlag::Load, + rcc::MemoryAccessFlag::Dereferenceable, + }; + Description.Size = *rcc::MemorySize::fixed(8); + Description.Align = *rcc::Alignment::create(8); + Description.AddressSpace = 0; + Description.Base = rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(3)); + Description.Offset = 16; + Description.AliasSource = rcc::MemoryAliasSourceID::fromValue(11); + return Description; +} + +void testMemoryValueTypes(TestContext &Context) { + Context.expect(!rcc::Alignment::create(0), "zero alignment must be rejected"); + Context.expect(!rcc::Alignment::create(3), "non-power-of-two alignment must be rejected"); + Context.expect(rcc::Alignment::create(16).has_value(), "power-of-two alignment must be accepted"); + Context.expect(!rcc::MemorySize::fixed(0), "zero fixed memory size must be rejected"); + Context.expect(!rcc::MemorySize::scalable(0), "zero scalable memory minimum must be rejected"); + + auto Range = rcc::MemoryOffsetRange::create(-4, 12); + Context.expect(Range && Range->Lower == -4 && Range->Upper == 12, + "valid memory offset range must preserve both bounds"); + Context.expect(!rcc::MemoryOffsetRange::create(1, -1), + "reversed memory offset range must be rejected"); + + const auto Symbol = rcc::MemoryBase::globalSymbol("global"); + Context.expect(Symbol.kind() == rcc::MemoryBase::Kind::GlobalSymbol, + "global memory base must preserve its kind"); + Context.expect(Symbol.symbolName() == "global", "global memory base must preserve its symbol"); + + rcc::GenericConstantData PoisonPointer; + PoisonPointer.Type = rcc::GenericConstantType::pointer(); + PoisonPointer.Kind = rcc::GenericConstantKind::Poison; + auto ConstantBase = rcc::MemoryBase::irConstant(PoisonPointer); + Context.expect(ConstantBase && ConstantBase->kind() == rcc::MemoryBase::Kind::IRConstant && + ConstantBase->constantValue() != nullptr && + *ConstantBase->constantValue() == PoisonPointer, + "IR-constant memory base must preserve a structured pointer constant"); + + rcc::GenericConstantData InvalidIntegerBase; + InvalidIntegerBase.Type = *rcc::GenericConstantType::integer(8); + InvalidIntegerBase.Kind = rcc::GenericConstantKind::Poison; + Context.expect(!rcc::MemoryBase::irConstant(std::move(InvalidIntegerBase)), + "IR-constant memory base must reject a non-pointer constant"); +} + +void testMachineMemOperand(TestContext &Context) { + auto Load = rcc::MachineMemOperand::create(makeBasicLoadDescription()); + Context.expect(Load.has_value(), "complete ordinary load MMO must be accepted"); + if (Load) { + Context.expect(Load->isLoad() && !Load->isStore(), + "ordinary load MMO must preserve access direction"); + Context.expect(!Load->isAtomic() && !Load->isVolatile(), + "ordinary load MMO must remain unordered and non-volatile"); + } + + rcc::MachineMemOperandDesc NoAccess; + Context.expect(!rcc::MachineMemOperand::create(NoAccess), + "MMO without load or store access must be rejected"); + + auto InvariantStore = makeBasicLoadDescription(); + InvariantStore.Accesses.set(rcc::MemoryAccessFlag::Store); + InvariantStore.Accesses.set(rcc::MemoryAccessFlag::Invariant); + Context.expect(!rcc::MachineMemOperand::create(InvariantStore), + "invariant store MMO must be rejected"); + + auto AtomicLoad = makeBasicLoadDescription(); + AtomicLoad.AtomicKind = rcc::AtomicOperation::Load; + AtomicLoad.SuccessOrdering = rcc::AtomicOrdering::Acquire; + Context.expect(rcc::MachineMemOperand::create(AtomicLoad).has_value(), + "acquire atomic load MMO must be accepted"); + + AtomicLoad.SuccessOrdering = rcc::AtomicOrdering::Release; + Context.expect(!rcc::MachineMemOperand::create(AtomicLoad), + "release ordering on atomic load must be rejected"); + + auto CompareExchange = makeBasicLoadDescription(); + CompareExchange.Accesses.set(rcc::MemoryAccessFlag::Store); + CompareExchange.AtomicKind = rcc::AtomicOperation::CompareExchange; + CompareExchange.SuccessOrdering = rcc::AtomicOrdering::AcquireRelease; + CompareExchange.FailureOrdering = rcc::AtomicOrdering::Acquire; + Context.expect(rcc::MachineMemOperand::create(CompareExchange).has_value(), + "compatible compare-exchange orderings must be accepted"); + + CompareExchange.FailureOrdering = rcc::AtomicOrdering::NotAtomic; + Context.expect(rcc::MachineMemOperand::create(CompareExchange).has_value(), + "LLVM MIR compare-exchange MMO without failure ordering must be accepted"); + + CompareExchange.FailureOrdering = rcc::AtomicOrdering::Acquire; + CompareExchange.SuccessOrdering = rcc::AtomicOrdering::Release; + Context.expect(!rcc::MachineMemOperand::create(CompareExchange), + "failure ordering stronger than success must be rejected"); + + auto BadAddressSpace = makeBasicLoadDescription(); + BadAddressSpace.AddressSpace = rcc::LowLevelType::MaxAddressSpace + 1; + Context.expect(!rcc::MachineMemOperand::create(BadAddressSpace), + "MMO address space wider than LowLevelType encoding must be rejected"); + + auto EmptySymbol = makeBasicLoadDescription(); + EmptySymbol.Base = rcc::MemoryBase::externalSymbol(""); + Context.expect(!rcc::MachineMemOperand::create(EmptySymbol), + "empty MMO symbol base must be rejected"); + + auto OffsetAlignment = makeBasicLoadDescription(); + OffsetAlignment.Offset = 12; + OffsetAlignment.Align = *rcc::Alignment::create(4); + OffsetAlignment.BaseAlign = *rcc::Alignment::create(8); + auto OffsetAligned = rcc::MachineMemOperand::create(OffsetAlignment); + Context.expect(OffsetAligned && + OffsetAligned->description().BaseAlign == *rcc::Alignment::create(8), + "MMO base alignment must derive effective alignment through its offset"); + + OffsetAlignment.Align = *rcc::Alignment::create(8); + Context.expect(!rcc::MachineMemOperand::create(OffsetAlignment), + "MMO alignment inconsistent with its base and offset must be rejected"); + + OffsetAlignment.Offset = 16; + OffsetAlignment.Align = *rcc::Alignment::create(8); + OffsetAlignment.BaseAlign = *rcc::Alignment::create(8); + auto RedundantBase = rcc::MachineMemOperand::create(OffsetAlignment); + Context.expect(RedundantBase && !RedundantBase->description().BaseAlign, + "MMO base alignment equal to effective alignment must canonicalize away"); +} + +} // namespace + +int main() { + TestContext Context; + testStrongValues(Context); + testBranchProbability(Context); + testRegisterMask(Context); + testMemoryValueTypes(Context); + testMachineMemOperand(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineVerifierTargetTest.cpp b/backend/unittests/CodeGen/MachineVerifierTargetTest.cpp new file mode 100644 index 0000000..3cc2b56 --- /dev/null +++ b/backend/unittests/CodeGen/MachineVerifierTargetTest.cpp @@ -0,0 +1,485 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/LowLevelType.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/MachineVerifier.h" +#include "rcc/CodeGen/RegisterMask.h" +#include "rcc/CodeGen/TargetDescription.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t DefGPR64Opcode = 1; +constexpr std::uint32_t DefGPR32Opcode = 2; +constexpr std::uint32_t EarlyClobberOpcode = 3; +constexpr std::uint32_t TiedAnyOpcode = 4; +constexpr std::uint32_t CallOpcode = 5; +constexpr std::uint32_t RenamableImplicitOpcode = 6; + +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; +}; + +struct FunctionFixture final { + std::unique_ptr<rcc::MachineFunction> Function; + rcc::MachineBasicBlock *Block = nullptr; +}; + +[[nodiscard]] rcc::PhysicalRegister phys(std::uint32_t Number) { + return *rcc::PhysicalRegister::create(Number); +} + +[[nodiscard]] rcc::OperandDescriptor +registerOperand(rcc::RegisterOperandRole Role, + std::optional<rcc::RegisterClassID> Class = std::nullopt, + std::optional<rcc::RegisterBankID> Bank = std::nullopt, bool AllowSubReg = false, + bool EarlyClobber = false) { + rcc::OperandDescriptor Operand; + Operand.Kinds = {rcc::MachineOperand::Kind::Register}; + Operand.RegisterRole = Role; + Operand.RegClass = Class; + Operand.RegBank = Bank; + Operand.AllowSubReg = AllowSubReg; + Operand.EarlyClobber = EarlyClobber; + return Operand; +} + +[[nodiscard]] rcc::InstructionDescriptor instruction(std::uint32_t OpcodeValue, std::string Name) { + rcc::InstructionDescriptor Instruction; + Instruction.InstructionOpcode = rcc::Opcode::fromValue(OpcodeValue); + Instruction.Name = std::move(Name); + Instruction.Flags = {rcc::InstructionFlag::ProducesCode}; + return Instruction; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + auto DefGPR64 = instruction(DefGPR64Opcode, "TEST_DEF_GPR64"); + DefGPR64.Operands.push_back(registerOperand(rcc::RegisterOperandRole::Def, + rcc::test::TestRegisterClass::GPR64, + rcc::test::TestRegisterBank::GPR, true)); + DefGPR64.NumExplicitDefs = 1; + + auto DefGPR32 = instruction(DefGPR32Opcode, "TEST_DEF_GPR32"); + DefGPR32.Operands.push_back(registerOperand(rcc::RegisterOperandRole::Def, + rcc::test::TestRegisterClass::GPR32, + rcc::test::TestRegisterBank::GPR, true)); + DefGPR32.NumExplicitDefs = 1; + + auto EarlyClobber = instruction(EarlyClobberOpcode, "TEST_EARLY_CLOBBER"); + EarlyClobber.Operands.push_back(registerOperand(rcc::RegisterOperandRole::Def, + rcc::test::TestRegisterClass::GPR64, + rcc::test::TestRegisterBank::GPR, false, true)); + EarlyClobber.Operands.push_back(registerOperand(rcc::RegisterOperandRole::Use, + rcc::test::TestRegisterClass::GPR64, + rcc::test::TestRegisterBank::GPR)); + EarlyClobber.NumExplicitDefs = 1; + + auto TiedAny = instruction(TiedAnyOpcode, "TEST_TIED_ANY"); + auto TiedDef = registerOperand(rcc::RegisterOperandRole::Def); + auto TiedUse = registerOperand(rcc::RegisterOperandRole::Use); + TiedDef.DefaultTiedOperand = 1; + TiedUse.DefaultTiedOperand = 0; + TiedAny.Operands.push_back(TiedDef); + TiedAny.Operands.push_back(TiedUse); + TiedAny.NumExplicitDefs = 1; + + auto Call = instruction(CallOpcode, "TEST_CALL"); + rcc::OperandDescriptor Mask; + Mask.Kinds = {rcc::MachineOperand::Kind::RegisterMask}; + Call.Operands.push_back(Mask); + Call.Flags.set(rcc::InstructionFlag::Call); + Call.Flags.set(rcc::InstructionFlag::HasSideEffects); + + auto RenamableImplicit = instruction(RenamableImplicitOpcode, "TEST_RENAMABLE_IMPLICIT"); + RenamableImplicit.ImplicitDefs.push_back({phys(rcc::test::TestRegister::G0), false, true}); + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(DefGPR64)); + Descriptors.push_back(std::move(DefGPR32)); + Descriptors.push_back(std::move(EarlyClobber)); + Descriptors.push_back(std::move(TiedAny)); + Descriptors.push_back(std::move(Call)); + Descriptors.push_back(std::move(RenamableImplicit)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] FunctionFixture createFunction(TestContext &Context, std::string Name, + std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "target verifier function must be created"); + if (!Function) + return {}; + + auto Block = (*Function)->createBasicBlock(); + Context.expect(Block && (*Function)->appendBlock(**Block).has_value(), + "target verifier block must be linked"); + if (!Block) + return {}; + return FunctionFixture{std::move(*Function), *Block}; +} + +[[nodiscard]] rcc::Expected<void> classify(rcc::MachineRegisterInfo &RegInfo, rcc::Register Reg, + rcc::RegisterBankID Bank, rcc::RegisterClassID Class) { + auto BankResult = RegInfo.setRegisterBank(Reg, Bank); + if (!BankResult) + return std::unexpected(BankResult.error()); + return RegInfo.setRegisterClass(Reg, Class); +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> +appendInstruction(FunctionFixture &Fixture, std::uint32_t OpcodeValue, + std::vector<rcc::MachineOperand> Operands, + std::optional<rcc::MachineOperandTie> Tie = std::nullopt) { + auto Instruction = Fixture.Function->createInstruction(rcc::Opcode::fromValue(OpcodeValue)); + if (!Instruction) + return std::unexpected(Instruction.error()); + for (rcc::MachineOperand &Operand : Operands) { + auto AddResult = (*Instruction)->addOperand(std::move(Operand)); + if (!AddResult) + return std::unexpected(AddResult.error()); + } + if (Tie) { + auto TieResult = (*Instruction)->tieOperands(Tie->DefOperand, Tie->UseOperand); + if (!TieResult) + return std::unexpected(TieResult.error()); + } + auto AppendResult = Fixture.Block->appendInstruction(**Instruction); + if (!AppendResult) + return std::unexpected(AppendResult.error()); + return *Instruction; +} + +void testValidTargetFacts(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction(Context, "valid_target_facts", 1); + if (!Fixture.Function) + return; + + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Reg = Fixture.Function->registerInfo().createVirtualRegister(*Type); + if (!Reg) + return; + Context.expect(classify(Fixture.Function->registerInfo(), *Reg, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64Alloc) + .has_value(), + "valid target verifier vreg must be classified"); + + const auto G0 = phys(rcc::test::TestRegister::G0); + const auto G1 = phys(rcc::test::TestRegister::G1); + Context.expect(Fixture.Function->registerInfo().addLiveIn(G0, *Reg).has_value() && + Fixture.Function->registerInfo() + .reservePhysicalRegister(phys(rcc::test::TestRegister::StackPointer)) + .has_value() && + Fixture.Block->addLiveIn(G0, rcc::LaneMask::fromRaw(0x3)).has_value(), + "valid target live-in and reserved register facts must be recorded"); + + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Block).has_value(), + "valid target builder insertion point must be set"); + auto Def = Builder.buildInstr(rcc::Opcode::fromValue(DefGPR64Opcode)); + Context.expect(Def && Def->addDef(*Reg).has_value() && Def->build(), + "register subclass must satisfy a target operand superclass"); + + auto Early = Builder.buildInstr(rcc::Opcode::fromValue(EarlyClobberOpcode)); + Context.expect( + Early && + Early->addDef(rcc::Register::fromPhysical(G0), {rcc::RegisterOperandFlag::EarlyClobber}) + .has_value() && + Early->addUse(rcc::Register::fromPhysical(G1)).has_value() && Early->build(), + "disjoint physical registers must satisfy early-clobber"); + + auto Call = Builder.buildInstr(rcc::Opcode::fromValue(CallOpcode)); + Context.expect(Call && + Call->addRegisterMask(rcc::test::createTestCallPreservedMask()).has_value() && + Call->build(), + "valid target call mask must build"); + Context.expect(rcc::MachineVerifier::verify(*Fixture.Function, Target).has_value(), + "complete valid target facts must pass MachineVerifier"); +} + +void testSubRegisterProjection(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction(Context, "subregister_projection", 2); + if (!Fixture.Function) + return; + + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Reg = Fixture.Function->registerInfo().createVirtualRegister(*Type); + if (!Reg) + return; + Context.expect(classify(Fixture.Function->registerInfo(), *Reg, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64) + .has_value(), + "subregister source vreg must be classified"); + + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Block).has_value(), + "subregister builder insertion point must be set"); + auto Def = Builder.buildInstr(rcc::Opcode::fromValue(DefGPR32Opcode)); + Context.expect(Def && Def->addDef(*Reg, {}, rcc::test::TestSubReg::Low32).has_value() && + Def->build(), + "projected GPR32Low class must satisfy the GPR32 operand class"); + Context.expect(rcc::MachineVerifier::verify(*Fixture.Function, Target).has_value(), + "valid virtual subregister projection must verify"); +} + +void testUnknownAndMalformedTargetFacts(TestContext &Context, + const rcc::TargetDescription &Target) { + { + auto Fixture = createFunction(Context, "unknown_physreg", 3); + auto Operand = rcc::MachineOperand::registerOperand(rcc::Register::fromPhysical(phys(99)), + {rcc::RegisterOperandFlag::Define}); + if (Fixture.Function && Operand) { + std::vector<rcc::MachineOperand> Operands; + Operands.push_back(std::move(*Operand)); + Context.expect(appendInstruction(Fixture, DefGPR64Opcode, std::move(Operands)) && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject unknown physical registers"); + } + } + { + auto Fixture = createFunction(Context, "physical_subreg", 4); + auto Operand = rcc::MachineOperand::registerOperand( + rcc::Register::fromPhysical(phys(rcc::test::TestRegister::G0)), + {rcc::RegisterOperandFlag::Define}, rcc::test::TestSubReg::Low32); + if (Fixture.Function && Operand) { + std::vector<rcc::MachineOperand> Operands; + Operands.push_back(std::move(*Operand)); + Context.expect(appendInstruction(Fixture, DefGPR64Opcode, std::move(Operands)) && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject physical operands with SubRegIndex"); + } + } + { + auto Fixture = createFunction(Context, "unknown_bank", 5); + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + if (!Fixture.Function || !Type) + return; + auto Reg = Fixture.Function->registerInfo().createVirtualRegister(*Type); + Context.expect(Reg && + Fixture.Function->registerInfo() + .setRegisterBank(*Reg, rcc::RegisterBankID::fromValue(99)) + .has_value() && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject unknown virtual register bank IDs"); + } + { + auto Fixture = createFunction(Context, "wide_bank_value", 6); + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(128); + if (!Fixture.Function || !Type) + return; + auto Reg = Fixture.Function->registerInfo().createVirtualRegister(*Type); + Context.expect(Reg && + Fixture.Function->registerInfo() + .setRegisterBank(*Reg, rcc::test::TestRegisterBank::GPR) + .has_value() && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject LLTs wider than their register bank"); + } + { + auto Fixture = createFunction(Context, "bad_function_livein", 7); + Context.expect(Fixture.Function && + Fixture.Function->registerInfo().addLiveIn(phys(99)).has_value() && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject unknown function physical live-ins"); + } + { + auto Fixture = createFunction(Context, "bad_block_lanes", 8); + Context.expect( + Fixture.Function && + Fixture.Block + ->addLiveIn(phys(rcc::test::TestRegister::G0), rcc::LaneMask::fromRaw(0x10)) + .has_value() && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject target-invalid block live-in lanes"); + } + { + auto Fixture = createFunction(Context, "bad_allocation_hint", 9); + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Reg = Fixture.Function->registerInfo().createVirtualRegister(*Type); + Context.expect(Reg && + classify(Fixture.Function->registerInfo(), *Reg, + rcc::test::TestRegisterBank::FPR, + rcc::test::TestRegisterClass::FPR64) + .has_value() && + Fixture.Function->registerInfo() + .addAllocationHint(*Reg, phys(rcc::test::TestRegister::G0)) + .has_value() && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject allocation hints outside a vreg class"); + } + { + auto Fixture = createFunction(Context, "reserved_allocation_hint", 14); + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Reg = Fixture.Function->registerInfo().createVirtualRegister(*Type); + const auto G0 = phys(rcc::test::TestRegister::G0); + Context.expect(Reg && + classify(Fixture.Function->registerInfo(), *Reg, + rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64) + .has_value() && + Fixture.Function->registerInfo().addAllocationHint(*Reg, G0).has_value() && + Fixture.Function->registerInfo().reservePhysicalRegister(G0).has_value() && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject allocation hints that overlap reserved registers"); + } + { + auto Fixture = createFunction(Context, "incompatible_function_livein", 15); + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Reg = Fixture.Function->registerInfo().createVirtualRegister(*Type); + Context.expect( + Reg && + classify(Fixture.Function->registerInfo(), *Reg, rcc::test::TestRegisterBank::FPR, + rcc::test::TestRegisterClass::FPR64) + .has_value() && + Fixture.Function->registerInfo() + .addLiveIn(phys(rcc::test::TestRegister::G0), *Reg) + .has_value() && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject a function live-in outside its initial vreg constraints"); + } +} + +void testRegMaskEarlyClobberAndTies(TestContext &Context, const rcc::TargetDescription &Target) { + { + auto Fixture = createFunction(Context, "bad_regmask", 10); + auto Mask = std::make_shared<const rcc::RegisterMask>( + std::vector<std::uint64_t>{std::uint64_t{1} << 63U}); + auto Operand = rcc::MachineOperand::registerMask(std::move(Mask)); + if (Fixture.Function && Operand) { + std::vector<rcc::MachineOperand> Operands; + Operands.push_back(std::move(*Operand)); + Context.expect(appendInstruction(Fixture, CallOpcode, std::move(Operands)) && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject RegMask bits for unknown registers"); + } + } + { + auto Fixture = createFunction(Context, "early_overlap", 11); + const auto G0 = phys(rcc::test::TestRegister::G0); + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Block).has_value(), + "early-clobber builder insertion point must be set"); + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(EarlyClobberOpcode)); + Context.expect( + Pending && + Pending + ->addDef(rcc::Register::fromPhysical(G0), {rcc::RegisterOperandFlag::EarlyClobber}) + .has_value() && + Pending->addUse(rcc::Register::fromPhysical(G0)).has_value() && !Pending->build() && + Fixture.Function->allInstructions().empty(), + "Builder must reject overlapping early-clobber before publication"); + + auto Def = rcc::MachineOperand::registerOperand( + rcc::Register::fromPhysical(G0), + {rcc::RegisterOperandFlag::Define, rcc::RegisterOperandFlag::EarlyClobber}); + auto Use = rcc::MachineOperand::registerOperand(rcc::Register::fromPhysical(G0), + {rcc::RegisterOperandFlag::Use}); + if (Def && Use) { + std::vector<rcc::MachineOperand> Operands; + Operands.push_back(std::move(*Def)); + Operands.push_back(std::move(*Use)); + Context.expect(appendInstruction(Fixture, EarlyClobberOpcode, std::move(Operands)) && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject overlapping early-clobber"); + } + } + { + auto Fixture = createFunction(Context, "incompatible_tie", 12); + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto GPR = Fixture.Function->registerInfo().createVirtualRegister(*Type); + auto FPR = Fixture.Function->registerInfo().createVirtualRegister(*Type); + if (!GPR || !FPR) + return; + Context.expect(classify(Fixture.Function->registerInfo(), *GPR, + rcc::test::TestRegisterBank::GPR, rcc::test::TestRegisterClass::GPR64) + .has_value() && + classify(Fixture.Function->registerInfo(), *FPR, + rcc::test::TestRegisterBank::FPR, + rcc::test::TestRegisterClass::FPR64) + .has_value(), + "incompatible tied vregs must have individually valid classes"); + + auto Def = rcc::MachineOperand::registerOperand(*GPR, {rcc::RegisterOperandFlag::Define}); + auto Use = rcc::MachineOperand::registerOperand(*FPR, {rcc::RegisterOperandFlag::Use}); + if (Def && Use) { + std::vector<rcc::MachineOperand> Operands; + Operands.push_back(std::move(*Def)); + Operands.push_back(std::move(*Use)); + Context.expect(appendInstruction(Fixture, TiedAnyOpcode, std::move(Operands), + rcc::MachineOperandTie{0, 1}) && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject tied classes without a physical intersection"); + } + } + { + auto Fixture = createFunction(Context, "reserved_renamable", 13); + const auto G0 = phys(rcc::test::TestRegister::G0); + Context.expect(Fixture.Function->registerInfo().reservePhysicalRegister(G0).has_value(), + "reserved renamable test register must be reserved"); + + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Block).has_value(), + "reserved renamable builder insertion point must be set"); + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(RenamableImplicitOpcode)); + Context.expect(Pending && !Pending->build(), + "Builder must reject a renamable reserved implicit register"); + + const rcc::InstructionDescriptor *Descriptor = + Target.instructionDescriptor(rcc::Opcode::fromValue(RenamableImplicitOpcode)); + if (Descriptor == nullptr) + return; + auto Implicits = Target.createDefaultImplicitOperands(*Descriptor); + if (Implicits) { + Context.expect(appendInstruction(Fixture, RenamableImplicitOpcode, std::move(*Implicits)) && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject a renamable reserved implicit register"); + } + } +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "target-aware verifier test target must be valid"); + if (!Target) + return Context.result(); + + testValidTargetFacts(Context, *Target); + testSubRegisterProjection(Context, *Target); + testUnknownAndMalformedTargetFacts(Context, *Target); + testRegMaskEarlyClobberAndTies(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineVerifierTest.cpp b/backend/unittests/CodeGen/MachineVerifierTest.cpp new file mode 100644 index 0000000..5327148 --- /dev/null +++ b/backend/unittests/CodeGen/MachineVerifierTest.cpp @@ -0,0 +1,738 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineVerifier.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/LowLevelType.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineCFGUpdater.h" +#include "rcc/CodeGen/MachineCompilationStatus.h" +#include "rcc/CodeGen/MachineConstantPool.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineFunctionProperties.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineJumpTableInfo.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t GenericDefOpcode = 1; +constexpr std::uint32_t GenericUseOpcode = 2; +constexpr std::uint32_t CopyOpcode = 3; +constexpr std::uint32_t BranchOpcode = 4; +constexpr std::uint32_t PhiOpcode = 5; +constexpr std::uint32_t FrameOpcode = 6; +constexpr std::uint32_t PseudoOpcode = 7; +constexpr std::uint32_t NopOpcode = 8; +constexpr std::uint32_t ConstantPoolOpcode = 9; +constexpr std::uint32_t JumpTableOpcode = 10; + +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::OperandDescriptor registerDescriptor(rcc::RegisterOperandRole Role) { + rcc::OperandDescriptor Descriptor; + Descriptor.Kinds = {rcc::MachineOperand::Kind::Register}; + Descriptor.RegisterRole = Role; + return Descriptor; +} + +[[nodiscard]] rcc::InstructionDescriptor instructionDescriptor(std::uint32_t OpcodeValue, + std::string Name) { + rcc::InstructionDescriptor Descriptor; + Descriptor.InstructionOpcode = rcc::Opcode::fromValue(OpcodeValue); + Descriptor.Name = std::move(Name); + Descriptor.Flags = {rcc::InstructionFlag::ProducesCode}; + return Descriptor; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + auto GenericDef = instructionDescriptor(GenericDefOpcode, "TEST_GENERIC_DEF"); + GenericDef.Operands.push_back(registerDescriptor(rcc::RegisterOperandRole::Def)); + GenericDef.NumExplicitDefs = 1; + GenericDef.Flags.set(rcc::InstructionFlag::Generic); + GenericDef.Flags.set(rcc::InstructionFlag::AllowedPreLegalizer); + + auto GenericUse = instructionDescriptor(GenericUseOpcode, "TEST_GENERIC_USE"); + GenericUse.Operands.push_back(registerDescriptor(rcc::RegisterOperandRole::Use)); + GenericUse.Flags.set(rcc::InstructionFlag::Generic); + GenericUse.Flags.set(rcc::InstructionFlag::AllowedPreLegalizer); + + auto Copy = instructionDescriptor(CopyOpcode, "TEST_COPY"); + auto CopyDef = registerDescriptor(rcc::RegisterOperandRole::Def); + auto CopyUse = registerDescriptor(rcc::RegisterOperandRole::Use); + CopyDef.DefaultTiedOperand = 1; + CopyUse.DefaultTiedOperand = 0; + Copy.Operands.push_back(CopyDef); + Copy.Operands.push_back(CopyUse); + Copy.NumExplicitDefs = 1; + + auto Branch = instructionDescriptor(BranchOpcode, "TEST_BRANCH"); + rcc::OperandDescriptor Block; + Block.Kinds = {rcc::MachineOperand::Kind::BasicBlock}; + Branch.Operands.push_back(Block); + Branch.Flags.set(rcc::InstructionFlag::Terminator); + Branch.Flags.set(rcc::InstructionFlag::Branch); + + auto Phi = instructionDescriptor(PhiOpcode, "TEST_PHI"); + Phi.Operands.push_back(registerDescriptor(rcc::RegisterOperandRole::Def)); + auto PhiValue = registerDescriptor(rcc::RegisterOperandRole::Use); + PhiValue.VariadicGroup = true; + Phi.Operands.push_back(PhiValue); + rcc::OperandDescriptor PhiBlock; + PhiBlock.Kinds = {rcc::MachineOperand::Kind::BasicBlock}; + PhiBlock.VariadicGroup = true; + Phi.Operands.push_back(PhiBlock); + Phi.NumExplicitDefs = 1; + Phi.Variadic = true; + Phi.Flags.set(rcc::InstructionFlag::Pseudo); + Phi.Flags.set(rcc::InstructionFlag::Phi); + Phi.Owner = rcc::PseudoOwner::PhiElimination; + Phi.Deadline = rcc::PipelineStage::SSAEliminated; + + auto Frame = instructionDescriptor(FrameOpcode, "TEST_FRAME"); + rcc::OperandDescriptor FrameIndex; + FrameIndex.Kinds = {rcc::MachineOperand::Kind::FrameIndex}; + Frame.Operands.push_back(FrameIndex); + + auto Pseudo = instructionDescriptor(PseudoOpcode, "TEST_PSEUDO"); + Pseudo.Flags.set(rcc::InstructionFlag::Pseudo); + Pseudo.Owner = rcc::PseudoOwner::InstructionSelect; + Pseudo.Deadline = rcc::PipelineStage::Selected; + + auto Nop = instructionDescriptor(NopOpcode, "TEST_NOP"); + + auto ConstantPool = instructionDescriptor(ConstantPoolOpcode, "TEST_CONSTANT_POOL"); + rcc::OperandDescriptor ConstantPoolIndex; + ConstantPoolIndex.Kinds = {rcc::MachineOperand::Kind::ConstantPoolIndex}; + ConstantPool.Operands.push_back(ConstantPoolIndex); + + auto JumpTable = instructionDescriptor(JumpTableOpcode, "TEST_JUMP_TABLE"); + rcc::OperandDescriptor JumpTableIndex; + JumpTableIndex.Kinds = {rcc::MachineOperand::Kind::JumpTableIndex}; + JumpTable.Operands.push_back(JumpTableIndex); + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(GenericDef)); + Descriptors.push_back(std::move(GenericUse)); + Descriptors.push_back(std::move(Copy)); + Descriptors.push_back(std::move(Branch)); + Descriptors.push_back(std::move(Phi)); + Descriptors.push_back(std::move(Frame)); + Descriptors.push_back(std::move(Pseudo)); + Descriptors.push_back(std::move(Nop)); + Descriptors.push_back(std::move(ConstantPool)); + Descriptors.push_back(std::move(JumpTable)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> +createFunction(TestContext &Context, std::string Name, std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "machine function creation must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineBasicBlock *createBlock(TestContext &Context, + rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + Context.expect(Block.has_value(), "machine basic block creation must succeed"); + if (!Block) + return nullptr; + Context.expect(Function.appendBlock(**Block).has_value(), + "machine basic block linking must succeed"); + return *Block; +} + +[[nodiscard]] rcc::MachineInstr * +buildNoOperand(TestContext &Context, rcc::MachineIRBuilder &Builder, std::uint32_t OpcodeValue) { + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(OpcodeValue)); + Context.expect(Pending.has_value(), "pending no-operand instruction must be created"); + if (!Pending) + return nullptr; + auto Result = Pending->build(); + Context.expect(Result.has_value(), "no-operand instruction must build"); + if (!Result) + return nullptr; + return *Result; +} + +[[nodiscard]] bool populateValidPreLegalizer(TestContext &Context, rcc::MachineFunction &Function, + const rcc::TargetDescription &Target) { + rcc::MachineBasicBlock *Entry = createBlock(Context, Function); + rcc::MachineBasicBlock *Exit = createBlock(Context, Function); + if (Entry == nullptr || Exit == nullptr) + return false; + + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Value = Function.registerInfo().createVirtualRegister(*Type); + Context.expect(Value.has_value(), "typed verifier virtual register must be allocated"); + if (!Value) + return false; + + rcc::MachineIRBuilder Builder(Function, Target); + Context.expect(Builder.setInsertPoint(*Entry).has_value(), "entry insertion point must be valid"); + auto Def = Builder.buildInstr(rcc::Opcode::fromValue(GenericDefOpcode)); + Context.expect(Def && Def->addDef(*Value).has_value(), "generic definition must be assembled"); + if (!Def || !Def->build()) + return false; + + auto Branch = Builder.buildInstr(rcc::Opcode::fromValue(BranchOpcode)); + Context.expect(Branch && Branch->addBasicBlock(*Exit).has_value(), + "branch operand must be assembled"); + if (!Branch || !Branch->build()) + return false; + + Context.expect( + Function.cfgUpdater().addSuccessor(*Entry, *Exit, rcc::BranchProbability::one()).has_value(), + "valid verifier CFG edge must be created"); + + Context.expect(Builder.setInsertPoint(*Exit).has_value(), "exit insertion point must be valid"); + auto Use = Builder.buildInstr(rcc::Opcode::fromValue(GenericUseOpcode)); + Context.expect(Use && Use->addUse(*Value).has_value(), "generic use must be assembled"); + if (!Use || !Use->build()) + return false; + + Function.properties().set(rcc::MachineFunctionProperty::IsSSA); + Function.properties().set(rcc::MachineFunctionProperty::TracksLiveness); + return true; +} + +void testValidFunctionAndModes(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, "valid", 1); + if (!Target || !Function || !populateValidPreLegalizer(Context, *Function, *Target)) { + return; + } + + Context.expect(rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "well-formed function must pass generic verification"); + Context.expect( + rcc::MachineVerifier::verify( + *Function, *Target, + rcc::MachineVerifierOptions{rcc::MachineVerifierMode::PreLegalizer, std::nullopt, true}) + .has_value(), + "typed SSA input must pass pre-legalizer verification"); + + auto DetachedBlock = Function->createBasicBlock(); + auto DetachedInstruction = Function->createInstruction(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(DetachedBlock && DetachedInstruction, + "detached arena history must be allocatable"); + Context.expect(rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "empty detached arena objects must not invalidate live layout"); + + Function->properties().set(rcc::MachineFunctionProperty::Legalized); + Context.expect( + !rcc::MachineVerifier::verify( + *Function, *Target, + rcc::MachineVerifierOptions{rcc::MachineVerifierMode::PreLegalizer, std::nullopt, true}), + "pre-legalizer mode must reject later-stage properties"); +} + +void testReachableStructuralFailures(TestContext &Context) { + auto Target = createTarget(); + if (!Target) + return; + + auto Unknown = createFunction(Context, "unknown_opcode", 2); + rcc::MachineBasicBlock *UnknownBlock = Unknown ? createBlock(Context, *Unknown) : nullptr; + if (Unknown && UnknownBlock != nullptr) { + auto Instruction = Unknown->createInstruction(rcc::Opcode::fromValue(999)); + Context.expect(Instruction && UnknownBlock->appendInstruction(**Instruction).has_value(), + "unknown opcode negative test must be linked"); + Context.expect(!rcc::MachineVerifier::verify(*Unknown, *Target), + "live unregistered opcode must fail verification"); + } + + auto WrongOperands = createFunction(Context, "wrong_operands", 3); + rcc::MachineBasicBlock *WrongBlock = + WrongOperands ? createBlock(Context, *WrongOperands) : nullptr; + if (WrongOperands && WrongBlock != nullptr) { + auto Instruction = WrongOperands->createInstruction(rcc::Opcode::fromValue(GenericDefOpcode)); + Context.expect(Instruction && WrongBlock->appendInstruction(**Instruction).has_value(), + "wrong-operand negative test must be linked"); + Context.expect(!rcc::MachineVerifier::verify(*WrongOperands, *Target), + "descriptor operand-count mismatch must fail verification"); + } + + auto DetachedChild = createFunction(Context, "detached_child", 4); + if (DetachedChild) { + auto Block = DetachedChild->createBasicBlock(); + auto Instruction = DetachedChild->createInstruction(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(Block && Instruction && (*Block)->appendInstruction(**Instruction).has_value(), + "detached block negative test must contain an instruction"); + Context.expect(!rcc::MachineVerifier::verify(*DetachedChild, *Target), + "detached block retaining instructions must fail verification"); + } + + auto DetachedCFG = createFunction(Context, "detached_cfg", 5); + rcc::MachineBasicBlock *Linked = DetachedCFG ? createBlock(Context, *DetachedCFG) : nullptr; + if (DetachedCFG && Linked != nullptr) { + auto Detached = DetachedCFG->createBasicBlock(); + Context.expect(Detached && DetachedCFG->cfgUpdater() + .addSuccessor(**Detached, *Linked, rcc::BranchProbability::one()) + .has_value(), + "detached CFG negative test edge must be created"); + Context.expect(!rcc::MachineVerifier::verify(*DetachedCFG, *Target), + "detached block retaining CFG state must fail verification"); + } +} + +enum class PhiTestShape : std::uint8_t { + Valid, + MissingPredecessor, + DuplicatePredecessor, + NonPredecessor, + AfterInstruction, +}; + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> +createPhiTestFunction(TestContext &Context, const rcc::TargetDescription &Target, std::string Name, + std::uint64_t ID, PhiTestShape Shape) { + auto Function = createFunction(Context, std::move(Name), ID); + if (!Function) + return nullptr; + rcc::MachineBasicBlock *Left = createBlock(Context, *Function); + rcc::MachineBasicBlock *Right = createBlock(Context, *Function); + rcc::MachineBasicBlock *Merge = createBlock(Context, *Function); + rcc::MachineBasicBlock *Other = createBlock(Context, *Function); + if (Left == nullptr || Right == nullptr || Merge == nullptr || Other == nullptr) + return nullptr; + + auto CFG = Function->cfgUpdater(); + if (!CFG.addSuccessor(*Left, *Merge, rcc::BranchProbability::one()) || + !CFG.addSuccessor(*Right, *Merge, rcc::BranchProbability::one())) { + Context.expect(false, "PHI verifier test CFG must be constructed"); + return nullptr; + } + + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Destination = Type ? Function->registerInfo().createVirtualRegister(*Type) + : rcc::Expected<rcc::Register>{std::unexpected(Type.error())}; + auto LeftValue = Type ? Function->registerInfo().createVirtualRegister(*Type) + : rcc::Expected<rcc::Register>{std::unexpected(Type.error())}; + auto RightValue = Type ? Function->registerInfo().createVirtualRegister(*Type) + : rcc::Expected<rcc::Register>{std::unexpected(Type.error())}; + if (!Destination || !LeftValue || !RightValue) { + Context.expect(false, "PHI verifier test registers must be created"); + return nullptr; + } + + rcc::MachineIRBuilder Builder(*Function, Target); + if (!Builder.setInsertPoint(*Merge)) { + Context.expect(false, "PHI verifier insertion point must be valid"); + return nullptr; + } + if (Shape == PhiTestShape::AfterInstruction && + buildNoOperand(Context, Builder, NopOpcode) == nullptr) + return nullptr; + + auto Phi = Builder.buildInstr(rcc::Opcode::fromValue(PhiOpcode)); + if (!Phi || !Phi->addDef(*Destination) || !Phi->addUse(*LeftValue) || + !Phi->addBasicBlock(*Left)) { + Context.expect(false, "PHI verifier instruction prefix must build"); + return nullptr; + } + if (Shape == PhiTestShape::DuplicatePredecessor) { + if (!Phi->addUse(*RightValue) || !Phi->addBasicBlock(*Left)) + return nullptr; + } else if (Shape == PhiTestShape::NonPredecessor) { + if (!Phi->addUse(*RightValue) || !Phi->addBasicBlock(*Other)) + return nullptr; + } else if (Shape != PhiTestShape::MissingPredecessor) { + if (!Phi->addUse(*RightValue) || !Phi->addBasicBlock(*Right)) + return nullptr; + } + if (!Phi->build()) { + Context.expect(false, "PHI verifier test instruction must publish"); + return nullptr; + } + return Function; +} + +void testPHIStructure(TestContext &Context) { + auto Target = createTarget(); + if (!Target) + return; + auto Valid = createPhiTestFunction(Context, *Target, "valid_phi", 60, PhiTestShape::Valid); + auto Missing = + createPhiTestFunction(Context, *Target, "missing_phi", 61, PhiTestShape::MissingPredecessor); + auto Duplicate = createPhiTestFunction(Context, *Target, "duplicate_phi", 62, + PhiTestShape::DuplicatePredecessor); + auto NonPredecessor = + createPhiTestFunction(Context, *Target, "foreign_phi", 63, PhiTestShape::NonPredecessor); + auto AfterInstruction = + createPhiTestFunction(Context, *Target, "late_phi", 64, PhiTestShape::AfterInstruction); + Context.expect(Valid && rcc::MachineVerifier::verify(*Valid, *Target), + "PHI with one incoming value per predecessor must verify"); + Context.expect(Missing && !rcc::MachineVerifier::verify(*Missing, *Target), + "PHI missing a predecessor must fail verification"); + Context.expect(Duplicate && !rcc::MachineVerifier::verify(*Duplicate, *Target), + "PHI with duplicate incoming blocks must fail verification"); + Context.expect(NonPredecessor && !rcc::MachineVerifier::verify(*NonPredecessor, *Target), + "PHI naming a non-predecessor must fail verification"); + Context.expect(AfterInstruction && !rcc::MachineVerifier::verify(*AfterInstruction, *Target), + "PHI after a code instruction must fail verification"); +} + +void testPropertyFailures(TestContext &Context) { + auto Target = createTarget(); + if (!Target) + return; + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + + auto SSA = createFunction(Context, "ssa", 6); + rcc::MachineBasicBlock *SSABlock = SSA ? createBlock(Context, *SSA) : nullptr; + if (SSA && SSABlock != nullptr) { + auto Value = SSA->registerInfo().createVirtualRegister(*Type); + rcc::MachineIRBuilder Builder(*SSA, *Target); + Context.expect(Value && Builder.setInsertPoint(*SSABlock).has_value(), + "SSA negative test setup must succeed"); + for (int Index = 0; Index != 2 && Value; ++Index) { + auto Def = Builder.buildInstr(rcc::Opcode::fromValue(GenericDefOpcode)); + Context.expect(Def && Def->addDef(*Value).has_value() && Def->build(), + "SSA negative test definition must build"); + } + SSA->properties().set(rcc::MachineFunctionProperty::IsSSA); + Context.expect(!rcc::MachineVerifier::verify(*SSA, *Target), + "IsSSA must reject a multiply-defined virtual register"); + } + + auto PHI = createFunction(Context, "phi", 7); + rcc::MachineBasicBlock *PHIBlock = PHI ? createBlock(Context, *PHI) : nullptr; + if (PHI && PHIBlock != nullptr) { + auto Value = PHI->registerInfo().createVirtualRegister(*Type); + rcc::MachineIRBuilder Builder(*PHI, *Target); + Context.expect(Value && Builder.setInsertPoint(*PHIBlock).has_value(), + "PHI negative test setup must succeed"); + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(PhiOpcode)); + Context.expect(Pending && Pending->addDef(*Value).has_value() && Pending->build(), + "PHI negative test instruction must build"); + PHI->properties().set(rcc::MachineFunctionProperty::NoPHIs); + Context.expect(!rcc::MachineVerifier::verify(*PHI, *Target), + "NoPHIs must reject a PHI instruction"); + } + + auto NoVRegs = createFunction(Context, "no_vregs", 8); + if (NoVRegs) { + rcc::MachineBasicBlock *Block = createBlock(Context, *NoVRegs); + auto Value = NoVRegs->registerInfo().createVirtualRegister(*Type); + rcc::MachineIRBuilder Builder(*NoVRegs, *Target); + Context.expect(Block != nullptr && Builder.setInsertPoint(*Block).has_value(), + "NoVRegs negative test setup must succeed"); + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(GenericDefOpcode)); + Context.expect(Value && Pending && Pending->addDef(*Value).has_value(), + "NoVRegs linked history must be assembled"); + if (!Value || !Pending || Block == nullptr) + return; + auto Built = Pending->build(); + NoVRegs->properties().set(rcc::MachineFunctionProperty::NoVRegs); + Context.expect(Value && Built && !rcc::MachineVerifier::verify(*NoVRegs, *Target), + "NoVRegs must reject a live virtual register"); + Context.expect(Value && Built && Block != nullptr && + Block->removeInstruction(**Built).has_value() && + NoVRegs->registerInfo().eraseVirtualRegister(*Value).has_value() && + rcc::MachineVerifier::verify(*NoVRegs, *Target).has_value(), + "NoVRegs must accept tombstoned def-use history"); + } + + auto Frame = createFunction(Context, "frame_index", 9); + rcc::MachineBasicBlock *FrameBlock = Frame ? createBlock(Context, *Frame) : nullptr; + if (Frame && FrameBlock != nullptr) { + rcc::StackObjectDesc StackSlot; + StackSlot.Size = 8; + StackSlot.Align = *rcc::Alignment::create(8); + auto FrameIndex = Frame->frameInfo().createObject(StackSlot); + rcc::MachineIRBuilder Builder(*Frame, *Target); + Context.expect(FrameIndex && Builder.setInsertPoint(*FrameBlock).has_value(), + "frame-index negative test setup must succeed"); + if (!FrameIndex) + return; + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(FrameOpcode)); + Context.expect(Pending && Pending->addFrameIndex(*FrameIndex).has_value() && Pending->build(), + "frame-index negative test instruction must build"); + Frame->properties().set(rcc::MachineFunctionProperty::NoFrameIndices); + Context.expect(!rcc::MachineVerifier::verify(*Frame, *Target), + "NoFrameIndices must reject a frame-index operand"); + } + + auto Pseudo = createFunction(Context, "pseudo", 10); + rcc::MachineBasicBlock *PseudoBlock = Pseudo ? createBlock(Context, *Pseudo) : nullptr; + if (Pseudo && PseudoBlock != nullptr) { + rcc::MachineIRBuilder Builder(*Pseudo, *Target); + Context.expect(Builder.setInsertPoint(*PseudoBlock).has_value() && + buildNoOperand(Context, Builder, PseudoOpcode) != nullptr, + "pseudo negative test instruction must build"); + Pseudo->properties().set(rcc::MachineFunctionProperty::NoExecutablePseudos); + Context.expect(!rcc::MachineVerifier::verify(*Pseudo, *Target), + "NoExecutablePseudos must reject an executable pseudo"); + } +} + +void testSelectedStateAndPseudoDeadline(TestContext &Context) { + auto Target = createTarget(); + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + if (!Target) + return; + + auto Tied = createFunction(Context, "tied", 11); + rcc::MachineBasicBlock *TiedBlock = Tied ? createBlock(Context, *Tied) : nullptr; + if (Tied && TiedBlock != nullptr) { + auto Def = Tied->registerInfo().createVirtualRegister(*Type); + auto Use = Tied->registerInfo().createVirtualRegister(*Type); + if (!Def || !Use) + return; + for (const rcc::Register Reg : {*Def, *Use}) { + Context.expect(Tied->registerInfo() + .setRegisterBank(Reg, rcc::RegisterBankID::fromValue(1)) + .has_value() && + Tied->registerInfo() + .setRegisterClass(Reg, rcc::RegisterClassID::fromValue(1)) + .has_value(), + "selected-state register constraints must be recorded"); + } + + rcc::MachineIRBuilder Builder(*Tied, *Target); + Context.expect(Builder.setInsertPoint(*TiedBlock).has_value(), + "tied-state insertion point must be valid"); + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(CopyOpcode)); + Context.expect(Pending && Pending->addDef(*Def).has_value() && + Pending->addUse(*Use).has_value() && Pending->build(), + "tied-state instruction must build"); + for (const rcc::MachineFunctionProperty Property : + {rcc::MachineFunctionProperty::Legalized, rcc::MachineFunctionProperty::RegBankSelected, + rcc::MachineFunctionProperty::Selected, rcc::MachineFunctionProperty::NoPHIs, + rcc::MachineFunctionProperty::TiedOpsRewritten}) { + Tied->properties().set(Property); + } + Context.expect(!rcc::MachineVerifier::verify(*Tied, *Target), + "TiedOpsRewritten must reject unequal tied registers"); + rcc::MachineInstr *Instruction = TiedBlock->firstInstruction(); + Context.expect(Instruction != nullptr && + Instruction->operandAt(1)->setRegister(*Def).has_value() && + rcc::MachineVerifier::verify(*Tied, *Target).has_value(), + "equal tied registers must satisfy TiedOpsRewritten"); + } + + auto Pseudo = createFunction(Context, "pseudo_deadline", 12); + rcc::MachineBasicBlock *Block = Pseudo ? createBlock(Context, *Pseudo) : nullptr; + if (Pseudo && Block != nullptr) { + rcc::MachineIRBuilder Builder(*Pseudo, *Target); + Context.expect(Builder.setInsertPoint(*Block).has_value() && + buildNoOperand(Context, Builder, PseudoOpcode) != nullptr, + "deadline pseudo must build"); + Context.expect( + rcc::MachineVerifier::verify(*Pseudo, *Target, + rcc::MachineVerifierOptions{rcc::MachineVerifierMode::Generic, + rcc::PipelineStage::Selected}) + .has_value(), + "pseudo may remain at its declared deadline"); + Context.expect(!rcc::MachineVerifier::verify( + *Pseudo, *Target, + rcc::MachineVerifierOptions{rcc::MachineVerifierMode::Generic, + rcc::PipelineStage::RegisterAllocated}), + "pseudo must not survive beyond its declared deadline"); + } + + auto Dependency = createFunction(Context, "property_dependency", 13); + if (Dependency) { + Dependency->properties().set(rcc::MachineFunctionProperty::Selected); + Context.expect(!rcc::MachineVerifier::verify(*Dependency, *Target), + "Selected must require prior pipeline properties"); + Dependency->properties().clear(rcc::MachineFunctionProperty::Selected); + Dependency->properties().set(rcc::MachineFunctionProperty::FrameFinalized); + Context.expect(!rcc::MachineVerifier::verify(*Dependency, *Target), + "FrameFinalized must require NoVRegs and NoFrameIndices"); + } + + auto SSAExitDependency = createFunction(Context, "ssa_exit_property_dependency", 65); + if (SSAExitDependency) { + for (const rcc::MachineFunctionProperty Property : + {rcc::MachineFunctionProperty::Legalized, rcc::MachineFunctionProperty::RegBankSelected, + rcc::MachineFunctionProperty::Selected, rcc::MachineFunctionProperty::TiedOpsRewritten}) { + SSAExitDependency->properties().set(Property); + } + Context.expect(!rcc::MachineVerifier::verify(*SSAExitDependency, *Target), + "TiedOpsRewritten must require NoPHIs"); + SSAExitDependency->properties().set(rcc::MachineFunctionProperty::NoPHIs); + Context.expect(rcc::MachineVerifier::verify(*SSAExitDependency, *Target).has_value(), + "TiedOpsRewritten dependency must be satisfied by NoPHIs"); + } +} + +void testFrameStateFailures(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, "frame_references", 14); + rcc::MachineBasicBlock *Block = Function ? createBlock(Context, *Function) : nullptr; + if (!Target || !Function || Block == nullptr) + return; + + rcc::MachineIRBuilder Builder(*Function, *Target); + Context.expect(Builder.setInsertPoint(*Block).has_value(), + "frame reference insertion point must be valid"); + auto Unknown = Builder.buildInstr(rcc::Opcode::fromValue(FrameOpcode)); + Context.expect(Unknown && Unknown->addFrameIndex(rcc::FrameIndex::fromValue(99)).has_value() && + !Unknown->build() && Function->allInstructions().empty(), + "builder must reject an unknown frame index before instruction allocation"); + + rcc::StackObjectDesc StackSlot; + StackSlot.Size = 8; + StackSlot.Align = *rcc::Alignment::create(8); + auto FrameIndex = Function->frameInfo().createObject(StackSlot); + if (!FrameIndex) + return; + auto Live = Builder.buildInstr(rcc::Opcode::fromValue(FrameOpcode)); + Context.expect(Live && Live->addFrameIndex(*FrameIndex).has_value() && Live->build(), + "known live frame index must build"); + Context.expect(Function->frameInfo().markObjectDead(*FrameIndex).has_value() && + !rcc::MachineVerifier::verify(*Function, *Target), + "verifier must reject an instruction that references a dead stack object"); + + auto Lifetime = createFunction(Context, "frame_lifetime", 15); + if (!Lifetime) + return; + rcc::StackObjectDesc LifetimeSlot; + LifetimeSlot.Size = 8; + LifetimeSlot.Align = *rcc::Alignment::create(8); + LifetimeSlot.Lifetime.push_back( + {rcc::MachineInstrStableID::fromValue(1), rcc::MachineInstrStableID::fromValue(2)}); + Context.expect(Lifetime->frameInfo().createObject(LifetimeSlot).has_value() && + !rcc::MachineVerifier::verify(*Lifetime, *Target), + "verifier must reject stack lifetimes that reference unknown instructions"); +} + +void testTableReferenceFailures(TestContext &Context) { + auto Target = createTarget(); + auto ConstantFunction = createFunction(Context, "constant_reference", 16); + rcc::MachineBasicBlock *ConstantBlock = + ConstantFunction ? createBlock(Context, *ConstantFunction) : nullptr; + if (!Target || !ConstantFunction || ConstantBlock == nullptr) + return; + + rcc::MachineConstantPoolEntryDesc ConstantDescription; + auto Integer = rcc::APInt::fromUnsigned(32, 0x01010101U); + ConstantDescription.Value = std::move(*rcc::GenericConstantData::integer(std::move(*Integer))); + ConstantDescription.Size = 4; + ConstantDescription.Align = *rcc::Alignment::create(4); + auto ConstantIndex = ConstantFunction->constantPool().addEntry(std::move(ConstantDescription)); + rcc::MachineIRBuilder ConstantBuilder(*ConstantFunction, *Target); + Context.expect(ConstantIndex && ConstantBuilder.setInsertPoint(*ConstantBlock).has_value(), + "constant-pool verifier setup must succeed"); + if (!ConstantIndex) + return; + auto ConstantInstruction = ConstantBuilder.buildInstr(rcc::Opcode::fromValue(ConstantPoolOpcode)); + Context.expect(ConstantInstruction && + ConstantInstruction->addConstantPool(*ConstantIndex).has_value() && + ConstantInstruction->build() && + ConstantFunction->constantPool().eraseEntry(*ConstantIndex).has_value() && + !rcc::MachineVerifier::verify(*ConstantFunction, *Target), + "verifier must reject a tombstoned constant-pool reference"); + + auto JumpFunction = createFunction(Context, "jump_reference", 17); + rcc::MachineBasicBlock *JumpBlock = JumpFunction ? createBlock(Context, *JumpFunction) : nullptr; + if (!JumpFunction || JumpBlock == nullptr) + return; + rcc::MachineJumpTableDesc JumpDescription; + JumpDescription.Targets = {JumpBlock}; + auto JumpIndex = JumpFunction->jumpTableInfo().createTable(std::move(JumpDescription)); + rcc::MachineIRBuilder JumpBuilder(*JumpFunction, *Target); + Context.expect(JumpIndex && JumpBuilder.setInsertPoint(*JumpBlock).has_value(), + "jump-table verifier setup must succeed"); + if (!JumpIndex) + return; + auto JumpInstruction = JumpBuilder.buildInstr(rcc::Opcode::fromValue(JumpTableOpcode)); + Context.expect(JumpInstruction && JumpInstruction->addJumpTable(*JumpIndex).has_value() && + JumpInstruction->build() && + JumpFunction->jumpTableInfo().eraseTable(*JumpIndex).has_value() && + !rcc::MachineVerifier::verify(*JumpFunction, *Target), + "verifier must reject a tombstoned jump-table reference"); +} + +void testCompilationStatusReferences(TestContext &Context) { + auto Target = createTarget(); + auto Unknown = createFunction(Context, "unknown_failure_instruction", 18); + if (!Target || !Unknown) + return; + + rcc::MachineCompilationFailure UnknownFailure; + UnknownFailure.Diagnostic.Code = rcc::ErrorCode::Unsupported; + UnknownFailure.Diagnostic.Message = "selection failed"; + UnknownFailure.FailureInstruction = rcc::MachineInstrStableID::fromValue(99); + UnknownFailure.FailureStage = rcc::PipelineStage::Selected; + Context.expect( + Unknown->compilationStatus() + .fail(rcc::MachineCompilationState::SelectionFailed, std::move(UnknownFailure)) + .has_value() && + !rcc::MachineVerifier::verify(*Unknown, *Target), + "verifier must reject compilation status that references an unknown instruction"); + + auto Live = createFunction(Context, "live_failure_instruction", 19); + rcc::MachineBasicBlock *Block = Live ? createBlock(Context, *Live) : nullptr; + if (!Live || Block == nullptr) + return; + rcc::MachineIRBuilder Builder(*Live, *Target); + Context.expect(Builder.setInsertPoint(*Block).has_value(), + "live failure instruction insertion point must be valid"); + rcc::MachineInstr *Instruction = buildNoOperand(Context, Builder, NopOpcode); + if (Instruction == nullptr) + return; + + rcc::MachineCompilationFailure LiveFailure; + LiveFailure.Diagnostic.Code = rcc::ErrorCode::Unsupported; + LiveFailure.Diagnostic.Message = "finalization failed"; + LiveFailure.FailureInstruction = Instruction->stableID(); + LiveFailure.FailureStage = rcc::PipelineStage::PostRAFinalized; + Context.expect( + Live->compilationStatus() + .fail(rcc::MachineCompilationState::FinalizationFailed, std::move(LiveFailure)) + .has_value() && + rcc::MachineVerifier::verify(*Live, *Target).has_value(), + "verifier must accept a structurally valid failed function"); +} + +} // namespace + +int main() { + TestContext Context; + testValidFunctionAndModes(Context); + testReachableStructuralFailures(Context); + testPHIStructure(Context); + testPropertyFailures(Context); + testSelectedStateAndPseudoDeadline(Context); + testFrameStateFailures(Context); + testTableReferenceFailures(Context); + testCompilationStatusReferences(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/PBQPTest.cpp b/backend/unittests/CodeGen/PBQPTest.cpp new file mode 100644 index 0000000..073de16 --- /dev/null +++ b/backend/unittests/CodeGen/PBQPTest.cpp @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/PBQP/Graph.h" +#include "rcc/CodeGen/PBQP/Math.h" +#include "rcc/CodeGen/PBQP/Solver.h" + +#include <algorithm> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <string_view> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +void testMath(TestContext &Context) { + using namespace rcc::pbqp; + Context.expect( + addCosts(InfiniteCost, -4) == InfiniteCost && addCosts(InfiniteCost - 1, 2) == InfiniteCost && + addCosts(NegativeInfiniteCost + 1, -2) == NegativeInfiniteCost && addCosts(7, -3) == 4, + "PBQP cost arithmetic must saturate deterministically"); + + CostVector Vector(std::vector<Cost>{8, -2, 4}); + Context.expect(Vector.minimumIndex() == 1, + "PBQP cost vectors must choose the first stable minimum"); + CostMatrix Matrix(2, 3); + Matrix.at(0, 0) = 1; + Matrix.at(0, 1) = 2; + Matrix.at(0, 2) = 3; + Matrix.at(1, 0) = 4; + Matrix.at(1, 1) = 5; + Matrix.at(1, 2) = 6; + const CostMatrix Transposed = Matrix.transpose(); + Context.expect(Transposed.rows() == 3 && Transposed.columns() == 2 && Transposed.at(2, 1) == 6 && + Transposed.at(1, 0) == 2, + "PBQP matrix transposition must preserve orientation"); +} + +void testGraphValidation(TestContext &Context) { + using namespace rcc::pbqp; + Graph Problem; + auto Empty = Problem.addNode(CostVector(0)); + auto First = Problem.addNode(CostVector(std::vector<Cost>{0, 1}), 20); + auto Second = Problem.addNode(CostVector(std::vector<Cost>{2, 0, 3}), 10); + Context.expect(!Empty && First && Second, + "PBQP graphs must reject empty nodes and accept stable nonempty nodes"); + if (!First || !Second) + return; + auto Self = Problem.addEdge(*First, *First, CostMatrix(2, 2)); + auto WrongShape = Problem.addEdge(*First, *Second, CostMatrix(2, 2)); + CostMatrix EdgeCosts(2, 3); + EdgeCosts.at(1, 2) = 7; + auto Edge = Problem.addEdge(*Second, *First, EdgeCosts.transpose(), 99); + auto Duplicate = Problem.addEdge(*First, *Second, CostMatrix(2, 3)); + Context.expect(!Self && !WrongShape && Edge && !Duplicate && Problem.verify().has_value(), + "PBQP graph edges must enforce identity, dimensions, orientation, and uniqueness"); + if (!Edge) + return; + const rcc::pbqp::Edge *Stored = Problem.edge(*Edge); + Context.expect(Stored != nullptr && Stored->First == *First && Stored->Second == *Second && + Stored->Costs.rows() == 2 && Stored->Costs.columns() == 3 && + Stored->Costs.at(1, 2) == 7, + "PBQP graph insertion must normalize reversed edge orientation"); +} + +void testReductions(TestContext &Context) { + using namespace rcc::pbqp; + DeterministicSolver Solver; + + Graph DegreeZero; + auto Only = DegreeZero.addNode(CostVector(std::vector<Cost>{4, 1, 1}), 3); + auto ZeroSolution = Solver.solve(DegreeZero); + Context.expect(Only && ZeroSolution && ZeroSolution->selection(*Only) == 1 && + ZeroSolution->totalCost() == 1, + "PBQP R0 reduction must select the first minimum-cost option"); + + Graph DegreeOne; + auto Left = DegreeOne.addNode(CostVector(std::vector<Cost>{3, 0}), 1); + auto Right = DegreeOne.addNode(CostVector(std::vector<Cost>{0, 2}), 2); + CostMatrix Pair(2, 2); + Pair.at(1, 0) = InfiniteCost; + auto PairEdge = DegreeOne.addEdge(*Left, *Right, std::move(Pair)); + auto OneSolution = Solver.solve(DegreeOne); + Context.expect(Left && Right && PairEdge && OneSolution && OneSolution->selection(*Left) == 1 && + OneSolution->selection(*Right) == 1 && OneSolution->totalCost() == 2, + "PBQP R1 reduction must propagate unary costs to its neighbor"); + + Graph DegreeTwo; + auto A = DegreeTwo.addNode(CostVector(std::vector<Cost>{0, 2}), 1); + auto B = DegreeTwo.addNode(CostVector(std::vector<Cost>{0, 0}), 2); + auto C = DegreeTwo.addNode(CostVector(std::vector<Cost>{2, 0}), 3); + CostMatrix AB(2, 2); + CostMatrix BC(2, 2); + AB.at(0, 1) = 5; + AB.at(1, 0) = 5; + BC.at(0, 1) = 5; + BC.at(1, 0) = 5; + auto ABEdge = DegreeTwo.addEdge(*A, *B, std::move(AB)); + auto BCEdge = DegreeTwo.addEdge(*B, *C, std::move(BC)); + auto TwoSolution = Solver.solve(DegreeTwo); + Context.expect(A && B && C && ABEdge && BCEdge && TwoSolution && + TwoSolution->selection(*A) == 0 && TwoSolution->selection(*B) == 0 && + TwoSolution->selection(*C) == 0 && TwoSolution->totalCost() == 2, + "PBQP R2 reduction must add the induced neighbor edge cost"); +} + +[[nodiscard]] rcc::Expected<rcc::pbqp::Solution> solveClique() { + using namespace rcc::pbqp; + Graph Problem; + std::vector<NodeID> Nodes; + for (std::uint64_t Index = 0; Index < 4; ++Index) { + auto Node = Problem.addNode(CostVector(std::vector<Cost>{9, 0, 0}), 100 - Index); + if (!Node) + return std::unexpected(Node.error()); + Nodes.push_back(*Node); + } + for (std::size_t Left = 0; Left < Nodes.size(); ++Left) { + for (std::size_t Right = Left + 1; Right < Nodes.size(); ++Right) { + CostMatrix Costs(3, 3); + Costs.at(1, 1) = InfiniteCost; + Costs.at(2, 2) = InfiniteCost; + auto Edge = Problem.addEdge(Nodes[Left], Nodes[Right], std::move(Costs), + static_cast<std::uint64_t>(Left * Nodes.size() + Right)); + if (!Edge) + return std::unexpected(Edge.error()); + } + } + return DeterministicSolver{}.solve(Problem); +} + +void testHeuristicAndDeterminism(TestContext &Context) { + auto First = solveClique(); + auto Second = solveClique(); + Context.expect(First && Second && std::ranges::equal(First->selections(), Second->selections()) && + First->totalCost() == Second->totalCost(), + "PBQP higher-degree reduction must be deterministic"); + if (First) { + Context.expect(First->totalCost() == 18, + "a four-node two-register clique must spill exactly two nodes"); + } + + rcc::pbqp::Graph Impossible; + auto Node = Impossible.addNode( + rcc::pbqp::CostVector(std::vector<rcc::pbqp::Cost>{rcc::pbqp::InfiniteCost})); + auto Failure = rcc::pbqp::DeterministicSolver{}.solve(Impossible); + Context.expect(Node && !Failure && + Failure.error().code() == rcc::ErrorCode::RegisterAllocationFailed, + "PBQP solver must diagnose a problem with no finite assignment"); +} + +} // namespace + +int main() { + TestContext Context; + testMath(Context); + testGraphValidation(Context); + testReductions(Context); + testHeuristicAndDeterminism(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/PHIEliminationTest.cpp b/backend/unittests/CodeGen/PHIEliminationTest.cpp new file mode 100644 index 0000000..bbc834f --- /dev/null +++ b/backend/unittests/CodeGen/PHIEliminationTest.cpp @@ -0,0 +1,479 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/PHIElimination.h" + +#include "SSAExitTestSupport.h" +#include "rcc/CodeGen/MachineCFGUpdater.h" +#include "rcc/CodeGen/MachinePassManager.h" +#include "rcc/CodeGen/MachineVerifier.h" + +#include <array> +#include <memory> +#include <utility> + +namespace { + +using namespace rcc::test::ssa; + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> buildJump(rcc::MachineFunction &Function, + const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, + rcc::MachineBasicBlock &Destination) { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = Builder.setInsertPoint(Block); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(testOpcode(JumpOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + auto TargetOperand = Pending->addBasicBlock(Destination); + if (!TargetOperand) + return std::unexpected(TargetOperand.error()); + return Pending->build(); +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> +buildPhi(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Register Destination, + std::span<const std::pair<rcc::Register, rcc::MachineBasicBlock *>> Incoming, + rcc::RegisterOperandFlags DestinationFlags = {}, std::uint32_t DestinationTargetFlags = 0, + rcc::RegisterOperandFlags IncomingFlags = {}, std::uint32_t IncomingTargetFlags = 0) { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = Builder.setInsertPoint(Block); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::PHI)); + if (!Pending) + return std::unexpected(Pending.error()); + auto Def = Pending->addDef(Destination, DestinationFlags, {}, DestinationTargetFlags); + if (!Def) + return std::unexpected(Def.error()); + for (const auto &[Value, Predecessor] : Incoming) { + auto Use = Pending->addUse(Value, IncomingFlags, {}, IncomingTargetFlags); + if (!Use) + return std::unexpected(Use.error()); + auto IncomingBlock = Pending->addBasicBlock(*Predecessor); + if (!IncomingBlock) + return std::unexpected(IncomingBlock.error()); + } + return Pending->build(); +} + +[[nodiscard]] rcc::Expected<void> runPHIElimination(rcc::MachineFunction &Function, + const rcc::TargetDescription &Target, + const rcc::TargetInstrInfo &Info) { + rcc::MachineFunctionPassManager Pipeline; + auto Added = Pipeline.addPass(std::make_unique<rcc::PHIEliminationPass>(Info)); + if (!Added) + return std::unexpected(Added.error()); + rcc::MachineFunctionAnalysisManager Analyses; + return Pipeline.run(Function, Target, Analyses); +} + +[[nodiscard]] std::vector<rcc::MachineInstr *> linkedInstructions(rcc::MachineBasicBlock &Block) { + std::vector<rcc::MachineInstr *> Result; + for (rcc::MachineInstr *Instruction = Block.firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + Result.push_back(Instruction); + } + return Result; +} + +[[nodiscard]] bool hasLiveIn(const rcc::MachineBasicBlock &Block, rcc::PhysicalRegister Reg) { + return std::ranges::any_of(Block.liveIns(), [Reg](const rcc::MachineBasicBlockLiveIn &LiveIn) { + return LiveIn.PhysReg == Reg; + }); +} + +void testParallelPHIsAndPlacement(TestContext &Context, const rcc::TargetDescription &Target, + const TestInstrInfo &Info) { + auto Fixture = createFunction(Context, "parallel_phis", 1, 3); + if (!Fixture.Function || Fixture.Blocks.size() != 3) + return; + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + if (!I64) + return; + auto &Function = *Fixture.Function; + rcc::MachineBasicBlock &Preheader = *Fixture.Blocks[0]; + rcc::MachineBasicBlock &Header = *Fixture.Blocks[1]; + rcc::MachineBasicBlock &Backedge = *Fixture.Blocks[2]; + const rcc::PhysicalRegister Hint = *rcc::PhysicalRegister::create(rcc::test::TestRegister::G1); + + auto InitA = createVReg(Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + auto InitB = createVReg(Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + auto DestinationA = createVReg(Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64, 0x1, Hint); + auto DestinationB = createVReg(Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + Context.expect(InitA && InitB && DestinationA && DestinationB, + "parallel PHI registers must be created"); + if (!InitA || !InitB || !DestinationA || !DestinationB) + return; + + auto CFG = Function.cfgUpdater(); + Context.expect( + CFG.addSuccessor(Preheader, Header, rcc::BranchProbability::one()).has_value() && + CFG.addSuccessor(Header, Backedge, rcc::BranchProbability::one()).has_value() && + CFG.addSuccessor(Backedge, Header, rcc::BranchProbability::one()).has_value(), + "parallel PHI loop CFG must be connected"); + + const std::array InitADef{*InitA}; + const std::array InitBDef{*InitB}; + Context.expect(buildRegisterInstruction(Function, Target, Preheader, testOpcode(DefOpcode), + InitADef, {}, {rcc::RegisterOperandFlag::Dead}) && + buildRegisterInstruction(Function, Target, Preheader, testOpcode(DefOpcode), + InitBDef, {}) && + buildJump(Function, Target, Preheader, Header), + "parallel PHI preheader must be populated"); + + const std::array IncomingA{ + std::pair<rcc::Register, rcc::MachineBasicBlock *>{*InitA, &Preheader}, + std::pair<rcc::Register, rcc::MachineBasicBlock *>{*DestinationB, &Backedge}}; + const std::array IncomingB{ + std::pair<rcc::Register, rcc::MachineBasicBlock *>{*InitB, &Preheader}, + std::pair<rcc::Register, rcc::MachineBasicBlock *>{*DestinationA, &Backedge}}; + Context.expect(buildPhi(Function, Target, Header, *DestinationA, IncomingA, {}, 0x4, {}, 0x8) && + buildPhi(Function, Target, Header, *DestinationB, IncomingB), + "parallel PHIs must be built"); + + rcc::MachineIRBuilder HeaderBuilder(Function, Target); + if (!HeaderBuilder.setInsertPoint(Header)) + return; + auto Meta = HeaderBuilder.buildInstr(testOpcode(MetaOpcode)); + const std::array HeaderUse{*DestinationA}; + Context.expect(Meta && Meta->build() && + buildRegisterInstruction(Function, Target, Header, testOpcode(UseOpcode), {}, + HeaderUse) && + buildJump(Function, Target, Header, Backedge), + "parallel PHI header must contain meta, code, and terminator instructions"); + + const std::array KilledUse{*DestinationA}; + Context.expect(buildRegisterInstruction(Function, Target, Backedge, testOpcode(UseOpcode), {}, + KilledUse, {}, {rcc::RegisterOperandFlag::Kill}) && + buildJump(Function, Target, Backedge, Header), + "parallel PHI backedge must be populated"); + auto InitialLiveness = setSelectedSSAProperties(Function, Target); + Context.expect(InitialLiveness.has_value(), "parallel PHI liveness rebuild must succeed"); + if (!InitialLiveness) + return; + + auto Run = runPHIElimination(Function, Target, Info); + Context.expect(Run.has_value(), "parallel PHI elimination must succeed"); + if (!Run) + return; + Context.expect(!Function.properties().has(rcc::MachineFunctionProperty::IsSSA) && + Function.properties().has(rcc::MachineFunctionProperty::NoPHIs) && + Function.properties().has(rcc::MachineFunctionProperty::TracksLiveness), + "PHI elimination must publish its exact property transition"); + Context.expect(instructionsWithOpcode(Function, rcc::opcode(rcc::GenericOpcode::PHI)).empty(), + "PHI elimination must remove every PHI"); + + const auto HeaderInstructions = linkedInstructions(Header); + Context.expect(HeaderInstructions.size() == 5 && + HeaderInstructions[0]->opcode() == testOpcode(MetaOpcode) && + HeaderInstructions[1]->opcode() == rcc::opcode(rcc::GenericOpcode::COPY) && + HeaderInstructions[2]->opcode() == rcc::opcode(rcc::GenericOpcode::COPY) && + HeaderInstructions[3]->opcode() == testOpcode(UseOpcode) && + HeaderInstructions[4]->opcode() == testOpcode(JumpOpcode), + "destination COPYs must follow the full leading PHI/meta region"); + if (HeaderInstructions.size() < 3) + return; + + const rcc::Register *FreshA = HeaderInstructions[1]->operandAt(1)->getIf<rcc::Register>(); + const rcc::Register *FreshB = HeaderInstructions[2]->operandAt(1)->getIf<rcc::Register>(); + Context.expect(FreshA != nullptr && FreshB != nullptr && *FreshA != *FreshB && + *FreshA != *DestinationA && *FreshB != *DestinationB, + "each PHI must receive an independent fresh incoming vreg"); + if (FreshA == nullptr || FreshB == nullptr) + return; + + const auto BackedgeInstructions = linkedInstructions(Backedge); + Context.expect(BackedgeInstructions.size() == 4 && + BackedgeInstructions[1]->opcode() == rcc::opcode(rcc::GenericOpcode::COPY) && + BackedgeInstructions[2]->opcode() == rcc::opcode(rcc::GenericOpcode::COPY) && + BackedgeInstructions[3]->opcode() == testOpcode(JumpOpcode), + "incoming COPYs must be placed together before the first terminator"); + if (BackedgeInstructions.size() >= 3) { + const auto FirstDef = BackedgeInstructions[1]->operandAt(0)->registerValue(); + const auto FirstUse = BackedgeInstructions[1]->operandAt(1)->registerValue(); + const auto SecondDef = BackedgeInstructions[2]->operandAt(0)->registerValue(); + const auto SecondUse = BackedgeInstructions[2]->operandAt(1)->registerValue(); + Context.expect(FirstDef == *FreshA && FirstUse == *DestinationB && SecondDef == *FreshB && + SecondUse == *DestinationA, + "backedge COPYs must read old PHI destinations into distinct temporaries"); + } + + auto FreshInfo = Function.registerInfo().virtualRegisterInfo(*FreshA); + Context.expect(FreshInfo.has_value(), "fresh PHI vreg must remain registered"); + if (!FreshInfo) + return; + Context.expect(FreshInfo->get().Type == I64, "fresh PHI vreg must clone its LowLevelType"); + Context.expect(FreshInfo->get().Bank == rcc::test::TestRegisterBank::GPR, + "fresh PHI vreg must clone its register bank"); + Context.expect(FreshInfo->get().RegClass == rcc::test::TestRegisterClass::GPR64, + "fresh PHI vreg must clone its register class"); + Context.expect(FreshInfo->get().TargetFlags == 0x1, + "fresh PHI vreg must clone registered target flags"); + Context.expect(FreshInfo->get().AllocationHints.size() == 1, + "fresh PHI vreg must clone the allocation-hint count"); + Context.expect(!FreshInfo->get().AllocationHints.empty() && + FreshInfo->get().AllocationHints.front() == Hint, + "fresh PHI vreg must clone the allocation-hint register"); + Context.expect(HeaderInstructions[1]->operandAt(0)->targetFlags() == 0x4 && + HeaderInstructions[1]->operandAt(1)->targetFlags() == 0x4, + "successor COPY must preserve registered destination target flags"); + + const rcc::MachineInstr *InitDefinition = Preheader.firstInstruction(); + Context.expect( + InitDefinition != nullptr && + !InitDefinition->operandAt(0)->registerFlags().has(rcc::RegisterOperandFlag::Dead) && + !BackedgeInstructions[0]->operandAt(0)->registerFlags().has( + rcc::RegisterOperandFlag::Kill), + "liveness-preserving PHI elimination must conservatively clear kill/dead flags"); + Context.expect(rcc::MachineVerifier::verify(Function, Target).has_value(), + "parallel PHI result must pass MachineVerifier"); +} + +void testCriticalEdgeAndPhysicalLiveIn(TestContext &Context, const rcc::TargetDescription &Target, + const TestInstrInfo &Info) { + auto Fixture = createFunction(Context, "critical_edge", 2, 4); + if (!Fixture.Function || Fixture.Blocks.size() != 4) + return; + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + if (!I64) + return; + auto &Function = *Fixture.Function; + rcc::MachineBasicBlock &From = *Fixture.Blocks[0]; + rcc::MachineBasicBlock &Side = *Fixture.Blocks[1]; + rcc::MachineBasicBlock &Other = *Fixture.Blocks[2]; + rcc::MachineBasicBlock &Merge = *Fixture.Blocks[3]; + auto Condition = createVReg(Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + auto OtherValue = createVReg(Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + auto Destination = createVReg(Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + if (!Condition || !OtherValue || !Destination) + return; + + auto Half = rcc::BranchProbability::fromFraction(1, 2); + auto CFG = Function.cfgUpdater(); + Context.expect(Half && CFG.addSuccessor(From, Side, *Half) && + CFG.addSuccessor(From, Merge, *Half) && + CFG.addSuccessor(Other, Merge, rcc::BranchProbability::one()), + "critical-edge CFG must be connected"); + + const std::array ConditionDef{*Condition}; + Context.expect( + buildRegisterInstruction(Function, Target, From, testOpcode(DefOpcode), ConditionDef, {}) + .has_value(), + "critical-edge condition must be defined"); + rcc::MachineIRBuilder FromBuilder(Function, Target); + if (!FromBuilder.setInsertPoint(From)) + return; + auto Branch = FromBuilder.buildInstr(testOpcode(ConditionalBranchOpcode)); + Context.expect(Branch && Branch->addUse(*Condition) && Branch->addBasicBlock(Merge) && + Branch->build(), + "critical-edge conditional branch must be built"); + const std::array OtherDef{*OtherValue}; + Context.expect( + buildRegisterInstruction(Function, Target, Other, testOpcode(DefOpcode), OtherDef, {}) + .has_value(), + "unrelated fallthrough predecessor must define its incoming value"); + + const rcc::PhysicalRegister G0 = *rcc::PhysicalRegister::create(rcc::test::TestRegister::G0); + const rcc::PhysicalRegister G1 = *rcc::PhysicalRegister::create(rcc::test::TestRegister::G1); + Context.expect(Merge.addLiveIn(G1, rcc::LaneMask::fromRaw(0xF)).has_value(), + "merge block live-in setup must succeed"); + const std::array Incoming{ + std::pair<rcc::Register, rcc::MachineBasicBlock *>{rcc::Register::fromPhysical(G0), &From}, + std::pair<rcc::Register, rcc::MachineBasicBlock *>{*OtherValue, &Other}}; + Context.expect(buildPhi(Function, Target, Merge, *Destination, Incoming).has_value(), + "critical-edge PHI must be built"); + const std::array G1Use{rcc::Register::fromPhysical(G1)}; + Context.expect(buildRegisterInstruction(Function, Target, Merge, testOpcode(UseOpcode), {}, G1Use) + .has_value(), + "merge block must consume its inherited physical live-in"); + auto InitialLiveness = setSelectedSSAProperties(Function, Target); + Context.expect(InitialLiveness.has_value(), "critical-edge PHI liveness rebuild must succeed"); + if (!InitialLiveness) + return; + + auto Run = runPHIElimination(Function, Target, Info); + Context.expect(Run.has_value(), "critical-edge PHI elimination must succeed"); + if (!Run) + return; + + rcc::MachineBasicBlock *Edge = Merge.previous(); + Context.expect( + Edge != nullptr && Edge != &Other && Edge->previous() == &Other && + From.successors().size() == 2 && + std::ranges::any_of(From.successors(), + [Edge](const rcc::MachineSuccessor &S) { return S.Block == Edge; }) && + Edge->predecessors().size() == 1 && Edge->predecessors()[0] == &From && + Edge->successors().size() == 1 && Edge->successors()[0].Block == &Merge, + "critical edge must become a reciprocal From-to-edge-to-Merge CFG path"); + if (Edge == nullptr || Edge == &Other) + return; + + const rcc::MachineOperand *BranchTarget = From.lastInstruction()->operandAt(1); + const auto *BranchBlock = + BranchTarget == nullptr ? nullptr : BranchTarget->getIf<rcc::BasicBlockOperand>(); + Context.expect(BranchBlock != nullptr && BranchBlock->Block == Edge, + "explicit critical-edge branch must target the split block"); + const auto OtherInstructions = linkedInstructions(Other); + const auto EdgeInstructions = linkedInstructions(*Edge); + Context.expect(OtherInstructions.size() == 3 && + OtherInstructions.back()->opcode() == testOpcode(JumpOpcode) && + EdgeInstructions.size() == 2 && + EdgeInstructions[0]->opcode() == rcc::opcode(rcc::GenericOpcode::COPY) && + EdgeInstructions[1]->opcode() == testOpcode(JumpOpcode), + "edge split must preserve unrelated fallthrough and terminator placement"); + Context.expect(hasLiveIn(*Edge, G0) && hasLiveIn(*Edge, G1), + "split edge must inherit successor live-ins and its physical PHI source"); + Context.expect(rcc::MachineVerifier::verify(Function, Target).has_value(), + "critical-edge result must pass MachineVerifier"); +} + +void testUndefAndFailureAtomicity(TestContext &Context, const rcc::TargetDescription &Target, + const TestInstrInfo &Info) { + auto BuildFixture = [&](std::uint64_t ID) { + auto Fixture = createFunction(Context, "undef_phi", ID, 2); + if (!Fixture.Function || Fixture.Blocks.size() != 2) + return Fixture; + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + if (!I64) + return Fixture; + auto Source = createVReg(*Fixture.Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + auto Destination = createVReg(*Fixture.Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + if (!Source || !Destination) + return Fixture; + auto CFG = Fixture.Function->cfgUpdater(); + if (!CFG.addSuccessor(*Fixture.Blocks[0], *Fixture.Blocks[1], rcc::BranchProbability::one()) || + !buildJump(*Fixture.Function, Target, *Fixture.Blocks[0], *Fixture.Blocks[1])) { + return Fixture; + } + const std::array Incoming{ + std::pair<rcc::Register, rcc::MachineBasicBlock *>{*Source, Fixture.Blocks[0]}}; + if (!buildPhi(*Fixture.Function, Target, *Fixture.Blocks[1], *Destination, Incoming, {}, 0, + {rcc::RegisterOperandFlag::Undef})) { + return Fixture; + } + Context.expect(setSelectedSSAProperties(*Fixture.Function, Target).has_value(), + "undef PHI liveness rebuild must succeed"); + return Fixture; + }; + + auto Undef = BuildFixture(3); + if (!Undef.Function) + return; + auto Run = runPHIElimination(*Undef.Function, Target, Info); + Context.expect(Run.has_value(), "undef PHI elimination must succeed"); + if (Run) { + const auto PredecessorInstructions = linkedInstructions(*Undef.Blocks[0]); + Context.expect(PredecessorInstructions.size() == 2 && + PredecessorInstructions[0]->opcode() == + rcc::opcode(rcc::GenericOpcode::IMPLICIT_DEF) && + PredecessorInstructions[1]->opcode() == testOpcode(JumpOpcode), + "undef PHI incoming values must become edge IMPLICIT_DEF instructions"); + } + + auto Atomic = BuildFixture(4); + if (!Atomic.Function) + return; + const std::size_t BlocksBefore = Atomic.Function->blockCount(); + const std::size_t InstructionsBefore = + instructionsWithOpcode(*Atomic.Function, rcc::opcode(rcc::GenericOpcode::PHI)).size(); + const std::size_t RegistersBefore = Atomic.Function->registerInfo().virtualRegisters().size(); + const std::uint64_t PropertiesBefore = Atomic.Function->properties().rawValue(); + TestInstrInfo RejectingInfo(true); + rcc::MachineFunctionAnalysisManager Analyses; + rcc::PHIEliminationPass FailingPass(RejectingInfo); + auto Failed = FailingPass.run(*Atomic.Function, Target, Analyses); + Context.expect(!Failed && Failed.error().code() == rcc::ErrorCode::Unsupported, + "COPY rejection must produce a structured PHI-elimination failure"); + Context.expect( + Atomic.Function->blockCount() == BlocksBefore && + instructionsWithOpcode(*Atomic.Function, rcc::opcode(rcc::GenericOpcode::PHI)).size() == + InstructionsBefore && + Atomic.Function->registerInfo().virtualRegisters().size() == RegistersBefore && + Atomic.Function->properties().rawValue() == PropertiesBefore, + "failed PHI transaction preflight must leave the original function unchanged"); +} + +void testMalformedPHIs(TestContext &Context, const rcc::TargetDescription &Target) { + auto BuildBadPhi = [&](std::uint64_t ID, rcc::LowLevelType DestinationType, + rcc::RegisterBankID DestinationBank, rcc::RegisterClassID DestinationClass, + rcc::LowLevelType SourceType, rcc::RegisterBankID SourceBank, + rcc::RegisterClassID SourceClass, rcc::SubRegIndex SourceSubReg = {}) { + auto Fixture = createFunction(Context, "bad_phi", ID, 2); + if (!Fixture.Function || Fixture.Blocks.size() != 2) + return Fixture; + auto Destination = + createVReg(*Fixture.Function, DestinationType, DestinationBank, DestinationClass); + auto Source = createVReg(*Fixture.Function, SourceType, SourceBank, SourceClass); + if (!Destination || !Source) + return Fixture; + auto CFG = Fixture.Function->cfgUpdater(); + if (!CFG.addSuccessor(*Fixture.Blocks[0], *Fixture.Blocks[1], rcc::BranchProbability::one())) { + return Fixture; + } + const std::array SourceDef{*Source}; + if (!buildRegisterInstruction(*Fixture.Function, Target, *Fixture.Blocks[0], + testOpcode(DefOpcode), SourceDef, {})) { + return Fixture; + } + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + if (!Builder.setInsertPoint(*Fixture.Blocks[1])) + return Fixture; + auto Phi = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::PHI)); + if (!Phi || !Phi->addDef(*Destination) || !Phi->addUse(*Source, {}, SourceSubReg) || + !Phi->addBasicBlock(*Fixture.Blocks[0]) || !Phi->build()) { + return Fixture; + } + return Fixture; + }; + + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto I32 = rcc::LowLevelType::makeCategoryNeutralScalar(32); + if (!I64 || !I32) + return; + auto BadType = + BuildBadPhi(6, *I64, rcc::test::TestRegisterBank::GPR, rcc::test::TestRegisterClass::GPR64, + *I32, rcc::test::TestRegisterBank::GPR, rcc::test::TestRegisterClass::GPR32); + Context.expect(BadType.Function && !rcc::MachineVerifier::verify(*BadType.Function, Target), + "MachineVerifier must reject incompatible PHI LLTs"); + + auto BadClass = + BuildBadPhi(7, *I64, rcc::test::TestRegisterBank::GPR, rcc::test::TestRegisterClass::GPR64, + *I64, rcc::test::TestRegisterBank::FPR, rcc::test::TestRegisterClass::FPR64); + Context.expect(BadClass.Function && !rcc::MachineVerifier::verify(*BadClass.Function, Target), + "MachineVerifier must reject nonoverlapping PHI register classes"); + + auto BadSubReg = + BuildBadPhi(8, *I32, rcc::test::TestRegisterBank::GPR, rcc::test::TestRegisterClass::GPR32Low, + *I64, rcc::test::TestRegisterBank::GPR, rcc::test::TestRegisterClass::GPR64, + rcc::test::TestSubReg::High32); + Context.expect(BadSubReg.Function && !rcc::MachineVerifier::verify(*BadSubReg.Function, Target), + "MachineVerifier must reject incompatible PHI subregister projections"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "PHI-elimination TestTarget must be created"); + if (!Target) + return Context.result(); + TestInstrInfo Info; + testParallelPHIsAndPlacement(Context, *Target, Info); + testCriticalEdgeAndPhysicalLiveIn(Context, *Target, Info); + testUndefAndFailureAtomicity(Context, *Target, Info); + testMalformedPHIs(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/PassInstrumentationTest.cpp b/backend/unittests/CodeGen/PassInstrumentationTest.cpp new file mode 100644 index 0000000..97ce445 --- /dev/null +++ b/backend/unittests/CodeGen/PassInstrumentationTest.cpp @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachinePassManager.h" + +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <memory> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + std::vector<rcc::InstructionDescriptor> Descriptors; + std::vector<rcc::ImmediatePredicate> Predicates; + auto Appended = rcc::appendGenericInstructionInfo(Descriptors, Predicates); + if (!Appended) + return std::unexpected(Appended.error()); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors), std::move(Predicates)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +class BlockCountAnalysis final { +public: + struct Result final { + std::size_t Count = 0; + }; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "BlockCount"; + } + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &Function, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + return Result{Function.blockCount()}; + } +}; + +class InstrumentedPass final : public rcc::MachineFunctionPass { +public: + [[nodiscard]] const rcc::MachinePassContract &contract() const noexcept override { + static const rcc::MachinePassContract Contract{ + .Name = "Instrumented", + .Requires = {}, + .Sets = {}, + .Clears = {}, + .RequiredAnalyses = + { + rcc::MachineFunctionAnalysisManager::id<BlockCountAnalysis>(), + }, + .VerificationStage = std::nullopt, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected<rcc::MachinePassResult> + run(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineFunctionAnalysisManager &Analyses) override { + auto First = Analyses.getResult<BlockCountAnalysis>(Function, Target); + auto Second = Analyses.getResult<BlockCountAnalysis>(Function, Target); + auto FirstAction = Analyses.requestAction("first", Function); + auto SecondAction = Analyses.requestAction("second", Function); + if (!First || !Second || !FirstAction || !SecondAction || !*FirstAction || *SecondAction) + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "instrumentation action policy was not enforced"); + rcc::MachinePassResult Result; + Result.Preserved = rcc::PreservedAnalyses::all(); + return Result; + } +}; + +void testInstrumentation(TestContext &Context, const rcc::TargetDescription &Target) { + auto Function = + rcc::MachineFunction::create("instrumentation", rcc::MachineFunctionStableID::fromValue(1)); + Context.expect(Function.has_value(), "instrumentation function creation must succeed"); + if (!Function) + return; + + rcc::MachineFunctionAnalysisManager Analyses; + Context.expect(Analyses + .registerAnalysis<BlockCountAnalysis>({ + .Name = "BlockCount", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + .ThreadSafety = rcc::PassThreadSafety::ThreadSafe, + }) + .has_value(), + "instrumentation analysis registration 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 CacheHits = 0; + std::uint32_t Printed = 0; + std::uint32_t Changes = 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, &CacheHits](std::string_view, const rcc::MachineFunction &, bool CacheHit, + std::uint64_t) { + ++AfterAnalysis; + CacheHits += CacheHit ? 1U : 0U; + }); + Instrumentation.addIRPrintCallback([&Printed](rcc::InstrumentationPhase, std::string_view, + const rcc::MachineFunction &) { ++Printed; }); + Instrumentation.addChangeCallback([&Changes](std::string_view, const rcc::MachineFunction &, + rcc::MachinePassChanges ReportedChanges, + bool FingerprintChanged) { + if (ReportedChanges.empty() && !FingerprintChanged) + ++Changes; + }); + + rcc::MachineFunctionPassManager Pipeline; + Context.expect(Pipeline.addPass(std::make_unique<InstrumentedPass>()).has_value() && + Pipeline.run(**Function, Target, Analyses, &Instrumentation).has_value(), + "instrumented pass must run"); + Context.expect(BeforePass == 1 && AfterPass == 1 && BeforeAnalysis == 1 && AfterAnalysis == 3 && + CacheHits == 2 && Printed == 2 && Changes == 1, + "all pass and analysis instrumentation points must run"); + + const auto Events = Instrumentation.actionEvents(); + Context.expect(Events.size() == 2 && Events[0].Accepted && !Events[1].Accepted, + "action instrumentation must record accepted and rejected decisions"); + const auto Statistics = Instrumentation.statistics(); + Context.expect(Statistics.size() == 2, + "instrumentation must collect one pass and one analysis statistic"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), + "pass instrumentation tests require a valid target description"); + if (!Target) + return Context.result(); + + testInstrumentation(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/PassManagerTest.cpp b/backend/unittests/CodeGen/PassManagerTest.cpp new file mode 100644 index 0000000..56c30f3 --- /dev/null +++ b/backend/unittests/CodeGen/PassManagerTest.cpp @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineFunctionAnalysisManager.h" + +#include <atomic> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + std::vector<rcc::InstructionDescriptor> Descriptors; + std::vector<rcc::ImmediatePredicate> Predicates; + auto Appended = rcc::appendGenericInstructionInfo(Descriptors, Predicates); + if (!Appended) + return std::unexpected(Appended.error()); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors), std::move(Predicates)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +class BlockCountAnalysis final { +public: + struct Result final { + std::size_t Count = 0; + }; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "BlockCount"; + } + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &Function, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + ++Runs; + return Result{Function.blockCount()}; + } + + static inline std::atomic<std::uint32_t> Runs = 0; +}; + +class DependentAnalysis final { +public: + struct Result final { + std::size_t Count = 0; + }; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "Dependent"; + } + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &Function, + const rcc::TargetDescription &Target, + rcc::MachineFunctionAnalysisManager &Analyses) { + auto Blocks = Analyses.getResult<BlockCountAnalysis>(Function, Target); + if (!Blocks) + return std::unexpected(Blocks.error()); + return Result{(*Blocks)->Count}; + } +}; + +class StickyAnalysis final { +public: + struct Result final { + [[nodiscard]] bool invalidate(const rcc::MachineFunction &, + const rcc::PreservedAnalyses &) noexcept { + return false; + } + }; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "Sticky"; + } + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + return Result{}; + } +}; + +class ExpiringAnalysis final { +public: + struct Result final { + [[nodiscard]] bool invalidate(const rcc::MachineFunction &, + const rcc::PreservedAnalyses &) noexcept { + return true; + } + }; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "Expiring"; + } + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + return Result{}; + } +}; + +class CycleA final { +public: + struct Result final {}; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "CycleA"; + } + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + return Result{}; + } +}; + +class CycleB final { +public: + struct Result final {}; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "CycleB"; + } + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + return Result{}; + } +}; + +void testAnalysisManager(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::MachineFunctionAnalysisManager Analyses; + Context.expect(Analyses + .registerAnalysis<BlockCountAnalysis>({ + .Name = "BlockCount", + .Dependencies = {}, + .InvalidatedBy = {rcc::MachinePassChange::CFG}, + }) + .has_value(), + "base analysis registration must succeed"); + Context.expect(Analyses + .registerAnalysis<DependentAnalysis>({ + .Name = "Dependent", + .Dependencies = + { + rcc::MachineFunctionAnalysisManager::id<BlockCountAnalysis>(), + }, + .InvalidatedBy = {rcc::MachinePassChange::Operands}, + }) + .has_value(), + "dependent analysis registration must succeed"); + + BlockCountAnalysis::Runs = 0; + auto First = Analyses.getResult<DependentAnalysis>(**Function, Target); + auto Second = Analyses.getResult<DependentAnalysis>(**Function, Target); + Context.expect(First && Second && *First == *Second && BlockCountAnalysis::Runs == 1, + "analysis dependencies must materialize once and then use the cache"); + + Analyses.invalidate(**Function, rcc::PreservedAnalyses::none(), + {rcc::MachinePassChange::Registers}); + Context.expect(Analyses.cachedResultCount(**Function) == 2, + "unrelated changes must preserve cached analyses precisely"); + Analyses.invalidate(**Function, rcc::PreservedAnalyses::none(), {rcc::MachinePassChange::CFG}); + Context.expect(Analyses.cachedResultCount(**Function) == 0, + "invalidating a dependency must invalidate its dependent analysis"); + + rcc::MachineFunctionAnalysisManager Cyclic; + Context.expect( + Cyclic.registerAnalysis<CycleA>({ + .Name = "CycleA", + .Dependencies = + { + rcc::MachineFunctionAnalysisManager::id<CycleB>(), + }, + }) + .has_value() && + Cyclic + .registerAnalysis<CycleB>({ + .Name = "CycleB", + .Dependencies = + { + rcc::MachineFunctionAnalysisManager::id<CycleA>(), + }, + }) + .has_value(), + "cyclic analysis registrations must be accepted until requested"); + Context.expect(!Cyclic.getResult<CycleA>(**Function, Target), + "analysis dependency cycles must produce a structured error"); + + rcc::MachineFunctionAnalysisManager CustomInvalidation; + Context.expect(CustomInvalidation + .registerAnalysis<StickyAnalysis>({ + .Name = "Sticky", + .Dependencies = {}, + .InvalidatedBy = {rcc::MachinePassChange::CFG}, + }) + .has_value() && + CustomInvalidation + .registerAnalysis<ExpiringAnalysis>({ + .Name = "Expiring", + .Dependencies = {}, + .InvalidatedBy = {rcc::MachinePassChange::CFG}, + }) + .has_value() && + CustomInvalidation.getResult<StickyAnalysis>(**Function, Target) && + CustomInvalidation.getResult<ExpiringAnalysis>(**Function, Target), + "custom invalidation analyses must materialize"); + CustomInvalidation.invalidate(**Function, rcc::PreservedAnalyses::none(), + {rcc::MachinePassChange::CFG}); + Context.expect(CustomInvalidation.hasCached( + **Function, rcc::MachineFunctionAnalysisManager::id<StickyAnalysis>()) && + !CustomInvalidation.hasCached( + **Function, rcc::MachineFunctionAnalysisManager::id<ExpiringAnalysis>()), + "analysis invalidate() must return false to preserve and true to invalidate"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "pass manager tests require a valid target description"); + if (!Target) + return Context.result(); + + testAnalysisManager(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/RegAllocBasicTest.cpp b/backend/unittests/CodeGen/RegAllocBasicTest.cpp new file mode 100644 index 0000000..015fe37 --- /dev/null +++ b/backend/unittests/CodeGen/RegAllocBasicTest.cpp @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "CodeGen/RegisterAllocationTestSupport.h" + +#include <iostream> + +namespace { + +using namespace rcc; +using namespace rcc::test; +using namespace rcc::test::ra; + +[[nodiscard]] Expected<MachineInstr *> buildEarly(MachineFunction &Function, + const TargetDescription &Target, + MachineBasicBlock &Block, Register Destination, + Register Source) { + MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(testOpcode(EarlyOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + RegisterOperandFlags Flags; + Flags.set(RegisterOperandFlag::EarlyClobber); + if (auto Result = Pending->addDef(Destination, Flags); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addUse(Source); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +void testHintsAndPressure(TestContext &Context, const TargetDescription &Target) { + auto HintFixture = createFunction("basic-hint", 30); + if (!HintFixture) + return; + auto HintValue = createVReg(*HintFixture->Function); + if (!HintValue || !buildDef(*HintFixture->Function, Target, *HintFixture->Block, *HintValue, 1) || + !buildUse(*HintFixture->Function, Target, *HintFixture->Block, *HintValue) || + !HintFixture->Function->registerInfo().addAllocationHint(*HintValue, + physical(TestRegister::G1)) || + !prepareForAllocation(*HintFixture->Function, Target)) + return; + TestRegisterAllocationInfo Info; + auto Allocated = runAllocator(*HintFixture->Function, Target, Info, AllocatorKind::Basic); + const MachineInstr *Use = HintFixture->Block->lastInstruction(); + Context.expect(Allocated && Use != nullptr && + Use->operandAt(0)->registerValue() == + Register::fromPhysical(physical(TestRegister::G1)), + "BasicAllocator must honor the first allocation hint"); + + auto Pressure = createFunction("basic-pressure", 31); + if (!Pressure) + return; + std::vector<Register> Values; + for (std::int64_t Value = 0; Value < 3; ++Value) { + auto Reg = createVReg(*Pressure->Function); + if (!Reg || !buildDef(*Pressure->Function, Target, *Pressure->Block, *Reg, Value)) + return; + Values.push_back(*Reg); + } + for (const Register Reg : Values) + if (!buildUse(*Pressure->Function, Target, *Pressure->Block, Reg)) + return; + if (!prepareForAllocation(*Pressure->Function, Target)) + return; + Allocated = runAllocator(*Pressure->Function, Target, Info, AllocatorKind::Basic, + {.EnableRematerialization = false}); + Context.expect(Allocated && + Pressure->Function->properties().has(MachineFunctionProperty::NoVRegs) && + Pressure->Function->frameInfo().liveObjectCount() != 0, + "BasicAllocator must close spill/reload pressure"); +} + +void testEarlyClobberAndRewrite(TestContext &Context, const TargetDescription &Target) { + auto Fixture = createFunction("basic-constraints", 32); + if (!Fixture) + return; + auto Source = createVReg(*Fixture->Function); + auto Destination = createVReg(*Fixture->Function); + if (!Source || !Destination || + !buildDef(*Fixture->Function, Target, *Fixture->Block, *Source, 1) || + !buildEarly(*Fixture->Function, Target, *Fixture->Block, *Destination, *Source) || + !buildTied(*Fixture->Function, Target, *Fixture->Block, *Destination) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Destination) || + !prepareForAllocation(*Fixture->Function, Target)) + return; + TestRegisterAllocationInfo Info; + auto Allocated = runAllocator(*Fixture->Function, Target, Info, AllocatorKind::Basic); + const MachineInstr *Early = Fixture->Block->firstInstruction()->next(); + const MachineInstr *Tied = Early == nullptr ? nullptr : Early->next(); + Context.expect(Allocated && Early != nullptr && Tied != nullptr && + Early->operandAt(0)->registerValue() != Early->operandAt(1)->registerValue() && + Tied->operandAt(0)->registerValue() == Tied->operandAt(1)->registerValue(), + "BasicAllocator must preserve early-clobber and tied constraints"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "BasicAllocator test target must be constructible"); + if (!Target) + return Context.result(); + testHintsAndPressure(Context, *Target); + testEarlyClobberAndRewrite(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/RegAllocFastTest.cpp b/backend/unittests/CodeGen/RegAllocFastTest.cpp new file mode 100644 index 0000000..cf5498f --- /dev/null +++ b/backend/unittests/CodeGen/RegAllocFastTest.cpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "CodeGen/RegisterAllocationTestSupport.h" +#include "rcc/CodeGen/MachineCFGUpdater.h" + +#include <iostream> + +namespace { + +using namespace rcc; +using namespace rcc::test; +using namespace rcc::test::ra; + +void testBlockBoundary(TestContext &Context, const TargetDescription &Target) { + auto Fixture = createFunction("fast-boundary", 40); + if (!Fixture) + return; + auto Exit = Fixture->Function->createBasicBlock(); + if (!Exit || !Fixture->Function->appendBlock(**Exit) || + !Fixture->Function->cfgUpdater().addSuccessor(*Fixture->Block, **Exit, + BranchProbability::one())) + return; + auto Value = createVReg(*Fixture->Function); + if (!Value || !buildDef(*Fixture->Function, Target, *Fixture->Block, *Value, 9) || + !buildUse(*Fixture->Function, Target, **Exit, *Value) || + !buildUse(*Fixture->Function, Target, **Exit, + Register::fromPhysical(physical(TestRegister::G0))) || + !prepareForAllocation(*Fixture->Function, Target)) + return; + TestRegisterAllocationInfo Info; + auto Allocated = runAllocator(*Fixture->Function, Target, Info, AllocatorKind::Fast); + Context.expect(Allocated && countOpcode(*Fixture->Function, testOpcode(SpillStoreOpcode)) == 1 && + countOpcode(*Fixture->Function, testOpcode(SpillLoadOpcode)) == 1 && + Fixture->Function->properties().has(MachineFunctionProperty::NoVRegs), + "FastAllocator must preserve values across a block boundary"); +} + +void testFunctionLiveInAndDebug(TestContext &Context, const TargetDescription &Target) { + auto Fixture = createFunction("fast-live-in", 41); + if (!Fixture) + return; + auto Value = createVReg(*Fixture->Function); + const PhysicalRegister G0 = physical(TestRegister::G0); + if (!Value || !Fixture->Function->registerInfo().addLiveIn(G0, *Value) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value, {}, true) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value) || + !prepareForAllocation(*Fixture->Function, Target)) + return; + TestRegisterAllocationInfo Info; + auto Allocated = runAllocator(*Fixture->Function, Target, Info, AllocatorKind::Fast); + const MachineOperand *DebugOperand = nullptr; + for (const MachineInstr *Instruction = Fixture->Block->firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + for (const MachineOperand &Operand : Instruction->operands()) { + if (Operand.isDebugUse()) { + DebugOperand = &Operand; + break; + } + } + if (DebugOperand != nullptr) + break; + } + Context.expect( + Allocated && DebugOperand != nullptr && DebugOperand->registerValue() && + (DebugOperand->registerValue()->isNoRegister() || + DebugOperand->registerValue() == Register::fromPhysical(G0)) && + std::ranges::none_of(Fixture->Function->registerInfo().virtualRegisters(), + [](const VirtualRegisterInfo &Info) { return !Info.IsTombstone; }), + "FastAllocator must preserve function live-ins and debug locations"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "FastAllocator test target must be constructible"); + if (!Target) + return Context.result(); + testBlockBoundary(Context, *Target); + testFunctionLiveInAndDebug(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/RegAllocPBQPTest.cpp b/backend/unittests/CodeGen/RegAllocPBQPTest.cpp new file mode 100644 index 0000000..9b70a9b --- /dev/null +++ b/backend/unittests/CodeGen/RegAllocPBQPTest.cpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "CodeGen/RegisterAllocationTestSupport.h" + +#include <iostream> + +namespace { + +using namespace rcc; +using namespace rcc::test; +using namespace rcc::test::ra; + +[[nodiscard]] Expected<FunctionFixture> createPressureFunction(const TargetDescription &Target, + std::uint64_t ID, bool WithCopy) { + auto Fixture = createFunction("pbqp-pressure", ID); + if (!Fixture) + return std::unexpected(Fixture.error()); + std::vector<Register> Values; + for (std::int64_t Value = 0; Value < 3; ++Value) { + auto Reg = createVReg(*Fixture->Function); + if (!Reg || !buildDef(*Fixture->Function, Target, *Fixture->Block, *Reg, Value)) + return makeUnexpected(ErrorCode::InternalError, "failed to construct PBQP pressure value"); + Values.push_back(*Reg); + } + if (WithCopy && !buildCopy(*Fixture->Function, Target, *Fixture->Block, Values[1], Values[0])) + return makeUnexpected(ErrorCode::InternalError, "failed to construct PBQP copy"); + for (const Register Reg : Values) + if (!buildUse(*Fixture->Function, Target, *Fixture->Block, Reg)) + return makeUnexpected(ErrorCode::InternalError, "failed to construct PBQP use"); + auto Prepared = prepareForAllocation(*Fixture->Function, Target); + if (!Prepared) + return std::unexpected(Prepared.error()); + return Fixture; +} + +[[nodiscard]] std::vector<std::uint64_t> signature(const MachineFunction &Function) { + std::vector<std::uint64_t> Result; + Result.push_back(Function.frameInfo().liveObjectCount()); + for (const MachineBasicBlock *Block = Function.firstBlock(); Block != nullptr; + Block = Block->next()) { + for (const MachineInstr *Instruction = Block->firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + Result.push_back(Instruction->opcode().value()); + for (const MachineOperand &Operand : Instruction->operands()) { + Result.push_back(static_cast<std::uint64_t>(Operand.kind())); + if (const Register *Reg = Operand.getIf<Register>()) + Result.push_back(Reg->opaqueValue()); + } + } + } + return Result; +} + +void testHintsAndDeterminism(TestContext &Context, const TargetDescription &Target) { + auto HintFixture = createFunction("pbqp-hint", 50); + if (!HintFixture) + return; + auto Value = createVReg(*HintFixture->Function); + if (!Value || !buildDef(*HintFixture->Function, Target, *HintFixture->Block, *Value, 1) || + !buildUse(*HintFixture->Function, Target, *HintFixture->Block, *Value) || + !HintFixture->Function->registerInfo().addAllocationHint(*Value, + physical(TestRegister::G1)) || + !HintFixture->Function->registerInfo().addAllocationHint(*Value, + physical(TestRegister::G0)) || + !prepareForAllocation(*HintFixture->Function, Target)) + return; + TestRegisterAllocationInfo Info; + auto Allocated = runAllocator(*HintFixture->Function, Target, Info, AllocatorKind::PBQP); + const MachineInstr *Use = HintFixture->Block->lastInstruction(); + Context.expect(Allocated && Use != nullptr && + Use->operandAt(0)->registerValue() == + Register::fromPhysical(physical(TestRegister::G1)), + "PBQP must preserve allocation-hint preference order"); + + auto First = createPressureFunction(Target, 51, false); + auto Second = createPressureFunction(Target, 52, false); + if (!First || !Second) + return; + auto FirstResult = runAllocator(*First->Function, Target, Info, AllocatorKind::PBQP, + {.EnableRematerialization = false}); + auto SecondResult = runAllocator(*Second->Function, Target, Info, AllocatorKind::PBQP, + {.EnableRematerialization = false}); + Context.expect(FirstResult && SecondResult && + signature(*First->Function) == signature(*Second->Function), + "PBQP allocation and spill/rebuild order must be deterministic"); +} + +void testCopyCoalescingSwitch(TestContext &Context, const TargetDescription &Target) { + TestRegisterAllocationInfo Info; + auto Coalesced = createPressureFunction(Target, 53, true); + auto Uncoalesced = createPressureFunction(Target, 54, true); + if (!Coalesced || !Uncoalesced) + return; + auto WithCoalescing = + runAllocator(*Coalesced->Function, Target, Info, AllocatorKind::PBQP, + {.EnableRematerialization = false, .EnableCopyCoalescing = true}); + auto WithoutCoalescing = + runAllocator(*Uncoalesced->Function, Target, Info, AllocatorKind::PBQP, + {.EnableRematerialization = false, .EnableCopyCoalescing = false}); + Context.expect(WithCoalescing && WithoutCoalescing && + Coalesced->Function->properties().has(MachineFunctionProperty::NoVRegs) && + Uncoalesced->Function->properties().has(MachineFunctionProperty::NoVRegs), + "PBQP copy-coalescing modes must both produce valid physical mappings"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "PBQP allocator test target must be constructible"); + if (!Target) + return Context.result(); + testHintsAndDeterminism(Context, *Target); + testCopyCoalescingSwitch(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/RegisterAllocationTest.cpp b/backend/unittests/CodeGen/RegisterAllocationTest.cpp new file mode 100644 index 0000000..443d00a --- /dev/null +++ b/backend/unittests/CodeGen/RegisterAllocationTest.cpp @@ -0,0 +1,1044 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/LiveIntervals.h" +#include "rcc/CodeGen/LiveRangeEdit.h" +#include "rcc/CodeGen/LiveRegMatrix.h" +#include "rcc/CodeGen/LiveVariables.h" +#include "rcc/CodeGen/LowLevelType.h" +#include "rcc/CodeGen/MachineBasicBlock.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/MachineOperand.h" +#include "rcc/CodeGen/MachinePassManager.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/RegAllocCommon.h" +#include "rcc/CodeGen/RegAllocFast.h" +#include "rcc/CodeGen/RegAllocPBQP.h" +#include "rcc/CodeGen/RegisterMask.h" +#include "rcc/CodeGen/Spiller.h" +#include "rcc/CodeGen/TargetDescription.h" +#include "rcc/CodeGen/TargetRegisterAllocationInfo.h" +#include "rcc/CodeGen/VirtRegMap.h" + +#include <algorithm> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#include <ranges> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t DefOpcode = 3000; +constexpr std::uint32_t RematerializableDefOpcode = 3001; +constexpr std::uint32_t UseOpcode = 3002; +constexpr std::uint32_t BinaryOpcode = 3003; +constexpr std::uint32_t SpillLoadOpcode = 3004; +constexpr std::uint32_t SpillStoreOpcode = 3005; +constexpr std::uint32_t MaskOpcode = 3006; +constexpr std::uint32_t EarlyOpcode = 3007; +constexpr std::uint32_t TiedOpcode = 3008; +constexpr std::uint32_t CallDefOpcode = 3009; + +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]] constexpr rcc::Opcode testOpcode(std::uint32_t Value) noexcept { + return rcc::Opcode::fromValue(Value); +} + +[[nodiscard]] rcc::PhysicalRegister physical(std::uint32_t Number) { + return *rcc::PhysicalRegister::create(Number); +} + +[[nodiscard]] rcc::OperandDescriptor registerOperand(rcc::RegisterOperandRole Role, + bool EarlyClobber = false) { + rcc::OperandDescriptor Result; + Result.Kinds = {rcc::MachineOperand::Kind::Register}; + Result.RegisterRole = Role; + Result.AllowSubReg = true; + Result.EarlyClobber = EarlyClobber; + return Result; +} + +[[nodiscard]] rcc::OperandDescriptor plainOperand(rcc::MachineOperand::Kind Kind) { + rcc::OperandDescriptor Result; + Result.Kinds = {Kind}; + return Result; +} + +[[nodiscard]] rcc::InstructionDescriptor descriptor(std::uint32_t Value, std::string Name) { + rcc::InstructionDescriptor Result; + Result.InstructionOpcode = testOpcode(Value); + Result.Name = std::move(Name); + Result.Flags = {rcc::InstructionFlag::ProducesCode}; + return Result; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> +createTarget(bool AddSharedMultiLaneUnit = false) { + std::vector<rcc::InstructionDescriptor> Descriptors; + + auto Def = descriptor(DefOpcode, "RA_DEF"); + Def.Operands = {registerOperand(rcc::RegisterOperandRole::Def), + plainOperand(rcc::MachineOperand::Kind::Immediate)}; + Def.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Def)); + + auto Remat = descriptor(RematerializableDefOpcode, "RA_REMAT_DEF"); + Remat.Operands = {registerOperand(rcc::RegisterOperandRole::Def), + plainOperand(rcc::MachineOperand::Kind::Immediate)}; + Remat.NumExplicitDefs = 1; + Remat.Flags.set(rcc::InstructionFlag::Rematerializable); + Descriptors.push_back(std::move(Remat)); + + auto Use = descriptor(UseOpcode, "RA_USE"); + Use.Operands = {registerOperand(rcc::RegisterOperandRole::Use)}; + Use.Flags.set(rcc::InstructionFlag::HasSideEffects); + Descriptors.push_back(std::move(Use)); + + auto Binary = descriptor(BinaryOpcode, "RA_BINARY"); + Binary.Operands = {registerOperand(rcc::RegisterOperandRole::Def), + registerOperand(rcc::RegisterOperandRole::Use), + registerOperand(rcc::RegisterOperandRole::Use)}; + Binary.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Binary)); + + auto Reload = descriptor(SpillLoadOpcode, "RA_RELOAD"); + Reload.Operands = {registerOperand(rcc::RegisterOperandRole::Def), + plainOperand(rcc::MachineOperand::Kind::FrameIndex)}; + Reload.NumExplicitDefs = 1; + Reload.Flags.set(rcc::InstructionFlag::MayLoad); + Reload.RequiresMemoryOperands = true; + Descriptors.push_back(std::move(Reload)); + + auto Spill = descriptor(SpillStoreOpcode, "RA_SPILL"); + Spill.Operands = {registerOperand(rcc::RegisterOperandRole::Use), + plainOperand(rcc::MachineOperand::Kind::FrameIndex)}; + Spill.Flags.set(rcc::InstructionFlag::MayStore); + Spill.RequiresMemoryOperands = true; + Descriptors.push_back(std::move(Spill)); + + auto Mask = descriptor(MaskOpcode, "RA_MASK"); + Mask.Operands = {plainOperand(rcc::MachineOperand::Kind::RegisterMask)}; + Mask.Flags.set(rcc::InstructionFlag::Call); + Mask.Flags.set(rcc::InstructionFlag::HasSideEffects); + Descriptors.push_back(std::move(Mask)); + + auto Early = descriptor(EarlyOpcode, "RA_EARLY"); + Early.Operands = {registerOperand(rcc::RegisterOperandRole::Def, true), + registerOperand(rcc::RegisterOperandRole::Use)}; + Early.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Early)); + + auto Tied = descriptor(TiedOpcode, "RA_TIED"); + auto TiedDef = registerOperand(rcc::RegisterOperandRole::Def); + auto TiedUse = registerOperand(rcc::RegisterOperandRole::Use); + TiedDef.DefaultTiedOperand = 1; + TiedUse.DefaultTiedOperand = 0; + Tied.Operands = {TiedDef, TiedUse}; + Tied.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Tied)); + + auto CallDef = descriptor(CallDefOpcode, "RA_CALL_DEF"); + CallDef.Operands = {registerOperand(rcc::RegisterOperandRole::Def), + plainOperand(rcc::MachineOperand::Kind::RegisterMask)}; + CallDef.NumExplicitDefs = 1; + CallDef.Flags.set(rcc::InstructionFlag::Call); + CallDef.Flags.set(rcc::InstructionFlag::HasSideEffects); + Descriptors.push_back(std::move(CallDef)); + + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors), {}); + if (!Instructions) + return std::unexpected(Instructions.error()); + if (AddSharedMultiLaneUnit) { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + const rcc::RegisterUnitID SharedUnit = rcc::RegisterUnitID::fromValue(12); + Description.RegisterUnits.push_back({SharedUnit, "shared.multi-lane"}); + for (rcc::PhysicalRegisterDesc &Register : Description.PhysicalRegisters) { + if (Register.Reg == physical(rcc::test::TestRegister::G0) || + Register.Reg == physical(rcc::test::TestRegister::G1)) { + Register.Units.push_back({SharedUnit, rcc::LaneMask::fromRaw(0xF)}); + } + } + auto Registers = rcc::TargetRegisterInfo::create(std::move(Description)); + if (!Registers) + return std::unexpected(Registers.error()); + return rcc::TargetDescription::create(std::move(*Instructions), std::move(*Registers), + rcc::test::createTestTargetMIRTextInfoDescription()); + } + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] rcc::Expected<const rcc::MachineMemOperand *> +createStackMemory(rcc::MachineIRBuilder &Builder, rcc::FrameIndex StackSlot, + rcc::MemoryAccessFlag Access) { + auto Size = rcc::MemorySize::fixed(8); + auto Align = rcc::Alignment::create(8); + if (!Size) + return std::unexpected(Size.error()); + if (!Align) + return std::unexpected(Align.error()); + rcc::MachineMemOperandDesc Description; + Description.Accesses = {Access}; + Description.Size = *Size; + Description.Align = *Align; + Description.Base = rcc::MemoryBase::frameIndex(StackSlot); + return Builder.function().createMemOperand(std::move(Description)); +} + +class TestRegisterAllocationInfo final : public rcc::TargetRegisterAllocationInfo { +public: + [[nodiscard]] rcc::Expected<void> + validateSpillReload(rcc::RegisterClassID Class, + const rcc::TargetDescription &Target) const override { + const rcc::RegisterClassDesc *Description = Target.registers().registerClass(Class); + if (Description == nullptr || !Description->Allocatable || Description->SpillSizeInBits != 64 || + Description->SpillAlignmentInBits != 64) { + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, + "test target register class is not spillable"); + } + return {}; + } + + [[nodiscard]] rcc::Expected<rcc::MachineInstr *> + emitReload(rcc::MachineIRBuilder &Builder, rcc::Register Destination, rcc::RegisterClassID Class, + rcc::FrameIndex StackSlot, rcc::LaneMask Lanes) const override { + auto Validated = validateSpillReload(Class, Builder.target()); + if (!Validated) + return std::unexpected(Validated.error()); + if (Lanes.empty()) + return rcc::makeUnexpected(rcc::ErrorCode::InvalidArgument, "test reload requires lanes"); + auto Memory = createStackMemory(Builder, StackSlot, rcc::MemoryAccessFlag::Load); + if (!Memory) + return std::unexpected(Memory.error()); + auto Pending = Builder.buildInstr(testOpcode(SpillLoadOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Destination); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addFrameIndex(StackSlot); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addMemOperand(**Memory); !Result) + return std::unexpected(Result.error()); + return Pending->build(); + } + + [[nodiscard]] rcc::Expected<rcc::MachineInstr *> + emitSpill(rcc::MachineIRBuilder &Builder, rcc::Register Source, rcc::RegisterClassID Class, + rcc::FrameIndex StackSlot, rcc::LaneMask Lanes) const override { + auto Validated = validateSpillReload(Class, Builder.target()); + if (!Validated) + return std::unexpected(Validated.error()); + if (Lanes.empty()) + return rcc::makeUnexpected(rcc::ErrorCode::InvalidArgument, "test spill requires lanes"); + auto Memory = createStackMemory(Builder, StackSlot, rcc::MemoryAccessFlag::Store); + if (!Memory) + return std::unexpected(Memory.error()); + auto Pending = Builder.buildInstr(testOpcode(SpillStoreOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addUse(Source); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addFrameIndex(StackSlot); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addMemOperand(**Memory); !Result) + return std::unexpected(Result.error()); + return Pending->build(); + } + + [[nodiscard]] rcc::Expected<bool> + isRematerializable(const rcc::MachineInstr &Instruction, rcc::Register VirtualReg, + const rcc::MachineFunction &, const rcc::TargetDescription &) const override { + const rcc::MachineOperand *Operand = Instruction.operandAt(0); + const rcc::Register *Reg = Operand == nullptr ? nullptr : Operand->getIf<rcc::Register>(); + return Instruction.opcode() == testOpcode(RematerializableDefOpcode) && Reg != nullptr && + *Reg == VirtualReg; + } + + [[nodiscard]] rcc::Expected<rcc::MachineInstr *> + rematerialize(rcc::MachineIRBuilder &Builder, const rcc::MachineInstr &Original, + rcc::Register Destination, rcc::Register OriginalRegister, + const rcc::TargetDescription &Target) const override { + auto Accepted = isRematerializable(Original, OriginalRegister, Builder.function(), Target); + if (!Accepted) + return std::unexpected(Accepted.error()); + if (!*Accepted) + return rcc::makeUnexpected(rcc::ErrorCode::InvalidArgument, + "test instruction is not rematerializable"); + auto Pending = Builder.buildInstr(testOpcode(RematerializableDefOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Destination); !Result) + return std::unexpected(Result.error()); + const rcc::MachineOperand *Immediate = Original.operandAt(1); + if (Immediate == nullptr) + return rcc::makeUnexpected(rcc::ErrorCode::InternalError, + "test rematerialization lost its immediate"); + if (auto Result = Pending->addOperand(Immediate->cloneDetached()); !Result) + return std::unexpected(Result.error()); + return Pending->build(); + } +}; + +class RejectingRegisterAllocationInfo final : public rcc::TargetRegisterAllocationInfo { +public: + [[nodiscard]] rcc::Expected<void> + validateSpillReload(rcc::RegisterClassID, const rcc::TargetDescription &) const override { + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, + "test target intentionally rejects spills"); + } + + [[nodiscard]] rcc::Expected<rcc::MachineInstr *> emitReload(rcc::MachineIRBuilder &, + rcc::Register, rcc::RegisterClassID, + rcc::FrameIndex, + rcc::LaneMask) const override { + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, + "test target intentionally rejects reloads"); + } + + [[nodiscard]] rcc::Expected<rcc::MachineInstr *> emitSpill(rcc::MachineIRBuilder &, rcc::Register, + rcc::RegisterClassID, rcc::FrameIndex, + rcc::LaneMask) const override { + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, + "test target intentionally rejects spills"); + } +}; + +struct FunctionFixture final { + std::unique_ptr<rcc::MachineFunction> Function; + rcc::MachineBasicBlock *Block = nullptr; +}; + +[[nodiscard]] rcc::Expected<FunctionFixture> createFunction(std::string Name, std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + if (!Function) + return std::unexpected(Function.error()); + auto Block = (*Function)->createBasicBlock(); + if (!Block) + return std::unexpected(Block.error()); + auto Appended = (*Function)->appendBlock(**Block); + if (!Appended) + return std::unexpected(Appended.error()); + return FunctionFixture{std::move(*Function), *Block}; +} + +[[nodiscard]] rcc::Expected<rcc::Register> +createVReg(rcc::MachineFunction &Function, + rcc::RegisterClassID Class = rcc::test::TestRegisterClass::GPR64) { + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + if (!Type) + return std::unexpected(Type.error()); + auto Reg = Function.registerInfo().createVirtualRegister(*Type); + if (!Reg) + return std::unexpected(Reg.error()); + if (auto Result = Function.registerInfo().setRegisterBank(*Reg, rcc::test::TestRegisterBank::GPR); + !Result) { + return std::unexpected(Result.error()); + } + if (auto Result = Function.registerInfo().setRegisterClass(*Reg, Class); !Result) + return std::unexpected(Result.error()); + return *Reg; +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> +buildDef(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Register Reg, std::int64_t Value, + rcc::SubRegIndex SubReg = {}, bool Rematerializable = false) { + rcc::MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = + Builder.buildInstr(testOpcode(Rematerializable ? RematerializableDefOpcode : DefOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Reg, {}, SubReg); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addImmediate(Value); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> +buildUse(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Register Reg, rcc::SubRegIndex SubReg = {}) { + rcc::MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(testOpcode(UseOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addUse(Reg, {}, SubReg); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> +buildMask(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, std::shared_ptr<const rcc::RegisterMask> Mask) { + rcc::MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(testOpcode(MaskOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addRegisterMask(std::move(Mask)); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> +buildCallDef(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Register Destination, + std::shared_ptr<const rcc::RegisterMask> Mask) { + rcc::MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(testOpcode(CallDefOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Destination); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addRegisterMask(std::move(Mask)); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> +buildEarly(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Register Destination, rcc::Register Source) { + rcc::MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(testOpcode(EarlyOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + rcc::RegisterOperandFlags Flags; + Flags.set(rcc::RegisterOperandFlag::EarlyClobber); + if (auto Result = Pending->addDef(Destination, Flags); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addUse(Source); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> buildTied(rcc::MachineFunction &Function, + const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, + rcc::Register Reg) { + rcc::MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(testOpcode(TiedOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Reg); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addUse(Reg); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->tieOperands(0, 1); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] rcc::Expected<void> prepareForAllocation(rcc::MachineFunction &Function, + const rcc::TargetDescription &Target) { + auto Rebuilt = rcc::rebuildMachineLiveness(Function, Target); + if (!Rebuilt) + return std::unexpected(Rebuilt.error()); + for (const rcc::MachineFunctionProperty Property : { + rcc::MachineFunctionProperty::TracksLiveness, + rcc::MachineFunctionProperty::Legalized, + rcc::MachineFunctionProperty::RegBankSelected, + rcc::MachineFunctionProperty::Selected, + rcc::MachineFunctionProperty::NoPHIs, + rcc::MachineFunctionProperty::TiedOpsRewritten, + }) { + Function.properties().set(Property); + } + return {}; +} + +enum class AllocatorKind : std::uint8_t { + Basic, + Fast, + PBQP, +}; + +[[nodiscard]] rcc::Expected<void> runAllocator(rcc::MachineFunction &Function, + const rcc::TargetDescription &Target, + const rcc::TargetRegisterAllocationInfo &Info, + AllocatorKind Kind, + rcc::RegisterAllocationOptions Options = {}) { + rcc::MachineFunctionAnalysisManager Analyses; + auto Registered = rcc::registerStage11MachineAnalyses(Analyses); + if (!Registered) + return std::unexpected(Registered.error()); + std::unique_ptr<rcc::MachineFunctionPass> Pass; + switch (Kind) { + case AllocatorKind::Basic: + Pass = rcc::createBasicAllocatorPass(Info, Options); + break; + case AllocatorKind::Fast: + Pass = std::make_unique<rcc::FastAllocatorPass>(Info); + break; + case AllocatorKind::PBQP: + Pass = std::make_unique<rcc::PBQPAllocatorPass>(Info, Options); + break; + } + rcc::MachineFunctionPassManager Pipeline; + auto Added = Pipeline.addPass(std::move(Pass)); + if (!Added) + return std::unexpected(Added.error()); + return Pipeline.run(Function, Target, Analyses); +} + +[[nodiscard]] std::size_t countOpcode(const rcc::MachineFunction &Function, std::uint32_t Value) { + std::size_t Count = 0; + for (const rcc::MachineBasicBlock *Block = Function.firstBlock(); Block != nullptr; + Block = Block->next()) { + for (const rcc::MachineInstr *Instruction = Block->firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + Count += Instruction->opcode() == testOpcode(Value) ? 1U : 0U; + } + } + return Count; +} + +[[nodiscard]] bool hasInterference(std::span<const rcc::RegisterInterference> Values, + rcc::RegisterInterferenceKind Kind) { + return std::ranges::any_of( + Values, [Kind](const rcc::RegisterInterference &Value) { return Value.Kind == Kind; }); +} + +[[nodiscard]] std::vector<std::uint64_t> functionSignature(const rcc::MachineFunction &Function) { + std::vector<std::uint64_t> Result; + Result.push_back(Function.frameInfo().liveObjectCount()); + for (const rcc::MachineBasicBlock *Block = Function.firstBlock(); Block != nullptr; + Block = Block->next()) { + Result.push_back(Block->stableID().value()); + for (const rcc::MachineInstr *Instruction = Block->firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + Result.push_back(Instruction->opcode().value()); + Result.push_back(Instruction->operandCount()); + for (const rcc::MachineOperand &Operand : Instruction->operands()) { + Result.push_back(static_cast<std::uint64_t>(Operand.kind())); + const rcc::Register *Reg = Operand.getIf<rcc::Register>(); + Result.push_back(Reg == nullptr ? 0 : Reg->opaqueValue()); + } + } + } + return Result; +} + +void testVirtRegMapAndEdit(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction("virtreg-map", 1); + if (!Fixture) + return; + auto Parent = createVReg(*Fixture->Function); + if (!Parent) + return; + rcc::VirtRegMap Mapping(*Fixture->Function); + Context.expect(Mapping.add(*Parent).has_value(), + "VirtRegMap must accept a known virtual register"); + rcc::LiveRangeEdit Edit(*Fixture->Function, Target, Mapping); + auto Child = Edit.createFrom(*Parent, false); + auto Slot = Edit.getOrCreateSpillSlot(*Parent); + Context.expect(Child && Slot && Mapping.stackSlot(*Parent) == Slot && + Mapping.entry(*Child) != nullptr && + Mapping.entry(*Child)->SplitParent == *Parent && + Mapping.entry(*Child)->Origin == *Parent && Mapping.verify().has_value(), + "LiveRangeEdit must preserve class, origin, split, and spill-slot state"); + if (!Child) + return; + const auto G0 = physical(rcc::test::TestRegister::G0); + Context.expect(Mapping.assign(*Child, G0).has_value() && Mapping.physicalRegister(*Child) == G0 && + !Mapping.isSpillable(*Child), + "VirtRegMap must publish deterministic child assignments"); +} + +void testLiveRegMatrix(TestContext &Context, const rcc::TargetDescription &Target) { + auto LaneFixture = createFunction("lane-matrix", 2); + if (!LaneFixture) + return; + auto Low = createVReg(*LaneFixture->Function); + auto High = createVReg(*LaneFixture->Function); + if (!Low || !High) + return; + if (!buildDef(*LaneFixture->Function, Target, *LaneFixture->Block, *Low, 1, + rcc::test::TestSubReg::Low32) || + !buildDef(*LaneFixture->Function, Target, *LaneFixture->Block, *High, 2, + rcc::test::TestSubReg::High32) || + !buildUse(*LaneFixture->Function, Target, *LaneFixture->Block, *High, + rcc::test::TestSubReg::High32) || + !buildUse(*LaneFixture->Function, Target, *LaneFixture->Block, *Low, + rcc::test::TestSubReg::Low32)) { + return; + } + if (!rcc::rebuildMachineLiveness(*LaneFixture->Function, Target)) + return; + rcc::MachineFunctionAnalysisManager LaneAnalyses; + if (!rcc::registerStage11MachineAnalyses(LaneAnalyses)) + return; + auto LaneIntervals = + LaneAnalyses.getResult<rcc::LiveIntervalsAnalysis>(*LaneFixture->Function, Target); + if (!LaneIntervals) + return; + const auto G0 = physical(rcc::test::TestRegister::G0); + rcc::LiveRegMatrix LaneMatrix(*LaneFixture->Function, Target, **LaneIntervals); + Context.expect(LaneMatrix.assign(*Low, G0).has_value() && + LaneMatrix.checkInterference(*High, G0).Kind == + rcc::RegisterInterferenceKind::Free && + LaneMatrix.assign(*High, G0).has_value(), + "LiveRegMatrix must permit overlapping intervals on disjoint subregister lanes"); + + auto FixedFixture = createFunction("fixed-matrix", 3); + if (!FixedFixture) + return; + auto Value = createVReg(*FixedFixture->Function); + if (!Value || !buildDef(*FixedFixture->Function, Target, *FixedFixture->Block, *Value, 7) || + !buildMask(*FixedFixture->Function, Target, *FixedFixture->Block, + rcc::test::createTestCallPreservedMask()) || + !buildUse(*FixedFixture->Function, Target, *FixedFixture->Block, + rcc::Register::fromPhysical(G0)) || + !buildUse(*FixedFixture->Function, Target, *FixedFixture->Block, *Value)) { + return; + } + if (!rcc::rebuildMachineLiveness(*FixedFixture->Function, Target)) + return; + rcc::MachineFunctionAnalysisManager FixedAnalyses; + if (!rcc::registerStage11MachineAnalyses(FixedAnalyses)) + return; + auto FixedIntervals = + FixedAnalyses.getResult<rcc::LiveIntervalsAnalysis>(*FixedFixture->Function, Target); + if (!FixedIntervals) + return; + rcc::LiveRegMatrix FixedMatrix(*FixedFixture->Function, Target, **FixedIntervals); + const auto G0Interferences = FixedMatrix.collectInterferences(*Value, G0); + const auto G1 = physical(rcc::test::TestRegister::G1); + Context.expect( + hasInterference(G0Interferences, rcc::RegisterInterferenceKind::RegisterMask) && + (hasInterference(G0Interferences, rcc::RegisterInterferenceKind::FixedRegister) || + hasInterference(G0Interferences, rcc::RegisterInterferenceKind::RegisterUnit)) && + FixedMatrix.checkInterference(*Value, G1).Kind == rcc::RegisterInterferenceKind::Free, + "LiveRegMatrix must classify fixed, unit, and RegMask interference separately"); + + auto FixedLaneFixture = createFunction("fixed-lane-matrix", 4); + if (!FixedLaneFixture) + return; + auto HighValue = createVReg(*FixedLaneFixture->Function); + const auto G0Low32 = physical(rcc::test::TestRegister::G0Low32); + if (!HighValue || + !buildDef(*FixedLaneFixture->Function, Target, *FixedLaneFixture->Block, *HighValue, 9, + rcc::test::TestSubReg::High32) || + !buildUse(*FixedLaneFixture->Function, Target, *FixedLaneFixture->Block, + rcc::Register::fromPhysical(G0Low32)) || + !buildUse(*FixedLaneFixture->Function, Target, *FixedLaneFixture->Block, *HighValue, + rcc::test::TestSubReg::High32) || + !rcc::rebuildMachineLiveness(*FixedLaneFixture->Function, Target)) { + return; + } + rcc::MachineFunctionAnalysisManager FixedLaneAnalyses; + if (!rcc::registerStage11MachineAnalyses(FixedLaneAnalyses)) + return; + auto FixedLaneIntervals = + FixedLaneAnalyses.getResult<rcc::LiveIntervalsAnalysis>(*FixedLaneFixture->Function, Target); + if (!FixedLaneIntervals) + return; + rcc::LiveRegMatrix FixedLaneMatrix(*FixedLaneFixture->Function, Target, **FixedLaneIntervals); + Context.expect(FixedLaneMatrix.checkInterference(*HighValue, G0).Kind == + rcc::RegisterInterferenceKind::Free, + "a fixed subregister must not interfere with a disjoint virtual lane"); +} + +void testSharedUnitLaneMatrix(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction("shared-unit-lane-matrix", 5); + if (!Fixture) + return; + auto Low = createVReg(*Fixture->Function); + auto High = createVReg(*Fixture->Function); + if (!Low || !High || + !buildDef(*Fixture->Function, Target, *Fixture->Block, *Low, 1, + rcc::test::TestSubReg::Low32) || + !buildDef(*Fixture->Function, Target, *Fixture->Block, *High, 2, + rcc::test::TestSubReg::High32) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *High, + rcc::test::TestSubReg::High32) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Low, rcc::test::TestSubReg::Low32) || + !rcc::rebuildMachineLiveness(*Fixture->Function, Target)) { + return; + } + rcc::MachineFunctionAnalysisManager Analyses; + if (!rcc::registerStage11MachineAnalyses(Analyses)) + return; + auto Intervals = Analyses.getResult<rcc::LiveIntervalsAnalysis>(*Fixture->Function, Target); + if (!Intervals) + return; + const auto G0 = physical(rcc::test::TestRegister::G0); + const auto G1 = physical(rcc::test::TestRegister::G1); + rcc::LiveRegMatrix Matrix(*Fixture->Function, Target, **Intervals); + Context.expect(Target.registers().regsOverlap(G0, G1) && Matrix.assign(*Low, G0).has_value() && + Matrix.checkInterference(*High, G1).Kind == + rcc::RegisterInterferenceKind::Free && + Matrix.assign(*High, G1).has_value(), + "disjoint lanes must coexist on a shared multi-lane RegisterUnit"); +} + +[[nodiscard]] rcc::Expected<FunctionFixture> +createPressureFunction(const rcc::TargetDescription &Target, std::uint64_t ID) { + auto Fixture = createFunction("pressure-" + std::to_string(ID), ID); + if (!Fixture) + return std::unexpected(Fixture.error()); + std::vector<rcc::Register> Values; + for (std::int64_t Index = 0; Index < 3; ++Index) { + auto Reg = createVReg(*Fixture->Function); + if (!Reg) + return std::unexpected(Reg.error()); + if (auto Built = buildDef(*Fixture->Function, Target, *Fixture->Block, *Reg, Index); !Built) + return std::unexpected(Built.error()); + Values.push_back(*Reg); + } + for (const rcc::Register Reg : Values) { + if (auto Built = buildUse(*Fixture->Function, Target, *Fixture->Block, Reg); !Built) + return std::unexpected(Built.error()); + } + auto Prepared = prepareForAllocation(*Fixture->Function, Target); + if (!Prepared) + return std::unexpected(Prepared.error()); + return Fixture; +} + +void testAllocatorsUnderPressure(TestContext &Context, const rcc::TargetDescription &Target, + const TestRegisterAllocationInfo &Info) { + for (const auto [Kind, ID, Name] : { + std::tuple{AllocatorKind::Basic, std::uint64_t{10}, std::string_view{"Basic"}}, + std::tuple{AllocatorKind::Fast, std::uint64_t{11}, std::string_view{"Fast"}}, + std::tuple{AllocatorKind::PBQP, std::uint64_t{12}, std::string_view{"PBQP"}}, + }) { + auto Fixture = createPressureFunction(Target, ID); + if (!Fixture) + continue; + auto Allocated = + runAllocator(*Fixture->Function, Target, Info, Kind, {.EnableRematerialization = false}); + if (!Allocated) + std::cerr << Name << " allocation error: " << Allocated.error().message() << '\n'; + Context.expect(Allocated.has_value() && + Fixture->Function->properties().has(rcc::MachineFunctionProperty::NoVRegs) && + Fixture->Function->frameInfo().liveObjectCount() != 0 && + countOpcode(*Fixture->Function, SpillLoadOpcode) != 0 && + countOpcode(*Fixture->Function, SpillStoreOpcode) != 0, + "all allocators must close a deterministic spill/reload cycle under pressure"); + } +} + +void testFailureAtomicity(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createPressureFunction(Target, 13); + if (!Fixture) + return; + const std::vector<std::uint64_t> Before = functionSignature(*Fixture->Function); + const std::size_t KnownBefore = static_cast<std::size_t>(std::ranges::count_if( + Fixture->Function->registerInfo().virtualRegisters(), + [](const rcc::VirtualRegisterInfo &Info) { return !Info.IsTombstone; })); + RejectingRegisterAllocationInfo Rejecting; + auto Failed = runAllocator(*Fixture->Function, Target, Rejecting, AllocatorKind::Basic, + {.EnableRematerialization = false}); + const std::size_t KnownAfter = static_cast<std::size_t>(std::ranges::count_if( + Fixture->Function->registerInfo().virtualRegisters(), + [](const rcc::VirtualRegisterInfo &Info) { return !Info.IsTombstone; })); + Context.expect(!Failed && Failed.error().code() == rcc::ErrorCode::InternalError && + functionSignature(*Fixture->Function) == Before && KnownAfter == KnownBefore && + !Fixture->Function->properties().has(rcc::MachineFunctionProperty::NoVRegs), + "failed register allocation must leave the original MachineFunction unchanged"); +} + +void testFastBlockBoundary(TestContext &Context, const rcc::TargetDescription &Target, + const TestRegisterAllocationInfo &Info) { + auto Fixture = createFunction("fast-block-boundary", 14); + if (!Fixture) + return; + auto Exit = Fixture->Function->createBasicBlock(); + if (!Exit || !Fixture->Function->appendBlock(**Exit) || + !Fixture->Function->cfgUpdater().addSuccessor(*Fixture->Block, **Exit, + rcc::BranchProbability::one())) { + return; + } + auto Value = createVReg(*Fixture->Function); + const rcc::Register G0 = rcc::Register::fromPhysical(physical(rcc::test::TestRegister::G0)); + if (!Value || !buildDef(*Fixture->Function, Target, *Fixture->Block, *Value, 9) || + !buildUse(*Fixture->Function, Target, **Exit, *Value) || + !buildUse(*Fixture->Function, Target, **Exit, G0) || + !prepareForAllocation(*Fixture->Function, Target)) { + return; + } + auto Allocated = runAllocator(*Fixture->Function, Target, Info, AllocatorKind::Fast); + Context.expect( + Allocated && countOpcode(*Fixture->Function, SpillStoreOpcode) == 1 && + countOpcode(*Fixture->Function, SpillLoadOpcode) == 1 && + (*Exit)->firstInstruction()->next() != nullptr && + (*Exit)->firstInstruction()->next()->operandAt(0)->registerValue() != G0, + "FastAllocator must preserve block live-ins and virtual values across a CFG boundary"); +} + +void testFastRegMaskCalls(TestContext &Context, const rcc::TargetDescription &Target, + const TestRegisterAllocationInfo &Info) { + auto LiveThrough = createFunction("fast-regmask-live-through", 16); + if (!LiveThrough) + return; + auto Value = createVReg(*LiveThrough->Function); + if (!Value || !buildDef(*LiveThrough->Function, Target, *LiveThrough->Block, *Value, 11) || + !buildMask(*LiveThrough->Function, Target, *LiveThrough->Block, + rcc::test::createTestCallPreservedMask()) || + !buildUse(*LiveThrough->Function, Target, *LiveThrough->Block, *Value) || + !prepareForAllocation(*LiveThrough->Function, Target)) { + return; + } + auto LiveThroughAllocated = + runAllocator(*LiveThrough->Function, Target, Info, AllocatorKind::Fast); + Context.expect(LiveThroughAllocated && + countOpcode(*LiveThrough->Function, SpillStoreOpcode) == 1 && + countOpcode(*LiveThrough->Function, SpillLoadOpcode) == 1, + "FastAllocator must spill and reload a value live through a clobbering RegMask"); + + auto CallResult = createFunction("fast-regmask-call-result", 17); + if (!CallResult) + return; + auto Result = createVReg(*CallResult->Function); + if (!Result || + !buildCallDef(*CallResult->Function, Target, *CallResult->Block, *Result, + rcc::test::createTestCallPreservedMask()) || + !buildUse(*CallResult->Function, Target, *CallResult->Block, *Result) || + !prepareForAllocation(*CallResult->Function, Target)) { + return; + } + auto ResultAllocated = runAllocator(*CallResult->Function, Target, Info, AllocatorKind::Fast); + Context.expect(ResultAllocated && countOpcode(*CallResult->Function, SpillStoreOpcode) == 0 && + countOpcode(*CallResult->Function, SpillLoadOpcode) == 0, + "FastAllocator must retain a call-defined result without reloading an " + "uninitialized spill slot"); +} + +void testTiedAndEarlyClobber(TestContext &Context, const rcc::TargetDescription &Target, + const TestRegisterAllocationInfo &Info) { + auto Fixture = createFunction("tied-early", 15); + if (!Fixture) + return; + auto Source = createVReg(*Fixture->Function); + auto Destination = createVReg(*Fixture->Function); + if (!Source || !Destination || + !buildDef(*Fixture->Function, Target, *Fixture->Block, *Source, 1)) { + return; + } + auto Early = buildEarly(*Fixture->Function, Target, *Fixture->Block, *Destination, *Source); + auto Tied = buildTied(*Fixture->Function, Target, *Fixture->Block, *Destination); + if (!Early || !Tied || !buildUse(*Fixture->Function, Target, *Fixture->Block, *Destination) || + !prepareForAllocation(*Fixture->Function, Target)) { + return; + } + auto Allocated = runAllocator(*Fixture->Function, Target, Info, AllocatorKind::Basic); + const std::optional<rcc::Register> EarlyDef = (*Early)->operandAt(0)->registerValue(); + const std::optional<rcc::Register> EarlyUse = (*Early)->operandAt(1)->registerValue(); + const std::optional<rcc::Register> TiedDef = (*Tied)->operandAt(0)->registerValue(); + const std::optional<rcc::Register> TiedUse = (*Tied)->operandAt(1)->registerValue(); + Context.expect(Allocated && EarlyDef && EarlyUse && *EarlyDef != *EarlyUse && TiedDef && + TiedUse && *TiedDef == *TiedUse, + "allocation must separate early-clobber operands and preserve rewritten ties"); +} + +void testRematerialization(TestContext &Context, const rcc::TargetDescription &Target, + const TestRegisterAllocationInfo &Info) { + auto Fixture = createFunction("rematerialization", 20); + if (!Fixture) + return; + auto Value = createVReg(*Fixture->Function); + if (!Value || !buildDef(*Fixture->Function, Target, *Fixture->Block, *Value, 42, {}, true) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value)) { + return; + } + rcc::VirtRegMap Mapping(*Fixture->Function); + if (!Mapping.add(*Value)) + return; + rcc::Spiller Spiller(*Fixture->Function, Target, Info, Mapping, true); + auto Children = Spiller.spill(*Value); + Context.expect(Children && Children->size() == 2 && + Fixture->Function->frameInfo().liveObjectCount() == 0 && + countOpcode(*Fixture->Function, RematerializableDefOpcode) == 2 && + countOpcode(*Fixture->Function, SpillLoadOpcode) == 0 && + countOpcode(*Fixture->Function, SpillStoreOpcode) == 0, + "Spiller must rematerialize each use without creating a stack object"); + + auto Unspillable = createVReg(*Fixture->Function); + if (!Unspillable || !Mapping.add(*Unspillable, false)) + return; + auto Rejected = Spiller.spill(*Unspillable); + Context.expect(!Rejected && Rejected.error().code() == rcc::ErrorCode::RegisterAllocationFailed, + "Spiller must reject pressure on a non-spillable virtual register"); +} + +void testRewriteAndConstraints(TestContext &Context, const rcc::TargetDescription &Target, + const TestRegisterAllocationInfo &Info) { + auto RewriteFixture = createFunction("subregister-rewrite", 30); + if (!RewriteFixture) + return; + auto Value = createVReg(*RewriteFixture->Function); + if (!Value || + !buildDef(*RewriteFixture->Function, Target, *RewriteFixture->Block, *Value, 1, + rcc::test::TestSubReg::Low32) || + !buildUse(*RewriteFixture->Function, Target, *RewriteFixture->Block, *Value, + rcc::test::TestSubReg::Low32)) { + return; + } + rcc::VirtRegMap Mapping(*RewriteFixture->Function); + const auto G0 = physical(rcc::test::TestRegister::G0); + if (!Mapping.add(*Value) || !Mapping.assign(*Value, G0)) + return; + auto Rewritten = rcc::rewriteVirtualRegisters(*RewriteFixture->Function, Target, Mapping); + const auto G0Low32 = physical(rcc::test::TestRegister::G0Low32); + Context.expect(Rewritten && *Rewritten && + RewriteFixture->Block->firstInstruction()->operandAt(0)->registerValue() == + rcc::Register::fromPhysical(G0Low32) && + RewriteFixture->Block->firstInstruction()->operandAt(0)->subReg().value() == 0, + "physical rewrite must project and clear virtual subregister operands"); + + auto MaskFixture = createFunction("mask-allocation", 31); + if (!MaskFixture) + return; + auto AcrossCall = createVReg(*MaskFixture->Function); + if (!AcrossCall || + !buildDef(*MaskFixture->Function, Target, *MaskFixture->Block, *AcrossCall, 5) || + !buildMask(*MaskFixture->Function, Target, *MaskFixture->Block, + rcc::test::createTestCallPreservedMask()) || + !buildUse(*MaskFixture->Function, Target, *MaskFixture->Block, *AcrossCall) || + !prepareForAllocation(*MaskFixture->Function, Target)) { + return; + } + auto Allocated = runAllocator(*MaskFixture->Function, Target, Info, AllocatorKind::Basic); + const rcc::MachineInstr *FinalUse = MaskFixture->Block->lastInstruction(); + const auto G1 = physical(rcc::test::TestRegister::G1); + Context.expect(Allocated && FinalUse != nullptr && + FinalUse->operandAt(0)->registerValue() == rcc::Register::fromPhysical(G1), + "global allocation must keep call-crossing values in a preserved register"); +} + +void testInvalidRewriteMappings(TestContext &Context, const rcc::TargetDescription &Target) { + auto ExpectAtomicFailure = [&](FunctionFixture &Fixture, const rcc::VirtRegMap &Mapping, + std::string_view Message) { + const std::vector<std::uint64_t> Before = functionSignature(*Fixture.Function); + auto Rewritten = rcc::rewriteVirtualRegisters(*Fixture.Function, Target, Mapping); + Context.expect(!Rewritten && + Rewritten.error().code() == rcc::ErrorCode::RegisterAllocationFailed && + functionSignature(*Fixture.Function) == Before, + Message); + }; + + auto ClassFixture = createFunction("invalid-class-mapping", 40); + if (!ClassFixture) + return; + auto ClassValue = createVReg(*ClassFixture->Function); + if (!ClassValue || + !buildDef(*ClassFixture->Function, Target, *ClassFixture->Block, *ClassValue, 1) || + !buildUse(*ClassFixture->Function, Target, *ClassFixture->Block, *ClassValue) || + !prepareForAllocation(*ClassFixture->Function, Target)) { + return; + } + rcc::VirtRegMap ClassMapping(*ClassFixture->Function); + const auto F0 = physical(rcc::test::TestRegister::F0); + if (!ClassMapping.add(*ClassValue) || !ClassMapping.assign(*ClassValue, F0)) + return; + ExpectAtomicFailure(*ClassFixture, ClassMapping, + "class-incompatible rewrite mappings must fail atomically"); + + auto ReservedFixture = createFunction("reserved-register-mapping", 41); + if (!ReservedFixture) + return; + auto ReservedValue = createVReg(*ReservedFixture->Function); + const auto G0 = physical(rcc::test::TestRegister::G0); + if (!ReservedValue || + !buildDef(*ReservedFixture->Function, Target, *ReservedFixture->Block, *ReservedValue, 2) || + !buildUse(*ReservedFixture->Function, Target, *ReservedFixture->Block, *ReservedValue) || + !ReservedFixture->Function->registerInfo().reservePhysicalRegister(G0) || + !prepareForAllocation(*ReservedFixture->Function, Target)) { + return; + } + rcc::VirtRegMap ReservedMapping(*ReservedFixture->Function); + if (!ReservedMapping.add(*ReservedValue) || !ReservedMapping.assign(*ReservedValue, G0)) + return; + ExpectAtomicFailure(*ReservedFixture, ReservedMapping, + "reserved-register rewrite mappings must fail atomically"); + + auto OverlapFixture = createFunction("overlapping-register-mapping", 42); + if (!OverlapFixture) + return; + auto Left = createVReg(*OverlapFixture->Function); + auto Right = createVReg(*OverlapFixture->Function); + if (!Left || !Right || + !buildDef(*OverlapFixture->Function, Target, *OverlapFixture->Block, *Left, 3) || + !buildDef(*OverlapFixture->Function, Target, *OverlapFixture->Block, *Right, 4) || + !buildUse(*OverlapFixture->Function, Target, *OverlapFixture->Block, *Left) || + !buildUse(*OverlapFixture->Function, Target, *OverlapFixture->Block, *Right) || + !prepareForAllocation(*OverlapFixture->Function, Target)) { + return; + } + rcc::VirtRegMap OverlapMapping(*OverlapFixture->Function); + if (!OverlapMapping.add(*Left) || !OverlapMapping.add(*Right) || + !OverlapMapping.assign(*Left, G0) || !OverlapMapping.assign(*Right, G0)) { + return; + } + ExpectAtomicFailure(*OverlapFixture, OverlapMapping, + "interfering rewrite mappings must fail atomically"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + auto SharedUnitTarget = createTarget(true); + Context.expect(Target.has_value(), "register-allocation TestTarget must be constructible"); + Context.expect(SharedUnitTarget.has_value(), + "shared-unit register-allocation TestTarget must be constructible"); + if (!Target) { + std::cerr << "target construction error: " << Target.error().message() << '\n'; + return Context.result(); + } + TestRegisterAllocationInfo Info; + testVirtRegMapAndEdit(Context, *Target); + testLiveRegMatrix(Context, *Target); + if (SharedUnitTarget) + testSharedUnitLaneMatrix(Context, *SharedUnitTarget); + testAllocatorsUnderPressure(Context, *Target, Info); + testFailureAtomicity(Context, *Target); + testFastBlockBoundary(Context, *Target, Info); + testFastRegMaskCalls(Context, *Target, Info); + testTiedAndEarlyClobber(Context, *Target, Info); + testRematerialization(Context, *Target, Info); + testRewriteAndConstraints(Context, *Target, Info); + testInvalidRewriteMappings(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/RegisterAllocationTestSupport.h b/backend/unittests/CodeGen/RegisterAllocationTestSupport.h new file mode 100644 index 0000000..7f0a641 --- /dev/null +++ b/backend/unittests/CodeGen/RegisterAllocationTestSupport.h @@ -0,0 +1,474 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +// The private test header has no stable include-root-relative guard name. +// NOLINTNEXTLINE(llvm-header-guard) +#ifndef RCC_BACKEND_REGISTER_ALLOCATION_TEST_SUPPORT_H +#define RCC_BACKEND_REGISTER_ALLOCATION_TEST_SUPPORT_H + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/LiveIntervals.h" +#include "rcc/CodeGen/LiveRangeEdit.h" +#include "rcc/CodeGen/LiveVariables.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/RegAllocFast.h" +#include "rcc/CodeGen/RegAllocPBQP.h" +#include "rcc/CodeGen/Spiller.h" +#include "rcc/CodeGen/TargetRegisterAllocationInfo.h" +#include "rcc/CodeGen/VirtRegMap.h" + +#include <algorithm> +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#include <ranges> +#include <span> +#include <string> +#include <string_view> +#include <tuple> +#include <utility> +#include <vector> + +namespace rcc::test::ra { + +enum class AllocatorKind : std::uint8_t { Basic, Fast, PBQP }; + +inline constexpr std::uint32_t DefOpcode = 3000; +inline constexpr std::uint32_t RematerializableDefOpcode = 3001; +inline constexpr std::uint32_t UseOpcode = 3002; +inline constexpr std::uint32_t BinaryOpcode = 3003; +inline constexpr std::uint32_t SpillLoadOpcode = 3004; +inline constexpr std::uint32_t SpillStoreOpcode = 3005; +inline constexpr std::uint32_t MaskOpcode = 3006; +inline constexpr std::uint32_t EarlyOpcode = 3007; +inline constexpr std::uint32_t TiedOpcode = 3008; +inline constexpr std::uint32_t CallDefOpcode = 3009; + +[[nodiscard]] constexpr Opcode testOpcode(std::uint32_t Value) noexcept { + return Opcode::fromValue(Value); +} + +[[nodiscard]] inline PhysicalRegister physical(std::uint32_t Number) { + return *PhysicalRegister::create(Number); +} + +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]] inline OperandDescriptor registerOperand(RegisterOperandRole Role, + bool EarlyClobber = false) { + OperandDescriptor Result; + Result.Kinds = {MachineOperand::Kind::Register}; + Result.RegisterRole = Role; + Result.AllowSubReg = true; + Result.EarlyClobber = EarlyClobber; + return Result; +} + +[[nodiscard]] inline OperandDescriptor plainOperand(MachineOperand::Kind Kind) { + OperandDescriptor Result; + Result.Kinds = {Kind}; + return Result; +} + +[[nodiscard]] inline InstructionDescriptor descriptor(std::uint32_t Value, std::string Name) { + InstructionDescriptor Result; + Result.InstructionOpcode = testOpcode(Value); + Result.Name = std::move(Name); + Result.Flags = {InstructionFlag::ProducesCode}; + return Result; +} + +[[nodiscard]] inline Expected<TargetDescription> createTarget() { + std::vector<InstructionDescriptor> Descriptors; + + auto Def = descriptor(DefOpcode, "RA_DEF"); + Def.Operands = {registerOperand(RegisterOperandRole::Def), + plainOperand(MachineOperand::Kind::Immediate)}; + Def.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Def)); + + auto Remat = descriptor(RematerializableDefOpcode, "RA_REMAT_DEF"); + Remat.Operands = {registerOperand(RegisterOperandRole::Def), + plainOperand(MachineOperand::Kind::Immediate)}; + Remat.NumExplicitDefs = 1; + Remat.Flags.set(InstructionFlag::Rematerializable); + Descriptors.push_back(std::move(Remat)); + + auto Use = descriptor(UseOpcode, "RA_USE"); + Use.Operands = {registerOperand(RegisterOperandRole::Use)}; + Use.Flags.set(InstructionFlag::HasSideEffects); + Descriptors.push_back(std::move(Use)); + + auto Binary = descriptor(BinaryOpcode, "RA_BINARY"); + Binary.Operands = {registerOperand(RegisterOperandRole::Def), + registerOperand(RegisterOperandRole::Use), + registerOperand(RegisterOperandRole::Use)}; + Binary.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Binary)); + + auto Reload = descriptor(SpillLoadOpcode, "RA_RELOAD"); + Reload.Operands = {registerOperand(RegisterOperandRole::Def), + plainOperand(MachineOperand::Kind::FrameIndex)}; + Reload.NumExplicitDefs = 1; + Reload.Flags.set(InstructionFlag::MayLoad); + Reload.RequiresMemoryOperands = true; + Descriptors.push_back(std::move(Reload)); + + auto Spill = descriptor(SpillStoreOpcode, "RA_SPILL"); + Spill.Operands = {registerOperand(RegisterOperandRole::Use), + plainOperand(MachineOperand::Kind::FrameIndex)}; + Spill.Flags.set(InstructionFlag::MayStore); + Spill.RequiresMemoryOperands = true; + Descriptors.push_back(std::move(Spill)); + + auto Mask = descriptor(MaskOpcode, "RA_MASK"); + Mask.Operands = {plainOperand(MachineOperand::Kind::RegisterMask)}; + Mask.Flags.set(InstructionFlag::Call); + Mask.Flags.set(InstructionFlag::HasSideEffects); + Descriptors.push_back(std::move(Mask)); + + auto Early = descriptor(EarlyOpcode, "RA_EARLY"); + Early.Operands = {registerOperand(RegisterOperandRole::Def, true), + registerOperand(RegisterOperandRole::Use)}; + Early.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Early)); + + auto Tied = descriptor(TiedOpcode, "RA_TIED"); + auto TiedDef = registerOperand(RegisterOperandRole::Def); + auto TiedUse = registerOperand(RegisterOperandRole::Use); + TiedDef.DefaultTiedOperand = 1; + TiedUse.DefaultTiedOperand = 0; + Tied.Operands = {TiedDef, TiedUse}; + Tied.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Tied)); + + auto CallDef = descriptor(CallDefOpcode, "RA_CALL_DEF"); + CallDef.Operands = {registerOperand(RegisterOperandRole::Def), + plainOperand(MachineOperand::Kind::RegisterMask)}; + CallDef.NumExplicitDefs = 1; + CallDef.Flags.set(InstructionFlag::Call); + CallDef.Flags.set(InstructionFlag::HasSideEffects); + Descriptors.push_back(std::move(CallDef)); + + auto Copy = descriptor(static_cast<std::uint32_t>(GenericOpcode::COPY), "COPY"); + Copy.Operands = {registerOperand(RegisterOperandRole::Def), + registerOperand(RegisterOperandRole::Use)}; + Copy.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Copy)); + + auto Instructions = InstructionSet::create(std::move(Descriptors), {}); + if (!Instructions) + return std::unexpected(Instructions.error()); + return test::createTestTargetDescription(std::move(*Instructions)); +} + +struct FunctionFixture final { + std::unique_ptr<MachineFunction> Function; + MachineBasicBlock *Block = nullptr; +}; + +[[nodiscard]] inline Expected<FunctionFixture> createFunction(std::string Name, std::uint64_t ID) { + auto Function = MachineFunction::create(std::move(Name), MachineFunctionStableID::fromValue(ID)); + if (!Function) + return std::unexpected(Function.error()); + auto Block = (*Function)->createBasicBlock(); + if (!Block) + return std::unexpected(Block.error()); + auto Appended = (*Function)->appendBlock(**Block); + if (!Appended) + return std::unexpected(Appended.error()); + return FunctionFixture{std::move(*Function), *Block}; +} + +[[nodiscard]] inline Expected<Register> +createVReg(MachineFunction &Function, RegisterClassID Class = TestRegisterClass::GPR64) { + auto Type = LowLevelType::makeCategoryNeutralScalar(64); + if (!Type) + return std::unexpected(Type.error()); + auto Reg = Function.registerInfo().createVirtualRegister(*Type); + if (!Reg) + return std::unexpected(Reg.error()); + if (auto Result = Function.registerInfo().setRegisterBank(*Reg, TestRegisterBank::GPR); !Result) + return std::unexpected(Result.error()); + if (auto Result = Function.registerInfo().setRegisterClass(*Reg, Class); !Result) + return std::unexpected(Result.error()); + return *Reg; +} + +[[nodiscard]] inline Expected<MachineInstr *> +buildDef(MachineFunction &Function, const TargetDescription &Target, MachineBasicBlock &Block, + Register Reg, std::int64_t Value, SubRegIndex SubReg = {}, bool Rematerializable = false) { + MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = + Builder.buildInstr(testOpcode(Rematerializable ? RematerializableDefOpcode : DefOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Reg, {}, SubReg); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addImmediate(Value); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] inline Expected<MachineInstr *> +buildUse(MachineFunction &Function, const TargetDescription &Target, MachineBasicBlock &Block, + Register Reg, SubRegIndex SubReg = {}, bool Debug = false) { + MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(testOpcode(UseOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + RegisterOperandFlags Flags; + if (Debug) + Flags.set(RegisterOperandFlag::Debug); + if (auto Result = Pending->addUse(Reg, Flags, SubReg); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] inline Expected<MachineInstr *> buildCopy(MachineFunction &Function, + const TargetDescription &Target, + MachineBasicBlock &Block, + Register Destination, Register Source) { + MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(opcode(GenericOpcode::COPY)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Destination); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addUse(Source); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] inline Expected<MachineInstr *> buildMask(MachineFunction &Function, + const TargetDescription &Target, + MachineBasicBlock &Block, + std::shared_ptr<const RegisterMask> Mask) { + MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(testOpcode(MaskOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addRegisterMask(std::move(Mask)); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] inline Expected<MachineInstr *> buildTied(MachineFunction &Function, + const TargetDescription &Target, + MachineBasicBlock &Block, Register Reg) { + MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(testOpcode(TiedOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Reg); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addUse(Reg); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->tieOperands(0, 1); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] inline Expected<void> prepareForAllocation(MachineFunction &Function, + const TargetDescription &Target) { + auto Rebuilt = rebuildMachineLiveness(Function, Target); + if (!Rebuilt) + return std::unexpected(Rebuilt.error()); + for (const MachineFunctionProperty Property : { + MachineFunctionProperty::TracksLiveness, + MachineFunctionProperty::Legalized, + MachineFunctionProperty::RegBankSelected, + MachineFunctionProperty::Selected, + MachineFunctionProperty::NoPHIs, + MachineFunctionProperty::TiedOpsRewritten, + }) + Function.properties().set(Property); + return {}; +} + +class TestRegisterAllocationInfo : public TargetRegisterAllocationInfo { +public: + std::uint64_t RegisterCost = 0; + std::uint64_t SpillCost = 1; + + [[nodiscard]] Expected<void> validateSpillReload(RegisterClassID Class, + const TargetDescription &Target) const override { + const RegisterClassDesc *Description = Target.registers().registerClass(Class); + if (Description == nullptr || !Description->Allocatable || Description->SpillSizeInBits == 0 || + Description->SpillAlignmentInBits == 0) + return makeUnexpected(ErrorCode::Unsupported, "test register class is not spillable"); + return {}; + } + + [[nodiscard]] Expected<MachineInstr *> emitReload(MachineIRBuilder &Builder, Register Destination, + RegisterClassID Class, FrameIndex StackSlot, + LaneMask Lanes) const override { + auto Validated = validateSpillReload(Class, Builder.target()); + if (!Validated) + return std::unexpected(Validated.error()); + if (Lanes.empty()) + return makeUnexpected(ErrorCode::InvalidArgument, "test reload requires lanes"); + auto Size = MemorySize::fixed(8); + auto Align = Alignment::create(8); + if (!Size || !Align) + return makeUnexpected(ErrorCode::InternalError, "test memory shape is invalid"); + MachineMemOperandDesc Memory; + Memory.Accesses = {MemoryAccessFlag::Load}; + Memory.Size = *Size; + Memory.Align = *Align; + Memory.Base = MemoryBase::frameIndex(StackSlot); + auto MMO = Builder.function().createMemOperand(std::move(Memory)); + if (!MMO) + return std::unexpected(MMO.error()); + auto Pending = Builder.buildInstr(testOpcode(SpillLoadOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Destination); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addFrameIndex(StackSlot); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addMemOperand(**MMO); !Result) + return std::unexpected(Result.error()); + return Pending->build(); + } + + [[nodiscard]] Expected<MachineInstr *> emitSpill(MachineIRBuilder &Builder, Register Source, + RegisterClassID Class, FrameIndex StackSlot, + LaneMask Lanes) const override { + auto Validated = validateSpillReload(Class, Builder.target()); + if (!Validated) + return std::unexpected(Validated.error()); + if (Lanes.empty()) + return makeUnexpected(ErrorCode::InvalidArgument, "test spill requires lanes"); + auto Size = MemorySize::fixed(8); + auto Align = Alignment::create(8); + if (!Size || !Align) + return makeUnexpected(ErrorCode::InternalError, "test memory shape is invalid"); + MachineMemOperandDesc Memory; + Memory.Accesses = {MemoryAccessFlag::Store}; + Memory.Size = *Size; + Memory.Align = *Align; + Memory.Base = MemoryBase::frameIndex(StackSlot); + auto MMO = Builder.function().createMemOperand(std::move(Memory)); + if (!MMO) + return std::unexpected(MMO.error()); + auto Pending = Builder.buildInstr(testOpcode(SpillStoreOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addUse(Source); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addFrameIndex(StackSlot); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addMemOperand(**MMO); !Result) + return std::unexpected(Result.error()); + return Pending->build(); + } + + [[nodiscard]] Expected<bool> isRematerializable(const MachineInstr &Instruction, + Register VirtualReg, const MachineFunction &, + const TargetDescription &) const override { + const Register *Defined = Instruction.operandAt(0)->getIf<Register>(); + return Instruction.opcode() == testOpcode(RematerializableDefOpcode) && Defined != nullptr && + *Defined == VirtualReg; + } + + [[nodiscard]] Expected<MachineInstr *> + rematerialize(MachineIRBuilder &Builder, const MachineInstr &Original, Register Destination, + Register OriginalRegister, const TargetDescription &Target) const override { + auto Accepted = isRematerializable(Original, OriginalRegister, Builder.function(), Target); + if (!Accepted) + return std::unexpected(Accepted.error()); + if (!*Accepted) + return makeUnexpected(ErrorCode::InvalidArgument, "test instruction is not rematerializable"); + auto Pending = Builder.buildInstr(testOpcode(RematerializableDefOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Destination); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addOperand(Original.operandAt(1)->cloneDetached()); !Result) + return std::unexpected(Result.error()); + return Pending->build(); + } + + [[nodiscard]] std::uint64_t physicalRegisterCost(const MachineFunction &, Register, + PhysicalRegister, RegisterClassID, + bool) const noexcept override { + return RegisterCost; + } + + [[nodiscard]] std::uint64_t spillCost(const MachineFunction &, const LiveInterval &, + RegisterClassID) const noexcept override { + return SpillCost; + } +}; + +[[nodiscard]] inline Expected<void> runAllocator(MachineFunction &Function, + const TargetDescription &Target, + const TargetRegisterAllocationInfo &Info, + AllocatorKind Kind, + RegisterAllocationOptions Options = {}) { + MachineFunctionAnalysisManager Analyses; + auto Registered = registerStage11MachineAnalyses(Analyses); + if (!Registered) + return std::unexpected(Registered.error()); + std::unique_ptr<MachineFunctionPass> Pass; + switch (Kind) { + case AllocatorKind::Basic: + Pass = createBasicAllocatorPass(Info, Options); + break; + case AllocatorKind::Fast: + Pass = std::make_unique<FastAllocatorPass>(Info); + break; + case AllocatorKind::PBQP: + Pass = std::make_unique<PBQPAllocatorPass>(Info, Options); + break; + } + MachineFunctionPassManager Pipeline; + auto Added = Pipeline.addPass(std::move(Pass)); + if (!Added) + return std::unexpected(Added.error()); + return Pipeline.run(Function, Target, Analyses); +} + +[[nodiscard]] inline std::size_t countOpcode(const MachineFunction &Function, Opcode OpcodeValue) { + std::size_t Count = 0; + for (const MachineBasicBlock *Block = Function.firstBlock(); Block != nullptr; + Block = Block->next()) + for (const MachineInstr *Instruction = Block->firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) + Count += Instruction->opcode() == OpcodeValue ? 1U : 0U; + return Count; +} + +} // namespace rcc::test::ra + +#endif // RCC_BACKEND_REGISTER_ALLOCATION_TEST_SUPPORT_H diff --git a/backend/unittests/CodeGen/SSAExitTestSupport.h b/backend/unittests/CodeGen/SSAExitTestSupport.h new file mode 100644 index 0000000..cf77d36 --- /dev/null +++ b/backend/unittests/CodeGen/SSAExitTestSupport.h @@ -0,0 +1,444 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +// This private test header has no stable include-root-relative guard name. +// NOLINTNEXTLINE(llvm-header-guard) +#ifndef RCC_BACKEND_SSA_EXIT_TEST_SUPPORT_H +#define RCC_BACKEND_SSA_EXIT_TEST_SUPPORT_H + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/LiveVariables.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/TargetDescription.h" +#include "rcc/CodeGen/TargetInstrInfo.h" + +#include <algorithm> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#include <ranges> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace rcc::test::ssa { + +constexpr std::uint32_t DefOpcode = 1024; +constexpr std::uint32_t UseOpcode = 1025; +constexpr std::uint32_t ConditionalBranchOpcode = 1026; +constexpr std::uint32_t JumpOpcode = 1027; +constexpr std::uint32_t ReturnOpcode = 1028; +constexpr std::uint32_t MetaOpcode = 1029; +constexpr std::uint32_t TiedOpcode = 1030; +constexpr std::uint32_t CommutableTiedOpcode = 1031; +constexpr std::uint32_t ConvertibleTiedOpcode = 1032; +constexpr std::uint32_t ThreeAddressOpcode = 1033; + +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]] inline rcc::Opcode testOpcode(std::uint32_t Value) { + return rcc::Opcode::fromValue(Value); +} + +[[nodiscard]] inline rcc::OperandDescriptor registerOperand(rcc::RegisterOperandRole Role, + bool AllowSubReg = true) { + rcc::OperandDescriptor Result; + Result.Kinds = {rcc::MachineOperand::Kind::Register}; + Result.RegisterRole = Role; + Result.AllowSubReg = AllowSubReg; + Result.AllowAnyTargetFlags = true; + return Result; +} + +[[nodiscard]] inline rcc::OperandDescriptor blockOperand() { + rcc::OperandDescriptor Result; + Result.Kinds = {rcc::MachineOperand::Kind::BasicBlock}; + return Result; +} + +[[nodiscard]] inline rcc::InstructionDescriptor instruction(std::uint32_t Value, std::string Name) { + rcc::InstructionDescriptor Result; + Result.InstructionOpcode = testOpcode(Value); + Result.Name = std::move(Name); + Result.Flags = {rcc::InstructionFlag::ProducesCode}; + return Result; +} + +[[nodiscard]] inline rcc::Expected<rcc::TargetDescription> createTarget() { + std::vector<rcc::InstructionDescriptor> Descriptors; + std::vector<rcc::ImmediatePredicate> Predicates; + auto Generic = rcc::appendGenericInstructionInfo(Descriptors, Predicates); + if (!Generic) + return std::unexpected(Generic.error()); + + auto Def = instruction(DefOpcode, "TEST_DEF"); + Def.Operands = {registerOperand(rcc::RegisterOperandRole::Def)}; + Def.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Def)); + + auto Use = instruction(UseOpcode, "TEST_USE"); + Use.Operands = {registerOperand(rcc::RegisterOperandRole::Use)}; + Descriptors.push_back(std::move(Use)); + + auto Conditional = instruction(ConditionalBranchOpcode, "TEST_BRCOND"); + Conditional.Operands = {registerOperand(rcc::RegisterOperandRole::Use, false), blockOperand()}; + Conditional.Flags.set(rcc::InstructionFlag::Branch); + Conditional.Flags.set(rcc::InstructionFlag::Terminator); + Descriptors.push_back(std::move(Conditional)); + + auto Jump = instruction(JumpOpcode, "TEST_JUMP"); + Jump.Operands = {blockOperand()}; + Jump.Flags.set(rcc::InstructionFlag::Branch); + Jump.Flags.set(rcc::InstructionFlag::Terminator); + Jump.Flags.set(rcc::InstructionFlag::Barrier); + Descriptors.push_back(std::move(Jump)); + + auto Return = instruction(ReturnOpcode, "TEST_RETURN"); + Return.Flags.set(rcc::InstructionFlag::Return); + Return.Flags.set(rcc::InstructionFlag::Terminator); + Return.Flags.set(rcc::InstructionFlag::Barrier); + Descriptors.push_back(std::move(Return)); + + auto Meta = instruction(MetaOpcode, "TEST_META"); + Meta.Flags = {rcc::InstructionFlag::Pseudo, rcc::InstructionFlag::Meta}; + Descriptors.push_back(std::move(Meta)); + + auto Tied = instruction(TiedOpcode, "TEST_TIED"); + auto TiedDef = registerOperand(rcc::RegisterOperandRole::Def); + auto TiedUse = registerOperand(rcc::RegisterOperandRole::Use); + TiedDef.DefaultTiedOperand = 1; + TiedUse.DefaultTiedOperand = 0; + Tied.Operands = {TiedDef, TiedUse}; + Tied.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Tied)); + + auto Commutable = instruction(CommutableTiedOpcode, "TEST_COMMUTABLE_TIED"); + Commutable.Operands = {TiedDef, TiedUse, registerOperand(rcc::RegisterOperandRole::Use)}; + Commutable.NumExplicitDefs = 1; + Commutable.Flags.set(rcc::InstructionFlag::Commutable); + Descriptors.push_back(std::move(Commutable)); + + auto Convertible = instruction(ConvertibleTiedOpcode, "TEST_CONVERTIBLE_TIED"); + Convertible.Operands = {TiedDef, TiedUse}; + Convertible.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Convertible)); + + auto ThreeAddress = instruction(ThreeAddressOpcode, "TEST_THREE_ADDRESS"); + ThreeAddress.Operands = {registerOperand(rcc::RegisterOperandRole::Def), + registerOperand(rcc::RegisterOperandRole::Use)}; + ThreeAddress.NumExplicitDefs = 1; + Descriptors.push_back(std::move(ThreeAddress)); + + 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 TestInstrInfo final : public rcc::TargetInstrInfo { +public: + explicit TestInstrInfo(bool RejectCopies = false) : RejectCopies(RejectCopies) {} + + [[nodiscard]] rcc::Expected<rcc::MachineBranchAnalysis> + analyzeBranch(rcc::MachineBasicBlock &, rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return rcc::MachineBranchAnalysis{}; + } + + [[nodiscard]] rcc::Expected<void> + reverseBranchCondition(rcc::MachineInstr &, rcc::MachineBasicBlock &, rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, "test target has no branch reversal"); + } + + [[nodiscard]] rcc::Expected<void> removeBranch(rcc::MachineInstr &, rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, "test target has no branch removal"); + } + + [[nodiscard]] rcc::Expected<bool> + isFallthroughEdge(const rcc::MachineBasicBlock &From, const rcc::MachineBasicBlock &To, + const rcc::MachineFunction &Function, + const rcc::TargetDescription &Target) const override { + if (&From.function() != &Function || &To.function() != &Function) { + return rcc::makeUnexpected(rcc::ErrorCode::InvalidArgument, + "fallthrough query references another function"); + } + if (From.next() != &To || + std::ranges::none_of(From.successors(), [&To](const rcc::MachineSuccessor &Successor) { + return Successor.Block == &To; + })) { + return false; + } + + bool CanFallThrough = true; + for (const rcc::MachineInstr *Instruction = From.firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + const rcc::InstructionDescriptor *Descriptor = + Target.instructionDescriptor(Instruction->opcode()); + if (Descriptor == nullptr) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "fallthrough query encountered an unknown opcode"); + } + if (Descriptor->Flags.has(rcc::InstructionFlag::Terminator)) + CanFallThrough = Instruction->opcode() == testOpcode(ConditionalBranchOpcode); + } + return CanFallThrough; + } + + [[nodiscard]] rcc::Expected<bool> + rewriteBranchTarget(rcc::MachineBasicBlock &Block, rcc::MachineBasicBlock &OldTarget, + rcc::MachineBasicBlock &NewTarget, rcc::MachineFunction &Function, + const rcc::TargetDescription &Target) const override { + if (&Block.function() != &Function || &OldTarget.function() != &Function || + &NewTarget.function() != &Function) { + return rcc::makeUnexpected(rcc::ErrorCode::InvalidArgument, + "branch rewrite references another function"); + } + bool Changed = false; + for (rcc::MachineInstr *Instruction = Block.firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + const rcc::InstructionDescriptor *Descriptor = + Target.instructionDescriptor(Instruction->opcode()); + if (Descriptor == nullptr) + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, "unknown branch opcode"); + if (!Descriptor->Flags.has(rcc::InstructionFlag::Branch)) + continue; + for (std::size_t Index = 0; Index < Instruction->operandCount(); ++Index) { + rcc::MachineOperand *Operand = Instruction->operandAt(Index); + const auto *BlockOperand = + Operand == nullptr ? nullptr : Operand->getIf<rcc::BasicBlockOperand>(); + if (BlockOperand == nullptr || BlockOperand->Block != &OldTarget) + continue; + auto Rewritten = Operand->setBasicBlock(NewTarget); + if (!Rewritten) + return std::unexpected(Rewritten.error()); + Changed = true; + } + } + return Changed; + } + + [[nodiscard]] rcc::Expected<rcc::MachineInstr *> + insertBranch(rcc::MachineBasicBlock &Block, rcc::MachineBasicBlock &TargetBlock, + rcc::MachineFunction &Function, + const rcc::TargetDescription &Target) const override { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = Builder.setInsertPoint(Block); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(testOpcode(JumpOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + auto Destination = Pending->addBasicBlock(TargetBlock); + if (!Destination) + return std::unexpected(Destination.error()); + auto Built = Pending->build(); + if (!Built) + return std::unexpected(Built.error()); + return *Built; + } + + [[nodiscard]] rcc::Expected<void> + validateCopy(rcc::Register Destination, rcc::SubRegIndex DestinationSubReg, rcc::Register Source, + rcc::SubRegIndex SourceSubReg, const rcc::MachineFunction &Function, + const rcc::TargetDescription &Target) const override { + if (RejectCopies) { + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, + "test target intentionally rejects COPY"); + } + if (Destination.isNoRegister() || Source.isNoRegister()) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "COPY requires nonzero registers"); + } + if ((Destination.isPhysical() && DestinationSubReg.value() != 0) || + (Source.isPhysical() && SourceSubReg.value() != 0)) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "physical COPY operands cannot carry subregisters"); + } + auto Def = rcc::MachineOperand::registerOperand(Destination, {rcc::RegisterOperandFlag::Define}, + DestinationSubReg); + auto Use = + rcc::MachineOperand::registerOperand(Source, {rcc::RegisterOperandFlag::Use}, SourceSubReg); + if (!Def || !Use) + return rcc::makeUnexpected(rcc::ErrorCode::InternalError, "failed to model COPY operands"); + return Target.validateTieCompatibility(*Def, *Use, Function.registerInfo()); + } + + [[nodiscard]] rcc::Expected<bool> + tryConvertToThreeAddress(rcc::MachineInstr &Instruction, std::size_t DefIndex, + std::size_t UseIndex, rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + if (Instruction.opcode() != testOpcode(ConvertibleTiedOpcode)) + return false; + if (DefIndex != 0 || UseIndex != 1) + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "unexpected convertible tie indices"); + auto Untied = Instruction.untieOperand(DefIndex); + if (!Untied) + return std::unexpected(Untied.error()); + auto Opcode = Instruction.setOpcode(testOpcode(ThreeAddressOpcode)); + if (!Opcode) + return std::unexpected(Opcode.error()); + return true; + } + + [[nodiscard]] rcc::Expected<std::uint32_t> + instructionSize(const rcc::MachineInstr &, const rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return 4; + } + + [[nodiscard]] rcc::Expected<rcc::MachineBasicBlock *> + branchTarget(rcc::MachineInstr &, rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return static_cast<rcc::MachineBasicBlock *>(nullptr); + } + + [[nodiscard]] bool isBranchOffsetInRange(const rcc::MachineInstr &, + std::int64_t) const noexcept override { + return true; + } + + [[nodiscard]] rcc::Expected<bool> relaxBranch(rcc::MachineInstr &, std::int64_t, + rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return false; + } + +private: + bool RejectCopies = false; +}; + +struct FunctionFixture final { + std::unique_ptr<rcc::MachineFunction> Function; + std::vector<rcc::MachineBasicBlock *> Blocks; +}; + +[[nodiscard]] inline FunctionFixture createFunction(TestContext &Context, std::string Name, + std::uint64_t ID, std::size_t BlockCount) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "SSA-exit function creation must succeed"); + if (!Function) + return {}; + + FunctionFixture Result{std::move(*Function), {}}; + for (std::size_t Index = 0; Index < BlockCount; ++Index) { + auto Block = Result.Function->createBasicBlock(); + Context.expect(Block && Result.Function->appendBlock(**Block).has_value(), + "SSA-exit block creation must succeed"); + if (!Block) + return {}; + Result.Blocks.push_back(*Block); + } + return Result; +} + +[[nodiscard]] inline rcc::Expected<rcc::Register> +createVReg(rcc::MachineFunction &Function, rcc::LowLevelType Type, rcc::RegisterBankID Bank, + rcc::RegisterClassID Class, std::uint8_t TargetFlags = 0, + std::optional<rcc::PhysicalRegister> Hint = std::nullopt) { + auto Reg = Function.registerInfo().createVirtualRegister(Type); + if (!Reg) + return std::unexpected(Reg.error()); + auto BankResult = Function.registerInfo().setRegisterBank(*Reg, Bank); + if (!BankResult) + return std::unexpected(BankResult.error()); + auto ClassResult = Function.registerInfo().setRegisterClass(*Reg, Class); + if (!ClassResult) + return std::unexpected(ClassResult.error()); + if (TargetFlags != 0) { + auto FlagsResult = Function.registerInfo().setTargetRegisterFlags(*Reg, TargetFlags); + if (!FlagsResult) + return std::unexpected(FlagsResult.error()); + } + if (Hint) { + auto HintResult = Function.registerInfo().addAllocationHint(*Reg, *Hint); + if (!HintResult) + return std::unexpected(HintResult.error()); + } + return *Reg; +} + +[[nodiscard]] inline rcc::Expected<rcc::MachineInstr *> +buildRegisterInstruction(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Opcode Opcode, + std::span<const rcc::Register> Defs, std::span<const rcc::Register> Uses, + rcc::RegisterOperandFlags DefFlags = {}, + rcc::RegisterOperandFlags UseFlags = {}) { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = Builder.setInsertPoint(Block); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(Opcode); + if (!Pending) + return std::unexpected(Pending.error()); + for (rcc::Register Reg : Defs) { + auto Added = Pending->addDef(Reg, DefFlags); + if (!Added) + return std::unexpected(Added.error()); + } + for (rcc::Register Reg : Uses) { + auto Added = Pending->addUse(Reg, UseFlags); + if (!Added) + return std::unexpected(Added.error()); + } + return Pending->build(); +} + +[[nodiscard]] inline rcc::Expected<void> +setSelectedSSAProperties(rcc::MachineFunction &Function, const rcc::TargetDescription &Target) { + auto Rebuilt = rcc::rebuildMachineLiveness(Function, Target); + if (!Rebuilt) + return std::unexpected(Rebuilt.error()); + for (rcc::MachineFunctionProperty Property : + {rcc::MachineFunctionProperty::IsSSA, rcc::MachineFunctionProperty::TracksLiveness, + rcc::MachineFunctionProperty::Legalized, rcc::MachineFunctionProperty::RegBankSelected, + rcc::MachineFunctionProperty::Selected}) { + Function.properties().set(Property); + } + return {}; +} + +[[nodiscard]] inline std::vector<rcc::MachineInstr *> +instructionsWithOpcode(rcc::MachineFunction &Function, rcc::Opcode Opcode) { + std::vector<rcc::MachineInstr *> Result; + for (rcc::MachineBasicBlock *Block = Function.firstBlock(); Block != nullptr; + Block = Block->next()) { + for (rcc::MachineInstr *Instruction = Block->firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + if (Instruction->opcode() == Opcode) + Result.push_back(Instruction); + } + } + return Result; +} + +} // namespace rcc::test::ssa + +#endif // RCC_BACKEND_SSA_EXIT_TEST_SUPPORT_H diff --git a/backend/unittests/CodeGen/SlotIndexesTest.cpp b/backend/unittests/CodeGen/SlotIndexesTest.cpp new file mode 100644 index 0000000..9d3f54b --- /dev/null +++ b/backend/unittests/CodeGen/SlotIndexesTest.cpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/SlotIndexes.h" + +#include "LiveTestSupport.h" +#include "rcc/CodeGen/LiveVariables.h" + +namespace { + +using namespace rcc::test::liveness; + +void testSlotIndexes(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction("slot-indexes", 1, 1); + if (!Fixture) { + Context.expect(false, "SlotIndexes fixture creation must succeed"); + return; + } + auto Reg = createVReg(*Fixture->Function); + auto Def = Reg ? buildDef(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg) + : rcc::Expected<rcc::MachineInstr *>{std::unexpected(Reg.error())}; + auto Use = Reg ? buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg) + : rcc::Expected<rcc::MachineInstr *>{std::unexpected(Reg.error())}; + if (!Reg || !Def || !Use) { + Context.expect(false, "SlotIndexes instructions must be built"); + return; + } + + rcc::MachineFunctionAnalysisManager Analyses; + auto Registered = rcc::registerStage11MachineAnalyses(Analyses); + auto Indexes = + Registered + ? Analyses.getResult<rcc::SlotIndexesAnalysis>(*Fixture->Function, Target) + : rcc::Expected<rcc::SlotIndexesAnalysis::Result *>{std::unexpected(Registered.error())}; + if (!Indexes) { + Context.expect(false, "SlotIndexes analysis must materialize"); + return; + } + const rcc::SlotIndex DefIndex = (*Indexes)->instructionIndex(**Def); + const rcc::SlotIndex UseIndex = (*Indexes)->instructionIndex(**Use); + Context.expect((*Indexes)->blockStart(*Fixture->Blocks[0]) < DefIndex.earlyClobberSlot() && + DefIndex.earlyClobberSlot() < DefIndex && DefIndex < DefIndex.deadSlot() && + DefIndex.deadSlot() < UseIndex && + UseIndex < (*Indexes)->blockEnd(*Fixture->Blocks[0]), + "SlotIndexes must order block, early-clobber, register, and dead slots"); + Context.expect((*Indexes)->instructionFromIndex(DefIndex.deadSlot()) == *Def && + (*Indexes)->blockFromIndex(UseIndex) == Fixture->Blocks[0], + "SlotIndexes reverse mappings must accept every instruction subslot"); + + auto Inserted = buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg, {}, *Use); + Context.expect(Inserted && (*Indexes)->insertInstruction(**Inserted).has_value(), + "local SlotIndex insertion must use reserved numbering space"); + if (Inserted) { + const rcc::SlotIndex InsertedIndex = (*Indexes)->instructionIndex(**Inserted); + Context.expect(DefIndex < InsertedIndex && InsertedIndex < UseIndex, + "locally inserted SlotIndex must lie between adjacent instructions"); + Context.expect((*Indexes)->eraseInstruction(**Inserted).has_value() && + !(*Indexes)->instructionIndex(**Inserted).isValid() && + (*Indexes)->renumber(*Fixture->Function).has_value() && + (*Indexes)->instructionIndex(**Inserted).isValid(), + "SlotIndex erasure and full renumbering must rebuild exact mappings"); + + auto Second = buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg, {}, *Inserted); + auto SecondIndexed = Second ? (*Indexes)->insertInstruction(**Second) + : rcc::Expected<void>{std::unexpected(Second.error())}; + auto Third = Second + ? buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg, {}, *Second) + : rcc::Expected<rcc::MachineInstr *>{std::unexpected(Second.error())}; + auto ThirdIndexed = Third ? (*Indexes)->insertInstruction(**Third) + : rcc::Expected<void>{std::unexpected(Third.error())}; + auto Exhausted = + Third ? buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg, {}, *Third) + : rcc::Expected<rcc::MachineInstr *>{std::unexpected(Third.error())}; + auto ExhaustedIndex = Exhausted ? (*Indexes)->insertInstruction(**Exhausted) + : rcc::Expected<void>{std::unexpected(Exhausted.error())}; + Context.expect(SecondIndexed && ThirdIndexed && Exhausted && !ExhaustedIndex && + ExhaustedIndex.error().code() == rcc::ErrorCode::Unsupported && + (*Indexes)->renumber(*Fixture->Function).has_value() && + (*Indexes)->instructionIndex(**Exhausted).isValid(), + "local SlotIndex exhaustion must request and recover through full renumbering"); + } +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "liveness TestTarget must be created"); + if (!Target) + return Context.result(); + testSlotIndexes(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/SpillerTest.cpp b/backend/unittests/CodeGen/SpillerTest.cpp new file mode 100644 index 0000000..1f22d8f --- /dev/null +++ b/backend/unittests/CodeGen/SpillerTest.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/Spiller.h" + +#include "CodeGen/RegisterAllocationTestSupport.h" + +#include <iostream> +#include <optional> + +namespace { + +using namespace rcc; +using namespace rcc::test; +using namespace rcc::test::ra; + +void testRematerialization(TestContext &Context, const TargetDescription &Target) { + auto Fixture = createFunction("spiller-remat", 20); + if (!Fixture) + return; + auto Value = createVReg(*Fixture->Function); + if (!Value || !buildDef(*Fixture->Function, Target, *Fixture->Block, *Value, 42, {}, true) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value)) + return; + VirtRegMap Mapping(*Fixture->Function); + TestRegisterAllocationInfo Info; + if (!Mapping.add(*Value)) + return; + Spiller ValueSpiller(*Fixture->Function, Target, Info, Mapping, true); + auto Children = ValueSpiller.spill(*Value); + Context.expect(Children && Children->size() == 2 && + Fixture->Function->frameInfo().liveObjectCount() == 0 && + countOpcode(*Fixture->Function, testOpcode(RematerializableDefOpcode)) == 2 && + countOpcode(*Fixture->Function, testOpcode(SpillLoadOpcode)) == 0, + "Spiller must rematerialize each use without allocating a stack slot"); +} + +void testDirectSpillAndDebugUse(TestContext &Context, const TargetDescription &Target) { + auto Fixture = createFunction("spiller-debug", 21); + if (!Fixture) + return; + auto Value = createVReg(*Fixture->Function); + if (!Value || !buildDef(*Fixture->Function, Target, *Fixture->Block, *Value, 7) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value, {}, true) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value)) + return; + VirtRegMap Mapping(*Fixture->Function); + TestRegisterAllocationInfo Info; + if (!Mapping.add(*Value)) + return; + Spiller ValueSpiller(*Fixture->Function, Target, Info, Mapping, false); + auto Children = ValueSpiller.spill(*Value); + const MachineOperand *DebugOperand = nullptr; + for (const MachineInstr *Instruction = Fixture->Block->firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + for (const MachineOperand &Operand : Instruction->operands()) { + if (Operand.isDebugUse()) { + DebugOperand = &Operand; + break; + } + } + if (DebugOperand != nullptr) + break; + } + const std::optional<Register> DebugRegister = + DebugOperand == nullptr ? std::nullopt : DebugOperand->registerValue(); + const bool DebugRegisterCleared = DebugRegister && DebugRegister.value().isNoRegister(); + Context.expect(Children && !Children->empty() && + countOpcode(*Fixture->Function, testOpcode(SpillLoadOpcode)) != 0 && + countOpcode(*Fixture->Function, testOpcode(SpillStoreOpcode)) != 0 && + DebugOperand != nullptr && DebugRegisterCleared && + DebugOperand->registerFlags().has(RegisterOperandFlag::Undef), + "Spiller must preserve direct spill code and clear debug-only uses"); + + auto Unspillable = createVReg(*Fixture->Function); + if (!Unspillable || !Mapping.add(*Unspillable, false)) + return; + auto Rejected = ValueSpiller.spill(*Unspillable); + Context.expect(!Rejected && Rejected.error().code() == ErrorCode::RegisterAllocationFailed, + "Spiller must reject an explicitly non-spillable value"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "Spiller test target must be constructible"); + if (!Target) + return Context.result(); + testRematerialization(Context, *Target); + testDirectSpillAndDebugUse(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..22a2b0c --- /dev/null +++ b/backend/unittests/CodeGen/TargetCallLoweringTest.cpp @@ -0,0 +1,328 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/TargetCallLowering.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" + +#include <concepts> +#include <cstdint> +#include <iostream> +#include <memory> +#include <span> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::Alignment align(std::uint64_t Bytes) { + const auto Value = rcc::Alignment::create(Bytes); + if (!Value) + std::terminate(); + return *Value; +} + +[[nodiscard]] rcc::LowLevelType integer(std::uint32_t Bits) { + const auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(Bits); + if (!Type) + std::terminate(); + return *Type; +} + +[[nodiscard]] rcc::PhysicalRegister physical(std::uint32_t ID) { + const auto Register = rcc::PhysicalRegister::create(ID); + if (!Register) + std::terminate(); + return *Register; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + rcc::InstructionDescriptor Nop; + Nop.InstructionOpcode = rcc::Opcode::fromValue(1); + Nop.Name = "TEST_NOP"; + Nop.Flags = {rcc::InstructionFlag::ProducesCode}; + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Nop)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] rcc::CallLoweringPlanDesc emptyPlanDescription(rcc::CallLoweringPlanKind Kind) { + rcc::CallLoweringPlanDesc Description; + Description.Kind = Kind; + Description.Target = "test-target:test-abi"; + Description.Convention = rcc::CallingConventionID::fromValue(1); + Description.StackArguments.Align = align(8); + Description.CallFrame.Align = align(8); + if (Kind == rcc::CallLoweringPlanKind::Call) { + Description.CallFrame.EmitSetup = true; + Description.CallFrame.EmitDestroy = true; + Description.PreservedMask = rcc::test::createTestCallPreservedMask(); + Description.Callee = *rcc::MachineCallTarget::externalSymbol("callee"); + } + return Description; +} + +[[nodiscard]] rcc::Expected<rcc::CallLoweringPlan> emptyPlan(rcc::CallLoweringPlanKind Kind) { + return rcc::CallLoweringPlan::create(emptyPlanDescription(Kind)); +} + +[[nodiscard]] rcc::CCAssignment registerAssignment() { + return { + .ValueIndex = 0, + .OriginalValue = rcc::Register::fromPhysical(physical(2)), + .OriginalValueIndex = 0, + .FragmentIndex = 0, + .FragmentCount = 1, + .ValueType = integer(64), + .LocationType = integer(64), + .LocationKind = rcc::CCLocationKind::Register, + .LocationRegister = physical(1), + .StackOffsetBytes = std::nullopt, + .LocationSizeBytes = 8, + .StackSlotSizeBytes = 8, + .LocationAlignment = align(8), + .Extension = rcc::ABIExtensionKind::None, + .CustomFlags = {}, + }; +} + +void testPlanValidation(TestContext &Context) { + static_assert( + std::same_as<decltype(std::declval<const rcc::CallLoweringPlan &>().argumentAssignments()), + std::span<const rcc::CCAssignment>>); + static_assert(std::same_as<decltype(std::declval<const rcc::CallLoweringPlan &>().byValCopies()), + std::span<const rcc::ByValCopyAssignment>>); + + auto Valid = emptyPlan(rcc::CallLoweringPlanKind::FormalArguments); + Context.expect(Valid && Valid->target() == "test-target:test-abi" && + Valid->argumentAssignments().empty(), + "a valid immutable formal-argument plan must be accepted"); + + auto MissingRegister = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments); + auto Assignment = registerAssignment(); + Assignment.OriginalValue = {}; + MissingRegister.ArgumentAssignments.push_back(Assignment); + Context.expect(!rcc::CallLoweringPlan::create(std::move(MissingRegister)), + "an assignment without a machine value must be rejected"); + + auto BadStack = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments); + BadStack.StackArguments.SizeBytes = 8; + auto StackAssignment = registerAssignment(); + StackAssignment.LocationKind = rcc::CCLocationKind::Stack; + StackAssignment.LocationRegister.reset(); + StackAssignment.StackOffsetBytes = 8; + BadStack.ArgumentAssignments.push_back(StackAssignment); + Context.expect(!rcc::CallLoweringPlan::create(std::move(BadStack)), + "an assignment outside the declared stack area must be rejected"); + + auto Overlap = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments); + Overlap.StackArguments.SizeBytes = 16; + StackAssignment.StackOffsetBytes = 0; + Overlap.ArgumentAssignments.push_back(StackAssignment); + StackAssignment.OriginalValue = rcc::Register::fromPhysical(physical(3)); + StackAssignment.StackOffsetBytes = 4; + Overlap.ArgumentAssignments.push_back(StackAssignment); + Context.expect(!rcc::CallLoweringPlan::create(std::move(Overlap)), + "overlapping stack assignments must be rejected"); + + auto MisalignedStack = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments); + MisalignedStack.StackArguments.SizeBytes = 16; + StackAssignment.StackOffsetBytes = 4; + MisalignedStack.ArgumentAssignments.push_back(StackAssignment); + Context.expect(!rcc::CallLoweringPlan::create(std::move(MisalignedStack)), + "a stack assignment must satisfy its declared alignment"); + + auto OversizedLocation = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments); + auto OversizedAssignment = registerAssignment(); + OversizedAssignment.LocationSizeBytes = 16; + OversizedLocation.ArgumentAssignments.push_back(OversizedAssignment); + Context.expect(!rcc::CallLoweringPlan::create(std::move(OversizedLocation)), + "an ABI location cannot exceed its transport slot"); + + auto UnknownLocation = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments); + auto UnknownAssignment = registerAssignment(); + UnknownAssignment.LocationKind = + static_cast<rcc::CCLocationKind>(255); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange) + UnknownLocation.ArgumentAssignments.push_back(UnknownAssignment); + Context.expect(!rcc::CallLoweringPlan::create(std::move(UnknownLocation)), + "an unknown ABI location kind must be rejected"); + + auto BadFrame = emptyPlanDescription(rcc::CallLoweringPlanKind::Call); + BadFrame.CallFrame.EmitDestroy = false; + Context.expect(!rcc::CallLoweringPlan::create(std::move(BadFrame)), + "unpaired call-frame setup and destroy requirements must be rejected"); + + auto SmallFrame = emptyPlanDescription(rcc::CallLoweringPlanKind::Call); + SmallFrame.StackArguments.SizeBytes = 16; + SmallFrame.CallFrame.SizeBytes = 8; + StackAssignment.StackOffsetBytes = 0; + SmallFrame.ArgumentAssignments.push_back(StackAssignment); + Context.expect(!rcc::CallLoweringPlan::create(std::move(SmallFrame)), + "a call frame must contain its complete stack-argument area"); + + auto BadTail = emptyPlanDescription(rcc::CallLoweringPlanKind::Call); + BadTail.CallFrame.EmitSetup = false; + BadTail.CallFrame.EmitDestroy = false; + BadTail.CallFrame.ReusesIncomingArea = true; + BadTail.TailDecision = rcc::TailCallKind::Tail; + BadTail.TailRejectionReason = "inconsistent accepted tail call"; + Context.expect(!rcc::CallLoweringPlan::create(std::move(BadTail)), + "an accepted tail call cannot retain a rejection reason"); + + auto UnknownTail = emptyPlanDescription(rcc::CallLoweringPlanKind::Call); + UnknownTail.TailDecision = + static_cast<rcc::TailCallKind>(255); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange) + Context.expect(!rcc::CallLoweringPlan::create(std::move(UnknownTail)), + "an unknown tail-call decision must be rejected"); + + auto BadVarArgs = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments); + BadVarArgs.VarArgs.IsVariadic = true; + BadVarArgs.VarArgs.RegisterSaveAreaSizeBytes = 8; + BadVarArgs.VarArgs.RegistersToSave.push_back({physical(1), 8}); + Context.expect(!rcc::CallLoweringPlan::create(std::move(BadVarArgs)), + "a variadic register save outside its area must be rejected"); + + auto BadByVal = emptyPlanDescription(rcc::CallLoweringPlanKind::Call); + auto ByValAssignment = registerAssignment(); + ByValAssignment.ValueType = *rcc::LowLevelType::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::LowLevelType::makePointer(0, 64); + FirstByVal.LocationType = FirstByVal.ValueType; + FirstByVal.CustomFlags.set(rcc::CCAssignmentFlag::ByVal); + auto SecondByVal = FirstByVal; + SecondByVal.ValueIndex = 1; + SecondByVal.OriginalValue = rcc::Register::fromPhysical(physical(3)); + SecondByVal.OriginalValueIndex = 1; + SecondByVal.LocationRegister = physical(2); + OverlappingByVal.ArgumentAssignments = {FirstByVal, SecondByVal}; + OverlappingByVal.ByValCopies = { + {0, FirstByVal.OriginalValue, FirstByVal.ValueType, 0, 16, align(8)}, + {1, SecondByVal.OriginalValue, SecondByVal.ValueType, 8, 16, align(8)}, + }; + Context.expect(!rcc::CallLoweringPlan::create(std::move(OverlappingByVal)), + "overlapping byval copy ranges must be rejected"); + + Context.expect(!rcc::MachineCallTarget::globalSymbol("") && !rcc::MachineCallTarget::indirect({}), + "call-target factories must reject missing targets"); +} + +class RecordingCallLowering final : public rcc::TargetCallLowering { +public: + enum class Behavior : std::uint8_t { + Valid, + PlannerFailure, + WrongKind, + }; + + explicit RecordingCallLowering(Behavior CurrentBehavior) : CurrentBehavior(CurrentBehavior) {} + + [[nodiscard]] rcc::Expected<rcc::CallLoweringPlan> + planFormalArguments(const rcc::FormalArgumentRequest &) const override { + return plan(rcc::CallLoweringPlanKind::FormalArguments); + } + + [[nodiscard]] rcc::Expected<rcc::CallLoweringPlan> + planCall(const rcc::CallLoweringRequest &) const override { + return plan(rcc::CallLoweringPlanKind::Call); + } + + [[nodiscard]] rcc::Expected<rcc::CallLoweringPlan> + planReturn(const rcc::ReturnLoweringRequest &) const override { + return plan(rcc::CallLoweringPlanKind::Return); + } + + [[nodiscard]] rcc::Expected<void> emitPlan(rcc::MachineIRBuilder &, + const rcc::CallLoweringPlan &) const override { + ++EmissionCount; + return {}; + } + + [[nodiscard]] std::uint32_t emissions() const noexcept { + return EmissionCount; + } + +private: + [[nodiscard]] rcc::Expected<rcc::CallLoweringPlan> + plan(rcc::CallLoweringPlanKind RequestedKind) const { + if (CurrentBehavior == Behavior::PlannerFailure) { + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, + "intentional call-lowering planner failure"); + } + if (CurrentBehavior == Behavior::WrongKind) + RequestedKind = RequestedKind == rcc::CallLoweringPlanKind::Call + ? rcc::CallLoweringPlanKind::Return + : rcc::CallLoweringPlanKind::Call; + return emptyPlan(RequestedKind); + } + + Behavior CurrentBehavior; + mutable std::uint32_t EmissionCount = 0; +}; + +void testPlanThenEmitBoundary(TestContext &Context) { + auto Target = createTarget(); + auto Function = + rcc::MachineFunction::create("plan_then_emit", rcc::MachineFunctionStableID::fromValue(1)); + Context.expect(Target && Function, "call-lowering workflow fixtures must be valid"); + if (!Target || !Function) + return; + rcc::MachineIRBuilder Builder(**Function, *Target); + rcc::CallLoweringRequest Request; + + RecordingCallLowering Failing(RecordingCallLowering::Behavior::PlannerFailure); + auto Failure = Failing.lowerCall(Builder, Request); + Context.expect(!Failure && Failing.emissions() == 0 && (*Function)->allInstructions().empty(), + "planner failure must not invoke emission or mutate MachineIR"); + + RecordingCallLowering WrongKind(RecordingCallLowering::Behavior::WrongKind); + auto Mismatch = WrongKind.lowerCall(Builder, Request); + Context.expect(!Mismatch && Mismatch.error().code() == rcc::ErrorCode::InternalError && + WrongKind.emissions() == 0, + "a target returning the wrong plan kind must be rejected before emission"); + + RecordingCallLowering Valid(RecordingCallLowering::Behavior::Valid); + Context.expect(Valid.lowerCall(Builder, Request) && Valid.emissions() == 1, + "a valid plan must be emitted exactly once"); +} + +} // namespace + +int main() { + TestContext Context; + testPlanValidation(Context); + testPlanThenEmitBoundary(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/TargetConstantTest.cpp b/backend/unittests/CodeGen/TargetConstantTest.cpp new file mode 100644 index 0000000..77338b7 --- /dev/null +++ b/backend/unittests/CodeGen/TargetConstantTest.cpp @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/TargetConstant.h" + +#include <bit> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <optional> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +struct TestRelocConstant final { + std::int64_t Addend = 0; + std::string Symbol; + + [[nodiscard]] static constexpr rcc::TargetConstantTypeID stableTypeID() noexcept { + return rcc::TargetConstantTypeID::fromValue(0x5253434354430001ULL); + } + + [[nodiscard]] static constexpr std::uint32_t schemaVersion() noexcept { + return 2; + } + + [[nodiscard]] rcc::Expected<void> verify() const { + if (Symbol.empty()) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "test target constant symbol cannot be empty"); + } + return {}; + } + + [[nodiscard]] rcc::Expected<TestRelocConstant> clone() const { + return TestRelocConstant{Addend, Symbol}; + } + + [[nodiscard]] std::uint64_t stableHash() const noexcept { + std::uint64_t Hash = 1469598103934665603ULL; + for (const char RawCharacter : Symbol) { + const auto Character = static_cast<unsigned char>(RawCharacter); + Hash ^= Character; + Hash *= 1099511628211ULL; + } + Hash ^= static_cast<std::uint64_t>(Addend); + return Hash * 1099511628211ULL; + } + + [[nodiscard]] std::string print() const { + return Symbol + "+" + std::to_string(Addend); + } + + [[nodiscard]] std::vector<std::byte> addendBytes() const { + std::vector<std::byte> Bytes(8); + const std::uint64_t Bits = static_cast<std::uint64_t>(Addend); + for (std::size_t Index = 0; Index < Bytes.size(); ++Index) + Bytes[Index] = static_cast<std::byte>((Bits >> (Index * 8U)) & 0xffU); + return Bytes; + } + + [[nodiscard]] rcc::Expected<std::vector<std::byte>> encode() const { + std::vector<std::byte> Payload = addendBytes(); + for (const char RawCharacter : Symbol) { + const auto Character = static_cast<unsigned char>(RawCharacter); + Payload.push_back(static_cast<std::byte>(Character)); + } + return Payload; + } + + [[nodiscard]] rcc::Expected<rcc::TargetConstantEmission> emit() const { + rcc::TargetConstantEmission Emission; + Emission.Bytes = addendBytes(); + Emission.Fixups.push_back({0, rcc::TargetRelocationKindID::fromValue(9), Symbol, Addend}); + return Emission; + } + + [[nodiscard]] rcc::Alignment requiredAlignment() const noexcept { + return *rcc::Alignment::create(8); + } + + [[nodiscard]] std::optional<rcc::TargetRelocationKindID> relocationKind() const noexcept { + return rcc::TargetRelocationKindID::fromValue(9); + } + + [[nodiscard]] static rcc::Expected<TestRelocConstant> parse(std::string_view Text) { + if (Text.empty()) + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, "test constant text is empty"); + return TestRelocConstant{7, std::string(Text)}; + } + + [[nodiscard]] static rcc::Expected<TestRelocConstant> decode(std::uint32_t Version, + std::span<const std::byte> Payload) { + if (Version != schemaVersion()) + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, + "unsupported test constant schema version"); + if (Payload.size() <= 8) + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, "test constant payload is incomplete"); + + std::uint64_t Bits = 0; + for (std::size_t Index = 0; Index < 8; ++Index) + Bits |= std::to_integer<std::uint64_t>(Payload[Index]) << (Index * 8U); + std::string Symbol; + Symbol.reserve(Payload.size() - 8); + for (const std::byte Character : Payload.subspan(8)) + Symbol.push_back(static_cast<char>(std::to_integer<unsigned char>(Character))); + return TestRelocConstant{std::bit_cast<std::int64_t>(Bits), std::move(Symbol)}; + } + + friend bool operator==(const TestRelocConstant &Left, const TestRelocConstant &Right) = default; +}; + +struct CollidingConstant final { + TestRelocConstant Value; + + [[nodiscard]] static constexpr rcc::TargetConstantTypeID stableTypeID() noexcept { + return TestRelocConstant::stableTypeID(); + } + + [[nodiscard]] static constexpr std::uint32_t schemaVersion() noexcept { + return TestRelocConstant::schemaVersion(); + } + + [[nodiscard]] rcc::Expected<void> verify() const { + return Value.verify(); + } + + [[nodiscard]] rcc::Expected<CollidingConstant> clone() const { + return CollidingConstant{Value}; + } + + [[nodiscard]] std::uint64_t stableHash() const noexcept { + return Value.stableHash(); + } + + [[nodiscard]] std::string print() const { + return Value.print(); + } + + [[nodiscard]] rcc::Expected<std::vector<std::byte>> encode() const { + return Value.encode(); + } + + [[nodiscard]] rcc::Expected<rcc::TargetConstantEmission> emit() const { + return Value.emit(); + } + + [[nodiscard]] rcc::Alignment requiredAlignment() const noexcept { + return Value.requiredAlignment(); + } + + [[nodiscard]] std::optional<rcc::TargetRelocationKindID> relocationKind() const noexcept { + return Value.relocationKind(); + } + + [[nodiscard]] static rcc::Expected<CollidingConstant> parse(std::string_view Text) { + auto Parsed = TestRelocConstant::parse(Text); + if (!Parsed) + return std::unexpected(Parsed.error()); + return CollidingConstant{std::move(*Parsed)}; + } + + [[nodiscard]] static rcc::Expected<CollidingConstant> decode(std::uint32_t Version, + std::span<const std::byte> Payload) { + auto Decoded = TestRelocConstant::decode(Version, Payload); + if (!Decoded) + return std::unexpected(Decoded.error()); + return CollidingConstant{std::move(*Decoded)}; + } + + friend bool operator==(const CollidingConstant &Left, const CollidingConstant &Right) = default; +}; + +void testTargetConstantContract(TestContext &Context) { + auto Constant = rcc::TargetConstant::create(TestRelocConstant{11, "symbol"}); + Context.expect(Constant.has_value(), "valid target constant must be type-erased"); + if (!Constant) + return; + + Context.expect(Constant->typeID() == TestRelocConstant::stableTypeID() && + Constant->schemaVersion() == TestRelocConstant::schemaVersion() && + Constant->getIf<TestRelocConstant>() != nullptr && + Constant->getIf<CollidingConstant>() == nullptr, + "strong query must require both stable and local type identity"); + auto Printed = Constant->print(); + auto RequiredAlignment = Constant->requiredAlignment(); + auto RelocationKind = Constant->relocationKind(); + Context.expect(Constant->verify().has_value() && Printed && *Printed == "symbol+11" && + RequiredAlignment && *RequiredAlignment == *rcc::Alignment::create(8) && + RelocationKind && *RelocationKind == rcc::TargetRelocationKindID::fromValue(9), + "target constant semantic hooks must remain available through erasure"); + + auto Cloned = Constant->clone(); + Context.expect(Cloned && Constant->equals(*Cloned) && + Cloned->getIf<TestRelocConstant>() != Constant->getIf<TestRelocConstant>(), + "target constant clone must own an equal independent value"); + + auto Encoded = Constant->encode(); + if (!Encoded) + return; + auto Decoded = rcc::TargetConstant::decode<TestRelocConstant>(*Encoded); + Context.expect(Encoded && Decoded && Constant->equals(*Decoded), + "versioned target constant encoding must round-trip losslessly"); + + auto Parsed = rcc::TargetConstant::parse<TestRelocConstant>("parsed"); + const TestRelocConstant *ParsedValue = Parsed ? Parsed->getIf<TestRelocConstant>() : nullptr; + Context.expect(ParsedValue != nullptr && ParsedValue->Addend == 7 && + ParsedValue->Symbol == "parsed", + "target constant text parser must construct the same strong type"); + + auto Emission = Constant->emit(); + Context.expect(Emission && Emission->Bytes.size() == 8 && Emission->Fixups.size() == 1 && + Emission->Fixups[0].Symbol == "symbol" && + Emission->Fixups[0].Kind == rcc::TargetRelocationKindID::fromValue(9), + "target constant emission must preserve bytes and structured fixups"); +} + +void testTargetConstantFailures(TestContext &Context) { + rcc::TargetConstant Empty; + Context.expect(!Empty.verify() && !Empty.clone() && !Empty.encode() && !Empty.emit(), + "empty target constant must reject every semantic operation"); + Context.expect(!rcc::TargetConstant::create(TestRelocConstant{0, ""}), + "target constant creation must verify its value"); + + auto Constant = rcc::TargetConstant::create(TestRelocConstant{3, "symbol"}); + if (!Constant) + return; + auto Encoded = Constant->encode(); + if (!Encoded) + return; + Encoded->Version = 99; + Context.expect(!rcc::TargetConstant::decode<TestRelocConstant>(*Encoded), + "target constant decode must reject unsupported versions"); + Encoded->Version = TestRelocConstant::schemaVersion(); + Encoded->TypeID = rcc::TargetConstantTypeID::fromValue(1); + Context.expect(!rcc::TargetConstant::decode<TestRelocConstant>(*Encoded), + "target constant decode must reject a mismatched TypeID"); + + rcc::TargetConstantEmission Invalid; + Invalid.Bytes.resize(4); + Invalid.Fixups.push_back({4, rcc::TargetRelocationKindID::fromValue(1), "symbol", 0}); + Context.expect(!Invalid.verify(), "target emission must reject out-of-range fixups"); + Invalid.Fixups[0].Offset = 0; + Invalid.Fixups[0].Kind = {}; + Context.expect(!Invalid.verify(), "target emission must reject invalid relocation kinds"); + Invalid.Fixups[0].Kind = rcc::TargetRelocationKindID::fromValue(1); + Invalid.Fixups.push_back(Invalid.Fixups[0]); + Context.expect(!Invalid.verify(), "target emission must reject duplicate fixup offsets"); +} + +} // namespace + +int main() { + TestContext Context; + testTargetConstantContract(Context); + testTargetConstantFailures(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/TargetFunctionStateTest.cpp b/backend/unittests/CodeGen/TargetFunctionStateTest.cpp new file mode 100644 index 0000000..2286400 --- /dev/null +++ b/backend/unittests/CodeGen/TargetFunctionStateTest.cpp @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/TargetFunctionState.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineFunctionCloner.h" +#include "rcc/CodeGen/MachineVerifier.h" + +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +struct TestFunctionInfo final { + std::uint64_t StackProbeSize = 0; + bool UsesShadowCallStack = false; + std::string CallingConvention; + + [[nodiscard]] static constexpr rcc::TargetFunctionStateTypeID stableTypeID() noexcept { + return rcc::TargetFunctionStateTypeID::fromValue(0x5243435446530001ULL); + } + + [[nodiscard]] static constexpr std::uint32_t schemaVersion() noexcept { + return 3; + } + + [[nodiscard]] rcc::Expected<void> verify() const { + if (StackProbeSize == 0 || CallingConvention.empty()) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "test function info is incomplete"); + } + return {}; + } + + [[nodiscard]] rcc::Expected<TestFunctionInfo> + clone(const rcc::MachineFunctionCloneContext &) const { + return TestFunctionInfo{StackProbeSize, UsesShadowCallStack, CallingConvention}; + } + + [[nodiscard]] std::string print() const { + return CallingConvention + ":" + std::to_string(StackProbeSize) + ":" + + (UsesShadowCallStack ? "shadow" : "plain"); + } + + [[nodiscard]] rcc::Expected<std::vector<std::byte>> encode() const { + std::vector<std::byte> Payload; + Payload.reserve(9 + CallingConvention.size()); + for (std::size_t Index = 0; Index < 8; ++Index) { + Payload.push_back(static_cast<std::byte>((StackProbeSize >> (Index * 8U)) & 0xffU)); + } + Payload.push_back(static_cast<std::byte>(UsesShadowCallStack ? 1 : 0)); + for (const char RawCharacter : CallingConvention) { + Payload.push_back(static_cast<std::byte>(static_cast<unsigned char>(RawCharacter))); + } + return Payload; + } + + [[nodiscard]] static rcc::Expected<TestFunctionInfo> decode(std::uint32_t Version, + std::span<const std::byte> Payload) { + if (Version != schemaVersion()) { + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, + "unsupported test function info version"); + } + if (Payload.size() <= 9 || std::to_integer<unsigned int>(Payload[8]) > 1) { + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, + "test function info payload is invalid"); + } + + std::uint64_t StackProbeSize = 0; + for (std::size_t Index = 0; Index < 8; ++Index) { + StackProbeSize |= std::to_integer<std::uint64_t>(Payload[Index]) << (Index * 8U); + } + std::string CallingConvention; + CallingConvention.reserve(Payload.size() - 9); + for (const std::byte Character : Payload.subspan(9)) { + CallingConvention.push_back(static_cast<char>(std::to_integer<unsigned char>(Character))); + } + return TestFunctionInfo{StackProbeSize, std::to_integer<unsigned int>(Payload[8]) != 0, + std::move(CallingConvention)}; + } + + friend bool operator==(const TestFunctionInfo &Left, const TestFunctionInfo &Right) = default; +}; + +struct CollidingFunctionInfo final { + TestFunctionInfo Value; + + [[nodiscard]] static constexpr rcc::TargetFunctionStateTypeID stableTypeID() noexcept { + return TestFunctionInfo::stableTypeID(); + } + + [[nodiscard]] static constexpr std::uint32_t schemaVersion() noexcept { + return TestFunctionInfo::schemaVersion(); + } + + [[nodiscard]] rcc::Expected<void> verify() const { + return Value.verify(); + } + + [[nodiscard]] rcc::Expected<CollidingFunctionInfo> + clone(const rcc::MachineFunctionCloneContext &) const { + return CollidingFunctionInfo{Value}; + } + + [[nodiscard]] std::string print() const { + return Value.print(); + } + + [[nodiscard]] rcc::Expected<std::vector<std::byte>> encode() const { + return Value.encode(); + } + + [[nodiscard]] static rcc::Expected<CollidingFunctionInfo> + decode(std::uint32_t Version, std::span<const std::byte> Payload) { + auto Decoded = TestFunctionInfo::decode(Version, Payload); + if (!Decoded) + return std::unexpected(Decoded.error()); + return CollidingFunctionInfo{std::move(*Decoded)}; + } + + friend bool operator==(const CollidingFunctionInfo &Left, + const CollidingFunctionInfo &Right) = default; +}; + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + rcc::InstructionDescriptor Descriptor; + Descriptor.InstructionOpcode = rcc::Opcode::fromValue(1); + Descriptor.Name = "TEST_NOP"; + Descriptor.Flags = {rcc::InstructionFlag::ProducesCode}; + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Descriptor)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +void testTargetFunctionStateContract(TestContext &Context) { + auto State = rcc::TargetFunctionState::create(TestFunctionInfo{4096, true, "lp64d"}); + Context.expect(State.has_value(), "valid target function state must be type-erased"); + if (!State) + return; + + Context.expect(State->typeID() == TestFunctionInfo::stableTypeID() && + State->schemaVersion() == TestFunctionInfo::schemaVersion() && + State->getIf<TestFunctionInfo>() != nullptr && + State->getIf<CollidingFunctionInfo>() == nullptr, + "strong query must require stable and local type identity"); + auto Printed = State->print(); + Context.expect(State->verify().has_value() && Printed && *Printed == "lp64d:4096:shadow", + "type-erased target state must preserve its semantic hooks"); + + auto Encoded = State->encode(); + if (!Encoded) + return; + auto Decoded = rcc::TargetFunctionState::decode<TestFunctionInfo>(*Encoded); + Context.expect(Decoded && State->equals(*Decoded) && + State->getIf<TestFunctionInfo>() != Decoded->getIf<TestFunctionInfo>(), + "versioned target function state must round-trip losslessly"); + + auto Collision = rcc::TargetFunctionState::create( + CollidingFunctionInfo{TestFunctionInfo{4096, true, "lp64d"}}); + Context.expect(Collision && !State->equals(*Collision), + "colliding stable TypeIDs must not defeat local type safety"); +} + +void testTargetFunctionStateFailures(TestContext &Context) { + rcc::TargetFunctionState Empty; + Context.expect(!Empty.verify() && !Empty.print() && !Empty.encode(), + "empty target function state must reject semantic operations"); + Context.expect(!rcc::TargetFunctionState::create(TestFunctionInfo{0, false, ""}), + "target function state creation must verify its concrete value"); + + auto State = rcc::TargetFunctionState::create(TestFunctionInfo{2048, false, "lp64"}); + if (!State) + return; + auto Encoded = State->encode(); + if (!Encoded) + return; + + Encoded->Version = 0; + Context.expect(!rcc::TargetFunctionState::decode<TestFunctionInfo>(*Encoded), + "target function state decode must reject version zero"); + Encoded->Version = 99; + Context.expect(!rcc::TargetFunctionState::decode<TestFunctionInfo>(*Encoded), + "target function state decode must reject unsupported versions"); + Encoded->Version = TestFunctionInfo::schemaVersion(); + Encoded->TypeID = rcc::TargetFunctionStateTypeID::fromValue(1); + Context.expect(!rcc::TargetFunctionState::decode<TestFunctionInfo>(*Encoded), + "target function state decode must reject a mismatched TypeID"); +} + +void testMachineFunctionOwnershipAndVerification(TestContext &Context) { + auto Target = createTarget(); + auto Function = + rcc::MachineFunction::create("target_state", rcc::MachineFunctionStableID::fromValue(1)); + if (!Target || !Function) + return; + + Context.expect(!(*Function)->setTargetState(rcc::TargetFunctionState{}), + "machine function must reject an empty target state"); + auto State = rcc::TargetFunctionState::create(TestFunctionInfo{4096, false, "lp64d"}); + Context.expect(State && (*Function)->setTargetState(std::move(*State)).has_value(), + "machine function must accept one verified target state"); + Context.expect((*Function)->getTargetInfo<TestFunctionInfo>() != nullptr && + (*Function)->getTargetInfo<CollidingFunctionInfo>() == nullptr && + rcc::MachineVerifier::verify(**Function, *Target).has_value(), + "machine function must expose and verify its strong target state"); + + auto Replacement = rcc::TargetFunctionState::create(TestFunctionInfo{8192, true, "lp64d"}); + Context.expect(Replacement && !(*Function)->setTargetState(std::move(*Replacement)), + "machine function must not accept a second current target state"); + + TestFunctionInfo *Info = (*Function)->getTargetInfo<TestFunctionInfo>(); + if (Info == nullptr) + return; + Info->CallingConvention.clear(); + Context.expect(!rcc::MachineVerifier::verify(**Function, *Target), + "machine verifier must reject invalid concrete target state"); +} + +} // namespace + +int main() { + TestContext Context; + testTargetFunctionStateContract(Context); + testTargetFunctionStateFailures(Context); + testMachineFunctionOwnershipAndVerification(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/TargetMIRTextInfoTest.cpp b/backend/unittests/CodeGen/TargetMIRTextInfoTest.cpp new file mode 100644 index 0000000..24945bd --- /dev/null +++ b/backend/unittests/CodeGen/TargetMIRTextInfoTest.cpp @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/TargetMIRTextInfo.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MachineCFIInfo.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/TargetDescription.h" +#include "rcc/CodeGen/TargetRegisterInfo.h" + +#include <array> +#include <cstdint> +#include <iostream> +#include <memory> +#include <span> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::PhysicalRegister phys(std::uint32_t Number) { + return *rcc::PhysicalRegister::create(Number); +} + +void testTargetRegisterNames(TestContext &Context) { + auto Registers = rcc::test::createTestTargetRegisterInfo(); + Context.expect(Registers.has_value(), "TestTarget register schema must be valid"); + if (!Registers) + return; + + const auto G0 = phys(rcc::test::TestRegister::G0); + Context.expect(Registers->physicalRegister("g0") != nullptr && + Registers->physicalRegister("G0") == nullptr && + Registers->physicalRegisterMIRName(G0) == "g0", + "physical register MIR names must be canonical lowercase names"); + Context.expect(Registers->subRegIndex("low32") != nullptr && + Registers->subRegIndex("LOW32") == nullptr, + "subregister MIR names must retain their exact target spelling"); + Context.expect(Registers->registerClass("gpr64") != nullptr && + Registers->registerClass("GPR64") == nullptr && + Registers->registerClassMIRName(rcc::test::TestRegisterClass::GPR64) == + "gpr64", + "register class MIR names must be canonical lowercase names"); + Context.expect(Registers->registerBank("gpr") != nullptr && + Registers->registerBank("GPR") == nullptr && + Registers->registerBankMIRName(rcc::test::TestRegisterBank::GPR) == "gpr", + "register bank MIR names must be canonical lowercase names"); +} + +void testTextSchemaQueries(TestContext &Context) { + auto Registers = rcc::test::createTestTargetRegisterInfo(); + if (!Registers) + return; + auto Text = rcc::TargetMIRTextInfo::create(rcc::test::createTestTargetMIRTextInfoDescription(), + *Registers); + Context.expect(Text.has_value(), "TestTarget MIR text schema must be valid"); + if (!Text) + return; + + const auto Preserved = rcc::test::createTestCallPreservedMask(); + const auto NamedMask = Text->registerMask("test-preserved"); + Context.expect(NamedMask && *NamedMask == *Preserved && !Text->registerMask("Test-Preserved") && + Text->registerMaskName(*Preserved) == "test-preserved", + "named register masks must use one canonical lowercase spelling"); + + Context.expect(Text->targetIndex("test-index") == rcc::TargetIndex::fromValue(0) && + Text->targetIndexName(rcc::TargetIndex::fromValue(0)) == "test-index", + "target index zero must remain a valid named target index"); + Context.expect(Text->directOperandFlag("test-direct-three") == 0x3 && + Text->bitmaskOperandFlag("test-bit-four") == 0x4, + "direct and bitmask target flags must retain distinct registries"); + + auto OperandNames = Text->operandFlagNames(0xD); + Context.expect(OperandNames && + *OperandNames == std::vector<std::string_view>( + {"test-direct-one", "test-bit-four", "test-bit-eight"}), + "operand flag decomposition must be canonical and declaration ordered"); + Context.expect(!Text->operandFlagNames(0x10), + "operand flag decomposition must reject unknown bits"); + + auto MMONames = Text->machineMemOperandFlagNames(0x3); + auto VRegNames = Text->virtualRegisterFlagNames(0x3); + Context.expect( + MMONames && *MMONames == std::vector<std::string_view>({"test-mmo-one", "test-mmo-two"}) && + VRegNames && + *VRegNames == std::vector<std::string_view>({"test-vreg-one", "test-vreg-two"}) && + !Text->machineMemOperandFlagNames(0x4) && !Text->virtualRegisterFlagNames(0x4), + "MMO and virtual-register flags must reject every unregistered bit"); + + Context.expect( + Text->intrinsic("llvm.test.seven") == rcc::IntrinsicID::fromValue(7) && + Text->intrinsicName(rcc::IntrinsicID::fromValue(17)) == "llvm.test.seventeen" && + Text->intrinsic("llvm.va_copy") == rcc::intrinsicID(rcc::GenericIntrinsic::VACopy) && + Text->intrinsicName(rcc::intrinsicID(rcc::GenericIntrinsic::VACopy)) == "llvm.va_copy", + "intrinsic names and strong IDs must round-trip"); + + const auto G0 = phys(rcc::test::TestRegister::G0); + Context.expect(Text->dwarfRegister(G0) == rcc::test::TestRegister::G0 && + Text->physicalRegister(rcc::test::TestRegister::G0) == G0, + "CFI register mappings must support both physical and DWARF lookup"); + Context.expect(Text->supportsStackID(rcc::StackID::defaultStack()) && + Text->supportsStackID(rcc::StackID::noAllocStack()) && + !Text->supportsStackID(rcc::StackID::sgprSpillStack()), + "target stack support must be explicit and include fixed baseline defaults"); +} + +void testTextSchemaRejection(TestContext &Context) { + auto Registers = rcc::test::createTestTargetRegisterInfo(); + if (!Registers) + return; + auto Reject = [&Context, &Registers](rcc::TargetMIRTextInfoDesc Description, + std::string_view Message) { + Context.expect(!rcc::TargetMIRTextInfo::create(std::move(Description), *Registers), Message); + }; + + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.RegisterMasks.push_back( + {"TEST-PRESERVED", + std::make_shared<const rcc::RegisterMask>(std::vector<std::uint64_t>{})}); + Reject(std::move(Description), "register mask names must be unique case-insensitively"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.RegisterMasks.push_back({"other-mask", rcc::test::createTestCallPreservedMask()}); + Reject(std::move(Description), "register mask values must have one canonical name"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.RegisterMasks.front().Mask = std::make_shared<const rcc::RegisterMask>( + std::vector<std::uint64_t>{std::uint64_t{1} << 63U}); + Reject(std::move(Description), "named masks must reject unknown physical registers"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.TargetIndices.push_back({"other-index", rcc::TargetIndex::fromValue(0)}); + Reject(std::move(Description), "target index values must be unique"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.DirectOperandFlagMask = 0; + Reject(std::move(Description), "direct flags require a direct flag mask"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.DirectOperandFlags.front().Value = 0x4; + Reject(std::move(Description), "direct flags must remain inside their direct field"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.BitmaskOperandFlags.front().Value = 0xC; + Reject(std::move(Description), "bitmask operand flags must contain one bit"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.MachineMemOperandFlags.front().Value = 0x3; + Reject(std::move(Description), "MMO target flags must contain one bit"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.VirtualRegisterFlags.back().Value = Description.VirtualRegisterFlags.front().Value; + Reject(std::move(Description), "virtual-register target flag values must be unique"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.Intrinsics.front().Name = "test.not-an-intrinsic"; + Reject(std::move(Description), "intrinsic names must use the llvm namespace"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.CFIRegisters[1].DwarfRegister = Description.CFIRegisters[0].DwarfRegister; + Reject(std::move(Description), + "a DWARF register must not have multiple canonical physical registers"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.CFIRegisters.front().IsCanonical = false; + Reject(std::move(Description), + "every referenced DWARF register must have a canonical physical register"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.CFIRegisters.front().Register = phys(99); + Reject(std::move(Description), "CFI mappings must reject unknown physical registers"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.FunctionStateType = rcc::TargetFunctionStateTypeID{}; + Reject(std::move(Description), "target function state schemas require a nonzero TypeID"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.SupportedStackIDs.pop_back(); + Reject(std::move(Description), "targets must support the noalloc stack ID"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.SupportedStackIDs.push_back(rcc::StackID::defaultStack()); + Reject(std::move(Description), "supported stack IDs must be unique"); + } + + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.CFIRegisters[1].DwarfRegister = Description.CFIRegisters[0].DwarfRegister; + Description.CFIRegisters[1].IsCanonical = false; + auto Text = rcc::TargetMIRTextInfo::create(std::move(Description), *Registers); + Context.expect(Text && + Text->dwarfRegister(phys(rcc::test::TestRegister::G0Low32)) == + rcc::test::TestRegister::G0 && + Text->physicalRegister(rcc::test::TestRegister::G0) == + phys(rcc::test::TestRegister::G0), + "noncanonical physical aliases must map to one canonical DWARF register"); + } + + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.PhysicalRegisters[1].Name = "G0"; + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "physical register names must be case-insensitively unique"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.RegisterClasses[1].Name = "gpr64"; + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "register class names must be case-insensitively unique"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.RegisterBanks[1].Name = "gpr"; + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "register bank names must be case-insensitively unique"); + } +} + +void testSemanticValidation(TestContext &Context) { + rcc::OperandDescriptor Operand; + Operand.Kinds = {rcc::MachineOperand::Kind::Immediate}; + Operand.AllowedTargetFlags = 0xF; + + rcc::InstructionDescriptor Instruction; + Instruction.InstructionOpcode = rcc::Opcode::fromValue(1); + Instruction.Name = "TEST_LOAD_IMM"; + Instruction.Operands.push_back(Operand); + Instruction.Flags = { + rcc::InstructionFlag::ProducesCode, + rcc::InstructionFlag::MayLoad, + }; + Instruction.RequiresMemoryOperands = true; + + auto Instructions = rcc::InstructionSet::create({std::move(Instruction)}); + if (!Instructions) + return; + auto Target = rcc::test::createTestTargetDescription(std::move(*Instructions)); + auto Function = + rcc::MachineFunction::create("schema_validation", rcc::MachineFunctionStableID::fromValue(1)); + Context.expect(Target.has_value() && Function.has_value(), + "semantic target schema fixture must be constructible"); + if (!Target || !Function) + return; + + auto Reg = (*Function)->registerInfo().createVirtualRegister(); + Context.expect(Reg && (*Function)->registerInfo().setTargetRegisterFlags(*Reg, 0x3).has_value() && + Target->validateRegisterInfo((*Function)->registerInfo()).has_value(), + "registered virtual-register target flags must pass target validation"); + if (Reg) { + Context.expect((*Function)->registerInfo().setTargetRegisterFlags(*Reg, 0x4).has_value() && + !Target->validateRegisterInfo((*Function)->registerInfo()), + "unregistered virtual-register target flags must fail target validation"); + Context.expect( + (*Function)->registerInfo().clearTargetRegisterFlags(*Reg).has_value(), + "semantic validation fixture must restore registered virtual-register target flags"); + } + + const rcc::MachineOperand ValidOperand = rcc::MachineOperand::immediate(7, 0x5); + const rcc::MachineOperand InvalidOperand = rcc::MachineOperand::immediate(7, 0x10); + Context.expect( + Target->validateExplicitOperands(rcc::Opcode::fromValue(1), + std::span<const rcc::MachineOperand>(&ValidOperand, 1), + (*Function)->registerInfo()) + .has_value() && + !Target->validateExplicitOperands( + rcc::Opcode::fromValue(1), std::span<const rcc::MachineOperand>(&InvalidOperand, 1), + (*Function)->registerInfo()), + "machine operands must use only descriptor-allowed serializable target flags"); + + rcc::MachineMemOperandDesc MemoryDescription; + MemoryDescription.Accesses = {rcc::MemoryAccessFlag::Load}; + MemoryDescription.Size = *rcc::MemorySize::fixed(8); + MemoryDescription.Align = *rcc::Alignment::create(8); + MemoryDescription.TargetFlags = 0x3; + auto ValidMemory = rcc::MachineMemOperand::create(MemoryDescription); + MemoryDescription.TargetFlags = 0x4; + auto InvalidMemory = rcc::MachineMemOperand::create(MemoryDescription); + if (ValidMemory && InvalidMemory) { + const std::array<const rcc::MachineMemOperand *, 1> ValidOperands{&*ValidMemory}; + const std::array<const rcc::MachineMemOperand *, 1> InvalidOperands{&*InvalidMemory}; + Context.expect( + Target->validateMemOperands(rcc::Opcode::fromValue(1), ValidOperands).has_value() && + !Target->validateMemOperands(rcc::Opcode::fromValue(1), InvalidOperands), + "machine memory operands must use only registered target flags"); + } + + rcc::MachineCFIInfo CFI; + auto CFIIndex = CFI.addInstruction( + rcc::MachineCFIInstruction::sameValue(rcc::DwarfRegisterNumber::fromValue(99))); + Context.expect(CFIIndex && !Target->validateCFIInfo(CFI), + "CFI instructions must reject unmapped DWARF registers"); + + rcc::MachineFrameInfo Frame; + rcc::StackObjectDesc StackObject; + StackObject.Kind = rcc::StackObjectKind::Target; + StackObject.Size = 8; + StackObject.Align = *rcc::Alignment::create(8); + StackObject.Stack = rcc::StackID::sgprSpillStack(); + auto StackIndex = Frame.createObject(StackObject); + Context.expect(StackIndex && !Target->validateFrameInfo(Frame), + "frame objects must reject stack IDs unsupported by the target"); + + rcc::OperandDescriptor BadOperand; + BadOperand.Kinds = {rcc::MachineOperand::Kind::Immediate}; + BadOperand.AllowedTargetFlags = 0x10; + rcc::InstructionDescriptor BadInstruction; + BadInstruction.InstructionOpcode = rcc::Opcode::fromValue(2); + BadInstruction.Name = "TEST_BAD_FLAGS"; + BadInstruction.Operands.push_back(BadOperand); + BadInstruction.Flags = {rcc::InstructionFlag::ProducesCode}; + auto BadInstructions = rcc::InstructionSet::create({std::move(BadInstruction)}); + Context.expect(BadInstructions && + !rcc::test::createTestTargetDescription(std::move(*BadInstructions)), + "target construction must reject operand flag bits outside its text schema"); + + auto PartialDescription = rcc::test::createTestTargetMIRTextInfoDescription(); + PartialDescription.DirectOperandFlags.erase(PartialDescription.DirectOperandFlags.begin() + 1, + PartialDescription.DirectOperandFlags.end()); + auto Registers = rcc::test::createTestTargetRegisterInfo(); + auto PartialText = + Registers ? rcc::TargetMIRTextInfo::create(std::move(PartialDescription), *Registers) + : rcc::Expected<rcc::TargetMIRTextInfo>(std::unexpected(Registers.error())); + Context.expect(PartialText && !PartialText->validateOperandFlags(0x2), + "unused direct-field encodings must remain invalid"); +} + +} // namespace + +int main() { + TestContext Context; + testTargetRegisterNames(Context); + testTextSchemaQueries(Context); + testTextSchemaRejection(Context); + testSemanticValidation(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/TargetRegisterInfoTest.cpp b/backend/unittests/CodeGen/TargetRegisterInfoTest.cpp new file mode 100644 index 0000000..bb9a0fc --- /dev/null +++ b/backend/unittests/CodeGen/TargetRegisterInfoTest.cpp @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/TargetRegisterInfo.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/LowLevelType.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/TargetDescription.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <ranges> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::PhysicalRegister phys(std::uint32_t Number) { + return *rcc::PhysicalRegister::create(Number); +} + +[[nodiscard]] rcc::OperandDescriptor registerOperand(rcc::RegisterOperandRole Role, + rcc::RegisterClassID Class, + rcc::RegisterBankID Bank, + bool AllowSubReg = false) { + rcc::OperandDescriptor Operand; + Operand.Kinds = {rcc::MachineOperand::Kind::Register}; + Operand.RegisterRole = Role; + Operand.RegClass = Class; + Operand.RegBank = Bank; + Operand.AllowSubReg = AllowSubReg; + return Operand; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> +createTarget(rcc::TargetRegisterInfoDesc RegisterDescription, + rcc::InstructionDescriptor Instruction) { + auto Registers = rcc::TargetRegisterInfo::create(std::move(RegisterDescription)); + if (!Registers) + return std::unexpected(Registers.error()); + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Instruction)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::TargetDescription::create(std::move(*Instructions), std::move(*Registers)); +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> +createOperandTarget(rcc::OperandDescriptor Operand, + rcc::TargetRegisterInfoDesc RegisterDescription = + rcc::test::createTestTargetRegisterInfoDescription()) { + rcc::InstructionDescriptor Instruction; + Instruction.InstructionOpcode = rcc::Opcode::fromValue(1); + Instruction.Name = "TEST_CONSTRAINED"; + Instruction.Operands.push_back(Operand); + if (Instruction.Operands.front().RegisterRole == rcc::RegisterOperandRole::Def) { + Instruction.NumExplicitDefs = 1; + } + Instruction.Flags = {rcc::InstructionFlag::ProducesCode}; + + return createTarget(std::move(RegisterDescription), std::move(Instruction)); +} + +void testRegisterTopology(TestContext &Context) { + auto Target = rcc::test::createTestTargetRegisterInfo(); + Context.expect(Target.has_value(), "TestTarget register description must be valid"); + if (!Target) + return; + + const auto G0 = phys(rcc::test::TestRegister::G0); + const auto G0Low32 = phys(rcc::test::TestRegister::G0Low32); + const auto G0High32 = phys(rcc::test::TestRegister::G0High32); + const auto G0Low16 = phys(rcc::test::TestRegister::G0Low16); + const auto G1 = phys(rcc::test::TestRegister::G1); + const auto F0 = phys(rcc::test::TestRegister::F0); + + Context.expect(Target->registerUnits().size() == 11 && Target->physicalRegisters().size() == 11 && + Target->registerClasses().size() == 8 && Target->registerBanks().size() == 2, + "TestTarget must expose its complete static register tables"); + const rcc::PhysicalRegisterDesc *Named = Target->physicalRegister("g0"); + Context.expect(Named != nullptr && Named->Reg == G0 && Named->Encoding == 0, + "physical register name lookup must preserve identity and encoding"); + + Context.expect(Target->regsOverlap(G0, G0Low32) && Target->regsOverlap(G0, G0High32) && + Target->regsOverlap(G0Low32, G0Low16) && + !Target->regsOverlap(G0Low32, G0High32) && !Target->regsOverlap(G0, G1), + "physical overlap must be derived from shared register units"); + Context.expect(std::ranges::find(Target->aliases(G0), G0Low32) != Target->aliases(G0).end() && + std::ranges::find(Target->aliases(G0Low32), G0High32) == + Target->aliases(G0Low32).end(), + "alias queries must include only overlapping physical registers"); + + Context.expect(Target->subRegister(G0, rcc::test::TestSubReg::Low32) == G0Low32 && + Target->subRegister(G0Low32, rcc::test::TestSubReg::Low16) == G0Low16 && + Target->isSubRegister(G0, G0Low16) && + std::ranges::find(Target->superRegisters(G0Low16), G0) != + Target->superRegisters(G0Low16).end() && + Target->composeSubRegIndices(rcc::test::TestSubReg::Low32, + rcc::test::TestSubReg::Low16) == + rcc::test::TestSubReg::Low16, + "subregister queries must preserve direct, transitive, and composed mappings"); + Context.expect(Target->isValidLaneMask(G0, rcc::LaneMask::fromRaw(0x3)) && + Target->isValidLaneMask(G0, rcc::LaneMask::all()) && + !Target->isValidLaneMask(G0, rcc::LaneMask::fromRaw(0x10)), + "lane masks must be checked against target register coverage"); + + Context.expect( + Target->isSubclass(rcc::test::TestRegisterClass::GPR64Alloc, + rcc::test::TestRegisterClass::GPR64) && + !Target->isSubclass(rcc::test::TestRegisterClass::GPR64, + rcc::test::TestRegisterClass::GPR64Alloc) && + Target->registerClassesOverlap(rcc::test::TestRegisterClass::GPR64, + rcc::test::TestRegisterClass::GPR64Alloc) && + Target->subRegClass(rcc::test::TestRegisterClass::GPR64, rcc::test::TestSubReg::Low32) == + rcc::test::TestRegisterClass::GPR32Low, + "register class queries must preserve hierarchy and subregister projection"); + Context.expect(Target->bankContainsRegister(rcc::test::TestRegisterBank::GPR, G0) && + !Target->bankContainsRegister(rcc::test::TestRegisterBank::GPR, F0) && + !Target->registerBanksOverlap(rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterBank::FPR), + "register bank coverage must remain distinct from register classes"); + + const auto CallMask = rcc::test::createTestCallPreservedMask(); + Context.expect(CallMask->preserves(G1) && + CallMask->preserves(phys(rcc::test::TestRegister::StackPointer)) && + CallMask->preserves(phys(rcc::test::TestRegister::F1)) && + CallMask->clobbers(G0) && Target->validateRegisterMask(*CallMask).has_value(), + "call register mask must use preserved-bit semantics"); + Context.expect(Target->validateRegisterMask(rcc::RegisterMask({})).has_value(), + "an empty register mask must represent clobber-all"); + Context.expect(!Target->validateRegisterMask(rcc::RegisterMask({std::uint64_t{1} << 63U})) && + !Target->validateRegisterMask(rcc::RegisterMask({1})), + "register masks must reject preserved bits for unknown registers and noreg"); +} + +void testDescriptionRejection(TestContext &Context) { + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.RegisterUnits.back().ID = Description.RegisterUnits.front().ID; + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "duplicate register unit IDs must be rejected"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.PhysicalRegisters.front().AlternativeNames = {"later-name"}; + Description.PhysicalRegisters[1].Name = "later-name"; + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "canonical register names must not collide with earlier ABI aliases"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.PhysicalRegisters.front().IsReserved = true; + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "statically reserved registers must not appear in allocation orders"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.PhysicalSubRegisters.front().SubReg = phys(rcc::test::TestRegister::G1Low32); + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "subregister mappings with unrelated units must be rejected"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.PhysicalRegisters[2].Units.front().Lanes = rcc::LaneMask::fromRaw(0x3); + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "register-unit views with different lane counts must be rejected"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.RegisterClasses.front().SuperClasses.push_back( + rcc::test::TestRegisterClass::GPR64Alloc); + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "cyclic register class hierarchies must be rejected"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.RegisterBanks.front().MaximumSizeInBits = 32; + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "register banks smaller than a covered class must be rejected"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.SubRegClassMappings.pop_back(); + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "supported subregisters without class projections must be rejected"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.RegisterClasses.front().Lanes = rcc::LaneMask::fromRaw(0x10); + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "register class lanes outside a member register must be rejected"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.RegisterBanks.front().RegisterClasses.erase( + Description.RegisterBanks.front().RegisterClasses.begin()); + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "a class default bank must explicitly cover that class"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.RegisterUnits.push_back({rcc::RegisterUnitID::fromValue(99), "unused"}); + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "unreferenced register units must be rejected"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.SubRegCompositions.push_back( + {rcc::test::TestSubReg::Low16, rcc::test::TestSubReg::Low16, rcc::test::TestSubReg::Low32}); + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "subregister composition bit ranges must be internally consistent"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.RegisterClasses[3].CoveredBySubRegs = true; + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "subregister-covered classes must cover all class lanes"); + } +} + +void testTargetDescriptionConstraints(TestContext &Context) { + auto SubclassTarget = createOperandTarget(registerOperand(rcc::RegisterOperandRole::Def, + rcc::test::TestRegisterClass::GPR64, + rcc::test::TestRegisterBank::GPR)); + auto Function = + rcc::MachineFunction::create("constraints", rcc::MachineFunctionStableID::fromValue(1)); + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + if (!SubclassTarget || !Function || !Type) + return; + + auto Reg = (*Function)->registerInfo().createVirtualRegister(*Type); + if (!Reg) + return; + Context.expect((*Function) + ->registerInfo() + .setRegisterBank(*Reg, rcc::test::TestRegisterBank::GPR) + .has_value() && + (*Function) + ->registerInfo() + .setRegisterClass(*Reg, rcc::test::TestRegisterClass::GPR64Alloc) + .has_value(), + "constrained test virtual register must be classified"); + + auto Operand = rcc::MachineOperand::registerOperand(*Reg, {rcc::RegisterOperandFlag::Define}); + Context.expect( + Operand && SubclassTarget + ->validateExplicitOperands(rcc::Opcode::fromValue(1), + std::span<const rcc::MachineOperand>(&*Operand, 1), + (*Function)->registerInfo()) + .has_value(), + "an actual register subclass must satisfy a superclass operand constraint"); + + auto SubRegTarget = createOperandTarget(registerOperand(rcc::RegisterOperandRole::Def, + rcc::test::TestRegisterClass::GPR32, + rcc::test::TestRegisterBank::GPR, true)); + auto BaseClassResult = + (*Function)->registerInfo().setRegisterClass(*Reg, rcc::test::TestRegisterClass::GPR64); + auto SubRegOperand = rcc::MachineOperand::registerOperand( + *Reg, {rcc::RegisterOperandFlag::Define}, rcc::test::TestSubReg::Low32); + Context.expect( + SubRegTarget && BaseClassResult && SubRegOperand && + SubRegTarget + ->validateExplicitOperands(rcc::Opcode::fromValue(1), + std::span<const rcc::MachineOperand>(&*SubRegOperand, 1), + (*Function)->registerInfo()) + .has_value(), + "subregister class projection must satisfy a compatible superclass constraint"); + + auto PhysicalSubReg = rcc::MachineOperand::registerOperand( + rcc::Register::fromPhysical(phys(rcc::test::TestRegister::G0)), + {rcc::RegisterOperandFlag::Define}, rcc::test::TestSubReg::Low32); + Context.expect(PhysicalSubReg && !SubRegTarget->validateExplicitOperands( + rcc::Opcode::fromValue(1), + std::span<const rcc::MachineOperand>(&*PhysicalSubReg, 1), + (*Function)->registerInfo()), + "physical register operands must reject nonzero subregister indices"); + + auto WideType = rcc::LowLevelType::makeCategoryNeutralScalar(128); + if (!WideType) + return; + auto WideReg = (*Function)->registerInfo().createVirtualRegister(*WideType); + Context.expect(WideReg && + (*Function) + ->registerInfo() + .setRegisterBank(*WideReg, rcc::test::TestRegisterBank::GPR) + .has_value() && + !SubclassTarget->validateRegisterInfo((*Function)->registerInfo()), + "virtual register type must fit its selected bank"); + + auto UnknownClassOperand = + registerOperand(rcc::RegisterOperandRole::Def, rcc::RegisterClassID::fromValue(99), + rcc::test::TestRegisterBank::GPR); + Context.expect(!createOperandTarget(UnknownClassOperand), + "target creation must reject unknown operand register classes"); + auto MismatchedBankOperand = + registerOperand(rcc::RegisterOperandRole::Def, rcc::test::TestRegisterClass::GPR64, + rcc::test::TestRegisterBank::FPR); + Context.expect(!createOperandTarget(MismatchedBankOperand), + "target creation must reject incompatible class and bank constraints"); + + auto ConstantDescription = rcc::test::createTestTargetRegisterInfoDescription(); + ConstantDescription.PhysicalRegisters.front().IsConstant = true; + auto ConstantTarget = createOperandTarget(registerOperand(rcc::RegisterOperandRole::Def, + rcc::test::TestRegisterClass::GPR64, + rcc::test::TestRegisterBank::GPR), + std::move(ConstantDescription)); + auto HintFunction = + rcc::MachineFunction::create("constant_hint", rcc::MachineFunctionStableID::fromValue(2)); + if (ConstantTarget && HintFunction) { + auto HintReg = (*HintFunction)->registerInfo().createVirtualRegister(*Type); + Context.expect(HintReg && + (*HintFunction) + ->registerInfo() + .setRegisterClass(*HintReg, rcc::test::TestRegisterClass::GPR64) + .has_value() && + (*HintFunction) + ->registerInfo() + .addAllocationHint(*HintReg, phys(rcc::test::TestRegister::G0)) + .has_value() && + !ConstantTarget->validateRegisterInfo((*HintFunction)->registerInfo()), + "constant physical registers must be rejected as allocation hints"); + } + + auto RenamableDescription = rcc::test::createTestTargetRegisterInfoDescription(); + RenamableDescription.PhysicalRegisters.front().IsArtificial = true; + rcc::InstructionDescriptor RenamableImplicit; + RenamableImplicit.InstructionOpcode = rcc::Opcode::fromValue(2); + RenamableImplicit.Name = "TEST_BAD_IMPLICIT"; + RenamableImplicit.ImplicitDefs.push_back({phys(rcc::test::TestRegister::G0), false, true}); + Context.expect(!createTarget(std::move(RenamableDescription), std::move(RenamableImplicit)), + "target creation must reject renamable artificial implicit registers"); + + rcc::InstructionDescriptor ConflictingImplicit; + ConflictingImplicit.InstructionOpcode = rcc::Opcode::fromValue(3); + ConflictingImplicit.Name = "TEST_BAD_EARLY_IMPLICIT"; + ConflictingImplicit.ImplicitDefs.push_back({phys(rcc::test::TestRegister::G0), true, false}); + ConflictingImplicit.ImplicitUses.push_back( + {phys(rcc::test::TestRegister::G0Low32), false, false}); + Context.expect(!createTarget(rcc::test::createTestTargetRegisterInfoDescription(), + std::move(ConflictingImplicit)), + "target creation must reject overlapping early-clobber implicit operands"); +} + +} // namespace + +int main() { + TestContext Context; + testRegisterTopology(Context); + testDescriptionRejection(Context); + testTargetDescriptionConstraints(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/TwoAddressInstructionPassTest.cpp b/backend/unittests/CodeGen/TwoAddressInstructionPassTest.cpp new file mode 100644 index 0000000..d23a2eb --- /dev/null +++ b/backend/unittests/CodeGen/TwoAddressInstructionPassTest.cpp @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/TwoAddressInstructionPass.h" + +#include "SSAExitTestSupport.h" +#include "rcc/CodeGen/MachinePassManager.h" +#include "rcc/CodeGen/MachineVerifier.h" + +#include <array> +#include <memory> +#include <optional> + +namespace { + +using namespace rcc::test::ssa; + +void testTwoAddressAndSubregisterPseudos(TestContext &Context, const rcc::TargetDescription &Target, + const TestInstrInfo &Info) { + auto Fixture = createFunction(Context, "two_address", 5, 1); + if (!Fixture.Function || Fixture.Blocks.empty()) + return; + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto I32 = rcc::LowLevelType::makeCategoryNeutralScalar(32); + if (!I64 || !I32) + return; + auto &Function = *Fixture.Function; + rcc::MachineBasicBlock &Block = *Fixture.Blocks[0]; + auto GPR64 = [&](std::uint8_t Flags = 0) { + return createVReg(Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64, Flags); + }; + auto GPR32 = [&]() { + return createVReg(Function, *I32, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR32); + }; + + auto Satisfied = GPR64(); + auto UndefSource = GPR64(); + auto UndefDestination = GPR64(); + auto CommuteSource = GPR64(); + auto CommuteDestination = GPR64(); + auto ConvertSource = GPR64(); + auto ConvertDestination = GPR64(); + auto FallbackSource = GPR64(); + auto FallbackDestination = GPR64(); + auto SequenceDestination = GPR64(); + auto SequenceLow = GPR32(); + auto SequenceHigh = GPR32(); + auto InsertDestination = GPR64(); + auto InsertBase = GPR64(); + auto Inserted = GPR32(); + const bool Complete = Satisfied && UndefSource && UndefDestination && CommuteSource && + CommuteDestination && ConvertSource && ConvertDestination && + FallbackSource && FallbackDestination && SequenceDestination && + SequenceLow && SequenceHigh && InsertDestination && InsertBase && Inserted; + Context.expect(Complete, "two-address test registers must be created"); + if (!Complete) + return; + + for (rcc::Register Source : {*CommuteSource, *ConvertSource, *FallbackSource, *SequenceLow, + *SequenceHigh, *InsertBase, *Inserted}) { + const std::array Def{Source}; + Context.expect(buildRegisterInstruction(Function, Target, Block, testOpcode(DefOpcode), Def, {}, + {rcc::RegisterOperandFlag::Dead}) + .has_value(), + "two-address source definition must be built"); + } + const std::array UndefDef{*UndefSource}; + Context.expect(buildRegisterInstruction(Function, Target, Block, + rcc::opcode(rcc::GenericOpcode::IMPLICIT_DEF), UndefDef, + {}) + .has_value(), + "two-address undef source must be defined by IMPLICIT_DEF"); + + auto BuildTied = [&](rcc::Opcode Opcode, rcc::Register Def, rcc::Register Use, + std::optional<rcc::Register> Other = std::nullopt) { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = Builder.setInsertPoint(Block); + if (!Point) + return rcc::Expected<rcc::MachineInstr *>{std::unexpected(Point.error())}; + auto Pending = Builder.buildInstr(Opcode); + if (!Pending) + return rcc::Expected<rcc::MachineInstr *>{std::unexpected(Pending.error())}; + auto AddedDef = Pending->addDef(Def); + if (!AddedDef) + return rcc::Expected<rcc::MachineInstr *>{std::unexpected(AddedDef.error())}; + auto AddedUse = Pending->addUse(Use, {rcc::RegisterOperandFlag::Kill}); + if (!AddedUse) + return rcc::Expected<rcc::MachineInstr *>{std::unexpected(AddedUse.error())}; + if (Other) { + auto AddedOther = Pending->addUse(*Other); + if (!AddedOther) + return rcc::Expected<rcc::MachineInstr *>{std::unexpected(AddedOther.error())}; + } + return Pending->build(); + }; + + auto AlreadyTied = BuildTied(testOpcode(TiedOpcode), *Satisfied, *Satisfied); + auto UndefTied = BuildTied(testOpcode(TiedOpcode), *UndefDestination, *UndefSource); + auto Commutable = BuildTied(testOpcode(CommutableTiedOpcode), *CommuteDestination, *CommuteSource, + *CommuteDestination); + auto Convertible = + BuildTied(testOpcode(ConvertibleTiedOpcode), *ConvertDestination, *ConvertSource); + auto Fallback = BuildTied(testOpcode(TiedOpcode), *FallbackDestination, *FallbackSource); + Context.expect(AlreadyTied && UndefTied && Commutable && Convertible && Fallback, + "two-address instruction fixtures must be built"); + if (!AlreadyTied || !UndefTied || !Commutable || !Convertible || !Fallback) + return; + + rcc::MachineIRBuilder Builder(Function, Target); + if (!Builder.setInsertPoint(Block)) + return; + auto Sequence = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::REG_SEQUENCE)); + Context.expect( + Sequence && Sequence->addDef(*SequenceDestination) && Sequence->addUse(*SequenceLow) && + Sequence->addImmediate(rcc::test::TestSubReg::Low32.value()) && + Sequence->addUse(*SequenceHigh) && + Sequence->addImmediate(rcc::test::TestSubReg::High32.value()) && Sequence->build(), + "REG_SEQUENCE fixture must be built"); + auto Insert = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::INSERT_SUBREG)); + Context.expect(Insert && Insert->addDef(*InsertDestination) && Insert->addUse(*InsertBase) && + Insert->addUse(*Inserted) && + Insert->addImmediate(rcc::test::TestSubReg::Low32.value()) && Insert->build(), + "INSERT_SUBREG fixture must be built"); + auto Return = Builder.buildInstr(testOpcode(ReturnOpcode)); + Context.expect(Return && Return->build(), "two-address return must be built"); + + auto InitialLiveness = setSelectedSSAProperties(Function, Target); + Context.expect(InitialLiveness.has_value(), "two-address liveness rebuild must succeed"); + if (!InitialLiveness) + return; + Function.properties().set(rcc::MachineFunctionProperty::NoPHIs); + rcc::MachineFunctionPassManager Pipeline; + Context.expect( + Pipeline.addPass(std::make_unique<rcc::TwoAddressInstructionPass>(Info)).has_value(), + "two-address pass must be registered"); + rcc::MachineFunctionAnalysisManager Analyses; + auto Run = Pipeline.run(Function, Target, Analyses); + Context.expect(Run.has_value(), "two-address rewrite must succeed"); + if (!Run) + return; + + Context.expect(Function.properties().has(rcc::MachineFunctionProperty::TiedOpsRewritten) && + !Function.properties().has(rcc::MachineFunctionProperty::IsSSA), + "two-address rewrite must publish TiedOpsRewritten and clear IsSSA"); + Context.expect( + instructionsWithOpcode(Function, rcc::opcode(rcc::GenericOpcode::REG_SEQUENCE)).empty() && + instructionsWithOpcode(Function, rcc::opcode(rcc::GenericOpcode::INSERT_SUBREG)).empty(), + "TwoAddressInstructionPass must consume all Stage 10 subregister pseudos"); + + Context.expect((*AlreadyTied)->operandAt(1)->registerValue() == *Satisfied, + "an already-satisfied tie must remain unchanged"); + Context.expect( + (*UndefTied)->operandAt(1)->registerValue() == *UndefDestination && + (*UndefTied)->operandAt(1)->registerFlags().has(rcc::RegisterOperandFlag::Undef), + "undef tied uses must be rewritten without a COPY"); + Context.expect((*Commutable)->operandAt(1)->registerValue() == *CommuteDestination && + (*Commutable)->operandAt(2)->registerValue() == *CommuteSource, + "commutation must satisfy a tie before COPY fallback"); + Context.expect((*Convertible)->opcode() == testOpcode(ThreeAddressOpcode) && + !(*Convertible)->operandAt(0)->tiedOperandIndex() && + !(*Convertible)->operandAt(1)->tiedOperandIndex(), + "target three-address conversion must run before COPY fallback"); + Context.expect((*Fallback)->previous() != nullptr && + (*Fallback)->previous()->opcode() == rcc::opcode(rcc::GenericOpcode::COPY) && + (*Fallback)->previous()->operandAt(0)->registerValue() == + *FallbackDestination && + (*Fallback)->previous()->operandAt(1)->registerValue() == *FallbackSource && + (*Fallback)->operandAt(1)->registerValue() == *FallbackDestination, + "unsatisfied ties must receive a COPY immediately before the instruction"); + + const auto Copies = instructionsWithOpcode(Function, rcc::opcode(rcc::GenericOpcode::COPY)); + const auto ImplicitDefs = + instructionsWithOpcode(Function, rcc::opcode(rcc::GenericOpcode::IMPLICIT_DEF)); + Context.expect(Copies.size() >= 5 && ImplicitDefs.size() >= 2, + "subregister pseudo expansion must materialize base and lane definitions"); + Context.expect( + std::ranges::any_of(Copies, + [InsertDestination](const rcc::MachineInstr *Copy) { + return Copy->operandAt(0)->registerValue() == *InsertDestination && + Copy->operandAt(0)->subReg() == rcc::test::TestSubReg::Low32; + }), + "INSERT_SUBREG must copy its inserted value into the requested destination lane"); + const bool HasKill = std::ranges::any_of(Function.allInstructions(), [](const auto *MI) { + return MI->parent() != nullptr && + std::ranges::any_of(MI->operands(), [](const rcc::MachineOperand &Operand) { + return Operand.registerFlags().has(rcc::RegisterOperandFlag::Kill); + }); + }); + const bool HasDead = std::ranges::any_of(Function.allInstructions(), [](const auto *MI) { + return MI->parent() != nullptr && + std::ranges::any_of(MI->operands(), [](const rcc::MachineOperand &Operand) { + return Operand.registerFlags().has(rcc::RegisterOperandFlag::Dead); + }); + }); + Context.expect(HasKill && HasDead, + "two-address rewrite must rebuild precise kill and dead flags"); + Context.expect(rcc::MachineVerifier::verify(Function, Target).has_value(), + "two-address and subregister expansion result must verify"); + + auto AtomicFixture = createFunction(Context, "two_address_atomic", 10, 1); + if (!AtomicFixture.Function || AtomicFixture.Blocks.empty()) + return; + auto AtomicSource = createVReg(*AtomicFixture.Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + auto AtomicDestination = + createVReg(*AtomicFixture.Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + if (!AtomicSource || !AtomicDestination) + return; + const std::array AtomicSourceDef{*AtomicSource}; + Context.expect(buildRegisterInstruction(*AtomicFixture.Function, Target, *AtomicFixture.Blocks[0], + testOpcode(DefOpcode), AtomicSourceDef, {}) + .has_value(), + "two-address atomicity source definition must build"); + rcc::MachineIRBuilder AtomicBuilder(*AtomicFixture.Function, Target); + if (!AtomicBuilder.setInsertPoint(*AtomicFixture.Blocks[0])) + return; + auto AtomicPending = AtomicBuilder.buildInstr(testOpcode(TiedOpcode)); + auto AtomicInstruction = + AtomicPending && AtomicPending->addDef(*AtomicDestination) && + AtomicPending->addUse(*AtomicSource, {rcc::RegisterOperandFlag::Kill}) + ? AtomicPending->build() + : rcc::Expected<rcc::MachineInstr *>{rcc::makeUnexpected( + rcc::ErrorCode::InternalError, "failed to build atomicity fixture")}; + Context.expect(AtomicInstruction.has_value(), + "two-address atomicity tied instruction must build"); + if (!AtomicInstruction) + return; + auto AtomicLiveness = setSelectedSSAProperties(*AtomicFixture.Function, Target); + Context.expect(AtomicLiveness.has_value(), "two-address atomicity liveness rebuild must succeed"); + if (!AtomicLiveness) + return; + AtomicFixture.Function->properties().set(rcc::MachineFunctionProperty::NoPHIs); + const std::size_t AtomicInstructionsBefore = AtomicFixture.Function->allInstructions().size(); + const std::size_t AtomicRegistersBefore = + AtomicFixture.Function->registerInfo().virtualRegisters().size(); + const std::uint64_t AtomicPropertiesBefore = AtomicFixture.Function->properties().rawValue(); + + TestInstrInfo RejectingInfo(true); + rcc::MachineFunctionAnalysisManager AtomicAnalyses; + rcc::TwoAddressInstructionPass FailingPass(RejectingInfo); + auto Failed = FailingPass.run(*AtomicFixture.Function, Target, AtomicAnalyses); + Context.expect(!Failed && Failed.error().code() == rcc::ErrorCode::Unsupported, + "COPY rejection must produce a structured two-address failure"); + Context.expect( + AtomicFixture.Function->allInstructions().size() == AtomicInstructionsBefore && + AtomicFixture.Function->registerInfo().virtualRegisters().size() == + AtomicRegistersBefore && + AtomicFixture.Function->properties().rawValue() == AtomicPropertiesBefore && + (*AtomicInstruction)->operandAt(0)->registerValue() == *AtomicDestination && + (*AtomicInstruction)->operandAt(1)->registerValue() == *AtomicSource && + (*AtomicInstruction)->operandAt(1)->registerFlags().has(rcc::RegisterOperandFlag::Kill), + "failed two-address transaction preflight must leave the original function unchanged"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "two-address TestTarget must be created"); + if (!Target) + return Context.result(); + TestInstrInfo Info; + testTwoAddressAndSubregisterPseudos(Context, *Target, Info); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/VirtRegMapTest.cpp b/backend/unittests/CodeGen/VirtRegMapTest.cpp new file mode 100644 index 0000000..a3bdfc1 --- /dev/null +++ b/backend/unittests/CodeGen/VirtRegMapTest.cpp @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "CodeGen/RegisterAllocationTestSupport.h" +#include "rcc/CodeGen/LiveRangeEdit.h" + +#include <iostream> + +namespace { + +using namespace rcc; +using namespace rcc::test; +using namespace rcc::test::ra; + +void testSplitMetadata(TestContext &Context, const TargetDescription &Target) { + auto Fixture = createFunction("virt-map", 10); + if (!Fixture) + return; + auto Parent = createVReg(*Fixture->Function); + if (!Parent) + return; + VirtRegMap Mapping(*Fixture->Function); + LiveRangeEdit Edit(*Fixture->Function, Target, Mapping); + Context.expect(Mapping.add(*Parent).has_value(), "VirtRegMap must accept a known vreg"); + auto Slot = Edit.getOrCreateSpillSlot(*Parent); + auto Child = Edit.createFrom(*Parent, false); + Context.expect( + Slot && Child && Mapping.verify().has_value() && Mapping.stackSlot(*Child) == Slot && + Mapping.entry(*Child)->SplitParent == *Parent && Mapping.entry(*Child)->Origin == *Parent, + "LiveRangeEdit must preserve split origin and spill metadata"); + if (!Child) + return; + const PhysicalRegister G0 = physical(TestRegister::G0); + Context.expect(Mapping.assign(*Child, G0).has_value() && Mapping.physicalRegister(*Child) == G0 && + Mapping.entry(*Child)->State == VirtRegAllocationState::Assigned, + "VirtRegMap must transition a split child to an assigned state"); +} + +void testRewriteAndAtomicValidation(TestContext &Context, const TargetDescription &Target) { + auto Fixture = createFunction("virt-rewrite", 11); + if (!Fixture) + return; + auto Value = createVReg(*Fixture->Function); + if (!Value || + !buildDef(*Fixture->Function, Target, *Fixture->Block, *Value, 1, TestSubReg::Low32) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value, TestSubReg::Low32) || + !prepareForAllocation(*Fixture->Function, Target)) + return; + VirtRegMap Mapping(*Fixture->Function); + const PhysicalRegister G0 = physical(TestRegister::G0); + Context.expect(Mapping.add(*Value).has_value() && Mapping.assign(*Value, G0).has_value(), + "VirtRegMap must accept a physical assignment before rewrite"); + auto Rewritten = rewriteVirtualRegisters(*Fixture->Function, Target, Mapping); + const MachineOperand *Def = Fixture->Block->firstInstruction()->operandAt(0); + Context.expect( + Rewritten && *Rewritten && Def != nullptr && Def->registerValue() && + Def->registerValue() == Register::fromPhysical(physical(TestRegister::G0Low32)) && + Def->subReg().value() == 0 && + std::ranges::none_of(Fixture->Function->registerInfo().virtualRegisters(), + [](const VirtualRegisterInfo &Info) { return !Info.IsTombstone; }), + "VirtualRegisterRewrite must project and clear subregister operands"); + + auto InvalidFixture = createFunction("virt-rewrite-invalid", 12); + if (!InvalidFixture) + return; + auto Invalid = createVReg(*InvalidFixture->Function); + if (!Invalid || + !buildDef(*InvalidFixture->Function, Target, *InvalidFixture->Block, *Invalid, 2) || + !buildUse(*InvalidFixture->Function, Target, *InvalidFixture->Block, *Invalid) || + !prepareForAllocation(*InvalidFixture->Function, Target)) + return; + VirtRegMap InvalidMapping(*InvalidFixture->Function); + const PhysicalRegister F0 = physical(TestRegister::F0); + if (!InvalidMapping.add(*Invalid) || !InvalidMapping.assign(*Invalid, F0)) + return; + auto Failed = rewriteVirtualRegisters(*InvalidFixture->Function, Target, InvalidMapping); + Context.expect(!Failed && Failed.error().code() == ErrorCode::RegisterAllocationFailed, + "VirtualRegisterRewrite must reject class-incompatible mappings atomically"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "VirtRegMap test target must be constructible"); + if (!Target) + return Context.result(); + testSplitMetadata(Context, *Target); + testRewriteAndAtomicValidation(Context, *Target); + return Context.result(); +} |
