aboutsummaryrefslogtreecommitdiff
path: root/backend/unittests
diff options
context:
space:
mode:
Diffstat (limited to 'backend/unittests')
-rw-r--r--backend/unittests/CodeGen/CMakeLists.txt14
-rw-r--r--backend/unittests/CodeGen/MachinePipelinePassesTest.cpp629
-rw-r--r--backend/unittests/Target/CMakeLists.txt7
-rw-r--r--backend/unittests/Target/RISCVCodeGenTest.cpp1108
4 files changed, 1758 insertions, 0 deletions
diff --git a/backend/unittests/CodeGen/CMakeLists.txt b/backend/unittests/CodeGen/CMakeLists.txt
index deb7a75..19beb2f 100644
--- a/backend/unittests/CodeGen/CMakeLists.txt
+++ b/backend/unittests/CodeGen/CMakeLists.txt
@@ -20,6 +20,7 @@ set(
MachineMetadataTest.cpp
MachineOperandTest.cpp
MachinePassTest.cpp
+ MachinePipelinePassesTest.cpp
MachineVerifierTest.cpp
MachineVerifierTargetTest.cpp
MachineValueTypesTest.cpp
@@ -252,6 +253,17 @@ target_link_libraries(
add_test(NAME RCCBackend.CodeGen.MachinePass COMMAND RCCBackendCodeGenMachinePassTests)
+add_executable(RCCBackendCodeGenMachinePipelinePassesTests MachinePipelinePassesTest.cpp)
+rcc_backend_configure_target(RCCBackendCodeGenMachinePipelinePassesTests)
+target_link_libraries(
+ RCCBackendCodeGenMachinePipelinePassesTests
+ PRIVATE RCCBackend::CodeGen RCCBackend::RISCV RCCBackend::TestTarget
+)
+add_test(
+ NAME RCCBackend.CodeGen.MachinePipelinePasses
+ COMMAND RCCBackendCodeGenMachinePipelinePassesTests
+)
+
add_executable(RCCBackendCodeGenMachineAnalysesTests MachineAnalysesTest.cpp)
rcc_backend_configure_target(RCCBackendCodeGenMachineAnalysesTests)
target_link_libraries(
@@ -337,6 +349,7 @@ rcc_backend_register_format_files(
"${CMAKE_CURRENT_SOURCE_DIR}/MachineMetadataTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineOperandTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachinePassTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/MachinePipelinePassesTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineVerifierTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineVerifierTargetTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineValueTypesTest.cpp"
@@ -365,6 +378,7 @@ rcc_backend_register_tidy_files(
"${CMAKE_CURRENT_SOURCE_DIR}/MachineMetadataTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachineOperandTest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MachinePassTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/MachinePipelinePassesTest.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/MachinePipelinePassesTest.cpp b/backend/unittests/CodeGen/MachinePipelinePassesTest.cpp
new file mode 100644
index 0000000..0fe2e80
--- /dev/null
+++ b/backend/unittests/CodeGen/MachinePipelinePassesTest.cpp
@@ -0,0 +1,629 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/CodeGen/MachinePipelinePasses.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/MachineMemOperand.h"
+#include "rcc/CodeGen/MachineRegisterInfo.h"
+#include "rcc/Target/RISCV/RISCVCodeGen.h"
+#include "rcc/Target/RISCV/RISCVTarget.h"
+
+#include <algorithm>
+#include <array>
+#include <cstdint>
+#include <iostream>
+#include <memory>
+#include <ranges>
+#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 FunctionFixture final {
+ std::unique_ptr<rcc::MachineFunction> Function;
+ rcc::MachineBasicBlock *Entry = nullptr;
+};
+
+class TogglingLegalizer final : public rcc::TargetLegalizerInfo {
+public:
+ [[nodiscard]] rcc::Expected<rcc::LegalizationAction>
+ classify(const rcc::MachineInstr &, const rcc::MachineFunction &,
+ const rcc::TargetDescription &) const override {
+ return rcc::LegalizationAction::Lower;
+ }
+
+ [[nodiscard]] rcc::Expected<void> lower(rcc::MachineInstr &Instruction, rcc::MachineFunction &,
+ const rcc::TargetDescription &,
+ rcc::LegalizationAction) const override {
+ const auto Generic = static_cast<rcc::GenericOpcode>(Instruction.opcode().value());
+ return Instruction.setOpcode(rcc::opcode(Generic == rcc::GenericOpcode::G_ADD
+ ? rcc::GenericOpcode::G_SUB
+ : rcc::GenericOpcode::G_ADD));
+ }
+};
+
+class MutatingFailingSelector final : public rcc::TargetInstructionSelector {
+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::TargetInstructionSelector {
+public:
+ explicit RecordingSelector(const rcc::TargetInstructionSelector &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::TargetInstructionSelector *Delegate;
+ mutable std::vector<std::uint64_t> SelectedIDs;
+};
+
+class TwoStepSelector final : public rcc::TargetInstructionSelector {
+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;
+};
+
+[[nodiscard]] 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]] rcc::Register createRegister(TestContext &Context, rcc::MachineFunction &Function,
+ std::uint32_t Bits) {
+ auto Type = rcc::LLT::makeInteger(Bits);
+ Context.expect(Type.has_value(), "integer LLT 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]] rcc::Register createFloatRegister(TestContext &Context,
+ rcc::MachineFunction &Function) {
+ auto Type = rcc::LLT::makeFloat(rcc::FloatSemantics::IEEEDouble);
+ Context.expect(Type.has_value(), "floating LLT 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]] 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]] 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]] 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;
+}
+
+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::LegalizerPass NullPass(nullptr);
+ rcc::MachineAnalysisManager 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");
+
+ rcc::LegalizerPass ValidPass(rcc::riscv::legalizerInfo());
+ 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 testPipelineAndUnsupportedRollback(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};
+ rcc::MachinePassManager Pipeline;
+ Context.expect(Pipeline.addPass(std::make_unique<rcc::LegalizerPass>(rcc::riscv::legalizerInfo()))
+ .has_value(),
+ "legalizer must be accepted by the pass manager");
+ Context.expect(
+ Pipeline
+ .addPass(std::make_unique<rcc::RegisterBankSelectPass>(rcc::riscv::registerBankInfo()))
+ .has_value(),
+ "register-bank selector must be accepted by the pass manager");
+ Context.expect(
+ Pipeline
+ .addPass(std::make_unique<rcc::InstructionSelectPass>(rcc::riscv::instructionSelector()))
+ .has_value(),
+ "instruction selector must be accepted by the pass manager");
+ rcc::MachineAnalysisManager 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");
+
+ 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 = createFloatRegister(Context, *Unsupported.Function);
+ const rcc::Register UnsupportedSource = createFloatRegister(Context, *Unsupported.Function);
+ rcc::MachineIRBuilder UnsupportedBuilder(*Unsupported.Function, *Target);
+ Context.expect(UnsupportedBuilder.setInsertPoint(*Unsupported.Entry).has_value(),
+ "unsupported pipeline insertion point must be valid");
+ if (UnsupportedReg.isNoRegister() || UnsupportedSource.isNoRegister())
+ return;
+ auto Pending = UnsupportedBuilder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_FEXP));
+ 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->addUse(UnsupportedSource).has_value() && Pending->build().has_value(),
+ "unsupported floating operation must be constructible");
+ Unsupported.Function->properties() = {rcc::MachineFunctionProperty::IsSSA,
+ rcc::MachineFunctionProperty::TracksLiveness};
+ rcc::MachinePassManager OnlyLegalizer;
+ Context.expect(
+ OnlyLegalizer.addPass(std::make_unique<rcc::LegalizerPass>(rcc::riscv::legalizerInfo()))
+ .has_value(),
+ "standalone legalizer must be accepted");
+ rcc::MachineAnalysisManager 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_FEXP") != 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 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};
+ rcc::RegisterBankSelectPass Pass(rcc::riscv::registerBankInfo());
+ rcc::MachineAnalysisManager 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 testBankClassOnlyChange(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};
+ rcc::RegisterBankSelectPass Pass(rcc::riscv::registerBankInfo());
+ rcc::MachineAnalysisManager Analyses;
+ auto Result = Pass.run(*Fixture.Function, *Target, Analyses);
+ Context.expect(Result.has_value() && Result->Changed &&
+ Result->Changes.has(rcc::MachinePassChange::Registers),
+ "bank selection must report a class-only register change");
+ auto Info = Fixture.Function->registerInfo().virtualRegisterInfo(Value);
+ Context.expect(Info && Info->get().Bank && Info->get().RegClass,
+ "bank selection must publish both bank and class");
+ if (!Info || !Info->get().RegClass)
+ return;
+ Context.expect(Info->get().RegClass.value_or(rcc::RegisterClassID{}) ==
+ rcc::RegisterClassID::fromValue(
+ static_cast<std::uint32_t>(rcc::riscv::RegisterClass::GPR)),
+ "i64 GPR bank must select the GPR class");
+}
+
+void testLegalizerCycleDiagnostic(TestContext &Context) {
+ auto Target = rcc::riscv::createTargetDescription();
+ auto Fixture = createFixture(Context, "pipeline_legalizer_cycle", 6);
+ Context.expect(Target.has_value() && Fixture.Function != nullptr && Fixture.Entry != nullptr,
+ "legalizer cycle 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);
+ if (Left.isNoRegister() || Right.isNoRegister() || Result.isNoRegister() ||
+ !Builder.setInsertPoint(*Fixture.Entry) || !buildAdd(Context, Builder, Result, Left, Right))
+ return;
+ Fixture.Function->properties() = {rcc::MachineFunctionProperty::IsSSA,
+ rcc::MachineFunctionProperty::TracksLiveness};
+ TogglingLegalizer Legalizer;
+ rcc::LegalizerPass StablePass(Legalizer);
+ rcc::MachineAnalysisManager Analyses;
+ auto ResultRun = StablePass.run(*Fixture.Function, *Target, Analyses);
+ Context.expect(!ResultRun && ResultRun.error().code() == rcc::ErrorCode::InternalError,
+ "repeated legalization states must report InternalError");
+ Context.expect(Fixture.Entry->firstInstruction() != nullptr &&
+ Fixture.Entry->firstInstruction()->opcode() ==
+ rcc::opcode(rcc::GenericOpcode::G_ADD),
+ "failed legalization preflight must preserve the original instruction");
+ Context.expect(!Fixture.Function->properties().has(rcc::MachineFunctionProperty::Legalized),
+ "a failed legalization cycle must not publish Legalized");
+}
+
+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::InstructionSelectPass Pass(Selector);
+ rcc::MachineAnalysisManager 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};
+ RecordingSelector Selector(rcc::riscv::instructionSelector());
+ rcc::InstructionSelectPass Pass(Selector);
+ rcc::MachineAnalysisManager 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::InstructionSelectPass Pass(Selector);
+ rcc::MachineAnalysisManager 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::LLT::makeInteger(1);
+ auto I64 = rcc::LLT::makeInteger(64);
+ auto Pointer = rcc::LLT::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};
+ rcc::MachinePassManager Pipeline;
+ if (!Pipeline.addPass(std::make_unique<rcc::LegalizerPass>(rcc::riscv::legalizerInfo())) ||
+ !Pipeline.addPass(
+ std::make_unique<rcc::RegisterBankSelectPass>(rcc::riscv::registerBankInfo())) ||
+ !Pipeline.addPass(
+ std::make_unique<rcc::InstructionSelectPass>(rcc::riscv::instructionSelector()))) {
+ Context.expect(false, "extended RV64GC pipeline construction must succeed");
+ return;
+ }
+ rcc::MachineAnalysisManager 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;
+ testNullAndMissingContracts(Context);
+ testPipelineAndUnsupportedRollback(Context);
+ testBankConflict(Context);
+ testBankClassOnlyChange(Context);
+ testLegalizerCycleDiagnostic(Context);
+ testInstructionSelectionTransaction(Context);
+ testInstructionSelectionOrder(Context);
+ testInstructionSelectionWorklist(Context);
+ testExtendedRV64GCPipeline(Context);
+ return Context.result();
+}
diff --git a/backend/unittests/Target/CMakeLists.txt b/backend/unittests/Target/CMakeLists.txt
index aa45b90..98c4426 100644
--- a/backend/unittests/Target/CMakeLists.txt
+++ b/backend/unittests/Target/CMakeLists.txt
@@ -28,10 +28,16 @@ rcc_backend_configure_target(RCCBackendRISCVCallLoweringTests)
target_link_libraries(RCCBackendRISCVCallLoweringTests PRIVATE RCCBackend::RISCV)
add_test(NAME RCCBackendRISCVCallLoweringTests COMMAND RCCBackendRISCVCallLoweringTests)
+add_executable(RCCBackendRISCVCodeGenTests RISCVCodeGenTest.cpp)
+rcc_backend_configure_target(RCCBackendRISCVCodeGenTests)
+target_link_libraries(RCCBackendRISCVCodeGenTests PRIVATE RCCBackend::RISCV RCCBackend::CodeGen)
+add_test(NAME RCCBackendRISCVCodeGenTests COMMAND RCCBackendRISCVCodeGenTests)
+
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}/TestTarget.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/TestTarget.h"
)
@@ -39,5 +45,6 @@ rcc_backend_register_tidy_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}/TestTarget.cpp"
)
diff --git a/backend/unittests/Target/RISCVCodeGenTest.cpp b/backend/unittests/Target/RISCVCodeGenTest.cpp
new file mode 100644
index 0000000..efb8eda
--- /dev/null
+++ b/backend/unittests/Target/RISCVCodeGenTest.cpp
@@ -0,0 +1,1108 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/Target/RISCV/RISCVCodeGen.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/MachineMemOperand.h"
+#include "rcc/Target/RISCV/RISCVTarget.h"
+
+#include <array>
+#include <cstdint>
+#include <iostream>
+#include <memory>
+#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 InstructionFixture final {
+ std::unique_ptr<rcc::MachineFunction> Function;
+ rcc::MachineInstr *Instruction = nullptr;
+ rcc::Register Result;
+};
+
+[[nodiscard]] InstructionFixture createBinary(TestContext &Context,
+ const rcc::TargetDescription &Target,
+ rcc::GenericOpcode Generic, std::uint32_t Bits) {
+ auto Function =
+ rcc::MachineFunction::create("riscv_codegen", rcc::MachineFunctionStableID::fromValue(Bits));
+ Context.expect(Function.has_value(), "RISC-V codegen function creation must succeed");
+ if (!Function)
+ return {};
+ auto Block = (*Function)->createBasicBlock();
+ if (!Block || !(*Function)->appendBlock(**Block))
+ return {};
+ auto Type = rcc::LLT::makeInteger(Bits);
+ if (!Type)
+ return {};
+ auto Left = (*Function)->registerInfo().createVirtualRegister(*Type);
+ auto Right = (*Function)->registerInfo().createVirtualRegister(*Type);
+ auto Result = (*Function)->registerInfo().createVirtualRegister(*Type);
+ if (!Left || !Right || !Result)
+ return {};
+ rcc::MachineIRBuilder Builder(**Function, Target);
+ if (!Builder.setInsertPoint(**Block))
+ return {};
+ auto Pending = Builder.buildInstr(rcc::opcode(Generic));
+ if (!Pending || !Pending->addDef(*Result) || !Pending->addUse(*Left) || !Pending->addUse(*Right))
+ return {};
+ auto Built = Pending->build();
+ if (!Built)
+ return {};
+ return {std::move(*Function), *Built, *Result};
+}
+
+[[nodiscard]] bool constrainRegister(rcc::MachineFunction &Function, rcc::Register Reg,
+ rcc::LLT Type) {
+ const auto Bank = Type.isFloat() ? rcc::RegisterBankID::fromValue(
+ static_cast<std::uint32_t>(rcc::riscv::RegisterBank::FPR))
+ : rcc::RegisterBankID::fromValue(
+ static_cast<std::uint32_t>(rcc::riscv::RegisterBank::GPR));
+ const auto Class = Type.isFloat()
+ ? rcc::RegisterClassID::fromValue(static_cast<std::uint32_t>(
+ Type.scalarBitWidth() == 32 ? rcc::riscv::RegisterClass::FPR32
+ : rcc::riscv::RegisterClass::FPR64))
+ : rcc::RegisterClassID::fromValue(
+ static_cast<std::uint32_t>(rcc::riscv::RegisterClass::GPR));
+ return Function.registerInfo().setRegisterBank(Reg, Bank).has_value() &&
+ Function.registerInfo().setRegisterClass(Reg, Class).has_value();
+}
+
+[[nodiscard]] bool constrainVirtualRegisters(rcc::MachineFunction &Function) {
+ const auto Registers = Function.registerInfo().virtualRegisters();
+ for (std::size_t Index = 0; Index < Registers.size(); ++Index) {
+ const rcc::VirtualRegisterInfo &Info = Registers[Index];
+ if (Info.IsTombstone)
+ continue;
+ auto Reg = rcc::Register::makeVirtual(Index);
+ if (!Reg || !Info.Type || !constrainRegister(Function, *Reg, *Info.Type))
+ return false;
+ }
+ return true;
+}
+
+[[nodiscard]] bool validatesSelectedFunction(const rcc::TargetDescription &Target,
+ const rcc::MachineFunction &Function) {
+ for (const rcc::MachineInstr *Instruction : Function.allInstructions()) {
+ if (rcc::isGenericOpcode(Instruction->opcode()) ||
+ !Target.validateInstruction(*Instruction, Function.registerInfo())) {
+ return false;
+ }
+ }
+ return Target.validateRegisterInfo(Function.registerInfo()).has_value();
+}
+
+[[nodiscard]] std::unique_ptr<rcc::MachineFunction>
+createUnaryFunction(TestContext &Context, const rcc::TargetDescription &Target,
+ rcc::GenericOpcode Generic, rcc::LLT DestinationType, rcc::LLT SourceType,
+ rcc::Register *DestinationOut = nullptr, rcc::Register *SourceOut = nullptr) {
+ auto Function = rcc::MachineFunction::create(
+ "riscv_unary",
+ rcc::MachineFunctionStableID::fromValue(200 + static_cast<std::uint32_t>(Generic)));
+ Context.expect(Function.has_value(), "unary test function creation must succeed");
+ if (!Function)
+ return nullptr;
+ auto Block = (*Function)->createBasicBlock();
+ if (!Block || !(*Function)->appendBlock(**Block))
+ return nullptr;
+ auto Destination = (*Function)->registerInfo().createVirtualRegister(DestinationType);
+ auto Source = (*Function)->registerInfo().createVirtualRegister(SourceType);
+ if (!Destination || !Source)
+ return nullptr;
+ if (!constrainRegister(**Function, *Destination, DestinationType) ||
+ !constrainRegister(**Function, *Source, SourceType))
+ return nullptr;
+ rcc::MachineIRBuilder Builder(**Function, Target);
+ if (!Builder.setInsertPoint(**Block))
+ return nullptr;
+ auto Pending = Builder.buildInstr(rcc::opcode(Generic));
+ if (!Pending || !Pending->addDef(*Destination) || !Pending->addUse(*Source) || !Pending->build())
+ return nullptr;
+ if (DestinationOut != nullptr)
+ *DestinationOut = *Destination;
+ if (SourceOut != nullptr)
+ *SourceOut = *Source;
+ return std::move(*Function);
+}
+
+[[nodiscard]] std::unique_ptr<rcc::MachineFunction>
+createCompareFunction(TestContext &Context, const rcc::TargetDescription &Target,
+ rcc::GenericOpcode Generic, rcc::LLT ValueType, bool Floating,
+ rcc::IntegerPredicate IntegerCondition,
+ rcc::FloatingPredicate FloatingCondition) {
+ auto Function = rcc::MachineFunction::create(
+ "riscv_compare",
+ rcc::MachineFunctionStableID::fromValue(500 + static_cast<std::uint32_t>(Generic)));
+ Context.expect(Function.has_value(), "comparison test function creation must succeed");
+ if (!Function)
+ return nullptr;
+ auto Block = (*Function)->createBasicBlock();
+ if (!Block || !(*Function)->appendBlock(**Block))
+ return nullptr;
+ auto ResultType = rcc::LLT::makeInteger(1);
+ if (!ResultType)
+ return nullptr;
+ auto Result = (*Function)->registerInfo().createVirtualRegister(*ResultType);
+ auto Left = (*Function)->registerInfo().createVirtualRegister(ValueType);
+ auto Right = (*Function)->registerInfo().createVirtualRegister(ValueType);
+ if (!Result || !Left || !Right || !constrainRegister(**Function, *Result, *ResultType) ||
+ !constrainRegister(**Function, *Left, ValueType) ||
+ !constrainRegister(**Function, *Right, ValueType))
+ return nullptr;
+ rcc::MachineIRBuilder Builder(**Function, Target);
+ if (!Builder.setInsertPoint(**Block))
+ return nullptr;
+ auto Pending = Builder.buildInstr(rcc::opcode(Generic));
+ if (!Pending || !Pending->addDef(*Result))
+ return nullptr;
+ if (Floating) {
+ if (!Pending->addFloatingPredicate(FloatingCondition))
+ return nullptr;
+ } else if (!Pending->addIntegerPredicate(IntegerCondition)) {
+ return nullptr;
+ }
+ if (!Pending->addUse(*Left) || !Pending->addUse(*Right) || !Pending->build())
+ return nullptr;
+ return std::move(*Function);
+}
+
+[[nodiscard]] std::unique_ptr<rcc::MachineFunction>
+createFloatBinaryFunction(TestContext &Context, const rcc::TargetDescription &Target,
+ rcc::GenericOpcode Generic, rcc::LLT Type) {
+ auto Function = rcc::MachineFunction::create(
+ "riscv_float_binary",
+ rcc::MachineFunctionStableID::fromValue(700 + static_cast<std::uint32_t>(Generic)));
+ Context.expect(Function.has_value(), "floating binary test function creation must succeed");
+ if (!Function)
+ return nullptr;
+ auto Block = (*Function)->createBasicBlock();
+ if (!Block || !(*Function)->appendBlock(**Block))
+ return nullptr;
+ auto Left = (*Function)->registerInfo().createVirtualRegister(Type);
+ auto Right = (*Function)->registerInfo().createVirtualRegister(Type);
+ auto Result = (*Function)->registerInfo().createVirtualRegister(Type);
+ if (!Left || !Right || !Result || !constrainRegister(**Function, *Left, Type) ||
+ !constrainRegister(**Function, *Right, Type) || !constrainRegister(**Function, *Result, Type))
+ return nullptr;
+ rcc::MachineIRBuilder Builder(**Function, Target);
+ if (!Builder.setInsertPoint(**Block))
+ return nullptr;
+ auto Pending = Builder.buildInstr(rcc::opcode(Generic));
+ if (!Pending || !Pending->addDef(*Result) || !Pending->addUse(*Left) ||
+ !Pending->addUse(*Right) || !Pending->build())
+ return nullptr;
+ return std::move(*Function);
+}
+
+[[nodiscard]] std::unique_ptr<rcc::MachineFunction> createRegisterOnlyFunction(
+ TestContext &Context, const rcc::TargetDescription &Target, rcc::GenericOpcode Generic,
+ std::initializer_list<rcc::LLT> DefinitionTypes, std::initializer_list<rcc::LLT> UseTypes) {
+ auto Function = rcc::MachineFunction::create(
+ "riscv_register_only",
+ rcc::MachineFunctionStableID::fromValue(1000 + static_cast<std::uint32_t>(Generic)));
+ Context.expect(Function.has_value(), "register-only test function creation must succeed");
+ if (!Function)
+ return nullptr;
+ auto Block = (*Function)->createBasicBlock();
+ if (!Block || !(*Function)->appendBlock(**Block))
+ return nullptr;
+ rcc::MachineIRBuilder Builder(**Function, Target);
+ if (!Builder.setInsertPoint(**Block))
+ return nullptr;
+ auto Pending = Builder.buildInstr(rcc::opcode(Generic));
+ if (!Pending)
+ return nullptr;
+ for (rcc::LLT Type : DefinitionTypes) {
+ auto Reg = (*Function)->registerInfo().createVirtualRegister(Type);
+ if (!Reg || !Pending->addDef(*Reg))
+ return nullptr;
+ }
+ for (rcc::LLT Type : UseTypes) {
+ auto Reg = (*Function)->registerInfo().createVirtualRegister(Type);
+ if (!Reg || !Pending->addUse(*Reg))
+ return nullptr;
+ }
+ if (!Pending->build() || !constrainVirtualRegisters(**Function))
+ return nullptr;
+ return std::move(*Function);
+}
+
+void testClassification(TestContext &Context) {
+ auto Target = rcc::riscv::createTargetDescription();
+ Context.expect(Target.has_value(), "RV64GC target must be available for codegen tests");
+ if (!Target)
+ return;
+ auto Add32 = createBinary(Context, *Target, rcc::GenericOpcode::G_ADD, 32);
+ auto Add64 = createBinary(Context, *Target, rcc::GenericOpcode::G_ADD, 64);
+ Context.expect(Add32.Instruction != nullptr && Add64.Instruction != nullptr,
+ "RISC-V integer test instructions must be constructible");
+ if (Add32.Instruction == nullptr || Add64.Instruction == nullptr)
+ return;
+ auto Add32Action =
+ rcc::riscv::legalizerInfo().classify(*Add32.Instruction, *Add32.Function, *Target);
+ auto Add64Action =
+ rcc::riscv::legalizerInfo().classify(*Add64.Instruction, *Add64.Function, *Target);
+ Context.expect(Add32Action && *Add32Action == rcc::LegalizationAction::Legal,
+ "RV64GC legalizer must accept i32 arithmetic");
+ Context.expect(Add64Action && *Add64Action == rcc::LegalizationAction::Legal,
+ "RV64GC legalizer must accept i64 arithmetic");
+ Context.expect(rcc::riscv::instructionSelector()
+ .canSelect(*Add32.Instruction, *Add32.Function, *Target)
+ .has_value(),
+ "RV64GC selector must pre-match i32 arithmetic");
+ Context.expect(rcc::riscv::instructionSelector()
+ .canSelect(*Add64.Instruction, *Add64.Function, *Target)
+ .has_value(),
+ "RV64GC selector must pre-match i64 arithmetic");
+ Context.expect(rcc::riscv::instructionSelector()
+ .select(*Add32.Instruction, *Add32.Function, *Target)
+ .has_value(),
+ "RV64GC selector must lower i32 arithmetic");
+ Context.expect(Add32.Instruction->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::ADDW)),
+ "i32 G_ADD must select ADDW");
+ Context.expect(rcc::riscv::instructionSelector()
+ .select(*Add64.Instruction, *Add64.Function, *Target)
+ .has_value(),
+ "RV64GC selector must lower i64 arithmetic");
+ Context.expect(Add64.Instruction->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::ADD)),
+ "i64 G_ADD must select ADD");
+
+ auto FloatFunction = rcc::MachineFunction::create("riscv_float_constants",
+ rcc::MachineFunctionStableID::fromValue(3));
+ Context.expect(FloatFunction.has_value(), "floating constant function creation must succeed");
+ if (!FloatFunction)
+ return;
+ auto FloatBlock = (*FloatFunction)->createBasicBlock();
+ Context.expect(FloatBlock.has_value(), "floating constant block creation must succeed");
+ if (!FloatBlock || !(*FloatFunction)->appendBlock(**FloatBlock))
+ return;
+ auto F32 = rcc::LLT::makeFloat(rcc::FloatSemantics::IEEESingle);
+ auto F64 = rcc::LLT::makeFloat(rcc::FloatSemantics::IEEEDouble);
+ Context.expect(F32.has_value() && F64.has_value(),
+ "RV64GC floating constant types must be constructible");
+ if (!F32 || !F64)
+ return;
+ auto F32Result = (*FloatFunction)->registerInfo().createVirtualRegister(*F32);
+ auto F64Result = (*FloatFunction)->registerInfo().createVirtualRegister(*F64);
+ Context.expect(F32Result.has_value() && F64Result.has_value(),
+ "RV64GC floating constant registers must be constructible");
+ if (!F32Result || !F64Result)
+ return;
+ Context.expect((*FloatFunction)
+ ->registerInfo()
+ .setRegisterBank(*F32Result,
+ rcc::RegisterBankID::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::RegisterBank::FPR)))
+ .has_value() &&
+ (*FloatFunction)
+ ->registerInfo()
+ .setRegisterClass(
+ *F32Result, rcc::RegisterClassID::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::RegisterClass::FPR32)))
+ .has_value(),
+ "f32 destination must have an FPR32 bank and class");
+ Context.expect((*FloatFunction)
+ ->registerInfo()
+ .setRegisterBank(*F64Result,
+ rcc::RegisterBankID::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::RegisterBank::FPR)))
+ .has_value() &&
+ (*FloatFunction)
+ ->registerInfo()
+ .setRegisterClass(
+ *F64Result, rcc::RegisterClassID::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::RegisterClass::FPR64)))
+ .has_value(),
+ "f64 destination must have an FPR64 bank and class");
+ rcc::MachineIRBuilder FloatBuilder(**FloatFunction, *Target);
+ if (!FloatBuilder.setInsertPoint(**FloatBlock))
+ return;
+ const std::array<std::uint64_t, 1> SingleWords{0x3f800000U};
+ const std::array<std::uint64_t, 1> DoubleWords{0x3ff0000000000000ULL};
+ auto SingleBits = rcc::FloatingPointBits::create(rcc::FloatSemantics::IEEESingle, SingleWords);
+ auto DoubleBits = rcc::FloatingPointBits::create(rcc::FloatSemantics::IEEEDouble, DoubleWords);
+ Context.expect(SingleBits.has_value() && DoubleBits.has_value(),
+ "floating constant payloads must be constructible");
+ if (!SingleBits || !DoubleBits)
+ return;
+ auto SinglePending = FloatBuilder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_FCONSTANT));
+ auto DoublePending = FloatBuilder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_FCONSTANT));
+ Context.expect(SinglePending.has_value() && DoublePending.has_value(),
+ "floating constant instructions must be constructible");
+ if (!SinglePending || !DoublePending)
+ return;
+ Context.expect(SinglePending->addDef(*F32Result).has_value() &&
+ SinglePending->addFloatingImmediate(*SingleBits).has_value() &&
+ SinglePending->build().has_value(),
+ "f32 floating constant must be buildable");
+ Context.expect(DoublePending->addDef(*F64Result).has_value() &&
+ DoublePending->addFloatingImmediate(*DoubleBits).has_value() &&
+ DoublePending->build().has_value(),
+ "f64 floating constant must be buildable");
+ if (FloatFunction->get()->firstBlock()->firstInstruction() == nullptr)
+ return;
+ auto Instructions = FloatFunction->get()->allInstructions();
+ auto It = Instructions.begin();
+ rcc::MachineInstr *F32Instruction = It == Instructions.end() ? nullptr : *It;
+ if (It != Instructions.end())
+ ++It;
+ rcc::MachineInstr *F64Instruction = It == Instructions.end() ? nullptr : *It;
+ Context.expect(F32Instruction != nullptr && F64Instruction != nullptr,
+ "floating constant instructions must remain in layout order");
+ if (F32Instruction == nullptr || F64Instruction == nullptr)
+ return;
+ auto F32Action = rcc::riscv::legalizerInfo().classify(*F32Instruction, **FloatFunction, *Target);
+ auto F64Action = rcc::riscv::legalizerInfo().classify(*F64Instruction, **FloatFunction, *Target);
+ Context.expect(F32Action && *F32Action == rcc::LegalizationAction::Legal,
+ "RV64GC legalizer must accept f32 constants");
+ Context.expect(F64Action && *F64Action == rcc::LegalizationAction::Legal,
+ "RV64GC legalizer must accept f64 constants");
+ auto F32Selection =
+ rcc::riscv::instructionSelector().select(*F32Instruction, **FloatFunction, *Target);
+ auto F64Selection =
+ rcc::riscv::instructionSelector().select(*F64Instruction, **FloatFunction, *Target);
+ Context.expect(F32Selection.has_value(), "RV64GC selector must lower f32 constants");
+ Context.expect(F64Selection.has_value(), "RV64GC selector must lower f64 constants");
+ bool HasFMVWX = false;
+ bool HasFMVDX = false;
+ for (const rcc::MachineInstr *Instruction : FloatFunction->get()->allInstructions()) {
+ HasFMVWX |=
+ Instruction->opcode() ==
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::FMV_W_X));
+ HasFMVDX |=
+ Instruction->opcode() ==
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::FMV_D_X));
+ }
+ Context.expect(HasFMVWX, "f32 constants must use FMV.W.X materialization");
+ Context.expect(HasFMVDX, "f64 constants must use FMV.D.X materialization");
+}
+
+void testNarrowIntegerSelection(TestContext &Context) {
+ auto Target = rcc::riscv::createTargetDescription();
+ if (!Target)
+ return;
+ struct BinaryCase final {
+ rcc::GenericOpcode Generic;
+ std::uint32_t Bits;
+ rcc::riscv::InstructionOpcode RequiredOpcode;
+ };
+ const std::array Cases{
+ BinaryCase{rcc::GenericOpcode::G_ADD, 1, rcc::riscv::InstructionOpcode::ADDW},
+ BinaryCase{rcc::GenericOpcode::G_UDIV, 8, rcc::riscv::InstructionOpcode::DIVUW},
+ BinaryCase{rcc::GenericOpcode::G_ASHR, 16, rcc::riscv::InstructionOpcode::SRAW},
+ BinaryCase{rcc::GenericOpcode::G_SMIN, 24, rcc::riscv::InstructionOpcode::SLT},
+ };
+ for (const BinaryCase &TestCase : Cases) {
+ auto Fixture = createBinary(Context, *Target, TestCase.Generic, TestCase.Bits);
+ if (!Fixture.Function || Fixture.Instruction == nullptr ||
+ !constrainVirtualRegisters(*Fixture.Function)) {
+ Context.expect(false, "narrow integer fixture must be constructible");
+ continue;
+ }
+ auto Action =
+ rcc::riscv::legalizerInfo().classify(*Fixture.Instruction, *Fixture.Function, *Target);
+ Context.expect(Action && *Action == rcc::LegalizationAction::Legal,
+ "i1-i24 scalar operations must be legal on RV64GC GPR containers");
+ auto Selection =
+ rcc::riscv::instructionSelector().select(*Fixture.Instruction, *Fixture.Function, *Target);
+ Context.expect(Selection.has_value(), "narrow integer operation must select");
+ bool FoundRequiredOpcode = false;
+ for (const rcc::MachineInstr *Instruction : Fixture.Function->allInstructions()) {
+ FoundRequiredOpcode |=
+ Instruction->opcode() ==
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(TestCase.RequiredOpcode));
+ }
+ Context.expect(FoundRequiredOpcode,
+ "narrow integer selection must use the required RV64 instruction");
+ Context.expect(Selection && validatesSelectedFunction(*Target, *Fixture.Function),
+ "narrow integer selection must satisfy target descriptors");
+ }
+
+ auto I7 = rcc::LLT::makeInteger(7);
+ auto I24 = rcc::LLT::makeInteger(24);
+ if (!I7 || !I24)
+ return;
+ auto Compare = createCompareFunction(Context, *Target, rcc::GenericOpcode::G_ICMP, *I7, false,
+ rcc::IntegerPredicate::SignedLessThan,
+ rcc::FloatingPredicate::AlwaysFalse);
+ if (Compare && Compare->firstBlock()->firstInstruction() != nullptr) {
+ rcc::MachineInstr *Instruction = Compare->firstBlock()->firstInstruction();
+ auto Action = rcc::riscv::legalizerInfo().classify(*Instruction, *Compare, *Target);
+ auto Selection = rcc::riscv::instructionSelector().select(*Instruction, *Compare, *Target);
+ Context.expect(Action && *Action == rcc::LegalizationAction::Legal && Selection,
+ "signed i7 comparisons must normalize and select");
+ Context.expect(Selection && validatesSelectedFunction(*Target, *Compare),
+ "selected i7 comparison must satisfy target descriptors");
+ }
+
+ auto Extension = createUnaryFunction(Context, *Target, rcc::GenericOpcode::G_SEXT, *I24, *I7);
+ if (Extension && Extension->firstBlock()->firstInstruction() != nullptr) {
+ rcc::MachineInstr *Instruction = Extension->firstBlock()->firstInstruction();
+ auto Action = rcc::riscv::legalizerInfo().classify(*Instruction, *Extension, *Target);
+ auto Selection = rcc::riscv::instructionSelector().select(*Instruction, *Extension, *Target);
+ Context.expect(Action && *Action == rcc::LegalizationAction::Legal && Selection,
+ "i7-to-i24 sign extension must select through a 32-bit container");
+ Context.expect(Selection && validatesSelectedFunction(*Target, *Extension),
+ "selected narrow extension must satisfy target descriptors");
+ }
+
+ auto UnsupportedMulHigh = createBinary(Context, *Target, rcc::GenericOpcode::G_SMULH, 16);
+ if (UnsupportedMulHigh.Function && UnsupportedMulHigh.Instruction != nullptr) {
+ auto Action = rcc::riscv::legalizerInfo().classify(*UnsupportedMulHigh.Instruction,
+ *UnsupportedMulHigh.Function, *Target);
+ Context.expect(Action && *Action == rcc::LegalizationAction::Unsupported,
+ "non-native multiply-high widths must be explicitly unsupported");
+ }
+}
+
+void testScalarSelection(TestContext &Context) {
+ auto Target = rcc::riscv::createTargetDescription();
+ Context.expect(Target.has_value(), "RV64GC target must be available for scalar selection");
+ if (!Target)
+ return;
+
+ auto I32 = rcc::LLT::makeInteger(32);
+ auto I64 = rcc::LLT::makeInteger(64);
+ auto F32 = rcc::LLT::makeFloat(rcc::FloatSemantics::IEEESingle);
+ auto F64 = rcc::LLT::makeFloat(rcc::FloatSemantics::IEEEDouble);
+ auto I1 = rcc::LLT::makeInteger(1);
+ Context.expect(I32 && I64 && F32 && F64 && I1, "scalar test types must be constructible");
+ if (!I32 || !I64 || !F32 || !F64 || !I1)
+ return;
+
+ auto AnyExt = createUnaryFunction(Context, *Target, rcc::GenericOpcode::G_ANYEXT, *I64, *I32);
+ auto Sext = createUnaryFunction(Context, *Target, rcc::GenericOpcode::G_SEXT, *I64, *I32);
+ auto Zext = createUnaryFunction(Context, *Target, rcc::GenericOpcode::G_ZEXT, *I64, *I32);
+ auto Trunc = createUnaryFunction(Context, *Target, rcc::GenericOpcode::G_TRUNC, *I32, *I64);
+ for (const auto &Entry : {std::pair{AnyExt.get(), rcc::GenericOpcode::G_ANYEXT},
+ std::pair{Sext.get(), rcc::GenericOpcode::G_SEXT},
+ std::pair{Zext.get(), rcc::GenericOpcode::G_ZEXT},
+ std::pair{Trunc.get(), rcc::GenericOpcode::G_TRUNC}}) {
+ Context.expect(Entry.first != nullptr, "integer extension test instruction must exist");
+ if (Entry.first == nullptr)
+ continue;
+ auto *Instruction = Entry.first->firstBlock()->firstInstruction();
+ auto Selection = rcc::riscv::instructionSelector().select(*Instruction, *Entry.first, *Target);
+ const std::string_view Name = Entry.second == rcc::GenericOpcode::G_ANYEXT ? "anyext"
+ : Entry.second == rcc::GenericOpcode::G_SEXT ? "sext"
+ : Entry.second == rcc::GenericOpcode::G_ZEXT ? "zext"
+ : "trunc";
+ Context.expect(Selection.has_value(),
+ std::string("RV64GC ") + std::string(Name) + " must select");
+ if (!Selection)
+ continue;
+ if (Entry.second == rcc::GenericOpcode::G_ANYEXT || Entry.second == rcc::GenericOpcode::G_TRUNC)
+ Context.expect(Instruction->opcode() == rcc::opcode(rcc::GenericOpcode::COPY),
+ "anyext/trunc must select COPY");
+ else
+ Context.expect(!rcc::isGenericOpcode(Instruction->opcode()),
+ "integer extension must not remain generic");
+ }
+ if (Zext) {
+ Context.expect(Zext->allInstructions().size() == 2,
+ "zero extension must emit a two-instruction RV64 sequence");
+ if (Zext->allInstructions().size() == 2) {
+ auto It = Zext->allInstructions().begin();
+ Context.expect((*It)->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::SLLI)),
+ "zero extension must shift left first");
+ ++It;
+ Context.expect((*It)->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::SRLI)),
+ "zero extension must shift right logically second");
+ }
+ }
+
+ auto FPExt = createUnaryFunction(Context, *Target, rcc::GenericOpcode::G_FPEXT, *F64, *F32);
+ auto FPTrunc = createUnaryFunction(Context, *Target, rcc::GenericOpcode::G_FPTRUNC, *F32, *F64);
+ auto FPToSI = createUnaryFunction(Context, *Target, rcc::GenericOpcode::G_FPTOSI, *I64, *F32);
+ auto SIToFP = createUnaryFunction(Context, *Target, rcc::GenericOpcode::G_SITOFP, *F64, *I32);
+ auto Bitcast = createUnaryFunction(Context, *Target, rcc::GenericOpcode::G_BITCAST, *I32, *F32);
+ if (FPExt) {
+ auto *Instruction = FPExt->firstBlock()->firstInstruction();
+ Context.expect(
+ rcc::riscv::instructionSelector().select(*Instruction, *FPExt, *Target).has_value(),
+ "fpext must select");
+ Context.expect(Instruction->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::FCVT_D_S)),
+ "fpext must select FCVT.D.S");
+ }
+ if (FPTrunc) {
+ auto *Instruction = FPTrunc->firstBlock()->firstInstruction();
+ Context.expect(
+ rcc::riscv::instructionSelector().select(*Instruction, *FPTrunc, *Target).has_value(),
+ "fptrunc must select");
+ Context.expect(Instruction->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::FCVT_S_D)),
+ "fptrunc must select FCVT.S.D");
+ }
+ if (FPToSI) {
+ auto *Instruction = FPToSI->firstBlock()->firstInstruction();
+ Context.expect(
+ rcc::riscv::instructionSelector().select(*Instruction, *FPToSI, *Target).has_value(),
+ "fptosi must select");
+ Context.expect(Instruction->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::FCVT_L_S)),
+ "fptosi must select FCVT.L.S");
+ }
+ if (SIToFP) {
+ auto *Instruction = SIToFP->firstBlock()->firstInstruction();
+ Context.expect(
+ rcc::riscv::instructionSelector().select(*Instruction, *SIToFP, *Target).has_value(),
+ "sitofp must select");
+ Context.expect(Instruction->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::FCVT_D_W)),
+ "sitofp must select FCVT.D.W");
+ }
+ if (Bitcast) {
+ auto *Instruction = Bitcast->firstBlock()->firstInstruction();
+ Context.expect(
+ rcc::riscv::instructionSelector().select(*Instruction, *Bitcast, *Target).has_value(),
+ "float-to-integer bitcast must select");
+ Context.expect(Instruction->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::FMV_X_W)),
+ "float-to-integer bitcast must select FMV.X.W");
+ }
+}
+
+void testComparisonsAndControl(TestContext &Context) {
+ auto Target = rcc::riscv::createTargetDescription();
+ Context.expect(Target.has_value(), "RV64GC target must be available for comparison selection");
+ if (!Target)
+ return;
+ auto I64 = rcc::LLT::makeInteger(64);
+ auto F32 = rcc::LLT::makeFloat(rcc::FloatSemantics::IEEESingle);
+ auto Pointer = rcc::LLT::makePointer(0, 64);
+ if (!I64 || !F32 || !Pointer)
+ return;
+
+ auto Equal =
+ createCompareFunction(Context, *Target, rcc::GenericOpcode::G_ICMP, *I64, false,
+ rcc::IntegerPredicate::Equal, rcc::FloatingPredicate::AlwaysFalse);
+ auto LessEqual = createCompareFunction(Context, *Target, rcc::GenericOpcode::G_ICMP, *I64, false,
+ rcc::IntegerPredicate::SignedLessEqual,
+ rcc::FloatingPredicate::AlwaysFalse);
+ if (Equal) {
+ auto *Instruction = Equal->firstBlock()->firstInstruction();
+ auto Selection = rcc::riscv::instructionSelector().select(*Instruction, *Equal, *Target);
+ Context.expect(Selection.has_value(), "integer equality must select");
+ Context.expect(Equal->allInstructions().size() == 2,
+ "integer equality must emit XOR and zero-test instructions");
+ if (Equal->allInstructions().size() == 2) {
+ auto It = Equal->allInstructions().begin();
+ Context.expect((*It)->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::XOR)),
+ "integer equality must start with XOR");
+ ++It;
+ Context.expect((*It)->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::SLTIU)),
+ "integer equality must finish with SLTIU");
+ }
+ }
+ if (LessEqual) {
+ auto *Instruction = LessEqual->firstBlock()->firstInstruction();
+ Context.expect(
+ rcc::riscv::instructionSelector().select(*Instruction, *LessEqual, *Target).has_value(),
+ "signed less-equal must select");
+ Context.expect(LessEqual->allInstructions().size() == 2,
+ "signed less-equal must emit compare and inversion");
+ }
+
+ auto Ordered =
+ createCompareFunction(Context, *Target, rcc::GenericOpcode::G_FCMP, *F32, true,
+ rcc::IntegerPredicate::Equal, rcc::FloatingPredicate::OrderedEqual);
+ auto UnorderedEqual =
+ createCompareFunction(Context, *Target, rcc::GenericOpcode::G_FCMP, *F32, true,
+ rcc::IntegerPredicate::Equal, rcc::FloatingPredicate::UnorderedEqual);
+ if (Ordered) {
+ auto *Instruction = Ordered->firstBlock()->firstInstruction();
+ Context.expect(
+ rcc::riscv::instructionSelector().select(*Instruction, *Ordered, *Target).has_value(),
+ "ordered floating equality must select");
+ Context.expect(Instruction->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::FEQ_S)),
+ "ordered floating equality must select FEQ.S");
+ }
+ if (UnorderedEqual) {
+ auto *Instruction = UnorderedEqual->firstBlock()->firstInstruction();
+ Context.expect(rcc::riscv::instructionSelector()
+ .select(*Instruction, *UnorderedEqual, *Target)
+ .has_value(),
+ "unordered floating equality must select");
+ Context.expect(UnorderedEqual->allInstructions().size() == 4,
+ "unordered floating equality must emit ordered compare sequence");
+ }
+
+ auto FMin = createFloatBinaryFunction(Context, *Target, rcc::GenericOpcode::G_FMINNUM, *F32);
+ if (FMin) {
+ auto *Instruction = FMin->firstBlock()->firstInstruction();
+ Context.expect(
+ rcc::riscv::instructionSelector().select(*Instruction, *FMin, *Target).has_value(),
+ "floating minnum must select");
+ Context.expect(Instruction->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::FMIN_S)),
+ "floating minnum must select FMIN.S");
+ }
+
+ auto ControlFunction =
+ rcc::MachineFunction::create("riscv_control", rcc::MachineFunctionStableID::fromValue(900));
+ Context.expect(ControlFunction.has_value(), "control test function creation must succeed");
+ if (!ControlFunction)
+ return;
+ auto Block = (*ControlFunction)->createBasicBlock();
+ if (!Block || !(*ControlFunction)->appendBlock(**Block))
+ return;
+ auto Address = (*ControlFunction)->registerInfo().createVirtualRegister(*Pointer);
+ if (!Address || !constrainRegister(**ControlFunction, *Address, *Pointer))
+ return;
+ rcc::MachineIRBuilder Builder(**ControlFunction, *Target);
+ if (!Builder.setInsertPoint(**Block))
+ return;
+ auto Indirect = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_BRINDIRECT));
+ auto Fence = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_FENCE));
+ auto Trap = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_TRAP));
+ if (!Indirect || !Fence || !Trap || !Indirect->addUse(*Address) || !Indirect->build() ||
+ !Fence->addImmediate(7) || !Fence->addImmediate(0) || !Fence->build() || !Trap->build())
+ return;
+ auto *IndirectInstruction = (*ControlFunction)->firstBlock()->firstInstruction();
+ Context.expect(rcc::riscv::instructionSelector()
+ .select(*IndirectInstruction, **ControlFunction, *Target)
+ .has_value(),
+ "indirect branch must select");
+ Context.expect(IndirectInstruction->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::JALR)),
+ "indirect branch must select JALR");
+ auto *FenceInstruction = IndirectInstruction->next();
+ Context.expect(rcc::riscv::instructionSelector()
+ .select(*FenceInstruction, **ControlFunction, *Target)
+ .has_value(),
+ "fence must select");
+ Context.expect(FenceInstruction->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::FENCE)),
+ "fence must select FENCE");
+ auto *TrapInstruction = FenceInstruction->next();
+ Context.expect(rcc::riscv::instructionSelector()
+ .select(*TrapInstruction, **ControlFunction, *Target)
+ .has_value(),
+ "trap must select");
+ Context.expect(TrapInstruction->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::EBREAK)),
+ "trap must select EBREAK");
+}
+
+void testCompositeScalarSelection(TestContext &Context) {
+ auto Target = rcc::riscv::createTargetDescription();
+ Context.expect(Target.has_value(), "RV64GC target must be available for composite selection");
+ if (!Target)
+ return;
+ auto I1 = rcc::LLT::makeInteger(1);
+ auto I32 = rcc::LLT::makeInteger(32);
+ auto I64 = rcc::LLT::makeInteger(64);
+ auto F64 = rcc::LLT::makeFloat(rcc::FloatSemantics::IEEEDouble);
+ if (!I1 || !I32 || !I64 || !F64)
+ return;
+
+ auto DivRem = createRegisterOnlyFunction(Context, *Target, rcc::GenericOpcode::G_SDIVREM,
+ {*I64, *I64}, {*I64, *I64});
+ auto MultiplyHigh = createRegisterOnlyFunction(Context, *Target, rcc::GenericOpcode::G_UMULH,
+ {*I32}, {*I32, *I32});
+ auto Minimum = createRegisterOnlyFunction(Context, *Target, rcc::GenericOpcode::G_SMIN, {*I64},
+ {*I64, *I64});
+ auto Absolute =
+ createRegisterOnlyFunction(Context, *Target, rcc::GenericOpcode::G_ABS, {*I32}, {*I32});
+ auto IntegerSelect = createRegisterOnlyFunction(Context, *Target, rcc::GenericOpcode::G_SELECT,
+ {*I64}, {*I1, *I64, *I64});
+ auto FloatSelect = createRegisterOnlyFunction(Context, *Target, rcc::GenericOpcode::G_SELECT,
+ {*F64}, {*I1, *F64, *F64});
+ auto CopySign = createRegisterOnlyFunction(Context, *Target, rcc::GenericOpcode::G_FCOPYSIGN,
+ {*F64}, {*F64, *F64});
+ const std::array Functions{DivRem.get(), MultiplyHigh.get(), Minimum.get(), Absolute.get(),
+ IntegerSelect.get(), FloatSelect.get(), CopySign.get()};
+ for (rcc::MachineFunction *Function : Functions) {
+ Context.expect(Function != nullptr, "composite scalar fixture must be complete");
+ if (Function == nullptr)
+ continue;
+ rcc::MachineInstr *Instruction = Function->firstBlock()->firstInstruction();
+ auto Classification = rcc::riscv::legalizerInfo().classify(*Instruction, *Function, *Target);
+ Context.expect(Classification && *Classification == rcc::LegalizationAction::Legal,
+ "composite scalar operation must be legal on RV64GC");
+ auto Selection = rcc::riscv::instructionSelector().select(*Instruction, *Function, *Target);
+ Context.expect(Selection.has_value(), "composite scalar operation must select on RV64GC");
+ Context.expect(Selection && validatesSelectedFunction(*Target, *Function),
+ "composite scalar selection must satisfy all target descriptors");
+ }
+ if (DivRem) {
+ const auto Instructions = DivRem->allInstructions();
+ Context.expect(Instructions.size() == 2, "G_SDIVREM must emit separate quotient and remainder");
+ if (Instructions.size() == 2) {
+ auto It = Instructions.begin();
+ Context.expect((*It)->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::DIV)),
+ "G_SDIVREM quotient must use DIV");
+ ++It;
+ Context.expect((*It)->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::REM)),
+ "G_SDIVREM remainder must use REM");
+ }
+ }
+ if (MultiplyHigh) {
+ Context.expect(MultiplyHigh->allInstructions().size() >= 4,
+ "i32 G_UMULH must explicitly zero-extend before multiplication");
+ }
+ if (CopySign) {
+ Context.expect(CopySign->firstBlock()->firstInstruction()->opcode() ==
+ rcc::Opcode::fromValue(
+ static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::FSGNJ_D)),
+ "G_FCOPYSIGN must select FSGNJ.D");
+ }
+}
+
+void testOverflowSelection(TestContext &Context) {
+ auto Target = rcc::riscv::createTargetDescription();
+ if (!Target)
+ return;
+ auto I1 = rcc::LLT::makeInteger(1);
+ auto I32 = rcc::LLT::makeInteger(32);
+ auto I64 = rcc::LLT::makeInteger(64);
+ if (!I1 || !I32 || !I64)
+ return;
+ const std::array Opcodes{rcc::GenericOpcode::G_UADDO, rcc::GenericOpcode::G_USUBO,
+ rcc::GenericOpcode::G_SADDO, rcc::GenericOpcode::G_SSUBO,
+ rcc::GenericOpcode::G_UMULO, rcc::GenericOpcode::G_SMULO};
+ for (rcc::GenericOpcode Generic : Opcodes) {
+ const rcc::LLT Type = Generic == rcc::GenericOpcode::G_UMULO ? *I32 : *I64;
+ auto Function =
+ createRegisterOnlyFunction(Context, *Target, Generic, {Type, *I1}, {Type, Type});
+ Context.expect(Function != nullptr, "overflow fixture must be complete");
+ if (!Function)
+ continue;
+ rcc::MachineInstr *Instruction = Function->firstBlock()->firstInstruction();
+ auto Selection = rcc::riscv::instructionSelector().select(*Instruction, *Function, *Target);
+ Context.expect(Selection.has_value(), "RV64GC overflow operation must select");
+ Context.expect(Selection && validatesSelectedFunction(*Target, *Function),
+ "overflow sequence must satisfy all target descriptors");
+ Context.expect(Function->allInstructions().size() >= 2,
+ "overflow operation must materialize its status result");
+ }
+ const std::array ExtendedOpcodes{rcc::GenericOpcode::G_UADDE, rcc::GenericOpcode::G_USUBE,
+ rcc::GenericOpcode::G_SADDE, rcc::GenericOpcode::G_SSUBE};
+ for (rcc::GenericOpcode Generic : ExtendedOpcodes) {
+ auto Function =
+ createRegisterOnlyFunction(Context, *Target, Generic, {*I64, *I1}, {*I64, *I64, *I1});
+ Context.expect(Function != nullptr, "extended overflow fixture must be complete");
+ if (!Function)
+ continue;
+ rcc::MachineInstr *Instruction = Function->firstBlock()->firstInstruction();
+ auto Selection = rcc::riscv::instructionSelector().select(*Instruction, *Function, *Target);
+ Context.expect(Selection.has_value(), "extended RV64GC overflow operation must select");
+ Context.expect(Selection && validatesSelectedFunction(*Target, *Function),
+ "extended overflow sequence must satisfy all target descriptors");
+ }
+}
+
+void testAtomicSelection(TestContext &Context) {
+ auto Target = rcc::riscv::createTargetDescription();
+ if (!Target)
+ return;
+ auto I64 = rcc::LLT::makeInteger(64);
+ auto Pointer = rcc::LLT::makePointer(0, 64);
+ auto Size = rcc::MemorySize::fixed(8);
+ auto Align = rcc::Alignment::create(8);
+ if (!I64 || !Pointer || !Size || !Align)
+ return;
+
+ const auto MakeRMW = [&](rcc::GenericOpcode Generic, std::uint64_t ID) -> InstructionFixture {
+ auto Function =
+ rcc::MachineFunction::create("riscv_atomic", rcc::MachineFunctionStableID::fromValue(ID));
+ if (!Function)
+ return {};
+ auto Block = (*Function)->createBasicBlock();
+ if (!Block || !(*Function)->appendBlock(**Block))
+ return {};
+ auto Result = (*Function)->registerInfo().createVirtualRegister(*I64);
+ auto Address = (*Function)->registerInfo().createVirtualRegister(*Pointer);
+ auto Value = (*Function)->registerInfo().createVirtualRegister(*I64);
+ if (!Result || !Address || !Value)
+ 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();
+ Description.Offset = 32;
+ auto Memory = (*Function)->createMemOperand(std::move(Description));
+ if (!Memory)
+ return {};
+ rcc::MachineIRBuilder Builder(**Function, *Target);
+ if (!Builder.setInsertPoint(**Block))
+ return {};
+ auto Pending = Builder.buildInstr(rcc::opcode(Generic));
+ if (!Pending || !Pending->addDef(*Result) || !Pending->addUse(*Address) ||
+ !Pending->addUse(*Value) || !Pending->addMemOperand(**Memory)) {
+ return {};
+ }
+ auto Built = Pending->build();
+ if (!Built || !constrainVirtualRegisters(**Function))
+ return {};
+ return {std::move(*Function), *Built, *Result};
+ };
+
+ auto Add = MakeRMW(rcc::GenericOpcode::G_ATOMICRMW_ADD, 1300);
+ auto Sub = MakeRMW(rcc::GenericOpcode::G_ATOMICRMW_SUB, 1301);
+ auto Nand = MakeRMW(rcc::GenericOpcode::G_ATOMICRMW_NAND, 1302);
+ Context.expect(Add.Instruction != nullptr && Sub.Instruction != nullptr &&
+ Nand.Instruction != nullptr,
+ "atomic RMW fixtures must be complete");
+ if (Add.Instruction != nullptr) {
+ auto Selection =
+ rcc::riscv::instructionSelector().select(*Add.Instruction, *Add.Function, *Target);
+ Context.expect(Selection.has_value() && Add.Instruction->opcode() ==
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::AMOADD_D)),
+ "G_ATOMICRMW_ADD must select AMOADD.D");
+ Context.expect(Selection && validatesSelectedFunction(*Target, *Add.Function),
+ "selected AMOADD.D must satisfy its descriptor and MMO contract");
+ }
+ if (Sub.Instruction != nullptr) {
+ auto Selection =
+ rcc::riscv::instructionSelector().select(*Sub.Instruction, *Sub.Function, *Target);
+ Context.expect(Selection.has_value() && Sub.Function->allInstructions().size() == 2,
+ "G_ATOMICRMW_SUB must negate its operand before AMOADD.D");
+ Context.expect(Selection && validatesSelectedFunction(*Target, *Sub.Function),
+ "selected atomic subtraction must satisfy target descriptors");
+ }
+ if (Nand.Instruction != nullptr) {
+ auto Classification =
+ rcc::riscv::legalizerInfo().classify(*Nand.Instruction, *Nand.Function, *Target);
+ Context.expect(
+ Classification && *Classification == rcc::LegalizationAction::Unsupported,
+ "an atomic operation without a single-instruction RV64GC mapping must be explicit");
+ auto Match =
+ rcc::riscv::instructionSelector().canSelect(*Nand.Instruction, *Nand.Function, *Target);
+ Context.expect(!Match && Match.error().code() == rcc::ErrorCode::Unsupported,
+ "unsupported atomic RMW must fail before mutating MachineIR");
+ Context.expect(!Match && Match.error().message().find("G_ATOMICRMW_NAND") != std::string::npos,
+ "unsupported atomic diagnostics must name the rejected opcode");
+ }
+
+ const auto MakeAtomicAccess = [&](bool Load, std::uint64_t ID) -> InstructionFixture {
+ auto Function = rcc::MachineFunction::create(Load ? "riscv_atomic_load" : "riscv_atomic_store",
+ rcc::MachineFunctionStableID::fromValue(ID));
+ if (!Function)
+ return {};
+ auto Block = (*Function)->createBasicBlock();
+ if (!Block || !(*Function)->appendBlock(**Block))
+ return {};
+ auto Value = (*Function)->registerInfo().createVirtualRegister(*I64);
+ auto Address = (*Function)->registerInfo().createVirtualRegister(*Pointer);
+ if (!Value || !Address)
+ return {};
+ rcc::MachineMemOperandDesc Description;
+ Description.Accesses = {Load ? rcc::MemoryAccessFlag::Load : rcc::MemoryAccessFlag::Store};
+ Description.AtomicKind = Load ? rcc::AtomicOperation::Load : rcc::AtomicOperation::Store;
+ Description.SuccessOrdering =
+ Load ? rcc::AtomicOrdering::Acquire : rcc::AtomicOrdering::Release;
+ Description.Size = *Size;
+ Description.Align = *Align;
+ Description.Base = rcc::MemoryBase::stack();
+ auto Memory = (*Function)->createMemOperand(std::move(Description));
+ if (!Memory)
+ return {};
+ rcc::MachineIRBuilder Builder(**Function, *Target);
+ if (!Builder.setInsertPoint(**Block))
+ return {};
+ auto Pending = Builder.buildInstr(
+ rcc::opcode(Load ? rcc::GenericOpcode::G_LOAD : rcc::GenericOpcode::G_STORE));
+ if (!Pending)
+ return {};
+ if (Load) {
+ if (!Pending->addDef(*Value) || !Pending->addUse(*Address))
+ return {};
+ } else if (!Pending->addUse(*Value) || !Pending->addUse(*Address)) {
+ return {};
+ }
+ if (!Pending->addMemOperand(**Memory))
+ return {};
+ auto Built = Pending->build();
+ if (!Built || !constrainVirtualRegisters(**Function))
+ return {};
+ return {std::move(*Function), *Built, *Value};
+ };
+ auto AtomicLoad = MakeAtomicAccess(true, 1303);
+ auto AtomicStore = MakeAtomicAccess(false, 1304);
+ if (AtomicLoad.Instruction != nullptr) {
+ auto Selection = rcc::riscv::instructionSelector().select(*AtomicLoad.Instruction,
+ *AtomicLoad.Function, *Target);
+ Context.expect(Selection && AtomicLoad.Instruction->opcode() ==
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::LR_D)),
+ "atomic G_LOAD must select LR.D");
+ Context.expect(Selection && validatesSelectedFunction(*Target, *AtomicLoad.Function),
+ "selected atomic load must satisfy target descriptors");
+ }
+ if (AtomicStore.Instruction != nullptr) {
+ auto Selection = rcc::riscv::instructionSelector().select(*AtomicStore.Instruction,
+ *AtomicStore.Function, *Target);
+ Context.expect(Selection && AtomicStore.Instruction->opcode() ==
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::AMOSWAP_D)),
+ "atomic G_STORE must select AMOSWAP.D with a discarded old value");
+ Context.expect(Selection && validatesSelectedFunction(*Target, *AtomicStore.Function),
+ "selected atomic store must satisfy target descriptors");
+ }
+}
+
+void testUnsupportedTypes(TestContext &Context) {
+ auto Target = rcc::riscv::createTargetDescription();
+ Context.expect(Target.has_value(), "RV64GC target must be available for type diagnostics");
+ if (!Target)
+ return;
+ auto WideInteger = createBinary(Context, *Target, rcc::GenericOpcode::G_ADD, 128);
+ if (WideInteger.Function && WideInteger.Instruction != nullptr) {
+ auto Selection = rcc::riscv::instructionSelector().canSelect(*WideInteger.Instruction,
+ *WideInteger.Function, *Target);
+ Context.expect(!Selection && Selection.error().code() == rcc::ErrorCode::Unsupported,
+ "i128 arithmetic must produce an Unsupported diagnostic");
+ Context.expect(!Selection && Selection.error().message().find("i128") != std::string::npos,
+ "unsupported type diagnostics must name the rejected LLT");
+ }
+ auto Function = rcc::MachineFunction::create("unsupported_type",
+ rcc::MachineFunctionStableID::fromValue(100));
+ if (!Function)
+ return;
+ auto Block = (*Function)->createBasicBlock();
+ if (!Block || !(*Function)->appendBlock(**Block))
+ return;
+ auto F64 = rcc::LLT::makeFloat(rcc::FloatSemantics::IEEEDouble);
+ if (!F64)
+ return;
+ auto Result = (*Function)->registerInfo().createVirtualRegister(*F64);
+ auto Source = (*Function)->registerInfo().createVirtualRegister(*F64);
+ if (!Result || !Source)
+ return;
+ rcc::MachineIRBuilder Builder(**Function, *Target);
+ if (!Builder.setInsertPoint(**Block))
+ return;
+ auto Pending = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_FEXP));
+ if (!Pending || !Pending->addDef(*Result) || !Pending->addUse(*Source))
+ return;
+ if (!Pending->build())
+ return;
+ const rcc::MachineInstr &Instruction = **Function->get()->allInstructions().begin();
+ auto Action = rcc::riscv::legalizerInfo().classify(Instruction, **Function, *Target);
+ Context.expect(Action && *Action == rcc::LegalizationAction::Unsupported,
+ "unsupported floating operations must remain explicitly diagnosed");
+ auto Selection = rcc::riscv::instructionSelector().canSelect(Instruction, **Function, *Target);
+ Context.expect(!Selection && Selection.error().code() == rcc::ErrorCode::Unsupported,
+ "selector pre-matching must reject unsupported floating operations");
+ Context.expect(!Selection && Selection.error().message().find("G_FEXP") != std::string::npos,
+ "unsupported selection diagnostics must name the rejected opcode");
+}
+
+void testMemoryOffsetSelection(TestContext &Context) {
+ auto Target = rcc::riscv::createTargetDescription();
+ Context.expect(Target.has_value(), "RV64GC target must be available for memory selection");
+ if (!Target)
+ return;
+ auto Function = rcc::MachineFunction::create("riscv_memory_offset",
+ rcc::MachineFunctionStableID::fromValue(101));
+ if (!Function)
+ return;
+ auto Block = (*Function)->createBasicBlock();
+ if (!Block || !(*Function)->appendBlock(**Block))
+ return;
+ auto I64 = rcc::LLT::makeInteger(64);
+ auto Pointer = rcc::LLT::makePointer(0, 64);
+ if (!I64 || !Pointer)
+ return;
+ auto Result = (*Function)->registerInfo().createVirtualRegister(*I64);
+ auto Address = (*Function)->registerInfo().createVirtualRegister(*Pointer);
+ if (!Result || !Address)
+ return;
+ rcc::MachineIRBuilder Builder(**Function, *Target);
+ if (!Builder.setInsertPoint(**Block))
+ return;
+ auto Pending = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_LOAD));
+ if (!Pending || !Pending->addDef(*Result) || !Pending->addUse(*Address))
+ 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::Volatile};
+ Description.Size = *Size;
+ Description.Align = *Align;
+ Description.Base = rcc::MemoryBase::stack();
+ Description.Offset = 24;
+ auto Memory = (*Function)->createMemOperand(std::move(Description));
+ if (!Memory || !Pending->addMemOperand(**Memory) || !Pending->build() ||
+ !constrainVirtualRegisters(**Function))
+ return;
+ rcc::MachineInstr *Instruction = (*Function)->firstBlock()->firstInstruction();
+ Context.expect(Instruction != nullptr, "generic load must remain attached to its block");
+ if (Instruction == nullptr)
+ return;
+ auto Action = rcc::riscv::legalizerInfo().classify(*Instruction, **Function, *Target);
+ Context.expect(Action && *Action == rcc::LegalizationAction::Legal,
+ "RV64GC legalizer must accept volatile scalar loads");
+ auto Selection = rcc::riscv::instructionSelector().select(*Instruction, **Function, *Target);
+ Context.expect(Selection.has_value(), "RV64GC selector must select a volatile scalar load");
+ if (!Selection)
+ return;
+ Context.expect(Instruction->opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::InstructionOpcode::LD)),
+ "i64 load with an offset must select LD");
+ const rcc::MachineOperand *OffsetOperand = Instruction->operandAt(2);
+ const std::int64_t *Offset =
+ OffsetOperand == nullptr ? nullptr : OffsetOperand->getIf<std::int64_t>();
+ Context.expect(Offset != nullptr && *Offset == 0,
+ "selected load must not reinterpret MMO provenance as an address immediate");
+ Context.expect(Instruction->memOperands().size() == 1 &&
+ Instruction->memOperands().front()->description().Offset == 24,
+ "selected load must preserve its MMO provenance offset");
+ Context.expect(validatesSelectedFunction(*Target, **Function),
+ "selected volatile load must satisfy the target descriptor");
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ testClassification(Context);
+ testNarrowIntegerSelection(Context);
+ testScalarSelection(Context);
+ testComparisonsAndControl(Context);
+ testCompositeScalarSelection(Context);
+ testOverflowSelection(Context);
+ testAtomicSelection(Context);
+ testUnsupportedTypes(Context);
+ testMemoryOffsetSelection(Context);
+ return Context.result();
+}