aboutsummaryrefslogtreecommitdiff
path: root/backend/unittests/Target
diff options
context:
space:
mode:
Diffstat (limited to 'backend/unittests/Target')
-rw-r--r--backend/unittests/Target/CMakeLists.txt117
-rw-r--r--backend/unittests/Target/Inputs/RISCVDataPipeline.mir18
-rw-r--r--backend/unittests/Target/Inputs/RISCVGenericPipeline.mir12
-rw-r--r--backend/unittests/Target/Inputs/RISCVStackFramePipeline.mir12
-rw-r--r--backend/unittests/Target/Inputs/RISCVWideConstantPipeline.mir10
-rw-r--r--backend/unittests/Target/RISCVAssemblyPrinterTest.cpp182
-rw-r--r--backend/unittests/Target/RISCVCallLoweringTest.cpp910
-rw-r--r--backend/unittests/Target/RISCVFrameLoweringTest.cpp1365
-rw-r--r--backend/unittests/Target/RISCVLateCodeGenTest.cpp1310
-rw-r--r--backend/unittests/Target/RISCVMCEncodingCoverageTest.cpp413
-rw-r--r--backend/unittests/Target/RISCVMCTest.cpp322
-rw-r--r--backend/unittests/Target/RISCVObjectFileTest.cpp791
-rw-r--r--backend/unittests/Target/RISCVRegisterAllocationTest.cpp539
-rw-r--r--backend/unittests/Target/RISCVSSAExitTest.cpp414
-rw-r--r--backend/unittests/Target/RISCVTargetTest.cpp330
-rw-r--r--backend/unittests/Target/TableGenTest.cpp202
-rw-r--r--backend/unittests/Target/TestTarget.cpp217
-rw-r--r--backend/unittests/Target/TestTarget.h66
18 files changed, 7230 insertions, 0 deletions
diff --git a/backend/unittests/Target/CMakeLists.txt b/backend/unittests/Target/CMakeLists.txt
new file mode 100644
index 0000000..31b48d5
--- /dev/null
+++ b/backend/unittests/Target/CMakeLists.txt
@@ -0,0 +1,117 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+include_directories("${RCC_BACKEND_SOURCE_DIR}/lib/Target/RISCV")
+
+add_library(RCCBackendTestTarget TestTarget.cpp)
+add_library(RCCBackend::TestTarget ALIAS RCCBackendTestTarget)
+
+rcc_backend_configure_target(RCCBackendTestTarget)
+target_include_directories(
+ RCCBackendTestTarget
+ PUBLIC "${RCC_BACKEND_SOURCE_DIR}/unittests"
+)
+target_link_libraries(RCCBackendTestTarget PUBLIC RCCBackend::CodeGen)
+
+add_executable(RCCBackendTableGenTests TableGenTest.cpp)
+rcc_backend_configure_target(RCCBackendTableGenTests)
+target_link_libraries(RCCBackendTableGenTests PRIVATE RCCBackend::TableGenBackends)
+add_test(NAME RCCBackend.TableGen COMMAND RCCBackendTableGenTests)
+
+add_executable(RCCBackendRISCVTargetTests RISCVTargetTest.cpp)
+rcc_backend_configure_target(RCCBackendRISCVTargetTests)
+target_link_libraries(RCCBackendRISCVTargetTests PRIVATE RCCBackend::RISCV)
+add_test(NAME RCCBackendRISCVTargetTests COMMAND RCCBackendRISCVTargetTests)
+
+add_executable(RCCBackendRISCVCallLoweringTests RISCVCallLoweringTest.cpp)
+rcc_backend_configure_target(RCCBackendRISCVCallLoweringTests)
+target_link_libraries(RCCBackendRISCVCallLoweringTests PRIVATE RCCBackend::RISCV)
+add_test(NAME RCCBackendRISCVCallLoweringTests COMMAND RCCBackendRISCVCallLoweringTests)
+
+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)
+
+add_executable(RCCBackendRISCVRegisterAllocationTests RISCVRegisterAllocationTest.cpp)
+rcc_backend_configure_target(RCCBackendRISCVRegisterAllocationTests)
+target_link_libraries(
+ RCCBackendRISCVRegisterAllocationTests
+ PRIVATE RCCBackend::RISCV RCCBackend::CodeGen
+)
+add_test(
+ NAME RCCBackendRISCVRegisterAllocationTests
+ COMMAND RCCBackendRISCVRegisterAllocationTests
+)
+
+add_executable(RCCBackendRISCVFrameLoweringTests RISCVFrameLoweringTest.cpp)
+rcc_backend_configure_target(RCCBackendRISCVFrameLoweringTests)
+target_link_libraries(
+ RCCBackendRISCVFrameLoweringTests
+ PRIVATE RCCBackend::RISCV RCCBackend::CodeGen
+)
+add_test(
+ NAME RCCBackendRISCVFrameLoweringTests
+ COMMAND RCCBackendRISCVFrameLoweringTests
+)
+
+add_executable(RCCBackendRISCVLateCodeGenTests RISCVLateCodeGenTest.cpp)
+rcc_backend_configure_target(RCCBackendRISCVLateCodeGenTests)
+target_link_libraries(
+ RCCBackendRISCVLateCodeGenTests
+ PRIVATE RCCBackend::RISCV RCCBackend::CodeGen
+)
+add_test(
+ NAME RCCBackendRISCVLateCodeGenTests
+ COMMAND RCCBackendRISCVLateCodeGenTests
+)
+
+add_executable(RCCBackendRISCVAssemblyPrinterTests RISCVAssemblyPrinterTest.cpp)
+rcc_backend_configure_target(RCCBackendRISCVAssemblyPrinterTests)
+target_link_libraries(
+ RCCBackendRISCVAssemblyPrinterTests
+ PRIVATE RCCBackend::RISCV RCCBackend::CodeGen
+)
+add_test(
+ NAME RCCBackendRISCVAssemblyPrinterTests
+ COMMAND RCCBackendRISCVAssemblyPrinterTests
+)
+
+add_executable(RCCBackendRISCVMCTests RISCVMCTest.cpp)
+rcc_backend_configure_target(RCCBackendRISCVMCTests)
+target_link_libraries(RCCBackendRISCVMCTests PRIVATE RCCBackend::RISCV RCCBackend::MC)
+add_test(NAME RCCBackendRISCVMCTests COMMAND RCCBackendRISCVMCTests)
+
+add_executable(RCCBackendRISCVMCEncodingCoverageTests RISCVMCEncodingCoverageTest.cpp)
+rcc_backend_configure_target(RCCBackendRISCVMCEncodingCoverageTests)
+target_link_libraries(
+ RCCBackendRISCVMCEncodingCoverageTests
+ PRIVATE RCCBackend::RISCV RCCBackend::MC
+)
+add_test(
+ NAME RCCBackendRISCVMCEncodingCoverageTests
+ COMMAND RCCBackendRISCVMCEncodingCoverageTests
+)
+
+add_executable(RCCBackendRISCVObjectFileTests RISCVObjectFileTest.cpp)
+rcc_backend_configure_target(RCCBackendRISCVObjectFileTests)
+target_link_libraries(
+ RCCBackendRISCVObjectFileTests
+ PRIVATE RCCBackend::RISCV RCCBackend::MC RCCBackend::CodeGen
+)
+add_test(NAME RCCBackendRISCVObjectFileTests COMMAND RCCBackendRISCVObjectFileTests)
+
+rcc_backend_register_format_files(
+ "${CMAKE_CURRENT_SOURCE_DIR}/TableGenTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/RISCVCallLoweringTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/RISCVTargetTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/RISCVSSAExitTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/RISCVRegisterAllocationTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/RISCVFrameLoweringTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/RISCVLateCodeGenTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/RISCVAssemblyPrinterTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/RISCVMCTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/RISCVMCEncodingCoverageTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/RISCVObjectFileTest.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/TestTarget.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/TestTarget.h"
+)
diff --git a/backend/unittests/Target/Inputs/RISCVDataPipeline.mir b/backend/unittests/Target/Inputs/RISCVDataPipeline.mir
new file mode 100644
index 0000000..0d0c35d
--- /dev/null
+++ b/backend/unittests/Target/Inputs/RISCVDataPipeline.mir
@@ -0,0 +1,18 @@
+---
+name: data_pipeline
+tracksRegLiveness: true
+isSSA: true
+constants:
+ - id: 0
+ value: 'i64 1234605616436508552'
+ alignment: 8
+jumpTable:
+ kind: block-address
+ entries:
+ - { id: 0, blocks: [ '%bb.0' ] }
+body: |
+ bb.0:
+ liveins: $x1
+ %0:gprb(i64) = G_CONSTANT 7
+ $x10 = COPY %0
+ PseudoRET implicit $x1, implicit $x10
diff --git a/backend/unittests/Target/Inputs/RISCVGenericPipeline.mir b/backend/unittests/Target/Inputs/RISCVGenericPipeline.mir
new file mode 100644
index 0000000..62c1baf
--- /dev/null
+++ b/backend/unittests/Target/Inputs/RISCVGenericPipeline.mir
@@ -0,0 +1,12 @@
+---
+name: generic_pipeline
+tracksRegLiveness: true
+isSSA: true
+body: |
+ bb.0:
+ liveins: $x1
+ %0:gprb(i64) = G_CONSTANT 42
+ %1:gprb(i64) = G_CONSTANT 1
+ %2:gprb(i64) = G_ADD %0, %1
+ $x10 = COPY %2
+ PseudoRET implicit $x1, implicit $x10
diff --git a/backend/unittests/Target/Inputs/RISCVStackFramePipeline.mir b/backend/unittests/Target/Inputs/RISCVStackFramePipeline.mir
new file mode 100644
index 0000000..4f37d15
--- /dev/null
+++ b/backend/unittests/Target/Inputs/RISCVStackFramePipeline.mir
@@ -0,0 +1,12 @@
+---
+name: stack_frame_pipeline
+tracksRegLiveness: true
+isSSA: true
+stack:
+ - { id: 0, size: 32, alignment: 16 }
+body: |
+ bb.0:
+ liveins: $x1
+ %0:gprb(i64) = G_CONSTANT 9
+ $x10 = COPY %0
+ PseudoRET implicit $x1, implicit $x10
diff --git a/backend/unittests/Target/Inputs/RISCVWideConstantPipeline.mir b/backend/unittests/Target/Inputs/RISCVWideConstantPipeline.mir
new file mode 100644
index 0000000..1b9ee82
--- /dev/null
+++ b/backend/unittests/Target/Inputs/RISCVWideConstantPipeline.mir
@@ -0,0 +1,10 @@
+---
+name: wide_constant_pipeline
+tracksRegLiveness: true
+isSSA: true
+body: |
+ bb.0:
+ liveins: $x1
+ %0:gprb(i64) = G_CONSTANT 9223372036854775807
+ $x10 = COPY %0
+ PseudoRET implicit $x1, implicit $x10
diff --git a/backend/unittests/Target/RISCVAssemblyPrinterTest.cpp b/backend/unittests/Target/RISCVAssemblyPrinterTest.cpp
new file mode 100644
index 0000000..3794227
--- /dev/null
+++ b/backend/unittests/Target/RISCVAssemblyPrinterTest.cpp
@@ -0,0 +1,182 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/Target/RISCV/RISCVAssemblyPrinter.h"
+
+#include "rcc/CodeGen/CompilationUnit.h"
+#include "rcc/CodeGen/GlobalObject.h"
+#include "rcc/Target/RISCV/RISCVTarget.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <iostream>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace {
+
+int fail(std::string_view Message) {
+ std::cerr << Message << '\n';
+ return 1;
+}
+
+[[nodiscard]] bool contains(std::string_view Text, std::string_view Needle) {
+ return Text.find(Needle) != std::string_view::npos;
+}
+
+[[nodiscard]] rcc::Expected<rcc::GlobalObjectDesc>
+makeObject(std::string Symbol, std::uint64_t Size, std::uint64_t Alignment,
+ rcc::GlobalInitializer Initializer, bool IsConstant = false) {
+ auto Align = rcc::Alignment::create(Alignment);
+ if (!Align)
+ return std::unexpected(Align.error());
+ rcc::GlobalObjectDesc Object;
+ Object.Symbol = std::move(Symbol);
+ Object.ObjectType = rcc::GenericConstantType::byte();
+ Object.Align = *Align;
+ Object.Size = Size;
+ Object.IsConstant = IsConstant;
+ Object.Initializer.emplace(std::move(Initializer));
+ auto Verification = Object.verify();
+ if (!Verification)
+ return std::unexpected(Verification.error());
+ return Object;
+}
+
+[[nodiscard]] rcc::Expected<void> addObjects(rcc::CompilationUnit &Unit) {
+ auto AnswerValue = rcc::APInt::fromUnsigned(32, 42);
+ if (!AnswerValue)
+ return std::unexpected(AnswerValue.error());
+ auto Answer =
+ makeObject("answer", 4, 4, rcc::GlobalInitializer::integer(std::move(*AnswerValue)), true);
+ if (!Answer)
+ return std::unexpected(Answer.error());
+ Answer->Visibility = rcc::GlobalVisibility::Hidden;
+ if (!Unit.addGlobalObject(std::move(*Answer)))
+ return rcc::makeUnexpected(rcc::ErrorCode::InternalError, "failed to add answer");
+
+ auto Zero = rcc::GlobalInitializer::zeroFill(16);
+ if (!Zero)
+ return std::unexpected(Zero.error());
+ auto Buffer = makeObject("zero_buffer", 16, 16, std::move(*Zero));
+ if (!Buffer)
+ return std::unexpected(Buffer.error());
+ Buffer->Linkage = rcc::GlobalLinkage::Internal;
+ if (!Unit.addGlobalObject(std::move(*Buffer)))
+ return rcc::makeUnexpected(rcc::ErrorCode::InternalError, "failed to add buffer");
+
+ auto TLSZero = rcc::GlobalInitializer::zeroFill(8);
+ if (!TLSZero)
+ return std::unexpected(TLSZero.error());
+ auto TLS = makeObject("tls_counter", 8, 8, std::move(*TLSZero));
+ if (!TLS)
+ return std::unexpected(TLS.error());
+ TLS->TLSModel = rcc::GlobalTLSModel::LocalExec;
+ if (!Unit.addGlobalObject(std::move(*TLS)))
+ return rcc::makeUnexpected(rcc::ErrorCode::InternalError, "failed to add TLS object");
+
+ auto Address = rcc::GlobalInitializer::symbolAddress(
+ rcc::GlobalSymbolAddress{.Symbol = "answer", .Addend = 8, .Size = 8, .TargetFlags = 0});
+ if (!Address)
+ return std::unexpected(Address.error());
+ auto Pointer = makeObject("answer_pointer", 8, 8, std::move(*Address), true);
+ if (!Pointer)
+ return std::unexpected(Pointer.error());
+ Pointer->Linkage = rcc::GlobalLinkage::WeakAny;
+ if (!Unit.addGlobalObject(std::move(*Pointer)))
+ return rcc::makeUnexpected(rcc::ErrorCode::InternalError, "failed to add pointer");
+
+ auto Bytes = rcc::GlobalInitializer::bytes(
+ {std::byte{0x52}, std::byte{0x43}, std::byte{0x43}, std::byte{0x00}});
+ if (!Bytes)
+ return std::unexpected(Bytes.error());
+ auto Grouped = makeObject("grouped_data", 4, 4, std::move(*Bytes));
+ if (!Grouped)
+ return std::unexpected(Grouped.error());
+ Grouped->Linkage = rcc::GlobalLinkage::LinkOnceODR;
+ Grouped->COMDAT = "grouped_data";
+ if (!Unit.addGlobalObject(std::move(*Grouped)))
+ return rcc::makeUnexpected(rcc::ErrorCode::InternalError, "failed to add COMDAT object");
+
+ auto CommonAlign = rcc::Alignment::create(16);
+ if (!CommonAlign)
+ return std::unexpected(CommonAlign.error());
+ rcc::GlobalObjectDesc Common;
+ Common.Symbol = "tentative";
+ Common.ObjectType = rcc::GenericConstantType::byte();
+ Common.Linkage = rcc::GlobalLinkage::Common;
+ Common.Align = *CommonAlign;
+ Common.Size = 32;
+ if (!Unit.addGlobalObject(std::move(Common)))
+ return rcc::makeUnexpected(rcc::ErrorCode::InternalError, "failed to add common object");
+ return {};
+}
+
+} // namespace
+
+int main(int ArgumentCount, char **Arguments) {
+ auto Target = rcc::riscv::createTargetDescription();
+ if (!Target)
+ return fail(Target.error().message());
+
+ rcc::CompilationUnit Unit;
+ auto Added = addObjects(Unit);
+ if (!Added)
+ return fail(Added.error().message());
+ auto Assembly = rcc::riscv::emitAssembly(Unit, *Target);
+ if (!Assembly)
+ return fail(Assembly.error().message());
+
+ if (ArgumentCount == 2 && std::string_view(Arguments[1]) == "--print") {
+ std::cout << *Assembly;
+ return 0;
+ }
+
+ const std::string_view Text = *Assembly;
+ if (!contains(Text, ".section .rodata,\"a\",@progbits\n") || !contains(Text, ".globl answer\n") ||
+ !contains(Text, ".hidden answer\n") || !contains(Text, ".type answer, @object\n") ||
+ !contains(Text, "answer:\n") || !contains(Text, ".byte 0x2a, 0x00, 0x00, 0x00\n") ||
+ !contains(Text, ".size answer, 4\n")) {
+ return fail("read-only global assembly is incomplete");
+ }
+ if (!contains(Text, ".section .bss,\"aw\",@nobits\n") ||
+ !contains(Text, ".local zero_buffer\n") || !contains(Text, "zero_buffer:\n") ||
+ !contains(Text, ".zero 16\n")) {
+ return fail("BSS global assembly is incomplete");
+ }
+ if (!contains(Text, ".section .tbss,\"awT\",@nobits\n") ||
+ !contains(Text, ".type tls_counter, @tls_object\n")) {
+ return fail("TLS global assembly is incomplete");
+ }
+ if (!contains(Text, ".section .data.rel.ro,\"aw\",@progbits\n") ||
+ !contains(Text, ".weak answer_pointer\n") || !contains(Text, ".8byte (answer+8)\n")) {
+ return fail("relocatable constant global assembly is incomplete");
+ }
+ if (!contains(Text, ".section .data.grouped_data,\"awG\",@progbits,grouped_data,comdat\n") ||
+ !contains(Text, ".weak grouped_data\n")) {
+ return fail("COMDAT global assembly is incomplete");
+ }
+ if (!contains(Text, ".comm tentative,32,16\n"))
+ return fail("common global assembly is incomplete");
+ if (!contains(Text, ".attribute arch, \"rv64gc\"\n") ||
+ !contains(Text, ".section \".note.GNU-stack\",\"\",@progbits\n")) {
+ std::cerr << Text;
+ return fail("RV64GC file directives are incomplete");
+ }
+
+ rcc::CompilationUnit UnsupportedUnit;
+ auto UnsupportedZero = rcc::GlobalInitializer::zeroFill(1);
+ if (!UnsupportedZero)
+ return fail("unsupported global test initializer creation failed");
+ auto UnsupportedObject = makeObject("unsupported_global", 1, 1, std::move(*UnsupportedZero));
+ if (!UnsupportedObject)
+ return fail("unsupported global test object creation failed");
+ UnsupportedObject->TargetFlags = 1;
+ if (!UnsupportedUnit.addGlobalObject(std::move(*UnsupportedObject)))
+ return fail("unsupported global test insertion failed");
+ auto UnsupportedAssembly = rcc::riscv::emitAssembly(UnsupportedUnit, *Target);
+ if (UnsupportedAssembly || UnsupportedAssembly.error().code() != rcc::ErrorCode::Unsupported)
+ return fail("RV64GC assembly must reject unknown global target state");
+ return 0;
+}
diff --git a/backend/unittests/Target/RISCVCallLoweringTest.cpp b/backend/unittests/Target/RISCVCallLoweringTest.cpp
new file mode 100644
index 0000000..738aeb1
--- /dev/null
+++ b/backend/unittests/Target/RISCVCallLoweringTest.cpp
@@ -0,0 +1,910 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "GISel/RISCVCallLowering.h"
+
+#include "rcc/CodeGen/GenericOpcodes.h"
+#include "rcc/CodeGen/MachineBasicBlock.h"
+#include "rcc/CodeGen/MachineFrameInfo.h"
+#include "rcc/CodeGen/MachineFunction.h"
+#include "rcc/CodeGen/MachineIRBuilder.h"
+#include "rcc/CodeGen/MachineInstr.h"
+#include "rcc/CodeGen/MachineMemOperand.h"
+#include "rcc/CodeGen/MachineOperand.h"
+#include "rcc/CodeGen/MachineVerifier.h"
+#include "rcc/Target/RISCV/RISCVSubtarget.h"
+#include "rcc/Target/RISCV/RISCVTarget.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <exception>
+#include <iostream>
+#include <memory>
+#include <optional>
+#include <ranges>
+#include <span>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace {
+
+const rcc::riscv::RISCVSubtarget Subtarget(rcc::riscv::RISCVSubtargetFeatures::rv64gc());
+const rcc::TargetCallLowering &CallLowering = *Subtarget.getCallLowering();
+
+class TestContext final {
+public:
+ void expect(bool Condition, std::string_view Message) {
+ if (Condition)
+ return;
+ std::cerr << Message << '\n';
+ ++FailureCount;
+ }
+
+ [[nodiscard]] int result() const noexcept {
+ return FailureCount == 0 ? 0 : 1;
+ }
+
+private:
+ int FailureCount = 0;
+};
+
+[[nodiscard]] rcc::CallingConventionID convention() noexcept {
+ return rcc::riscv::callingConventionID(rcc::riscv::CallingConvention::LP64D);
+}
+
+[[nodiscard]] rcc::Alignment align(std::uint64_t Bytes) {
+ const auto Value = rcc::Alignment::create(Bytes);
+ if (!Value)
+ std::terminate();
+ return *Value;
+}
+
+[[nodiscard]] rcc::LowLevelType integer(std::uint32_t Bits) {
+ const auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(Bits);
+ if (!Type)
+ std::terminate();
+ return *Type;
+}
+
+[[nodiscard]] rcc::LowLevelType anyScalar(std::uint32_t Bits) {
+ const auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(Bits);
+ if (!Type)
+ std::terminate();
+ return *Type;
+}
+
+[[nodiscard]] rcc::LowLevelType floating(rcc::FloatSemantics Semantics) {
+ const std::uint32_t Bits = Semantics == rcc::FloatSemantics::IEEESingle ? 32 : 64;
+ const auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(Bits);
+ if (!Type)
+ std::terminate();
+ return *Type;
+}
+
+[[nodiscard]] rcc::LowLevelType pointer() {
+ const auto Type = rcc::LowLevelType::makePointer(0, 64);
+ if (!Type)
+ std::terminate();
+ return *Type;
+}
+
+[[nodiscard]] rcc::PhysicalRegister physical(rcc::riscv::PhysicalRegisterID ID) {
+ const auto Register = rcc::riscv::physicalRegister(ID);
+ if (!Register)
+ std::terminate();
+ return *Register;
+}
+
+[[nodiscard]] rcc::PhysicalRegister argumentGPR(std::size_t Index) {
+ const auto *Convention = rcc::riscv::callingConvention(rcc::riscv::CallingConvention::LP64D);
+ if (Convention == nullptr || Index >= Convention->IntegerArgumentRegisters.size())
+ std::terminate();
+ return Convention->IntegerArgumentRegisters[Index];
+}
+
+[[nodiscard]] rcc::PhysicalRegister argumentFPR(std::size_t Index) {
+ const auto *Convention = rcc::riscv::callingConvention(rcc::riscv::CallingConvention::LP64D);
+ if (Convention == nullptr || Index >= Convention->FloatingArgumentRegisters.size())
+ std::terminate();
+ return Convention->FloatingArgumentRegisters[Index];
+}
+
+[[nodiscard]] rcc::PhysicalRegister returnGPR(std::size_t Index) {
+ const auto *Convention = rcc::riscv::callingConvention(rcc::riscv::CallingConvention::LP64D);
+ if (Convention == nullptr || Index >= Convention->IntegerReturnRegisters.size())
+ std::terminate();
+ return Convention->IntegerReturnRegisters[Index];
+}
+
+[[nodiscard]] rcc::PhysicalRegister returnFPR(std::size_t Index) {
+ const auto *Convention = rcc::riscv::callingConvention(rcc::riscv::CallingConvention::LP64D);
+ if (Convention == nullptr || Index >= Convention->FloatingReturnRegisters.size())
+ std::terminate();
+ return Convention->FloatingReturnRegisters[Index];
+}
+
+[[nodiscard]] rcc::Register planningRegister() {
+ return rcc::Register::fromPhysical(physical(rcc::riscv::PhysicalRegisterID::X5));
+}
+
+[[nodiscard]] rcc::ABIValue abiValue(rcc::LowLevelType Type, std::uint32_t OriginalValue,
+ rcc::Register Value = planningRegister(),
+ rcc::ABIExtensionKind Extension = rcc::ABIExtensionKind::None,
+ std::optional<rcc::ABIValueCategory> Category = std::nullopt) {
+ const std::uint64_t Size = std::max<std::uint64_t>(1, (Type.scalarBitWidth() + 7U) / 8U);
+ return {
+ .Value = Value,
+ .Type = Type,
+ .Category = Category.value_or(Type.isPointer() ? rcc::ABIValueCategory::Pointer
+ : rcc::ABIValueCategory::Integer),
+ .OriginalValue = OriginalValue,
+ .FragmentIndex = 0,
+ .FragmentCount = 1,
+ .FragmentOffsetBytes = 0,
+ .OriginalAlignment = align(std::min<std::uint64_t>(Size, 8)),
+ .Extension = Extension,
+ .Flags = {},
+ .ByValSizeBytes = 0,
+ .ByValAlignment = align(1),
+ };
+}
+
+[[nodiscard]] rcc::ABIValue floatingABIValue(rcc::FloatSemantics Semantics,
+ std::uint32_t OriginalValue,
+ rcc::Register Value = planningRegister()) {
+ return abiValue(floating(Semantics), OriginalValue, Value, rcc::ABIExtensionKind::None,
+ rcc::ABIValueCategory::FloatingPoint);
+}
+
+[[nodiscard]] std::vector<rcc::ABIValue> integerValues(std::size_t Count) {
+ std::vector<rcc::ABIValue> Values;
+ Values.reserve(Count);
+ for (std::size_t Index = 0; Index < Count; ++Index)
+ Values.push_back(abiValue(integer(64), static_cast<std::uint32_t>(Index)));
+ return Values;
+}
+
+[[nodiscard]] std::vector<rcc::ABIValue> floatingValues(std::size_t Count) {
+ std::vector<rcc::ABIValue> Values;
+ Values.reserve(Count);
+ for (std::size_t Index = 0; Index < Count; ++Index) {
+ Values.push_back(
+ floatingABIValue(rcc::FloatSemantics::IEEEDouble, static_cast<std::uint32_t>(Index)));
+ }
+ return Values;
+}
+
+[[nodiscard]] rcc::MachineCallTarget externalTarget() {
+ auto Target = rcc::MachineCallTarget::externalSymbol("callee");
+ if (!Target)
+ std::terminate();
+ return std::move(*Target);
+}
+
+[[nodiscard]] rcc::CallLoweringRequest callRequest(std::vector<rcc::ABIValue> Arguments,
+ std::size_t FixedArgumentCount,
+ bool IsVariadic = false) {
+ rcc::CallLoweringRequest Request;
+ Request.Convention = convention();
+ Request.Callee = externalTarget();
+ Request.Arguments = std::move(Arguments);
+ Request.FixedArgumentCount = FixedArgumentCount;
+ Request.IsVariadic = IsVariadic;
+ return Request;
+}
+
+[[nodiscard]] bool isLocation(const rcc::CCAssignment &Assignment, rcc::PhysicalRegister Register) {
+ return Assignment.LocationRegister && *Assignment.LocationRegister == Register;
+}
+
+void testIntegerAndFloatingAssignments(TestContext &Context) {
+ auto Integers = callRequest(integerValues(9), 9);
+ auto IntegerPlan = CallLowering.planCall(Integers);
+ Context.expect(IntegerPlan.has_value(), "nine integer arguments must produce an LP64D call plan");
+ if (!IntegerPlan)
+ return;
+ const auto IntegerAssignments = IntegerPlan->argumentAssignments();
+ const auto LP64DMask = rcc::riscv::createCallPreservedMask(rcc::riscv::CallingConvention::LP64D);
+ bool IntegerRegistersMatch = IntegerAssignments.size() == 9;
+ for (std::size_t Index = 0; IntegerRegistersMatch && Index < 8; ++Index)
+ IntegerRegistersMatch = isLocation(IntegerAssignments[Index], argumentGPR(Index));
+ Context.expect(IntegerRegistersMatch &&
+ IntegerAssignments[8].LocationKind == rcc::CCLocationKind::Stack &&
+ IntegerAssignments[8].StackOffsetBytes == 0 &&
+ IntegerPlan->stackArguments().SizeBytes == 16 &&
+ IntegerPlan->callFrame().SizeBytes == 16 &&
+ IntegerPlan->callee()->targetFlags() ==
+ static_cast<std::uint32_t>(rcc::riscv::CallTargetFlag::DirectCall) &&
+ LP64DMask != nullptr && IntegerPlan->preservedMask() != nullptr &&
+ *IntegerPlan->preservedMask() == *LP64DMask,
+ "LP64D integer arguments must use a0-a7 before a 16-byte stack area");
+
+ auto Floats = callRequest(floatingValues(17), 17);
+ auto FloatPlan = CallLowering.planCall(Floats);
+ Context.expect(FloatPlan.has_value(),
+ "seventeen floating arguments must produce an LP64D call plan");
+ if (!FloatPlan)
+ return;
+ const auto FloatAssignments = FloatPlan->argumentAssignments();
+ bool FloatingRegistersMatch = FloatAssignments.size() == 17;
+ for (std::size_t Index = 0; FloatingRegistersMatch && Index < 8; ++Index)
+ FloatingRegistersMatch = isLocation(FloatAssignments[Index], argumentFPR(Index));
+ for (std::size_t Index = 0; FloatingRegistersMatch && Index < 8; ++Index) {
+ FloatingRegistersMatch =
+ isLocation(FloatAssignments[Index + 8], argumentGPR(Index)) &&
+ FloatAssignments[Index + 8].CustomFlags.has(rcc::CCAssignmentFlag::Bitcast);
+ }
+ Context.expect(FloatingRegistersMatch &&
+ FloatAssignments[16].LocationKind == rcc::CCLocationKind::Stack &&
+ FloatAssignments[16].StackOffsetBytes == 0,
+ "LP64D floating arguments must fall back from fa0-fa7 to a0-a7 and then stack");
+
+ auto Single = callRequest({floatingABIValue(rcc::FloatSemantics::IEEESingle, 0)}, 1);
+ Single.Results = {floatingABIValue(rcc::FloatSemantics::IEEESingle, 0)};
+ auto SinglePlan = CallLowering.planCall(Single);
+ Context.expect(SinglePlan && SinglePlan->argumentAssignments().size() == 1 &&
+ SinglePlan->returnAssignments().size() == 1 &&
+ isLocation(SinglePlan->argumentAssignments()[0],
+ physical(rcc::riscv::PhysicalRegisterID::F10_F)) &&
+ isLocation(SinglePlan->returnAssignments()[0],
+ physical(rcc::riscv::PhysicalRegisterID::F10_F)),
+ "LP64D f32 arguments and returns must use the FPR32 physical-register view");
+
+ auto FloatFragment = floatingABIValue(rcc::FloatSemantics::IEEEDouble, 0);
+ FloatFragment.FragmentCount = 2;
+ FloatFragment.OriginalAlignment = align(16);
+ auto IntegerFragment = abiValue(integer(64), 0);
+ IntegerFragment.FragmentIndex = 1;
+ IntegerFragment.FragmentCount = 2;
+ IntegerFragment.FragmentOffsetBytes = 8;
+ IntegerFragment.OriginalAlignment = align(16);
+ auto Mixed = callRequest({FloatFragment, IntegerFragment}, 1);
+ auto MixedPlan = CallLowering.planCall(Mixed);
+ Context.expect(MixedPlan && MixedPlan->argumentAssignments().size() == 2 &&
+ isLocation(MixedPlan->argumentAssignments()[0], argumentFPR(0)) &&
+ isLocation(MixedPlan->argumentAssignments()[1], argumentGPR(0)),
+ "a mixed floating/integer aggregate must use one FPR and one GPR");
+
+ auto GenericScalar = callRequest({abiValue(anyScalar(64), 0)}, 1);
+ GenericScalar.Results = {abiValue(anyScalar(64), 0)};
+ auto GenericScalarPlan = CallLowering.planCall(GenericScalar);
+ Context.expect(GenericScalarPlan && GenericScalarPlan->argumentAssignments().size() == 1 &&
+ GenericScalarPlan->returnAssignments().size() == 1 &&
+ GenericScalarPlan->argumentAssignments()[0].ValueType == anyScalar(64) &&
+ GenericScalarPlan->returnAssignments()[0].ValueType == anyScalar(64),
+ "LLVM generic scalar LLTs must use integer ABI transport without type loss");
+
+ auto LP64Float = callRequest({floatingABIValue(rcc::FloatSemantics::IEEEDouble, 0)}, 1);
+ LP64Float.Convention = rcc::riscv::callingConventionID(rcc::riscv::CallingConvention::LP64);
+ LP64Float.Results = {floatingABIValue(rcc::FloatSemantics::IEEEDouble, 0)};
+ auto LP64Plan = CallLowering.planCall(LP64Float);
+ const auto LP64Mask = rcc::riscv::createCallPreservedMask(rcc::riscv::CallingConvention::LP64);
+ Context.expect(
+ LP64Plan && LP64Plan->argumentAssignments().size() == 1 &&
+ LP64Plan->returnAssignments().size() == 1 &&
+ isLocation(LP64Plan->argumentAssignments()[0], argumentGPR(0)) &&
+ isLocation(LP64Plan->returnAssignments()[0], returnGPR(0)) &&
+ LP64Plan->argumentAssignments()[0].CustomFlags.has(rcc::CCAssignmentFlag::Bitcast) &&
+ LP64Plan->returnAssignments()[0].CustomFlags.has(rcc::CCAssignmentFlag::Bitcast) &&
+ LP64Mask != nullptr && LP64Plan->preservedMask() != nullptr &&
+ *LP64Plan->preservedMask() == *LP64Mask,
+ "LP64 must transport floating values through GPRs and use the integer preserved mask");
+
+ auto Narrow = abiValue(integer(32), 0);
+ auto InvalidNarrow = callRequest({Narrow}, 1);
+ Context.expect(!CallLowering.planCall(InvalidNarrow),
+ "a narrow integer without an explicit ABI extension must be rejected");
+ Narrow.Extension = rcc::ABIExtensionKind::SignExtend;
+ auto ExtendedNarrow = callRequest({Narrow}, 1);
+ auto NarrowPlan = CallLowering.planCall(ExtendedNarrow);
+ Context.expect(NarrowPlan && NarrowPlan->argumentAssignments()[0].LocationType == integer(64) &&
+ NarrowPlan->argumentAssignments()[0].Extension ==
+ rcc::ABIExtensionKind::SignExtend,
+ "a narrow integer extension must be explicit in its LP64D assignment");
+}
+
+void testVariadicAssignments(TestContext &Context) {
+ auto FixedFloat = floatingABIValue(rcc::FloatSemantics::IEEEDouble, 0);
+ auto VariableFloat = floatingABIValue(rcc::FloatSemantics::IEEEDouble, 1);
+ auto Request = callRequest({FixedFloat, VariableFloat}, 1, true);
+ auto Plan = CallLowering.planCall(Request);
+ Context.expect(
+ Plan && isLocation(Plan->argumentAssignments()[0], argumentFPR(0)) &&
+ isLocation(Plan->argumentAssignments()[1], argumentGPR(0)) &&
+ Plan->argumentAssignments()[1].CustomFlags.has(rcc::CCAssignmentFlag::Variadic) &&
+ Plan->argumentAssignments()[1].CustomFlags.has(rcc::CCAssignmentFlag::Bitcast),
+ "variadic floating arguments must use integer calling-convention transport");
+
+ auto FixedInteger = abiValue(integer(64), 0);
+ auto PairLow = abiValue(integer(64), 1);
+ PairLow.FragmentCount = 2;
+ PairLow.OriginalAlignment = align(16);
+ auto PairHigh = abiValue(integer(64), 1);
+ PairHigh.FragmentIndex = 1;
+ PairHigh.FragmentCount = 2;
+ PairHigh.FragmentOffsetBytes = 8;
+ PairHigh.OriginalAlignment = align(16);
+ auto PairRequest = callRequest({FixedInteger, PairLow, PairHigh}, 1, true);
+ auto PairPlan = CallLowering.planCall(PairRequest);
+ Context.expect(PairPlan && isLocation(PairPlan->argumentAssignments()[0], argumentGPR(0)) &&
+ isLocation(PairPlan->argumentAssignments()[1], argumentGPR(2)) &&
+ isLocation(PairPlan->argumentAssignments()[2], argumentGPR(3)),
+ "an aligned variadic pair must skip an odd GPR before assignment");
+
+ rcc::FormalArgumentRequest Formal;
+ Formal.Convention = convention();
+ Formal.Arguments = {FixedInteger};
+ Formal.FixedArgumentCount = 1;
+ Formal.IsVariadic = true;
+ auto FormalPlan = CallLowering.planFormalArguments(Formal);
+ Context.expect(FormalPlan && FormalPlan->varArgs().IsVariadic &&
+ FormalPlan->varArgs().FirstUnallocatedIntegerRegister == 1 &&
+ FormalPlan->varArgs().RegisterSaveAreaSizeBytes == 56 &&
+ FormalPlan->varArgs().RegisterSaveAreaOffsetBytes == -56 &&
+ FormalPlan->varArgs().RegisterSavePaddingBytes == 8 &&
+ FormalPlan->varArgs().RegistersToSave.size() == 7 &&
+ FormalPlan->varArgs().RegistersToSave.front().Reg == argumentGPR(1) &&
+ FormalPlan->varArgs().RegistersToSave.back().Reg == argumentGPR(7),
+ "a variadic formal function must describe its GPR save area and padding");
+}
+
+void testSpecialArgumentsAndReturns(TestContext &Context) {
+ auto ByVal = abiValue(pointer(), 0);
+ ByVal.Flags.set(rcc::ABIValueFlag::ByVal);
+ ByVal.ByValSizeBytes = 24;
+ ByVal.ByValAlignment = align(16);
+ auto ByValRequest = callRequest({ByVal}, 1);
+ auto ByValPlan = CallLowering.planCall(ByValRequest);
+ Context.expect(
+ ByValPlan && ByValPlan->byValCopies().size() == 1 &&
+ ByValPlan->byValCopies()[0].StackOffsetBytes == 0 &&
+ ByValPlan->callFrame().SizeBytes == 32 &&
+ ByValPlan->argumentAssignments()[0].CustomFlags.has(rcc::CCAssignmentFlag::ByVal),
+ "byval must allocate an aligned caller-owned copy and pass its pointer");
+
+ auto SRet = abiValue(pointer(), 0);
+ SRet.Flags.set(rcc::ABIValueFlag::StructReturn);
+ auto SRetPlan = CallLowering.planCall(callRequest({SRet}, 1));
+ Context.expect(
+ SRetPlan && isLocation(SRetPlan->argumentAssignments()[0], argumentGPR(0)) &&
+ SRetPlan->argumentAssignments()[0].CustomFlags.has(rcc::CCAssignmentFlag::StructReturn),
+ "sret must be carried as the first integer argument");
+ auto SRetWithDirectResult = callRequest({SRet}, 1);
+ SRetWithDirectResult.Results = {abiValue(integer(64), 0)};
+ Context.expect(!CallLowering.planCall(SRetWithDirectResult),
+ "an sret call cannot also expose direct ABI return registers");
+ auto Plain = abiValue(integer(64), 0);
+ SRet.OriginalValue = 1;
+ Context.expect(!CallLowering.planCall(callRequest({Plain, SRet}, 2)),
+ "sret outside the first argument position must be rejected");
+
+ auto Indirect = abiValue(pointer(), 0);
+ Indirect.Flags.set(rcc::ABIValueFlag::Indirect);
+ auto IndirectPlan = CallLowering.planCall(callRequest({Indirect}, 1));
+ Context.expect(
+ IndirectPlan &&
+ IndirectPlan->argumentAssignments()[0].LocationKind == rcc::CCLocationKind::Indirect &&
+ IndirectPlan->argumentAssignments()[0].CustomFlags.has(rcc::CCAssignmentFlag::Indirect),
+ "an explicitly indirect ABI value must preserve its transport classification");
+
+ auto InRegister = integerValues(9);
+ InRegister.back().Flags.set(rcc::ABIValueFlag::InRegister);
+ Context.expect(!CallLowering.planCall(callRequest(std::move(InRegister), 9)),
+ "an inreg argument that would spill to the stack must be rejected");
+
+ rcc::ReturnLoweringRequest Return;
+ Return.Convention = convention();
+ Return.Values = {abiValue(integer(64), 0), floatingABIValue(rcc::FloatSemantics::IEEEDouble, 1)};
+ auto ReturnPlan = CallLowering.planReturn(Return);
+ Context.expect(ReturnPlan && ReturnPlan->returnAssignments().size() == 2 &&
+ isLocation(ReturnPlan->returnAssignments()[0], returnGPR(0)) &&
+ isLocation(ReturnPlan->returnAssignments()[1], returnFPR(0)),
+ "mixed integer and floating returns must use a0 and fa0 independently");
+}
+
+[[nodiscard]] rcc::TailCallContext emptyTailContext() {
+ rcc::TailCallContext Context;
+ Context.CallerConvention = convention();
+ Context.ReusableIncomingArgumentBytes = 0;
+ Context.CallerFixedArgumentCount = 0;
+ return Context;
+}
+
+void testTailCalls(TestContext &Context) {
+ auto Tail = callRequest({}, 0);
+ Tail.TailKind = rcc::TailCallKind::Tail;
+ Tail.TailContext = emptyTailContext();
+ auto TailPlan = CallLowering.planCall(Tail);
+ Context.expect(TailPlan && TailPlan->tailDecision() == rcc::TailCallKind::Tail &&
+ TailPlan->callFrame().ReusesIncomingArea && !TailPlan->callFrame().EmitSetup,
+ "an ABI-compatible tail call must reuse the incoming argument area");
+
+ auto Fallback = callRequest({}, 0);
+ Fallback.TailKind = rcc::TailCallKind::Tail;
+ auto FallbackPlan = CallLowering.planCall(Fallback);
+ Context.expect(FallbackPlan && FallbackPlan->tailDecision() == rcc::TailCallKind::NotTail &&
+ !FallbackPlan->tailRejectionReason().empty() &&
+ FallbackPlan->callFrame().EmitSetup,
+ "a non-mandatory incompatible tail call must fall back with a reason");
+
+ auto MustTail = callRequest({}, 0);
+ MustTail.TailKind = rcc::TailCallKind::MustTail;
+ auto MustTailPlan = CallLowering.planCall(MustTail);
+ Context.expect(!MustTailPlan && MustTailPlan.error().code() == rcc::ErrorCode::Unsupported,
+ "an incompatible musttail call must fail instead of falling back");
+
+ auto StackTail = callRequest(integerValues(9), 9);
+ StackTail.TailKind = rcc::TailCallKind::Tail;
+ auto StackContext = emptyTailContext();
+ StackContext.CallerFixedArgumentCount = 9;
+ StackContext.ReusableIncomingArgumentBytes = 16;
+ StackTail.TailContext = std::move(StackContext);
+ auto StackTailPlan = CallLowering.planCall(StackTail);
+ Context.expect(StackTailPlan && StackTailPlan->tailDecision() == rcc::TailCallKind::Tail &&
+ StackTailPlan->callFrame().SizeBytes == 16,
+ "a tail call may reuse a sufficiently large incoming stack argument area");
+}
+
+[[nodiscard]] std::unique_ptr<rcc::MachineFunction> createFunction(TestContext &Context,
+ std::string Name,
+ std::uint64_t ID,
+ rcc::MachineBasicBlock *&Block) {
+ auto Function =
+ rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID));
+ Context.expect(Function.has_value(), "machine function creation must succeed");
+ if (!Function)
+ return nullptr;
+ auto CreatedBlock = (*Function)->createBasicBlock();
+ Context.expect(CreatedBlock && (*Function)->appendBlock(**CreatedBlock),
+ "machine entry block creation must succeed");
+ if (!CreatedBlock || !(*Function)->firstBlock())
+ return nullptr;
+ Block = *CreatedBlock;
+ return std::move(*Function);
+}
+
+[[nodiscard]] rcc::Register virtualRegister(rcc::MachineFunction &Function,
+ rcc::LowLevelType Type) {
+ auto Register = Function.registerInfo().createVirtualRegister(Type);
+ if (!Register)
+ std::terminate();
+ return *Register;
+}
+
+[[nodiscard]] rcc::Opcode opcode(rcc::riscv::InstructionOpcode Opcode) noexcept {
+ return rcc::Opcode::fromValue(static_cast<std::uint32_t>(Opcode));
+}
+
+[[nodiscard]] std::vector<rcc::MachineInstr *> instructions(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]] rcc::MachineInstr *findInstruction(rcc::MachineBasicBlock &Block,
+ rcc::riscv::InstructionOpcode OpcodeValue) {
+ for (rcc::MachineInstr *Instruction = Block.firstInstruction(); Instruction != nullptr;
+ Instruction = Instruction->next()) {
+ if (Instruction->opcode() == opcode(OpcodeValue))
+ return Instruction;
+ }
+ return nullptr;
+}
+
+[[nodiscard]] bool verifyFunction(const rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target) {
+ rcc::MachineVerifierOptions Options;
+ Options.VerifyProperties = false;
+ return rcc::MachineVerifier::verify(Function, Target, Options).has_value();
+}
+
+void testOrdinaryAndIndirectEmission(TestContext &Context) {
+ auto Target = rcc::riscv::createTargetDescription();
+ rcc::MachineBasicBlock *Block = nullptr;
+ auto Function = createFunction(Context, "ordinary_call", 1, Block);
+ Context.expect(Target && Function && Block != nullptr, "ordinary-call fixtures must be valid");
+ if (!Target || !Function || Block == nullptr)
+ return;
+
+ const rcc::LowLevelType I64 = integer(64);
+ const rcc::LowLevelType F64 = floating(rcc::FloatSemantics::IEEEDouble);
+ const rcc::Register Argument = virtualRegister(*Function, I64);
+ const rcc::Register Result = virtualRegister(*Function, F64);
+ auto Request = callRequest({abiValue(I64, 0, Argument)}, 1);
+ Request.Results = {floatingABIValue(rcc::FloatSemantics::IEEEDouble, 0, Result)};
+
+ rcc::MachineIRBuilder Builder(*Function, *Target);
+ Context.expect(Builder.setInsertPoint(*Block).has_value(),
+ "ordinary-call insertion point must be valid");
+ auto Lowered = CallLowering.lowerCall(Builder, Request);
+ Context.expect(Lowered.has_value(), "ordinary direct call emission must succeed");
+ if (!Lowered)
+ return;
+
+ const auto Instructions = instructions(*Block);
+ rcc::MachineInstr *FrameDown = Instructions.empty() ? nullptr : Instructions.front();
+ rcc::MachineInstr *FrameUp = Instructions.size() > 3 ? Instructions[3] : nullptr;
+ rcc::MachineInstr *Call = findInstruction(*Block, rcc::riscv::InstructionOpcode::PseudoCALL);
+ Context.expect(Instructions.size() == 5 &&
+ Instructions.front()->opcode() ==
+ opcode(rcc::riscv::InstructionOpcode::ADJCALLSTACKDOWN) &&
+ Instructions[1]->opcode() == rcc::opcode(rcc::GenericOpcode::COPY) &&
+ Instructions.back()->opcode() == rcc::opcode(rcc::GenericOpcode::COPY) &&
+ Call != nullptr && Call->operandCount() >= 5 &&
+ Call->operandAt(0)->kind() == rcc::MachineOperand::Kind::ExternalSymbol &&
+ Call->operandAt(0)->targetFlags() ==
+ static_cast<std::uint32_t>(rcc::riscv::CallTargetFlag::DirectCall) &&
+ Call->operandAt(1)->kind() == rcc::MachineOperand::Kind::RegisterMask,
+ "ordinary emission must materialize canonical full-value copies, call-frame, "
+ "target, and RegMask");
+ const rcc::Register StackPointer =
+ rcc::Register::fromPhysical(physical(rcc::riscv::PhysicalRegisterID::X2));
+ const auto HasStackPointerEffects = [StackPointer](const rcc::MachineInstr *Instruction) {
+ bool HasDef = false;
+ bool HasUse = false;
+ if (Instruction == nullptr)
+ return false;
+ for (const rcc::MachineOperand &Operand : Instruction->operands()) {
+ if (!Operand.isRegister() || !Operand.isImplicit() ||
+ Operand.registerValue() != StackPointer) {
+ continue;
+ }
+ HasDef |= Operand.isDef();
+ HasUse |= Operand.isUse();
+ }
+ return HasDef && HasUse;
+ };
+ const auto ImmediateAt = [](const rcc::MachineInstr *Instruction, std::size_t Index) {
+ const rcc::MachineOperand *Operand =
+ Instruction == nullptr ? nullptr : Instruction->operandAt(Index);
+ return Operand == nullptr ? nullptr : Operand->getIf<std::int64_t>();
+ };
+ const auto *DownAmount = ImmediateAt(FrameDown, 0);
+ const auto *DownPrepared = ImmediateAt(FrameDown, 1);
+ const auto *UpAmount = ImmediateAt(FrameUp, 0);
+ const auto *UpPrepared = ImmediateAt(FrameUp, 1);
+ Context.expect(
+ DownAmount != nullptr && *DownAmount == 0 && DownPrepared != nullptr && *DownPrepared == 0 &&
+ UpAmount != nullptr && *UpAmount == 0 && UpPrepared != nullptr && *UpPrepared == 0 &&
+ HasStackPointerEffects(FrameDown) && HasStackPointerEffects(FrameUp) &&
+ !FrameDown->flags().has(rcc::MachineInstrFlag::FrameSetup) &&
+ !FrameUp->flags().has(rcc::MachineInstrFlag::FrameDestroy),
+ "call-frame pseudos must use LLVM amount/prepared-byte operands and implicit SP effects");
+ bool HasArgumentUse = false;
+ bool HasResultDef = false;
+ std::optional<std::size_t> ArgumentUseIndex;
+ std::optional<std::size_t> ResultDefIndex;
+ std::size_t OperandIndex = 0;
+ for (const rcc::MachineOperand &Operand : Call->operands()) {
+ if (!Operand.isRegister() || !Operand.isImplicit()) {
+ ++OperandIndex;
+ continue;
+ }
+ if (Operand.isUse() && Operand.registerValue() == rcc::Register::fromPhysical(argumentGPR(0))) {
+ HasArgumentUse = true;
+ ArgumentUseIndex = OperandIndex;
+ }
+ if (Operand.isDef() && Operand.registerValue() == rcc::Register::fromPhysical(returnFPR(0))) {
+ HasResultDef = true;
+ ResultDefIndex = OperandIndex;
+ }
+ ++OperandIndex;
+ }
+ Context.expect(HasArgumentUse && HasResultDef && ArgumentUseIndex && ResultDefIndex &&
+ *ArgumentUseIndex < *ResultDefIndex &&
+ Function->frameInfo().state().HasCalls &&
+ Function->frameInfo().maxCallFrameSize() == 0 &&
+ Function->frameInfo().stackAlignment().bytes() == 16 &&
+ verifyFunction(*Function, *Target),
+ "ordinary call emission must order implicit uses before defs and record frame "
+ "state");
+
+ rcc::MachineBasicBlock *IndirectBlock = nullptr;
+ auto IndirectFunction = createFunction(Context, "indirect_call", 2, IndirectBlock);
+ if (!IndirectFunction || IndirectBlock == nullptr)
+ return;
+ const rcc::Register Callee = virtualRegister(*IndirectFunction, pointer());
+ auto IndirectTarget = rcc::MachineCallTarget::indirect(Callee);
+ if (!IndirectTarget)
+ return;
+ auto IndirectRequest = callRequest({}, 0);
+ IndirectRequest.Callee = std::move(*IndirectTarget);
+ rcc::MachineIRBuilder IndirectBuilder(*IndirectFunction, *Target);
+ if (!IndirectBuilder.setInsertPoint(*IndirectBlock))
+ return;
+ auto IndirectLowered = CallLowering.lowerCall(IndirectBuilder, IndirectRequest);
+ rcc::MachineInstr *IndirectCall =
+ findInstruction(*IndirectBlock, rcc::riscv::InstructionOpcode::PseudoCALL);
+ Context.expect(IndirectLowered && IndirectCall != nullptr &&
+ IndirectCall->operandAt(0)->registerValue() == Callee &&
+ verifyFunction(*IndirectFunction, *Target),
+ "indirect call emission must retain the callee register operand");
+}
+
+void testStackAndByValEmission(TestContext &Context) {
+ auto Target = rcc::riscv::createTargetDescription();
+ rcc::MachineBasicBlock *Block = nullptr;
+ auto Function = createFunction(Context, "stack_byval_call", 3, Block);
+ if (!Target || !Function || Block == nullptr)
+ return;
+
+ std::vector<rcc::ABIValue> Arguments;
+ const rcc::Register ByValSource = virtualRegister(*Function, pointer());
+ auto ByVal = abiValue(pointer(), 0, ByValSource);
+ ByVal.Flags.set(rcc::ABIValueFlag::ByVal);
+ ByVal.ByValSizeBytes = 24;
+ ByVal.ByValAlignment = align(16);
+ Arguments.push_back(ByVal);
+ for (std::uint32_t Index = 1; Index < 9; ++Index) {
+ Arguments.push_back(abiValue(integer(64), Index, virtualRegister(*Function, integer(64))));
+ }
+ auto Request = callRequest(std::move(Arguments), 9);
+ rcc::MachineIRBuilder Builder(*Function, *Target);
+ if (!Builder.setInsertPoint(*Block))
+ return;
+ auto Lowered = CallLowering.lowerCall(Builder, Request);
+ rcc::MachineInstr *ByValCopy =
+ findInstruction(*Block, rcc::riscv::InstructionOpcode::PseudoBYVAL_COPY);
+ rcc::MachineInstr *StackStore =
+ findInstruction(*Block, rcc::riscv::InstructionOpcode::PseudoSTORE_STACK_ARG);
+ bool HasByValObject = false;
+ bool HasOutgoingObject = false;
+ for (const rcc::StackObject &Object : Function->frameInfo().objects()) {
+ const rcc::StackObjectDesc *Description = Object.description();
+ if (Description == nullptr)
+ continue;
+ HasByValObject |= Description->Kind == rcc::StackObjectKind::Target &&
+ Description->TargetFlags ==
+ static_cast<std::uint64_t>(rcc::riscv::CallStackObjectFlag::ByValCopy);
+ HasOutgoingObject |=
+ Description->Kind == rcc::StackObjectKind::Target &&
+ Description->TargetFlags ==
+ static_cast<std::uint64_t>(rcc::riscv::CallStackObjectFlag::OutgoingArgument);
+ }
+ Context.expect(
+ Lowered && ByValCopy != nullptr && ByValCopy->memOperands().size() == 2 &&
+ ByValCopy->memOperands()[0]->isLoad() && ByValCopy->memOperands()[1]->isStore() &&
+ StackStore != nullptr && StackStore->memOperands().size() == 1 &&
+ StackStore->memOperands()[0]->isStore() && HasByValObject && HasOutgoingObject &&
+ Function->frameInfo().liveObjectCount() == 2 &&
+ Function->frameInfo().maxCallFrameSize() == 48 && verifyFunction(*Function, *Target),
+ "stack and byval emission must create precise objects, MMOs, and frame size");
+}
+
+void testFormalReturnAndTailEmission(TestContext &Context) {
+ auto Target = rcc::riscv::createTargetDescription();
+ if (!Target)
+ return;
+
+ rcc::MachineBasicBlock *FormalBlock = nullptr;
+ auto FormalFunction = createFunction(Context, "variadic_formal", 4, FormalBlock);
+ if (!FormalFunction || FormalBlock == nullptr)
+ return;
+ const rcc::Register FormalValue = virtualRegister(*FormalFunction, integer(64));
+ rcc::FormalArgumentRequest Formal;
+ Formal.Convention = convention();
+ Formal.Arguments = {abiValue(integer(64), 0, FormalValue)};
+ Formal.FixedArgumentCount = 1;
+ Formal.IsVariadic = true;
+ rcc::MachineIRBuilder FormalBuilder(*FormalFunction, *Target);
+ if (!FormalBuilder.setInsertPoint(*FormalBlock))
+ return;
+ auto FormalLowered = CallLowering.lowerFormalArguments(FormalBuilder, Formal);
+ std::size_t SaveStores = 0;
+ for (rcc::MachineInstr *Instruction : instructions(*FormalBlock)) {
+ if (Instruction->opcode() == opcode(rcc::riscv::InstructionOpcode::PseudoSTORE_STACK_ARG))
+ ++SaveStores;
+ }
+ bool HasSaveArea = false;
+ bool HasPadding = false;
+ for (const rcc::StackObject &Object : FormalFunction->frameInfo().objects()) {
+ const rcc::StackObjectDesc *Description = Object.description();
+ if (Description == nullptr)
+ continue;
+ HasSaveArea |= Description->TargetFlags ==
+ static_cast<std::uint64_t>(rcc::riscv::CallStackObjectFlag::VarArgSaveArea);
+ HasPadding |=
+ Description->TargetFlags ==
+ static_cast<std::uint64_t>(rcc::riscv::CallStackObjectFlag::VarArgAlignmentPadding);
+ }
+ Context.expect(FormalLowered && SaveStores == 7 && HasSaveArea && HasPadding &&
+ FormalFunction->registerInfo().liveIns().size() == 8 &&
+ FormalBlock->liveIns().size() == 8 && verifyFunction(*FormalFunction, *Target),
+ "variadic formal emission must save remaining GPRs with explicit padding");
+
+ rcc::MachineBasicBlock *ReturnBlock = nullptr;
+ auto ReturnFunction = createFunction(Context, "return_value", 5, ReturnBlock);
+ if (!ReturnFunction || ReturnBlock == nullptr)
+ return;
+ const rcc::Register ReturnValue = virtualRegister(*ReturnFunction, integer(64));
+ rcc::ReturnLoweringRequest Return;
+ Return.Convention = convention();
+ Return.Values = {abiValue(integer(64), 0, ReturnValue)};
+ rcc::MachineIRBuilder ReturnBuilder(*ReturnFunction, *Target);
+ if (!ReturnBuilder.setInsertPoint(*ReturnBlock))
+ return;
+ auto ReturnLowered = CallLowering.lowerReturn(ReturnBuilder, Return);
+ rcc::MachineInstr *ReturnPseudo =
+ findInstruction(*ReturnBlock, rcc::riscv::InstructionOpcode::PseudoRET);
+ bool HasReturnRegister = false;
+ if (ReturnPseudo != nullptr) {
+ HasReturnRegister = std::ranges::any_of(ReturnPseudo->operands(), [](const auto &Operand) {
+ return Operand.isRegister() && Operand.isImplicit() && Operand.isUse() &&
+ Operand.registerValue() == rcc::Register::fromPhysical(returnGPR(0));
+ });
+ }
+ Context.expect(ReturnLowered && instructions(*ReturnBlock).size() == 2 &&
+ ReturnPseudo != nullptr && HasReturnRegister &&
+ verifyFunction(*ReturnFunction, *Target),
+ "return emission must copy into a0 and record its implicit PseudoRET use");
+
+ rcc::MachineBasicBlock *TailBlock = nullptr;
+ auto TailFunction = createFunction(Context, "tail_call", 6, TailBlock);
+ if (!TailFunction || TailBlock == nullptr)
+ return;
+ auto Tail = callRequest({}, 0);
+ Tail.TailKind = rcc::TailCallKind::Tail;
+ Tail.TailContext = emptyTailContext();
+ rcc::MachineIRBuilder TailBuilder(*TailFunction, *Target);
+ if (!TailBuilder.setInsertPoint(*TailBlock))
+ return;
+ auto TailLowered = CallLowering.lowerCall(TailBuilder, Tail);
+ Context.expect(TailLowered && instructions(*TailBlock).size() == 1 &&
+ TailBlock->firstInstruction()->opcode() ==
+ opcode(rcc::riscv::InstructionOpcode::PseudoTAIL) &&
+ TailFunction->frameInfo().state().HasCalls &&
+ TailFunction->frameInfo().state().HasTailCall &&
+ !TailFunction->frameInfo().state().AdjustsStack &&
+ verifyFunction(*TailFunction, *Target),
+ "tail emission must avoid a private call frame and mark function frame state");
+
+ rcc::MachineBasicBlock *StackTailBlock = nullptr;
+ auto StackTailFunction = createFunction(Context, "stack_tail_call", 8, StackTailBlock);
+ if (!StackTailFunction || StackTailBlock == nullptr)
+ return;
+ std::vector<rcc::ABIValue> StackTailArguments;
+ StackTailArguments.reserve(9);
+ for (std::uint32_t Index = 0; Index < 9; ++Index) {
+ StackTailArguments.push_back(
+ abiValue(integer(64), Index, virtualRegister(*StackTailFunction, integer(64))));
+ }
+ auto StackTail = callRequest(std::move(StackTailArguments), 9);
+ StackTail.TailKind = rcc::TailCallKind::Tail;
+ auto StackTailContext = emptyTailContext();
+ StackTailContext.CallerFixedArgumentCount = 9;
+ StackTailContext.ReusableIncomingArgumentBytes = 16;
+ StackTail.TailContext = std::move(StackTailContext);
+ rcc::MachineIRBuilder StackTailBuilder(*StackTailFunction, *Target);
+ if (!StackTailBuilder.setInsertPoint(*StackTailBlock))
+ return;
+ auto StackTailLowered = CallLowering.lowerCall(StackTailBuilder, StackTail);
+ Context.expect(
+ StackTailLowered &&
+ findInstruction(*StackTailBlock, rcc::riscv::InstructionOpcode::PseudoSTORE_STACK_ARG) !=
+ nullptr &&
+ findInstruction(*StackTailBlock, rcc::riscv::InstructionOpcode::ADJCALLSTACKDOWN) ==
+ nullptr &&
+ StackTailFunction->frameInfo().state().HasTailCall &&
+ !StackTailFunction->frameInfo().state().AdjustsStack &&
+ verifyFunction(*StackTailFunction, *Target),
+ "a stack-argument tail call must reuse fixed incoming slots without claiming an SP change");
+}
+
+void testFormalStackAlignmentAndPreflight(TestContext &Context) {
+ auto Target = rcc::riscv::createTargetDescription();
+ if (!Target)
+ return;
+
+ rcc::MachineBasicBlock *StackBlock = nullptr;
+ auto StackFunction = createFunction(Context, "stack_formal", 9, StackBlock);
+ if (!StackFunction || StackBlock == nullptr)
+ return;
+ std::vector<rcc::ABIValue> StackArguments;
+ StackArguments.reserve(9);
+ for (std::uint32_t Index = 0; Index < 9; ++Index) {
+ StackArguments.push_back(
+ abiValue(integer(64), Index, virtualRegister(*StackFunction, integer(64))));
+ }
+ rcc::FormalArgumentRequest StackRequest;
+ StackRequest.Convention = convention();
+ StackRequest.Arguments = std::move(StackArguments);
+ StackRequest.FixedArgumentCount = 9;
+ rcc::MachineIRBuilder StackBuilder(*StackFunction, *Target);
+ if (!StackBuilder.setInsertPoint(*StackBlock))
+ return;
+ auto StackLowered = CallLowering.lowerFormalArguments(StackBuilder, StackRequest);
+ bool HasIncomingObject = false;
+ for (const rcc::StackObject &Object : StackFunction->frameInfo().objects()) {
+ const rcc::StackObjectDesc *Description = Object.description();
+ HasIncomingObject |=
+ Description != nullptr && Description->Kind == rcc::StackObjectKind::FixedABI &&
+ Description->Align.bytes() == 8 && Description->Offset == 0 &&
+ Description->TargetFlags ==
+ static_cast<std::uint64_t>(rcc::riscv::CallStackObjectFlag::IncomingArgument);
+ }
+ Context.expect(
+ StackLowered && StackFunction->frameInfo().liveObjectCount() == 1 && HasIncomingObject &&
+ StackFunction->frameInfo().stackAlignment().bytes() == 8 &&
+ findInstruction(*StackBlock, rcc::riscv::InstructionOpcode::PseudoLOAD_STACK_ARG) !=
+ nullptr &&
+ verifyFunction(*StackFunction, *Target),
+ "non-variadic stack formals must publish fixed objects and maximum frame alignment");
+
+ rcc::MachineBasicBlock *ConflictBlock = nullptr;
+ auto ConflictFunction = createFunction(Context, "conflicting_formal", 10, ConflictBlock);
+ if (!ConflictFunction || ConflictBlock == nullptr)
+ return;
+ const rcc::Register Existing = virtualRegister(*ConflictFunction, integer(64));
+ const rcc::Register First = virtualRegister(*ConflictFunction, integer(64));
+ const rcc::Register Second = virtualRegister(*ConflictFunction, integer(64));
+ const rcc::PhysicalRegister ConflictingRegister = argumentGPR(1);
+ if (!ConflictFunction->registerInfo().addLiveIn(ConflictingRegister, Existing) ||
+ !ConflictBlock->addLiveIn(ConflictingRegister, rcc::LaneMask::all())) {
+ Context.expect(false, "formal live-in conflict fixture must be valid");
+ return;
+ }
+ rcc::FormalArgumentRequest ConflictRequest;
+ ConflictRequest.Convention = convention();
+ ConflictRequest.Arguments = {abiValue(integer(64), 0, First), abiValue(integer(64), 1, Second)};
+ ConflictRequest.FixedArgumentCount = 2;
+ rcc::MachineIRBuilder ConflictBuilder(*ConflictFunction, *Target);
+ if (!ConflictBuilder.setInsertPoint(*ConflictBlock))
+ return;
+ auto ConflictLowered = CallLowering.lowerFormalArguments(ConflictBuilder, ConflictRequest);
+ Context.expect(
+ !ConflictLowered && ConflictBlock->instructionCount() == 0 &&
+ ConflictFunction->allInstructions().empty() &&
+ ConflictFunction->registerInfo().liveIns().size() == 1 &&
+ ConflictBlock->liveIns().size() == 1 &&
+ ConflictFunction->frameInfo().liveObjectCount() == 0,
+ "formal live-in conflicts must fail preflight without partially modifying MachineIR");
+}
+
+void testPlanningFailureDoesNotMutate(TestContext &Context) {
+ auto Target = rcc::riscv::createTargetDescription();
+ rcc::MachineBasicBlock *Block = nullptr;
+ auto Function = createFunction(Context, "failed_call", 7, Block);
+ if (!Target || !Function || Block == nullptr)
+ return;
+ const rcc::Register Value = virtualRegister(*Function, integer(32));
+ auto Request = callRequest({abiValue(integer(32), 0, Value)}, 1);
+ rcc::MachineIRBuilder Builder(*Function, *Target);
+ if (!Builder.setInsertPoint(*Block))
+ return;
+ auto Lowered = CallLowering.lowerCall(Builder, Request);
+ auto UnknownPhysical = rcc::PhysicalRegister::create(999);
+ auto UnknownRequest =
+ UnknownPhysical
+ ? callRequest({abiValue(integer(64), 0, rcc::Register::fromPhysical(*UnknownPhysical))},
+ 1)
+ : rcc::CallLoweringRequest{};
+ auto UnknownLowered = UnknownPhysical
+ ? CallLowering.lowerCall(Builder, UnknownRequest)
+ : rcc::Expected<void>{std::unexpected(UnknownPhysical.error())};
+ Context.expect(!Lowered && !UnknownLowered && Block->instructionCount() == 0 &&
+ Function->allInstructions().empty() &&
+ Function->frameInfo().liveObjectCount() == 0,
+ "RISC-V planning and preflight failures must leave MachineIR and frame state "
+ "untouched");
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ testIntegerAndFloatingAssignments(Context);
+ testVariadicAssignments(Context);
+ testSpecialArgumentsAndReturns(Context);
+ testTailCalls(Context);
+ testOrdinaryAndIndirectEmission(Context);
+ testStackAndByValEmission(Context);
+ testFormalReturnAndTailEmission(Context);
+ testFormalStackAlignmentAndPreflight(Context);
+ testPlanningFailureDoesNotMutate(Context);
+ return Context.result();
+}
diff --git a/backend/unittests/Target/RISCVFrameLoweringTest.cpp b/backend/unittests/Target/RISCVFrameLoweringTest.cpp
new file mode 100644
index 0000000..9b8e25c
--- /dev/null
+++ b/backend/unittests/Target/RISCVFrameLoweringTest.cpp
@@ -0,0 +1,1365 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "GISel/RISCVCallLowering.h"
+#include "RISCV.h"
+#include "rcc/CodeGen/GenericOpcodes.h"
+#include "rcc/CodeGen/MachineBasicBlock.h"
+#include "rcc/CodeGen/MachineCFGUpdater.h"
+#include "rcc/CodeGen/MachineCFIInfo.h"
+#include "rcc/CodeGen/MachineFrameInfo.h"
+#include "rcc/CodeGen/MachineFunction.h"
+#include "rcc/CodeGen/MachineIRBuilder.h"
+#include "rcc/CodeGen/MachineInstr.h"
+#include "rcc/CodeGen/MachineMemOperand.h"
+#include "rcc/CodeGen/MachineOperand.h"
+#include "rcc/CodeGen/MachinePassManager.h"
+#include "rcc/CodeGen/PrologueEpilogueInsertion.h"
+#include "rcc/CodeGen/RegisterMask.h"
+#include "rcc/CodeGen/TargetDescription.h"
+#include "rcc/CodeGen/TargetFrameLowering.h"
+#include "rcc/CodeGen/TargetRegisterAllocationInfo.h"
+#include "rcc/Target/RISCV/RISCVInstructions.h"
+#include "rcc/Target/RISCV/RISCVRegisters.h"
+#include "rcc/Target/RISCV/RISCVTarget.h"
+
+#include <algorithm>
+#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;
+};
+
+struct FunctionFixture final {
+ std::unique_ptr<rcc::MachineFunction> Function;
+ rcc::MachineBasicBlock *Block = nullptr;
+};
+
+[[nodiscard]] const rcc::riscv::RISCVSubtarget &subtarget() {
+ static const rcc::riscv::RISCVSubtarget Subtarget(rcc::riscv::RISCVSubtargetFeatures::rv64gc());
+ return Subtarget;
+}
+
+class ObservingFrameLowering final : public rcc::TargetFrameLowering {
+public:
+ explicit ObservingFrameLowering(const rcc::TargetFrameLowering &Delegate,
+ bool SkipFrameIndexElimination = false) noexcept
+ : Delegate(&Delegate), SkipFrameIndexElimination(SkipFrameIndexElimination) {}
+
+ [[nodiscard]] rcc::Expected<void> prepareFrame(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target,
+ rcc::FrameLoweringOptions Options) const override {
+ PrepareSawUnfrozen = !Function.frameInfo().isFrozen();
+ return Delegate->prepareFrame(Function, Target, Options);
+ }
+
+ [[nodiscard]] rcc::Expected<void>
+ insertCalleeSavedSpillsAndRestores(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target) const override {
+ InsertSawUnfrozen = !Function.frameInfo().isFrozen();
+ return Delegate->insertCalleeSavedSpillsAndRestores(Function, Target);
+ }
+
+ [[nodiscard]] rcc::Expected<void>
+ processFunctionBeforeFrameFinalized(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target) const override {
+ return Delegate->processFunctionBeforeFrameFinalized(Function, Target);
+ }
+
+ [[nodiscard]] rcc::Expected<void>
+ finalizeFrame(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target) const override {
+ FinalizeSawUnfrozen = !Function.frameInfo().isFrozen();
+ bool HasCalleeSave = false;
+ for (const rcc::MachineInstr *Instruction : Function.allInstructions()) {
+ if (Instruction == nullptr || Instruction->parent() == nullptr)
+ continue;
+ for (const rcc::MachineOperand &Operand : Instruction->operands()) {
+ const auto *Frame = Operand.getIf<rcc::FrameIndexOperand>();
+ const rcc::StackObject *Object =
+ Frame == nullptr ? nullptr : Function.frameInfo().object(Frame->Index);
+ HasCalleeSave |=
+ Object != nullptr && Object->description()->Kind == rcc::StackObjectKind::CalleeSaved;
+ }
+ }
+ FinalizeSawCalleeSave = HasCalleeSave;
+ auto Result = Delegate->finalizeFrame(Function, Target);
+ FinalizeProducedFrozen = Result && Function.frameInfo().isFrozen();
+ return Result;
+ }
+
+ [[nodiscard]] bool
+ isCallFramePseudo(const rcc::MachineInstr &Instruction) const noexcept override {
+ return Delegate->isCallFramePseudo(Instruction);
+ }
+
+ [[nodiscard]] rcc::Expected<void>
+ eliminateCallFramePseudo(rcc::MachineInstr &Instruction, rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target,
+ rcc::RegisterScavenger &Scavenger) const override {
+ return Delegate->eliminateCallFramePseudo(Instruction, Function, Target, Scavenger);
+ }
+
+ [[nodiscard]] rcc::Expected<void> emitPrologue(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target,
+ rcc::RegisterScavenger &Scavenger) const override {
+ PrologueSawFrozen = Function.frameInfo().isFrozen();
+ return Delegate->emitPrologue(Function, Target, Scavenger);
+ }
+
+ [[nodiscard]] bool
+ isEpilogueTerminator(const rcc::MachineInstr &Instruction,
+ const rcc::TargetDescription &Target) const noexcept override {
+ return Delegate->isEpilogueTerminator(Instruction, Target);
+ }
+
+ [[nodiscard]] rcc::Expected<void> emitEpilogue(rcc::MachineBasicBlock &Block,
+ rcc::MachineInstr &Terminator,
+ rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target,
+ rcc::RegisterScavenger &Scavenger) const override {
+ EpilogueSawFrozen = Function.frameInfo().isFrozen();
+ return Delegate->emitEpilogue(Block, Terminator, Function, Target, Scavenger);
+ }
+
+ [[nodiscard]] rcc::Expected<void>
+ processFunctionBeforeFrameIndexElimination(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target) const override {
+ return Delegate->processFunctionBeforeFrameIndexElimination(Function, Target);
+ }
+
+ [[nodiscard]] rcc::Expected<rcc::FrameIndexReference>
+ getFrameIndexReference(const rcc::MachineFunction &Function, const rcc::MachineInstr &Instruction,
+ rcc::FrameIndex Index, std::int64_t OperandOffset) const override {
+ return Delegate->getFrameIndexReference(Function, Instruction, Index, OperandOffset);
+ }
+
+ [[nodiscard]] rcc::Expected<void>
+ eliminateFrameIndex(rcc::MachineInstr &Instruction, std::size_t OperandIndex,
+ rcc::MachineFunction &Function, const rcc::TargetDescription &Target,
+ rcc::RegisterScavenger &Scavenger) const override {
+ if (SkipFrameIndexElimination)
+ return {};
+ return Delegate->eliminateFrameIndex(Instruction, OperandIndex, Function, Target, Scavenger);
+ }
+
+ mutable bool PrepareSawUnfrozen = false;
+ mutable bool InsertSawUnfrozen = false;
+ mutable bool FinalizeSawUnfrozen = false;
+ mutable bool FinalizeSawCalleeSave = false;
+ mutable bool FinalizeProducedFrozen = false;
+ mutable bool PrologueSawFrozen = false;
+ mutable bool EpilogueSawFrozen = false;
+
+private:
+ const rcc::TargetFrameLowering *Delegate;
+ bool SkipFrameIndexElimination = false;
+};
+
+[[nodiscard]] constexpr rcc::Opcode opcode(rcc::riscv::InstructionOpcode Value) noexcept {
+ return rcc::Opcode::fromValue(static_cast<std::uint32_t>(Value));
+}
+
+[[nodiscard]] rcc::PhysicalRegister physReg(rcc::riscv::PhysicalRegisterID ID) {
+ return *rcc::riscv::physicalRegister(ID);
+}
+
+[[nodiscard]] rcc::Register physical(rcc::riscv::PhysicalRegisterID ID) {
+ return rcc::Register::fromPhysical(physReg(ID));
+}
+
+[[nodiscard]] rcc::Expected<FunctionFixture> createFunction(std::string Name,
+ std::uint64_t StableID) {
+ auto Function = rcc::MachineFunction::create(std::move(Name),
+ rcc::MachineFunctionStableID::fromValue(StableID));
+ if (!Function)
+ return std::unexpected(Function.error());
+ auto Block = (*Function)->createBasicBlock();
+ if (!Block)
+ return std::unexpected(Block.error());
+ if (auto Result = (*Function)->appendBlock(**Block); !Result)
+ return std::unexpected(Result.error());
+ return FunctionFixture{std::move(*Function), *Block};
+}
+
+void setFramePassProperties(rcc::MachineFunction &Function) {
+ for (const rcc::MachineFunctionProperty Property : {
+ rcc::MachineFunctionProperty::Legalized,
+ rcc::MachineFunctionProperty::RegBankSelected,
+ rcc::MachineFunctionProperty::Selected,
+ rcc::MachineFunctionProperty::NoPHIs,
+ rcc::MachineFunctionProperty::TiedOpsRewritten,
+ rcc::MachineFunctionProperty::NoVRegs,
+ }) {
+ Function.properties().set(Property);
+ }
+}
+
+[[nodiscard]] rcc::Expected<void>
+runFrameLoweringWith(rcc::MachineFunction &Function, const rcc::TargetDescription &Target,
+ const rcc::TargetFrameLowering &FrameLowering) {
+ setFramePassProperties(Function);
+ rcc::MachineFunctionPassManager Pipeline;
+ auto Added =
+ Pipeline.addPass(std::make_unique<rcc::PrologueEpilogueInsertionPass>(FrameLowering));
+ if (!Added)
+ return std::unexpected(Added.error());
+ rcc::MachineFunctionAnalysisManager Analyses;
+ return Pipeline.run(Function, Target, Analyses);
+}
+
+[[nodiscard]] rcc::Expected<void> runFrameLowering(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target) {
+ return runFrameLoweringWith(Function, Target, subtarget().frameLowering());
+}
+
+[[nodiscard]] rcc::Expected<rcc::FrameIndex>
+createStackObject(rcc::MachineFunction &Function, rcc::StackObjectKind Kind,
+ std::optional<std::uint64_t> Size, std::uint64_t Alignment,
+ std::optional<std::int64_t> Offset = std::nullopt) {
+ auto Align = rcc::Alignment::create(Alignment);
+ if (!Align)
+ return std::unexpected(Align.error());
+ rcc::StackObjectDesc Description;
+ Description.Kind = Kind;
+ Description.Size = Size;
+ Description.Align = *Align;
+ Description.Offset = Offset;
+ return Function.frameInfo().createObject(std::move(Description));
+}
+
+[[nodiscard]] rcc::Expected<const rcc::MachineMemOperand *>
+createStackMemory(rcc::MachineFunction &Function, rcc::FrameIndex Index,
+ rcc::MemoryAccessFlag Access, std::int64_t Offset) {
+ auto Size = rcc::MemorySize::fixed(8);
+ auto Align = rcc::Alignment::create(8);
+ if (!Size || !Align)
+ return rcc::makeUnexpected(rcc::ErrorCode::InternalError, "invalid test memory facts");
+ rcc::MachineMemOperandDesc Description;
+ Description.Accesses = {Access};
+ Description.Size = *Size;
+ Description.Align = *Align;
+ Description.Base = rcc::MemoryBase::frameIndex(Index);
+ Description.Offset = Offset;
+ return Function.createMemOperand(std::move(Description));
+}
+
+[[nodiscard]] rcc::Expected<rcc::MachineInstr *>
+buildStackAccess(rcc::MachineFunction &Function, const rcc::TargetDescription &Target,
+ rcc::MachineBasicBlock &Block, rcc::riscv::InstructionOpcode OpcodeValue,
+ rcc::Register Value, rcc::FrameIndex Index, std::int64_t Offset) {
+ const bool IsLoad = OpcodeValue == rcc::riscv::InstructionOpcode::LD ||
+ OpcodeValue == rcc::riscv::InstructionOpcode::FLD;
+ auto Memory = createStackMemory(
+ Function, Index, IsLoad ? rcc::MemoryAccessFlag::Load : rcc::MemoryAccessFlag::Store, Offset);
+ if (!Memory)
+ return std::unexpected(Memory.error());
+ rcc::MachineIRBuilder Builder(Function, Target);
+ if (auto Result = Builder.setInsertPoint(Block); !Result)
+ return std::unexpected(Result.error());
+ auto Pending = Builder.buildInstr(opcode(OpcodeValue));
+ if (!Pending)
+ return std::unexpected(Pending.error());
+ auto RegisterResult = IsLoad ? Pending->addDef(Value) : Pending->addUse(Value);
+ if (!RegisterResult)
+ return std::unexpected(RegisterResult.error());
+ if (auto Result = Pending->addFrameIndex(Index); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Pending->addImmediate(Offset); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Pending->addMemOperand(**Memory); !Result)
+ return std::unexpected(Result.error());
+ return Pending->build();
+}
+
+[[nodiscard]] rcc::Expected<rcc::MachineInstr *>
+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);
+ if (auto Result = Builder.setInsertPoint(Block); !Result)
+ return std::unexpected(Result.error());
+ auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::ADDI));
+ if (!Pending)
+ return std::unexpected(Pending.error());
+ if (auto Result = Pending->addDef(Destination); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Pending->addUse(Source); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Pending->addImmediate(Immediate); !Result)
+ return std::unexpected(Result.error());
+ return Pending->build();
+}
+
+[[nodiscard]] rcc::Expected<rcc::MachineInstr *> buildAdd(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target,
+ rcc::MachineBasicBlock &Block,
+ rcc::Register Destination,
+ rcc::Register Left, rcc::Register Right) {
+ rcc::MachineIRBuilder Builder(Function, Target);
+ if (auto Result = Builder.setInsertPoint(Block); !Result)
+ return std::unexpected(Result.error());
+ auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::ADD));
+ if (!Pending)
+ return std::unexpected(Pending.error());
+ if (auto Result = Pending->addDef(Destination); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Pending->addUse(Left); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Pending->addUse(Right); !Result)
+ return std::unexpected(Result.error());
+ return Pending->build();
+}
+
+[[nodiscard]] rcc::Expected<rcc::MachineInstr *>
+buildMoveToDouble(rcc::MachineFunction &Function, const rcc::TargetDescription &Target,
+ rcc::MachineBasicBlock &Block, rcc::Register Destination, rcc::Register Source) {
+ rcc::MachineIRBuilder Builder(Function, Target);
+ if (auto Result = Builder.setInsertPoint(Block); !Result)
+ return std::unexpected(Result.error());
+ auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::FMV_D_X));
+ if (!Pending)
+ return std::unexpected(Pending.error());
+ if (auto Result = Pending->addDef(Destination); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Pending->addUse(Source); !Result)
+ return std::unexpected(Result.error());
+ return Pending->build();
+}
+
+[[nodiscard]] rcc::Expected<rcc::MachineInstr *>
+buildCallFramePseudo(rcc::MachineFunction &Function, const rcc::TargetDescription &Target,
+ rcc::MachineBasicBlock &Block, rcc::riscv::InstructionOpcode OpcodeValue,
+ std::int64_t Amount, std::int64_t PreparedBytes = 0) {
+ rcc::MachineIRBuilder Builder(Function, Target);
+ if (auto Result = Builder.setInsertPoint(Block); !Result)
+ return std::unexpected(Result.error());
+ auto Pending = Builder.buildInstr(opcode(OpcodeValue));
+ if (!Pending)
+ return std::unexpected(Pending.error());
+ if (auto Result = Pending->addImmediate(Amount); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Pending->addImmediate(PreparedBytes); !Result)
+ return std::unexpected(Result.error());
+ return Pending->build();
+}
+
+[[nodiscard]] rcc::Expected<rcc::MachineInstr *>
+buildRegisterMaskClobber(rcc::MachineFunction &Function, const rcc::TargetDescription &Target,
+ rcc::MachineBasicBlock &Block, rcc::PhysicalRegister Clobbered) {
+ std::uint64_t Preserved = ~std::uint64_t{1};
+ Preserved &= ~(std::uint64_t{1} << Clobbered.number());
+ auto Mask = std::make_shared<const rcc::RegisterMask>(std::vector<std::uint64_t>{Preserved});
+ rcc::MachineIRBuilder Builder(Function, Target);
+ if (auto Result = Builder.setInsertPoint(Block); !Result)
+ return std::unexpected(Result.error());
+ auto Pending = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::INLINEASM));
+ if (!Pending)
+ return std::unexpected(Pending.error());
+ if (auto Result = Pending->addRegisterMask(std::move(Mask)); !Result)
+ return std::unexpected(Result.error());
+ return Pending->build();
+}
+
+[[nodiscard]] rcc::Expected<rcc::MachineInstr *>
+buildIndirectTail(rcc::MachineFunction &Function, const rcc::TargetDescription &Target,
+ rcc::MachineBasicBlock &Block, rcc::Register Callee) {
+ auto Mask =
+ std::make_shared<const rcc::RegisterMask>(std::vector<std::uint64_t>{~std::uint64_t{1}});
+ rcc::MachineIRBuilder Builder(Function, Target);
+ if (auto Result = Builder.setInsertPoint(Block); !Result)
+ return std::unexpected(Result.error());
+ auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::PseudoTAIL));
+ if (!Pending)
+ return std::unexpected(Pending.error());
+ if (auto Result = Pending->addUse(Callee); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Pending->addRegisterMask(std::move(Mask)); !Result)
+ return std::unexpected(Result.error());
+ return Pending->build();
+}
+
+[[nodiscard]] rcc::Expected<rcc::MachineInstr *> buildReturn(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target,
+ rcc::MachineBasicBlock &Block) {
+ rcc::MachineIRBuilder Builder(Function, Target);
+ if (auto Result = Builder.setInsertPoint(Block); !Result)
+ return std::unexpected(Result.error());
+ auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::PseudoRET));
+ if (!Pending)
+ return std::unexpected(Pending.error());
+ return Pending->build();
+}
+
+[[nodiscard]] std::vector<rcc::MachineInstr *> instructions(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::size_t countOpcode(const rcc::MachineFunction &Function,
+ rcc::Opcode OpcodeValue) {
+ std::size_t Result = 0;
+ for (const rcc::MachineBasicBlock *Block = Function.firstBlock(); Block != nullptr;
+ Block = Block->next()) {
+ for (const rcc::MachineInstr *Instruction = Block->firstInstruction(); Instruction != nullptr;
+ Instruction = Instruction->next()) {
+ Result += Instruction->opcode() == OpcodeValue ? 1U : 0U;
+ }
+ }
+ return Result;
+}
+
+[[nodiscard]] std::size_t countOpcode(const rcc::MachineFunction &Function,
+ rcc::riscv::InstructionOpcode OpcodeValue) {
+ return countOpcode(Function, opcode(OpcodeValue));
+}
+
+[[nodiscard]] std::optional<rcc::PhysicalRegister> registerAt(const rcc::MachineInstr &Instruction,
+ std::size_t Index) {
+ const rcc::MachineOperand *Operand = Instruction.operandAt(Index);
+ if (Operand == nullptr)
+ return std::nullopt;
+ const rcc::Register *Reg = Operand->getIf<rcc::Register>();
+ if (Reg == nullptr || !Reg->isPhysical())
+ return std::nullopt;
+ return Reg->physicalRegister();
+}
+
+[[nodiscard]] std::optional<std::int64_t> immediateAt(const rcc::MachineInstr &Instruction,
+ std::size_t Index) {
+ const rcc::MachineOperand *Operand = Instruction.operandAt(Index);
+ if (Operand == nullptr)
+ return std::nullopt;
+ const auto *Immediate = Operand->getIf<std::int64_t>();
+ return Immediate == nullptr ? std::nullopt : std::optional<std::int64_t>(*Immediate);
+}
+
+[[nodiscard]] bool hasFrameIndex(const rcc::MachineFunction &Function) {
+ for (const rcc::MachineBasicBlock *Block = Function.firstBlock(); Block != nullptr;
+ Block = Block->next()) {
+ for (const rcc::MachineInstr *Instruction = Block->firstInstruction(); Instruction != nullptr;
+ Instruction = Instruction->next()) {
+ for (const rcc::MachineOperand &Operand : Instruction->operands()) {
+ if (Operand.kind() == rcc::MachineOperand::Kind::FrameIndex)
+ return true;
+ }
+ for (const rcc::MachineMemOperand *Memory : Instruction->memOperands()) {
+ if (Memory->description().Base.frameIndex())
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+[[nodiscard]] const rcc::StackObject *findStackObject(const rcc::MachineFunction &Function,
+ rcc::StackObjectKind Kind) {
+ const auto Position =
+ std::ranges::find_if(Function.frameInfo().objects(), [Kind](const rcc::StackObject &Object) {
+ return !Object.isTombstone() && !Object.description()->IsDead &&
+ Object.description()->Kind == Kind;
+ });
+ return Position == Function.frameInfo().objects().end() ? nullptr : &*Position;
+}
+
+[[nodiscard]] const rcc::StackObject *findCalleeSave(const rcc::MachineFunction &Function,
+ rcc::PhysicalRegister Reg) {
+ const auto Position =
+ std::ranges::find_if(Function.frameInfo().objects(), [Reg](const rcc::StackObject &Object) {
+ return !Object.isTombstone() && !Object.description()->IsDead &&
+ Object.description()->CalleeSavedRegister == Reg;
+ });
+ return Position == Function.frameInfo().objects().end() ? nullptr : &*Position;
+}
+
+void testLeafAndSmallFrame(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Leaf = createFunction("leaf", 1);
+ if (!Leaf || !buildReturn(*Leaf->Function, Target, *Leaf->Block))
+ return;
+ auto LoweredLeaf = runFrameLowering(*Leaf->Function, Target);
+ Context.expect(
+ LoweredLeaf && Leaf->Function->frameInfo().isFrozen() &&
+ Leaf->Function->frameInfo().state().StackSize == 0 &&
+ instructions(*Leaf->Block).size() == 1 &&
+ Leaf->Function->properties().has(rcc::MachineFunctionProperty::FrameFinalized) &&
+ Leaf->Function->properties().has(rcc::MachineFunctionProperty::NoFrameIndices),
+ "leaf frame lowering must freeze an empty frame without extra code");
+
+ auto DeadObject = createFunction("dead-stack-object", 24);
+ if (!DeadObject)
+ return;
+ auto DeadSlot = createStackObject(*DeadObject->Function, rcc::StackObjectKind::Default, 32, 16);
+ if (!DeadSlot || !DeadObject->Function->frameInfo().markObjectDead(*DeadSlot) ||
+ !buildReturn(*DeadObject->Function, Target, *DeadObject->Block)) {
+ return;
+ }
+ auto DeadResult = runFrameLowering(*DeadObject->Function, Target);
+ Context.expect(DeadResult && DeadObject->Function->frameInfo().isFrozen() &&
+ DeadObject->Function->frameInfo().state().StackSize == 0,
+ "dead stack objects must not require layout offsets or stack space");
+
+ auto Small = createFunction("small-frame", 2);
+ if (!Small)
+ return;
+ auto Slot = createStackObject(*Small->Function, rcc::StackObjectKind::SpillSlot, 8, 8);
+ if (!Slot)
+ return;
+ auto Store =
+ buildStackAccess(*Small->Function, Target, *Small->Block, rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X10), *Slot, 0);
+ if (!Store || !buildReturn(*Small->Function, Target, *Small->Block))
+ return;
+ auto Lowered = runFrameLowering(*Small->Function, Target);
+ Context.expect(Lowered && Small->Function->frameInfo().state().StackSize == 16 &&
+ !hasFrameIndex(*Small->Function) &&
+ countOpcode(*Small->Function, rcc::riscv::InstructionOpcode::ADDI) == 2,
+ "a small frame must use one aligned allocation and eliminate every FrameIndex");
+ const auto SmallCFI = Small->Function->cfiInfo().instructions();
+ Context.expect(
+ Lowered && SmallCFI.size() == 4 &&
+ SmallCFI.front().operation() == rcc::MachineCFIOperation::DefCFA &&
+ SmallCFI.front().getIf<rcc::MachineCFIRegisterOffsetRule>()->Offset == 16 &&
+ SmallCFI[1].operation() == rcc::MachineCFIOperation::RememberState &&
+ SmallCFI[2].operation() == rcc::MachineCFIOperation::DefCFA &&
+ SmallCFI[2].getIf<rcc::MachineCFIRegisterOffsetRule>()->Offset == 0 &&
+ SmallCFI.back().operation() == rcc::MachineCFIOperation::RestoreState &&
+ countOpcode(*Small->Function, rcc::opcode(rcc::GenericOpcode::CFI_INSTRUCTION)) == 4,
+ "small-frame CFI must track the allocated and restored CFA exactly");
+ Context.expect(Lowered && registerAt(**Store, 1) == physReg(rcc::riscv::PhysicalRegisterID::X2) &&
+ immediateAt(**Store, 2) == 0 && (*Store)->memOperands().size() == 1 &&
+ (*Store)->memOperands().front()->description().Base.kind() ==
+ rcc::MemoryBase::Kind::Stack &&
+ (*Store)->memOperands().front()->description().Offset == 0,
+ "small-frame instruction and MMO addresses must agree after elimination");
+
+ auto MultiExit = createFunction("multi-exit-frame", 14);
+ if (!MultiExit)
+ return;
+ auto SecondBlock = MultiExit->Function->createBasicBlock();
+ if (!SecondBlock || !MultiExit->Function->appendBlock(**SecondBlock))
+ return;
+ auto MultiSlot = createStackObject(*MultiExit->Function, rcc::StackObjectKind::Default, 8, 8);
+ if (!MultiSlot ||
+ !buildStackAccess(*MultiExit->Function, Target, *MultiExit->Block,
+ rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X10), *MultiSlot, 0) ||
+ !buildReturn(*MultiExit->Function, Target, *MultiExit->Block) ||
+ !buildReturn(*MultiExit->Function, Target, **SecondBlock)) {
+ return;
+ }
+ auto MultiResult = runFrameLowering(*MultiExit->Function, Target);
+ const auto MultiCFI = MultiExit->Function->cfiInfo().instructions();
+ const auto CountCFIOperation = [MultiCFI](rcc::MachineCFIOperation Operation) {
+ return std::ranges::count_if(MultiCFI,
+ [Operation](const rcc::MachineCFIInstruction &Instruction) {
+ return Instruction.operation() == Operation;
+ });
+ };
+ Context.expect(MultiResult && MultiCFI.size() == 7 &&
+ CountCFIOperation(rcc::MachineCFIOperation::RememberState) == 2 &&
+ CountCFIOperation(rcc::MachineCFIOperation::RestoreState) == 2,
+ "multiple epilogues must isolate their linear CFI state");
+}
+
+void testCalleeSavedRegisters(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Fixture = createFunction("callee-saves", 3);
+ if (!Fixture)
+ return;
+ const rcc::Register Zero = physical(rcc::riscv::PhysicalRegisterID::X0);
+ if (!buildAddImmediate(*Fixture->Function, Target, *Fixture->Block,
+ physical(rcc::riscv::PhysicalRegisterID::X1), Zero, 1) ||
+ !buildAddImmediate(*Fixture->Function, Target, *Fixture->Block,
+ physical(rcc::riscv::PhysicalRegisterID::X9), Zero, 2) ||
+ !buildMoveToDouble(*Fixture->Function, Target, *Fixture->Block,
+ physical(rcc::riscv::PhysicalRegisterID::F8_D), Zero) ||
+ !buildReturn(*Fixture->Function, Target, *Fixture->Block)) {
+ return;
+ }
+ auto Lowered = runFrameLowering(*Fixture->Function, Target);
+ const auto Saved = Fixture->Function->records().calleeSavedRegisters();
+ Context.expect(
+ Lowered && Saved.size() == 3 && Saved[0] == physReg(rcc::riscv::PhysicalRegisterID::X1) &&
+ Saved[1] == physReg(rcc::riscv::PhysicalRegisterID::X9) &&
+ Saved[2] == physReg(rcc::riscv::PhysicalRegisterID::F8_D) &&
+ countOpcode(*Fixture->Function, rcc::riscv::InstructionOpcode::SD) == 2 &&
+ countOpcode(*Fixture->Function, rcc::riscv::InstructionOpcode::FSD) == 1 &&
+ countOpcode(*Fixture->Function, rcc::riscv::InstructionOpcode::LD) == 2 &&
+ countOpcode(*Fixture->Function, rcc::riscv::InstructionOpcode::FLD) == 1,
+ "used GPR, FPR, and return-address callee saves must have symmetric save/restore code");
+ Context.expect(
+ Lowered && Fixture->Function->cfiInfo().instructions().size() == 10 &&
+ countOpcode(*Fixture->Function, rcc::opcode(rcc::GenericOpcode::CFI_INSTRUCTION)) == 10,
+ "callee-save CFI must describe every save, restore, and CFA transition");
+ Context.expect(
+ findCalleeSave(*Fixture->Function, physReg(rcc::riscv::PhysicalRegisterID::X1)) != nullptr &&
+ findCalleeSave(*Fixture->Function, physReg(rcc::riscv::PhysicalRegisterID::X9)) !=
+ nullptr &&
+ findCalleeSave(*Fixture->Function, physReg(rcc::riscv::PhysicalRegisterID::F8_D)) !=
+ nullptr &&
+ !hasFrameIndex(*Fixture->Function),
+ "callee-save records, stack objects, and eliminated operands must remain consistent");
+
+ auto MaskFixture = createFunction("regmask-callee-save", 15);
+ const rcc::PhysicalRegister X18 = physReg(rcc::riscv::PhysicalRegisterID::X18);
+ if (!MaskFixture ||
+ !buildRegisterMaskClobber(*MaskFixture->Function, Target, *MaskFixture->Block, X18) ||
+ !buildReturn(*MaskFixture->Function, Target, *MaskFixture->Block)) {
+ return;
+ }
+ auto MaskLowered = runFrameLowering(*MaskFixture->Function, Target);
+ Context.expect(MaskLowered &&
+ MaskFixture->Function->records().calleeSavedRegisters().size() == 1 &&
+ MaskFixture->Function->records().calleeSavedRegisters().front() == X18 &&
+ countOpcode(*MaskFixture->Function, rcc::riscv::InstructionOpcode::SD) == 1 &&
+ countOpcode(*MaskFixture->Function, rcc::riscv::InstructionOpcode::LD) == 1,
+ "RegMask clobbers must participate in callee-save discovery");
+
+ auto NoRestore = createFunction("callee-save-no-restore", 16);
+ auto Align8 = rcc::Alignment::create(8);
+ if (!NoRestore || !Align8)
+ return;
+ rcc::StackObjectDesc NoRestoreDescription;
+ NoRestoreDescription.Kind = rcc::StackObjectKind::CalleeSaved;
+ NoRestoreDescription.Size = 8;
+ NoRestoreDescription.Align = *Align8;
+ NoRestoreDescription.CalleeSavedRegister = X18;
+ NoRestoreDescription.CalleeSavedRestored = false;
+ if (!NoRestore->Function->frameInfo().createObject(std::move(NoRestoreDescription)) ||
+ !buildReturn(*NoRestore->Function, Target, *NoRestore->Block)) {
+ return;
+ }
+ auto NoRestoreResult = runFrameLowering(*NoRestore->Function, Target);
+ Context.expect(
+ NoRestoreResult &&
+ countOpcode(*NoRestore->Function, rcc::riscv::InstructionOpcode::SD) == 1 &&
+ countOpcode(*NoRestore->Function, rcc::riscv::InstructionOpcode::LD) == 0 &&
+ NoRestore->Function->frameInfo().state().RestorePoints.empty(),
+ "CalleeSavedRestored=false must suppress the restore without suppressing the save");
+}
+
+void testFramePointerAndFixedObjects(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Fixture = createFunction("frame-pointer", 4);
+ if (!Fixture)
+ return;
+ rcc::MachineFrameState State = Fixture->Function->frameInfo().state();
+ State.FramePointerPolicy = rcc::MachineFramePointerPolicy::All;
+ if (!Fixture->Function->frameInfo().setState(State))
+ return;
+ auto Local = createStackObject(*Fixture->Function, rcc::StackObjectKind::Default, 8, 8);
+ auto Fixed = createStackObject(*Fixture->Function, rcc::StackObjectKind::FixedABI, 8, 8, 16);
+ auto Variable =
+ createStackObject(*Fixture->Function, rcc::StackObjectKind::VariableSized, std::nullopt, 16);
+ if (!Local || !Fixed || !Variable)
+ return;
+ auto LocalLoad = buildStackAccess(*Fixture->Function, Target, *Fixture->Block,
+ rcc::riscv::InstructionOpcode::LD,
+ physical(rcc::riscv::PhysicalRegisterID::X10), *Local, 0);
+ auto FixedLoad = buildStackAccess(*Fixture->Function, Target, *Fixture->Block,
+ rcc::riscv::InstructionOpcode::LD,
+ physical(rcc::riscv::PhysicalRegisterID::X11), *Fixed, 0);
+ if (!LocalLoad || !FixedLoad || !buildReturn(*Fixture->Function, Target, *Fixture->Block)) {
+ return;
+ }
+ auto Lowered = runFrameLowering(*Fixture->Function, Target);
+ const rcc::PhysicalRegister FP = physReg(rcc::riscv::PhysicalRegisterID::X8);
+ Context.expect(Lowered && registerAt(**LocalLoad, 1) == FP &&
+ immediateAt(**LocalLoad, 2) == -16 && registerAt(**FixedLoad, 1) == FP &&
+ immediateAt(**FixedLoad, 2) == 16 &&
+ findCalleeSave(*Fixture->Function, FP) != nullptr,
+ "FP frames must address locals below and fixed ABI objects above incoming SP");
+
+ auto Preallocated = createFunction("preallocated-local-frame", 13);
+ if (!Preallocated)
+ return;
+ rcc::MachineFrameState PreallocatedState = Preallocated->Function->frameInfo().state();
+ PreallocatedState.LocalFrameSize = 32;
+ if (!Preallocated->Function->frameInfo().setState(PreallocatedState))
+ return;
+ auto Align8 = rcc::Alignment::create(8);
+ if (!Align8)
+ return;
+ rcc::StackObjectDesc PreallocatedDescription;
+ PreallocatedDescription.Size = 8;
+ PreallocatedDescription.Align = *Align8;
+ PreallocatedDescription.LocalOffset = 16;
+ auto PreallocatedObject =
+ Preallocated->Function->frameInfo().createObject(std::move(PreallocatedDescription));
+ if (!PreallocatedObject)
+ return;
+ auto PreallocatedStore = buildStackAccess(
+ *Preallocated->Function, Target, *Preallocated->Block, rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X10), *PreallocatedObject, 0);
+ if (!PreallocatedStore || !buildReturn(*Preallocated->Function, Target, *Preallocated->Block)) {
+ return;
+ }
+ auto PreallocatedResult = runFrameLowering(*Preallocated->Function, Target);
+ Context.expect(
+ PreallocatedResult && Preallocated->Function->frameInfo().state().StackSize == 32 &&
+ Preallocated->Function->frameInfo().state().LocalFrameSize == 32 &&
+ registerAt(**PreallocatedStore, 1) == physReg(rcc::riscv::PhysicalRegisterID::X2) &&
+ immediateAt(**PreallocatedStore, 2) == 16,
+ "preallocated local blobs must preserve localFrameSize and local-offset semantics");
+}
+
+void testStackRealignment(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Fixture = createFunction("realigned-frame", 5);
+ if (!Fixture)
+ return;
+ auto Local = createStackObject(*Fixture->Function, rcc::StackObjectKind::Default, 32, 32);
+ if (!Local)
+ return;
+ auto Store = buildStackAccess(*Fixture->Function, Target, *Fixture->Block,
+ rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X10), *Local, 0);
+ if (!Store || !buildReturn(*Fixture->Function, Target, *Fixture->Block))
+ return;
+
+ const auto &RA = rcc::riscv::registerAllocationInfo();
+ const rcc::PhysicalRegister FP = physReg(rcc::riscv::PhysicalRegisterID::X8);
+ const rcc::PhysicalRegister BP = physReg(rcc::riscv::PhysicalRegisterID::X9);
+ Context.expect(RA.isReservedForFunction(*Fixture->Function, FP, Target) &&
+ RA.isReservedForFunction(*Fixture->Function, BP, Target),
+ "stack realignment must reserve both FP and BP before allocation");
+
+ auto Lowered = runFrameLowering(*Fixture->Function, Target);
+ const bool Valid = Lowered && Fixture->Function->frameInfo().stackAlignment().bytes() == 32 &&
+ Fixture->Function->frameInfo().state().StackSize == 64 &&
+ registerAt(**Store, 1) == BP && immediateAt(**Store, 2) == 32 &&
+ countOpcode(*Fixture->Function, rcc::riscv::InstructionOpcode::ANDI) == 1 &&
+ findCalleeSave(*Fixture->Function, FP) != nullptr &&
+ findCalleeSave(*Fixture->Function, BP) != nullptr;
+ Context.expect(Valid,
+ "over-aligned frames must align SP and use a preserved aligned base pointer");
+ const auto CFI = Fixture->Function->cfiInfo().instructions();
+ const std::size_t Expressions = static_cast<std::size_t>(
+ std::ranges::count_if(CFI, [](const rcc::MachineCFIInstruction &Instruction) {
+ const auto *Escape = Instruction.getIf<rcc::MachineCFIEscapeRule>();
+ return Escape != nullptr && !Escape->Bytes.empty() && Escape->Bytes.front() == 0x10U;
+ }));
+ Context.expect(Lowered && Expressions == 2,
+ "realigned callee saves must use BP-relative DWARF expressions");
+}
+
+void testCallFrames(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Reserved = createFunction("reserved-call-frame", 6);
+ if (!Reserved || !Reserved->Function->frameInfo().setMaxCallFrameSize(48))
+ return;
+ auto Align16 = rcc::Alignment::create(16);
+ if (!Align16)
+ return;
+ rcc::StackObjectDesc OutgoingDescription;
+ OutgoingDescription.Kind = rcc::StackObjectKind::Target;
+ OutgoingDescription.Size = 8;
+ OutgoingDescription.Align = *Align16;
+ OutgoingDescription.LocalOffset = 32;
+ OutgoingDescription.TargetFlags =
+ static_cast<std::uint64_t>(rcc::riscv::CallStackObjectFlag::OutgoingArgument);
+ auto Outgoing = Reserved->Function->frameInfo().createObject(std::move(OutgoingDescription));
+ if (!Outgoing)
+ return;
+ auto OutgoingStore = buildStackAccess(
+ *Reserved->Function, Target, *Reserved->Block, rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X10), *Outgoing, 0);
+ if (!OutgoingStore ||
+ !buildCallFramePseudo(*Reserved->Function, Target, *Reserved->Block,
+ rcc::riscv::InstructionOpcode::ADJCALLSTACKDOWN, 48) ||
+ !buildCallFramePseudo(*Reserved->Function, Target, *Reserved->Block,
+ rcc::riscv::InstructionOpcode::ADJCALLSTACKUP, 48) ||
+ !buildReturn(*Reserved->Function, Target, *Reserved->Block)) {
+ return;
+ }
+ auto ReservedResult = runFrameLowering(*Reserved->Function, Target);
+ Context.expect(
+ ReservedResult && Reserved->Function->frameInfo().state().StackSize == 48 &&
+ countOpcode(*Reserved->Function, rcc::riscv::InstructionOpcode::ADJCALLSTACKDOWN) == 0 &&
+ countOpcode(*Reserved->Function, rcc::riscv::InstructionOpcode::ADJCALLSTACKUP) == 0 &&
+ countOpcode(*Reserved->Function, rcc::riscv::InstructionOpcode::ADDI) == 2 &&
+ registerAt(**OutgoingStore, 1) == physReg(rcc::riscv::PhysicalRegisterID::X2) &&
+ immediateAt(**OutgoingStore, 2) == 32,
+ "reserved call frames must fold call pseudos into the fixed frame");
+
+ auto Computed = createFunction("computed-call-frame", 17);
+ if (!Computed)
+ return;
+ auto ComputedOutgoingAlign = rcc::Alignment::create(64);
+ if (!ComputedOutgoingAlign)
+ return;
+ rcc::StackObjectDesc ComputedOutgoingDescription;
+ ComputedOutgoingDescription.Kind = rcc::StackObjectKind::Target;
+ ComputedOutgoingDescription.Size = 8;
+ ComputedOutgoingDescription.Align = *ComputedOutgoingAlign;
+ ComputedOutgoingDescription.LocalOffset = 0;
+ ComputedOutgoingDescription.TargetFlags =
+ static_cast<std::uint64_t>(rcc::riscv::CallStackObjectFlag::OutgoingArgument);
+ auto ComputedOutgoing =
+ Computed->Function->frameInfo().createObject(std::move(ComputedOutgoingDescription));
+ auto ComputedLocal = createStackObject(*Computed->Function, rcc::StackObjectKind::Default, 8, 8);
+ if (!ComputedOutgoing || !ComputedLocal ||
+ !buildStackAccess(*Computed->Function, Target, *Computed->Block,
+ rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X10), *ComputedOutgoing, 0) ||
+ !buildStackAccess(*Computed->Function, Target, *Computed->Block,
+ rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X11), *ComputedLocal, 0) ||
+ !buildCallFramePseudo(*Computed->Function, Target, *Computed->Block,
+ rcc::riscv::InstructionOpcode::ADJCALLSTACKDOWN, 64) ||
+ !buildCallFramePseudo(*Computed->Function, Target, *Computed->Block,
+ rcc::riscv::InstructionOpcode::ADJCALLSTACKUP, 64) ||
+ !buildReturn(*Computed->Function, Target, *Computed->Block)) {
+ return;
+ }
+ auto ComputedResult = runFrameLowering(*Computed->Function, Target);
+ const rcc::StackObject *ComputedLocalObject =
+ Computed->Function->frameInfo().object(*ComputedLocal);
+ Context.expect(ComputedResult && Computed->Function->frameInfo().maxCallFrameSize() == 64 &&
+ Computed->Function->frameInfo().stackAlignment().bytes() == 64 &&
+ Computed->Function->frameInfo().state().StackSize == 128 &&
+ ComputedLocalObject != nullptr &&
+ ComputedLocalObject->description()->Offset >= 64,
+ "PEI must compute and align the reserved call frame from call-frame pseudos");
+
+ auto Mismatched = createFunction("mismatched-call-frame", 18);
+ if (!Mismatched ||
+ !buildCallFramePseudo(*Mismatched->Function, Target, *Mismatched->Block,
+ rcc::riscv::InstructionOpcode::ADJCALLSTACKDOWN, 48, 16) ||
+ !buildCallFramePseudo(*Mismatched->Function, Target, *Mismatched->Block,
+ rcc::riscv::InstructionOpcode::ADJCALLSTACKUP, 48, 24) ||
+ !buildReturn(*Mismatched->Function, Target, *Mismatched->Block)) {
+ return;
+ }
+ auto MismatchResult = runFrameLowering(*Mismatched->Function, Target);
+ Context.expect(
+ !MismatchResult && MismatchResult.error().code() == rcc::ErrorCode::VerificationError &&
+ !Mismatched->Function->frameInfo().isFrozen() &&
+ countOpcode(*Mismatched->Function, rcc::riscv::InstructionOpcode::ADJCALLSTACKDOWN) ==
+ 1 &&
+ countOpcode(*Mismatched->Function, rcc::riscv::InstructionOpcode::ADJCALLSTACKUP) == 1,
+ "mismatched call-frame pairs must fail transactionally");
+
+ auto Unmatched = createFunction("unmatched-call-frame", 19);
+ if (!Unmatched ||
+ !buildCallFramePseudo(*Unmatched->Function, Target, *Unmatched->Block,
+ rcc::riscv::InstructionOpcode::ADJCALLSTACKDOWN, 32) ||
+ !buildReturn(*Unmatched->Function, Target, *Unmatched->Block)) {
+ return;
+ }
+ auto UnmatchedResult = runFrameLowering(*Unmatched->Function, Target);
+ Context.expect(!UnmatchedResult &&
+ UnmatchedResult.error().code() == rcc::ErrorCode::VerificationError &&
+ !Unmatched->Function->frameInfo().isFrozen(),
+ "unmatched call-frame setup must fail transactionally");
+
+ auto Dynamic = createFunction("dynamic-call-frame", 7);
+ if (!Dynamic)
+ return;
+ auto Variable =
+ createStackObject(*Dynamic->Function, rcc::StackObjectKind::DynamicAlloca, std::nullopt, 16);
+ auto OverAligned = createStackObject(*Dynamic->Function, rcc::StackObjectKind::Default, 8, 32);
+ if (!Variable || !OverAligned ||
+ !buildCallFramePseudo(*Dynamic->Function, Target, *Dynamic->Block,
+ rcc::riscv::InstructionOpcode::ADJCALLSTACKDOWN, 4096) ||
+ !buildCallFramePseudo(*Dynamic->Function, Target, *Dynamic->Block,
+ rcc::riscv::InstructionOpcode::ADJCALLSTACKUP, 4096) ||
+ !buildReturn(*Dynamic->Function, Target, *Dynamic->Block)) {
+ return;
+ }
+ Context.expect(rcc::riscv::registerAllocationInfo().isReservedForFunction(
+ *Dynamic->Function, physReg(rcc::riscv::PhysicalRegisterID::X9), Target),
+ "over-aligned dynamic frames must reserve BP before register allocation");
+ auto DynamicResult = runFrameLowering(*Dynamic->Function, Target);
+ Context.expect(
+ DynamicResult &&
+ countOpcode(*Dynamic->Function, rcc::riscv::InstructionOpcode::ADJCALLSTACKDOWN) == 0 &&
+ countOpcode(*Dynamic->Function, rcc::riscv::InstructionOpcode::ADJCALLSTACKUP) == 0 &&
+ countOpcode(*Dynamic->Function, rcc::riscv::InstructionOpcode::ADD) >= 2 &&
+ countOpcode(*Dynamic->Function, rcc::riscv::InstructionOpcode::ANDI) == 1 &&
+ findStackObject(*Dynamic->Function, rcc::StackObjectKind::EmergencySpill) != nullptr,
+ "dynamic large call frames must materialize SP adjustments and reserve emergency space");
+
+ auto DynamicSpill = createFunction("dynamic-call-frame-spill", 25);
+ if (!DynamicSpill)
+ return;
+ auto DynamicVariable = createStackObject(*DynamicSpill->Function,
+ rcc::StackObjectKind::DynamicAlloca, std::nullopt, 16);
+ const std::vector<rcc::riscv::PhysicalRegisterID> ScratchIDs = {
+ rcc::riscv::PhysicalRegisterID::X5, rcc::riscv::PhysicalRegisterID::X6,
+ rcc::riscv::PhysicalRegisterID::X7, rcc::riscv::PhysicalRegisterID::X28,
+ rcc::riscv::PhysicalRegisterID::X29, rcc::riscv::PhysicalRegisterID::X30,
+ rcc::riscv::PhysicalRegisterID::X31,
+ };
+ const rcc::Register Zero = physical(rcc::riscv::PhysicalRegisterID::X0);
+ if (!DynamicVariable)
+ return;
+ for (std::size_t Index = 0; Index < ScratchIDs.size(); ++Index) {
+ if (!buildAddImmediate(*DynamicSpill->Function, Target, *DynamicSpill->Block,
+ physical(ScratchIDs[Index]), Zero,
+ static_cast<std::int64_t>(Index + 1U))) {
+ return;
+ }
+ }
+ if (!buildCallFramePseudo(*DynamicSpill->Function, Target, *DynamicSpill->Block,
+ rcc::riscv::InstructionOpcode::ADJCALLSTACKDOWN, 4096) ||
+ !buildCallFramePseudo(*DynamicSpill->Function, Target, *DynamicSpill->Block,
+ rcc::riscv::InstructionOpcode::ADJCALLSTACKUP, 4096)) {
+ return;
+ }
+ for (const rcc::riscv::PhysicalRegisterID ID : ScratchIDs) {
+ if (!buildAdd(*DynamicSpill->Function, Target, *DynamicSpill->Block,
+ physical(rcc::riscv::PhysicalRegisterID::X10), physical(ID), Zero)) {
+ return;
+ }
+ }
+ if (!buildReturn(*DynamicSpill->Function, Target, *DynamicSpill->Block))
+ return;
+ auto DynamicSpillResult = runFrameLowering(*DynamicSpill->Function, Target);
+ const rcc::PhysicalRegister FP = physReg(rcc::riscv::PhysicalRegisterID::X8);
+ std::size_t StableEmergencyStores = 0;
+ std::size_t StableEmergencyLoads = 0;
+ for (const rcc::MachineInstr *Instruction : DynamicSpill->Function->allInstructions()) {
+ if (Instruction == nullptr || Instruction->parent() == nullptr ||
+ Instruction->memOperands().empty() ||
+ Instruction->flags().has(rcc::MachineInstrFlag::FrameSetup) ||
+ Instruction->flags().has(rcc::MachineInstrFlag::FrameDestroy)) {
+ continue;
+ }
+ const auto &Memory = Instruction->memOperands().front()->description();
+ if (Memory.Base.kind() != rcc::MemoryBase::Kind::Stack || registerAt(*Instruction, 1) != FP)
+ continue;
+ StableEmergencyStores +=
+ Instruction->opcode() == opcode(rcc::riscv::InstructionOpcode::SD) ? 1U : 0U;
+ StableEmergencyLoads +=
+ Instruction->opcode() == opcode(rcc::riscv::InstructionOpcode::LD) ? 1U : 0U;
+ }
+ Context.expect(DynamicSpillResult && StableEmergencyStores == 2 && StableEmergencyLoads == 2,
+ "dynamic call-frame scavenging must preserve scratch values through stable "
+ "FP-relative emergency slots");
+}
+
+void testLargeOffsetAndScavenging(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Fixture = createFunction("large-frame-offset", 8);
+ if (!Fixture)
+ return;
+ auto Local = createStackObject(*Fixture->Function, rcc::StackObjectKind::Default, 8192, 8);
+ if (!Local)
+ return;
+
+ const std::vector<rcc::riscv::PhysicalRegisterID> ScratchIDs = {
+ rcc::riscv::PhysicalRegisterID::X5, rcc::riscv::PhysicalRegisterID::X6,
+ rcc::riscv::PhysicalRegisterID::X7, rcc::riscv::PhysicalRegisterID::X28,
+ rcc::riscv::PhysicalRegisterID::X29, rcc::riscv::PhysicalRegisterID::X30,
+ rcc::riscv::PhysicalRegisterID::X31,
+ };
+ const rcc::Register Zero = physical(rcc::riscv::PhysicalRegisterID::X0);
+ for (std::size_t Index = 0; Index < ScratchIDs.size(); ++Index) {
+ if (!buildAddImmediate(*Fixture->Function, Target, *Fixture->Block, physical(ScratchIDs[Index]),
+ Zero, static_cast<std::int64_t>(Index + 1U))) {
+ return;
+ }
+ }
+ auto Store = buildStackAccess(*Fixture->Function, Target, *Fixture->Block,
+ rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X10), *Local, 5000);
+ if (!Store)
+ return;
+ for (const rcc::riscv::PhysicalRegisterID ID : ScratchIDs) {
+ if (!buildAdd(*Fixture->Function, Target, *Fixture->Block,
+ physical(rcc::riscv::PhysicalRegisterID::X10), physical(ID), Zero)) {
+ return;
+ }
+ }
+ if (!buildReturn(*Fixture->Function, Target, *Fixture->Block))
+ return;
+
+ auto Lowered = runFrameLowering(*Fixture->Function, Target);
+ Context.expect(Lowered &&
+ findStackObject(*Fixture->Function, rcc::StackObjectKind::EmergencySpill) !=
+ nullptr &&
+ registerAt(**Store, 1) == physReg(rcc::riscv::PhysicalRegisterID::X5) &&
+ immediateAt(**Store, 2) == 0 && !hasFrameIndex(*Fixture->Function),
+ "large FrameIndex offsets must use a scavenged physical address register");
+
+ bool HasUnflaggedEmergencyStore = false;
+ bool HasUnflaggedEmergencyLoad = false;
+ for (rcc::MachineInstr *Instruction : instructions(*Fixture->Block)) {
+ if (Instruction == *Store || Instruction->memOperands().empty())
+ continue;
+ const auto &Memory = Instruction->memOperands().front()->description();
+ if (Memory.Base.kind() != rcc::MemoryBase::Kind::Stack || Memory.Offset != 0)
+ continue;
+ const bool HasFrameFlag = Instruction->flags().has(rcc::MachineInstrFlag::FrameSetup) ||
+ Instruction->flags().has(rcc::MachineInstrFlag::FrameDestroy);
+ HasUnflaggedEmergencyStore |=
+ Instruction->opcode() == opcode(rcc::riscv::InstructionOpcode::SD) && !HasFrameFlag;
+ HasUnflaggedEmergencyLoad |=
+ Instruction->opcode() == opcode(rcc::riscv::InstructionOpcode::LD) && !HasFrameFlag;
+ }
+ Context.expect(HasUnflaggedEmergencyStore && HasUnflaggedEmergencyLoad,
+ "live scratch registers must be preserved without false frame flags");
+
+ auto Huge = createFunction("huge-rv64-frame", 26);
+ constexpr std::uint64_t HugeFrameSize = (std::uint64_t{1} << 32U) + 16U;
+ if (!Huge ||
+ !createStackObject(*Huge->Function, rcc::StackObjectKind::Default, HugeFrameSize, 16) ||
+ !buildReturn(*Huge->Function, Target, *Huge->Block)) {
+ return;
+ }
+ auto HugeResult = runFrameLowering(*Huge->Function, Target);
+ Context.expect(HugeResult && Huge->Function->frameInfo().state().StackSize == HugeFrameSize &&
+ countOpcode(*Huge->Function, rcc::riscv::InstructionOpcode::ADD) >= 2,
+ "RV64 frames larger than uint32 must use 64-bit stack adjustments");
+
+ auto TailScratch = createFunction("large-frame-tail-scratch", 27);
+ if (!TailScratch ||
+ !createStackObject(*TailScratch->Function, rcc::StackObjectKind::Default, 8192, 16) ||
+ !buildIndirectTail(*TailScratch->Function, Target, *TailScratch->Block,
+ physical(rcc::riscv::PhysicalRegisterID::X5))) {
+ return;
+ }
+ auto TailResult = runFrameLowering(*TailScratch->Function, Target);
+ bool FrameCodeClobbersTailCallee = false;
+ for (const rcc::MachineInstr *Instruction : TailScratch->Function->allInstructions()) {
+ if (Instruction == nullptr || Instruction->parent() == nullptr ||
+ (!Instruction->flags().has(rcc::MachineInstrFlag::FrameSetup) &&
+ !Instruction->flags().has(rcc::MachineInstrFlag::FrameDestroy))) {
+ continue;
+ }
+ FrameCodeClobbersTailCallee |=
+ registerAt(*Instruction, 0) == physReg(rcc::riscv::PhysicalRegisterID::X5);
+ }
+ Context.expect(TailResult && !FrameCodeClobbersTailCallee,
+ "large prologue/epilogue adjustments must not clobber a tail-call operand");
+}
+
+void testCFGScavenging(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Fixture = createFunction("cfg-scavenging", 13);
+ if (!Fixture)
+ return;
+ auto Loop = Fixture->Function->createBasicBlock();
+ if (!Loop || !Fixture->Function->appendBlock(**Loop) ||
+ !Fixture->Function->cfgUpdater().addSuccessor(*Fixture->Block, **Loop,
+ rcc::BranchProbability::one()) ||
+ !Fixture->Function->cfgUpdater().addSuccessor(**Loop, **Loop,
+ rcc::BranchProbability::one())) {
+ return;
+ }
+ auto Local = createStackObject(*Fixture->Function, rcc::StackObjectKind::Default, 8192, 8);
+ if (!Local)
+ return;
+
+ const std::vector<rcc::riscv::PhysicalRegisterID> ScratchIDs = {
+ rcc::riscv::PhysicalRegisterID::X5, rcc::riscv::PhysicalRegisterID::X6,
+ rcc::riscv::PhysicalRegisterID::X7, rcc::riscv::PhysicalRegisterID::X28,
+ rcc::riscv::PhysicalRegisterID::X29, rcc::riscv::PhysicalRegisterID::X30,
+ rcc::riscv::PhysicalRegisterID::X31,
+ };
+ const rcc::Register Zero = physical(rcc::riscv::PhysicalRegisterID::X0);
+ for (std::size_t Index = 0; Index < ScratchIDs.size(); ++Index) {
+ if (!buildAddImmediate(*Fixture->Function, Target, *Fixture->Block, physical(ScratchIDs[Index]),
+ Zero, static_cast<std::int64_t>(Index + 1U))) {
+ return;
+ }
+ }
+ auto Store = buildStackAccess(*Fixture->Function, Target, *Fixture->Block,
+ rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X10), *Local, 5000);
+ if (!Store)
+ return;
+ for (const rcc::riscv::PhysicalRegisterID ID : ScratchIDs) {
+ if (!buildAdd(*Fixture->Function, Target, **Loop, physical(rcc::riscv::PhysicalRegisterID::X10),
+ physical(ID), Zero)) {
+ return;
+ }
+ }
+ if (!buildReturn(*Fixture->Function, Target, **Loop))
+ return;
+
+ Context.expect((*Loop)->liveIns().empty(),
+ "CFG scavenging fixture must not depend on prebuilt live-ins");
+ auto Lowered = runFrameLowering(*Fixture->Function, Target);
+ bool HasEmergencyStore = false;
+ bool HasEmergencyLoad = false;
+ for (rcc::MachineInstr *Instruction : instructions(*Fixture->Block)) {
+ if (Instruction == *Store || Instruction->memOperands().empty())
+ continue;
+ const auto &Memory = Instruction->memOperands().front()->description();
+ if (Memory.Base.kind() != rcc::MemoryBase::Kind::Stack || Memory.Offset != 0) {
+ continue;
+ }
+ HasEmergencyStore |= Instruction->opcode() == opcode(rcc::riscv::InstructionOpcode::SD);
+ HasEmergencyLoad |= Instruction->opcode() == opcode(rcc::riscv::InstructionOpcode::LD);
+ }
+ Context.expect(
+ Lowered && HasEmergencyStore && HasEmergencyLoad &&
+ registerAt(**Store, 1) == physReg(rcc::riscv::PhysicalRegisterID::X5) &&
+ !hasFrameIndex(*Fixture->Function),
+ "successor and loop uses must keep scratch registers live across FrameIndex elimination");
+}
+
+[[nodiscard]] std::vector<std::int64_t> frameSignature(const rcc::MachineFunction &Function) {
+ std::vector<std::int64_t> Result;
+ Result.push_back(static_cast<std::int64_t>(Function.frameInfo().state().StackSize));
+ Result.push_back(static_cast<std::int64_t>(Function.frameInfo().stackAlignment().bytes()));
+ for (const rcc::StackObject &Object : Function.frameInfo().objects()) {
+ if (Object.isTombstone() || Object.description()->IsDead)
+ continue;
+ Result.push_back(static_cast<std::int64_t>(Object.description()->Kind));
+ Result.push_back(Object.description()->Offset.value_or(-1));
+ }
+ for (const rcc::MachineBasicBlock *Block = Function.firstBlock(); Block != nullptr;
+ Block = Block->next()) {
+ for (const rcc::MachineInstr *Instruction = Block->firstInstruction(); Instruction != nullptr;
+ Instruction = Instruction->next()) {
+ Result.push_back(static_cast<std::int64_t>(Instruction->opcode().value()));
+ for (std::size_t Index = 0; Index < Instruction->operandCount(); ++Index) {
+ Result.push_back(immediateAt(*Instruction, Index).value_or(0));
+ Result.push_back(registerAt(*Instruction, Index)
+ .transform([](rcc::PhysicalRegister Reg) {
+ return static_cast<std::int64_t>(Reg.number());
+ })
+ .value_or(0));
+ }
+ }
+ }
+ return Result;
+}
+
+[[nodiscard]] rcc::Expected<FunctionFixture>
+createDeterminismFixture(const rcc::TargetDescription &Target, std::uint64_t StableID) {
+ auto Fixture = createFunction("deterministic-frame", StableID);
+ if (!Fixture)
+ return std::unexpected(Fixture.error());
+ auto Small = createStackObject(*Fixture->Function, rcc::StackObjectKind::Default, 8, 8);
+ auto Large = createStackObject(*Fixture->Function, rcc::StackObjectKind::SpillSlot, 24, 16);
+ if (!Small || !Large)
+ return std::unexpected(!Small ? Small.error() : Large.error());
+ auto Store = buildStackAccess(*Fixture->Function, Target, *Fixture->Block,
+ rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X10), *Small, 0);
+ if (!Store)
+ return std::unexpected(Store.error());
+ auto Return = buildReturn(*Fixture->Function, Target, *Fixture->Block);
+ if (!Return)
+ return std::unexpected(Return.error());
+ return Fixture;
+}
+
+void testPEIOrderingAndPostconditions(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Ordered = createFunction("pei-ordering", 22);
+ if (!Ordered ||
+ !buildAddImmediate(*Ordered->Function, Target, *Ordered->Block,
+ physical(rcc::riscv::PhysicalRegisterID::X18),
+ physical(rcc::riscv::PhysicalRegisterID::X0), 1) ||
+ !buildReturn(*Ordered->Function, Target, *Ordered->Block)) {
+ return;
+ }
+ ObservingFrameLowering Observer(subtarget().frameLowering());
+ auto OrderedResult = runFrameLoweringWith(*Ordered->Function, Target, Observer);
+ Context.expect(
+ OrderedResult && Observer.PrepareSawUnfrozen && Observer.InsertSawUnfrozen &&
+ Observer.FinalizeSawUnfrozen && Observer.FinalizeSawCalleeSave &&
+ Observer.FinalizeProducedFrozen && Observer.PrologueSawFrozen &&
+ Observer.EpilogueSawFrozen,
+ "PEI must insert callee saves before freeze and emit prologue/epilogue after freeze");
+
+ auto InvalidPostcondition = createFunction("pei-invalid-postcondition", 23);
+ if (!InvalidPostcondition)
+ return;
+ auto Slot =
+ createStackObject(*InvalidPostcondition->Function, rcc::StackObjectKind::Default, 8, 8);
+ if (!Slot)
+ return;
+ auto Store = buildStackAccess(*InvalidPostcondition->Function, Target,
+ *InvalidPostcondition->Block, rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X10), *Slot, 0);
+ if (!Store ||
+ !buildReturn(*InvalidPostcondition->Function, Target, *InvalidPostcondition->Block)) {
+ return;
+ }
+ const std::size_t OriginalInstructionCount = InvalidPostcondition->Block->instructionCount();
+ ObservingFrameLowering Broken(subtarget().frameLowering(), true);
+ auto Rejected = runFrameLoweringWith(*InvalidPostcondition->Function, Target, Broken);
+ Context.expect(
+ !Rejected && !InvalidPostcondition->Function->frameInfo().isFrozen() &&
+ InvalidPostcondition->Block->instructionCount() == OriginalInstructionCount &&
+ (*Store)->operandAt(1)->kind() == rcc::MachineOperand::Kind::FrameIndex &&
+ InvalidPostcondition->Function->cfiInfo().instructions().empty(),
+ "PEI transaction preflight must verify final properties before mutating the original");
+}
+
+void testAtomicityAndDeterminism(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Invalid = createFunction("invalid-frame", 9);
+ if (!Invalid)
+ return;
+ auto Slot = createStackObject(*Invalid->Function, rcc::StackObjectKind::Default, 8, 8);
+ if (!Slot)
+ return;
+ auto Store = buildStackAccess(*Invalid->Function, Target, *Invalid->Block,
+ rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X10), *Slot, 0);
+ if (!Store || !(*Store)->eraseOperand(2) ||
+ !buildReturn(*Invalid->Function, Target, *Invalid->Block)) {
+ return;
+ }
+ auto Rejected = runFrameLowering(*Invalid->Function, Target);
+ Context.expect(!Rejected && !Invalid->Function->frameInfo().isFrozen() &&
+ Invalid->Function->frameInfo().liveObjectCount() == 1 &&
+ (*Store)->operandCount() == 2 &&
+ (*Store)->operandAt(1)->kind() == rcc::MachineOperand::Kind::FrameIndex,
+ "failed frame lowering must leave the original MachineFunction unchanged");
+
+ auto OverflowingCallFrame = createFunction("overflowing-call-frame", 12);
+ if (!OverflowingCallFrame ||
+ !OverflowingCallFrame->Function->frameInfo().setMaxCallFrameSize(16)) {
+ return;
+ }
+ auto Align8 = rcc::Alignment::create(8);
+ if (!Align8)
+ return;
+ rcc::StackObjectDesc OutgoingDescription;
+ OutgoingDescription.Kind = rcc::StackObjectKind::Target;
+ OutgoingDescription.Size = 8;
+ OutgoingDescription.Align = *Align8;
+ OutgoingDescription.LocalOffset = 16;
+ OutgoingDescription.TargetFlags =
+ static_cast<std::uint64_t>(rcc::riscv::CallStackObjectFlag::OutgoingArgument);
+ auto Outgoing =
+ OverflowingCallFrame->Function->frameInfo().createObject(std::move(OutgoingDescription));
+ if (!Outgoing)
+ return;
+ auto OutgoingStore =
+ buildStackAccess(*OverflowingCallFrame->Function, Target, *OverflowingCallFrame->Block,
+ rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X10), *Outgoing, 0);
+ if (!OutgoingStore ||
+ !buildReturn(*OverflowingCallFrame->Function, Target, *OverflowingCallFrame->Block)) {
+ return;
+ }
+ auto FrameRejected = runFrameLowering(*OverflowingCallFrame->Function, Target);
+ Context.expect(
+ !FrameRejected && FrameRejected.error().code() == rcc::ErrorCode::FrameLayoutFailed &&
+ !OverflowingCallFrame->Function->frameInfo().isFrozen() &&
+ OverflowingCallFrame->Function->cfiInfo().instructions().empty() &&
+ (*OutgoingStore)->operandAt(1)->kind() == rcc::MachineOperand::Kind::FrameIndex &&
+ (*OutgoingStore)->memOperands().front()->description().Base.frameIndex() == *Outgoing,
+ "transaction preflight must reject invalid call-frame layout without mutation");
+
+ auto MalformedSave = createFunction("malformed-callee-save", 20);
+ auto Align4 = rcc::Alignment::create(4);
+ if (!MalformedSave || !Align4)
+ return;
+ rcc::StackObjectDesc MalformedDescription;
+ MalformedDescription.Kind = rcc::StackObjectKind::CalleeSaved;
+ MalformedDescription.Size = 4;
+ MalformedDescription.Align = *Align4;
+ MalformedDescription.CalleeSavedRegister = physReg(rcc::riscv::PhysicalRegisterID::X18);
+ if (!MalformedSave->Function->frameInfo().createObject(std::move(MalformedDescription)) ||
+ !buildReturn(*MalformedSave->Function, Target, *MalformedSave->Block)) {
+ return;
+ }
+ auto MalformedResult = runFrameLowering(*MalformedSave->Function, Target);
+ Context.expect(
+ !MalformedResult && MalformedResult.error().code() == rcc::ErrorCode::VerificationError &&
+ !MalformedSave->Function->frameInfo().isFrozen() &&
+ MalformedSave->Function->frameInfo().liveObjectCount() == 1 &&
+ MalformedSave->Function->cfiInfo().instructions().empty(),
+ "malformed callee-save slots must be rejected before mutating the original function");
+
+ auto DuplicateSave = createFunction("duplicate-callee-save", 28);
+ auto Align8ForDuplicates = rcc::Alignment::create(8);
+ if (!DuplicateSave || !Align8ForDuplicates)
+ return;
+ for (std::size_t Index = 0; Index < 2; ++Index) {
+ rcc::StackObjectDesc DuplicateDescription;
+ DuplicateDescription.Kind = rcc::StackObjectKind::CalleeSaved;
+ DuplicateDescription.Size = 8;
+ DuplicateDescription.Align = *Align8ForDuplicates;
+ DuplicateDescription.CalleeSavedRegister = physReg(rcc::riscv::PhysicalRegisterID::X18);
+ if (!DuplicateSave->Function->frameInfo().createObject(std::move(DuplicateDescription))) {
+ return;
+ }
+ }
+ if (!buildReturn(*DuplicateSave->Function, Target, *DuplicateSave->Block))
+ return;
+ auto DuplicateResult = runFrameLowering(*DuplicateSave->Function, Target);
+ Context.expect(!DuplicateResult &&
+ DuplicateResult.error().code() == rcc::ErrorCode::VerificationError &&
+ !DuplicateSave->Function->frameInfo().isFrozen(),
+ "duplicate callee-save slots must be rejected transactionally");
+
+ auto ReservedFrameRegister = createFunction("used-reserved-frame-register", 21);
+ if (!ReservedFrameRegister)
+ return;
+ auto OverAligned =
+ createStackObject(*ReservedFrameRegister->Function, rcc::StackObjectKind::Default, 32, 32);
+ if (!OverAligned ||
+ !buildAddImmediate(*ReservedFrameRegister->Function, Target, *ReservedFrameRegister->Block,
+ physical(rcc::riscv::PhysicalRegisterID::X8),
+ physical(rcc::riscv::PhysicalRegisterID::X0), 1) ||
+ !buildReturn(*ReservedFrameRegister->Function, Target, *ReservedFrameRegister->Block)) {
+ return;
+ }
+ auto ReservedFrameResult = runFrameLowering(*ReservedFrameRegister->Function, Target);
+ Context.expect(
+ !ReservedFrameResult &&
+ ReservedFrameResult.error().code() == rcc::ErrorCode::VerificationError &&
+ !ReservedFrameRegister->Function->frameInfo().isFrozen() &&
+ ReservedFrameRegister->Function->frameInfo().liveObjectCount() == 1,
+ "post-RA uses of required FP/BP registers must fail without partial frame mutation");
+
+ auto First = createDeterminismFixture(Target, 10);
+ auto Second = createDeterminismFixture(Target, 11);
+ if (!First || !Second)
+ return;
+ auto FirstResult = runFrameLowering(*First->Function, Target);
+ auto SecondResult = runFrameLowering(*Second->Function, Target);
+ Context.expect(FirstResult && SecondResult &&
+ frameSignature(*First->Function) == frameSignature(*Second->Function),
+ "identical frame inputs must produce deterministic layouts and code");
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ auto Target = rcc::riscv::createTargetDescription();
+ Context.expect(Target.has_value(), "RV64GC frame-lowering target must be constructible");
+ if (!Target)
+ return Context.result();
+
+ testLeafAndSmallFrame(Context, *Target);
+ testCalleeSavedRegisters(Context, *Target);
+ testFramePointerAndFixedObjects(Context, *Target);
+ testStackRealignment(Context, *Target);
+ testCallFrames(Context, *Target);
+ testLargeOffsetAndScavenging(Context, *Target);
+ testCFGScavenging(Context, *Target);
+ testPEIOrderingAndPostconditions(Context, *Target);
+ testAtomicityAndDeterminism(Context, *Target);
+ return Context.result();
+}
diff --git a/backend/unittests/Target/RISCVLateCodeGenTest.cpp b/backend/unittests/Target/RISCVLateCodeGenTest.cpp
new file mode 100644
index 0000000..d78d4c4
--- /dev/null
+++ b/backend/unittests/Target/RISCVLateCodeGenTest.cpp
@@ -0,0 +1,1310 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "RISCV.h"
+#include "RISCVInstrInfo.h"
+#include "rcc/CodeGen/GenericOpcodes.h"
+#include "rcc/CodeGen/LiveVariables.h"
+#include "rcc/CodeGen/MachineBasicBlock.h"
+#include "rcc/CodeGen/MachineBlockPlacement.h"
+#include "rcc/CodeGen/MachineBranchOptimization.h"
+#include "rcc/CodeGen/MachineBranchRelaxation.h"
+#include "rcc/CodeGen/MachineCFGUpdater.h"
+#include "rcc/CodeGen/MachineCopyPropagation.h"
+#include "rcc/CodeGen/MachineFinalization.h"
+#include "rcc/CodeGen/MachineFunction.h"
+#include "rcc/CodeGen/MachineIRBuilder.h"
+#include "rcc/CodeGen/MachineInstr.h"
+#include "rcc/CodeGen/MachineMemoryFolding.h"
+#include "rcc/CodeGen/MachineOperand.h"
+#include "rcc/CodeGen/MachineOptimizationPipeline.h"
+#include "rcc/CodeGen/MachinePassManager.h"
+#include "rcc/CodeGen/MachinePseudoExpansion.h"
+#include "rcc/CodeGen/MachineShrinkWrapping.h"
+#include "rcc/CodeGen/MachineStackSlotColoring.h"
+#include "rcc/CodeGen/MachineTailDuplication.h"
+#include "rcc/CodeGen/PrologueEpilogueInsertion.h"
+#include "rcc/CodeGen/RegisterMask.h"
+#include "rcc/Target/RISCV/RISCVInstructions.h"
+#include "rcc/Target/RISCV/RISCVRegisters.h"
+#include "rcc/Target/RISCV/RISCVTarget.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <iostream>
+#include <limits>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace {
+
+class TestContext final {
+public:
+ void expect(bool Condition, std::string_view Message) {
+ if (Condition)
+ return;
+ std::cerr << Message << '\n';
+ ++Failures;
+ }
+
+ [[nodiscard]] int result() const noexcept {
+ return Failures == 0 ? 0 : 1;
+ }
+
+private:
+ int Failures = 0;
+};
+
+[[nodiscard]] constexpr rcc::Opcode opcode(rcc::riscv::InstructionOpcode Value) noexcept {
+ return rcc::Opcode::fromValue(static_cast<std::uint32_t>(Value));
+}
+
+constexpr rcc::Opcode GrowingLatePseudoOpcode = rcc::Opcode::fromValue(10'000);
+constexpr rcc::Opcode LateExpansionRealOpcode = rcc::Opcode::fromValue(10'001);
+constexpr rcc::Opcode ShrinkingLatePseudoOpcode = rcc::Opcode::fromValue(10'002);
+
+[[nodiscard]] const rcc::riscv::RISCVSubtarget &subtarget() {
+ static const rcc::riscv::RISCVSubtarget Subtarget(rcc::riscv::RISCVSubtargetFeatures::rv64gc());
+ return Subtarget;
+}
+
+class GrowingLatePseudoInfo final : public rcc::TargetPseudoExpansionInfo {
+public:
+ [[nodiscard]] rcc::Expected<bool>
+ expandPseudo(rcc::MachineInstr &Instruction, rcc::MachinePseudoExpansionPhase Phase,
+ rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target) const override {
+ if (Phase != rcc::MachinePseudoExpansionPhase::TargetLate ||
+ (Instruction.opcode() != GrowingLatePseudoOpcode &&
+ Instruction.opcode() != ShrinkingLatePseudoOpcode)) {
+ return false;
+ }
+ rcc::MachineBasicBlock *Block = Instruction.parent();
+ if (Block == nullptr) {
+ return rcc::makeUnexpected(rcc::ErrorCode::VerificationError,
+ "late expansion test pseudo is detached");
+ }
+ if (Instruction.opcode() == GrowingLatePseudoOpcode) {
+ rcc::MachineIRBuilder Builder(Function, Target);
+ if (auto Point = Builder.setInsertPointBefore(Instruction); !Point)
+ return std::unexpected(Point.error());
+ for (std::size_t Index = 0; Index < 2; ++Index) {
+ auto Pending = Builder.buildInstr(LateExpansionRealOpcode);
+ if (!Pending)
+ return std::unexpected(Pending.error());
+ auto Built = Pending->build();
+ if (!Built)
+ return std::unexpected(Built.error());
+ }
+ }
+ if (auto Removed = Block->removeInstruction(Instruction); !Removed)
+ return std::unexpected(Removed.error());
+ return true;
+ }
+};
+
+class FixedSizeInstrInfo final : public rcc::TargetInstrInfo {
+public:
+ [[nodiscard]] rcc::Expected<rcc::MachineBranchAnalysis>
+ analyzeBranch(rcc::MachineBasicBlock &, rcc::MachineFunction &,
+ const rcc::TargetDescription &) const override {
+ return rcc::MachineBranchAnalysis{};
+ }
+
+ [[nodiscard]] rcc::Expected<void>
+ reverseBranchCondition(rcc::MachineInstr &, rcc::MachineBasicBlock &, rcc::MachineFunction &,
+ const rcc::TargetDescription &) const override {
+ return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, "test target has no branches");
+ }
+
+ [[nodiscard]] rcc::Expected<void> removeBranch(rcc::MachineInstr &, rcc::MachineFunction &,
+ const rcc::TargetDescription &) const override {
+ return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, "test target has no branches");
+ }
+
+ [[nodiscard]] rcc::Expected<bool>
+ rewriteBranchTarget(rcc::MachineBasicBlock &, rcc::MachineBasicBlock &, rcc::MachineBasicBlock &,
+ rcc::MachineFunction &, const rcc::TargetDescription &) const override {
+ return false;
+ }
+
+ [[nodiscard]] rcc::Expected<rcc::MachineInstr *>
+ insertBranch(rcc::MachineBasicBlock &, rcc::MachineBasicBlock &, rcc::MachineFunction &,
+ const rcc::TargetDescription &) const override {
+ return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, "test target has no branches");
+ }
+
+ [[nodiscard]] rcc::Expected<bool>
+ isFallthroughEdge(const rcc::MachineBasicBlock &, const rcc::MachineBasicBlock &,
+ const rcc::MachineFunction &, const rcc::TargetDescription &) const override {
+ return false;
+ }
+
+ [[nodiscard]] rcc::Expected<void> validateCopy(rcc::Register, rcc::SubRegIndex, rcc::Register,
+ rcc::SubRegIndex, const rcc::MachineFunction &,
+ const rcc::TargetDescription &) const override {
+ return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, "test target has no COPY support");
+ }
+
+ [[nodiscard]] rcc::Expected<std::uint32_t>
+ instructionSize(const rcc::MachineInstr &, const rcc::MachineFunction &,
+ const rcc::TargetDescription &) const override {
+ return 4;
+ }
+
+ [[nodiscard]] rcc::Expected<rcc::MachineBasicBlock *>
+ branchTarget(rcc::MachineInstr &, rcc::MachineFunction &,
+ const rcc::TargetDescription &) const override {
+ return static_cast<rcc::MachineBasicBlock *>(nullptr);
+ }
+
+ [[nodiscard]] bool isBranchOffsetInRange(const rcc::MachineInstr &,
+ std::int64_t) const noexcept override {
+ return true;
+ }
+
+ [[nodiscard]] rcc::Expected<bool> relaxBranch(rcc::MachineInstr &, std::int64_t,
+ rcc::MachineFunction &,
+ const rcc::TargetDescription &) const override {
+ return false;
+ }
+};
+
+[[nodiscard]] rcc::Expected<rcc::TargetDescription> createLateExpansionSizeTarget() {
+ rcc::InstructionDescriptor Pseudo;
+ Pseudo.InstructionOpcode = GrowingLatePseudoOpcode;
+ Pseudo.Name = "TEST_GROWING_LATE_PSEUDO";
+ Pseudo.Flags = {rcc::InstructionFlag::Pseudo, rcc::InstructionFlag::ProducesCode};
+ Pseudo.Owner = rcc::PseudoOwner::TargetLateExpansion;
+ Pseudo.Deadline = rcc::PipelineStage::Emission;
+
+ rcc::InstructionDescriptor ShrinkingPseudo;
+ ShrinkingPseudo.InstructionOpcode = ShrinkingLatePseudoOpcode;
+ ShrinkingPseudo.Name = "TEST_SHRINKING_LATE_PSEUDO";
+ ShrinkingPseudo.Flags = {rcc::InstructionFlag::Pseudo, rcc::InstructionFlag::ProducesCode};
+ ShrinkingPseudo.Owner = rcc::PseudoOwner::TargetLateExpansion;
+ ShrinkingPseudo.Deadline = rcc::PipelineStage::Emission;
+
+ rcc::InstructionDescriptor Real;
+ Real.InstructionOpcode = LateExpansionRealOpcode;
+ Real.Name = "TEST_LATE_REAL";
+ Real.Flags = {rcc::InstructionFlag::ProducesCode};
+
+ std::vector<rcc::InstructionDescriptor> Descriptors;
+ Descriptors.push_back(std::move(Pseudo));
+ Descriptors.push_back(std::move(Real));
+ Descriptors.push_back(std::move(ShrinkingPseudo));
+ auto Instructions = rcc::InstructionSet::create(std::move(Descriptors));
+ if (!Instructions)
+ return std::unexpected(Instructions.error());
+ auto Registers = rcc::riscv::createRegisterInfo();
+ if (!Registers)
+ return std::unexpected(Registers.error());
+ return rcc::TargetDescription::create(std::move(*Instructions), std::move(*Registers));
+}
+
+[[nodiscard]] rcc::Register physical(rcc::riscv::PhysicalRegisterID ID) {
+ return rcc::Register::fromPhysical(*rcc::riscv::physicalRegister(ID));
+}
+
+[[nodiscard]] std::unique_ptr<rcc::MachineFunction>
+makeFunction(TestContext &Context, std::string Name, std::uint64_t StableID) {
+ auto Function = rcc::MachineFunction::create(std::move(Name),
+ rcc::MachineFunctionStableID::fromValue(StableID));
+ Context.expect(Function.has_value(), "late-codegen function creation must succeed");
+ return Function ? std::move(*Function) : nullptr;
+}
+
+[[nodiscard]] rcc::MachineBasicBlock *appendBlock(TestContext &Context,
+ rcc::MachineFunction &Function) {
+ auto Block = Function.createBasicBlock();
+ Context.expect(Block.has_value() && Function.appendBlock(**Block).has_value(),
+ "late-codegen block creation must succeed");
+ return Block.value_or(nullptr);
+}
+
+void setSelectedProperties(rcc::MachineFunction &Function) {
+ for (const rcc::MachineFunctionProperty Property : {
+ rcc::MachineFunctionProperty::Legalized,
+ rcc::MachineFunctionProperty::RegBankSelected,
+ rcc::MachineFunctionProperty::Selected,
+ rcc::MachineFunctionProperty::NoPHIs,
+ rcc::MachineFunctionProperty::TiedOpsRewritten,
+ }) {
+ Function.properties().set(Property);
+ }
+}
+
+void setPostRAProperties(rcc::MachineFunction &Function) {
+ setSelectedProperties(Function);
+ Function.properties().set(rcc::MachineFunctionProperty::TracksLiveness);
+ Function.properties().set(rcc::MachineFunctionProperty::NoVRegs);
+}
+
+void setRelaxationProperties(TestContext &Context, rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target) {
+ setPostRAProperties(Function);
+ auto StackAlignment = rcc::Alignment::create(16);
+ Context.expect(StackAlignment && Function.frameInfo().freeze(0, *StackAlignment).has_value(),
+ "relaxation fixture frame must be frozen");
+ Function.properties().set(rcc::MachineFunctionProperty::FrameFinalized);
+ Function.properties().set(rcc::MachineFunctionProperty::NoFrameIndices);
+ Function.properties().set(rcc::MachineFunctionProperty::NoExecutablePseudos);
+ auto Liveness = rcc::rebuildMachineLiveness(Function, Target);
+ Context.expect(Liveness.has_value(), "relaxation fixture liveness must be rebuilt");
+}
+
+[[nodiscard]] rcc::MachineInstr *buildADDI(TestContext &Context, rcc::MachineIRBuilder &Builder,
+ rcc::Register Destination, rcc::Register Source,
+ std::int64_t Immediate) {
+ auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::ADDI));
+ if (!Pending || !Pending->addDef(Destination) || !Pending->addUse(Source) ||
+ !Pending->addImmediate(Immediate)) {
+ Context.expect(false, "late-codegen ADDI construction must succeed");
+ return nullptr;
+ }
+ auto Built = Pending->build();
+ Context.expect(Built.has_value(), "late-codegen ADDI insertion must succeed");
+ return Built.value_or(nullptr);
+}
+
+[[nodiscard]] bool constrainGPR64(TestContext &Context, rcc::MachineFunction &Function,
+ rcc::Register Reg) {
+ const auto Bank =
+ rcc::RegisterBankID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterBank::GPR));
+ const auto Class =
+ rcc::RegisterClassID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterClass::GPR));
+ const bool Valid = Function.registerInfo().setRegisterBank(Reg, Bank).has_value() &&
+ Function.registerInfo().setRegisterClass(Reg, Class).has_value();
+ Context.expect(Valid, "late-codegen GPR64 constraint must succeed");
+ return Valid;
+}
+
+[[nodiscard]] rcc::Expected<const rcc::MachineMemOperand *>
+createStackMemory(rcc::MachineFunction &Function, rcc::FrameIndex Slot,
+ rcc::MemoryAccessFlag Access) {
+ auto Size = rcc::MemorySize::fixed(8);
+ auto Align = rcc::Alignment::create(8);
+ if (!Size)
+ return std::unexpected(Size.error());
+ if (!Align)
+ return std::unexpected(Align.error());
+ rcc::MachineMemOperandDesc Description;
+ Description.Accesses = {Access};
+ Description.Size = *Size;
+ Description.Align = *Align;
+ Description.Base = rcc::MemoryBase::frameIndex(Slot);
+ return Function.createMemOperand(std::move(Description));
+}
+
+[[nodiscard]] rcc::MachineInstr *buildStackAccess(TestContext &Context,
+ rcc::MachineIRBuilder &Builder,
+ rcc::riscv::InstructionOpcode Opcode,
+ rcc::Register Reg, rcc::FrameIndex Slot,
+ rcc::MemoryAccessFlag Access) {
+ auto Memory = createStackMemory(Builder.function(), Slot, Access);
+ auto Pending = Builder.buildInstr(opcode(Opcode));
+ const bool IsLoad = Access == rcc::MemoryAccessFlag::Load;
+ if (!Memory || !Pending ||
+ !(IsLoad ? Pending->addDef(Reg).has_value() : Pending->addUse(Reg).has_value()) ||
+ !Pending->addFrameIndex(Slot) || !Pending->addImmediate(0) ||
+ !Pending->addMemOperand(**Memory)) {
+ Context.expect(false, "late-codegen stack access construction must succeed");
+ return nullptr;
+ }
+ auto Built = Pending->build();
+ Context.expect(Built.has_value(), "late-codegen stack access insertion must succeed");
+ return Built.value_or(nullptr);
+}
+
+[[nodiscard]] rcc::MachineInstr *buildConditional(TestContext &Context,
+ rcc::MachineIRBuilder &Builder,
+ rcc::riscv::InstructionOpcode Opcode,
+ rcc::MachineBasicBlock &Target) {
+ auto Pending = Builder.buildInstr(opcode(Opcode));
+ if (!Pending || !Pending->addUse(physical(rcc::riscv::PhysicalRegisterID::X10)) ||
+ !Pending->addUse(physical(rcc::riscv::PhysicalRegisterID::X0)) ||
+ !Pending->addBasicBlock(Target)) {
+ Context.expect(false, "late-codegen conditional branch construction must succeed");
+ return nullptr;
+ }
+ auto Built = Pending->build();
+ Context.expect(Built.has_value(), "late-codegen conditional branch insertion must succeed");
+ return Built.value_or(nullptr);
+}
+
+[[nodiscard]] rcc::MachineInstr *
+buildJAL(TestContext &Context, rcc::MachineIRBuilder &Builder, rcc::MachineBasicBlock &Target,
+ rcc::riscv::PhysicalRegisterID Destination = rcc::riscv::PhysicalRegisterID::X0) {
+ auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::JAL));
+ if (!Pending || !Pending->addDef(physical(Destination)) || !Pending->addBasicBlock(Target)) {
+ Context.expect(false, "late-codegen JAL construction must succeed");
+ return nullptr;
+ }
+ auto Built = Pending->build();
+ Context.expect(Built.has_value(), "late-codegen JAL insertion must succeed");
+ return Built.value_or(nullptr);
+}
+
+[[nodiscard]] rcc::MachineBasicBlock *branchTarget(rcc::MachineInstr &Instruction) {
+ for (std::size_t Index = 0; Index < Instruction.operandCount(); ++Index) {
+ rcc::MachineOperand *Operand = Instruction.operandAt(Index);
+ const auto *Block = Operand == nullptr ? nullptr : Operand->getIf<rcc::BasicBlockOperand>();
+ if (Block != nullptr)
+ return Block->Block;
+ }
+ return nullptr;
+}
+
+void testBlockPlacement(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Function = makeFunction(Context, "block_placement", 1);
+ if (!Function)
+ return;
+ rcc::MachineBasicBlock *Entry = appendBlock(Context, *Function);
+ rcc::MachineBasicBlock *Cold = appendBlock(Context, *Function);
+ rcc::MachineBasicBlock *Hot = appendBlock(Context, *Function);
+ if (Entry == nullptr || Cold == nullptr || Hot == nullptr)
+ return;
+ auto ColdProbability = rcc::BranchProbability::fromFraction(1, 10);
+ auto HotProbability = rcc::BranchProbability::fromFraction(9, 10);
+ Context.expect(ColdProbability && HotProbability,
+ "placement probabilities must be representable");
+ if (!ColdProbability || !HotProbability)
+ return;
+ auto CFG = Function->cfgUpdater();
+ Context.expect(CFG.addSuccessor(*Entry, *Cold, *ColdProbability).has_value() &&
+ CFG.addSuccessor(*Entry, *Hot, *HotProbability).has_value(),
+ "placement CFG construction must succeed");
+ setSelectedProperties(*Function);
+ rcc::MachineFunctionAnalysisManager Analyses;
+ rcc::MachineBlockPlacementPass Pass;
+ auto Result = Pass.run(*Function, Target, Analyses);
+ if (!Result)
+ std::cerr << "block placement failure: " << Result.error().message() << '\n';
+ Context.expect(Result && Result->Changed && Entry->next() == Hot && Hot->next() == Cold,
+ "block placement must put the highest-probability successor on the trace");
+}
+
+void testBranchOptimization(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Function = makeFunction(Context, "branch_optimization", 9);
+ if (!Function)
+ return;
+ rcc::MachineBasicBlock *Entry = appendBlock(Context, *Function);
+ rcc::MachineBasicBlock *Forward = appendBlock(Context, *Function);
+ rcc::MachineBasicBlock *Exit = appendBlock(Context, *Function);
+ rcc::MachineBasicBlock *Dead = appendBlock(Context, *Function);
+ rcc::MachineBasicBlock *AddressTaken = appendBlock(Context, *Function);
+ if (Entry == nullptr || Forward == nullptr || Exit == nullptr || Dead == nullptr ||
+ AddressTaken == nullptr) {
+ return;
+ }
+ AddressTaken->attributes().set(rcc::MachineBasicBlockAttribute::MachineBlockAddressTaken);
+ auto CFG = Function->cfgUpdater();
+ Context.expect(CFG.addSuccessor(*Entry, *Forward, rcc::BranchProbability::one()).has_value() &&
+ CFG.addSuccessor(*Forward, *Exit, rcc::BranchProbability::one()).has_value(),
+ "branch-optimization CFG construction must succeed");
+ rcc::MachineIRBuilder Builder(*Function, Target);
+ if (!Builder.setInsertPoint(*Entry) || buildJAL(Context, Builder, *Forward) == nullptr ||
+ !Builder.setInsertPoint(*Forward) || buildJAL(Context, Builder, *Exit) == nullptr ||
+ !Builder.setInsertPoint(*Dead) ||
+ buildADDI(Context, Builder, physical(rcc::riscv::PhysicalRegisterID::X12),
+ physical(rcc::riscv::PhysicalRegisterID::X11), 1) == nullptr ||
+ !Builder.setInsertPoint(*AddressTaken) ||
+ buildADDI(Context, Builder, physical(rcc::riscv::PhysicalRegisterID::X13),
+ physical(rcc::riscv::PhysicalRegisterID::X11), 2) == nullptr) {
+ return;
+ }
+ setSelectedProperties(*Function);
+ rcc::MachineFunctionAnalysisManager Analyses;
+ rcc::MachineBranchOptimizationPass Pass(rcc::riscv::instrInfo());
+ auto Result = Pass.run(*Function, Target, Analyses);
+ if (!Result)
+ std::cerr << "branch optimization failure: " << Result.error().message() << '\n';
+ Context.expect(Result && Result->Changed && !Forward->isLinked() && !Dead->isLinked() &&
+ AddressTaken->isLinked() && Entry->successors().size() == 1 &&
+ Entry->successors().front().Block == Exit &&
+ branchTarget(*Entry->lastInstruction()) == Exit,
+ "branch optimization must thread empty blocks and delete unreachable code");
+
+ auto ConstantFunction = makeFunction(Context, "constant_branch", 10);
+ if (!ConstantFunction)
+ return;
+ rcc::MachineBasicBlock *ConstantEntry = appendBlock(Context, *ConstantFunction);
+ rcc::MachineBasicBlock *Taken = appendBlock(Context, *ConstantFunction);
+ rcc::MachineBasicBlock *NotTaken = appendBlock(Context, *ConstantFunction);
+ if (ConstantEntry == nullptr || Taken == nullptr || NotTaken == nullptr)
+ return;
+ auto Half = rcc::BranchProbability::fromFraction(1, 2);
+ auto ConstantCFG = ConstantFunction->cfgUpdater();
+ Context.expect(Half && ConstantCFG.addSuccessor(*ConstantEntry, *Taken, *Half).has_value() &&
+ ConstantCFG.addSuccessor(*ConstantEntry, *NotTaken, *Half).has_value(),
+ "constant-branch CFG construction must succeed");
+ rcc::MachineIRBuilder ConstantBuilder(*ConstantFunction, Target);
+ if (!ConstantBuilder.setInsertPoint(*ConstantEntry))
+ return;
+ auto Pending = ConstantBuilder.buildInstr(opcode(rcc::riscv::InstructionOpcode::BEQ));
+ if (!Pending || !Pending->addUse(physical(rcc::riscv::PhysicalRegisterID::X0)) ||
+ !Pending->addUse(physical(rcc::riscv::PhysicalRegisterID::X0)) ||
+ !Pending->addBasicBlock(*Taken) || !Pending->build() ||
+ buildJAL(Context, ConstantBuilder, *NotTaken) == nullptr) {
+ Context.expect(false, "constant branch construction must succeed");
+ return;
+ }
+ setSelectedProperties(*ConstantFunction);
+ rcc::MachineFunctionAnalysisManager ConstantAnalyses;
+ auto ConstantResult = Pass.run(*ConstantFunction, Target, ConstantAnalyses);
+ if (!ConstantResult)
+ std::cerr << "constant branch folding failure: " << ConstantResult.error().message() << '\n';
+ Context.expect(
+ ConstantResult && ConstantResult->Changed && ConstantEntry->successors().size() == 1 &&
+ ConstantEntry->successors().front().Block == Taken &&
+ branchTarget(*ConstantEntry->lastInstruction()) == Taken && !NotTaken->isLinked(),
+ "branch optimization must fold an always-true RV64GC branch");
+}
+
+void testTailDuplication(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Function = makeFunction(Context, "tail_duplication", 11);
+ if (!Function)
+ return;
+ rcc::MachineBasicBlock *Entry = appendBlock(Context, *Function);
+ rcc::MachineBasicBlock *Left = appendBlock(Context, *Function);
+ rcc::MachineBasicBlock *Right = appendBlock(Context, *Function);
+ rcc::MachineBasicBlock *Tail = appendBlock(Context, *Function);
+ rcc::MachineBasicBlock *Exit = appendBlock(Context, *Function);
+ if (Entry == nullptr || Left == nullptr || Right == nullptr || Tail == nullptr ||
+ Exit == nullptr) {
+ return;
+ }
+ auto Half = rcc::BranchProbability::fromFraction(1, 2);
+ auto CFG = Function->cfgUpdater();
+ Context.expect(Half && CFG.addSuccessor(*Entry, *Left, *Half).has_value() &&
+ CFG.addSuccessor(*Entry, *Right, *Half).has_value() &&
+ CFG.addSuccessor(*Left, *Tail, rcc::BranchProbability::one()).has_value() &&
+ CFG.addSuccessor(*Right, *Tail, rcc::BranchProbability::one()).has_value() &&
+ CFG.addSuccessor(*Tail, *Exit, rcc::BranchProbability::one()).has_value(),
+ "tail-duplication CFG construction must succeed");
+ rcc::MachineIRBuilder Builder(*Function, Target);
+ if (!Builder.setInsertPoint(*Entry) ||
+ buildConditional(Context, Builder, rcc::riscv::InstructionOpcode::BNE, *Left) == nullptr ||
+ buildJAL(Context, Builder, *Right) == nullptr || !Builder.setInsertPoint(*Left) ||
+ buildJAL(Context, Builder, *Tail) == nullptr || !Builder.setInsertPoint(*Right) ||
+ buildJAL(Context, Builder, *Tail) == nullptr || !Builder.setInsertPoint(*Tail) ||
+ buildADDI(Context, Builder, physical(rcc::riscv::PhysicalRegisterID::X12),
+ physical(rcc::riscv::PhysicalRegisterID::X11), 1) == nullptr ||
+ buildJAL(Context, Builder, *Exit) == nullptr) {
+ return;
+ }
+ setRelaxationProperties(Context, *Function, Target);
+ rcc::MachineFunctionAnalysisManager Analyses;
+ rcc::MachineTailDuplicationPass Pass(rcc::riscv::instrInfo());
+ Tail->attributes().set(rcc::MachineBasicBlockAttribute::MachineBlockAddressTaken);
+ auto Protected = Pass.run(*Function, Target, Analyses);
+ Context.expect(Protected && !Protected->Changed && Tail->isLinked(),
+ "tail duplication must preserve an address-taken tail");
+ Tail->attributes().clear(rcc::MachineBasicBlockAttribute::MachineBlockAddressTaken);
+ auto Result = Pass.run(*Function, Target, Analyses);
+ if (!Result)
+ std::cerr << "tail duplication failure: " << Result.error().message() << '\n';
+ Context.expect(
+ Result && Result->Changed && !Tail->isLinked() && Left->instructionCount() == 2 &&
+ Right->instructionCount() == 2 &&
+ Left->firstInstruction()->opcode() == opcode(rcc::riscv::InstructionOpcode::ADDI) &&
+ Right->firstInstruction()->opcode() == opcode(rcc::riscv::InstructionOpcode::ADDI) &&
+ branchTarget(*Left->lastInstruction()) == Exit &&
+ branchTarget(*Right->lastInstruction()) == Exit &&
+ rcc::verifyTrackedMachineLiveness(*Function, Target).has_value(),
+ "tail duplication must clone a small shared tail and repair liveness");
+}
+
+void testBranchFoldingTailMerge(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Function = makeFunction(Context, "tail_merge", 12);
+ if (!Function)
+ return;
+ rcc::MachineBasicBlock *Entry = appendBlock(Context, *Function);
+ rcc::MachineBasicBlock *First = appendBlock(Context, *Function);
+ rcc::MachineBasicBlock *Second = appendBlock(Context, *Function);
+ rcc::MachineBasicBlock *Exit = appendBlock(Context, *Function);
+ if (Entry == nullptr || First == nullptr || Second == nullptr || Exit == nullptr)
+ return;
+ auto Half = rcc::BranchProbability::fromFraction(1, 2);
+ auto CFG = Function->cfgUpdater();
+ Context.expect(Half && CFG.addSuccessor(*Entry, *First, *Half).has_value() &&
+ CFG.addSuccessor(*Entry, *Second, *Half).has_value() &&
+ CFG.addSuccessor(*First, *Exit, rcc::BranchProbability::one()).has_value() &&
+ CFG.addSuccessor(*Second, *Exit, rcc::BranchProbability::one()).has_value(),
+ "tail-merge CFG construction must succeed");
+ rcc::MachineIRBuilder Builder(*Function, Target);
+ if (!Builder.setInsertPoint(*Entry) ||
+ buildConditional(Context, Builder, rcc::riscv::InstructionOpcode::BNE, *First) == nullptr ||
+ buildJAL(Context, Builder, *Second) == nullptr || !Builder.setInsertPoint(*First) ||
+ buildADDI(Context, Builder, physical(rcc::riscv::PhysicalRegisterID::X12),
+ physical(rcc::riscv::PhysicalRegisterID::X11), 7) == nullptr ||
+ buildJAL(Context, Builder, *Exit) == nullptr || !Builder.setInsertPoint(*Second) ||
+ buildADDI(Context, Builder, physical(rcc::riscv::PhysicalRegisterID::X12),
+ physical(rcc::riscv::PhysicalRegisterID::X11), 7) == nullptr ||
+ buildJAL(Context, Builder, *Exit) == nullptr) {
+ return;
+ }
+ setRelaxationProperties(Context, *Function, Target);
+ rcc::MachineFunctionAnalysisManager Analyses;
+ rcc::MachineBranchOptimizationPass Pass(rcc::riscv::instrInfo());
+ Second->attributes().set(rcc::MachineBasicBlockAttribute::MachineBlockAddressTaken);
+ auto Protected = Pass.run(*Function, Target, Analyses);
+ Context.expect(Protected && !Protected->Changed && Second->isLinked(),
+ "tail merge must preserve an address-taken duplicate block");
+ Second->attributes().clear(rcc::MachineBasicBlockAttribute::MachineBlockAddressTaken);
+ auto Result = Pass.run(*Function, Target, Analyses);
+ if (!Result)
+ std::cerr << "tail merge failure: " << Result.error().message() << '\n';
+ Context.expect(Result && Result->Changed && !Second->isLinked() &&
+ Entry->successors().size() == 1 &&
+ Entry->successors().front().Block == First &&
+ branchTarget(*Entry->firstInstruction()) == First &&
+ branchTarget(*Entry->lastInstruction()) == First &&
+ rcc::verifyTrackedMachineLiveness(*Function, Target).has_value(),
+ "tail merge must redirect identical tails and repair liveness");
+}
+
+void testStackSlotColoring(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Function = makeFunction(Context, "stack_slot_coloring", 15);
+ if (!Function)
+ return;
+ rcc::MachineBasicBlock *Block = appendBlock(Context, *Function);
+ auto Align = rcc::Alignment::create(8);
+ if (Block == nullptr || !Align)
+ return;
+ rcc::StackObjectDesc Description;
+ Description.Kind = rcc::StackObjectKind::SpillSlot;
+ Description.Size = 8;
+ Description.Align = *Align;
+ auto FirstSlot = Function->frameInfo().createObject(Description);
+ auto SecondSlot = Function->frameInfo().createObject(Description);
+ if (!FirstSlot || !SecondSlot)
+ return;
+ rcc::MachineIRBuilder Builder(*Function, Target);
+ if (!Builder.setInsertPoint(*Block) ||
+ buildStackAccess(Context, Builder, rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X10), *FirstSlot,
+ rcc::MemoryAccessFlag::Store) == nullptr ||
+ buildStackAccess(Context, Builder, rcc::riscv::InstructionOpcode::LD,
+ physical(rcc::riscv::PhysicalRegisterID::X11), *FirstSlot,
+ rcc::MemoryAccessFlag::Load) == nullptr ||
+ buildStackAccess(Context, Builder, rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X12), *SecondSlot,
+ rcc::MemoryAccessFlag::Store) == nullptr ||
+ buildStackAccess(Context, Builder, rcc::riscv::InstructionOpcode::LD,
+ physical(rcc::riscv::PhysicalRegisterID::X13), *SecondSlot,
+ rcc::MemoryAccessFlag::Load) == nullptr) {
+ return;
+ }
+ setPostRAProperties(*Function);
+ Context.expect(rcc::rebuildMachineLiveness(*Function, Target).has_value(),
+ "stack-slot coloring liveness must be rebuilt");
+ rcc::MachineFunctionAnalysisManager Analyses;
+ rcc::MachineStackSlotColoringPass Pass;
+ auto Result = Pass.run(*Function, Target, Analyses);
+ if (!Result)
+ std::cerr << "stack-slot coloring failure: " << Result.error().message() << '\n';
+ const rcc::StackObject *Colored = Function->frameInfo().object(*SecondSlot);
+ rcc::MachineInstr *SecondStore = Block->firstInstruction();
+ SecondStore = SecondStore == nullptr ? nullptr : SecondStore->next();
+ SecondStore = SecondStore == nullptr ? nullptr : SecondStore->next();
+ const auto *Frame =
+ SecondStore == nullptr ? nullptr : SecondStore->operandAt(1)->getIf<rcc::FrameIndexOperand>();
+ Context.expect(
+ Result && Result->Changed && Colored != nullptr && Colored->description() != nullptr &&
+ Colored->description()->IsDead && Frame != nullptr && Frame->Index == *FirstSlot &&
+ SecondStore->memOperands().front()->description().Base.frameIndex() == *FirstSlot,
+ "stack-slot coloring must coalesce disjoint spills and rewrite MMOs");
+
+ auto OverlapFunction = makeFunction(Context, "stack_slot_overlap", 17);
+ if (!OverlapFunction)
+ return;
+ rcc::MachineBasicBlock *OverlapBlock = appendBlock(Context, *OverlapFunction);
+ auto Left = OverlapFunction->frameInfo().createObject(Description);
+ auto Right = OverlapFunction->frameInfo().createObject(Description);
+ if (OverlapBlock == nullptr || !Left || !Right)
+ return;
+ rcc::MachineIRBuilder OverlapBuilder(*OverlapFunction, Target);
+ if (!OverlapBuilder.setInsertPoint(*OverlapBlock) ||
+ buildStackAccess(Context, OverlapBuilder, rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X10), *Left,
+ rcc::MemoryAccessFlag::Store) == nullptr ||
+ buildStackAccess(Context, OverlapBuilder, rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X11), *Right,
+ rcc::MemoryAccessFlag::Store) == nullptr ||
+ buildStackAccess(Context, OverlapBuilder, rcc::riscv::InstructionOpcode::LD,
+ physical(rcc::riscv::PhysicalRegisterID::X12), *Left,
+ rcc::MemoryAccessFlag::Load) == nullptr ||
+ buildStackAccess(Context, OverlapBuilder, rcc::riscv::InstructionOpcode::LD,
+ physical(rcc::riscv::PhysicalRegisterID::X13), *Right,
+ rcc::MemoryAccessFlag::Load) == nullptr) {
+ return;
+ }
+ setPostRAProperties(*OverlapFunction);
+ Context.expect(rcc::rebuildMachineLiveness(*OverlapFunction, Target).has_value(),
+ "overlapping stack-slot fixture liveness must be rebuilt");
+ auto OverlapResult = Pass.run(*OverlapFunction, Target, Analyses);
+ const rcc::StackObject *RightObject = OverlapFunction->frameInfo().object(*Right);
+ Context.expect(OverlapResult && !OverlapResult->Changed && RightObject != nullptr &&
+ RightObject->description() != nullptr && !RightObject->description()->IsDead,
+ "stack-slot coloring must preserve overlapping spill lifetimes");
+}
+
+void testRISCVMemoryFoldingBoundary(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Function = makeFunction(Context, "riscv_memory_folding", 16);
+ if (!Function)
+ return;
+ rcc::MachineBasicBlock *Block = appendBlock(Context, *Function);
+ auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64);
+ auto Align = rcc::Alignment::create(8);
+ if (Block == nullptr || !I64 || !Align)
+ return;
+ auto Loaded = Function->registerInfo().createVirtualRegister(*I64);
+ auto ResultReg = Function->registerInfo().createVirtualRegister(*I64);
+ if (!Loaded || !ResultReg || !constrainGPR64(Context, *Function, *Loaded) ||
+ !constrainGPR64(Context, *Function, *ResultReg)) {
+ return;
+ }
+ rcc::StackObjectDesc Description;
+ Description.Size = 8;
+ Description.Align = *Align;
+ auto Slot = Function->frameInfo().createObject(std::move(Description));
+ if (!Slot)
+ return;
+ rcc::MachineIRBuilder Builder(*Function, Target);
+ rcc::MachineInstr *Load = nullptr;
+ rcc::MachineInstr *Consumer = nullptr;
+ if (Builder.setInsertPoint(*Block)) {
+ Load = buildStackAccess(Context, Builder, rcc::riscv::InstructionOpcode::LD, *Loaded, *Slot,
+ rcc::MemoryAccessFlag::Load);
+ Consumer = buildADDI(Context, Builder, *ResultReg, *Loaded, 1);
+ }
+ if (Load == nullptr || Consumer == nullptr)
+ return;
+ setSelectedProperties(*Function);
+ Function->properties().set(rcc::MachineFunctionProperty::IsSSA);
+ rcc::MachineFunctionAnalysisManager Analyses;
+ rcc::MachineMemoryFoldingPass Pass(rcc::riscv::memoryFoldingInfo());
+ auto Result = Pass.run(*Function, Target, Analyses);
+ if (!Result)
+ std::cerr << "RISC-V memory folding boundary failure: " << Result.error().message() << '\n';
+ Context.expect(Result && !Result->Changed && Load->parent() == Block &&
+ Load->memOperands().size() == 1 && Consumer->parent() == Block,
+ "RV64GC must explicitly decline unsupported folded-memory ALU forms");
+}
+
+void testShrinkWrappedFrameRegion(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Function = makeFunction(Context, "shrink_wrapped_frame", 20);
+ if (!Function)
+ return;
+ rcc::MachineBasicBlock *Entry = appendBlock(Context, *Function);
+ rcc::MachineBasicBlock *Cold = appendBlock(Context, *Function);
+ rcc::MachineBasicBlock *Hot = appendBlock(Context, *Function);
+ auto Align = rcc::Alignment::create(8);
+ auto ColdProbability = rcc::BranchProbability::fromFraction(1, 10);
+ auto HotProbability = rcc::BranchProbability::fromFraction(9, 10);
+ if (Entry == nullptr || Cold == nullptr || Hot == nullptr || !Align || !ColdProbability ||
+ !HotProbability) {
+ return;
+ }
+ auto CFG = Function->cfgUpdater();
+ if (!CFG.addSuccessor(*Entry, *Cold, *ColdProbability) ||
+ !CFG.addSuccessor(*Entry, *Hot, *HotProbability)) {
+ return;
+ }
+ rcc::StackObjectDesc SpillDescription;
+ SpillDescription.Kind = rcc::StackObjectKind::SpillSlot;
+ SpillDescription.Size = 8;
+ SpillDescription.Align = *Align;
+ auto Spill = Function->frameInfo().createObject(SpillDescription);
+ if (!Spill)
+ return;
+
+ rcc::MachineIRBuilder Builder(*Function, Target);
+ if (!Builder.setInsertPoint(*Cold) ||
+ buildStackAccess(Context, Builder, rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X10), *Spill,
+ rcc::MemoryAccessFlag::Store) == nullptr) {
+ return;
+ }
+ auto ColdReturn = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::PseudoRET));
+ if (!ColdReturn || !ColdReturn->addImplicitUse(physical(rcc::riscv::PhysicalRegisterID::X10)) ||
+ !ColdReturn->build() || !Builder.setInsertPoint(*Hot)) {
+ return;
+ }
+ auto HotReturn = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::PseudoRET));
+ if (!HotReturn || !HotReturn->addImplicitUse(physical(rcc::riscv::PhysicalRegisterID::X10)) ||
+ !HotReturn->build()) {
+ return;
+ }
+
+ setPostRAProperties(*Function);
+ Context.expect(rcc::rebuildMachineLiveness(*Function, Target).has_value(),
+ "shrink-wrapping fixture liveness must be rebuilt");
+ rcc::MachineFunctionAnalysisManager Analyses;
+ rcc::MachineShrinkWrappingPass Shrink{subtarget().frameLowering()};
+ auto Shrunk = Shrink.run(*Function, Target, Analyses);
+ Context.expect(Shrunk && Shrunk->Changed &&
+ Function->frameInfo().state().SavePoints.size() == 1 &&
+ Function->frameInfo().state().SavePoints.front().Block == Cold->stableID() &&
+ Function->frameInfo().state().RestorePoints.size() == 1 &&
+ Function->frameInfo().state().RestorePoints.front().Block == Cold->stableID(),
+ "shrink wrapping must select the cold frame region only");
+ if (!Shrunk)
+ return;
+
+ rcc::MachineFunctionPassManager FramePipeline;
+ Context.expect(FramePipeline
+ .addPass(std::make_unique<rcc::PrologueEpilogueInsertionPass>(
+ subtarget().frameLowering()))
+ .has_value(),
+ "shrink-wrapped PEI pass must register");
+ auto Lowered = FramePipeline.run(*Function, Target, Analyses);
+ if (!Lowered)
+ std::cerr << "shrink-wrapped PEI failure: " << Lowered.error().message() << '\n';
+ const bool EntryHasFrameCode = std::ranges::any_of(
+ Function->allInstructions(), [Entry](const rcc::MachineInstr *Instruction) {
+ return Instruction != nullptr && Instruction->parent() == Entry &&
+ (Instruction->flags().has(rcc::MachineInstrFlag::FrameSetup) ||
+ Instruction->flags().has(rcc::MachineInstrFlag::FrameDestroy));
+ });
+ const bool ColdHasSetup = std::ranges::any_of(
+ Function->allInstructions(), [Cold](const rcc::MachineInstr *Instruction) {
+ return Instruction != nullptr && Instruction->parent() == Cold &&
+ Instruction->flags().has(rcc::MachineInstrFlag::FrameSetup);
+ });
+ const bool ColdHasDestroy = std::ranges::any_of(
+ Function->allInstructions(), [Cold](const rcc::MachineInstr *Instruction) {
+ return Instruction != nullptr && Instruction->parent() == Cold &&
+ Instruction->flags().has(rcc::MachineInstrFlag::FrameDestroy);
+ });
+ Context.expect(
+ Lowered && !EntryHasFrameCode && ColdHasSetup && ColdHasDestroy &&
+ Function->properties().has(rcc::MachineFunctionProperty::FrameFinalized),
+ "PEI must materialize a shrink-wrapped prologue and epilogue only in the frame region");
+
+ rcc::MachineBlockPlacementPass Placement;
+ auto Placed = Placement.run(*Function, Target, Analyses);
+ Context.expect(Placed && Cold->next() == nullptr,
+ "block placement must keep a shrink-wrapped frame region as a layout suffix");
+}
+
+void testCompressedAndConditionalRelaxation(TestContext &Context,
+ const rcc::TargetDescription &Target) {
+ auto Function = makeFunction(Context, "branch_relaxation", 3);
+ if (!Function)
+ return;
+ rcc::MachineBasicBlock *Entry = appendBlock(Context, *Function);
+ rcc::MachineBasicBlock *Padding = appendBlock(Context, *Function);
+ rcc::MachineBasicBlock *Far = appendBlock(Context, *Function);
+ if (Entry == nullptr || Padding == nullptr || Far == nullptr)
+ return;
+ auto Half = rcc::BranchProbability::fromFraction(1, 2);
+ auto CFG = Function->cfgUpdater();
+ Context.expect(Half && CFG.addSuccessor(*Entry, *Far, *Half).has_value() &&
+ CFG.addSuccessor(*Entry, *Padding, *Half).has_value() &&
+ CFG.addSuccessor(*Padding, *Far, rcc::BranchProbability::one()).has_value(),
+ "relaxation CFG construction must succeed");
+ rcc::MachineIRBuilder Builder(*Function, Target);
+ Context.expect(Builder.setInsertPoint(*Entry).has_value(),
+ "relaxation entry insertion point must be established");
+ rcc::MachineInstr *Conditional =
+ buildConditional(Context, Builder, rcc::riscv::InstructionOpcode::BEQ, *Far);
+ Context.expect(Builder.setInsertPoint(*Padding).has_value(),
+ "relaxation padding insertion point must be established");
+ for (std::size_t Index = 0; Index < 1100; ++Index) {
+ if (buildADDI(Context, Builder, physical(rcc::riscv::PhysicalRegisterID::X0),
+ physical(rcc::riscv::PhysicalRegisterID::X0), 0) == nullptr) {
+ return;
+ }
+ }
+ (void)buildJAL(Context, Builder, *Far);
+ if (Conditional == nullptr)
+ return;
+ setRelaxationProperties(Context, *Function, Target);
+ rcc::MachineFunctionAnalysisManager Analyses;
+ rcc::MachineBranchRelaxationPass Pass(rcc::riscv::instrInfo());
+ auto Result = Pass.run(*Function, Target, Analyses);
+ if (!Result)
+ std::cerr << "conditional relaxation failure: " << Result.error().message() << '\n';
+ Context.expect(Result && Result->Changed &&
+ Conditional->opcode() == opcode(rcc::riscv::InstructionOpcode::BNE) &&
+ branchTarget(*Conditional) == Padding,
+ "out-of-range conditional branch must invert onto the local fallthrough");
+ rcc::MachineInstr *LongJump = Conditional->next();
+ Context.expect(LongJump != nullptr &&
+ LongJump->opcode() == opcode(rcc::riscv::InstructionOpcode::JAL) &&
+ branchTarget(*LongJump) == Far,
+ "conditional relaxation must insert a reachable long jump to the old target");
+
+ auto CompressedFunction = makeFunction(Context, "compressed_relaxation", 4);
+ if (!CompressedFunction)
+ return;
+ rcc::MachineBasicBlock *CompressedEntry = appendBlock(Context, *CompressedFunction);
+ rcc::MachineBasicBlock *CompressedTarget = appendBlock(Context, *CompressedFunction);
+ rcc::MachineIRBuilder CompressedBuilder(*CompressedFunction, Target);
+ Context.expect(CompressedEntry != nullptr && CompressedTarget != nullptr &&
+ CompressedBuilder.setInsertPoint(*CompressedEntry).has_value(),
+ "compressed relaxation fixture must be valid");
+ if (CompressedEntry == nullptr || CompressedTarget == nullptr)
+ return;
+ auto Pending = CompressedBuilder.buildInstr(opcode(rcc::riscv::InstructionOpcode::C_J));
+ if (!Pending || !Pending->addBasicBlock(*CompressedTarget)) {
+ Context.expect(false, "compressed jump construction must succeed");
+ return;
+ }
+ auto Compressed = Pending->build();
+ Context.expect(Compressed.has_value(), "compressed jump insertion must succeed");
+ if (!Compressed)
+ return;
+ auto Expanded =
+ rcc::riscv::instrInfo().relaxBranch(**Compressed, 4096, *CompressedFunction, Target);
+ Context.expect(Expanded && *Expanded && CompressedEntry->firstInstruction() != nullptr &&
+ CompressedEntry->firstInstruction()->opcode() ==
+ opcode(rcc::riscv::InstructionOpcode::JAL) &&
+ (*Compressed)->parent() == nullptr,
+ "compressed jump relaxation must monotonically expand to JAL");
+}
+
+void testLongJALRelaxation(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Function = makeFunction(Context, "long_jal", 5);
+ if (!Function)
+ return;
+ rcc::MachineBasicBlock *Entry = appendBlock(Context, *Function);
+ rcc::MachineBasicBlock *TargetBlock = appendBlock(Context, *Function);
+ if (Entry == nullptr || TargetBlock == nullptr)
+ return;
+ rcc::MachineIRBuilder Builder(*Function, Target);
+ Context.expect(Builder.setInsertPoint(*Entry).has_value(),
+ "long JAL insertion point must be established");
+ rcc::MachineInstr *JAL = buildJAL(Context, Builder, *TargetBlock);
+ if (JAL == nullptr)
+ return;
+ auto X5 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X5);
+ auto X6 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X6);
+ Context.expect(X5 && X6 && TargetBlock->addLiveIn(*X5).has_value() &&
+ TargetBlock->addLiveIn(*X6).has_value() &&
+ JAL->setPreInstrSymbol(".Lpreserved_long_branch").has_value() &&
+ JAL->setPostInstrSymbol(".Lafter_long_branch").has_value(),
+ "long JAL live-in and annotation fixture must be valid");
+ if (!X5 || !X6)
+ return;
+ auto TooFar = rcc::riscv::instrInfo().relaxBranch(*JAL, std::numeric_limits<std::int64_t>::max(),
+ *Function, Target);
+ Context.expect(!TooFar && TooFar.error().code() == rcc::ErrorCode::BranchRelaxationFailed &&
+ JAL->parent() == Entry,
+ "long JAL relaxation must reject an unencodable PC-relative distance atomically");
+ auto Relaxed =
+ rcc::riscv::instrInfo().relaxBranch(*JAL, std::int64_t{2} * 1024 * 1024, *Function, Target);
+ if (!Relaxed)
+ std::cerr << "long JAL relaxation failure: " << Relaxed.error().message() << '\n';
+ rcc::MachineInstr *High = Entry->firstInstruction();
+ rcc::MachineInstr *Low = High == nullptr ? nullptr : High->next();
+ const rcc::Register *Scratch = High == nullptr || High->operandAt(0) == nullptr
+ ? nullptr
+ : High->operandAt(0)->getIf<rcc::Register>();
+ const std::optional<rcc::PhysicalRegister> ScratchPhysical =
+ Scratch == nullptr ? std::nullopt : Scratch->physicalRegister();
+ Context.expect(Relaxed && *Relaxed && High != nullptr && Low != nullptr &&
+ High->opcode() == opcode(rcc::riscv::InstructionOpcode::AUIPC) &&
+ Low->opcode() == opcode(rcc::riscv::InstructionOpcode::JALR) &&
+ ScratchPhysical && *ScratchPhysical != *X5 && *ScratchPhysical != *X6 &&
+ High->operandAt(1)->targetFlags() == 2 &&
+ Low->operandAt(2)->targetFlags() == 4 &&
+ High->flags().has(rcc::MachineInstrFlag::BundledWithSucc) &&
+ Low->flags().has(rcc::MachineInstrFlag::BundledWithPred) &&
+ High->preInstrSymbol() == ".Lpreserved_long_branch" &&
+ Low->postInstrSymbol() == ".Lafter_long_branch" && JAL->parent() == nullptr,
+ "out-of-range JAL must use a dead scratch and preserve its paired annotations");
+}
+
+void testPseudoPreparation(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Function = makeFunction(Context, "pseudo_preparation", 6);
+ if (!Function)
+ return;
+ rcc::MachineBasicBlock *Block = appendBlock(Context, *Function);
+ auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64);
+ auto F64 = rcc::LowLevelType::makeCategoryNeutralScalar(64);
+ if (Block == nullptr || !I64 || !F64)
+ return;
+ auto Source = Function->registerInfo().createVirtualRegister(*I64);
+ auto Destination = Function->registerInfo().createVirtualRegister(*F64);
+ const auto GPRBank =
+ rcc::RegisterBankID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterBank::GPR));
+ const auto FPRBank =
+ rcc::RegisterBankID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterBank::FPR));
+ const auto GPRClass =
+ rcc::RegisterClassID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterClass::GPR));
+ const auto FPRClass =
+ rcc::RegisterClassID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterClass::FPR64));
+ Context.expect(Source && Destination &&
+ Function->registerInfo().setRegisterBank(*Source, GPRBank).has_value() &&
+ Function->registerInfo().setRegisterClass(*Source, GPRClass).has_value() &&
+ Function->registerInfo().setRegisterBank(*Destination, FPRBank).has_value() &&
+ Function->registerInfo().setRegisterClass(*Destination, FPRClass).has_value(),
+ "pseudo preparation virtual-register constraints must be valid");
+ if (!Source || !Destination)
+ return;
+ rcc::MachineIRBuilder Builder(*Function, Target);
+ if (!Builder.setInsertPoint(*Block))
+ return;
+ auto Pending = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::COPY));
+ if (!Pending || !Pending->addDef(*Destination) || !Pending->addUse(*Source))
+ return;
+ auto Copy = Pending->build();
+ if (!Copy)
+ return;
+ setSelectedProperties(*Function);
+ rcc::MachineFunctionAnalysisManager Analyses;
+ rcc::PreparePostRAPseudosPass Pass(rcc::riscv::pseudoExpansionInfo());
+ auto Result = Pass.run(*Function, Target, Analyses);
+ Context.expect(Result && Result->Changed && (*Copy)->operandAt(0)->targetFlags() == 16 &&
+ (*Copy)->operandAt(1)->targetFlags() == 16,
+ "pre-RA pseudo preparation must retain a 64-bit cross-bank COPY width");
+}
+
+void testPostRAPseudoExpansionAndFinalization(TestContext &Context,
+ const rcc::TargetDescription &Target) {
+ auto Function = makeFunction(Context, "post_ra_pseudos", 7);
+ if (!Function)
+ return;
+ rcc::MachineBasicBlock *Block = appendBlock(Context, *Function);
+ if (Block == nullptr)
+ return;
+ rcc::MachineIRBuilder Builder(*Function, Target);
+ if (!Builder.setInsertPoint(*Block))
+ return;
+
+ auto GPRCopy = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::COPY));
+ if (!GPRCopy || !GPRCopy->addDef(physical(rcc::riscv::PhysicalRegisterID::X11)) ||
+ !GPRCopy->addUse(physical(rcc::riscv::PhysicalRegisterID::X10)) || !GPRCopy->build()) {
+ Context.expect(false, "post-RA GPR copy construction must succeed");
+ return;
+ }
+ auto CrossCopy = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::COPY));
+ if (!CrossCopy ||
+ !CrossCopy->addDef(physical(rcc::riscv::PhysicalRegisterID::F10_D), {}, {}, 16) ||
+ !CrossCopy->addUse(physical(rcc::riscv::PhysicalRegisterID::X10), {}, {}, 16) ||
+ !CrossCopy->build()) {
+ Context.expect(false, "post-RA cross-bank copy construction must succeed");
+ return;
+ }
+ auto ReadFFLAGS = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::ReadFFLAGS));
+ if (!ReadFFLAGS || !ReadFFLAGS->addDef(physical(rcc::riscv::PhysicalRegisterID::X12)) ||
+ !ReadFFLAGS->build()) {
+ Context.expect(false, "ReadFFLAGS pseudo construction must succeed");
+ return;
+ }
+ auto WriteFFLAGS = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::WriteFFLAGS));
+ if (!WriteFFLAGS || !WriteFFLAGS->addUse(physical(rcc::riscv::PhysicalRegisterID::X12)) ||
+ !WriteFFLAGS->build()) {
+ Context.expect(false, "WriteFFLAGS pseudo construction must succeed");
+ return;
+ }
+ auto Call = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::PseudoCALL));
+ auto Preserved = rcc::riscv::createCallPreservedMask(rcc::riscv::CallingConvention::LP64D);
+ if (!Call || !Call->addExternalSymbol("callee", 0, 1) || !Call->addRegisterMask(Preserved) ||
+ !Call->addImplicitUse(physical(rcc::riscv::PhysicalRegisterID::X10)) || !Call->build()) {
+ Context.expect(false, "post-RA call pseudo construction must succeed");
+ return;
+ }
+ auto Return = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::PseudoRET));
+ if (!Return || !Return->addImplicitUse(physical(rcc::riscv::PhysicalRegisterID::X10)) ||
+ !Return->build()) {
+ Context.expect(false, "post-RA return pseudo construction must succeed");
+ return;
+ }
+
+ setRelaxationProperties(Context, *Function, Target);
+ Function->properties().clear(rcc::MachineFunctionProperty::NoExecutablePseudos);
+ rcc::MachineFunctionAnalysisManager Analyses;
+ rcc::ExpandPostRAPseudosPass Expansion(rcc::riscv::pseudoExpansionInfo());
+ auto Expanded = Expansion.run(*Function, Target, Analyses);
+ if (!Expanded)
+ std::cerr << "post-RA pseudo expansion failure: " << Expanded.error().message() << '\n';
+ std::size_t ADDICount = 0;
+ std::size_t CrossMoveCount = 0;
+ std::size_t CallCount = 0;
+ std::size_t ReturnCount = 0;
+ std::size_t CSRRSCount = 0;
+ std::size_t CSRRWCount = 0;
+ for (rcc::MachineInstr *Instruction = Block->firstInstruction(); Instruction != nullptr;
+ Instruction = Instruction->next()) {
+ ADDICount += Instruction->opcode() == opcode(rcc::riscv::InstructionOpcode::ADDI);
+ CrossMoveCount += Instruction->opcode() == opcode(rcc::riscv::InstructionOpcode::FMV_D_X);
+ CallCount += Instruction->opcode() == opcode(rcc::riscv::InstructionOpcode::JALR_CALL);
+ ReturnCount += Instruction->opcode() == opcode(rcc::riscv::InstructionOpcode::JALR_RET);
+ CSRRSCount += Instruction->opcode() == opcode(rcc::riscv::InstructionOpcode::CSRRS);
+ CSRRWCount += Instruction->opcode() == opcode(rcc::riscv::InstructionOpcode::CSRRW);
+ }
+ Context.expect(Expanded && Expanded->Changed && ADDICount == 1 && CrossMoveCount == 1 &&
+ CallCount == 1 && ReturnCount == 1 && CSRRSCount == 1 && CSRRWCount == 1,
+ "post-RA expansion must lower copies, calls, returns, and FFLAGS accesses");
+ if (!Expanded)
+ return;
+
+ rcc::MachineFunctionPassManager Pipeline;
+ Context.expect(Pipeline.addPass(std::make_unique<rcc::FinalizeEmissionPseudosPass>()) &&
+ Pipeline.addPass(std::make_unique<rcc::FinalEncodabilityVerifierPass>(
+ rcc::riscv::instrInfo())),
+ "finalization passes must register");
+ auto Finalized = Pipeline.run(*Function, Target, Analyses);
+ if (!Finalized)
+ std::cerr << "final encodability failure: " << Finalized.error().message() << '\n';
+ Context.expect(Finalized &&
+ Function->properties().has(rcc::MachineFunctionProperty::NoExecutablePseudos),
+ "emission finalization must exclusively publish NoExecutablePseudos");
+}
+
+void testCanonicalOptimizationPipelines(TestContext &Context,
+ const rcc::TargetDescription &Target) {
+ const rcc::MachineOptimizationPipelineTargetInfo TargetInfo =
+ rcc::riscv::optimizationPipelineInfo(subtarget());
+ rcc::MachineFunctionPassManager SelectedPipeline;
+ rcc::MachineFunctionPassManager PreRAPipeline;
+ Context.expect(rcc::addSelectedSSAOptimizationPipeline(SelectedPipeline, TargetInfo) &&
+ SelectedPipeline.size() == 6,
+ "canonical selected-SSA optimization pipeline must register completely");
+ Context.expect(rcc::addPreRegisterAllocationOptimizationPipeline(PreRAPipeline, TargetInfo) &&
+ PreRAPipeline.size() == 2,
+ "canonical pre-RA optimization pipeline must register completely");
+
+ auto Function = makeFunction(Context, "canonical_post_ra_pipeline", 21);
+ if (!Function)
+ return;
+ rcc::MachineBasicBlock *Block = appendBlock(Context, *Function);
+ if (Block == nullptr)
+ return;
+ auto SlotAlignment = rcc::Alignment::create(8);
+ if (!SlotAlignment)
+ return;
+ rcc::StackObjectDesc SpillDescription;
+ SpillDescription.Kind = rcc::StackObjectKind::SpillSlot;
+ SpillDescription.Size = 8;
+ SpillDescription.Align = *SlotAlignment;
+ auto Spill = Function->frameInfo().createObject(SpillDescription);
+ if (!Spill)
+ return;
+ rcc::MachineIRBuilder Builder(*Function, Target);
+ if (!Builder.setInsertPoint(*Block) ||
+ buildADDI(Context, Builder, physical(rcc::riscv::PhysicalRegisterID::X10),
+ physical(rcc::riscv::PhysicalRegisterID::X0), 42) == nullptr ||
+ buildStackAccess(Context, Builder, rcc::riscv::InstructionOpcode::SD,
+ physical(rcc::riscv::PhysicalRegisterID::X10), *Spill,
+ rcc::MemoryAccessFlag::Store) == nullptr) {
+ return;
+ }
+ auto Return = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::PseudoRET));
+ if (!Return || !Return->addImplicitUse(physical(rcc::riscv::PhysicalRegisterID::X10)) ||
+ !Return->build()) {
+ Context.expect(false, "canonical post-RA return construction must succeed");
+ return;
+ }
+ setPostRAProperties(*Function);
+ if (!rcc::rebuildMachineLiveness(*Function, Target)) {
+ Context.expect(false, "canonical post-RA input liveness must be valid");
+ return;
+ }
+
+ rcc::MachineFunctionPassManager Pipeline;
+ auto Added = rcc::addPostRegisterAllocationPipeline(Pipeline, TargetInfo);
+ Context.expect(Added && Pipeline.size() == 17,
+ "canonical post-RA pipeline must register every required pass");
+ if (!Added)
+ return;
+ rcc::MachineFunctionAnalysisManager Analyses;
+ auto Registered = rcc::registerMachineOptimizationAnalyses(Analyses);
+ Context.expect(Registered.has_value(),
+ "canonical optimization analyses must register completely");
+ if (!Registered)
+ return;
+ auto Result = Pipeline.run(*Function, Target, Analyses);
+ if (!Result)
+ std::cerr << "canonical post-RA pipeline failure: " << Result.error().message() << '\n';
+ const bool HasReturn =
+ std::ranges::any_of(Function->allInstructions(), [](const rcc::MachineInstr *Instruction) {
+ return Instruction != nullptr &&
+ Instruction->opcode() == opcode(rcc::riscv::InstructionOpcode::JALR_RET);
+ });
+ const bool HasFrameSetup =
+ std::ranges::any_of(Function->allInstructions(), [](const rcc::MachineInstr *Instruction) {
+ return Instruction != nullptr &&
+ Instruction->flags().has(rcc::MachineInstrFlag::FrameSetup);
+ });
+ Context.expect(Result && HasReturn && HasFrameSetup &&
+ Function->properties().has(rcc::MachineFunctionProperty::FrameFinalized) &&
+ Function->properties().has(rcc::MachineFunctionProperty::NoFrameIndices) &&
+ Function->properties().has(rcc::MachineFunctionProperty::NoExecutablePseudos),
+ "canonical post-RA pipeline must reach final encodable RV64GC MachineIR");
+}
+
+void testTargetLateExpansionRejectsSizeGrowth(TestContext &Context) {
+ auto Target = createLateExpansionSizeTarget();
+ Context.expect(Target.has_value(), "target-late size-contract target construction must succeed");
+ if (!Target)
+ return;
+ auto Function = makeFunction(Context, "late_expansion_size_growth", 22);
+ if (!Function)
+ return;
+ rcc::MachineBasicBlock *Block = appendBlock(Context, *Function);
+ if (Block == nullptr)
+ return;
+ rcc::MachineIRBuilder Builder(*Function, *Target);
+ if (!Builder.setInsertPoint(*Block))
+ return;
+ auto Pending = Builder.buildInstr(GrowingLatePseudoOpcode);
+ if (!Pending)
+ return;
+ auto Pseudo = Pending->build();
+ Context.expect(Pseudo.has_value(), "target-late growth pseudo construction must succeed");
+ if (!Pseudo)
+ return;
+
+ setPostRAProperties(*Function);
+ auto StackAlignment = rcc::Alignment::create(16);
+ if (!StackAlignment || !Function->frameInfo().freeze(0, *StackAlignment))
+ return;
+ Function->properties().set(rcc::MachineFunctionProperty::FrameFinalized);
+ Function->properties().set(rcc::MachineFunctionProperty::NoFrameIndices);
+ if (!rcc::rebuildMachineLiveness(*Function, *Target))
+ return;
+
+ GrowingLatePseudoInfo ExpansionInfo;
+ FixedSizeInstrInfo InstrInfo;
+ rcc::TargetLatePseudoExpansionPass Pass(ExpansionInfo, InstrInfo);
+ rcc::MachineFunctionAnalysisManager Analyses;
+ auto Result = Pass.run(*Function, *Target, Analyses);
+ Context.expect(!Result && Result.error().code() == rcc::ErrorCode::PseudoExpansionFailed &&
+ (*Pseudo)->parent() == Block && Block->firstInstruction() == *Pseudo &&
+ Block->lastInstruction() == *Pseudo,
+ "target-late expansion must reject block-size growth atomically");
+}
+
+void testTargetLateExpansionRejectsCompensatedGrowth(TestContext &Context) {
+ auto Target = createLateExpansionSizeTarget();
+ Context.expect(Target.has_value(), "target-late compensation target construction must succeed");
+ if (!Target)
+ return;
+ auto Function = makeFunction(Context, "late_expansion_compensated_growth", 23);
+ if (!Function)
+ return;
+ rcc::MachineBasicBlock *Block = appendBlock(Context, *Function);
+ if (Block == nullptr)
+ return;
+ rcc::MachineIRBuilder Builder(*Function, *Target);
+ if (!Builder.setInsertPoint(*Block))
+ return;
+ auto Shrinking = Builder.buildInstr(ShrinkingLatePseudoOpcode);
+ auto ShrinkingPseudo =
+ Shrinking ? Shrinking->build()
+ : rcc::Expected<rcc::MachineInstr *>(std::unexpected(
+ rcc::Error(rcc::ErrorCode::InternalError, "no shrinking pseudo builder")));
+ auto Growing = Builder.buildInstr(GrowingLatePseudoOpcode);
+ auto GrowingPseudo = Growing ? Growing->build()
+ : rcc::Expected<rcc::MachineInstr *>(std::unexpected(rcc::Error(
+ rcc::ErrorCode::InternalError, "no growing pseudo builder")));
+ Context.expect(ShrinkingPseudo.has_value() && GrowingPseudo.has_value(),
+ "compensated target-late pseudos must be constructed");
+ if (!ShrinkingPseudo || !GrowingPseudo)
+ return;
+
+ setPostRAProperties(*Function);
+ auto StackAlignment = rcc::Alignment::create(16);
+ if (!StackAlignment || !Function->frameInfo().freeze(0, *StackAlignment))
+ return;
+ Function->properties().set(rcc::MachineFunctionProperty::FrameFinalized);
+ Function->properties().set(rcc::MachineFunctionProperty::NoFrameIndices);
+ if (!rcc::rebuildMachineLiveness(*Function, *Target))
+ return;
+
+ GrowingLatePseudoInfo ExpansionInfo;
+ FixedSizeInstrInfo InstrInfo;
+ rcc::TargetLatePseudoExpansionPass Pass(ExpansionInfo, InstrInfo);
+ rcc::MachineFunctionAnalysisManager Analyses;
+ auto Result = Pass.run(*Function, *Target, Analyses);
+ Context.expect(
+ !Result && Result.error().code() == rcc::ErrorCode::PseudoExpansionFailed &&
+ (*ShrinkingPseudo)->parent() == Block && (*GrowingPseudo)->parent() == Block &&
+ Block->firstInstruction() == *ShrinkingPseudo &&
+ Block->lastInstruction() == *GrowingPseudo,
+ "one growing target-late expansion must not be hidden by another pseudo shrinking");
+}
+
+void testFinalEncodabilityRejectsInvalidInstruction(TestContext &Context,
+ const rcc::TargetDescription &Target) {
+ auto ImmediateFunction = makeFunction(Context, "final_invalid_immediate", 18);
+ if (!ImmediateFunction)
+ return;
+ rcc::MachineBasicBlock *ImmediateBlock = appendBlock(Context, *ImmediateFunction);
+ if (ImmediateBlock == nullptr)
+ return;
+ rcc::MachineIRBuilder ImmediateBuilder(*ImmediateFunction, Target);
+ if (!ImmediateBuilder.setInsertPoint(*ImmediateBlock))
+ return;
+ rcc::MachineInstr *InvalidImmediate =
+ buildADDI(Context, ImmediateBuilder, physical(rcc::riscv::PhysicalRegisterID::X10),
+ physical(rcc::riscv::PhysicalRegisterID::X0), 0);
+ if (InvalidImmediate == nullptr || !InvalidImmediate->eraseOperand(2) ||
+ !InvalidImmediate->insertOperand(2, rcc::MachineOperand::immediate(4096))) {
+ Context.expect(false, "invalid final immediate fixture must be constructed");
+ return;
+ }
+ setRelaxationProperties(Context, *ImmediateFunction, Target);
+ rcc::MachineFunctionAnalysisManager Analyses;
+ rcc::FinalEncodabilityVerifierPass Verifier(rcc::riscv::instrInfo());
+ auto ImmediateResult = Verifier.run(*ImmediateFunction, Target, Analyses);
+ Context.expect(!ImmediateResult &&
+ ImmediateResult.error().code() == rcc::ErrorCode::FinalEncodabilityFailed,
+ "final encodability must reject an out-of-range target immediate");
+
+ auto BundleFunction = makeFunction(Context, "final_invalid_bundle", 19);
+ if (!BundleFunction)
+ return;
+ rcc::MachineBasicBlock *BundleBlock = appendBlock(Context, *BundleFunction);
+ if (BundleBlock == nullptr)
+ return;
+ rcc::MachineIRBuilder BundleBuilder(*BundleFunction, Target);
+ if (!BundleBuilder.setInsertPoint(*BundleBlock))
+ return;
+ rcc::MachineInstr *InvalidBundle =
+ buildADDI(Context, BundleBuilder, physical(rcc::riscv::PhysicalRegisterID::X10),
+ physical(rcc::riscv::PhysicalRegisterID::X0), 0);
+ if (InvalidBundle == nullptr)
+ return;
+ InvalidBundle->setFlags({rcc::MachineInstrFlag::BundledWithSucc});
+ setRelaxationProperties(Context, *BundleFunction, Target);
+ auto BundleResult = Verifier.run(*BundleFunction, Target, Analyses);
+ Context.expect(!BundleResult &&
+ BundleResult.error().code() == rcc::ErrorCode::FinalEncodabilityFailed,
+ "final encodability must reject an inconsistent bundle boundary");
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ auto Target = rcc::riscv::createTargetDescription();
+ Context.expect(Target.has_value(), "RV64GC target construction must succeed");
+ if (!Target)
+ return Context.result();
+ testBlockPlacement(Context, *Target);
+ testBranchOptimization(Context, *Target);
+ testTailDuplication(Context, *Target);
+ testBranchFoldingTailMerge(Context, *Target);
+ testStackSlotColoring(Context, *Target);
+ testRISCVMemoryFoldingBoundary(Context, *Target);
+ testShrinkWrappedFrameRegion(Context, *Target);
+ testCompressedAndConditionalRelaxation(Context, *Target);
+ testLongJALRelaxation(Context, *Target);
+ testPseudoPreparation(Context, *Target);
+ testPostRAPseudoExpansionAndFinalization(Context, *Target);
+ testCanonicalOptimizationPipelines(Context, *Target);
+ testTargetLateExpansionRejectsSizeGrowth(Context);
+ testTargetLateExpansionRejectsCompensatedGrowth(Context);
+ testFinalEncodabilityRejectsInvalidInstruction(Context, *Target);
+ return Context.result();
+}
diff --git a/backend/unittests/Target/RISCVMCEncodingCoverageTest.cpp b/backend/unittests/Target/RISCVMCEncodingCoverageTest.cpp
new file mode 100644
index 0000000..43a0c1d
--- /dev/null
+++ b/backend/unittests/Target/RISCVMCEncodingCoverageTest.cpp
@@ -0,0 +1,413 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/CodeGen/InstructionDescriptor.h"
+#include "rcc/CodeGen/TargetDescription.h"
+#include "rcc/MC/MCInst.h"
+#include "rcc/Target/RISCV/RISCVMCCodeEmitter.h"
+#include "rcc/Target/RISCV/RISCVTarget.h"
+
+#include <cstdint>
+#include <iostream>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace {
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCInst> fixtureError(std::string_view Message) {
+ return rcc::makeUnexpected(rcc::ErrorCode::InternalError, std::string(Message));
+}
+
+[[nodiscard]] std::int64_t signExtend(std::uint32_t Value, unsigned Width) {
+ const std::uint32_t Mask = (std::uint32_t{1} << Width) - 1U;
+ const std::uint32_t Sign = std::uint32_t{1} << (Width - 1U);
+ Value &= Mask;
+ if ((Value & Sign) == 0)
+ return Value;
+ return static_cast<std::int64_t>(Value) - (std::int64_t{1} << Width);
+}
+
+void addRegister(rcc::mc::MCInst &Instruction, const rcc::InstructionDescriptor &Descriptor,
+ std::uint32_t Encoding) {
+ const std::size_t OperandIndex = Instruction.operands().size();
+ const rcc::RegisterClassID Class = Descriptor.Operands[OperandIndex].RegClass.value_or(
+ rcc::RegisterClassID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterClass::GPR)));
+ const auto IsClass = [Class](rcc::riscv::RegisterClass Candidate) {
+ return Class.value() == static_cast<std::uint32_t>(Candidate);
+ };
+ std::uint32_t FirstRegister = 1;
+ if (IsClass(rcc::riscv::RegisterClass::FPR32) || IsClass(rcc::riscv::RegisterClass::FPR32C)) {
+ FirstRegister = static_cast<std::uint32_t>(rcc::riscv::PhysicalRegisterID::F0_F);
+ } else if (IsClass(rcc::riscv::RegisterClass::FPR64) ||
+ IsClass(rcc::riscv::RegisterClass::FPR64C)) {
+ FirstRegister = static_cast<std::uint32_t>(rcc::riscv::PhysicalRegisterID::F0_D);
+ }
+ const std::uint32_t ID = Encoding + FirstRegister;
+ Instruction.addOperand(rcc::mc::MCOperand::registerOperand(rcc::mc::MCRegister::fromValue(ID)));
+}
+
+void addImmediate(rcc::mc::MCInst &Instruction, std::int64_t Value) {
+ Instruction.addOperand(rcc::mc::MCOperand::immediate(Value));
+}
+
+[[nodiscard]] std::int64_t decodeBranchImmediate(std::uint32_t Encoding) {
+ const std::uint32_t Value = ((Encoding >> 31U) & 1U) << 12U | ((Encoding >> 25U) & 0x3fU) << 5U |
+ ((Encoding >> 8U) & 0xfU) << 1U | ((Encoding >> 7U) & 1U) << 11U;
+ return signExtend(Value, 13);
+}
+
+[[nodiscard]] std::int64_t decodeJALImmediate(std::uint32_t Encoding) {
+ const std::uint32_t Value = ((Encoding >> 31U) & 1U) << 20U | ((Encoding >> 21U) & 0x3ffU) << 1U |
+ ((Encoding >> 20U) & 1U) << 11U | ((Encoding >> 12U) & 0xffU) << 12U;
+ return signExtend(Value, 21);
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCInst>
+createStandardFixture(const rcc::InstructionDescriptor &Descriptor,
+ const rcc::riscv::AssemblyInstructionInfo &Assembly) {
+ using enum rcc::riscv::AssemblyFormat;
+ using enum rcc::riscv::InstructionOpcode;
+
+ const auto Category =
+ static_cast<rcc::riscv::EncodingCategory>(Descriptor.EncodingCategory.value());
+ const auto Format = static_cast<rcc::riscv::AssemblyFormat>(Assembly.Format.value());
+ const auto Opcode =
+ static_cast<rcc::riscv::InstructionOpcode>(Assembly.InstructionOpcode.value());
+ const std::uint32_t Encoding = Assembly.Encoding;
+ const std::uint32_t DestinationEncoding = (Encoding >> 7U) & 0x1fU;
+ const std::uint32_t Source1Encoding = (Encoding >> 15U) & 0x1fU;
+ const std::uint32_t Source2Encoding = (Encoding >> 20U) & 0x1fU;
+ rcc::mc::MCInst Result(rcc::mc::MCOpcode::fromValue(Assembly.InstructionOpcode.value()));
+
+ if (Category == rcc::riscv::EncodingCategory::R) {
+ switch (Format) {
+ case RD_RS1_RS2:
+ case FRD_FRS1_FRS2:
+ case RD_FRS1_FRS2:
+ addRegister(Result, Descriptor, DestinationEncoding);
+ addRegister(Result, Descriptor, Source1Encoding);
+ addRegister(Result, Descriptor, Source2Encoding);
+ return Result;
+ case FRD_FRS1_FRS2_RM:
+ addRegister(Result, Descriptor, DestinationEncoding);
+ addRegister(Result, Descriptor, Source1Encoding);
+ addRegister(Result, Descriptor, Source2Encoding);
+ addImmediate(Result, (Encoding >> 12U) & 7U);
+ return Result;
+ case FRD_FRS1_RM:
+ case RD_FRS1_RM:
+ case FRD_RS1_RM:
+ addRegister(Result, Descriptor, DestinationEncoding);
+ addRegister(Result, Descriptor, Source1Encoding);
+ addImmediate(Result, (Encoding >> 12U) & 7U);
+ return Result;
+ case RD_RS1:
+ case RD_FRS1:
+ case FRD_RS1:
+ addRegister(Result, Descriptor, DestinationEncoding);
+ addRegister(Result, Descriptor, Source1Encoding);
+ return Result;
+ default:
+ return fixtureError("unsupported R-type reference fixture format");
+ }
+ }
+
+ if (Category == rcc::riscv::EncodingCategory::I) {
+ if (Format == NONE)
+ return Result;
+ if (Format == IMM_IMM) {
+ addImmediate(Result, (Encoding >> 24U) & 0xfU);
+ addImmediate(Result, (Encoding >> 20U) & 0xfU);
+ return Result;
+ }
+ if (Format == RD_RS1) {
+ addRegister(Result, Descriptor, DestinationEncoding);
+ addRegister(Result, Descriptor, Source1Encoding);
+ return Result;
+ }
+ const bool FloatingDestination = Format == FRD_OFFSET_RS1;
+ if (Format != RD_RS1_IMM && Format != RD_OFFSET_RS1 && !FloatingDestination)
+ return fixtureError("unsupported I-type reference fixture format");
+ addRegister(Result, Descriptor, DestinationEncoding);
+ addRegister(Result, Descriptor, Source1Encoding);
+ const bool Shift64 = Opcode == SLLI || Opcode == SLLI_UW || Opcode == SRLI || Opcode == SRAI;
+ const bool Shift32 = Opcode == SLLIW || Opcode == SRLIW || Opcode == SRAIW;
+ if (Shift64)
+ addImmediate(Result, (Encoding >> 20U) & 0x3fU);
+ else if (Shift32)
+ addImmediate(Result, (Encoding >> 20U) & 0x1fU);
+ else
+ addImmediate(Result, signExtend(Encoding >> 20U, 12));
+ return Result;
+ }
+
+ if (Category == rcc::riscv::EncodingCategory::S) {
+ if (Format != RS2_OFFSET_RS1 && Format != FRS2_OFFSET_RS1)
+ return fixtureError("unsupported S-type reference fixture format");
+ addRegister(Result, Descriptor, Source2Encoding);
+ addRegister(Result, Descriptor, Source1Encoding);
+ const std::uint32_t Immediate = ((Encoding >> 25U) & 0x7fU) << 5U | ((Encoding >> 7U) & 0x1fU);
+ addImmediate(Result, signExtend(Immediate, 12));
+ return Result;
+ }
+
+ if (Category == rcc::riscv::EncodingCategory::B) {
+ if (Format != RS1_RS2_TARGET)
+ return fixtureError("unsupported B-type reference fixture format");
+ addRegister(Result, Descriptor, Source1Encoding);
+ addRegister(Result, Descriptor, Source2Encoding);
+ addImmediate(Result, decodeBranchImmediate(Encoding));
+ return Result;
+ }
+
+ if (Category == rcc::riscv::EncodingCategory::U) {
+ if (Format != RD_IMM)
+ return fixtureError("unsupported U-type reference fixture format");
+ addRegister(Result, Descriptor, DestinationEncoding);
+ addImmediate(Result, signExtend(Encoding >> 12U, 20));
+ return Result;
+ }
+
+ if (Category == rcc::riscv::EncodingCategory::J) {
+ if (Format != RD_TARGET)
+ return fixtureError("unsupported J-type reference fixture format");
+ addRegister(Result, Descriptor, DestinationEncoding);
+ addImmediate(Result, decodeJALImmediate(Encoding));
+ return Result;
+ }
+
+ if (Category == rcc::riscv::EncodingCategory::SYSTEM) {
+ if (Format == NONE)
+ return Result;
+ if (Format != RD_CSR_RS1 && Format != RD_CSR_IMM)
+ return fixtureError("unsupported system reference fixture format");
+ addRegister(Result, Descriptor, DestinationEncoding);
+ addImmediate(Result, Encoding >> 20U);
+ if (Format == RD_CSR_RS1)
+ addRegister(Result, Descriptor, Source1Encoding);
+ else
+ addImmediate(Result, Source1Encoding);
+ return Result;
+ }
+
+ if (Category == rcc::riscv::EncodingCategory::AMO) {
+ if (Format != RD_PAREN_RS1 && Format != RD_RS2_PAREN_RS1)
+ return fixtureError("unsupported atomic reference fixture format");
+ addRegister(Result, Descriptor, DestinationEncoding);
+ if (Format == RD_RS2_PAREN_RS1)
+ addRegister(Result, Descriptor, Source2Encoding);
+ addRegister(Result, Descriptor, Source1Encoding);
+ return Result;
+ }
+
+ if (Category == rcc::riscv::EncodingCategory::R4) {
+ if (Format != FRD_FRS1_FRS2_FRS3_RM)
+ return fixtureError("unsupported R4 reference fixture format");
+ addRegister(Result, Descriptor, DestinationEncoding);
+ addRegister(Result, Descriptor, Source1Encoding);
+ addRegister(Result, Descriptor, Source2Encoding);
+ addRegister(Result, Descriptor, (Encoding >> 27U) & 0x1fU);
+ addImmediate(Result, (Encoding >> 12U) & 7U);
+ return Result;
+ }
+
+ return fixtureError("unsupported standard reference fixture category");
+}
+
+[[nodiscard]] std::int64_t decodeRVCJumpImmediate(std::uint16_t Encoding) {
+ const std::uint32_t Value = ((Encoding >> 12U) & 1U) << 11U | ((Encoding >> 11U) & 1U) << 4U |
+ ((Encoding >> 9U) & 3U) << 8U | ((Encoding >> 8U) & 1U) << 10U |
+ ((Encoding >> 7U) & 1U) << 6U | ((Encoding >> 6U) & 1U) << 7U |
+ ((Encoding >> 3U) & 7U) << 1U | ((Encoding >> 2U) & 1U) << 5U;
+ return signExtend(Value, 12);
+}
+
+[[nodiscard]] std::int64_t decodeRVCBranchImmediate(std::uint16_t Encoding) {
+ const std::uint32_t Value = ((Encoding >> 12U) & 1U) << 8U | ((Encoding >> 10U) & 3U) << 3U |
+ ((Encoding >> 5U) & 3U) << 6U | ((Encoding >> 3U) & 3U) << 1U |
+ ((Encoding >> 2U) & 1U) << 5U;
+ return signExtend(Value, 9);
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCInst>
+createCompressedFixture(const rcc::InstructionDescriptor &Descriptor,
+ const rcc::riscv::AssemblyInstructionInfo &Assembly) {
+ using enum rcc::riscv::InstructionOpcode;
+ const auto Opcode =
+ static_cast<rcc::riscv::InstructionOpcode>(Assembly.InstructionOpcode.value());
+ const auto Encoding = static_cast<std::uint16_t>(Assembly.Encoding);
+ rcc::mc::MCInst Result(rcc::mc::MCOpcode::fromValue(Assembly.InstructionOpcode.value()));
+
+ if (Opcode == C_ADDI4SPN) {
+ addRegister(Result, Descriptor, ((Encoding >> 2U) & 7U) + 8U);
+ const std::uint32_t Immediate = ((Encoding >> 11U) & 3U) << 4U |
+ ((Encoding >> 7U) & 0xfU) << 6U |
+ ((Encoding >> 6U) & 1U) << 2U | ((Encoding >> 5U) & 1U) << 3U;
+ addImmediate(Result, Immediate);
+ return Result;
+ }
+
+ const bool DoubleLoad = Opcode == C_FLD || Opcode == C_LD;
+ const bool WordLoad = Opcode == C_LW;
+ const bool DoubleStore = Opcode == C_FSD || Opcode == C_SD;
+ const bool WordStore = Opcode == C_SW;
+ if (DoubleLoad || WordLoad || DoubleStore || WordStore) {
+ addRegister(Result, Descriptor, ((Encoding >> 2U) & 7U) + 8U);
+ addRegister(Result, Descriptor, ((Encoding >> 7U) & 7U) + 8U);
+ const bool Double = DoubleLoad || DoubleStore;
+ std::uint32_t Immediate = ((Encoding >> 10U) & 7U) << 3U;
+ if (Double) {
+ Immediate |= ((Encoding >> 5U) & 3U) << 6U;
+ } else {
+ Immediate |= ((Encoding >> 6U) & 1U) << 2U | ((Encoding >> 5U) & 1U) << 6U;
+ }
+ addImmediate(Result, Immediate);
+ return Result;
+ }
+
+ const bool CI = Opcode == C_ADDI || Opcode == C_ADDIW || Opcode == C_LI || Opcode == C_LUI ||
+ Opcode == C_SLLI;
+ if (CI) {
+ addRegister(Result, Descriptor, (Encoding >> 7U) & 0x1fU);
+ const std::uint32_t Immediate = ((Encoding >> 12U) & 1U) << 5U | ((Encoding >> 2U) & 0x1fU);
+ addImmediate(Result, Opcode == C_SLLI ? Immediate : signExtend(Immediate, 6));
+ return Result;
+ }
+
+ if (Opcode == C_ADDI16SP) {
+ const std::uint32_t Immediate = ((Encoding >> 12U) & 1U) << 9U | ((Encoding >> 6U) & 1U) << 4U |
+ ((Encoding >> 5U) & 1U) << 6U | ((Encoding >> 3U) & 3U) << 7U |
+ ((Encoding >> 2U) & 1U) << 5U;
+ addImmediate(Result, signExtend(Immediate, 10));
+ return Result;
+ }
+
+ const bool CompactShift = Opcode == C_SRLI || Opcode == C_SRAI;
+ if (CompactShift || Opcode == C_ANDI) {
+ addRegister(Result, Descriptor, ((Encoding >> 7U) & 7U) + 8U);
+ const std::uint32_t Immediate = ((Encoding >> 12U) & 1U) << 5U | ((Encoding >> 2U) & 0x1fU);
+ addImmediate(Result, CompactShift ? Immediate : signExtend(Immediate, 6));
+ return Result;
+ }
+
+ const bool CompactALU = Opcode == C_SUB || Opcode == C_XOR || Opcode == C_OR || Opcode == C_AND ||
+ Opcode == C_SUBW || Opcode == C_ADDW;
+ if (CompactALU) {
+ addRegister(Result, Descriptor, ((Encoding >> 7U) & 7U) + 8U);
+ addRegister(Result, Descriptor, ((Encoding >> 2U) & 7U) + 8U);
+ return Result;
+ }
+
+ if (Opcode == C_J) {
+ addImmediate(Result, decodeRVCJumpImmediate(Encoding));
+ return Result;
+ }
+
+ if (Opcode == C_BEQZ || Opcode == C_BNEZ) {
+ addRegister(Result, Descriptor, ((Encoding >> 7U) & 7U) + 8U);
+ addImmediate(Result, decodeRVCBranchImmediate(Encoding));
+ return Result;
+ }
+
+ const bool DoubleStackLoad = Opcode == C_FLDSP || Opcode == C_LDSP;
+ if (DoubleStackLoad || Opcode == C_LWSP) {
+ addRegister(Result, Descriptor, (Encoding >> 7U) & 0x1fU);
+ const std::uint32_t Immediate =
+ DoubleStackLoad ? ((Encoding >> 12U) & 1U) << 5U | ((Encoding >> 5U) & 3U) << 3U |
+ ((Encoding >> 2U) & 7U) << 6U
+ : ((Encoding >> 12U) & 1U) << 5U | ((Encoding >> 4U) & 7U) << 2U |
+ ((Encoding >> 2U) & 3U) << 6U;
+ addImmediate(Result, Immediate);
+ return Result;
+ }
+
+ const bool CRSingle = Opcode == C_JR || Opcode == C_JALR;
+ const bool CRDouble = Opcode == C_MV || Opcode == C_ADD;
+ if (CRSingle || CRDouble) {
+ addRegister(Result, Descriptor, (Encoding >> 7U) & 0x1fU);
+ if (CRDouble)
+ addRegister(Result, Descriptor, (Encoding >> 2U) & 0x1fU);
+ return Result;
+ }
+
+ if (Opcode == C_EBREAK)
+ return Result;
+
+ const bool DoubleStackStore = Opcode == C_FSDSP || Opcode == C_SDSP;
+ if (DoubleStackStore || Opcode == C_SWSP) {
+ addRegister(Result, Descriptor, (Encoding >> 2U) & 0x1fU);
+ const std::uint32_t Immediate =
+ DoubleStackStore ? ((Encoding >> 10U) & 7U) << 3U | ((Encoding >> 7U) & 7U) << 6U
+ : ((Encoding >> 9U) & 0xfU) << 2U | ((Encoding >> 7U) & 3U) << 6U;
+ addImmediate(Result, Immediate);
+ return Result;
+ }
+
+ return fixtureError("unsupported compressed reference fixture opcode");
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCInst>
+createReferenceFixture(const rcc::InstructionDescriptor &Descriptor,
+ const rcc::riscv::AssemblyInstructionInfo &Assembly) {
+ const auto Category =
+ static_cast<rcc::riscv::EncodingCategory>(Descriptor.EncodingCategory.value());
+ if (Category == rcc::riscv::EncodingCategory::C)
+ return createCompressedFixture(Descriptor, Assembly);
+ return createStandardFixture(Descriptor, Assembly);
+}
+
+[[nodiscard]] std::vector<std::uint8_t> referenceBytes(std::uint32_t Encoding, std::uint8_t Size) {
+ std::vector<std::uint8_t> Result;
+ Result.reserve(Size);
+ for (std::uint8_t Index = 0; Index < Size; ++Index)
+ Result.push_back(static_cast<std::uint8_t>(Encoding >> (Index * 8U)));
+ return Result;
+}
+
+} // namespace
+
+int main() {
+ auto Target = rcc::riscv::createTargetDescription();
+ if (!Target) {
+ std::cerr << Target.error().message() << '\n';
+ return 1;
+ }
+ const std::span<const rcc::riscv::AssemblyInstructionInfo> Instructions =
+ rcc::riscv::assemblyInstructions();
+ if (Instructions.size() != 211) {
+ std::cerr << "RISC-V encoding coverage does not contain all 211 instructions\n";
+ return 1;
+ }
+
+ rcc::riscv::RISCVMCCodeEmitter Emitter(*Target);
+ for (const rcc::riscv::AssemblyInstructionInfo &Assembly : Instructions) {
+ const rcc::InstructionDescriptor *Descriptor =
+ Target->instructionDescriptor(Assembly.InstructionOpcode);
+ if (Descriptor == nullptr || Assembly.Encoding == 0) {
+ std::cerr << Assembly.Mnemonic << ": missing encoding description\n";
+ return 1;
+ }
+ auto Fixture = createReferenceFixture(*Descriptor, Assembly);
+ if (!Fixture) {
+ std::cerr << Assembly.Mnemonic << ": " << Fixture.error().message() << '\n';
+ return 1;
+ }
+ auto Encoded = Emitter.encodeInstruction(*Fixture);
+ if (!Encoded) {
+ std::cerr << Assembly.Mnemonic << ": " << Encoded.error().message() << '\n';
+ return 1;
+ }
+ const auto Category =
+ static_cast<rcc::riscv::EncodingCategory>(Descriptor->EncodingCategory.value());
+ const std::uint8_t Size = Category == rcc::riscv::EncodingCategory::C ? 2 : 4;
+ if (!Encoded->Fixups.empty() || Encoded->Bytes != referenceBytes(Assembly.Encoding, Size)) {
+ std::uint32_t Actual = 0;
+ for (std::size_t Index = 0; Index < Encoded->Bytes.size(); ++Index)
+ Actual |= static_cast<std::uint32_t>(Encoded->Bytes[Index]) << (Index * 8U);
+ std::cerr << Assembly.Mnemonic << ": encoding differs from its RV64GC reference word ("
+ << Actual << " != " << Assembly.Encoding << ")\n";
+ return 1;
+ }
+ }
+ return 0;
+}
diff --git a/backend/unittests/Target/RISCVMCTest.cpp b/backend/unittests/Target/RISCVMCTest.cpp
new file mode 100644
index 0000000..f43c574
--- /dev/null
+++ b/backend/unittests/Target/RISCVMCTest.cpp
@@ -0,0 +1,322 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/CodeGen/CompilationUnit.h"
+#include "rcc/CodeGen/GlobalObject.h"
+#include "rcc/MC/MCContext.h"
+#include "rcc/MC/MCExpr.h"
+#include "rcc/MC/MCInst.h"
+#include "rcc/Target/RISCV/RISCVAsmBackend.h"
+#include "rcc/Target/RISCV/RISCVFixupKinds.h"
+#include "rcc/Target/RISCV/RISCVInstPrinter.h"
+#include "rcc/Target/RISCV/RISCVMCCodeEmitter.h"
+#include "rcc/Target/RISCV/RISCVObjectFile.h"
+#include "rcc/Target/RISCV/RISCVTarget.h"
+
+#include <cstdint>
+#include <cstring>
+#include <initializer_list>
+#include <iostream>
+#include <span>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace {
+
+int fail(std::string_view Message) {
+ std::cerr << Message << '\n';
+ return 1;
+}
+
+[[nodiscard]] rcc::mc::MCOperand reg(rcc::riscv::PhysicalRegisterID ID) {
+ return rcc::mc::MCOperand::registerOperand(
+ rcc::mc::MCRegister::fromValue(static_cast<std::uint32_t>(ID)));
+}
+
+[[nodiscard]] rcc::mc::MCOperand imm(std::int64_t Value) {
+ return rcc::mc::MCOperand::immediate(Value);
+}
+
+[[nodiscard]] rcc::mc::MCInst instruction(rcc::riscv::InstructionOpcode Opcode,
+ std::initializer_list<rcc::mc::MCOperand> Operands) {
+ rcc::mc::MCInst Result(rcc::mc::MCOpcode::fromValue(static_cast<std::uint32_t>(Opcode)));
+ Result.reserveOperands(Operands.size());
+ for (const rcc::mc::MCOperand &Operand : Operands)
+ Result.addOperand(Operand);
+ return Result;
+}
+
+[[nodiscard]] bool bytesEqual(std::span<const std::uint8_t> Left,
+ std::initializer_list<std::uint8_t> Right) {
+ return Left.size() == Right.size() &&
+ std::equal(Left.begin(), Left.end(), Right.begin(), Right.end());
+}
+
+[[nodiscard]] bool expectEncoding(const rcc::riscv::RISCVMCCodeEmitter &Emitter,
+ const rcc::mc::MCInst &Instruction,
+ std::initializer_list<std::uint8_t> Expected,
+ std::string_view Name) {
+ auto Encoded = Emitter.encodeInstruction(Instruction);
+ if (!Encoded) {
+ std::cerr << Name << ": " << Encoded.error().message() << '\n';
+ return false;
+ }
+ if (!Encoded->Fixups.empty() || !bytesEqual(Encoded->Bytes, Expected)) {
+ std::cerr << Name << ": encoded bytes differ\n";
+ return false;
+ }
+ return true;
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCExprRef>
+symbolExpression(rcc::mc::MCContext &Context, std::string_view Name, std::uint32_t TargetKind = 0) {
+ auto Symbol = Context.getOrCreateSymbol(std::string(Name), rcc::mc::MCSymbolKind::Generic);
+ if (!Symbol)
+ return std::unexpected(Symbol.error());
+ auto Expression = rcc::mc::MCExpr::symbol(*Symbol);
+ if (!Expression)
+ return std::unexpected(Expression.error());
+ if (TargetKind == 0)
+ return *Expression;
+ return rcc::mc::MCExpr::target(TargetKind, *Expression);
+}
+
+[[nodiscard]] bool testEncodings(const rcc::riscv::RISCVMCCodeEmitter &Emitter) {
+ using enum rcc::riscv::InstructionOpcode;
+ using enum rcc::riscv::PhysicalRegisterID;
+ return expectEncoding(Emitter, instruction(AUIPC, {reg(X11), imm(7)}), {0x97, 0x75, 0x00, 0x00},
+ "AUIPC") &&
+ expectEncoding(Emitter, instruction(ADDI, {reg(X12), reg(X13), imm(-17)}),
+ {0x13, 0x86, 0xf6, 0xfe}, "ADDI") &&
+ expectEncoding(Emitter, instruction(SRAI, {reg(X14), reg(X15), imm(63)}),
+ {0x13, 0xd7, 0xf7, 0x43}, "SRAI") &&
+ expectEncoding(Emitter, instruction(SH1ADD, {reg(X8), reg(X9), reg(X10)}),
+ {0x33, 0xa4, 0xa4, 0x20}, "SH1ADD") &&
+ expectEncoding(Emitter, instruction(SEXT_B, {reg(X8), reg(X9)}), {0x13, 0x94, 0x44, 0x60},
+ "SEXT.B") &&
+ expectEncoding(Emitter, instruction(ZEXT_H_RV64, {reg(X8), reg(X9)}),
+ {0x3b, 0xc4, 0x04, 0x08}, "ZEXT.H") &&
+ expectEncoding(Emitter, instruction(SD, {reg(X16), reg(X17), imm(-32)}),
+ {0x23, 0xb0, 0x08, 0xff}, "SD") &&
+ expectEncoding(Emitter, instruction(BGE, {reg(X18), reg(X19), imm(18)}),
+ {0x63, 0x59, 0x39, 0x01}, "BGE") &&
+ expectEncoding(Emitter, instruction(JAL, {reg(X20), imm(-20)}), {0x6f, 0xfa, 0xdf, 0xfe},
+ "JAL") &&
+ expectEncoding(Emitter, instruction(FENCE, {imm(10), imm(5)}), {0x0f, 0x00, 0x50, 0x0a},
+ "FENCE") &&
+ expectEncoding(Emitter, instruction(CSRRWI, {reg(X21), imm(0x305), imm(17)}),
+ {0xf3, 0xda, 0x58, 0x30}, "CSRRWI") &&
+ expectEncoding(Emitter, instruction(AMOADD_D, {reg(X22), reg(X23), reg(X24)}),
+ {0x2f, 0x3b, 0x7c, 0x01}, "AMOADD.D") &&
+ expectEncoding(Emitter,
+ instruction(FMADD_D, {reg(F1_D), reg(F2_D), reg(F3_D), reg(F4_D), imm(2)}),
+ {0xc3, 0x20, 0x31, 0x22}, "FMADD.D") &&
+ expectEncoding(Emitter, instruction(FADD_S, {reg(F5_F), reg(F6_F), reg(F7_F), imm(3)}),
+ {0xd3, 0x32, 0x73, 0x00}, "FADD.S") &&
+ expectEncoding(Emitter, instruction(C_ADDI4SPN, {reg(X8), imm(48)}), {0x00, 0x18},
+ "C.ADDI4SPN") &&
+ expectEncoding(Emitter, instruction(C_LD, {reg(X9), reg(X8), imm(56)}), {0x04, 0x7c},
+ "C.LD") &&
+ expectEncoding(Emitter, instruction(C_SW, {reg(X10), reg(X11), imm(28)}), {0xc8, 0xcd},
+ "C.SW") &&
+ expectEncoding(Emitter, instruction(C_ADDI, {reg(X12), imm(-9)}), {0x5d, 0x16},
+ "C.ADDI") &&
+ expectEncoding(Emitter, instruction(C_ADDI16SP, {imm(-128)}), {0x19, 0x71},
+ "C.ADDI16SP") &&
+ expectEncoding(Emitter, instruction(C_SRAI, {reg(X13), imm(33)}), {0x85, 0x96},
+ "C.SRAI") &&
+ expectEncoding(Emitter, instruction(C_SUBW, {reg(X14), reg(X15)}), {0x1d, 0x9f},
+ "C.SUBW") &&
+ expectEncoding(Emitter, instruction(C_J, {imm(42)}), {0x2d, 0xa0}, "C.J") &&
+ expectEncoding(Emitter, instruction(C_BNEZ, {reg(X8), imm(-30)}), {0x6d, 0xf0},
+ "C.BNEZ") &&
+ expectEncoding(Emitter, instruction(C_LDSP, {reg(X10), imm(248)}), {0x6e, 0x75},
+ "C.LDSP") &&
+ expectEncoding(Emitter, instruction(C_SWSP, {reg(X11), imm(124)}), {0xae, 0xde},
+ "C.SWSP") &&
+ expectEncoding(Emitter, instruction(C_JALR, {reg(X12)}), {0x02, 0x96}, "C.JALR");
+}
+
+[[nodiscard]] bool testFixups(const rcc::riscv::RISCVMCCodeEmitter &Emitter,
+ const rcc::riscv::RISCVAsmBackend &Backend) {
+ using enum rcc::riscv::InstructionOpcode;
+ using enum rcc::riscv::PhysicalRegisterID;
+ rcc::mc::MCContext Context;
+
+ auto HighExpression =
+ symbolExpression(Context, "destination",
+ static_cast<std::uint32_t>(rcc::riscv::RISCVMCExprKind::PCRelativeHigh));
+ if (!HighExpression)
+ return false;
+ auto High = instruction(AUIPC, {reg(X5), rcc::mc::MCOperand::expression(*HighExpression)});
+ auto HighEncoding = Emitter.encodeInstruction(High);
+ if (!HighEncoding || HighEncoding->Fixups.size() != 1 ||
+ HighEncoding->Fixups[0].Kind !=
+ rcc::riscv::fixupKind(rcc::riscv::RISCVFixupKind::PCRelativeHigh20) ||
+ !HighEncoding->Fixups[0].PCRelative ||
+ !bytesEqual(HighEncoding->Bytes, {0x97, 0x02, 0x00, 0x00})) {
+ return false;
+ }
+ auto HighValue = rcc::mc::evaluateAsRelocatable(**HighExpression);
+ if (!HighValue)
+ return false;
+ auto HighRelocation = Backend.relocationType(HighEncoding->Fixups[0], *HighValue);
+ if (!HighRelocation || *HighRelocation != 23)
+ return false;
+
+ auto LowExpression = symbolExpression(
+ Context, "anchor", static_cast<std::uint32_t>(rcc::riscv::RISCVMCExprKind::PCRelativeLow));
+ if (!LowExpression)
+ return false;
+ auto Low = instruction(JALR, {reg(X1), reg(X5), rcc::mc::MCOperand::expression(*LowExpression)});
+ auto LowEncoding = Emitter.encodeInstruction(Low);
+ if (!LowEncoding || LowEncoding->Fixups.size() != 1 ||
+ LowEncoding->Fixups[0].Kind !=
+ rcc::riscv::fixupKind(rcc::riscv::RISCVFixupKind::PCRelativeLow12I)) {
+ return false;
+ }
+
+ auto AbsoluteHighExpression = symbolExpression(
+ Context, "absolute", static_cast<std::uint32_t>(rcc::riscv::RISCVMCExprKind::AbsoluteHigh));
+ if (!AbsoluteHighExpression)
+ return false;
+ auto AbsoluteHigh =
+ instruction(LUI, {reg(X6), rcc::mc::MCOperand::expression(*AbsoluteHighExpression)});
+ auto AbsoluteHighEncoding = Emitter.encodeInstruction(AbsoluteHigh);
+ if (!AbsoluteHighEncoding || AbsoluteHighEncoding->Fixups.size() != 1 ||
+ AbsoluteHighEncoding->Fixups[0].Kind !=
+ rcc::riscv::fixupKind(rcc::riscv::RISCVFixupKind::High20)) {
+ return false;
+ }
+ auto AbsoluteHighValue = rcc::mc::evaluateAsRelocatable(**AbsoluteHighExpression);
+ auto AbsoluteHighRelocation =
+ AbsoluteHighValue
+ ? Backend.relocationType(AbsoluteHighEncoding->Fixups[0], *AbsoluteHighValue)
+ : rcc::Expected<std::uint32_t>{std::unexpected(AbsoluteHighValue.error())};
+ if (!AbsoluteHighRelocation || *AbsoluteHighRelocation != 26)
+ return false;
+
+ auto AbsoluteLowExpression = symbolExpression(
+ Context, "absolute", static_cast<std::uint32_t>(rcc::riscv::RISCVMCExprKind::AbsoluteLow));
+ if (!AbsoluteLowExpression)
+ return false;
+ auto AbsoluteLow =
+ instruction(ADDI, {reg(X6), reg(X6), rcc::mc::MCOperand::expression(*AbsoluteLowExpression)});
+ auto AbsoluteLowEncoding = Emitter.encodeInstruction(AbsoluteLow);
+ if (!AbsoluteLowEncoding || AbsoluteLowEncoding->Fixups.size() != 1 ||
+ AbsoluteLowEncoding->Fixups[0].Kind !=
+ rcc::riscv::fixupKind(rcc::riscv::RISCVFixupKind::Low12I)) {
+ return false;
+ }
+ auto AbsoluteLowValue = rcc::mc::evaluateAsRelocatable(**AbsoluteLowExpression);
+ auto AbsoluteLowRelocation =
+ AbsoluteLowValue ? Backend.relocationType(AbsoluteLowEncoding->Fixups[0], *AbsoluteLowValue)
+ : rcc::Expected<std::uint32_t>{std::unexpected(AbsoluteLowValue.error())};
+ if (!AbsoluteLowRelocation || *AbsoluteLowRelocation != 27)
+ return false;
+
+ auto BranchExpression = symbolExpression(Context, "block");
+ if (!BranchExpression)
+ return false;
+ auto Branch =
+ instruction(BEQ, {reg(X10), reg(X11), rcc::mc::MCOperand::expression(*BranchExpression)});
+ auto BranchEncoding = Emitter.encodeInstruction(Branch);
+ if (!BranchEncoding || BranchEncoding->Fixups.size() != 1 ||
+ BranchEncoding->Fixups[0].Kind != rcc::riscv::fixupKind(rcc::riscv::RISCVFixupKind::Branch)) {
+ return false;
+ }
+ auto Applied = Backend.applyFixup(BranchEncoding->Fixups[0].Kind, BranchEncoding->Bytes, 0, 18);
+ if (!Applied || !bytesEqual(BranchEncoding->Bytes, {0x63, 0x09, 0xb5, 0x00}))
+ return false;
+
+ std::vector<std::uint8_t> Word(4);
+ auto OutOfRange =
+ Backend.applyFixup(rcc::riscv::fixupKind(rcc::riscv::RISCVFixupKind::Branch), Word, 0, 4096);
+ return !OutOfRange && OutOfRange.error().code() == rcc::ErrorCode::FixupApplicationFailed;
+}
+
+[[nodiscard]] bool testFailures(const rcc::riscv::RISCVMCCodeEmitter &Emitter) {
+ using enum rcc::riscv::InstructionOpcode;
+ using enum rcc::riscv::PhysicalRegisterID;
+ auto BadShift = Emitter.encodeInstruction(instruction(SLLI, {reg(X1), reg(X2), imm(64)}));
+ auto BadCompactRegister =
+ Emitter.encodeInstruction(instruction(C_LD, {reg(X1), reg(X8), imm(8)}));
+ auto BadAlignment = Emitter.encodeInstruction(instruction(C_SDSP, {reg(X10), imm(6)}));
+ auto Pseudo = Emitter.encodeInstruction(instruction(PseudoCALL, {}));
+ return !BadShift && BadShift.error().code() == rcc::ErrorCode::InstructionEncodingFailed &&
+ !BadCompactRegister &&
+ BadCompactRegister.error().code() == rcc::ErrorCode::InstructionEncodingFailed &&
+ !BadAlignment &&
+ BadAlignment.error().code() == rcc::ErrorCode::InstructionEncodingFailed && !Pseudo &&
+ Pseudo.error().code() == rcc::ErrorCode::InstructionEncodingFailed;
+}
+
+[[nodiscard]] bool containsBytes(std::span<const std::uint8_t> Bytes, std::string_view Text) {
+ if (Text.size() > Bytes.size())
+ return false;
+ const auto *Begin = reinterpret_cast<const char *>(Bytes.data());
+ return std::string_view(Begin, Bytes.size()).find(Text) != std::string_view::npos;
+}
+
+[[nodiscard]] bool testObjectEmission(const rcc::TargetDescription &Target) {
+ auto Alignment = rcc::Alignment::create(8);
+ auto Value = rcc::APInt::fromUnsigned(64, 0x1122334455667788ULL);
+ if (!Alignment || !Value)
+ return false;
+ rcc::GlobalObjectDesc Global;
+ Global.Symbol = "object_value";
+ Global.ObjectType = rcc::GenericConstantType::byte();
+ Global.Align = *Alignment;
+ Global.Size = 8;
+ Global.Initializer.emplace(rcc::GlobalInitializer::integer(std::move(*Value)));
+ rcc::CompilationUnit Unit;
+ if (!Unit.addGlobalObject(std::move(Global)))
+ return false;
+ auto Object = rcc::riscv::emitObject(Unit, Target);
+ if (!Object || Object->size() < 64 || (*Object)[0] != 0x7f || (*Object)[1] != 'E' ||
+ (*Object)[2] != 'L' || (*Object)[3] != 'F') {
+ return false;
+ }
+ const std::uint16_t Machine =
+ static_cast<std::uint16_t>((*Object)[18]) | static_cast<std::uint16_t>((*Object)[19] << 8U);
+ const std::uint32_t Flags = static_cast<std::uint32_t>((*Object)[48]) |
+ static_cast<std::uint32_t>((*Object)[49]) << 8U |
+ static_cast<std::uint32_t>((*Object)[50]) << 16U |
+ static_cast<std::uint32_t>((*Object)[51]) << 24U;
+ return Machine == 243 && Flags == 5 && containsBytes(*Object, ".data") &&
+ containsBytes(*Object, ".riscv.attributes") && containsBytes(*Object, "object_value") &&
+ containsBytes(*Object, std::string_view("\x88\x77\x66\x55\x44\x33\x22\x11", 8));
+}
+
+} // namespace
+
+int main() {
+ auto Target = rcc::riscv::createTargetDescription();
+ if (!Target)
+ return fail(Target.error().message());
+ rcc::riscv::RISCVMCCodeEmitter Emitter(*Target);
+ rcc::riscv::RISCVAsmBackend Backend;
+ rcc::riscv::RISCVInstPrinter Printer(*Target);
+ auto PrintedHigh = Printer.printTargetExpression(
+ static_cast<std::uint32_t>(rcc::riscv::RISCVMCExprKind::AbsoluteHigh), "symbol");
+ auto PrintedLow = Printer.printTargetExpression(
+ static_cast<std::uint32_t>(rcc::riscv::RISCVMCExprKind::AbsoluteLow), "symbol");
+ if (!PrintedHigh || *PrintedHigh != "%hi(symbol)" || !PrintedLow || *PrintedLow != "%lo(symbol)")
+ return fail("RISC-V absolute address modifiers do not print canonically");
+ auto PrintedUnary = Printer.printInstruction(instruction(
+ rcc::riscv::InstructionOpcode::SEXT_B,
+ {reg(rcc::riscv::PhysicalRegisterID::X8), reg(rcc::riscv::PhysicalRegisterID::X9)}));
+ if (!PrintedUnary || *PrintedUnary != "sext.b x8, x9")
+ return fail("RISC-V unary integer instructions do not print canonically");
+ if (!testEncodings(Emitter))
+ return fail("RISC-V MC encodings differ from the RV64GC reference encodings");
+ if (!testFixups(Emitter, Backend))
+ return fail("RISC-V MC fixup or relocation behavior is incorrect");
+ if (!testFailures(Emitter))
+ return fail("RISC-V MC encoding failures are not diagnosed");
+ if (!testObjectEmission(*Target))
+ return fail("RISC-V ELF object emission is incomplete");
+ if (Backend.objectFlags() != 5)
+ return fail("RISC-V ELF flags do not describe RV64GC LP64D");
+ return 0;
+}
diff --git a/backend/unittests/Target/RISCVObjectFileTest.cpp b/backend/unittests/Target/RISCVObjectFileTest.cpp
new file mode 100644
index 0000000..95ed5c8
--- /dev/null
+++ b/backend/unittests/Target/RISCVObjectFileTest.cpp
@@ -0,0 +1,791 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/Target/RISCV/RISCVObjectFile.h"
+
+#include "rcc/CodeGen/CompilationUnit.h"
+#include "rcc/CodeGen/GlobalObject.h"
+#include "rcc/MC/ELFObjectWriter.h"
+#include "rcc/MC/MCAssembler.h"
+#include "rcc/MC/MCContext.h"
+#include "rcc/MC/MCDwarfFrameEmitter.h"
+#include "rcc/MC/MCExpr.h"
+#include "rcc/MC/MCObjectStreamer.h"
+#include "rcc/Target/RISCV/RISCVAsmBackend.h"
+#include "rcc/Target/RISCV/RISCVMCCodeEmitter.h"
+#include "rcc/Target/RISCV/RISCVTarget.h"
+#include "rcc/Target/RISCV/RISCVTargetConstant.h"
+
+#include <algorithm>
+#include <array>
+#include <bit>
+#include <cstddef>
+#include <cstdint>
+#include <iostream>
+#include <optional>
+#include <span>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace {
+
+constexpr std::uint32_t SHTRela = 4;
+constexpr std::uint32_t SHTNoBits = 8;
+constexpr std::uint32_t SHTGroup = 17;
+constexpr std::uint16_t SHNUndefined = 0;
+constexpr std::uint16_t SHNCommon = 0xfff2U;
+constexpr std::uint64_t SHFWrite = 0x1;
+constexpr std::uint64_t SHFAlloc = 0x2;
+constexpr std::uint64_t SHFGroup = 0x200;
+constexpr std::uint64_t SHFTLS = 0x400;
+
+class TestContext final {
+public:
+ void expect(bool Condition, std::string_view Message) {
+ if (Condition)
+ return;
+ std::cerr << Message << '\n';
+ ++Failures;
+ }
+
+ [[nodiscard]] int result() const noexcept {
+ return Failures == 0 ? 0 : 1;
+ }
+
+private:
+ int Failures = 0;
+};
+
+[[nodiscard]] std::optional<std::uint16_t> readU16(std::span<const std::uint8_t> Bytes,
+ std::uint64_t Offset) {
+ if (Offset > Bytes.size() || 2U > Bytes.size() - static_cast<std::size_t>(Offset))
+ return std::nullopt;
+ const std::size_t Position = static_cast<std::size_t>(Offset);
+ return static_cast<std::uint16_t>(Bytes[Position]) |
+ static_cast<std::uint16_t>(Bytes[Position + 1U]) << 8U;
+}
+
+[[nodiscard]] std::optional<std::uint32_t> readU32(std::span<const std::uint8_t> Bytes,
+ std::uint64_t Offset) {
+ if (Offset > Bytes.size() || 4U > Bytes.size() - static_cast<std::size_t>(Offset))
+ return std::nullopt;
+ std::uint32_t Result = 0;
+ for (std::uint32_t Index = 0; Index < 4; ++Index)
+ Result |= static_cast<std::uint32_t>(Bytes[static_cast<std::size_t>(Offset) + Index])
+ << (Index * 8U);
+ return Result;
+}
+
+[[nodiscard]] std::optional<std::uint64_t> readU64(std::span<const std::uint8_t> Bytes,
+ std::uint64_t Offset) {
+ if (Offset > Bytes.size() || 8U > Bytes.size() - static_cast<std::size_t>(Offset))
+ return std::nullopt;
+ std::uint64_t Result = 0;
+ for (std::uint32_t Index = 0; Index < 8; ++Index)
+ Result |= static_cast<std::uint64_t>(Bytes[static_cast<std::size_t>(Offset) + Index])
+ << (Index * 8U);
+ return Result;
+}
+
+struct ELFSection final {
+ std::string Name;
+ std::uint32_t Type = 0;
+ std::uint64_t Flags = 0;
+ std::uint64_t Offset = 0;
+ std::uint64_t Size = 0;
+ std::uint32_t Link = 0;
+ std::uint32_t Info = 0;
+ std::uint64_t Alignment = 0;
+ std::uint64_t EntrySize = 0;
+};
+
+struct ELFSymbol final {
+ std::string Name;
+ std::uint8_t Binding = 0;
+ std::uint8_t Type = 0;
+ std::uint8_t Visibility = 0;
+ std::uint16_t Section = 0;
+ std::uint64_t Value = 0;
+ std::uint64_t Size = 0;
+};
+
+struct ELFRelocation final {
+ std::uint64_t Offset = 0;
+ std::uint32_t Symbol = 0;
+ std::uint32_t Type = 0;
+ std::int64_t Addend = 0;
+};
+
+class ELFView final {
+public:
+ [[nodiscard]] static std::optional<ELFView> create(std::span<const std::uint8_t> Bytes) {
+ if (Bytes.size() < 64 || Bytes[0] != 0x7f || Bytes[1] != 'E' || Bytes[2] != 'L' ||
+ Bytes[3] != 'F' || Bytes[4] != 2 || Bytes[5] != 1) {
+ return std::nullopt;
+ }
+ const auto HeaderOffset = readU64(Bytes, 40);
+ const auto EntrySize = readU16(Bytes, 58);
+ const auto Count = readU16(Bytes, 60);
+ const auto NamesIndex = readU16(Bytes, 62);
+ if (!HeaderOffset || !EntrySize || !Count || !NamesIndex || *EntrySize != 64 || *Count == 0 ||
+ *NamesIndex >= *Count) {
+ return std::nullopt;
+ }
+ if (*HeaderOffset > Bytes.size() ||
+ static_cast<std::uint64_t>(*Count) * *EntrySize > Bytes.size() - *HeaderOffset) {
+ return std::nullopt;
+ }
+
+ const std::uint64_t NamesHeader = *HeaderOffset + static_cast<std::uint64_t>(*NamesIndex) * 64U;
+ const auto NamesOffset = readU64(Bytes, NamesHeader + 24U);
+ const auto NamesSize = readU64(Bytes, NamesHeader + 32U);
+ if (!NamesOffset || !NamesSize || *NamesOffset > Bytes.size() ||
+ *NamesSize > Bytes.size() - *NamesOffset) {
+ return std::nullopt;
+ }
+ const std::span<const std::uint8_t> Names =
+ Bytes.subspan(static_cast<std::size_t>(*NamesOffset), static_cast<std::size_t>(*NamesSize));
+
+ ELFView Result(Bytes);
+ Result.Sections.reserve(*Count);
+ for (std::uint16_t Index = 0; Index < *Count; ++Index) {
+ const std::uint64_t Position = *HeaderOffset + static_cast<std::uint64_t>(Index) * 64U;
+ const auto NameOffset = readU32(Bytes, Position);
+ const auto Type = readU32(Bytes, Position + 4U);
+ const auto Flags = readU64(Bytes, Position + 8U);
+ const auto Offset = readU64(Bytes, Position + 24U);
+ const auto Size = readU64(Bytes, Position + 32U);
+ const auto Link = readU32(Bytes, Position + 40U);
+ const auto Info = readU32(Bytes, Position + 44U);
+ const auto Alignment = readU64(Bytes, Position + 48U);
+ const auto SectionEntrySize = readU64(Bytes, Position + 56U);
+ if (!NameOffset || !Type || !Flags || !Offset || !Size || !Link || !Info || !Alignment ||
+ !SectionEntrySize || *NameOffset >= Names.size()) {
+ return std::nullopt;
+ }
+ std::size_t End = *NameOffset;
+ while (End < Names.size() && Names[End] != 0)
+ ++End;
+ if (End == Names.size())
+ return std::nullopt;
+ Result.Sections.push_back(ELFSection{
+ .Name = std::string(reinterpret_cast<const char *>(Names.data() + *NameOffset),
+ End - *NameOffset),
+ .Type = *Type,
+ .Flags = *Flags,
+ .Offset = *Offset,
+ .Size = *Size,
+ .Link = *Link,
+ .Info = *Info,
+ .Alignment = *Alignment,
+ .EntrySize = *SectionEntrySize,
+ });
+ }
+ return Result;
+ }
+
+ [[nodiscard]] const ELFSection *section(std::string_view Name) const noexcept {
+ const auto Position =
+ std::ranges::find(Sections, Name, [](const ELFSection &Value) { return Value.Name; });
+ return Position == Sections.end() ? nullptr : &*Position;
+ }
+
+ [[nodiscard]] std::optional<std::size_t> sectionIndex(std::string_view Name) const noexcept {
+ for (std::size_t Index = 0; Index < Sections.size(); ++Index) {
+ if (Sections[Index].Name == Name)
+ return Index;
+ }
+ return std::nullopt;
+ }
+
+ [[nodiscard]] std::span<const std::uint8_t> contents(const ELFSection &Section) const noexcept {
+ if (Section.Type == SHTNoBits || Section.Offset > Bytes.size() ||
+ Section.Size > Bytes.size() - Section.Offset) {
+ return {};
+ }
+ return Bytes.subspan(static_cast<std::size_t>(Section.Offset),
+ static_cast<std::size_t>(Section.Size));
+ }
+
+ [[nodiscard]] std::optional<std::vector<ELFSymbol>> symbols() const {
+ const ELFSection *Table = section(".symtab");
+ if (Table == nullptr || Table->EntrySize != 24 || Table->Link >= Sections.size())
+ return std::nullopt;
+ const std::span<const std::uint8_t> TableBytes = contents(*Table);
+ const std::span<const std::uint8_t> Names = contents(Sections[Table->Link]);
+ if (TableBytes.size() % 24U != 0 || Names.empty())
+ return std::nullopt;
+ std::vector<ELFSymbol> Result;
+ Result.reserve(TableBytes.size() / 24U);
+ for (std::size_t Offset = 0; Offset < TableBytes.size(); Offset += 24U) {
+ const auto NameOffset = readU32(TableBytes, Offset);
+ const auto Section = readU16(TableBytes, Offset + 6U);
+ const auto Value = readU64(TableBytes, Offset + 8U);
+ const auto Size = readU64(TableBytes, Offset + 16U);
+ if (!NameOffset || !Section || !Value || !Size || *NameOffset >= Names.size())
+ return std::nullopt;
+ std::size_t End = *NameOffset;
+ while (End < Names.size() && Names[End] != 0)
+ ++End;
+ if (End == Names.size())
+ return std::nullopt;
+ Result.push_back(ELFSymbol{
+ .Name = std::string(reinterpret_cast<const char *>(Names.data() + *NameOffset),
+ End - *NameOffset),
+ .Binding = static_cast<std::uint8_t>(TableBytes[Offset + 4U] >> 4U),
+ .Type = static_cast<std::uint8_t>(TableBytes[Offset + 4U] & 0xfU),
+ .Visibility = static_cast<std::uint8_t>(TableBytes[Offset + 5U] & 0x3U),
+ .Section = *Section,
+ .Value = *Value,
+ .Size = *Size,
+ });
+ }
+ return Result;
+ }
+
+ [[nodiscard]] std::optional<std::vector<ELFRelocation>> relocations(std::string_view Name) const {
+ const ELFSection *Table = section(Name);
+ if (Table == nullptr || Table->Type != SHTRela || Table->EntrySize != 24)
+ return std::nullopt;
+ const std::span<const std::uint8_t> TableBytes = contents(*Table);
+ if (TableBytes.size() % 24U != 0)
+ return std::nullopt;
+ std::vector<ELFRelocation> Result;
+ Result.reserve(TableBytes.size() / 24U);
+ for (std::size_t Offset = 0; Offset < TableBytes.size(); Offset += 24U) {
+ const auto RelocationOffset = readU64(TableBytes, Offset);
+ const auto Info = readU64(TableBytes, Offset + 8U);
+ const auto Addend = readU64(TableBytes, Offset + 16U);
+ if (!RelocationOffset || !Info || !Addend)
+ return std::nullopt;
+ Result.push_back(ELFRelocation{
+ .Offset = *RelocationOffset,
+ .Symbol = static_cast<std::uint32_t>(*Info >> 32U),
+ .Type = static_cast<std::uint32_t>(*Info),
+ .Addend = std::bit_cast<std::int64_t>(*Addend),
+ });
+ }
+ return Result;
+ }
+
+ [[nodiscard]] const std::vector<ELFSection> &sections() const noexcept {
+ return Sections;
+ }
+
+private:
+ explicit ELFView(std::span<const std::uint8_t> Bytes) : Bytes(Bytes) {}
+
+ std::span<const std::uint8_t> Bytes;
+ std::vector<ELFSection> Sections;
+};
+
+[[nodiscard]] const ELFSymbol *findSymbol(const std::vector<ELFSymbol> &Symbols,
+ std::string_view Name) {
+ const auto Position =
+ std::ranges::find(Symbols, Name, [](const ELFSymbol &Symbol) { return Symbol.Name; });
+ return Position == Symbols.end() ? nullptr : &*Position;
+}
+
+[[nodiscard]] rcc::Expected<rcc::Alignment> alignment(std::uint64_t Value) {
+ return rcc::Alignment::create(Value);
+}
+
+[[nodiscard]] rcc::Expected<rcc::GlobalInitializer> integer(std::uint32_t Bits,
+ std::uint64_t Value) {
+ auto Immediate = rcc::APInt::fromUnsigned(Bits, Value);
+ if (!Immediate)
+ return std::unexpected(Immediate.error());
+ return rcc::GlobalInitializer::integer(std::move(*Immediate));
+}
+
+[[nodiscard]] bool addObject(rcc::CompilationUnit &Unit, std::string Name, std::uint64_t Size,
+ std::uint64_t Alignment, rcc::GlobalLinkage Linkage,
+ std::optional<rcc::GlobalInitializer> Initializer,
+ const auto &Configure) {
+ auto Align = alignment(Alignment);
+ if (!Align)
+ return false;
+ rcc::GlobalObjectDesc Object;
+ Object.Symbol = std::move(Name);
+ Object.ObjectType = rcc::GenericConstantType::byte();
+ Object.Linkage = Linkage;
+ Object.Align = *Align;
+ Object.Size = Size;
+ Object.Initializer = std::move(Initializer);
+ Configure(Object);
+ return Unit.addGlobalObject(std::move(Object)).has_value();
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCExprRef> symbolDifference(rcc::mc::MCContext &Context,
+ std::string_view Left,
+ std::string_view Right,
+ std::int64_t Addend) {
+ auto LeftSymbol = Context.getOrCreateSymbol(std::string(Left), rcc::mc::MCSymbolKind::Generic);
+ auto RightSymbol = Context.getOrCreateSymbol(std::string(Right), rcc::mc::MCSymbolKind::Generic);
+ if (!LeftSymbol || !RightSymbol)
+ return std::unexpected(!LeftSymbol ? LeftSymbol.error() : RightSymbol.error());
+ auto LeftExpression = rcc::mc::MCExpr::symbol(*LeftSymbol);
+ auto RightExpression = rcc::mc::MCExpr::symbol(*RightSymbol);
+ if (!LeftExpression || !RightExpression)
+ return std::unexpected(!LeftExpression ? LeftExpression.error() : RightExpression.error());
+ auto Difference = rcc::mc::MCExpr::binary(
+ rcc::mc::MCBinaryOperator::Subtract, std::move(*LeftExpression), std::move(*RightExpression));
+ if (!Difference)
+ return std::unexpected(Difference.error());
+ return rcc::mc::MCExpr::binary(rcc::mc::MCBinaryOperator::Add, std::move(*Difference),
+ rcc::mc::MCExpr::constant(Addend));
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCExprRef>
+symbolExpression(rcc::mc::MCContext &Context, std::string_view Name,
+ rcc::mc::MCSymbolKind Kind = rcc::mc::MCSymbolKind::Generic) {
+ auto Symbol = Context.getOrCreateSymbol(std::string(Name), Kind);
+ if (!Symbol)
+ return std::unexpected(Symbol.error());
+ return rcc::mc::MCExpr::symbol(std::move(*Symbol));
+}
+
+[[nodiscard]] rcc::mc::MCInst instruction(rcc::riscv::InstructionOpcode Opcode) {
+ return rcc::mc::MCInst(rcc::mc::MCOpcode::fromValue(static_cast<std::uint32_t>(Opcode)));
+}
+
+void testMCRelaxation(TestContext &Context, const rcc::TargetDescription &Target) {
+ rcc::riscv::RISCVMCCodeEmitter Emitter(Target);
+ rcc::riscv::RISCVAsmBackend Backend;
+ rcc::mc::MCContext MCContext;
+ auto Text = MCContext.getOrCreateSection(rcc::mc::MCSectionDesc{
+ .Name = ".text.relax",
+ .Kind = rcc::mc::MCSectionKind::Text,
+ .Flags = {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Executable},
+ .Alignment = 2,
+ .EntrySize = 0,
+ .Group = {},
+ .UniqueID = std::nullopt,
+ });
+ auto JumpTarget = MCContext.createTemporarySymbol(".Ljump_target");
+ auto BranchTarget = MCContext.createTemporarySymbol(".Lbranch_target");
+ if (!Text || !JumpTarget || !BranchTarget) {
+ Context.expect(false, "RISC-V MC relaxation fixture construction must succeed");
+ return;
+ }
+ auto JumpExpression = rcc::mc::MCExpr::symbol(*JumpTarget);
+ auto BranchExpression = rcc::mc::MCExpr::symbol(*BranchTarget);
+ if (!JumpExpression || !BranchExpression) {
+ Context.expect(false, "RISC-V MC relaxation expressions must be constructible");
+ return;
+ }
+
+ rcc::mc::MCInst Jump = instruction(rcc::riscv::InstructionOpcode::C_J);
+ Jump.addOperand(rcc::mc::MCOperand::expression(*JumpExpression));
+ rcc::mc::MCInst Branch = instruction(rcc::riscv::InstructionOpcode::C_BEQZ);
+ Branch.addOperand(rcc::mc::MCOperand::registerOperand(rcc::mc::MCRegister::fromValue(
+ static_cast<std::uint32_t>(rcc::riscv::PhysicalRegisterID::X8))));
+ Branch.addOperand(rcc::mc::MCOperand::expression(*BranchExpression));
+
+ rcc::mc::MCAssembler Assembler(Emitter, Backend);
+ if (!Assembler.switchSection(*Text) || !Assembler.emitInstruction(Jump) ||
+ !Assembler.emitZeroFill(3000) || !Assembler.emitLabel(*JumpTarget) ||
+ !Assembler.emitInstruction(Branch) || !Assembler.emitZeroFill(600) ||
+ !Assembler.emitLabel(*BranchTarget)) {
+ Context.expect(false, "RISC-V MC relaxation events must succeed");
+ return;
+ }
+ auto Object = Assembler.assemble();
+ if (!Object)
+ std::cerr << "RISC-V MC relaxation failure: " << Object.error().message() << '\n';
+ Context.expect(Object.has_value(), "RISC-V MC relaxation must reach a fixed point");
+ if (!Object || Object->Sections.size() != 1)
+ return;
+ const auto JumpWord = readU32(Object->Sections[0].Contents, 0);
+ const auto BranchWord = readU32(Object->Sections[0].Contents, 3004);
+ Context.expect(JumpWord && (*JumpWord & 0xfffU) == 0x6fU,
+ "out-of-range C.J must monotonically relax to JAL x0");
+ Context.expect(BranchWord && (*BranchWord & 0x01fff07fU) == 0x00040063U,
+ "out-of-range C.BEQZ must monotonically relax to BEQ x8, x0");
+ Context.expect(Object->Sections[0].AddressSize == 3608 && Object->Sections[0].Relocations.empty(),
+ "resolved MC relaxation must update layout without linker relocations");
+
+ auto ExternalExpression = symbolExpression(MCContext, "external_jump");
+ if (!ExternalExpression) {
+ Context.expect(false, "external MC relaxation expression must be constructible");
+ return;
+ }
+ rcc::mc::MCInst ExternalJump = instruction(rcc::riscv::InstructionOpcode::C_J);
+ ExternalJump.addOperand(rcc::mc::MCOperand::expression(*ExternalExpression));
+ rcc::mc::MCAssembler Relocatable(Emitter, Backend);
+ auto Switched = Relocatable.switchSection(*Text);
+ auto Emitted = Relocatable.emitInstruction(ExternalJump);
+ auto ExternalObject = Relocatable.assemble();
+ Context.expect(
+ Switched && Emitted && ExternalObject &&
+ ExternalObject->Sections[0].Relocations.size() == 1 &&
+ ExternalObject->Sections[0].Relocations[0].Type == 45,
+ "unresolved C.J must retain one mandatory relocation without implicit linker relax");
+}
+
+void testAssemblerLayout(TestContext &Context, const rcc::TargetDescription &Target) {
+ rcc::riscv::RISCVMCCodeEmitter Emitter(Target);
+ rcc::riscv::RISCVAsmBackend Backend;
+ rcc::mc::MCContext MCContext;
+ auto Data = MCContext.getOrCreateSection(rcc::mc::MCSectionDesc{
+ .Name = ".data",
+ .Kind = rcc::mc::MCSectionKind::Data,
+ .Flags = {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Writable},
+ .Alignment = 1,
+ .EntrySize = 0,
+ .Group = {},
+ .UniqueID = std::nullopt,
+ });
+ auto BSS = MCContext.getOrCreateSection(rcc::mc::MCSectionDesc{
+ .Name = ".bss",
+ .Kind = rcc::mc::MCSectionKind::BSS,
+ .Flags = {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Writable},
+ .Alignment = 1,
+ .EntrySize = 0,
+ .Group = {},
+ .UniqueID = std::nullopt,
+ });
+ auto Difference = symbolDifference(MCContext, "left", "right", 5);
+ if (!Data || !BSS || !Difference) {
+ Context.expect(false, "MC assembler layout fixture construction must succeed");
+ return;
+ }
+
+ rcc::mc::MCAssembler Assembler(Emitter, Backend);
+ constexpr std::uint64_t LargeBSS = (std::uint64_t{1} << 32U) + 32U;
+ if (!Assembler.switchSection(*Data) || !Assembler.emitAlignment(128, 0, 0) ||
+ !Assembler.emitValue(*Difference, 8) || !Assembler.switchSection(*BSS) ||
+ !Assembler.emitAlignment(4096, 0, 0) || !Assembler.emitZeroFill(LargeBSS)) {
+ Context.expect(false, "MC assembler layout events must succeed");
+ return;
+ }
+ auto Object = Assembler.assemble();
+ if (!Object)
+ std::cerr << "MC assembler layout failure: " << Object.error().message() << '\n';
+ Context.expect(Object.has_value(), "MC assembler layout must complete");
+ if (!Object || Object->Sections.size() != 2)
+ return;
+ Context.expect(Object->Sections[0].Alignment == 128 &&
+ Object->Sections[0].Relocations.size() == 2 &&
+ Object->Sections[0].Relocations[0].Type == 36 &&
+ Object->Sections[0].Relocations[0].Addend == 5 &&
+ Object->Sections[0].Relocations[1].Type == 40 &&
+ Object->Sections[0].Relocations[1].Addend == 0,
+ "MC assembler must preserve effective alignment and paired symbol relocations");
+ Context.expect(Object->Sections[1].Alignment == 4096 &&
+ Object->Sections[1].AddressSize == LargeBSS &&
+ Object->Sections[1].Contents.empty(),
+ "MC assembler must keep NOBITS address size without materializing zero bytes");
+
+ rcc::mc::MCAssembler BadAssembler(Emitter, Backend);
+ const std::array<std::uint8_t, 1> Nonzero{1};
+ auto Switched = BadAssembler.switchSection(*BSS);
+ auto Emitted = BadAssembler.emitBytes(Nonzero);
+ auto Failure = BadAssembler.assemble();
+ Context.expect(Switched && Emitted && !Failure &&
+ Failure.error().code() == rcc::ErrorCode::ObjectWritingFailed,
+ "MC assembler must reject initialized bytes in a NOBITS section");
+}
+
+void testNativeObjectBoundaries(TestContext &Context, const rcc::TargetDescription &Target) {
+ rcc::riscv::RISCVMCCodeEmitter Emitter(Target);
+ rcc::riscv::RISCVAsmBackend Backend;
+ rcc::mc::ELF64LEObjectWriter Writer(rcc::mc::ELFObjectTargetInfo{
+ .Machine = 243, .OSABI = 0, .ABIVersion = 0, .Flags = 0, .ExtraSections = {}});
+ rcc::mc::ELFObjectStreamer Streamer(Emitter, Backend, Writer);
+ auto EmptyAssembly = Streamer.emitInlineAssembly({});
+ auto RawAssembly = Streamer.emitInlineAssembly("addi a0, a0, 1");
+ Context.expect(EmptyAssembly && !RawAssembly &&
+ RawAssembly.error().code() == rcc::ErrorCode::Unsupported,
+ "native object emission must reject unparsed inline assembly without dropping it");
+
+ rcc::mc::MCContext MCContext;
+ auto EHFrame = MCContext.getOrCreateSection(rcc::mc::MCSectionDesc{
+ .Name = ".eh_frame.boundary",
+ .Kind = rcc::mc::MCSectionKind::EHFrame,
+ .Flags = {rcc::mc::MCSectionFlag::Alloc},
+ .Alignment = 8,
+ .EntrySize = 0,
+ .Group = {},
+ .UniqueID = std::nullopt,
+ });
+ auto Function = MCContext.getOrCreateSymbol("cfi_boundary", rcc::mc::MCSymbolKind::Function);
+ auto Personality =
+ MCContext.getOrCreateSymbol("cfi_personality", rcc::mc::MCSymbolKind::Function);
+ if (!EHFrame || !Function || !Personality) {
+ Context.expect(false, "native CFI boundary fixture must be constructible");
+ return;
+ }
+ const rcc::mc::MCDwarfFrameInfo Info{.CodeAlignment = 1,
+ .DataAlignment = -8,
+ .ReturnAddressRegister = 1,
+ .InitialCFARegister = 2,
+ .InitialCFAOffset = 0};
+ const std::array UnsupportedEvents{
+ rcc::mc::MCCFIInstruction{.Operation = rcc::mc::MCCFIOperation::Personality,
+ .Operands = {0},
+ .Bytes = {},
+ .Symbol = *Personality},
+ rcc::mc::MCCFIInstruction{.Operation = rcc::mc::MCCFIOperation::LSDA,
+ .Operands = {0},
+ .Bytes = {},
+ .Symbol = *Personality},
+ rcc::mc::MCCFIInstruction{.Operation = rcc::mc::MCCFIOperation::SignalFrame,
+ .Operands = {},
+ .Bytes = {},
+ .Symbol = {}},
+ rcc::mc::MCCFIInstruction{.Operation = rcc::mc::MCCFIOperation::ReturnColumn,
+ .Operands = {1},
+ .Bytes = {},
+ .Symbol = {}},
+ rcc::mc::MCCFIInstruction{.Operation = rcc::mc::MCCFIOperation::NegateReturnAddressState,
+ .Operands = {},
+ .Bytes = {},
+ .Symbol = {}},
+ rcc::mc::MCCFIInstruction{.Operation =
+ rcc::mc::MCCFIOperation::NegateReturnAddressStateWithPC,
+ .Operands = {},
+ .Bytes = {},
+ .Symbol = {}},
+ };
+ for (const rcc::mc::MCCFIInstruction &Event : UnsupportedEvents) {
+ rcc::mc::MCDwarfFrameProcedure Procedure{
+ .Function = *Function,
+ .AddressRange = 4,
+ .Instructions = {{.Instruction = Event, .Offset = 0}},
+ };
+ auto Frame = rcc::mc::emitDwarfEHFrame(*EHFrame, std::span(&Procedure, 1), Info, Backend);
+ Context.expect(!Frame && Frame.error().code() == rcc::ErrorCode::Unsupported,
+ "unsupported RV64GC CFI extensions must fail structurally");
+ }
+}
+
+[[nodiscard]] bool buildGlobalFixture(rcc::CompilationUnit &Unit) {
+ constexpr auto NoConfiguration = [](rcc::GlobalObjectDesc &) {};
+ if (!addObject(Unit, "external_target", 0, 1, rcc::GlobalLinkage::External, std::nullopt,
+ NoConfiguration) ||
+ !addObject(Unit, "weak_target", 0, 1, rcc::GlobalLinkage::ExternalWeak, std::nullopt,
+ NoConfiguration)) {
+ return false;
+ }
+
+ constexpr std::uint64_t LargeBSS = (std::uint64_t{1} << 32U) + 32U;
+ auto LargeZero = rcc::GlobalInitializer::zeroFill(LargeBSS);
+ auto TLSValue = integer(64, 0x1122334455667788ULL);
+ auto TLSZero = rcc::GlobalInitializer::zeroFill(32);
+ auto Relocation = rcc::GlobalInitializer::symbolAddress(
+ {.Symbol = "external_target", .Addend = 7, .Size = 8, .TargetFlags = 0});
+ auto COMDATRelocation = rcc::GlobalInitializer::symbolAddress(
+ {.Symbol = "external_target", .Addend = 3, .Size = 8, .TargetFlags = 0});
+ auto HiddenValue = integer(32, 0x12345678U);
+ auto ProtectedValue = integer(64, 0x8877665544332211ULL);
+ auto ReadOnlyValue = integer(32, 0xaabbccddU);
+ auto TargetConstant = rcc::TargetConstant::create(rcc::riscv::RISCVSymbolConstant(
+ rcc::riscv::RISCVSymbolConstantKind::ExternalSymbol, "external_target", 13));
+ auto TargetInitializer =
+ TargetConstant
+ ? rcc::GlobalInitializer::target(std::move(*TargetConstant))
+ : rcc::Expected<rcc::GlobalInitializer>(std::unexpected(TargetConstant.error()));
+ if (!LargeZero || !TLSValue || !TLSZero || !Relocation || !COMDATRelocation || !HiddenValue ||
+ !ProtectedValue || !ReadOnlyValue || !TargetInitializer) {
+ return false;
+ }
+
+ std::vector<rcc::GlobalInitializer> Differences;
+ for (const auto [Size, Addend] :
+ std::array<std::pair<std::uint8_t, std::int64_t>, 4>{{{1, 11}, {2, 9}, {4, 7}, {8, 5}}}) {
+ auto Difference = rcc::GlobalInitializer::symbolDifference({.Minuend = "external_target",
+ .Subtrahend = "weak_target",
+ .Addend = Addend,
+ .Size = Size});
+ if (!Difference)
+ return false;
+ Differences.push_back(std::move(*Difference));
+ }
+ auto DifferenceAggregate = rcc::GlobalInitializer::aggregate(std::move(Differences));
+ if (!DifferenceAggregate)
+ return false;
+
+ return addObject(Unit, "large_bss", LargeBSS, 4096, rcc::GlobalLinkage::Internal,
+ std::move(*LargeZero), NoConfiguration) &&
+ addObject(Unit, "tls_data", 8, 8, rcc::GlobalLinkage::External, std::move(*TLSValue),
+ [](rcc::GlobalObjectDesc &Object) {
+ Object.TLSModel = rcc::GlobalTLSModel::LocalExec;
+ }) &&
+ addObject(Unit, "tls_bss", 32, 32, rcc::GlobalLinkage::External, std::move(*TLSZero),
+ [](rcc::GlobalObjectDesc &Object) {
+ Object.TLSModel = rcc::GlobalTLSModel::GeneralDynamic;
+ }) &&
+ addObject(Unit, "common_value", 128, 64, rcc::GlobalLinkage::Common, std::nullopt,
+ NoConfiguration) &&
+ addObject(Unit, "relro_pointer", 8, 8, rcc::GlobalLinkage::External,
+ std::move(*Relocation),
+ [](rcc::GlobalObjectDesc &Object) { Object.IsConstant = true; }) &&
+ addObject(Unit, "difference_value", 15, 1, rcc::GlobalLinkage::External,
+ std::move(*DifferenceAggregate), NoConfiguration) &&
+ addObject(Unit, "comdat_value", 8, 8, rcc::GlobalLinkage::LinkOnceODR,
+ std::move(*COMDATRelocation),
+ [](rcc::GlobalObjectDesc &Object) { Object.COMDAT = Object.Symbol; }) &&
+ addObject(Unit, "hidden_value", 4, 16, rcc::GlobalLinkage::External,
+ std::move(*HiddenValue),
+ [](rcc::GlobalObjectDesc &Object) {
+ Object.Visibility = rcc::GlobalVisibility::Hidden;
+ Object.Section = ".custom.data";
+ }) &&
+ addObject(Unit, "protected_value", 8, 128, rcc::GlobalLinkage::External,
+ std::move(*ProtectedValue),
+ [](rcc::GlobalObjectDesc &Object) {
+ Object.Visibility = rcc::GlobalVisibility::Protected;
+ }) &&
+ addObject(Unit, "readonly_value", 4, 4, rcc::GlobalLinkage::External,
+ std::move(*ReadOnlyValue),
+ [](rcc::GlobalObjectDesc &Object) { Object.IsConstant = true; }) &&
+ addObject(Unit, "target_constant_value", 8, 8, rcc::GlobalLinkage::Internal,
+ std::move(*TargetInitializer), [](rcc::GlobalObjectDesc &Object) {
+ Object.IsConstant = true;
+ Object.Section = ".data.rel.ro.target-constant";
+ });
+}
+
+void testGlobalObjectEmission(TestContext &Context, const rcc::TargetDescription &Target) {
+ rcc::CompilationUnit Unit;
+ Context.expect(buildGlobalFixture(Unit), "global object fixture construction must succeed");
+ auto First = rcc::riscv::emitObject(Unit, Target);
+ auto Second = rcc::riscv::emitObject(Unit, Target);
+ if (!First)
+ std::cerr << "first global object emission failure: " << First.error().message() << '\n';
+ if (!Second)
+ std::cerr << "second global object emission failure: " << Second.error().message() << '\n';
+ Context.expect(First && Second && *First == *Second,
+ "global object emission must be successful and deterministic");
+ if (!First)
+ return;
+ Context.expect(First->size() < std::size_t{64} * 1024U,
+ "large NOBITS globals must not inflate the ELF file");
+ auto View = ELFView::create(*First);
+ Context.expect(View.has_value(), "emitted global object must be a valid ELF64LE container");
+ if (!View)
+ return;
+
+ const ELFSection *BSS = View->section(".bss");
+ const ELFSection *Data = View->section(".data");
+ const ELFSection *TLSData = View->section(".tdata");
+ const ELFSection *TLSBSS = View->section(".tbss");
+ const ELFSection *RelRO = View->section(".data.rel.ro");
+ const ELFSection *COMDAT = View->section(".data.comdat_value");
+ const ELFSection *COMDATRelocations = View->section(".rela.data.comdat_value");
+ const ELFSection *Group = View->section(".group");
+ constexpr std::uint64_t LargeBSS = (std::uint64_t{1} << 32U) + 32U;
+ Context.expect(BSS != nullptr && BSS->Type == SHTNoBits && BSS->Size == LargeBSS &&
+ BSS->Alignment == 4096 &&
+ (BSS->Flags & (SHFWrite | SHFAlloc)) == (SHFWrite | SHFAlloc),
+ "ELF BSS section must preserve its address size, flags, and alignment");
+ Context.expect(Data != nullptr && Data->Alignment == 128,
+ "ELF data section alignment must include later fragment requirements");
+ Context.expect(TLSData != nullptr && TLSData->Type != SHTNoBits &&
+ (TLSData->Flags & SHFTLS) != 0 && TLSBSS != nullptr &&
+ TLSBSS->Type == SHTNoBits && (TLSBSS->Flags & SHFTLS) != 0,
+ "ELF TLS data and BSS sections must carry their TLS semantics");
+ Context.expect(RelRO != nullptr && COMDAT != nullptr && COMDATRelocations != nullptr &&
+ Group != nullptr && Group->Type == SHTGroup &&
+ (COMDAT->Flags & SHFGroup) != 0 && (COMDATRelocations->Flags & SHFGroup) != 0,
+ "ELF COMDAT data and relocation sections must share a group");
+ Context.expect(View->section(".custom.data") != nullptr && View->section(".rodata") != nullptr &&
+ View->section(".riscv.attributes") != nullptr,
+ "ELF custom, read-only, and RISC-V attribute sections must be present");
+ const ELFSection *TargetConstantSection = View->section(".data.rel.ro.target-constant");
+ Context.expect(TargetConstantSection != nullptr && TargetConstantSection->Size == 8 &&
+ std::ranges::all_of(View->contents(*TargetConstantSection),
+ [](std::uint8_t Byte) { return Byte == 0; }),
+ "RV64GC target constants must reserve their relocation storage");
+
+ auto Symbols = View->symbols();
+ Context.expect(Symbols.has_value(), "ELF symbol table must be parseable");
+ if (!Symbols)
+ return;
+ const ELFSymbol *External = findSymbol(*Symbols, "external_target");
+ const ELFSymbol *Weak = findSymbol(*Symbols, "weak_target");
+ const ELFSymbol *Large = findSymbol(*Symbols, "large_bss");
+ const ELFSymbol *TLS = findSymbol(*Symbols, "tls_data");
+ const ELFSymbol *Common = findSymbol(*Symbols, "common_value");
+ const ELFSymbol *Hidden = findSymbol(*Symbols, "hidden_value");
+ const ELFSymbol *Protected = findSymbol(*Symbols, "protected_value");
+ const ELFSymbol *COMDATSymbol = findSymbol(*Symbols, "comdat_value");
+ Context.expect(External != nullptr && External->Binding == 1 &&
+ External->Section == SHNUndefined && External->Type == 1 && Weak != nullptr &&
+ Weak->Binding == 2 && Weak->Section == SHNUndefined,
+ "undefined object declarations must retain global and weak bindings");
+ Context.expect(Large != nullptr && Large->Binding == 0 && Large->Size == LargeBSS &&
+ TLS != nullptr && TLS->Binding == 1 && TLS->Type == 6,
+ "local BSS and global TLS symbols must have correct ELF types and sizes");
+ Context.expect(Common != nullptr && Common->Binding == 1 && Common->Section == SHNCommon &&
+ Common->Value == 64 && Common->Size == 128,
+ "ELF common symbol alignment and size must be preserved");
+ Context.expect(Hidden != nullptr && Hidden->Visibility == 2 && Protected != nullptr &&
+ Protected->Visibility == 3 && COMDATSymbol != nullptr &&
+ COMDATSymbol->Binding == 2,
+ "ELF visibility and COMDAT weak binding must be preserved");
+
+ auto DifferenceRelocations = View->relocations(".rela.data");
+ Context.expect(DifferenceRelocations && DifferenceRelocations->size() == 8,
+ "symbol differences must emit eight paired ELF relocations");
+ if (DifferenceRelocations) {
+ const std::array<std::uint32_t, 8> ExpectedTypes{33, 37, 34, 38, 35, 39, 36, 40};
+ for (std::size_t Index = 0; Index < ExpectedTypes.size(); ++Index) {
+ Context.expect((*DifferenceRelocations)[Index].Type == ExpectedTypes[Index],
+ "symbol difference relocation type order is incorrect");
+ }
+ const std::array<std::int64_t, 8> ExpectedAddends{11, 0, 9, 0, 7, 0, 5, 0};
+ for (std::size_t Index = 0; Index < ExpectedAddends.size(); ++Index) {
+ Context.expect((*DifferenceRelocations)[Index].Addend == ExpectedAddends[Index],
+ "symbol difference relocation addend is incorrect");
+ }
+ }
+ auto Relocations = View->relocations(".rela.data.rel.ro");
+ Context.expect(Relocations && Relocations->size() == 1 && (*Relocations)[0].Type == 2 &&
+ (*Relocations)[0].Addend == 7,
+ "read-only pointer data must emit an R_RISCV_64 relocation");
+ auto COMDATFixups = View->relocations(".rela.data.comdat_value");
+ Context.expect(COMDATFixups && COMDATFixups->size() == 1 && (*COMDATFixups)[0].Type == 2 &&
+ (*COMDATFixups)[0].Addend == 3,
+ "COMDAT pointer data must retain its relocation");
+ auto TargetConstantFixups = View->relocations(".rela.data.rel.ro.target-constant");
+ Context.expect(TargetConstantFixups && TargetConstantFixups->size() == 1 &&
+ (*TargetConstantFixups)[0].Type == 2 &&
+ (*TargetConstantFixups)[0].Addend == 13,
+ "RV64GC target constants must emit their declared R_RISCV_64 relocation");
+
+ if (Group != nullptr && COMDATSymbol != nullptr) {
+ const std::span<const std::uint8_t> GroupBytes = View->contents(*Group);
+ const auto GroupFlags = readU32(GroupBytes, 0);
+ const auto COMDATIndex = View->sectionIndex(".data.comdat_value");
+ const auto RelocationIndex = View->sectionIndex(".rela.data.comdat_value");
+ bool ContainsCOMDAT = false;
+ bool ContainsRelocations = false;
+ for (std::size_t Offset = 4; Offset + 4 <= GroupBytes.size(); Offset += 4) {
+ const auto Member = readU32(GroupBytes, Offset);
+ ContainsCOMDAT |= Member && COMDATIndex && *Member == *COMDATIndex;
+ ContainsRelocations |= Member && RelocationIndex && *Member == *RelocationIndex;
+ }
+ Context.expect(GroupFlags == 1 && Group->Info < Symbols->size() &&
+ (*Symbols)[Group->Info].Name == "comdat_value" && ContainsCOMDAT &&
+ ContainsRelocations,
+ "ELF COMDAT group must name its signature and both member sections");
+ }
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ auto Target = rcc::riscv::createTargetDescription();
+ Context.expect(Target.has_value(), "RISC-V object test target creation must succeed");
+ if (!Target)
+ return Context.result();
+ testAssemblerLayout(Context, *Target);
+ testMCRelaxation(Context, *Target);
+ testNativeObjectBoundaries(Context, *Target);
+ testGlobalObjectEmission(Context, *Target);
+ return Context.result();
+}
diff --git a/backend/unittests/Target/RISCVRegisterAllocationTest.cpp b/backend/unittests/Target/RISCVRegisterAllocationTest.cpp
new file mode 100644
index 0000000..29bc756
--- /dev/null
+++ b/backend/unittests/Target/RISCVRegisterAllocationTest.cpp
@@ -0,0 +1,539 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "RISCV.h"
+#include "rcc/CodeGen/LiveIntervals.h"
+#include "rcc/CodeGen/LiveVariables.h"
+#include "rcc/CodeGen/LowLevelType.h"
+#include "rcc/CodeGen/MachineBasicBlock.h"
+#include "rcc/CodeGen/MachineFrameInfo.h"
+#include "rcc/CodeGen/MachineFunction.h"
+#include "rcc/CodeGen/MachineIRBuilder.h"
+#include "rcc/CodeGen/MachineInstr.h"
+#include "rcc/CodeGen/MachineMemOperand.h"
+#include "rcc/CodeGen/MachineOperand.h"
+#include "rcc/CodeGen/MachinePassManager.h"
+#include "rcc/CodeGen/MachineRegisterInfo.h"
+#include "rcc/CodeGen/RegAllocCommon.h"
+#include "rcc/CodeGen/RegAllocFast.h"
+#include "rcc/CodeGen/RegAllocPBQP.h"
+#include "rcc/CodeGen/TargetDescription.h"
+#include "rcc/CodeGen/TargetRegisterAllocationInfo.h"
+#include "rcc/Target/RISCV/RISCVInstructions.h"
+#include "rcc/Target/RISCV/RISCVRegisters.h"
+#include "rcc/Target/RISCV/RISCVTarget.h"
+
+#include <algorithm>
+#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]] constexpr rcc::RegisterClassID
+registerClass(rcc::riscv::RegisterClass Value) noexcept {
+ return rcc::RegisterClassID::fromValue(static_cast<std::uint32_t>(Value));
+}
+
+[[nodiscard]] constexpr rcc::RegisterBankID registerBank(rcc::riscv::RegisterBank Value) noexcept {
+ return rcc::RegisterBankID::fromValue(static_cast<std::uint32_t>(Value));
+}
+
+[[nodiscard]] rcc::Register physical(rcc::riscv::PhysicalRegisterID ID) {
+ return rcc::Register::fromPhysical(*rcc::riscv::physicalRegister(ID));
+}
+
+struct FunctionFixture final {
+ std::unique_ptr<rcc::MachineFunction> Function;
+ rcc::MachineBasicBlock *Block = nullptr;
+};
+
+[[nodiscard]] rcc::Expected<FunctionFixture> createFunction(std::string Name, std::uint64_t ID) {
+ auto Function =
+ rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID));
+ if (!Function)
+ return std::unexpected(Function.error());
+ auto Block = (*Function)->createBasicBlock();
+ if (!Block)
+ return std::unexpected(Block.error());
+ auto Appended = (*Function)->appendBlock(**Block);
+ if (!Appended)
+ return std::unexpected(Appended.error());
+ return FunctionFixture{std::move(*Function), *Block};
+}
+
+[[nodiscard]] rcc::Expected<rcc::Register> createVReg(rcc::MachineFunction &Function,
+ rcc::LowLevelType Type,
+ rcc::riscv::RegisterBank Bank,
+ rcc::riscv::RegisterClass Class) {
+ auto Reg = Function.registerInfo().createVirtualRegister(Type);
+ if (!Reg)
+ return std::unexpected(Reg.error());
+ if (auto Result = Function.registerInfo().setRegisterBank(*Reg, registerBank(Bank)); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Function.registerInfo().setRegisterClass(*Reg, registerClass(Class)); !Result)
+ return std::unexpected(Result.error());
+ return *Reg;
+}
+
+[[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);
+ if (auto Result = Builder.setInsertPoint(Block); !Result)
+ return std::unexpected(Result.error());
+ auto Pending =
+ Builder.buildInstr(rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::ADDI));
+ if (!Pending)
+ return std::unexpected(Pending.error());
+ if (auto Result = Pending->addDef(Destination); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Pending->addUse(Source); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Pending->addImmediate(Immediate); !Result)
+ return std::unexpected(Result.error());
+ return Pending->build();
+}
+
+[[nodiscard]] rcc::Expected<rcc::MachineInstr *> buildAdd(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target,
+ rcc::MachineBasicBlock &Block,
+ rcc::Register Destination,
+ rcc::Register Left, rcc::Register Right) {
+ rcc::MachineIRBuilder Builder(Function, Target);
+ if (auto Result = Builder.setInsertPoint(Block); !Result)
+ return std::unexpected(Result.error());
+ auto Pending =
+ Builder.buildInstr(rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::ADD));
+ if (!Pending)
+ return std::unexpected(Pending.error());
+ if (auto Result = Pending->addDef(Destination); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Pending->addUse(Left); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Pending->addUse(Right); !Result)
+ return std::unexpected(Result.error());
+ return Pending->build();
+}
+
+[[nodiscard]] rcc::Expected<void> prepareForAllocation(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target) {
+ auto Rebuilt = rcc::rebuildMachineLiveness(Function, Target);
+ if (!Rebuilt)
+ return std::unexpected(Rebuilt.error());
+ for (const rcc::MachineFunctionProperty Property : {
+ rcc::MachineFunctionProperty::TracksLiveness,
+ rcc::MachineFunctionProperty::Legalized,
+ rcc::MachineFunctionProperty::RegBankSelected,
+ rcc::MachineFunctionProperty::Selected,
+ rcc::MachineFunctionProperty::NoPHIs,
+ rcc::MachineFunctionProperty::TiedOpsRewritten,
+ }) {
+ Function.properties().set(Property);
+ }
+ return {};
+}
+
+enum class AllocatorKind : std::uint8_t {
+ Basic,
+ Fast,
+ PBQP,
+};
+
+[[nodiscard]] rcc::Expected<void> runAllocator(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target,
+ AllocatorKind Kind,
+ rcc::RegisterAllocationOptions Options = {}) {
+ rcc::MachineFunctionAnalysisManager Analyses;
+ auto Registered = rcc::registerStage11MachineAnalyses(Analyses);
+ if (!Registered)
+ return std::unexpected(Registered.error());
+ std::unique_ptr<rcc::MachineFunctionPass> Pass;
+ switch (Kind) {
+ case AllocatorKind::Basic:
+ Pass = rcc::createBasicAllocatorPass(rcc::riscv::registerAllocationInfo(), Options);
+ break;
+ case AllocatorKind::Fast:
+ Pass = std::make_unique<rcc::FastAllocatorPass>(rcc::riscv::registerAllocationInfo());
+ break;
+ case AllocatorKind::PBQP:
+ Pass = std::make_unique<rcc::PBQPAllocatorPass>(rcc::riscv::registerAllocationInfo(), Options);
+ break;
+ }
+ rcc::MachineFunctionPassManager Pipeline;
+ auto Added = Pipeline.addPass(std::move(Pass));
+ if (!Added)
+ return std::unexpected(Added.error());
+ return Pipeline.run(Function, Target, Analyses);
+}
+
+[[nodiscard]] std::size_t countOpcode(const rcc::MachineFunction &Function,
+ rcc::riscv::InstructionOpcode Value) {
+ std::size_t Count = 0;
+ for (const rcc::MachineBasicBlock *Block = Function.firstBlock(); Block != nullptr;
+ Block = Block->next()) {
+ for (const rcc::MachineInstr *Instruction = Block->firstInstruction(); Instruction != nullptr;
+ Instruction = Instruction->next()) {
+ Count += Instruction->opcode() == rcc::riscv::instructionOpcode(Value) ? 1U : 0U;
+ }
+ }
+ return Count;
+}
+
+[[nodiscard]] std::vector<std::uint64_t> allocationSignature(const rcc::MachineFunction &Function) {
+ std::vector<std::uint64_t> Result;
+ Result.push_back(Function.frameInfo().liveObjectCount());
+ for (const rcc::MachineBasicBlock *Block = Function.firstBlock(); Block != nullptr;
+ Block = Block->next()) {
+ for (const rcc::MachineInstr *Instruction = Block->firstInstruction(); Instruction != nullptr;
+ Instruction = Instruction->next()) {
+ Result.push_back(Instruction->opcode().value());
+ for (const rcc::MachineOperand &Operand : Instruction->operands()) {
+ Result.push_back(static_cast<std::uint64_t>(Operand.kind()));
+ const rcc::Register *Reg = Operand.getIf<rcc::Register>();
+ Result.push_back(Reg == nullptr ? 0 : Reg->opaqueValue());
+ const auto *Frame = Operand.getIf<rcc::FrameIndexOperand>();
+ Result.push_back(Frame == nullptr ? 0
+ : static_cast<std::uint64_t>(Frame->Index.value()) + 1U);
+ }
+ }
+ }
+ return Result;
+}
+
+void testTargetHooks(TestContext &Context, const rcc::TargetDescription &Target) {
+ const rcc::TargetRegisterAllocationInfo &Info = rcc::riscv::registerAllocationInfo();
+ for (const rcc::riscv::RegisterClass Class : {
+ rcc::riscv::RegisterClass::GPR,
+ rcc::riscv::RegisterClass::GPRC,
+ rcc::riscv::RegisterClass::FPR64,
+ rcc::riscv::RegisterClass::FPR64C,
+ rcc::riscv::RegisterClass::FPR32,
+ rcc::riscv::RegisterClass::FPR32C,
+ rcc::riscv::RegisterClass::GPR_NX0,
+ rcc::riscv::RegisterClass::GPR_NX0_NX2,
+ }) {
+ Context.expect(Info.validateSpillReload(registerClass(Class), Target).has_value(),
+ "every allocatable RV64GC scalar class must have spill/reload hooks");
+ }
+
+ auto Fixture = createFunction("riscv-spill-hooks", 1);
+ if (!Fixture)
+ return;
+ rcc::StackObjectDesc StackDescription;
+ StackDescription.Kind = rcc::StackObjectKind::SpillSlot;
+ StackDescription.Size = 8;
+ auto Align = rcc::Alignment::create(8);
+ if (!Align)
+ return;
+ StackDescription.Align = *Align;
+ auto Slot = Fixture->Function->frameInfo().createObject(std::move(StackDescription));
+ if (!Slot)
+ return;
+ rcc::MachineIRBuilder Builder(*Fixture->Function, Target);
+ if (!Builder.setInsertPoint(*Fixture->Block))
+ return;
+
+ auto GPRReload =
+ Info.emitReload(Builder, physical(rcc::riscv::PhysicalRegisterID::X5),
+ registerClass(rcc::riscv::RegisterClass::GPR), *Slot, rcc::LaneMask::all());
+ auto GPRSpill =
+ Info.emitSpill(Builder, physical(rcc::riscv::PhysicalRegisterID::X5),
+ registerClass(rcc::riscv::RegisterClass::GPR), *Slot, rcc::LaneMask::all());
+ auto F32Reload =
+ Info.emitReload(Builder, physical(rcc::riscv::PhysicalRegisterID::F10_F),
+ registerClass(rcc::riscv::RegisterClass::FPR32), *Slot, rcc::LaneMask::all());
+ auto F32Spill =
+ Info.emitSpill(Builder, physical(rcc::riscv::PhysicalRegisterID::F10_F),
+ registerClass(rcc::riscv::RegisterClass::FPR32), *Slot, rcc::LaneMask::all());
+ auto F64Reload =
+ Info.emitReload(Builder, physical(rcc::riscv::PhysicalRegisterID::F11_D),
+ registerClass(rcc::riscv::RegisterClass::FPR64), *Slot, rcc::LaneMask::all());
+ auto F64Spill =
+ Info.emitSpill(Builder, physical(rcc::riscv::PhysicalRegisterID::F11_D),
+ registerClass(rcc::riscv::RegisterClass::FPR64), *Slot, rcc::LaneMask::all());
+ Context.expect(GPRReload && GPRSpill && F32Reload && F32Spill && F64Reload && F64Spill &&
+ (*GPRReload)->opcode() ==
+ rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::LD) &&
+ (*GPRSpill)->opcode() ==
+ rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::SD) &&
+ (*F32Reload)->opcode() ==
+ rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::FLW) &&
+ (*F32Spill)->opcode() ==
+ rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::FSW) &&
+ (*F64Reload)->opcode() ==
+ rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::FLD) &&
+ (*F64Spill)->opcode() ==
+ rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::FSD),
+ "RV64GC hooks must select width- and bank-correct stack instructions");
+ if (GPRReload && F32Reload && F64Reload) {
+ Context.expect(
+ (*GPRReload)->operandAt(1)->getIf<rcc::FrameIndexOperand>() != nullptr &&
+ (*GPRReload)->memOperands().front()->description().Size.minimumBytes() == 8 &&
+ (*F32Reload)->memOperands().front()->description().Size.minimumBytes() == 4 &&
+ (*F64Reload)->memOperands().front()->description().Size.minimumBytes() == 8,
+ "RV64GC stack instructions must retain precise FrameIndex MMO size metadata");
+ }
+ auto EmptyLane = Info.emitReload(Builder, physical(rcc::riscv::PhysicalRegisterID::X5),
+ registerClass(rcc::riscv::RegisterClass::GPR), *Slot, {});
+ Context.expect(!EmptyLane && EmptyLane.error().code() == rcc::ErrorCode::InvalidArgument,
+ "RV64GC spill hooks must reject an empty lane request");
+}
+
+void testRematerialization(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Fixture = createFunction("riscv-rematerialization", 2);
+ auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64);
+ if (!Fixture || !I64)
+ return;
+ auto Original = createVReg(*Fixture->Function, *I64, rcc::riscv::RegisterBank::GPR,
+ rcc::riscv::RegisterClass::GPR);
+ auto Child = createVReg(*Fixture->Function, *I64, rcc::riscv::RegisterBank::GPR,
+ rcc::riscv::RegisterClass::GPR);
+ if (!Original || !Child)
+ return;
+ auto Definition = buildAddImmediate(*Fixture->Function, Target, *Fixture->Block, *Original,
+ physical(rcc::riscv::PhysicalRegisterID::X0), 17);
+ if (!Definition)
+ return;
+ const rcc::TargetRegisterAllocationInfo &Info = rcc::riscv::registerAllocationInfo();
+ auto Accepted = Info.isRematerializable(**Definition, *Original, *Fixture->Function, Target);
+ rcc::MachineIRBuilder Builder(*Fixture->Function, Target);
+ if (!Builder.setInsertPointAfter(**Definition))
+ return;
+ auto Materialized = Info.rematerialize(Builder, **Definition, *Child, *Original, Target);
+ Context.expect(Accepted && *Accepted && Materialized &&
+ (*Materialized)->opcode() ==
+ rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::ADDI) &&
+ (*Materialized)->operandAt(0)->registerValue() == *Child &&
+ (*Materialized)->operandAt(1)->registerValue() ==
+ physical(rcc::riscv::PhysicalRegisterID::X0),
+ "RV64GC must rematerialize constant ADDI forms with a replacement definition");
+}
+
+[[nodiscard]] rcc::Expected<FunctionFixture>
+createSimpleAllocationFunction(const rcc::TargetDescription &Target, std::uint64_t ID) {
+ auto Fixture = createFunction("riscv-ra-" + std::to_string(ID), ID);
+ auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64);
+ if (!Fixture)
+ return std::unexpected(Fixture.error());
+ if (!I64)
+ return std::unexpected(I64.error());
+ std::vector<rcc::Register> Values;
+ for (std::int64_t Immediate : {1, 2, 3}) {
+ auto Reg = createVReg(*Fixture->Function, *I64, rcc::riscv::RegisterBank::GPR,
+ rcc::riscv::RegisterClass::GPR);
+ if (!Reg)
+ return std::unexpected(Reg.error());
+ if (auto Built = buildAddImmediate(*Fixture->Function, Target, *Fixture->Block, *Reg,
+ physical(rcc::riscv::PhysicalRegisterID::X0), Immediate);
+ !Built) {
+ return std::unexpected(Built.error());
+ }
+ Values.push_back(*Reg);
+ }
+ auto Sum = createVReg(*Fixture->Function, *I64, rcc::riscv::RegisterBank::GPR,
+ rcc::riscv::RegisterClass::GPR);
+ if (!Sum)
+ return std::unexpected(Sum.error());
+ if (auto Built =
+ buildAdd(*Fixture->Function, Target, *Fixture->Block, *Sum, Values[0], Values[1]);
+ !Built) {
+ return std::unexpected(Built.error());
+ }
+ if (auto Built = buildAdd(*Fixture->Function, Target, *Fixture->Block,
+ physical(rcc::riscv::PhysicalRegisterID::X0), *Sum, Values[2]);
+ !Built) {
+ return std::unexpected(Built.error());
+ }
+ auto Prepared = prepareForAllocation(*Fixture->Function, Target);
+ if (!Prepared)
+ return std::unexpected(Prepared.error());
+ return Fixture;
+}
+
+void testRealAllocators(TestContext &Context, const rcc::TargetDescription &Target) {
+ for (const auto [Kind, ID, Name] : {
+ std::tuple{AllocatorKind::Basic, std::uint64_t{10}, std::string_view{"Basic"}},
+ std::tuple{AllocatorKind::Fast, std::uint64_t{11}, std::string_view{"Fast"}},
+ std::tuple{AllocatorKind::PBQP, std::uint64_t{12}, std::string_view{"PBQP"}},
+ }) {
+ auto Fixture = createSimpleAllocationFunction(Target, ID);
+ if (!Fixture)
+ continue;
+ auto Allocated = runAllocator(*Fixture->Function, Target, Kind);
+ if (!Allocated)
+ std::cerr << "RV64GC " << Name << " error: " << Allocated.error().message() << '\n';
+ Context.expect(
+ Allocated && Fixture->Function->properties().has(rcc::MachineFunctionProperty::NoVRegs) &&
+ Fixture->Function->registerInfo().virtualRegisters().size() == 4,
+ "every allocator must rewrite real RV64GC selected instructions to physical registers");
+ if (!Allocated)
+ continue;
+ bool OnlyPhysical = true;
+ for (const rcc::MachineInstr *Instruction : Fixture->Function->allInstructions()) {
+ if (Instruction == nullptr || Instruction->parent() == nullptr)
+ continue;
+ for (const rcc::MachineOperand &Operand : Instruction->operands()) {
+ const rcc::Register *Reg = Operand.getIf<rcc::Register>();
+ OnlyPhysical &= Reg == nullptr || !Reg->isVirtual();
+ }
+ }
+ Context.expect(OnlyPhysical,
+ "RV64GC allocator output must not retain a linked virtual-register operand");
+ }
+}
+
+void testFunctionReservedRegisters(TestContext &Context, const rcc::TargetDescription &Target) {
+ for (const auto [Kind, ID, Name] : {
+ std::tuple{AllocatorKind::Basic, std::uint64_t{13}, std::string_view{"Basic"}},
+ std::tuple{AllocatorKind::Fast, std::uint64_t{14}, std::string_view{"Fast"}},
+ std::tuple{AllocatorKind::PBQP, std::uint64_t{15}, std::string_view{"PBQP"}},
+ }) {
+ auto Fixture = createSimpleAllocationFunction(Target, ID);
+ auto Align64 = rcc::Alignment::create(64);
+ if (!Fixture || !Align64)
+ continue;
+ rcc::StackObjectDesc Description;
+ Description.Size = 8;
+ Description.Align = *Align64;
+ if (!Fixture->Function->frameInfo().createObject(std::move(Description)))
+ continue;
+ auto Allocated = runAllocator(*Fixture->Function, Target, Kind);
+ const rcc::PhysicalRegister FP =
+ *rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X8);
+ const rcc::PhysicalRegister BP =
+ *rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X9);
+ bool AllocatedFrameRegister = false;
+ for (const rcc::MachineInstr *Instruction : Fixture->Function->allInstructions()) {
+ if (Instruction == nullptr || Instruction->parent() == nullptr)
+ continue;
+ for (const rcc::MachineOperand &Operand : Instruction->operands()) {
+ const rcc::Register *Reg = Operand.getIf<rcc::Register>();
+ const auto Physical =
+ Reg != nullptr && Reg->isPhysical() ? Reg->physicalRegister() : std::nullopt;
+ AllocatedFrameRegister |= Physical && (*Physical == FP || *Physical == BP);
+ }
+ }
+ if (!Allocated)
+ std::cerr << "RV64GC " << Name << " reservation error: " << Allocated.error().message()
+ << '\n';
+ Context.expect(Allocated && Fixture->Function->registerInfo().isReserved(FP) &&
+ Fixture->Function->registerInfo().isReserved(BP) && !AllocatedFrameRegister,
+ "every allocator must persist and honor function-level FP/BP reservations");
+ }
+}
+
+[[nodiscard]] rcc::Expected<FunctionFixture>
+createPressureFunction(const rcc::TargetDescription &Target, std::uint64_t ID) {
+ auto Fixture = createFunction("riscv-pressure-" + std::to_string(ID), ID);
+ auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64);
+ if (!Fixture)
+ return std::unexpected(Fixture.error());
+ if (!I64)
+ return std::unexpected(I64.error());
+
+ std::vector<rcc::Register> Values;
+ Values.reserve(30);
+ for (std::int64_t Immediate = 0; Immediate < 30; ++Immediate) {
+ auto Reg = createVReg(*Fixture->Function, *I64, rcc::riscv::RegisterBank::GPR,
+ rcc::riscv::RegisterClass::GPR);
+ if (!Reg)
+ return std::unexpected(Reg.error());
+ auto Built = buildAddImmediate(*Fixture->Function, Target, *Fixture->Block, *Reg,
+ physical(rcc::riscv::PhysicalRegisterID::X0), Immediate);
+ if (!Built)
+ return std::unexpected(Built.error());
+ Values.push_back(*Reg);
+ }
+ const rcc::Register Zero = physical(rcc::riscv::PhysicalRegisterID::X0);
+ for (const rcc::Register Value : Values) {
+ auto Built = buildAdd(*Fixture->Function, Target, *Fixture->Block, Zero, Value, Zero);
+ if (!Built)
+ return std::unexpected(Built.error());
+ }
+ auto Prepared = prepareForAllocation(*Fixture->Function, Target);
+ if (!Prepared)
+ return std::unexpected(Prepared.error());
+ return Fixture;
+}
+
+void testRealPressureAndDeterminism(TestContext &Context, const rcc::TargetDescription &Target) {
+ std::vector<std::uint64_t> FirstPBQPSignature;
+ for (const auto [Kind, ID, Name] : {
+ std::tuple{AllocatorKind::Basic, std::uint64_t{20}, std::string_view{"Basic"}},
+ std::tuple{AllocatorKind::PBQP, std::uint64_t{21}, std::string_view{"PBQP"}},
+ std::tuple{AllocatorKind::PBQP, std::uint64_t{22}, std::string_view{"PBQP-repeat"}},
+ }) {
+ auto Fixture = createPressureFunction(Target, ID);
+ if (!Fixture)
+ continue;
+ auto Allocated =
+ runAllocator(*Fixture->Function, Target, Kind, {.EnableRematerialization = false});
+ if (!Allocated)
+ std::cerr << "RV64GC pressure " << Name << " error: " << Allocated.error().message() << '\n';
+ Context.expect(Allocated && Fixture->Function->frameInfo().liveObjectCount() != 0 &&
+ countOpcode(*Fixture->Function, rcc::riscv::InstructionOpcode::LD) != 0 &&
+ countOpcode(*Fixture->Function, rcc::riscv::InstructionOpcode::SD) != 0,
+ "RV64GC global allocators must spill and reload real GPR pressure");
+ if (!Allocated || Kind != AllocatorKind::PBQP)
+ continue;
+ const std::vector<std::uint64_t> Signature = allocationSignature(*Fixture->Function);
+ if (FirstPBQPSignature.empty())
+ FirstPBQPSignature = Signature;
+ else
+ Context.expect(Signature == FirstPBQPSignature,
+ "RV64GC PBQP allocation must be stable across identical functions");
+ }
+
+ auto RematerializedPressure = createPressureFunction(Target, 23);
+ if (RematerializedPressure) {
+ auto Allocated = runAllocator(*RematerializedPressure->Function, Target, AllocatorKind::PBQP);
+ if (!Allocated)
+ std::cerr << "RV64GC rematerialization pressure error: " << Allocated.error().message()
+ << '\n';
+ Context.expect(Allocated && RematerializedPressure->Function->properties().has(
+ rcc::MachineFunctionProperty::NoVRegs),
+ "RV64GC PBQP rematerialization must finish with a valid register map");
+ }
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ auto Target = rcc::riscv::createTargetDescription();
+ Context.expect(Target.has_value(), "RV64GC register-allocation target must be constructible");
+ if (!Target) {
+ std::cerr << "target construction error: " << Target.error().message() << '\n';
+ return Context.result();
+ }
+ testTargetHooks(Context, *Target);
+ testRematerialization(Context, *Target);
+ testRealAllocators(Context, *Target);
+ testFunctionReservedRegisters(Context, *Target);
+ testRealPressureAndDeterminism(Context, *Target);
+ return Context.result();
+}
diff --git a/backend/unittests/Target/RISCVSSAExitTest.cpp b/backend/unittests/Target/RISCVSSAExitTest.cpp
new file mode 100644
index 0000000..1912666
--- /dev/null
+++ b/backend/unittests/Target/RISCVSSAExitTest.cpp
@@ -0,0 +1,414 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "RISCV.h"
+#include "RISCVInstrInfo.h"
+#include "rcc/CodeGen/GenericOpcodes.h"
+#include "rcc/CodeGen/LiveVariables.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/MachinePassManager.h"
+#include "rcc/CodeGen/MachineVerifier.h"
+#include "rcc/CodeGen/PHIElimination.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::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::LowLevelType 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(rcc::riscv::instructionOpcode(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(rcc::riscv::instructionOpcode(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;
+}
+
+[[nodiscard]] rcc::Expected<void> setSelectedSSAProperties(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target) {
+ auto Rebuilt = rcc::rebuildMachineLiveness(Function, Target);
+ if (!Rebuilt)
+ return std::unexpected(Rebuilt.error());
+ for (rcc::MachineFunctionProperty Property :
+ {rcc::MachineFunctionProperty::IsSSA, rcc::MachineFunctionProperty::TracksLiveness,
+ rcc::MachineFunctionProperty::Legalized, rcc::MachineFunctionProperty::RegBankSelected,
+ rcc::MachineFunctionProperty::Selected}) {
+ Function.properties().set(Property);
+ }
+ return {};
+}
+
+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::instrInfo().isFallthroughEdge(**First, **Second, **Function, Target);
+ Context.expect(Fallthrough && *Fallthrough,
+ "RV64GC SSA behavior must recognize an empty layout fallthrough edge");
+ auto Inserted = rcc::riscv::instrInfo().insertBranch(**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() ==
+ rcc::riscv::instructionOpcode(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::instrInfo().isFallthroughEdge(**First, **Second, **Function, Target);
+ Context.expect(NoFallthrough && !*NoFallthrough, "JAL must terminate RV64GC layout fallthrough");
+ auto Rewritten =
+ rcc::riscv::instrInfo().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::LowLevelType::makeCategoryNeutralScalar(64);
+ auto F64 = rcc::LowLevelType::makeCategoryNeutralScalar(64);
+ auto F32 = rcc::LowLevelType::makeCategoryNeutralScalar(32);
+ 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::instrInfo().validateCopy(*GPR, {}, *OtherGPR, {}, **Function, Target).has_value(),
+ "RV64GC must allow a compatible GPR COPY");
+ Context.expect(
+ rcc::riscv::instrInfo().validateCopy(*GPR, {}, *FPR64, {}, **Function, Target).has_value(),
+ "RV64GC must allow supported 64-bit GPR/FPR bit moves");
+ Context.expect(!rcc::riscv::instrInfo().validateCopy(*FPR64, {}, *FPR32, {}, **Function, Target),
+ "RV64GC must reject incompatible same-bank COPY widths");
+ Context.expect(!rcc::riscv::instrInfo().validateCopy(*GPR, rcc::SubRegIndex::fromValue(1),
+ *OtherGPR, {}, **Function, Target),
+ "RV64GC must reject unmodeled subregister COPYs");
+ Context.expect(
+ !rcc::riscv::instrInfo().validateCopy(*GPR, {}, *Unclassified, {}, **Function, Target),
+ "RV64GC must reject COPYs involving an unconstrained virtual register");
+}
+
+void testCompressedConditionalBranchBehavior(TestContext &Context,
+ const rcc::TargetDescription &Target) {
+ auto Function = rcc::MachineFunction::create("riscv_compressed_branch",
+ rcc::MachineFunctionStableID::fromValue(3));
+ if (!Function)
+ return;
+ auto Source = appendBlock(**Function);
+ auto LayoutSuccessor = appendBlock(**Function);
+ auto BranchTarget = appendBlock(**Function);
+ auto ReplacementTarget = appendBlock(**Function);
+ if (!Source || !LayoutSuccessor || !BranchTarget || !ReplacementTarget)
+ return;
+ auto Half = rcc::BranchProbability::fromFraction(1, 2);
+ auto CFG = (*Function)->cfgUpdater();
+ if (!Half || !CFG.addSuccessor(**Source, **BranchTarget, *Half) ||
+ !CFG.addSuccessor(**Source, **LayoutSuccessor, *Half)) {
+ Context.expect(false, "compressed-branch CFG construction must succeed");
+ return;
+ }
+
+ auto X8 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X8);
+ rcc::MachineIRBuilder Builder(**Function, Target);
+ auto Point = Builder.setInsertPoint(**Source);
+ if (!Point) {
+ Context.expect(false, "compressed conditional branch insertion point must be established");
+ return;
+ }
+ auto Pending =
+ Builder.buildInstr(rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::C_BEQZ));
+ if (!X8 || !Pending || !Pending->addUse(rcc::Register::fromPhysical(*X8)) ||
+ !Pending->addBasicBlock(**BranchTarget)) {
+ Context.expect(false, "compressed conditional branch construction must succeed");
+ return;
+ }
+ auto Branch = Pending->build();
+ if (!Branch) {
+ Context.expect(false, "compressed conditional branch insertion must succeed");
+ return;
+ }
+
+ auto Fallthrough =
+ rcc::riscv::instrInfo().isFallthroughEdge(**Source, **LayoutSuccessor, **Function, Target);
+ auto Analysis = rcc::riscv::instrInfo().analyzeBranch(**Source, **Function, Target);
+ Context.expect(Fallthrough && *Fallthrough && Analysis && Analysis->Conditional == *Branch &&
+ Analysis->TrueTarget == *BranchTarget &&
+ Analysis->FalseTarget == *LayoutSuccessor,
+ "RV64GC instruction information must recognize compressed conditional branches");
+
+ auto Rewritten = rcc::riscv::instrInfo().rewriteBranchTarget(
+ **Source, **BranchTarget, **ReplacementTarget, **Function, Target);
+ Context.expect(Rewritten && *Rewritten && blockOperand(**Branch, 1) != nullptr &&
+ blockOperand(**Branch, 1)->Block == *ReplacementTarget,
+ "RV64GC instruction information must rewrite compressed branch targets");
+}
+
+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::LowLevelType::makeCategoryNeutralScalar(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");
+ auto InitialLiveness = setSelectedSSAProperties(**Function, Target);
+ Context.expect(InitialLiveness.has_value(), "RV64GC PHI liveness rebuild must succeed");
+ if (!InitialLiveness)
+ return;
+
+ rcc::MachineFunctionPassManager Pipeline;
+ Context.expect(
+ Pipeline.addPass(std::make_unique<rcc::PHIEliminationPass>(rcc::riscv::instrInfo()))
+ .has_value(),
+ "RV64GC PHI-elimination pass must be registered");
+ rcc::MachineFunctionAnalysisManager 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() ==
+ rcc::riscv::instructionOpcode(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() ==
+ rcc::riscv::instructionOpcode(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() ==
+ rcc::riscv::instructionOpcode(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);
+ testCompressedConditionalBranchBehavior(Context, *Target);
+ testRV64GCCriticalEdge(Context, *Target);
+ return Context.result();
+}
diff --git a/backend/unittests/Target/RISCVTargetTest.cpp b/backend/unittests/Target/RISCVTargetTest.cpp
new file mode 100644
index 0000000..c067168
--- /dev/null
+++ b/backend/unittests/Target/RISCVTargetTest.cpp
@@ -0,0 +1,330 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/Target/RISCV/RISCVTarget.h"
+
+#include "rcc/CodeGen/GenericOpcodes.h"
+
+#include <algorithm>
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <iostream>
+#include <ranges>
+#include <string_view>
+
+namespace {
+
+class TestContext final {
+public:
+ void expect(bool Condition, std::string_view Message) {
+ if (Condition)
+ return;
+ std::cerr << Message << '\n';
+ ++FailureCount;
+ }
+
+ [[nodiscard]] int result() const noexcept {
+ return FailureCount == 0 ? 0 : 1;
+ }
+
+private:
+ int FailureCount = 0;
+};
+
+[[nodiscard]] rcc::PhysicalRegister registerID(rcc::riscv::PhysicalRegisterID ID) {
+ const auto Register = rcc::riscv::physicalRegister(ID);
+ if (!Register)
+ std::terminate();
+ return *Register;
+}
+
+void testRegisterFacts(TestContext &Context) {
+ auto Registers = rcc::riscv::createRegisterInfo();
+ Context.expect(Registers.has_value(), "RV64GC register description must be valid");
+ if (!Registers)
+ return;
+
+ const rcc::PhysicalRegister X0 = registerID(rcc::riscv::PhysicalRegisterID::X0);
+ const rcc::PhysicalRegister X2 = registerID(rcc::riscv::PhysicalRegisterID::X2);
+ const rcc::PhysicalRegister X5 = registerID(rcc::riscv::PhysicalRegisterID::X5);
+ const rcc::PhysicalRegister X8 = registerID(rcc::riscv::PhysicalRegisterID::X8);
+ const rcc::PhysicalRegister F31D = registerID(rcc::riscv::PhysicalRegisterID::F31_D);
+ const rcc::PhysicalRegister F31F = registerID(rcc::riscv::PhysicalRegisterID::F31_F);
+ const rcc::PhysicalRegister FFLAGS = registerID(rcc::riscv::PhysicalRegisterID::FFLAGS);
+ const rcc::PhysicalRegister FRM = registerID(rcc::riscv::PhysicalRegisterID::FRM);
+ const rcc::PhysicalRegister V8 = registerID(rcc::riscv::PhysicalRegisterID::V8);
+
+ Context.expect(
+ Registers->physicalRegisters().size() == 326 && Registers->registerUnits().size() == 98 &&
+ Registers->registerClasses().size() == 18 && Registers->registerBanks().size() == 4,
+ "RISC-V must expose scalar, vector, restricted, and status physical registers");
+ Context.expect(Registers->physicalRegister("x0") != nullptr &&
+ Registers->physicalRegister("zero") == nullptr &&
+ Registers->physicalRegisterByAlternativeName("ZERO") != nullptr &&
+ Registers->physicalRegisterByAlternativeName("zero")->Reg == X0 &&
+ Registers->physicalRegisterByAlternativeName("Sp") != nullptr &&
+ Registers->physicalRegisterByAlternativeName("sp")->Reg == X2 &&
+ Registers->physicalRegisterMIRName(X8) == "x8",
+ "canonical MIR names and case-insensitive ABI aliases must use separate lookups");
+ Context.expect(
+ Registers->isReservedRegister(X0) && Registers->isReservedRegister(X2) &&
+ !Registers->isReservedRegister(X5),
+ "zero, stack, global, and thread register constraints must be recorded statically");
+ Context.expect(Registers->physicalRegister(F31D) != nullptr &&
+ Registers->physicalRegister(F31D)->Encoding == 31 &&
+ Registers->physicalRegister("f31_d") != nullptr &&
+ Registers->physicalRegister("f31_d")->Reg == F31D &&
+ Registers->physicalRegister("f31_f") != nullptr &&
+ Registers->physicalRegister("f31_f")->Reg == F31F &&
+ Registers->physicalRegisterMIRName(F31D) == "f31_d" &&
+ Registers->physicalRegisterMIRName(F31F) == "f31_f" &&
+ Registers->regsOverlap(F31D, F31F) &&
+ Registers->subRegister(F31D, rcc::SubRegIndex::fromValue(1)) == F31F,
+ "FPR32 and FPR64 MIR views must retain encoding, overlap, and subregister facts");
+ Context.expect(Registers->physicalRegister("v8") != nullptr &&
+ Registers->physicalRegister("v8")->Reg == V8 &&
+ Registers->physicalRegister("v8")->Encoding == 8 &&
+ Registers->physicalRegisterMIRName(V8) == "v8" &&
+ Registers->registerClass("vr") != nullptr &&
+ !Registers->registerClass("vr")->Allocatable &&
+ Registers->registerClassContains(Registers->registerClass("vr")->ID, V8),
+ "vector physical registers must retain LLVM MIR names and encodings");
+ const auto *V8V9 = Registers->physicalRegister("v8_v9");
+ const auto *V28V29V30 = Registers->physicalRegister("v28_v29_v30");
+ const auto *V0ThroughV7 = Registers->physicalRegister("v0_v1_v2_v3_v4_v5_v6_v7");
+ Context.expect(V8V9 != nullptr && V28V29V30 != nullptr && V0ThroughV7 != nullptr &&
+ Registers->regsOverlap(V8V9->Reg, V8) &&
+ !Registers->regsOverlap(V28V29V30->Reg, V8) &&
+ Registers->regsOverlap(V0ThroughV7->Reg,
+ registerID(rcc::riscv::PhysicalRegisterID::V7)) &&
+ Registers->registerClass("vrtuple2") != nullptr &&
+ !Registers->registerClass("vrtuple2")->Allocatable,
+ "vector tuple registers must expose canonical names and physical overlap");
+ Context.expect(Registers->physicalRegister("fflags") != nullptr &&
+ Registers->physicalRegister("fflags")->Reg == FFLAGS &&
+ Registers->isReservedRegister(FFLAGS) &&
+ Registers->registerClass("fflags") != nullptr &&
+ !Registers->registerClass("fflags")->Allocatable,
+ "FFLAGS must be a reserved nonallocatable status register");
+ Context.expect(Registers->physicalRegister("frm") != nullptr &&
+ Registers->physicalRegister("frm")->Reg == FRM &&
+ Registers->isReservedRegister(FRM) &&
+ Registers->registerClass("frm") != nullptr &&
+ !Registers->registerClass("frm")->Allocatable,
+ "FRM must be a reserved nonallocatable status register");
+
+ const auto *GPR = Registers->registerClass("gpr");
+ Context.expect(
+ GPR != nullptr && std::ranges::find(GPR->AllocationOrder, X0) == GPR->AllocationOrder.end() &&
+ std::ranges::find(GPR->AllocationOrder, X2) == GPR->AllocationOrder.end() &&
+ std::ranges::find(GPR->AllocationOrder, X5) != GPR->AllocationOrder.end(),
+ "the general-register allocation order must exclude statically reserved registers");
+ Context.expect(Registers->registerClass("gpr_nx0") != nullptr &&
+ Registers->registerClass("gpr_nx0_nx2") != nullptr,
+ "compressed instructions must have explicit nonzero register classes");
+}
+
+void testABIAndDebugFacts(TestContext &Context) {
+ const rcc::PhysicalRegister X1 = registerID(rcc::riscv::PhysicalRegisterID::X1);
+ const rcc::PhysicalRegister X10 = registerID(rcc::riscv::PhysicalRegisterID::X10);
+ const rcc::PhysicalRegister F8D = registerID(rcc::riscv::PhysicalRegisterID::F8_D);
+ const rcc::PhysicalRegister F8F = registerID(rcc::riscv::PhysicalRegisterID::F8_F);
+ const rcc::PhysicalRegister F10D = registerID(rcc::riscv::PhysicalRegisterID::F10_D);
+ const rcc::PhysicalRegister F10F = registerID(rcc::riscv::PhysicalRegisterID::F10_F);
+ const rcc::PhysicalRegister F31D = registerID(rcc::riscv::PhysicalRegisterID::F31_D);
+ const rcc::PhysicalRegister F31F = registerID(rcc::riscv::PhysicalRegisterID::F31_F);
+
+ const auto LP64Mask = rcc::riscv::createCallPreservedMask(rcc::riscv::CallingConvention::LP64);
+ const auto LP64DMask = rcc::riscv::createCallPreservedMask(rcc::riscv::CallingConvention::LP64D);
+ Context.expect(
+ LP64DMask != nullptr && LP64DMask->preserves(X1) && LP64DMask->preserves(F8D) &&
+ LP64DMask->preserves(F8F) && LP64DMask->clobbers(X10) && LP64DMask->clobbers(F10D) &&
+ LP64DMask->clobbers(F10F) && LP64Mask != nullptr && LP64Mask->preserves(X1) &&
+ LP64Mask->clobbers(F8D) && LP64Mask->clobbers(F8F) && LP64Mask->clobbers(X10),
+ "LP64 and LP64D masks must distinguish integer and floating callee-saved registers");
+
+ auto MIRText = rcc::riscv::createMIRTextInfo();
+ Context.expect(MIRText.has_value(), "RV64GC MIR text facts must be valid");
+ if (!MIRText)
+ return;
+ const auto TextLP64Mask = MIRText->registerMask("csr_ilp32_lp64");
+ const auto TextLP64DMask = MIRText->registerMask("csr_ilp32d_lp64d");
+ Context.expect(MIRText->registerMasks().size() == 2 && TextLP64Mask != nullptr &&
+ LP64Mask != nullptr && *TextLP64Mask == *LP64Mask &&
+ TextLP64DMask != nullptr && LP64DMask != nullptr &&
+ *TextLP64DMask == *LP64DMask,
+ "RV64GC must expose named LP64 and LP64D call-preserved masks");
+ Context.expect(MIRText->directOperandFlagMask() == 127 &&
+ MIRText->directOperandFlag("riscv-call") == 1 &&
+ MIRText->directOperandFlag("riscv-hi") == 32 &&
+ MIRText->directOperandFlag("riscv-lo") == 64,
+ "RV64GC MIR target flags must expose canonical call and address spellings");
+ Context.expect(MIRText->dwarfRegister(X1) == 1,
+ "the return-address register must retain its DWARF register number");
+ Context.expect(MIRText->dwarfRegister(F31D) == 63 && MIRText->dwarfRegister(F31F) == 63 &&
+ MIRText->physicalRegister(63) == F31D,
+ "typed floating views must share one canonical DWARF register mapping");
+
+ const auto Conventions = rcc::riscv::callingConventions();
+ const auto *LP64 = rcc::riscv::callingConvention(rcc::riscv::CallingConvention::LP64);
+ const auto *LP64D = rcc::riscv::callingConvention(rcc::riscv::CallingConvention::LP64D);
+ Context.expect(
+ Conventions.size() == 2 && LP64 != nullptr && LP64->Name == "lp64" &&
+ LP64->IntegerArgumentRegisters.size() == 8 && LP64->FloatingArgumentRegisters.empty() &&
+ LP64->IntegerReturnRegisters.size() == 2 && LP64->FloatingReturnRegisters.empty() &&
+ LP64->PreservedMaskName == "CSR_ILP32_LP64" && LP64D != nullptr &&
+ LP64D->Name == "lp64d" && LP64D->IntegerArgumentRegisters.size() == 8 &&
+ LP64D->FloatingArgumentRegisters.size() == 8 &&
+ LP64D->IntegerReturnRegisters.size() == 2 && LP64D->FloatingReturnRegisters.size() == 2 &&
+ LP64D->StackAlignmentBytes == 16 && LP64D->StackArgumentAlignmentBytes == 16 &&
+ LP64D->SupportsVarArgs && LP64D->SupportsTailCalls &&
+ LP64D->PreservedMaskName == "CSR_ILP32D_LP64D",
+ "LP64 and LP64D calling-convention tables must expose complete ABI facts");
+}
+
+void testInstructionFacts(TestContext &Context) {
+ constexpr std::size_t TargetInstructionCount =
+ static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::MAXU) -
+ static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::LUI) + 1U;
+ auto Target = rcc::riscv::createTargetDescription();
+ Context.expect(Target.has_value(), "RV64GC target description must be structurally valid");
+ if (!Target) {
+ std::cerr << "target construction error: " << Target.error().message() << '\n';
+ return;
+ }
+
+ const auto *Addi = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::ADDI)));
+ const auto *Load = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::LD)));
+ const auto *Branch = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::BEQ)));
+ const auto *Floating = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::FADD_S)));
+ const auto *Atomic = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::LR_W)));
+ const auto *ShiftAdd = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::SH1ADD)));
+ const auto *ShiftLeftUnsignedWord = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::SLLI_UW)));
+ const auto *SignExtendByte = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::SEXT_B)));
+ const auto *SignedMinimum = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::MIN)));
+ const auto *Compressed = Target->instructionDescriptor(rcc::Opcode::fromValue(
+ static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::C_ADDI16SP)));
+ const auto *GenericAdd = Target->instructionDescriptor(rcc::opcode(rcc::GenericOpcode::G_ADD));
+ const auto *PseudoCall = Target->instructionDescriptor(rcc::Opcode::fromValue(
+ static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::PseudoCALL)));
+ const auto *PseudoTail = Target->instructionDescriptor(rcc::Opcode::fromValue(
+ static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::PseudoTAIL)));
+ const auto *Assembly = rcc::riscv::assemblyInstruction(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::ADDI)));
+ const auto *FenceAssembly = rcc::riscv::assemblyInstruction(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::FENCE_I)));
+
+ constexpr std::array SymbolicLowOpcodes = {
+ rcc::riscv::InstructionOpcode::LB, rcc::riscv::InstructionOpcode::LH,
+ rcc::riscv::InstructionOpcode::LW, rcc::riscv::InstructionOpcode::LBU,
+ rcc::riscv::InstructionOpcode::LHU, rcc::riscv::InstructionOpcode::LWU,
+ rcc::riscv::InstructionOpcode::SB, rcc::riscv::InstructionOpcode::SH,
+ rcc::riscv::InstructionOpcode::SW, rcc::riscv::InstructionOpcode::LD,
+ rcc::riscv::InstructionOpcode::SD, rcc::riscv::InstructionOpcode::FLW,
+ rcc::riscv::InstructionOpcode::FSW, rcc::riscv::InstructionOpcode::FLD,
+ rcc::riscv::InstructionOpcode::FSD,
+ };
+ const rcc::OperandKindSet SymbolicLowKinds = {
+ rcc::MachineOperand::Kind::Immediate, rcc::MachineOperand::Kind::ConstantPoolIndex,
+ rcc::MachineOperand::Kind::JumpTableIndex, rcc::MachineOperand::Kind::GlobalSymbol,
+ rcc::MachineOperand::Kind::ExternalSymbol, rcc::MachineOperand::Kind::BlockAddress,
+ rcc::MachineOperand::Kind::MCSymbol,
+ };
+ Context.expect(
+ Load != nullptr && Load->Operands.size() == 3 &&
+ std::ranges::all_of(SymbolicLowOpcodes,
+ [&](rcc::riscv::InstructionOpcode Opcode) {
+ const auto *Descriptor = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(Opcode)));
+ return Descriptor != nullptr && Descriptor->Operands.size() == 3 &&
+ Descriptor->Operands[2].Kinds == SymbolicLowKinds &&
+ Descriptor->Operands[2].ImmediatePredicate ==
+ Load->Operands[2].ImmediatePredicate &&
+ Descriptor->Operands[2].AllowedTargetFlags == 68;
+ }),
+ "RV64GC loads and stores must accept SIMM12 low-part symbolic operands");
+
+ Context.expect(
+ Target->instructions().descriptors().size() ==
+ TargetInstructionCount + rcc::genericInstructionDescriptors().size() &&
+ Addi != nullptr && Addi->Name == "ADDI" && Addi->NumExplicitDefs == 1 &&
+ Addi->Operands.size() == 3 &&
+ Addi->Operands[1].Kinds.contains(rcc::MachineOperand::Kind::FrameIndex) &&
+ Addi->Operands.back().ImmediatePredicate && Load != nullptr &&
+ Load->Flags.has(rcc::InstructionFlag::MayLoad) && !Load->RequiresMemoryOperands &&
+ Branch != nullptr && Branch->Flags.has(rcc::InstructionFlag::Terminator) &&
+ Branch->Flags.has(rcc::InstructionFlag::Branch) && Floating != nullptr &&
+ Floating->Operands.size() == 4 && Floating->Operands.back().ImmediatePredicate &&
+ Floating->AllowExtraImplicitOperands && Atomic != nullptr &&
+ Atomic->Flags.has(rcc::InstructionFlag::MayLoad) && !Atomic->RequiresMemoryOperands &&
+ Compressed != nullptr && Compressed->ImplicitDefs.size() == 1 &&
+ Compressed->ImplicitUses.size() == 1 && GenericAdd != nullptr &&
+ GenericAdd->Flags.has(rcc::InstructionFlag::Generic) && PseudoCall != nullptr &&
+ PseudoCall->Operands.size() == 2 && PseudoCall->Operands[1].Optional &&
+ PseudoTail != nullptr && PseudoTail->Operands.size() == 2 &&
+ PseudoTail->Operands[1].Optional && PseudoCall->Flags.has(rcc::InstructionFlag::Pseudo) &&
+ PseudoCall->Flags.has(rcc::InstructionFlag::Call) &&
+ rcc::riscv::assemblyInstruction(PseudoCall->InstructionOpcode) == nullptr &&
+ Assembly != nullptr && Assembly->Mnemonic == "addi" &&
+ Assembly->Format == rcc::AssemblyFormatID::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::AssemblyFormat::RD_RS1_IMM)),
+ "generated instruction and assembly descriptors must drive the formal RV64GC target");
+ Context.expect(
+ Load != nullptr &&
+ Target->instructions()
+ .validateMemOperands(Load->InstructionOpcode,
+ std::span<const rcc::MachineMemOperand *const>{})
+ .has_value() &&
+ !Target->instructions().validateMemOperands(
+ rcc::opcode(rcc::GenericOpcode::G_LOAD),
+ std::span<const rcc::MachineMemOperand *const>{}),
+ "target memory operands must be optional while generic memory operands remain required");
+ Context.expect(ShiftAdd != nullptr && ShiftAdd->Operands.size() == 3 &&
+ ShiftLeftUnsignedWord != nullptr &&
+ ShiftLeftUnsignedWord->Operands.size() == 3 &&
+ ShiftLeftUnsignedWord->Operands.back().ImmediatePredicate &&
+ SignExtendByte != nullptr && SignExtendByte->Operands.size() == 2 &&
+ SignedMinimum != nullptr && SignedMinimum->Operands.size() == 3 &&
+ SignedMinimum->Flags.has(rcc::InstructionFlag::Commutable),
+ "RISC-V Zba and Zbb instructions must expose complete generated constraints");
+
+ const auto Schedules = rcc::riscv::schedulingClasses();
+ const auto Encodings = rcc::riscv::encodingCategories();
+ const auto Formats = rcc::riscv::assemblyFormats();
+ Context.expect(Schedules.size() == 15 && Encodings.size() == 10 && Formats.size() == 35 &&
+ rcc::riscv::assemblyInstructions().size() == 211 && FenceAssembly != nullptr &&
+ FenceAssembly->Mnemonic == "fence.i" &&
+ std::ranges::any_of(Schedules,
+ [](const rcc::riscv::SchedulingClassInfo &Class) {
+ return Class.Name == "load" && Class.MicroOps == 1 &&
+ Class.Latency == 3;
+ }) &&
+ std::ranges::any_of(Encodings,
+ [](const rcc::riscv::EncodingCategoryInfo &Category) {
+ return Category.Name == "i" && Category.SizeInBytes == 4;
+ }) &&
+ std::ranges::any_of(Formats,
+ [](const rcc::riscv::AssemblyFormatInfo &Format) {
+ return Format.Name == "rd-rs1-imm";
+ }),
+ "generated scheduling, encoding, and assembly tables must preserve target facts");
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ testRegisterFacts(Context);
+ testABIAndDebugFacts(Context);
+ testInstructionFacts(Context);
+ return Context.result();
+}
diff --git a/backend/unittests/Target/TableGenTest.cpp b/backend/unittests/Target/TableGenTest.cpp
new file mode 100644
index 0000000..af4d718
--- /dev/null
+++ b/backend/unittests/Target/TableGenTest.cpp
@@ -0,0 +1,202 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "TableGenBackends.h"
+
+#include <cstddef>
+#include <iostream>
+#include <string>
+#include <string_view>
+
+namespace {
+
+class TestContext final {
+public:
+ void expect(bool Condition, std::string_view Message) {
+ if (Condition)
+ return;
+ std::cerr << Message << '\n';
+ ++FailureCount;
+ }
+
+ [[nodiscard]] int result() const noexcept {
+ return FailureCount == 0 ? 0 : 1;
+ }
+
+private:
+ int FailureCount = 0;
+};
+
+[[nodiscard]] std::string validDescription(std::string_view Aliases = "[acc]") {
+ return std::string(R"(schema: rcc-target-description-v2
+target:
+ namespace: tiny
+ name: Tiny
+ triple: tiny-unknown-none
+ cpu: tiny-cpu
+ abi: tiny-abi
+ data_layout: e-p:64:64
+ features: [base]
+register_units:
+ - { id: 1, symbol: U_R1, name: r1.unit }
+registers:
+ - { id: 1, symbol: R1, name: r1, encoding: 0, dwarf_register: 0, units: [{ unit: U_R1, lanes: 1 }], aliases: )") +
+ std::string(Aliases) + R"( }
+register_banks:
+ - { id: 1, symbol: GPR, name: GPR, maximum_size_bits: 64 }
+register_classes:
+ - { id: 1, symbol: GPR64, name: GPR64, bank: GPR, size_bits: 64, spill_size_bits: 64, spill_alignment_bits: 64, lanes: 1, registers: [R1], allocation_order: [R1] }
+register_masks:
+ - { name: CSR_TINY, preserved: [R1] }
+mir_operand_flags:
+ direct_mask: 31
+ direct: [{ name: tiny-call, value: 1 }]
+ bitmask: [{ name: tiny-relax, value: 32 }]
+calling_conventions:
+ - { id: 1, symbol: TINY, name: tiny, integer_register_class: GPR64, floating_register_class: GPR64, integer_argument_registers: [R1], floating_argument_registers: [R1], integer_return_registers: [R1], floating_return_registers: [R1], preserved_mask: CSR_TINY, stack_alignment_bytes: 8, stack_argument_alignment_bytes: 8, shadow_space_bytes: 0, supports_varargs: true, supports_tail_calls: true }
+named_operands:
+ - { id: 1, symbol: RD, name: rd }
+ - { id: 2, symbol: IMM, name: imm }
+immediate_predicates:
+ - { id: 1, symbol: SIMM8, name: simm8, kind: signed, bits: 8 }
+scheduling_classes:
+ - { id: 1, symbol: ALU, name: alu, micro_ops: 1, latency: 1 }
+encoding_categories:
+ - { id: 1, symbol: I, name: i, size_bytes: 4 }
+assembly_formats:
+ - { id: 1, symbol: RD_IMM, name: rd-imm }
+instructions:
+ - { id: 1024, symbol: LOAD_IMM, name: LOAD_IMM, assembly_mnemonic: load.imm, encoding: 19, operands: [{ kinds: [register], role: def, register_class: GPR64, register_bank: GPR, name: RD }, { kinds: [immediate, global_symbol], immediate_predicate: SIMM8, name: IMM, allowed_target_flags: 33 }], num_explicit_defs: 1, flags: [produces_code], scheduling_class: ALU, encoding_category: I, assembly_format: RD_IMM }
+)";
+}
+
+void testGeneration(TestContext &Context) {
+ const auto First = rcc::targetgen::generateDescription("tiny.rccdesc", validDescription());
+ const auto Second = rcc::targetgen::generateDescription("tiny.rccdesc", validDescription());
+ if (!First.succeeded()) {
+ for (const rcc::TextDiagnostic &Diagnostic : First.Diagnostics)
+ std::cerr << Diagnostic.Message << '\n';
+ }
+ Context.expect(First.succeeded() && Second.succeeded(),
+ "a complete restricted target description must generate successfully");
+ if (!First.succeeded() || !Second.succeeded() || !First.Output || !Second.Output)
+ return;
+ const auto &FirstOutput = First.Output.value();
+ const auto &SecondOutput = Second.Output.value();
+
+ Context.expect(FirstOutput.RegisterHeader == SecondOutput.RegisterHeader &&
+ FirstOutput.InstructionHeader == SecondOutput.InstructionHeader &&
+ FirstOutput.TargetTables == SecondOutput.TargetTables,
+ "target description generation must be deterministic");
+ Context.expect(
+ FirstOutput.RegisterHeader.find("enum class PhysicalRegisterID") != std::string::npos &&
+ FirstOutput.RegisterHeader.find("R1 = 1") != std::string::npos &&
+ FirstOutput.InstructionHeader.find("enum class InstructionOpcode") != std::string::npos &&
+ FirstOutput.InstructionHeader.find("LOAD_IMM = 1024") != std::string::npos &&
+ FirstOutput.TargetTables.find("appendGeneratedRegisterInfo") != std::string::npos &&
+ FirstOutput.TargetTables.find("appendGeneratedInstructionInfo") != std::string::npos &&
+ FirstOutput.TargetTables.find("GeneratedAssemblyInstructions") != std::string::npos &&
+ FirstOutput.TargetTables.find("Description.DirectOperandFlagMask = 31") !=
+ std::string::npos &&
+ FirstOutput.TargetTables.find("\"tiny-call\", 1") != std::string::npos &&
+ FirstOutput.TargetTables.find("\"load.imm\"") != std::string::npos &&
+ FirstOutput.TargetTables.find("makePreservedMask") != std::string::npos,
+ "generated output must contain typed target IDs, assembly mnemonics, and static target "
+ "tables");
+}
+
+void testSemanticRejection(TestContext &Context) {
+ const auto CollidingAlias =
+ rcc::targetgen::generateDescription("bad-alias.rccdesc", validDescription("[r1]"));
+ if (CollidingAlias.succeeded())
+ std::cerr << "colliding alias description unexpectedly generated\n";
+ Context.expect(!CollidingAlias.succeeded() && !CollidingAlias.Diagnostics.empty() &&
+ CollidingAlias.Diagnostics.front().Message.find("aliases") !=
+ std::string::npos,
+ "an ABI alias that collides with a canonical name must be rejected");
+
+ std::string UnknownUnit = validDescription();
+ constexpr std::string_view UnitReference = "unit: U_R1";
+ const std::size_t UnitPosition = UnknownUnit.find(UnitReference);
+ if (UnitPosition != std::string::npos)
+ UnknownUnit.replace(UnitPosition, UnitReference.size(), "unit: U_MISSING");
+ const auto BadUnit = rcc::targetgen::generateDescription("bad-unit.rccdesc", UnknownUnit);
+ Context.expect(!BadUnit.succeeded() && !BadUnit.Diagnostics.empty() &&
+ BadUnit.Diagnostics.front().Message.find("declared units") !=
+ std::string::npos,
+ "a register unit reference must resolve during semantic validation");
+
+ std::string UnknownClass = validDescription();
+ constexpr std::string_view ClassReference = "register_class: GPR64";
+ const std::size_t ClassPosition = UnknownClass.find(ClassReference);
+ if (ClassPosition != std::string::npos)
+ UnknownClass.replace(ClassPosition, ClassReference.size(), "register_class: MISSING");
+ const auto BadClass =
+ rcc::targetgen::generateDescription("bad-instruction-class.rccdesc", UnknownClass);
+ Context.expect(!BadClass.succeeded() && !BadClass.Diagnostics.empty() &&
+ BadClass.Diagnostics.front().Message.find("register class") !=
+ std::string::npos,
+ "instruction register classes must resolve during semantic validation");
+
+ std::string BadAllowedValue = validDescription();
+ constexpr std::string_view Predicate = "kind: signed, bits: 8";
+ const std::size_t PredicatePosition = BadAllowedValue.find(Predicate);
+ if (PredicatePosition != std::string::npos) {
+ BadAllowedValue.replace(PredicatePosition, Predicate.size(),
+ "kind: signed, bits: 8, allowed_values: [128]");
+ }
+ const auto InvalidAllowedValue =
+ rcc::targetgen::generateDescription("bad-allowed-value.rccdesc", BadAllowedValue);
+ Context.expect(!InvalidAllowedValue.succeeded() && !InvalidAllowedValue.Diagnostics.empty() &&
+ InvalidAllowedValue.Diagnostics.front().Message.find("allowed_values") !=
+ std::string::npos,
+ "immediate predicates must reject allowed values outside their signed range");
+
+ std::string ContradictoryNonZero = validDescription();
+ if (PredicatePosition != std::string::npos) {
+ const std::size_t Position = ContradictoryNonZero.find(Predicate);
+ ContradictoryNonZero.replace(Position, Predicate.size(),
+ "kind: signed, bits: 8, nonzero: true, allowed_values: [0]");
+ }
+ const auto InvalidNonZero =
+ rcc::targetgen::generateDescription("bad-nonzero.rccdesc", ContradictoryNonZero);
+ Context.expect(!InvalidNonZero.succeeded() && !InvalidNonZero.Diagnostics.empty() &&
+ InvalidNonZero.Diagnostics.front().Message.find("allowed_values") !=
+ std::string::npos,
+ "nonzero immediate predicates must reject an explicit zero value");
+
+ std::string BadStackAlignment = validDescription();
+ constexpr std::string_view StackAlignment =
+ "stack_alignment_bytes: 8, stack_argument_alignment_bytes: 8";
+ const std::size_t StackPosition = BadStackAlignment.find(StackAlignment);
+ if (StackPosition != std::string::npos) {
+ BadStackAlignment.replace(StackPosition, StackAlignment.size(),
+ "stack_alignment_bytes: 8, stack_argument_alignment_bytes: 16");
+ }
+ const auto InvalidStackAlignment =
+ rcc::targetgen::generateDescription("bad-stack-alignment.rccdesc", BadStackAlignment);
+ Context.expect(!InvalidStackAlignment.succeeded() && !InvalidStackAlignment.Diagnostics.empty() &&
+ InvalidStackAlignment.Diagnostics.front().Message.find("stack alignments") !=
+ std::string::npos,
+ "calling conventions must reject incompatible stack alignments");
+
+ std::string BadMnemonic = validDescription();
+ constexpr std::string_view Mnemonic = "assembly_mnemonic: load.imm";
+ const std::size_t MnemonicPosition = BadMnemonic.find(Mnemonic);
+ if (MnemonicPosition != std::string::npos)
+ BadMnemonic.replace(MnemonicPosition, Mnemonic.size(), "assembly_mnemonic: .load");
+ const auto InvalidMnemonic =
+ rcc::targetgen::generateDescription("bad-mnemonic.rccdesc", BadMnemonic);
+ Context.expect(!InvalidMnemonic.succeeded() && !InvalidMnemonic.Diagnostics.empty() &&
+ InvalidMnemonic.Diagnostics.front().Message.find("assembly mnemonics") !=
+ std::string::npos,
+ "assembly mnemonics must use a canonical lowercase spelling");
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ testGeneration(Context);
+ testSemanticRejection(Context);
+ return Context.result();
+}
diff --git a/backend/unittests/Target/TestTarget.cpp b/backend/unittests/Target/TestTarget.cpp
new file mode 100644
index 0000000..a367454
--- /dev/null
+++ b/backend/unittests/Target/TestTarget.cpp
@@ -0,0 +1,217 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "Target/TestTarget.h"
+
+#include <cstdint>
+#include <memory>
+#include <utility>
+#include <vector>
+
+namespace rcc::test {
+namespace {
+
+[[nodiscard]] PhysicalRegister phys(std::uint32_t Number) {
+ return *PhysicalRegister::create(Number);
+}
+
+[[nodiscard]] RegisterUnitDesc unit(std::uint32_t ID, const char *Name) {
+ return RegisterUnitDesc{RegisterUnitID::fromValue(ID), Name};
+}
+
+[[nodiscard]] RegisterUnitLane unitLane(std::uint32_t ID, std::uint64_t Lanes) {
+ return RegisterUnitLane{RegisterUnitID::fromValue(ID), LaneMask::fromRaw(Lanes)};
+}
+
+[[nodiscard]] PhysicalRegisterDesc physicalRegister(std::uint32_t Number, const char *Name,
+ std::uint32_t Encoding,
+ std::vector<RegisterUnitLane> Units) {
+ return PhysicalRegisterDesc{phys(Number), Name, Encoding, std::move(Units),
+ false, false, false, {}};
+}
+
+[[nodiscard]] RegisterClassDesc
+registerClass(RegisterClassID ID, const char *Name, std::vector<PhysicalRegister> Registers,
+ std::vector<PhysicalRegister> AllocationOrder, std::uint32_t SizeInBits,
+ LaneMask Lanes, RegisterBankID DefaultBank,
+ std::vector<SubRegIndex> SupportedSubRegs = {},
+ std::vector<RegisterClassID> SuperClasses = {}, bool Allocatable = true,
+ bool CoveredBySubRegs = false) {
+ return RegisterClassDesc{
+ ID,
+ Name,
+ std::move(Registers),
+ std::move(AllocationOrder),
+ SizeInBits,
+ SizeInBits,
+ SizeInBits,
+ Lanes,
+ std::move(SupportedSubRegs),
+ std::move(SuperClasses),
+ DefaultBank,
+ Allocatable,
+ 1,
+ CoveredBySubRegs,
+ };
+}
+
+} // namespace
+
+TargetRegisterInfoDesc createTestTargetRegisterInfoDescription() {
+ TargetRegisterInfoDesc Description;
+ Description.RegisterUnits = {
+ unit(1, "g0.lo16"), unit(2, "g0.bits16_31"), unit(3, "g0.bits32_47"), unit(4, "g0.hi16"),
+ unit(5, "g1.lo16"), unit(6, "g1.bits16_31"), unit(7, "g1.bits32_47"), unit(8, "g1.hi16"),
+ unit(9, "sp"), unit(10, "f0"), unit(11, "f1"),
+ };
+
+ Description.PhysicalRegisters = {
+ physicalRegister(TestRegister::G0, "g0", 0,
+ {unitLane(1, 0x1), unitLane(2, 0x2), unitLane(3, 0x4), unitLane(4, 0x8)}),
+ physicalRegister(TestRegister::G0Low32, "g0l", 0, {unitLane(1, 0x1), unitLane(2, 0x2)}),
+ physicalRegister(TestRegister::G0High32, "g0h", 0, {unitLane(3, 0x1), unitLane(4, 0x2)}),
+ physicalRegister(TestRegister::G0Low16, "g0w", 0, {unitLane(1, 0x1)}),
+ physicalRegister(TestRegister::G1, "g1", 1,
+ {unitLane(5, 0x1), unitLane(6, 0x2), unitLane(7, 0x4), unitLane(8, 0x8)}),
+ physicalRegister(TestRegister::G1Low32, "g1l", 1, {unitLane(5, 0x1), unitLane(6, 0x2)}),
+ physicalRegister(TestRegister::G1High32, "g1h", 1, {unitLane(7, 0x1), unitLane(8, 0x2)}),
+ physicalRegister(TestRegister::G1Low16, "g1w", 1, {unitLane(5, 0x1)}),
+ physicalRegister(TestRegister::StackPointer, "sp", 2, {unitLane(9, 0xF)}),
+ physicalRegister(TestRegister::F0, "f0", 0, {unitLane(10, 0x1)}),
+ physicalRegister(TestRegister::F1, "f1", 1, {unitLane(11, 0x1)}),
+ };
+
+ Description.SubRegIndices = {
+ SubRegIndexDesc{TestSubReg::Low32, "low32", LaneMask::fromRaw(0x3), 32, 0},
+ SubRegIndexDesc{TestSubReg::High32, "high32", LaneMask::fromRaw(0xC), 32, 32},
+ SubRegIndexDesc{TestSubReg::Low16, "low16", LaneMask::fromRaw(0x1), 16, 0},
+ };
+ Description.PhysicalSubRegisters = {
+ {phys(TestRegister::G0), TestSubReg::Low32, phys(TestRegister::G0Low32)},
+ {phys(TestRegister::G0), TestSubReg::High32, phys(TestRegister::G0High32)},
+ {phys(TestRegister::G0), TestSubReg::Low16, phys(TestRegister::G0Low16)},
+ {phys(TestRegister::G0Low32), TestSubReg::Low16, phys(TestRegister::G0Low16)},
+ {phys(TestRegister::G1), TestSubReg::Low32, phys(TestRegister::G1Low32)},
+ {phys(TestRegister::G1), TestSubReg::High32, phys(TestRegister::G1High32)},
+ {phys(TestRegister::G1), TestSubReg::Low16, phys(TestRegister::G1Low16)},
+ {phys(TestRegister::G1Low32), TestSubReg::Low16, phys(TestRegister::G1Low16)},
+ };
+ Description.SubRegCompositions = {
+ {TestSubReg::Low32, TestSubReg::Low16, TestSubReg::Low16},
+ };
+
+ const auto G0 = phys(TestRegister::G0);
+ const auto G0Low32 = phys(TestRegister::G0Low32);
+ const auto G0High32 = phys(TestRegister::G0High32);
+ const auto G0Low16 = phys(TestRegister::G0Low16);
+ const auto G1 = phys(TestRegister::G1);
+ const auto G1Low32 = phys(TestRegister::G1Low32);
+ const auto G1High32 = phys(TestRegister::G1High32);
+ const auto G1Low16 = phys(TestRegister::G1Low16);
+ const auto StackPointer = phys(TestRegister::StackPointer);
+ const auto F0 = phys(TestRegister::F0);
+ const auto F1 = phys(TestRegister::F1);
+
+ Description.RegisterClasses = {
+ registerClass(TestRegisterClass::GPR64, "GPR64", {G0, G1}, {G0, G1}, 64,
+ LaneMask::fromRaw(0xF), TestRegisterBank::GPR,
+ {TestSubReg::Low32, TestSubReg::High32, TestSubReg::Low16}, {}, true, true),
+ registerClass(TestRegisterClass::GPR64Alloc, "GPR64Alloc", {G0, G1}, {G1, G0}, 64,
+ LaneMask::fromRaw(0xF), TestRegisterBank::GPR, {}, {TestRegisterClass::GPR64}),
+ registerClass(TestRegisterClass::GPR32, "GPR32", {G0Low32, G0High32, G1Low32, G1High32},
+ {G0Low32, G1Low32, G0High32, G1High32}, 32, LaneMask::fromRaw(0x3),
+ TestRegisterBank::GPR),
+ registerClass(TestRegisterClass::GPR32Low, "GPR32Low", {G0Low32, G1Low32}, {G0Low32, G1Low32},
+ 32, LaneMask::fromRaw(0x3), TestRegisterBank::GPR, {TestSubReg::Low16},
+ {TestRegisterClass::GPR32}),
+ registerClass(TestRegisterClass::GPR32High, "GPR32High", {G0High32, G1High32},
+ {G0High32, G1High32}, 32, LaneMask::fromRaw(0x3), TestRegisterBank::GPR, {},
+ {TestRegisterClass::GPR32}),
+ registerClass(TestRegisterClass::GPR16, "GPR16", {G0Low16, G1Low16}, {G0Low16, G1Low16}, 16,
+ LaneMask::fromRaw(0x1), TestRegisterBank::GPR),
+ registerClass(TestRegisterClass::FPR64, "FPR64", {F0, F1}, {F0, F1}, 64,
+ LaneMask::fromRaw(0x1), TestRegisterBank::FPR),
+ registerClass(TestRegisterClass::SpecialGPR64, "SpecialGPR64", {StackPointer}, {}, 64,
+ LaneMask::fromRaw(0xF), TestRegisterBank::GPR, {}, {}, false),
+ };
+
+ Description.SubRegClassMappings = {
+ {TestRegisterClass::GPR64, TestSubReg::Low32, TestRegisterClass::GPR32Low},
+ {TestRegisterClass::GPR64, TestSubReg::High32, TestRegisterClass::GPR32High},
+ {TestRegisterClass::GPR64, TestSubReg::Low16, TestRegisterClass::GPR16},
+ {TestRegisterClass::GPR32Low, TestSubReg::Low16, TestRegisterClass::GPR16},
+ };
+ Description.RegisterBanks = {
+ {TestRegisterBank::GPR,
+ "GPR",
+ 64,
+ {TestRegisterClass::GPR64, TestRegisterClass::GPR64Alloc, TestRegisterClass::GPR32,
+ TestRegisterClass::GPR32Low, TestRegisterClass::GPR32High, TestRegisterClass::GPR16,
+ TestRegisterClass::SpecialGPR64}},
+ {TestRegisterBank::FPR, "FPR", 64, {TestRegisterClass::FPR64}},
+ };
+
+ return Description;
+}
+
+Expected<TargetRegisterInfo> createTestTargetRegisterInfo() {
+ return TargetRegisterInfo::create(createTestTargetRegisterInfoDescription());
+}
+
+TargetMIRTextInfoDesc createTestTargetMIRTextInfoDescription() {
+ TargetMIRTextInfoDesc Description;
+ Description.RegisterMasks = {
+ {"Test-Preserved", createTestCallPreservedMask()},
+ };
+ Description.TargetIndices = {
+ {"test-index", TargetIndex::fromValue(0)},
+ };
+ Description.DirectOperandFlagMask = 0x3;
+ Description.DirectOperandFlags = {
+ {"test-direct-one", 0x1},
+ {"test-direct-two", 0x2},
+ {"test-direct-three", 0x3},
+ };
+ Description.BitmaskOperandFlags = {
+ {"test-bit-four", 0x4},
+ {"test-bit-eight", 0x8},
+ };
+ Description.MachineMemOperandFlags = {
+ {"test-mmo-one", 0x1},
+ {"test-mmo-two", 0x2},
+ };
+ Description.VirtualRegisterFlags = {
+ {"test-vreg-one", 0x1},
+ {"test-vreg-two", 0x2},
+ };
+ Description.Intrinsics = {
+ {"llvm.test.seven", IntrinsicID::fromValue(7)},
+ {"llvm.test.seventeen", IntrinsicID::fromValue(17)},
+ };
+
+ for (const PhysicalRegisterDesc &Register :
+ createTestTargetRegisterInfoDescription().PhysicalRegisters) {
+ Description.CFIRegisters.push_back({Register.Reg, Register.Reg.number(), true});
+ }
+ return Description;
+}
+
+Expected<TargetDescription> createTestTargetDescription(InstructionSet Instructions) {
+ auto Registers = createTestTargetRegisterInfo();
+ if (!Registers)
+ return std::unexpected(Registers.error());
+ return TargetDescription::create(std::move(Instructions), std::move(*Registers),
+ createTestTargetMIRTextInfoDescription());
+}
+
+std::shared_ptr<const RegisterMask> createTestCallPreservedMask() {
+ std::uint64_t Preserved = 0;
+ Preserved |= std::uint64_t{1} << TestRegister::G1;
+ Preserved |= std::uint64_t{1} << TestRegister::G1Low32;
+ Preserved |= std::uint64_t{1} << TestRegister::G1High32;
+ Preserved |= std::uint64_t{1} << TestRegister::G1Low16;
+ Preserved |= std::uint64_t{1} << TestRegister::StackPointer;
+ Preserved |= std::uint64_t{1} << TestRegister::F1;
+ return std::make_shared<const RegisterMask>(std::vector<std::uint64_t>{Preserved});
+}
+
+} // namespace rcc::test
diff --git a/backend/unittests/Target/TestTarget.h b/backend/unittests/Target/TestTarget.h
new file mode 100644
index 0000000..fe434a2
--- /dev/null
+++ b/backend/unittests/Target/TestTarget.h
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef HOME_RICHARD_CODE_RCC_PROJECT_BACKEND_UNITTESTS_TARGET_TESTTARGET_H
+#define HOME_RICHARD_CODE_RCC_PROJECT_BACKEND_UNITTESTS_TARGET_TESTTARGET_H
+
+#include "rcc/CodeGen/InstructionDescriptor.h"
+#include "rcc/CodeGen/RegisterMask.h"
+#include "rcc/CodeGen/TargetDescription.h"
+#include "rcc/CodeGen/TargetMIRTextInfo.h"
+#include "rcc/CodeGen/TargetRegisterInfo.h"
+#include "rcc/Support/Error.h"
+
+#include <cstdint>
+#include <memory>
+
+namespace rcc::test {
+
+struct TestRegister final {
+
+ static constexpr std::uint32_t G0 = 1;
+ static constexpr std::uint32_t G0Low32 = 2;
+ static constexpr std::uint32_t G0High32 = 3;
+ static constexpr std::uint32_t G0Low16 = 4;
+ static constexpr std::uint32_t G1 = 5;
+ static constexpr std::uint32_t G1Low32 = 6;
+ static constexpr std::uint32_t G1High32 = 7;
+ static constexpr std::uint32_t G1Low16 = 8;
+ static constexpr std::uint32_t StackPointer = 9;
+ static constexpr std::uint32_t F0 = 10;
+ static constexpr std::uint32_t F1 = 11;
+};
+
+struct TestSubReg final {
+
+ static constexpr SubRegIndex Low32 = SubRegIndex::fromValue(1);
+ static constexpr SubRegIndex High32 = SubRegIndex::fromValue(2);
+ static constexpr SubRegIndex Low16 = SubRegIndex::fromValue(3);
+};
+
+struct TestRegisterBank final {
+
+ static constexpr RegisterBankID GPR = RegisterBankID::fromValue(1);
+ static constexpr RegisterBankID FPR = RegisterBankID::fromValue(2);
+};
+
+struct TestRegisterClass final {
+
+ static constexpr RegisterClassID GPR64 = RegisterClassID::fromValue(1);
+ static constexpr RegisterClassID GPR64Alloc = RegisterClassID::fromValue(2);
+ static constexpr RegisterClassID GPR32 = RegisterClassID::fromValue(3);
+ static constexpr RegisterClassID GPR32Low = RegisterClassID::fromValue(4);
+ static constexpr RegisterClassID GPR32High = RegisterClassID::fromValue(5);
+ static constexpr RegisterClassID GPR16 = RegisterClassID::fromValue(6);
+ static constexpr RegisterClassID FPR64 = RegisterClassID::fromValue(7);
+ static constexpr RegisterClassID SpecialGPR64 = RegisterClassID::fromValue(8);
+};
+
+[[nodiscard]] TargetRegisterInfoDesc createTestTargetRegisterInfoDescription();
+[[nodiscard]] Expected<TargetRegisterInfo> createTestTargetRegisterInfo();
+[[nodiscard]] TargetMIRTextInfoDesc createTestTargetMIRTextInfoDescription();
+[[nodiscard]] Expected<TargetDescription> createTestTargetDescription(InstructionSet Instructions);
+[[nodiscard]] std::shared_ptr<const RegisterMask> createTestCallPreservedMask();
+
+} // namespace rcc::test
+
+#endif // HOME_RICHARD_CODE_RCC_PROJECT_BACKEND_UNITTESTS_TARGET_TESTTARGET_H