aboutsummaryrefslogtreecommitdiff
path: root/backend/unittests/CodeGen
diff options
context:
space:
mode:
Diffstat (limited to 'backend/unittests/CodeGen')
-rw-r--r--backend/unittests/CodeGen/CMakeLists.txt545
-rw-r--r--backend/unittests/CodeGen/CompilationUnitTest.cpp104
-rw-r--r--backend/unittests/CodeGen/CoreTypesTest.cpp224
-rw-r--r--backend/unittests/CodeGen/GenericOpcodesTest.cpp748
-rw-r--r--backend/unittests/CodeGen/GlobalISel/CMakeLists.txt40
-rw-r--r--backend/unittests/CodeGen/GlobalISel/GISelMITest.h115
-rw-r--r--backend/unittests/CodeGen/GlobalISel/InstructionSelectTest.cpp364
-rw-r--r--backend/unittests/CodeGen/GlobalISel/LegalizerTest.cpp174
-rw-r--r--backend/unittests/CodeGen/GlobalISel/RegBankSelectTest.cpp89
-rw-r--r--backend/unittests/CodeGen/InstructionDescriptorTest.cpp368
-rw-r--r--backend/unittests/CodeGen/LiveIntervalTest.cpp116
-rw-r--r--backend/unittests/CodeGen/LiveIntervalsTest.cpp265
-rw-r--r--backend/unittests/CodeGen/LivePhysRegsTest.cpp56
-rw-r--r--backend/unittests/CodeGen/LiveRegMatrixTest.cpp92
-rw-r--r--backend/unittests/CodeGen/LiveRegUnitsTest.cpp37
-rw-r--r--backend/unittests/CodeGen/LiveTestSupport.h264
-rw-r--r--backend/unittests/CodeGen/LiveVariablesTest.cpp320
-rw-r--r--backend/unittests/CodeGen/MachineAnalysesTest.cpp582
-rw-r--r--backend/unittests/CodeGen/MachineCompilationStatusTest.cpp164
-rw-r--r--backend/unittests/CodeGen/MachineConstantPoolTest.cpp260
-rw-r--r--backend/unittests/CodeGen/MachineDebugInfoTest.cpp518
-rw-r--r--backend/unittests/CodeGen/MachineFrameInfoTest.cpp191
-rw-r--r--backend/unittests/CodeGen/MachineFunctionClonerTest.cpp846
-rw-r--r--backend/unittests/CodeGen/MachineFunctionRecordsTest.cpp362
-rw-r--r--backend/unittests/CodeGen/MachineIRAttributesTest.cpp206
-rw-r--r--backend/unittests/CodeGen/MachineIRBuilderTest.cpp556
-rw-r--r--backend/unittests/CodeGen/MachineIRCoreTest.cpp484
-rw-r--r--backend/unittests/CodeGen/MachineJumpTableInfoTest.cpp173
-rw-r--r--backend/unittests/CodeGen/MachineMetadataTest.cpp211
-rw-r--r--backend/unittests/CodeGen/MachineOperandTest.cpp524
-rw-r--r--backend/unittests/CodeGen/MachineOptimizationTest.cpp798
-rw-r--r--backend/unittests/CodeGen/MachinePassManagerTest.cpp426
-rw-r--r--backend/unittests/CodeGen/MachineSchedulingTest.cpp518
-rw-r--r--backend/unittests/CodeGen/MachineValueTypesTest.cpp227
-rw-r--r--backend/unittests/CodeGen/MachineVerifierTargetTest.cpp485
-rw-r--r--backend/unittests/CodeGen/MachineVerifierTest.cpp738
-rw-r--r--backend/unittests/CodeGen/PBQPTest.cpp177
-rw-r--r--backend/unittests/CodeGen/PHIEliminationTest.cpp479
-rw-r--r--backend/unittests/CodeGen/PassInstrumentationTest.cpp173
-rw-r--r--backend/unittests/CodeGen/PassManagerTest.cpp260
-rw-r--r--backend/unittests/CodeGen/RegAllocBasicTest.cpp108
-rw-r--r--backend/unittests/CodeGen/RegAllocFastTest.cpp83
-rw-r--r--backend/unittests/CodeGen/RegAllocPBQPTest.cpp117
-rw-r--r--backend/unittests/CodeGen/RegisterAllocationTest.cpp1044
-rw-r--r--backend/unittests/CodeGen/RegisterAllocationTestSupport.h474
-rw-r--r--backend/unittests/CodeGen/SSAExitTestSupport.h444
-rw-r--r--backend/unittests/CodeGen/SlotIndexesTest.cpp93
-rw-r--r--backend/unittests/CodeGen/SpillerTest.cpp94
-rw-r--r--backend/unittests/CodeGen/TargetCallLoweringTest.cpp328
-rw-r--r--backend/unittests/CodeGen/TargetConstantTest.cpp278
-rw-r--r--backend/unittests/CodeGen/TargetFunctionStateTest.cpp255
-rw-r--r--backend/unittests/CodeGen/TargetMIRTextInfoTest.cpp374
-rw-r--r--backend/unittests/CodeGen/TargetRegisterInfoTest.cpp378
-rw-r--r--backend/unittests/CodeGen/TwoAddressInstructionPassTest.cpp268
-rw-r--r--backend/unittests/CodeGen/VirtRegMapTest.cpp91
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();
+}