diff options
| author | Richard-Qin-X <richard.qin.001@gmail.com> | 2026-08-08 04:14:34 +0800 |
|---|---|---|
| committer | Richard-Qin-X <richard.qin.001@gmail.com> | 2026-08-08 04:14:34 +0800 |
| commit | 77884c967d1cbc48afcbaef7e5a35adcb3c5da1b (patch) | |
| tree | 8cc31c1c5115ed909ec9f8751964bfdc96fbd4ad /backend/unittests | |
| parent | 10b7e69448f866bcd6893b21d7f78d766252eb11 (diff) | |
| download | rcc-project-77884c967d1cbc48afcbaef7e5a35adcb3c5da1b.tar.gz rcc-project-77884c967d1cbc48afcbaef7e5a35adcb3c5da1b.tar.xz | |
[CodeGen] Implement machine SSA exit
Diffstat (limited to 'backend/unittests')
| -rw-r--r-- | backend/unittests/CodeGen/CMakeLists.txt | 11 | ||||
| -rw-r--r-- | backend/unittests/CodeGen/MachineAnalysesTest.cpp | 103 | ||||
| -rw-r--r-- | backend/unittests/CodeGen/MachineSSAExitTest.cpp | 1113 | ||||
| -rw-r--r-- | backend/unittests/CodeGen/MachineVerifierTest.cpp | 126 | ||||
| -rw-r--r-- | backend/unittests/Target/CMakeLists.txt | 7 | ||||
| -rw-r--r-- | backend/unittests/Target/RISCVSSAExitTest.cpp | 346 |
6 files changed, 1703 insertions, 3 deletions
diff --git a/backend/unittests/CodeGen/CMakeLists.txt b/backend/unittests/CodeGen/CMakeLists.txt index 19beb2f..c8ea9b8 100644 --- a/backend/unittests/CodeGen/CMakeLists.txt +++ b/backend/unittests/CodeGen/CMakeLists.txt @@ -21,6 +21,7 @@ set( MachineOperandTest.cpp MachinePassTest.cpp MachinePipelinePassesTest.cpp + MachineSSAExitTest.cpp MachineVerifierTest.cpp MachineVerifierTargetTest.cpp MachineValueTypesTest.cpp @@ -273,6 +274,14 @@ target_link_libraries( add_test(NAME RCCBackend.CodeGen.MachineAnalyses COMMAND RCCBackendCodeGenMachineAnalysesTests) +add_executable(RCCBackendCodeGenMachineSSAExitTests MachineSSAExitTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineSSAExitTests) +target_link_libraries( + RCCBackendCodeGenMachineSSAExitTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test(NAME RCCBackend.CodeGen.MachineSSAExit COMMAND RCCBackendCodeGenMachineSSAExitTests) + add_executable(RCCBackendCodeGenTargetConstantTests TargetConstantTest.cpp) rcc_backend_configure_target(RCCBackendCodeGenTargetConstantTests) target_link_libraries(RCCBackendCodeGenTargetConstantTests PRIVATE RCCBackend::CodeGen) @@ -350,6 +359,7 @@ rcc_backend_register_format_files( "${CMAKE_CURRENT_SOURCE_DIR}/MachineOperandTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MachinePassTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MachinePipelinePassesTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineSSAExitTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MachineVerifierTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MachineVerifierTargetTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MachineValueTypesTest.cpp" @@ -379,6 +389,7 @@ rcc_backend_register_tidy_files( "${CMAKE_CURRENT_SOURCE_DIR}/MachineOperandTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MachinePassTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MachinePipelinePassesTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineSSAExitTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MachineVerifierTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MachineVerifierTargetTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MachineValueTypesTest.cpp" diff --git a/backend/unittests/CodeGen/MachineAnalysesTest.cpp b/backend/unittests/CodeGen/MachineAnalysesTest.cpp index 23c5e19..1c9f255 100644 --- a/backend/unittests/CodeGen/MachineAnalysesTest.cpp +++ b/backend/unittests/CodeGen/MachineAnalysesTest.cpp @@ -458,6 +458,108 @@ void testCFGUpdaterPHIs(TestContext &Context, const rcc::TargetDescription &Targ "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::LLT::makeInteger(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() { @@ -472,6 +574,7 @@ int main() { testAliasAnalysis(Context); testMemoryDependence(Context, *Target); testCFGUpdaterPHIs(Context, *Target); + testCFGUpdaterFailureAtomicity(Context, *Target); testAtomicStandardRegistration(Context); return Context.result(); } diff --git a/backend/unittests/CodeGen/MachineSSAExitTest.cpp b/backend/unittests/CodeGen/MachineSSAExitTest.cpp new file mode 100644 index 0000000..7c65345 --- /dev/null +++ b/backend/unittests/CodeGen/MachineSSAExitTest.cpp @@ -0,0 +1,1113 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineSSAExit.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/MachinePass.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 <optional> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +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]] rcc::Opcode testOpcode(std::uint32_t Value) { + return rcc::Opcode::fromValue(Value); +} + +[[nodiscard]] 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]] rcc::OperandDescriptor blockOperand() { + rcc::OperandDescriptor Result; + Result.Kinds = {rcc::MachineOperand::Kind::BasicBlock}; + return Result; +} + +[[nodiscard]] 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]] 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 TestSSAInfo final : public rcc::TargetSSAInfo { +public: + explicit TestSSAInfo(bool RejectCopies = false) : RejectCopies(RejectCopies) {} + + [[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<void> + insertUnconditionalBranch(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 {}; + } + + [[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; + } + +private: + bool RejectCopies = false; +}; + +struct FunctionFixture final { + std::unique_ptr<rcc::MachineFunction> Function; + std::vector<rcc::MachineBasicBlock *> Blocks; +}; + +[[nodiscard]] 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]] rcc::Expected<rcc::Register> +createVReg(rcc::MachineFunction &Function, rcc::LLT 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]] 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]] 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(); +} + +void setSelectedSSAProperties(rcc::MachineFunction &Function) { + for (rcc::MachineFunctionProperty Property : + {rcc::MachineFunctionProperty::IsSSA, rcc::MachineFunctionProperty::TracksLiveness, + rcc::MachineFunctionProperty::Legalized, rcc::MachineFunctionProperty::RegBankSelected, + rcc::MachineFunctionProperty::Selected}) { + Function.properties().set(Property); + } +} + +[[nodiscard]] rcc::Expected<void> runPHIElimination(rcc::MachineFunction &Function, + const rcc::TargetDescription &Target, + const rcc::TargetSSAInfo &Info) { + rcc::MachinePassManager Pipeline; + auto Added = Pipeline.addPass(std::make_unique<rcc::PHIEliminationPass>(Info)); + if (!Added) + return std::unexpected(Added.error()); + rcc::MachineAnalysisManager 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]] 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; +} + +[[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 TestSSAInfo &Info) { + auto Fixture = createFunction(Context, "parallel_phis", 1, 3); + if (!Fixture.Function || Fixture.Blocks.size() != 3) + return; + auto I64 = rcc::LLT::makeInteger(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"); + setSelectedSSAProperties(Function); + + 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 LLT"); + 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 TestSSAInfo &Info) { + auto Fixture = createFunction(Context, "critical_edge", 2, 4); + if (!Fixture.Function || Fixture.Blocks.size() != 4) + return; + auto I64 = rcc::LLT::makeInteger(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"); + setSelectedSSAProperties(Function); + + 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 TestSSAInfo &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::LLT::makeInteger(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; + } + setSelectedSSAProperties(*Fixture.Function); + 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(); + TestSSAInfo RejectingInfo(true); + rcc::MachineAnalysisManager 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"); + + rcc::PHIEliminationPass MissingBehavior; + auto Missing = MissingBehavior.run(*Atomic.Function, Target, Analyses); + Context.expect(!Missing && Missing.error().code() == rcc::ErrorCode::InvalidArgument, + "PHI elimination without target behavior must fail explicitly"); +} + +void testTwoAddressAndSubregisterPseudos(TestContext &Context, const rcc::TargetDescription &Target, + const TestSSAInfo &Info) { + auto Fixture = createFunction(Context, "two_address", 5, 1); + if (!Fixture.Function || Fixture.Blocks.empty()) + return; + auto I64 = rcc::LLT::makeInteger(64); + auto I32 = rcc::LLT::makeInteger(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"); + + setSelectedSSAProperties(Function); + Function.properties().set(rcc::MachineFunctionProperty::NoPHIs); + rcc::MachinePassManager Pipeline; + Context.expect(Pipeline.addPass(std::make_unique<rcc::TwoAddressRewritePass>(Info)).has_value(), + "two-address pass must be registered"); + rcc::MachineAnalysisManager 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(), + "TwoAddressRewrite 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"); + Context.expect( + std::ranges::all_of(Function.allInstructions(), + [](const rcc::MachineInstr *MI) { + if (MI->parent() == nullptr) + return true; + return std::ranges::none_of( + MI->operands(), [](const rcc::MachineOperand &Op) { + return Op.registerFlags().has(rcc::RegisterOperandFlag::Kill) || + Op.registerFlags().has(rcc::RegisterOperandFlag::Dead); + }); + }), + "two-address rewrite must conservatively clear stale kill/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; + setSelectedSSAProperties(*AtomicFixture.Function); + 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(); + + TestSSAInfo RejectingInfo(true); + rcc::MachineAnalysisManager AtomicAnalyses; + rcc::TwoAddressRewritePass 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"); + + rcc::TwoAddressRewritePass MissingBehavior; + auto Missing = MissingBehavior.run(*AtomicFixture.Function, Target, AtomicAnalyses); + Context.expect(!Missing && Missing.error().code() == rcc::ErrorCode::InvalidArgument, + "two-address rewrite without target behavior must fail explicitly"); +} + +void testVerifierAndPseudoFailures(TestContext &Context, const rcc::TargetDescription &Target) { + auto BuildBadPhi = [&](std::uint64_t ID, rcc::LLT DestinationType, + rcc::RegisterBankID DestinationBank, rcc::RegisterClassID DestinationClass, + rcc::LLT 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::LLT::makeInteger(64); + auto I32 = rcc::LLT::makeInteger(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"); + + auto Residual = createFunction(Context, "residual_stage10_pseudo", 9, 1); + if (!Residual.Function || Residual.Blocks.empty()) + return; + auto Destination = createVReg(*Residual.Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + auto Source = createVReg(*Residual.Function, *I32, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR32); + if (!Destination || !Source) + return; + rcc::MachineIRBuilder Builder(*Residual.Function, Target); + if (!Builder.setInsertPoint(*Residual.Blocks[0])) + return; + auto Sequence = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::REG_SEQUENCE)); + Context.expect(Sequence && Sequence->addDef(*Destination) && Sequence->addUse(*Source) && + Sequence->addImmediate(rcc::test::TestSubReg::Low32.value()) && + Sequence->build(), + "residual Stage 10 pseudo fixture must be built"); + for (rcc::MachineFunctionProperty Property : + {rcc::MachineFunctionProperty::Legalized, rcc::MachineFunctionProperty::RegBankSelected, + rcc::MachineFunctionProperty::Selected, rcc::MachineFunctionProperty::NoPHIs, + rcc::MachineFunctionProperty::TiedOpsRewritten}) { + Residual.Function->properties().set(Property); + } + Context.expect(!rcc::MachineVerifier::verify(*Residual.Function, Target), + "TiedOpsRewritten must reject residual REG_SEQUENCE/INSERT_SUBREG pseudos"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "SSA-exit TestTarget must be created"); + if (!Target) + return Context.result(); + TestSSAInfo Info; + testParallelPHIsAndPlacement(Context, *Target, Info); + testCriticalEdgeAndPhysicalLiveIn(Context, *Target, Info); + testUndefAndFailureAtomicity(Context, *Target, Info); + testTwoAddressAndSubregisterPseudos(Context, *Target, Info); + testVerifierAndPseudoFailures(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineVerifierTest.cpp b/backend/unittests/CodeGen/MachineVerifierTest.cpp index ca265a7..528ff3b 100644 --- a/backend/unittests/CodeGen/MachineVerifierTest.cpp +++ b/backend/unittests/CodeGen/MachineVerifierTest.cpp @@ -102,7 +102,15 @@ private: 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; @@ -302,6 +310,103 @@ void testReachableStructuralFailures(TestContext &Context) { } } +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::LLT::makeInteger(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) @@ -428,7 +533,8 @@ void testSelectedStateAndPseudoDeadline(TestContext &Context) { "tied-state instruction must build"); for (const rcc::MachineFunctionProperty Property : {rcc::MachineFunctionProperty::Legalized, rcc::MachineFunctionProperty::RegBankSelected, - rcc::MachineFunctionProperty::Selected, rcc::MachineFunctionProperty::TiedOpsRewritten}) { + rcc::MachineFunctionProperty::Selected, rcc::MachineFunctionProperty::NoPHIs, + rcc::MachineFunctionProperty::TiedOpsRewritten}) { Tied->properties().set(Property); } Context.expect(!rcc::MachineVerifier::verify(*Tied, *Target), @@ -470,6 +576,20 @@ void testSelectedStateAndPseudoDeadline(TestContext &Context) { 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) { @@ -523,8 +643,7 @@ void testTableReferenceFailures(TestContext &Context) { rcc::MachineConstantPoolEntryDesc ConstantDescription; auto Integer = rcc::IntegerImmediate::fromUnsigned(32, 0x01010101U); - ConstantDescription.Value = - std::move(*rcc::GenericConstantData::integer(std::move(*Integer))); + 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)); @@ -609,6 +728,7 @@ int main() { TestContext Context; testValidFunctionAndModes(Context); testReachableStructuralFailures(Context); + testPHIStructure(Context); testPropertyFailures(Context); testSelectedStateAndPseudoDeadline(Context); testFrameStateFailures(Context); diff --git a/backend/unittests/Target/CMakeLists.txt b/backend/unittests/Target/CMakeLists.txt index 98c4426..0bffb47 100644 --- a/backend/unittests/Target/CMakeLists.txt +++ b/backend/unittests/Target/CMakeLists.txt @@ -33,11 +33,17 @@ rcc_backend_configure_target(RCCBackendRISCVCodeGenTests) target_link_libraries(RCCBackendRISCVCodeGenTests PRIVATE RCCBackend::RISCV RCCBackend::CodeGen) add_test(NAME RCCBackendRISCVCodeGenTests COMMAND RCCBackendRISCVCodeGenTests) +add_executable(RCCBackendRISCVSSAExitTests RISCVSSAExitTest.cpp) +rcc_backend_configure_target(RCCBackendRISCVSSAExitTests) +target_link_libraries(RCCBackendRISCVSSAExitTests PRIVATE RCCBackend::RISCV RCCBackend::CodeGen) +add_test(NAME RCCBackendRISCVSSAExitTests COMMAND RCCBackendRISCVSSAExitTests) + rcc_backend_register_format_files( "${CMAKE_CURRENT_SOURCE_DIR}/DescriptionGeneratorTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/RISCVCallLoweringTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/RISCVTargetTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/RISCVCodeGenTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/RISCVSSAExitTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/TestTarget.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/TestTarget.h" ) @@ -46,5 +52,6 @@ rcc_backend_register_tidy_files( "${CMAKE_CURRENT_SOURCE_DIR}/RISCVCallLoweringTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/RISCVTargetTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/RISCVCodeGenTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/RISCVSSAExitTest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/TestTarget.cpp" ) diff --git a/backend/unittests/Target/RISCVSSAExitTest.cpp b/backend/unittests/Target/RISCVSSAExitTest.cpp new file mode 100644 index 0000000..1440855 --- /dev/null +++ b/backend/unittests/Target/RISCVSSAExitTest.cpp @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#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/MachinePass.h" +#include "rcc/CodeGen/MachineSSAExit.h" +#include "rcc/CodeGen/MachineVerifier.h" +#include "rcc/Target/RISCV/RISCVCodeGen.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +#include <algorithm> +#include <array> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#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::Opcode targetOpcode(rcc::riscv::InstructionOpcode Opcode) { + return rcc::Opcode::fromValue(static_cast<std::uint32_t>(Opcode)); +} + +[[nodiscard]] rcc::RegisterBankID bank(rcc::riscv::RegisterBank Bank) { + return rcc::RegisterBankID::fromValue(static_cast<std::uint32_t>(Bank)); +} + +[[nodiscard]] rcc::RegisterClassID regClass(rcc::riscv::RegisterClass Class) { + return rcc::RegisterClassID::fromValue(static_cast<std::uint32_t>(Class)); +} + +[[nodiscard]] rcc::Expected<rcc::Register> createVReg(rcc::MachineFunction &Function, rcc::LLT Type, + rcc::riscv::RegisterBank Bank, + rcc::riscv::RegisterClass Class) { + auto Reg = Function.registerInfo().createVirtualRegister(Type); + if (!Reg) + return std::unexpected(Reg.error()); + auto BankResult = Function.registerInfo().setRegisterBank(*Reg, bank(Bank)); + if (!BankResult) + return std::unexpected(BankResult.error()); + auto ClassResult = Function.registerInfo().setRegisterClass(*Reg, regClass(Class)); + if (!ClassResult) + return std::unexpected(ClassResult.error()); + return *Reg; +} + +[[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; +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> +buildAddImmediate(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Register Destination, rcc::Register Source, + std::int64_t Immediate) { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = Builder.setInsertPoint(Block); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(targetOpcode(rcc::riscv::InstructionOpcode::ADDI)); + if (!Pending) + return std::unexpected(Pending.error()); + auto Def = Pending->addDef(Destination); + if (!Def) + return std::unexpected(Def.error()); + auto Use = Pending->addUse(Source); + if (!Use) + return std::unexpected(Use.error()); + auto Value = Pending->addImmediate(Immediate); + if (!Value) + return std::unexpected(Value.error()); + return Pending->build(); +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> +buildBranchEqual(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Register Left, rcc::Register Right, + rcc::MachineBasicBlock &Destination) { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = Builder.setInsertPoint(Block); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(targetOpcode(rcc::riscv::InstructionOpcode::BEQ)); + if (!Pending) + return std::unexpected(Pending.error()); + auto LeftUse = Pending->addUse(Left); + if (!LeftUse) + return std::unexpected(LeftUse.error()); + auto RightUse = Pending->addUse(Right); + if (!RightUse) + return std::unexpected(RightUse.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, + const std::array<std::pair<rcc::Register, rcc::MachineBasicBlock *>, 2> &Incoming) { + 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); + if (!Def) + return std::unexpected(Def.error()); + for (const auto &[Value, Predecessor] : Incoming) { + auto Use = Pending->addUse(Value); + if (!Use) + return std::unexpected(Use.error()); + auto IncomingBlock = Pending->addBasicBlock(*Predecessor); + if (!IncomingBlock) + return std::unexpected(IncomingBlock.error()); + } + return Pending->build(); +} + +[[nodiscard]] const rcc::BasicBlockOperand *blockOperand(const rcc::MachineInstr &Instruction, + std::size_t Index) { + const rcc::MachineOperand *Operand = Instruction.operandAt(Index); + return Operand == nullptr ? nullptr : Operand->getIf<rcc::BasicBlockOperand>(); +} + +[[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; +} + +void setSelectedSSAProperties(rcc::MachineFunction &Function) { + for (rcc::MachineFunctionProperty Property : + {rcc::MachineFunctionProperty::IsSSA, rcc::MachineFunctionProperty::TracksLiveness, + rcc::MachineFunctionProperty::Legalized, rcc::MachineFunctionProperty::RegBankSelected, + rcc::MachineFunctionProperty::Selected}) { + Function.properties().set(Property); + } +} + +void testBehaviorAndCopyLegality(TestContext &Context, const rcc::TargetDescription &Target) { + auto Function = rcc::MachineFunction::create("riscv_ssa_behavior", + rcc::MachineFunctionStableID::fromValue(1)); + if (!Function) + return; + auto First = appendBlock(**Function); + auto Second = appendBlock(**Function); + auto Third = appendBlock(**Function); + if (!First || !Second || !Third) + return; + auto CFG = (*Function)->cfgUpdater(); + if (!CFG.addSuccessor(**First, **Second, rcc::BranchProbability::one())) + return; + + auto Fallthrough = rcc::riscv::ssaInfo().isFallthroughEdge(**First, **Second, **Function, Target); + Context.expect(Fallthrough && *Fallthrough, + "RV64GC SSA behavior must recognize an empty layout fallthrough edge"); + auto Inserted = + rcc::riscv::ssaInfo().insertUnconditionalBranch(**First, **Second, **Function, Target); + Context.expect(Inserted.has_value(), "RV64GC SSA behavior must insert an unconditional branch"); + if (!Inserted) + return; + rcc::MachineInstr *Jump = (*First)->lastInstruction(); + const auto X0 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X0); + Context.expect(Jump != nullptr && + Jump->opcode() == targetOpcode(rcc::riscv::InstructionOpcode::JAL) && + Jump->operandAt(0)->registerValue() == rcc::Register::fromPhysical(*X0) && + blockOperand(*Jump, 1) != nullptr && blockOperand(*Jump, 1)->Block == *Second, + "RV64GC unconditional branches must use JAL with x0 as the link destination"); + auto NoFallthrough = + rcc::riscv::ssaInfo().isFallthroughEdge(**First, **Second, **Function, Target); + Context.expect(NoFallthrough && !*NoFallthrough, "JAL must terminate RV64GC layout fallthrough"); + auto Rewritten = + rcc::riscv::ssaInfo().rewriteBranchTarget(**First, **Second, **Third, **Function, Target); + Context.expect(Rewritten && *Rewritten && blockOperand(*Jump, 1)->Block == *Third, + "RV64GC branch target rewriting must update direct JAL operands"); + + auto I64 = rcc::LLT::makeInteger(64); + auto F64 = rcc::LLT::makeFloat(rcc::FloatSemantics::IEEEDouble); + auto F32 = rcc::LLT::makeFloat(rcc::FloatSemantics::IEEESingle); + if (!I64 || !F64 || !F32) + return; + auto GPR = + createVReg(**Function, *I64, rcc::riscv::RegisterBank::GPR, rcc::riscv::RegisterClass::GPR); + auto OtherGPR = + createVReg(**Function, *I64, rcc::riscv::RegisterBank::GPR, rcc::riscv::RegisterClass::GPR); + auto FPR64 = + createVReg(**Function, *F64, rcc::riscv::RegisterBank::FPR, rcc::riscv::RegisterClass::FPR64); + auto FPR32 = + createVReg(**Function, *F32, rcc::riscv::RegisterBank::FPR, rcc::riscv::RegisterClass::FPR32); + auto Unclassified = (*Function)->registerInfo().createVirtualRegister(*I64); + if (!GPR || !OtherGPR || !FPR64 || !FPR32 || !Unclassified) + return; + + Context.expect( + rcc::riscv::ssaInfo().validateCopy(*GPR, {}, *OtherGPR, {}, **Function, Target).has_value(), + "RV64GC must allow a compatible GPR COPY"); + Context.expect( + rcc::riscv::ssaInfo().validateCopy(*GPR, {}, *FPR64, {}, **Function, Target).has_value(), + "RV64GC must allow supported 64-bit GPR/FPR bit moves"); + Context.expect(!rcc::riscv::ssaInfo().validateCopy(*FPR64, {}, *FPR32, {}, **Function, Target), + "RV64GC must reject incompatible same-bank COPY widths"); + Context.expect(!rcc::riscv::ssaInfo().validateCopy(*GPR, rcc::SubRegIndex::fromValue(1), + *OtherGPR, {}, **Function, Target), + "RV64GC must reject unmodeled subregister COPYs"); + Context.expect( + !rcc::riscv::ssaInfo().validateCopy(*GPR, {}, *Unclassified, {}, **Function, Target), + "RV64GC must reject COPYs involving an unconstrained virtual register"); +} + +void testRV64GCCriticalEdge(TestContext &Context, const rcc::TargetDescription &Target) { + auto Function = rcc::MachineFunction::create("riscv_ssa_critical_edge", + rcc::MachineFunctionStableID::fromValue(2)); + if (!Function) + return; + auto From = appendBlock(**Function); + auto Side = appendBlock(**Function); + auto Other = appendBlock(**Function); + auto Merge = appendBlock(**Function); + if (!From || !Side || !Other || !Merge) + return; + auto I64 = rcc::LLT::makeInteger(64); + auto X0 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X0); + if (!I64 || !X0) + return; + auto FromValue = + createVReg(**Function, *I64, rcc::riscv::RegisterBank::GPR, rcc::riscv::RegisterClass::GPR); + auto OtherValue = + createVReg(**Function, *I64, rcc::riscv::RegisterBank::GPR, rcc::riscv::RegisterClass::GPR); + auto Destination = + createVReg(**Function, *I64, rcc::riscv::RegisterBank::GPR, rcc::riscv::RegisterClass::GPR); + if (!FromValue || !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()), + "RV64GC critical-edge CFG must be connected"); + const rcc::Register Zero = rcc::Register::fromPhysical(*X0); + Context.expect(buildAddImmediate(**Function, Target, **From, *FromValue, Zero, 1) && + buildBranchEqual(**Function, Target, **From, Zero, Zero, **Merge) && + buildAddImmediate(**Function, Target, **Other, *OtherValue, Zero, 2), + "RV64GC critical-edge instructions must be built"); + const std::array Incoming{ + std::pair<rcc::Register, rcc::MachineBasicBlock *>{*FromValue, *From}, + std::pair<rcc::Register, rcc::MachineBasicBlock *>{*OtherValue, *Other}}; + Context.expect(buildPhi(**Function, Target, **Merge, *Destination, Incoming).has_value(), + "RV64GC critical-edge PHI must be built"); + setSelectedSSAProperties(**Function); + + rcc::MachinePassManager Pipeline; + Context.expect(Pipeline.addPass(std::make_unique<rcc::PHIEliminationPass>(rcc::riscv::ssaInfo())) + .has_value(), + "RV64GC PHI-elimination pass must be registered"); + rcc::MachineAnalysisManager Analyses; + auto Run = Pipeline.run(**Function, Target, Analyses); + Context.expect(Run.has_value(), "RV64GC critical-edge PHI elimination must succeed"); + if (!Run) + return; + + rcc::MachineBasicBlock *Edge = (*Merge)->previous(); + Context.expect(Edge != nullptr && Edge != *Other && Edge->previous() == *Other, + "RV64GC critical edge must receive a dedicated layout block"); + if (Edge == nullptr || Edge == *Other) + return; + rcc::MachineInstr *Conditional = (*From)->lastInstruction(); + Context.expect(Conditional != nullptr && + Conditional->opcode() == targetOpcode(rcc::riscv::InstructionOpcode::BEQ) && + blockOperand(*Conditional, 2) != nullptr && + blockOperand(*Conditional, 2)->Block == Edge, + "RV64GC BEQ must be redirected to the critical-edge block"); + + const auto OtherInstructions = linkedInstructions(**Other); + const auto EdgeInstructions = linkedInstructions(*Edge); + Context.expect(OtherInstructions.size() == 3 && + OtherInstructions[1]->opcode() == rcc::opcode(rcc::GenericOpcode::COPY) && + OtherInstructions[2]->opcode() == + targetOpcode(rcc::riscv::InstructionOpcode::JAL) && + blockOperand(*OtherInstructions[2], 1)->Block == *Merge, + "RV64GC must materialize an unrelated fallthrough before edge-block insertion"); + Context.expect(EdgeInstructions.size() == 2 && + EdgeInstructions[0]->opcode() == rcc::opcode(rcc::GenericOpcode::COPY) && + EdgeInstructions[1]->opcode() == + targetOpcode(rcc::riscv::InstructionOpcode::JAL) && + EdgeInstructions[1]->operandAt(0)->registerValue() == Zero && + blockOperand(*EdgeInstructions[1], 1)->Block == *Merge, + "RV64GC edge block must transfer the PHI value and jump to the merge"); + Context.expect((*Function)->properties().has(rcc::MachineFunctionProperty::NoPHIs) && + !(*Function)->properties().has(rcc::MachineFunctionProperty::IsSSA), + "RV64GC PHI elimination must publish SSA-exit properties"); + Context.expect(rcc::MachineVerifier::verify(**Function, Target).has_value(), + "RV64GC SSA-exit result must pass MachineVerifier"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = rcc::riscv::createTargetDescription(); + Context.expect(Target.has_value(), "RV64GC target must be created for SSA-exit tests"); + if (!Target) + return Context.result(); + testBehaviorAndCopyLegality(Context, *Target); + testRV64GCCriticalEdge(Context, *Target); + return Context.result(); +} |
