diff options
Diffstat (limited to 'backend/unittests/Target')
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> §ions() 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 |
