diff options
Diffstat (limited to 'backend/unittests')
95 files changed, 30080 insertions, 0 deletions
diff --git a/backend/unittests/AsmParser/CMakeLists.txt b/backend/unittests/AsmParser/CMakeLists.txt new file mode 100644 index 0000000..df6fa12 --- /dev/null +++ b/backend/unittests/AsmParser/CMakeLists.txt @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +add_executable(RCCBackendLLParserTests LLParserTest.cpp) +rcc_backend_configure_target(RCCBackendLLParserTests) +target_link_libraries( + RCCBackendLLParserTests + PRIVATE + RCCBackend::AsmParser + RCCBackend::IR +) + +add_test(NAME RCCBackend.AsmParser.LLParser COMMAND RCCBackendLLParserTests) + +rcc_backend_register_format_files("${CMAKE_CURRENT_SOURCE_DIR}/LLParserTest.cpp") diff --git a/backend/unittests/AsmParser/LLParserTest.cpp b/backend/unittests/AsmParser/LLParserTest.cpp new file mode 100644 index 0000000..75312e4 --- /dev/null +++ b/backend/unittests/AsmParser/LLParserTest.cpp @@ -0,0 +1,533 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/AsmParser/LLParser.h" + +#include "rcc/IR/DataLayout.h" + +#include <algorithm> +#include <array> +#include <cstdint> +#include <iostream> +#include <limits> +#include <span> +#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; +}; + +struct TestOptions final { + std::string_view DataLayout; + std::span<const rcc::LLNamedTypeDefinition> NamedTypes; +}; + +[[nodiscard]] rcc::Expected<rcc::GenericConstantData> parseConstant(std::string_view Text, + TestOptions Options) { + return rcc::parseLLVMConstant(Text, {.NamedTypes = Options.NamedTypes}); +} + +[[nodiscard]] rcc::Expected<rcc::GenericConstantType> parseType(std::string_view Text, + TestOptions Options) { + return rcc::parseLLVMType(Text, {.NamedTypes = Options.NamedTypes}); +} + +[[nodiscard]] rcc::Expected<std::uint64_t> allocationSize(const rcc::GenericConstantType &Type, + TestOptions Options) { + auto Layout = rcc::DataLayout::parse(Options.DataLayout); + if (!Layout) + return std::unexpected(Layout.error()); + return Layout->getTypeAllocSize(Type); +} + +[[nodiscard]] rcc::Expected<std::uint64_t> preferredAlignment(const rcc::GenericConstantType &Type, + TestOptions Options) { + auto Layout = rcc::DataLayout::parse(Options.DataLayout); + if (!Layout) + return std::unexpected(Layout.error()); + return Layout->getPrefTypeAlign(Type); +} + +[[nodiscard]] rcc::Expected<std::uint32_t> pointerSizeInBits(std::uint32_t AddressSpace, + TestOptions Options = {}) { + auto Layout = rcc::DataLayout::parse(Options.DataLayout); + if (!Layout) + return std::unexpected(Layout.error()); + return Layout->getPointerSizeInBits(AddressSpace); +} + +constexpr TestOptions Options{ + .DataLayout = "e-p:64:64", + .NamedTypes = {}, +}; + +void expectRoundTrip(TestContext &Context, std::string_view Text, std::uint64_t ExpectedSize) { + auto Parsed = parseConstant(Text, Options); + Context.expect(Parsed.has_value(), "valid LLVM constant must parse"); + if (!Parsed) { + std::cerr << Text << ": " << Parsed.error().message() << '\n'; + return; + } + auto Size = allocationSize(Parsed->Type, Options); + Context.expect(Size.has_value() && *Size == ExpectedSize, + "LLVM constant storage size must match its structured type"); + auto Printed = Parsed->toLLVMString(); + Context.expect(Printed.has_value(), "structured LLVM constant must print"); + if (!Printed) + return; + auto Reparsing = parseConstant(*Printed, Options); + Context.expect(Reparsing.has_value() && *Reparsing == *Parsed, + "LLVM constant parse-print-parse must preserve the value tree"); + if (!Reparsing) + std::cerr << *Printed << ": " << Reparsing.error().message() << '\n'; +} + +void expectLayoutRoundTrip(TestContext &Context, std::string_view Text, TestOptions LayoutOptions, + std::uint64_t ExpectedSize, std::uint64_t ExpectedPreferredAlignment) { + auto Parsed = parseConstant(Text, LayoutOptions); + Context.expect(Parsed.has_value(), "DataLayout-aware LLVM constant must parse"); + if (!Parsed) { + std::cerr << Text << ": " << Parsed.error().message() << '\n'; + return; + } + auto Size = allocationSize(Parsed->Type, LayoutOptions); + auto Alignment = preferredAlignment(Parsed->Type, LayoutOptions); + Context.expect(Size.has_value() && *Size == ExpectedSize, + "DataLayout must determine the exact allocation size"); + Context.expect(Alignment.has_value() && *Alignment == ExpectedPreferredAlignment, + "DataLayout must determine the exact preferred alignment"); + auto Printed = Parsed->toLLVMString(); + Context.expect(Printed.has_value(), "DataLayout-aware LLVM constant must print"); + if (!Printed) + return; + auto Reparsing = parseConstant(*Printed, LayoutOptions); + Context.expect(Reparsing.has_value() && *Reparsing == *Parsed, + "DataLayout-aware named constants must round-trip structurally"); +} + +void testScalarConstants(TestContext &Context) { + expectRoundTrip(Context, "i1 true", 1); + expectRoundTrip(Context, "i65 -1", 12); + expectRoundTrip(Context, "half 0xH3C00", 2); + expectRoundTrip(Context, "bfloat 0xR3F80", 2); + expectRoundTrip(Context, "float 1.250000e+00", 4); + expectRoundTrip(Context, "double 0x3FF0000000000000", 8); + expectRoundTrip(Context, "x86_fp80 f0x4000C000000000000000", 16); + expectRoundTrip(Context, "fp128 f0x3FFF0000000000000000000000000000", 16); + expectRoundTrip(Context, "ptr null", 8); + expectRoundTrip(Context, "i32 undef", 4); + expectRoundTrip(Context, "i32 poison", 4); + expectRoundTrip(Context, "i32 zeroinitializer", 4); + + auto BFloat = parseConstant("bfloat 0xR3E80", Options); + auto PrintedBFloat = + BFloat ? BFloat->toLLVMString() : rcc::Expected<std::string>(std::unexpected(BFloat.error())); + Context.expect(PrintedBFloat && *PrintedBFloat == "bfloat 2.500000e-01", + "LLVM-compatible floating printing must prefer an exact decimal spelling"); + + auto Pi = parseConstant("double f0x400921FB54442D18", Options); + auto PrintedPi = + Pi ? Pi->toLLVMString() : rcc::Expected<std::string>(std::unexpected(Pi.error())); + Context.expect( + PrintedPi && *PrintedPi == "double f0x400921FB54442D18", + "floating printing must retain exact bits when six decimal digits do not round-trip"); +} + +void expectIntegerWords(TestContext &Context, std::string_view Text, + std::span<const std::uint64_t> ExpectedWords) { + auto Parsed = parseConstant(Text, Options); + Context.expect(Parsed.has_value() && Parsed->IntegerValue.has_value() && + std::ranges::equal(Parsed->IntegerValue->words(), ExpectedWords), + "LLVM integer literal must preserve its exact bit pattern"); + if (!Parsed) + std::cerr << Text << ": " << Parsed.error().message() << '\n'; +} + +void testDecimalIntegerTruncation(TestContext &Context) { + constexpr std::array<std::uint64_t, 1> NegativeOne{std::numeric_limits<std::uint64_t>::max()}; + constexpr std::array<std::uint64_t, 1> NarrowNegative{767}; + constexpr std::array<std::uint64_t, 1> NarrowMinimum{8}; + constexpr std::array<std::uint64_t, 1> WidePositive{1}; + + expectIntegerWords(Context, "i64 -1", NegativeOne); + expectIntegerWords(Context, "i16 -64769", NarrowNegative); + expectIntegerWords(Context, "i4 -8", NarrowMinimum); + expectIntegerWords(Context, "i64 18446744073709551617", WidePositive); +} + +void expectFloatingWords(TestContext &Context, std::string_view Text, + std::span<const std::uint64_t> ExpectedWords) { + auto Parsed = parseConstant(Text, Options); + Context.expect(Parsed.has_value() && Parsed->FloatingValue.has_value() && + std::ranges::equal(Parsed->FloatingValue->words(), ExpectedWords), + "LLVM floating special literal must preserve its exact bit pattern"); + if (!Parsed) { + std::cerr << Text << ": " << Parsed.error().message() << '\n'; + return; + } + if (!Parsed->FloatingValue || + !std::ranges::equal(Parsed->FloatingValue->words(), ExpectedWords)) { + std::cerr << Text << ": expected words"; + for (const std::uint64_t Word : ExpectedWords) + std::cerr << " 0x" << std::hex << Word; + std::cerr << ", got"; + if (Parsed->FloatingValue) { + for (const std::uint64_t Word : Parsed->FloatingValue->words()) + std::cerr << " 0x" << std::hex << Word; + } + std::cerr << std::dec << '\n'; + } + auto Printed = Parsed->toLLVMString(); + Context.expect(Printed.has_value(), "LLVM floating special literal must print"); + if (!Printed) + return; + auto Reparsing = parseConstant(*Printed, Options); + Context.expect(Reparsing.has_value() && *Reparsing == *Parsed, + "printed LLVM floating special literal must round-trip"); +} + +void testExplicitHexIntegerConstants(TestContext &Context) { + constexpr std::array<std::uint64_t, 2> AllOnes65{std::numeric_limits<std::uint64_t>::max(), 1}; + constexpr std::array<std::uint64_t, 2> SignBit65{0, 1}; + constexpr std::array<std::uint64_t, 1> UnsignedFF{0xff}; + constexpr std::array<std::uint64_t, 1> SignedFF{0xffff}; + constexpr std::array<std::uint64_t, 1> SignedFF8{0xff}; + + expectIntegerWords(Context, "i65 u0x1FFFFFFFFFFFFFFFF", AllOnes65); + expectIntegerWords(Context, "i65 s0x1FFFFFFFFFFFFFFFF", AllOnes65); + expectIntegerWords(Context, "i65 s0x10000000000000000", SignBit65); + expectIntegerWords(Context, "i65 s0x01", AllOnes65); + expectIntegerWords(Context, "i16 u0xFF", UnsignedFF); + expectIntegerWords(Context, "i16 s0xFF", SignedFF); + expectIntegerWords(Context, "i8 s0x1FF", SignedFF8); + + expectRoundTrip(Context, "i65 s0x01", 12); + Context.expect(!parseConstant("i32 u0x", Options) && !parseConstant("i32 u0X1", Options), + "explicit LLVM hexadecimal integers must use the fixed spelling"); +} + +void testSpecialFloatingLiterals(TestContext &Context) { + constexpr std::array<std::uint64_t, 1> HalfInfinity{0x7c00}; + constexpr std::array<std::uint64_t, 1> NegativeHalfInfinity{0xfc00}; + constexpr std::array<std::uint64_t, 1> HalfQuietNaN{0x7e00}; + constexpr std::array<std::uint64_t, 1> NegativeHalfQuietNaN{0xfe00}; + constexpr std::array<std::uint64_t, 1> HalfSignalingNaN{0x7d00}; + constexpr std::array<std::uint64_t, 1> HalfPayloadNaN{0x7fef}; + constexpr std::array<std::uint64_t, 1> FloatSignalingNaN{0x7f800001}; + constexpr std::array<std::uint64_t, 2> X87Infinity{0x8000000000000000, 0x7fff}; + constexpr std::array<std::uint64_t, 2> X87QuietNaN{0xc000000000000000, 0x7fff}; + constexpr std::array<std::uint64_t, 2> QuadOne{0, 0x3fff000000000000}; + constexpr std::array<std::uint64_t, 2> QuadInfinity{0, 0x7fff000000000000}; + constexpr std::array<std::uint64_t, 2> QuadQuietNaN{0, 0x7fff800000000000}; + constexpr std::array<std::uint64_t, 2> PPCOne{0x3ff0000000000000, 0}; + constexpr std::array<std::uint64_t, 2> PPCInfinity{0x7ff0000000000000, 0}; + constexpr std::array<std::uint64_t, 2> PPCQuietNaN{0x7ff8000000000000, 0}; + + expectFloatingWords(Context, "half +inf", HalfInfinity); + expectFloatingWords(Context, "half -inf", NegativeHalfInfinity); + expectFloatingWords(Context, "half +qnan", HalfQuietNaN); + expectFloatingWords(Context, "half -qnan", NegativeHalfQuietNaN); + expectFloatingWords(Context, "half +snan(0x100)", HalfSignalingNaN); + expectFloatingWords(Context, "half +nan(0x123456789ABCDEF)", HalfPayloadNaN); + expectFloatingWords(Context, "float +snan(0x400001)", FloatSignalingNaN); + expectFloatingWords(Context, "x86_fp80 +inf", X87Infinity); + expectFloatingWords(Context, "x86_fp80 +qnan", X87QuietNaN); + expectFloatingWords(Context, "fp128 +inf", QuadInfinity); + expectFloatingWords(Context, "fp128 +qnan", QuadQuietNaN); + expectFloatingWords(Context, "fp128 0xL00000000000000003FFF000000000000", QuadOne); + expectFloatingWords(Context, "ppc_fp128 +inf", PPCInfinity); + expectFloatingWords(Context, "ppc_fp128 +qnan", PPCQuietNaN); + expectFloatingWords(Context, "ppc_fp128 0xM3FF00000000000000000000000000000", PPCOne); + + for (const std::string_view Text : { + std::string_view{"float nan(0x)"}, + std::string_view{"float snan(12)"}, + std::string_view{"float qnan(0x1)"}, + std::string_view{"float inf(0x1)"}, + std::string_view{"float inf"}, + std::string_view{"float +nan"}, + std::string_view{"float +snan"}, + std::string_view{"float +qnan(0x1)"}, + }) { + Context.expect(!parseConstant(Text, Options), + "malformed LLVM floating special literal must be rejected"); + } +} + +void testExactFloatingConversion(TestContext &Context) { + constexpr std::array<std::uint64_t, 1> PositiveZero{0}; + constexpr std::array<std::uint64_t, 1> NegativeFloatZero{0x80000000}; + constexpr std::array<std::uint64_t, 1> HalfOne{0x3c00}; + constexpr std::array<std::uint64_t, 1> HalfTwoUlps{0x3c02}; + constexpr std::array<std::uint64_t, 1> HalfSmallestSubnormal{0x0001}; + constexpr std::array<std::uint64_t, 1> BFloatOne{0x3f80}; + constexpr std::array<std::uint64_t, 1> BFloatTwoUlps{0x3f82}; + constexpr std::array<std::uint64_t, 1> FloatOne{0x3f800000}; + constexpr std::array<std::uint64_t, 1> FloatTwoUlps{0x3f800002}; + constexpr std::array<std::uint64_t, 1> FloatHexPower{0x2f800000}; + constexpr std::array<std::uint64_t, 1> FloatSignalingNaN{0x7f800001}; + constexpr std::array<std::uint64_t, 1> FloatQuietNaN{0x7fc00000}; + constexpr std::array<std::uint64_t, 1> LargestDouble{0x7fefffffffffffff}; + constexpr std::array<std::uint64_t, 2> X87One{0x8000000000000000, 0x3fff}; + constexpr std::array<std::uint64_t, 2> QuadOne{0, 0x3fff000000000000}; + constexpr std::array<std::uint64_t, 2> PPCOne{0x3ff0000000000000, 0}; + + expectFloatingWords(Context, "float +0.0", PositiveZero); + expectFloatingWords(Context, "float -0.0", NegativeFloatZero); + expectFloatingWords(Context, "half 1.00048828125", HalfOne); + expectFloatingWords(Context, "half 1.00146484375", HalfTwoUlps); + expectFloatingWords(Context, "half 0.0000000298023223876953126", HalfSmallestSubnormal); + expectFloatingWords(Context, "bfloat 1.00390625", BFloatOne); + expectFloatingWords(Context, "bfloat 1.01171875", BFloatTwoUlps); + expectFloatingWords(Context, "float 1.000000059604644775390625", FloatOne); + expectFloatingWords(Context, "float 1.000000178813934326171875", FloatTwoUlps); + expectFloatingWords(Context, "float 0x1.0p-32", FloatHexPower); + expectFloatingWords(Context, "double 1.79769313486231570815e+308", LargestDouble); + expectFloatingWords(Context, "x86_fp80 1.0", X87One); + expectFloatingWords(Context, "x86_fp80 0x1.0p0", X87One); + expectFloatingWords(Context, "fp128 1.0", QuadOne); + expectFloatingWords(Context, "fp128 0x1.0p0", QuadOne); + expectFloatingWords(Context, "ppc_fp128 1.0", PPCOne); + expectFloatingWords(Context, "ppc_fp128 0x1.0p0", PPCOne); + expectFloatingWords(Context, "float 0x3FF0000000000000", FloatOne); + expectFloatingWords(Context, "float 0x7FF0000020000000", FloatSignalingNaN); + expectFloatingWords(Context, "float 0x7FF8000000000000", FloatQuietNaN); + expectFloatingWords(Context, "float f0x3F800000", FloatOne); + + for (const std::string_view Text : { + std::string_view{"half 0.0000000298023223876953125"}, + std::string_view{"float 0x3FF0000000000001"}, + std::string_view{"float 0x7FF8000000000001"}, + std::string_view{"float f0x3F8000"}, + std::string_view{"float f0X3F800000"}, + std::string_view{"float 0X1.0p0"}, + std::string_view{"float 0x1p0"}, + }) { + Context.expect(!parseConstant(Text, Options), + "noncanonical LLVM floating syntax must be rejected"); + } +} + +void testAggregateConstants(TestContext &Context) { + expectRoundTrip(Context, "[4 x i8] c\"ab\\00\\FF\"", 4); + expectRoundTrip(Context, "[3 x i16] [i16 1, i16 2, i16 3]", 6); + expectRoundTrip(Context, "<4 x i32> <i32 0, i32 1, i32 2, i32 3>", 16); + expectRoundTrip(Context, "<4 x float> <float 1.0, float -1.0, float 2.0, float -2.0>", 16); + expectRoundTrip(Context, "{ i32, ptr } { i32 7, ptr @global }", 16); + expectRoundTrip(Context, "<{ i8, i32 }> <{ i8 1, i32 2 }>", 5); + expectRoundTrip(Context, "<8 x i16> splat (i16 9)", 16); +} + +void testAddressConstants(TestContext &Context) { + expectRoundTrip(Context, "ptr @global", 8); + expectRoundTrip(Context, "ptr @\"quoted name\"", 8); + expectRoundTrip(Context, "ptr blockaddress(@function, %entry)", 8); + expectRoundTrip(Context, "ptr dso_local_equivalent @function", 8); + expectRoundTrip(Context, "ptr no_cfi @function", 8); + expectRoundTrip(Context, "i64 ptrtoint (ptr @global to i64)", 8); + expectRoundTrip(Context, "ptr bitcast (ptr @global to ptr)", 8); + expectRoundTrip(Context, "i64 add nuw nsw (i64 1, i64 2)", 8); + expectRoundTrip(Context, "ptr getelementptr inbounds nusw nuw (i8, ptr @global, i64 4)", 8); + expectRoundTrip(Context, + "<2 x i32> shufflevector (<2 x i32> <i32 1, i32 2>, " + "<2 x i32> <i32 3, i32 4>, <2 x i32> <i32 0, i32 3>)", + 8); +} + +void testConstantExpressionVerification(TestContext &Context) { + expectRoundTrip(Context, "i8 trunc (i16 257 to i8)", 1); + expectRoundTrip(Context, "i32 bitcast (float 1.0 to i32)", 4); + expectRoundTrip(Context, "ptr addrspacecast (ptr addrspace(1) @global to ptr)", 8); + expectRoundTrip(Context, "ptr inttoptr (i64 0 to ptr)", 8); + expectRoundTrip(Context, "i64 ptrtoaddr (ptr @global to i64)", 8); + expectRoundTrip(Context, "i64 ptrtoint (ptr @global to i64)", 8); + expectRoundTrip(Context, + "<2 x i32> insertelement (<2 x i32> <i32 1, i32 2>, " + "i32 3, i64 0)", + 8); + expectRoundTrip(Context, "i32 extractelement (<2 x i32> <i32 1, i32 2>, i64 0)", 4); + expectRoundTrip(Context, "ptr ptrauth (ptr @function, i32 1, i64 0, ptr null, ptr null)", 8); + + for (const std::string_view Text : { + std::string_view{"i16 trunc (i8 1 to i16)"}, + std::string_view{"i64 bitcast (float 1.0 to i64)"}, + std::string_view{"ptr bitcast (ptr addrspace(1) @global to ptr)"}, + std::string_view{"ptr addrspacecast (ptr @global to ptr)"}, + std::string_view{"ptr inttoptr (float 1.0 to ptr)"}, + std::string_view{"i64 ptrtoint (i64 0 to i64)"}, + std::string_view{"i32 add nuw (i32 1, i64 2)"}, + std::string_view{"ptr getelementptr (i8, ptr @global, float 1.0)"}, + std::string_view{"<2 x i32> shufflevector (<2 x i32> <i32 1, i32 2>, " + "<2 x i32> <i32 3, i32 4>, <2 x i64> <i64 0, i64 3>)"}, + std::string_view{"ptr ptrauth (ptr @function, i64 1)"}, + }) { + Context.expect(!parseConstant(Text, Options), + "constant expression with incompatible typed operands must be rejected"); + } +} + +void testDataLayoutAndNamedTypes(TestContext &Context) { + static constexpr std::array Definitions{ + rcc::LLNamedTypeDefinition{.Name = "pair", .Definition = "{ i8, i64 }", .NumericName = false}, + rcc::LLNamedTypeDefinition{.Name = "word", .Definition = "i32", .NumericName = false}, + rcc::LLNamedTypeDefinition{ + .Name = "node", .Definition = "{ i32, %node* }", .NumericName = false}, + rcc::LLNamedTypeDefinition{.Name = "loop", .Definition = "{ %loop }", .NumericName = false}, + }; + static constexpr TestOptions LayoutOptions{ + .DataLayout = "e-p:64:64-p1:32:16:64-i64:64:128-i128:128-v96:128:256-a:0:64", + .NamedTypes = Definitions, + }; + + expectLayoutRoundTrip(Context, "i65 -1", LayoutOptions, 16, 16); + expectLayoutRoundTrip(Context, "[3 x i24] [i24 1, i24 2, i24 3]", LayoutOptions, 12, 4); + expectLayoutRoundTrip(Context, "<3 x i32> <i32 1, i32 2, i32 3>", LayoutOptions, 16, 32); + expectLayoutRoundTrip(Context, "ptr addrspace(1) null", LayoutOptions, 4, 8); + expectLayoutRoundTrip(Context, "%pair { i8 1, i64 2 }", LayoutOptions, 16, 8); + expectLayoutRoundTrip(Context, "%word 7", LayoutOptions, 4, 4); + expectLayoutRoundTrip(Context, "%node zeroinitializer", LayoutOptions, 16, 8); + + Context.expect(!parseType("%loop", LayoutOptions), + "directly recursive named aggregate types must be rejected as unsized"); + auto NodeType = parseType("%node", LayoutOptions); + Context.expect(NodeType.has_value(), + "named recursion through a pointer must remain sized and legal"); +} + +void testAddressSpacePointerWidths(TestContext &Context) { + constexpr TestOptions HeterogeneousLayoutOptions{ + .DataLayout = "e-p:64:64-p1:32:16:64", + .NamedTypes = {}, + }; + auto DefaultPointer = pointerSizeInBits(0, HeterogeneousLayoutOptions); + auto AddressSpaceOnePointer = pointerSizeInBits(1, HeterogeneousLayoutOptions); + Context.expect(DefaultPointer.has_value() && *DefaultPointer == 64 && + AddressSpaceOnePointer.has_value() && *AddressSpaceOnePointer == 32, + "LLVM data layout must provide pointer width by address space"); + + auto MissingPointer = pointerSizeInBits(7); + Context.expect(!MissingPointer, + "pointer width lookup must reject a missing explicit data layout"); +} + +void testDataLayoutBoundaryRules(TestContext &Context) { + static constexpr TestOptions IntegerAlignmentOptions{ + .DataLayout = "e-p:64:64-i24:128:128", + .NamedTypes = {}, + }; + static constexpr TestOptions HistoricalAggregateOptions{ + .DataLayout = "e-p:64:64-a0:64:128", + .NamedTypes = {}, + }; + static constexpr TestOptions ZeroAggregateOptions{ + .DataLayout = "e-p:64:64-a:0:64", + .NamedTypes = {}, + }; + static constexpr TestOptions NonBytePointerOptions{ + .DataLayout = "e-p:33:8:8:32", + .NamedTypes = {}, + }; + static constexpr TestOptions PointerFlagsOptions{ + .DataLayout = "e-pue1(storage):33:8:8:32-po2(allones):64:64", + .NamedTypes = {}, + }; + + expectLayoutRoundTrip(Context, "i20 1", IntegerAlignmentOptions, 16, 16); + expectLayoutRoundTrip(Context, "{ i8, i32 } { i8 1, i32 2 }", HistoricalAggregateOptions, 8, 16); + expectLayoutRoundTrip(Context, "{ i8, i32 } { i8 1, i32 2 }", ZeroAggregateOptions, 8, 8); + expectLayoutRoundTrip(Context, "<{ i8, i32 }> <{ i8 1, i32 2 }>", ZeroAggregateOptions, 5, 8); + expectLayoutRoundTrip(Context, "ptr null", NonBytePointerOptions, 5, 1); + expectLayoutRoundTrip(Context, "ptr addrspace(1) null", PointerFlagsOptions, 5, 1); + + static constexpr TestOptions InvalidIndexSizeOptions{ + .DataLayout = "e-p:33:8:8:34", + .NamedTypes = {}, + }; + auto Pointer = parseType("ptr", InvalidIndexSizeOptions); + Context.expect(Pointer.has_value() && !allocationSize(*Pointer, InvalidIndexSizeOptions), + "LLVM pointer index size larger than its representation must be rejected"); + + static constexpr TestOptions NonLayoutOptions{ + .DataLayout = "e-m:e-n8:16:32-S128-Fi64-P1-G2-A3-ni:1:2-ve-s-p:64:64", + .NamedTypes = {}, + }; + expectLayoutRoundTrip(Context, "ptr null", NonLayoutOptions, 8, 8); + + const auto ExpectInvalidDataLayout = [&Context](std::string_view DataLayout) { + const TestOptions InvalidOptions{ + .DataLayout = DataLayout, + .NamedTypes = {}, + }; + auto Type = parseType("ptr", InvalidOptions); + Context.expect(Type.has_value() && !allocationSize(*Type, InvalidOptions), + "malformed non-layout LLVM data-layout component must be rejected"); + }; + for (const std::string_view DataLayout : { + std::string_view{"ee-p:64:64"}, + std::string_view{"m:q-p:64:64"}, + std::string_view{"n64:0-p:64:64"}, + std::string_view{"S-p:64:64"}, + std::string_view{"Fx64-p:64:64"}, + std::string_view{"P16777216-p:64:64"}, + std::string_view{"ni:-p:64:64"}, + std::string_view{"ni:0-p:64:64"}, + std::string_view{"pe:64:64"}, + std::string_view{"pu:64:64"}, + std::string_view{"pzo1:64:64"}, + std::string_view{"p1(P):64:64"}, + std::string_view{"p1(foo):64:64-p2(foo):64:64"}, + std::string_view{"p16777216:64:64"}, + }) { + ExpectInvalidDataLayout(DataLayout); + } +} + +void testInvalidConstants(TestContext &Context) { + for (const std::string_view Text : { + std::string_view{""}, + std::string_view{"opaque 0"}, + std::string_view{"i32 null"}, + std::string_view{"ptr 42"}, + std::string_view{"[2 x i8] [i8 1]"}, + std::string_view{"<2 x i32> <i32 1, i64 2>"}, + std::string_view{"i64 ptrtoint (ptr @global to i32)"}, + std::string_view{"ptr getelementptr (i8, i64 0)"}, + std::string_view{"i32 1 trailing"}, + }) { + Context.expect(!parseConstant(Text, Options), "invalid LLVM constant must be rejected"); + } +} + +} // namespace + +int main() { + TestContext Context; + testScalarConstants(Context); + testExplicitHexIntegerConstants(Context); + testDecimalIntegerTruncation(Context); + testSpecialFloatingLiterals(Context); + testExactFloatingConversion(Context); + testAggregateConstants(Context); + testAddressConstants(Context); + testConstantExpressionVerification(Context); + testDataLayoutAndNamedTypes(Context); + testAddressSpacePointerWidths(Context); + testDataLayoutBoundaryRules(Context); + testInvalidConstants(Context); + return Context.result(); +} diff --git a/backend/unittests/CMakeLists.txt b/backend/unittests/CMakeLists.txt new file mode 100644 index 0000000..59e3c24 --- /dev/null +++ b/backend/unittests/CMakeLists.txt @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +add_subdirectory(Support) +add_subdirectory(MC) +add_subdirectory(AsmParser) +add_subdirectory(Target) +add_subdirectory(CodeGen) +add_subdirectory(MIR) +add_subdirectory(Tooling) diff --git a/backend/unittests/CodeGen/CMakeLists.txt b/backend/unittests/CodeGen/CMakeLists.txt new file mode 100644 index 0000000..62a5025 --- /dev/null +++ b/backend/unittests/CodeGen/CMakeLists.txt @@ -0,0 +1,545 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +set( + RCCBackendCodeGenTestSources + CompilationUnitTest.cpp + CoreTypesTest.cpp + GenericOpcodesTest.cpp + InstructionDescriptorTest.cpp + MachineCompilationStatusTest.cpp + MachineConstantPoolTest.cpp + MachineDebugInfoTest.cpp + MachineFrameInfoTest.cpp + MachineFunctionClonerTest.cpp + MachineFunctionRecordsTest.cpp + MachineIRAttributesTest.cpp + MachineIRBuilderTest.cpp + MachineIRCoreTest.cpp + MachineAnalysesTest.cpp + MachineJumpTableInfoTest.cpp + SlotIndexesTest.cpp + LiveVariablesTest.cpp + LiveIntervalTest.cpp + LiveIntervalsTest.cpp + LivePhysRegsTest.cpp + LiveRegUnitsTest.cpp + LiveTestSupport.h + MachineMetadataTest.cpp + MachineOperandTest.cpp + MachineOptimizationTest.cpp + PassManagerTest.cpp + PassInstrumentationTest.cpp + MachinePassManagerTest.cpp + MachineSchedulingTest.cpp + PBQPTest.cpp + RegisterAllocationTest.cpp + PHIEliminationTest.cpp + TwoAddressInstructionPassTest.cpp + MachineVerifierTest.cpp + MachineVerifierTargetTest.cpp + MachineValueTypesTest.cpp + TargetConstantTest.cpp + TargetCallLoweringTest.cpp + TargetFunctionStateTest.cpp + TargetMIRTextInfoTest.cpp + TargetRegisterInfoTest.cpp +) + +add_executable(RCCBackendCodeGenCompilationUnitTests CompilationUnitTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenCompilationUnitTests) +target_link_libraries(RCCBackendCodeGenCompilationUnitTests PRIVATE RCCBackend::CodeGen) + +add_test( + NAME RCCBackend.CodeGen.CompilationUnit + COMMAND RCCBackendCodeGenCompilationUnitTests +) + +add_executable(RCCBackendCodeGenCoreTypesTests CoreTypesTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenCoreTypesTests) +target_link_libraries(RCCBackendCodeGenCoreTypesTests PRIVATE RCCBackend::CodeGen) + +add_test(NAME RCCBackend.CodeGen.CoreTypes COMMAND RCCBackendCodeGenCoreTypesTests) + +add_executable(RCCBackendCodeGenPBQPTests PBQPTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenPBQPTests) +target_link_libraries(RCCBackendCodeGenPBQPTests PRIVATE RCCBackend::CodeGen) + +add_test(NAME RCCBackend.CodeGen.PBQP COMMAND RCCBackendCodeGenPBQPTests) + +add_executable(RCCBackendCodeGenRegisterAllocationTests RegisterAllocationTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenRegisterAllocationTests) +target_link_libraries( + RCCBackendCodeGenRegisterAllocationTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.RegisterAllocation + COMMAND RCCBackendCodeGenRegisterAllocationTests +) + +add_executable(RCCBackendCodeGenLiveRegMatrixTests LiveRegMatrixTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenLiveRegMatrixTests) +target_link_libraries( + RCCBackendCodeGenLiveRegMatrixTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test(NAME RCCBackend.CodeGen.LiveRegMatrix COMMAND RCCBackendCodeGenLiveRegMatrixTests) + +add_executable(RCCBackendCodeGenVirtRegMapTests VirtRegMapTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenVirtRegMapTests) +target_link_libraries( + RCCBackendCodeGenVirtRegMapTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test(NAME RCCBackend.CodeGen.VirtRegMap COMMAND RCCBackendCodeGenVirtRegMapTests) + +add_executable(RCCBackendCodeGenSpillerTests SpillerTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenSpillerTests) +target_link_libraries( + RCCBackendCodeGenSpillerTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test(NAME RCCBackend.CodeGen.Spiller COMMAND RCCBackendCodeGenSpillerTests) + +add_executable(RCCBackendCodeGenBasicAllocatorTests RegAllocBasicTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenBasicAllocatorTests) +target_link_libraries( + RCCBackendCodeGenBasicAllocatorTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test(NAME RCCBackend.CodeGen.BasicAllocator COMMAND RCCBackendCodeGenBasicAllocatorTests) + +add_executable(RCCBackendCodeGenFastAllocatorTests RegAllocFastTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenFastAllocatorTests) +target_link_libraries( + RCCBackendCodeGenFastAllocatorTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test(NAME RCCBackend.CodeGen.FastAllocator COMMAND RCCBackendCodeGenFastAllocatorTests) + +add_executable(RCCBackendCodeGenPBQPAllocatorTests RegAllocPBQPTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenPBQPAllocatorTests) +target_link_libraries( + RCCBackendCodeGenPBQPAllocatorTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test(NAME RCCBackend.CodeGen.PBQPAllocator COMMAND RCCBackendCodeGenPBQPAllocatorTests) + +add_executable(RCCBackendCodeGenGenericOpcodesTests GenericOpcodesTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenGenericOpcodesTests) +target_link_libraries(RCCBackendCodeGenGenericOpcodesTests PRIVATE RCCBackend::CodeGen) + +add_test(NAME RCCBackend.CodeGen.GenericOpcodes COMMAND RCCBackendCodeGenGenericOpcodesTests) + +add_executable( + RCCBackendCodeGenInstructionDescriptorTests + InstructionDescriptorTest.cpp +) +rcc_backend_configure_target(RCCBackendCodeGenInstructionDescriptorTests) +target_link_libraries( + RCCBackendCodeGenInstructionDescriptorTests + PRIVATE RCCBackend::CodeGen +) + +add_test( + NAME RCCBackend.CodeGen.InstructionDescriptor + COMMAND RCCBackendCodeGenInstructionDescriptorTests +) + +add_executable( + RCCBackendCodeGenMachineCompilationStatusTests + MachineCompilationStatusTest.cpp +) +rcc_backend_configure_target(RCCBackendCodeGenMachineCompilationStatusTests) +target_link_libraries( + RCCBackendCodeGenMachineCompilationStatusTests + PRIVATE RCCBackend::CodeGen +) + +add_test( + NAME RCCBackend.CodeGen.MachineCompilationStatus + COMMAND RCCBackendCodeGenMachineCompilationStatusTests +) + +add_executable(RCCBackendCodeGenMachineConstantPoolTests MachineConstantPoolTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineConstantPoolTests) +target_link_libraries(RCCBackendCodeGenMachineConstantPoolTests PRIVATE RCCBackend::CodeGen) + +add_test( + NAME RCCBackend.CodeGen.MachineConstantPool + COMMAND RCCBackendCodeGenMachineConstantPoolTests +) + +add_executable(RCCBackendCodeGenMachineDebugInfoTests MachineDebugInfoTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineDebugInfoTests) +target_link_libraries( + RCCBackendCodeGenMachineDebugInfoTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.MachineDebugInfo + COMMAND RCCBackendCodeGenMachineDebugInfoTests +) + +add_executable(RCCBackendCodeGenMachineFrameInfoTests MachineFrameInfoTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineFrameInfoTests) +target_link_libraries(RCCBackendCodeGenMachineFrameInfoTests PRIVATE RCCBackend::CodeGen) + +add_test( + NAME RCCBackend.CodeGen.MachineFrameInfo + COMMAND RCCBackendCodeGenMachineFrameInfoTests +) + +add_executable( + RCCBackendCodeGenMachineFunctionClonerTests + MachineFunctionClonerTest.cpp +) +rcc_backend_configure_target(RCCBackendCodeGenMachineFunctionClonerTests) +target_link_libraries( + RCCBackendCodeGenMachineFunctionClonerTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.MachineFunctionCloner + COMMAND RCCBackendCodeGenMachineFunctionClonerTests +) + +add_executable( + RCCBackendCodeGenMachineFunctionRecordsTests + MachineFunctionRecordsTest.cpp +) +rcc_backend_configure_target(RCCBackendCodeGenMachineFunctionRecordsTests) +target_link_libraries( + RCCBackendCodeGenMachineFunctionRecordsTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.MachineFunctionRecords + COMMAND RCCBackendCodeGenMachineFunctionRecordsTests +) + +add_executable(RCCBackendCodeGenMachineIRBuilderTests MachineIRBuilderTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineIRBuilderTests) +target_link_libraries( + RCCBackendCodeGenMachineIRBuilderTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.MachineIRBuilder + COMMAND RCCBackendCodeGenMachineIRBuilderTests +) + +add_executable(RCCBackendCodeGenMachineIRAttributesTests MachineIRAttributesTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineIRAttributesTests) +target_link_libraries( + RCCBackendCodeGenMachineIRAttributesTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.MachineIRAttributes + COMMAND RCCBackendCodeGenMachineIRAttributesTests +) + +add_executable(RCCBackendCodeGenMachineVerifierTests MachineVerifierTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineVerifierTests) +target_link_libraries( + RCCBackendCodeGenMachineVerifierTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.MachineVerifier + COMMAND RCCBackendCodeGenMachineVerifierTests +) + +add_executable( + RCCBackendCodeGenMachineVerifierTargetTests + MachineVerifierTargetTest.cpp +) +rcc_backend_configure_target(RCCBackendCodeGenMachineVerifierTargetTests) +target_link_libraries( + RCCBackendCodeGenMachineVerifierTargetTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.MachineVerifierTarget + COMMAND RCCBackendCodeGenMachineVerifierTargetTests +) + +add_executable(RCCBackendCodeGenMachineValueTypesTests MachineValueTypesTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineValueTypesTests) +target_link_libraries(RCCBackendCodeGenMachineValueTypesTests PRIVATE RCCBackend::CodeGen) + +add_test( + NAME RCCBackend.CodeGen.MachineValueTypes + COMMAND RCCBackendCodeGenMachineValueTypesTests +) + +add_executable(RCCBackendCodeGenMachineIRCoreTests MachineIRCoreTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineIRCoreTests) +target_link_libraries(RCCBackendCodeGenMachineIRCoreTests PRIVATE RCCBackend::CodeGen) + +add_test( + NAME RCCBackend.CodeGen.MachineIRCore + COMMAND RCCBackendCodeGenMachineIRCoreTests +) + +add_executable(RCCBackendCodeGenMachineJumpTableInfoTests MachineJumpTableInfoTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineJumpTableInfoTests) +target_link_libraries(RCCBackendCodeGenMachineJumpTableInfoTests PRIVATE RCCBackend::CodeGen) + +add_test( + NAME RCCBackend.CodeGen.MachineJumpTableInfo + COMMAND RCCBackendCodeGenMachineJumpTableInfoTests +) + +foreach( + Component + SlotIndexes + LiveVariables + LiveInterval + LiveIntervals + LivePhysRegs + LiveRegUnits +) + add_executable(RCCBackendCodeGen${Component}Tests ${Component}Test.cpp) + rcc_backend_configure_target(RCCBackendCodeGen${Component}Tests) + target_link_libraries( + RCCBackendCodeGen${Component}Tests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget RCCBackend::RISCV + ) + add_test( + NAME RCCBackend.CodeGen.${Component} + COMMAND RCCBackendCodeGen${Component}Tests + ) +endforeach() + +add_executable(RCCBackendCodeGenMachineMetadataTests MachineMetadataTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineMetadataTests) +target_link_libraries( + RCCBackendCodeGenMachineMetadataTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.MachineMetadata + COMMAND RCCBackendCodeGenMachineMetadataTests +) + +add_executable(RCCBackendCodeGenMachineOperandTests MachineOperandTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineOperandTests) +target_link_libraries( + RCCBackendCodeGenMachineOperandTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.MachineOperand + COMMAND RCCBackendCodeGenMachineOperandTests +) + +add_executable(RCCBackendCodeGenPassManagerTests PassManagerTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenPassManagerTests) +target_link_libraries( + RCCBackendCodeGenPassManagerTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test(NAME RCCBackend.CodeGen.PassManager COMMAND RCCBackendCodeGenPassManagerTests) + +add_executable(RCCBackendCodeGenPassInstrumentationTests PassInstrumentationTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenPassInstrumentationTests) +target_link_libraries( + RCCBackendCodeGenPassInstrumentationTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test( + NAME RCCBackend.CodeGen.PassInstrumentation + COMMAND RCCBackendCodeGenPassInstrumentationTests +) + +add_executable(RCCBackendCodeGenMachinePassManagerTests MachinePassManagerTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachinePassManagerTests) +target_link_libraries( + RCCBackendCodeGenMachinePassManagerTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test( + NAME RCCBackend.CodeGen.MachinePassManager + COMMAND RCCBackendCodeGenMachinePassManagerTests +) + +add_subdirectory(GlobalISel) + +add_executable(RCCBackendCodeGenMachineAnalysesTests MachineAnalysesTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenMachineAnalysesTests) +target_link_libraries( + RCCBackendCodeGenMachineAnalysesTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test(NAME RCCBackend.CodeGen.MachineAnalyses COMMAND RCCBackendCodeGenMachineAnalysesTests) + +add_executable(RCCBackendCodeGenMachineOptimizationTests MachineOptimizationTest.cpp) +target_include_directories( + RCCBackendCodeGenMachineOptimizationTests + PRIVATE "${RCC_BACKEND_SOURCE_DIR}/lib/Target/RISCV" +) +rcc_backend_configure_target(RCCBackendCodeGenMachineOptimizationTests) +target_link_libraries( + RCCBackendCodeGenMachineOptimizationTests + PRIVATE RCCBackend::CodeGen RCCBackend::RISCV +) +add_test(NAME RCCBackend.CodeGen.MachineOptimization COMMAND RCCBackendCodeGenMachineOptimizationTests) + +add_executable(RCCBackendCodeGenMachineSchedulingTests MachineSchedulingTest.cpp) +target_include_directories( + RCCBackendCodeGenMachineSchedulingTests + PRIVATE "${RCC_BACKEND_SOURCE_DIR}/lib/Target/RISCV" +) +rcc_backend_configure_target(RCCBackendCodeGenMachineSchedulingTests) +target_link_libraries( + RCCBackendCodeGenMachineSchedulingTests + PRIVATE RCCBackend::CodeGen RCCBackend::RISCV +) +add_test(NAME RCCBackend.CodeGen.MachineScheduling COMMAND RCCBackendCodeGenMachineSchedulingTests) + +add_executable(RCCBackendCodeGenPHIEliminationTests PHIEliminationTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenPHIEliminationTests) +target_link_libraries( + RCCBackendCodeGenPHIEliminationTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test( + NAME RCCBackend.CodeGen.PHIElimination + COMMAND RCCBackendCodeGenPHIEliminationTests +) + +add_executable( + RCCBackendCodeGenTwoAddressInstructionPassTests + TwoAddressInstructionPassTest.cpp +) +rcc_backend_configure_target(RCCBackendCodeGenTwoAddressInstructionPassTests) +target_link_libraries( + RCCBackendCodeGenTwoAddressInstructionPassTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) +add_test( + NAME RCCBackend.CodeGen.TwoAddressInstructionPass + COMMAND RCCBackendCodeGenTwoAddressInstructionPassTests +) + +add_executable(RCCBackendCodeGenTargetConstantTests TargetConstantTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenTargetConstantTests) +target_link_libraries(RCCBackendCodeGenTargetConstantTests PRIVATE RCCBackend::CodeGen) + +add_test( + NAME RCCBackend.CodeGen.TargetConstant + COMMAND RCCBackendCodeGenTargetConstantTests +) + +add_executable(RCCBackendCodeGenTargetCallLoweringTests TargetCallLoweringTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenTargetCallLoweringTests) +target_link_libraries( + RCCBackendCodeGenTargetCallLoweringTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.TargetCallLowering + COMMAND RCCBackendCodeGenTargetCallLoweringTests +) + +add_executable(RCCBackendCodeGenTargetFunctionStateTests TargetFunctionStateTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenTargetFunctionStateTests) +target_link_libraries( + RCCBackendCodeGenTargetFunctionStateTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.TargetFunctionState + COMMAND RCCBackendCodeGenTargetFunctionStateTests +) + +add_executable(RCCBackendCodeGenTargetMIRTextInfoTests TargetMIRTextInfoTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenTargetMIRTextInfoTests) +target_link_libraries( + RCCBackendCodeGenTargetMIRTextInfoTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.TargetMIRTextInfo + COMMAND RCCBackendCodeGenTargetMIRTextInfoTests +) + +add_executable(RCCBackendCodeGenTargetRegisterInfoTests TargetRegisterInfoTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenTargetRegisterInfoTests) +target_link_libraries( + RCCBackendCodeGenTargetRegisterInfoTests + PRIVATE RCCBackend::CodeGen RCCBackend::TestTarget +) + +add_test( + NAME RCCBackend.CodeGen.TargetRegisterInfo + COMMAND RCCBackendCodeGenTargetRegisterInfoTests +) + +rcc_backend_register_format_files( + "${CMAKE_CURRENT_SOURCE_DIR}/CompilationUnitTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/CoreTypesTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/GenericOpcodesTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/InstructionDescriptorTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineCompilationStatusTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineConstantPoolTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineDebugInfoTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineFrameInfoTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineFunctionClonerTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineFunctionRecordsTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineIRAttributesTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineIRBuilderTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineIRCoreTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineAnalysesTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineJumpTableInfoTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/SlotIndexesTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/LiveVariablesTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/LiveIntervalTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/LiveIntervalsTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/LivePhysRegsTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/LiveRegUnitsTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/LiveTestSupport.h" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineMetadataTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineOperandTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineOptimizationTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/PassManagerTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/PassInstrumentationTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachinePassManagerTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineSchedulingTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/PBQPTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/RegisterAllocationTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/PHIEliminationTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/SSAExitTestSupport.h" + "${CMAKE_CURRENT_SOURCE_DIR}/TwoAddressInstructionPassTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineVerifierTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineVerifierTargetTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MachineValueTypesTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/TargetConstantTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/TargetCallLoweringTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/TargetFunctionStateTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/TargetMIRTextInfoTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/TargetRegisterInfoTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/RegisterAllocationTestSupport.h" + "${CMAKE_CURRENT_SOURCE_DIR}/LiveRegMatrixTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/VirtRegMapTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/SpillerTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/RegAllocBasicTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/RegAllocFastTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/RegAllocPBQPTest.cpp" +) diff --git a/backend/unittests/CodeGen/CompilationUnitTest.cpp b/backend/unittests/CodeGen/CompilationUnitTest.cpp new file mode 100644 index 0000000..141d6db --- /dev/null +++ b/backend/unittests/CodeGen/CompilationUnitTest.cpp @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/CompilationUnit.h" + +#include "rcc/CodeGen/MachineFunction.h" + +#include <cstddef> +#include <iostream> +#include <utility> + +namespace { + +int fail(const char *Message) { + std::cerr << Message << '\n'; + return 1; +} + +} // namespace + +int main() { + rcc::CompilationUnit Unit; + auto FirstAnonymous = Unit.createMachineFunction(""); + auto SecondAnonymous = Unit.createMachineFunction(""); + if (!FirstAnonymous || !SecondAnonymous || Unit.functionCount() != 2 || + (*FirstAnonymous)->stableID().value() != 1 || (*SecondAnonymous)->stableID().value() != 2 || + Unit.function("") != nullptr) + return fail("CompilationUnit must retain anonymous functions by order and StableID"); + + auto First = Unit.createMachineFunction("first"); + if (!First || (*First)->stableID().value() != 3) + return fail("CompilationUnit must allocate the first stable function identity"); + + auto Duplicate = Unit.createMachineFunction("first"); + if (Duplicate || Unit.functionCount() != 3) + return fail("CompilationUnit must reject duplicate function names atomically"); + + auto Second = Unit.createMachineFunction("second"); + if (!Second || (*Second)->stableID().value() != 4) + return fail("Rejected function creation must not consume a StableID"); + if (Unit.functionCount() != 4 || Unit.functionAt(0) != *FirstAnonymous || + Unit.functionAt(1) != *SecondAnonymous || Unit.functionAt(2) != *First || + Unit.functionAt(3) != *Second) + return fail("CompilationUnit must preserve function document order"); + if (Unit.function("first") != *First || Unit.function("missing") != nullptr) + return fail("CompilationUnit must provide exact name lookup"); + if (Unit.function((*Second)->stableID()) != *Second || + Unit.function(rcc::MachineFunctionStableID::fromValue(99)) != nullptr) + return fail("CompilationUnit must provide exact StableID lookup"); + if (Unit.functionAt(4) != nullptr) + return fail("CompilationUnit must reject an out-of-range ordered lookup"); + + auto Zero = rcc::GlobalInitializer::zeroFill(4); + if (!Zero) + return fail("Global initializer creation must succeed"); + rcc::GlobalObjectDesc Object; + Object.Symbol = "global"; + Object.ObjectType = rcc::GenericConstantType::byte(); + Object.Size = 4; + Object.Initializer.emplace(std::move(*Zero)); + auto Global = Unit.addGlobalObject(std::move(Object)); + if (!Global || Unit.globalObjectCount() != 1 || Unit.globalObject("global") != *Global || + Unit.globalObjectAt(0) != *Global || Unit.globalObjectAt(1) != nullptr) + return fail("CompilationUnit must own and index global objects"); + + auto ConflictingFunction = Unit.createMachineFunction("global"); + if (ConflictingFunction || Unit.functionCount() != 4) + return fail("CompilationUnit must reject function/global symbol conflicts atomically"); + + rcc::GlobalObjectDesc ConflictingGlobal; + ConflictingGlobal.Symbol = "first"; + ConflictingGlobal.ObjectType = rcc::GenericConstantType::byte(); + ConflictingGlobal.Size = 1; + auto Byte = rcc::GlobalInitializer::bytes({std::byte{1}}); + if (!Byte) + return fail("Byte initializer creation must succeed"); + ConflictingGlobal.Initializer.emplace(std::move(*Byte)); + if (Unit.addGlobalObject(std::move(ConflictingGlobal)) || Unit.globalObjectCount() != 1) + return fail("CompilationUnit must reject global/function symbol conflicts atomically"); + + auto MismatchedBytes = rcc::GlobalInitializer::bytes({std::byte{1}, std::byte{2}}); + if (!MismatchedBytes) + return fail("Mismatched initializer construction must succeed before object verification"); + rcc::GlobalObjectDesc MismatchedObject; + MismatchedObject.Symbol = "mismatched"; + MismatchedObject.ObjectType = rcc::GenericConstantType::byte(); + MismatchedObject.Size = 1; + MismatchedObject.Initializer.emplace(std::move(*MismatchedBytes)); + if (Unit.addGlobalObject(std::move(MismatchedObject)) || Unit.globalObjectCount() != 1) + return fail("CompilationUnit must reject mismatched global initializer sizes atomically"); + + auto Verification = Unit.verify(); + if (!Verification) + return fail("A valid CompilationUnit must pass verification"); + + rcc::CompilationUnit Moved = std::move(Unit); + if (Moved.functionCount() != 4 || Moved.functionAt(0) != *FirstAnonymous || + Moved.functionAt(1) != *SecondAnonymous || Moved.function("first") != *First || + Moved.function("second") != *Second || Moved.globalObject("global") != *Global) + return fail("Moving a CompilationUnit must preserve owned object addresses and indices"); + Verification = Moved.verify(); + if (!Verification) + return fail("A moved CompilationUnit must pass verification"); + return 0; +} diff --git a/backend/unittests/CodeGen/CoreTypesTest.cpp b/backend/unittests/CodeGen/CoreTypesTest.cpp new file mode 100644 index 0000000..8ad0ce6 --- /dev/null +++ b/backend/unittests/CodeGen/CoreTypesTest.cpp @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/LowLevelType.h" +#include "rcc/CodeGen/MachineFunctionProperties.h" +#include "rcc/CodeGen/Register.h" +#include "rcc/CodeGen/StableID.h" + +#include <cstdint> +#include <iostream> +#include <string_view> +#include <type_traits> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +void testLowLevelTypes(TestContext &Context) { + const rcc::LowLevelType Invalid; + Context.expect(!Invalid.isValid(), "default LowLevelType must be invalid"); + Context.expect(Invalid.toString() == "LLT_invalid", + "invalid LowLevelType spelling must be stable"); + + const rcc::LowLevelType Token = rcc::LowLevelType::token(); + Context.expect(Token.isToken(), "token LowLevelType must preserve its kind"); + Context.expect(Token.scalarBitWidth() == 0, "token LowLevelType must have no bit width"); + Context.expect(Token.toString() == "s0", "token LowLevelType must use the MIR token spelling"); + + auto Scalar = rcc::LowLevelType::makeCategoryNeutralScalar(3); + auto SameScalar = rcc::LowLevelType::makeCategoryNeutralScalar(3); + Context.expect(Scalar && SameScalar, "arbitrary positive scalar widths must be accepted"); + if (Scalar && SameScalar) { + Context.expect(Scalar->isCategoryNeutralScalar(), + "scalar LowLevelType must preserve its category-neutral kind"); + Context.expect(*Scalar == *SameScalar, + "equal-width scalars must have one canonical low-level type"); + Context.expect(Scalar->toString() == "s3", + "scalar LowLevelType spelling must include its width"); + } + + Context.expect(!rcc::LowLevelType::makeCategoryNeutralScalar(0), + "zero-width scalar LowLevelType must be rejected"); + Context.expect(!rcc::LowLevelType::makeCategoryNeutralScalar(std::uint64_t{1} << 16), + "scalar LowLevelType wider than the encoding must be rejected"); + + auto Pointer = rcc::LowLevelType::makePointer(rcc::LowLevelType::MaxAddressSpace, 64); + Context.expect(Pointer.has_value(), "maximum encodable pointer address space must be accepted"); + if (Pointer) { + Context.expect(Pointer->isPointer(), "pointer LowLevelType must preserve its kind"); + Context.expect(Pointer->addressSpace() == rcc::LowLevelType::MaxAddressSpace, + "pointer LowLevelType must preserve its address space"); + Context.expect(Pointer->toString() == "p16777215:64", + "pointer LowLevelType spelling must preserve address space and width"); + } + Context.expect(!rcc::LowLevelType::makePointer(std::uint64_t{1} << 24, 64), + "pointer address spaces wider than the encoding must be rejected"); + + auto I33 = rcc::LowLevelType::makeCategoryNeutralScalar(33); + if (!I33) { + Context.expect(false, "i33 construction must succeed before vector tests"); + return; + } + + auto FixedVector = rcc::LowLevelType::makeFixedVector(4, *I33); + Context.expect(FixedVector.has_value(), "fixed integer vector LowLevelType must be accepted"); + if (FixedVector) { + Context.expect(FixedVector->isVector(), "fixed vector LowLevelType must preserve vector kind"); + Context.expect(!FixedVector->isScalableVector(), + "fixed vector LowLevelType must not be marked scalable"); + Context.expect(FixedVector->minimumElementCount() == 4, + "fixed vector LowLevelType must preserve element count"); + Context.expect(FixedVector->scalarElementType() == I33, + "fixed vector LowLevelType must preserve its scalar element type"); + Context.expect(FixedVector->toString() == "<4 x s33>", + "fixed vector LowLevelType spelling must be stable"); + } + + auto S16 = rcc::LowLevelType::makeCategoryNeutralScalar(16); + if (!S16) { + Context.expect(false, "s16 construction must succeed before vector tests"); + return; + } + auto ScalableVector = rcc::LowLevelType::makeScalableVector(1, *S16); + Context.expect(ScalableVector.has_value(), "scalable vector with one minimum lane must work"); + if (ScalableVector) { + Context.expect(ScalableVector->isScalableVector(), + "scalable vector LowLevelType must preserve scalability"); + Context.expect(ScalableVector->isVectorValue(), + "category-neutral vectors must preserve their value kind"); + Context.expect(ScalableVector->scalarElementType() == S16, + "scalable vectors must preserve their element type"); + Context.expect(ScalableVector->toString() == "<vscale x 1 x s16>", + "scalable vector spelling must preserve its shape"); + } + + Context.expect(!rcc::LowLevelType::makeFixedVector(1, *I33), + "single-lane fixed vectors must be rejected"); + Context.expect(!rcc::LowLevelType::makeScalableVector(0, *I33), + "zero-lane scalable vectors must be rejected"); + Context.expect(!rcc::LowLevelType::makeFixedVector(4, Token), + "token vector elements must be rejected"); + if (FixedVector) { + Context.expect(!rcc::LowLevelType::makeFixedVector(4, *FixedVector), + "nested LowLevelType vectors must be rejected"); + } +} + +void testRegisters(TestContext &Context) { + const rcc::Register NoRegister{0}; + Context.expect(NoRegister.isNoRegister(), "Register{0} must represent NoRegister"); + Context.expect(NoRegister.toString() == "noreg", "NoRegister spelling must be stable"); + + auto PhysReg = rcc::PhysicalRegister::create(7); + Context.expect(PhysReg.has_value(), "positive physical register numbers must be accepted"); + if (PhysReg) { + const rcc::Register Reg = rcc::Register::fromPhysical(*PhysReg); + Context.expect(Reg.isPhysical(), "physical Register must preserve its category"); + Context.expect(Reg.physicalRegister() == PhysReg, + "physical Register must round-trip through PhysicalRegister"); + Context.expect(Reg.toString() == "$physreg7", + "target-independent physical register spelling must be stable"); + } + + Context.expect(!rcc::PhysicalRegister::create(0), "physical register zero must remain reserved"); + Context.expect(!rcc::Register::makePhysical(std::uint64_t{1} << 31), + "out-of-range physical registers must be rejected"); + + auto FirstVirtual = rcc::Register::makeVirtual(0); + auto LastVirtual = rcc::Register::makeVirtual((std::uint64_t{1} << 31) - 1); + Context.expect(FirstVirtual && LastVirtual, + "the complete virtual register index domain must be accepted"); + if (FirstVirtual) { + Context.expect(FirstVirtual->isVirtual(), "virtual Register must preserve its category"); + Context.expect(FirstVirtual->virtualIndex() == 0, "virtual Register must preserve index zero"); + Context.expect(FirstVirtual->toString() == "%0", + "virtual Register spelling must match MIR numbering"); + } + Context.expect(!rcc::Register::makeVirtual(std::uint64_t{1} << 31), + "out-of-range virtual register indices must be rejected"); +} + +void testStableIDs(TestContext &Context) { + static_assert(!std::is_same_v<rcc::MachineFunctionStableID, rcc::MachineBlockStableID>); + static_assert(!std::is_same_v<rcc::MachineBlockStableID, rcc::MachineInstrStableID>); + static_assert(!std::is_copy_constructible_v<rcc::StableIDGenerator<rcc::MachineInstrStableID>>); + + const rcc::MachineInstrStableID Invalid; + Context.expect(!Invalid.isValid(), "zero StableID must be invalid"); + + rcc::StableIDGenerator<rcc::MachineInstrStableID> Generator; + auto First = Generator.allocate(); + auto Second = Generator.allocate(); + Context.expect(First && Second, "StableID generator must allocate IDs"); + if (First && Second) { + Context.expect(First->value() == 1 && Second->value() == 2, + "StableID generator must allocate monotonically"); + Context.expect(*First != *Second, "StableID generator must not reuse live IDs"); + } +} + +void testMachineFunctionProperties(TestContext &Context) { + using enum rcc::MachineFunctionProperty; + + rcc::MachineFunctionProperties Properties{IsSSA, TracksLiveness}; + Context.expect(Properties.count() == 2, "property set must count active properties"); + Context.expect(Properties.has(IsSSA), "property set must retain IsSSA"); + Context.expect(Properties.toString() == "IsSSA, TracksLiveness", + "property spelling must have deterministic enum order"); + + const auto Original = Properties; + auto MissingRequired = + Properties.applyTransition(rcc::MachineFunctionProperties{Legalized}, + rcc::MachineFunctionProperties{RegBankSelected}, {}); + Context.expect(!MissingRequired, "property transition must reject missing requirements"); + Context.expect(Properties == Original, "failed property transition must be atomic"); + if (!MissingRequired) { + Context.expect(MissingRequired.error().code() == rcc::ErrorCode::VerificationError, + "missing property requirements must be verification errors"); + } + + auto ConflictingTransition = Properties.applyTransition( + {}, rcc::MachineFunctionProperties{Selected}, rcc::MachineFunctionProperties{Selected}); + Context.expect(!ConflictingTransition, + "property transition must reject overlapping sets and clears"); + Context.expect(Properties == Original, "conflicting property transition must be atomic"); + + auto ValidTransition = Properties.applyTransition(rcc::MachineFunctionProperties{IsSSA}, + rcc::MachineFunctionProperties{NoPHIs}, + rcc::MachineFunctionProperties{IsSSA}); + Context.expect(ValidTransition.has_value(), "valid property transition must succeed"); + Context.expect(Properties.has(NoPHIs) && !Properties.has(IsSSA), + "valid property transition must apply sets and clears together"); + + auto RawRoundTrip = rcc::MachineFunctionProperties::fromRaw(Properties.rawValue()); + Context.expect(RawRoundTrip && *RawRoundTrip == Properties, + "known property masks must round-trip"); + Context.expect(!rcc::MachineFunctionProperties::fromRaw(std::uint64_t{1} << 63), + "unknown property bits must be rejected"); +} + +} // namespace + +int main() { + TestContext Context; + testLowLevelTypes(Context); + testRegisters(Context); + testStableIDs(Context); + testMachineFunctionProperties(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/GenericOpcodesTest.cpp b/backend/unittests/CodeGen/GenericOpcodesTest.cpp new file mode 100644 index 0000000..f946c90 --- /dev/null +++ b/backend/unittests/CodeGen/GenericOpcodesTest.cpp @@ -0,0 +1,748 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/GenericOpcodes.h" + +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineMemOperand.h" + +#include <array> +#include <bit> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <ranges> +#include <span> +#include <string> +#include <string_view> +#include <unordered_set> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +[[nodiscard]] std::vector<rcc::InstructionDescriptor> copyDescriptors() { + const auto Source = rcc::genericInstructionDescriptors(); + return {Source.begin(), Source.end()}; +} + +[[nodiscard]] rcc::MachineOperand registerOperand(rcc::Register Reg, bool IsDef) { + rcc::RegisterOperandFlags Flags; + Flags.set(IsDef ? rcc::RegisterOperandFlag::Define : rcc::RegisterOperandFlag::Use); + auto Operand = rcc::MachineOperand::registerOperand(Reg, Flags); + if (!Operand) + std::terminate(); + return std::move(*Operand); +} + +[[nodiscard]] rcc::Register createVirtualRegister(rcc::MachineFunction &Function, + rcc::LowLevelType Type) { + auto Reg = Function.registerInfo().createVirtualRegister(Type); + if (!Reg) + std::terminate(); + return *Reg; +} + +[[nodiscard]] rcc::MachineInstr &createInstruction(rcc::MachineFunction &Function, + rcc::GenericOpcode OpcodeValue, + std::vector<rcc::MachineOperand> Operands) { + auto Instruction = Function.createInstruction(rcc::opcode(OpcodeValue)); + if (!Instruction) + std::terminate(); + for (rcc::MachineOperand &Operand : Operands) { + if (!(*Instruction)->addOperand(std::move(Operand))) + std::terminate(); + } + return **Instruction; +} + +[[nodiscard]] rcc::MachineMemOperandDesc +memoryDescription(rcc::MemoryAccessFlags Accesses, std::uint64_t Bytes, + std::uint32_t AddressSpace = 0, + rcc::AtomicOperation AtomicKind = rcc::AtomicOperation::None) { + rcc::MachineMemOperandDesc Description; + Description.Accesses = Accesses; + Description.AtomicKind = AtomicKind; + if (AtomicKind == rcc::AtomicOperation::ReadModifyWrite) { + Description.SuccessOrdering = rcc::AtomicOrdering::AcquireRelease; + } else if (AtomicKind == rcc::AtomicOperation::CompareExchange) { + Description.SuccessOrdering = rcc::AtomicOrdering::AcquireRelease; + Description.FailureOrdering = rcc::AtomicOrdering::Acquire; + } + Description.Size = *rcc::MemorySize::fixed(Bytes); + Description.Align = *rcc::Alignment::create(std::bit_floor(Bytes)); + Description.AddressSpace = AddressSpace; + Description.Base = rcc::MemoryBase::stack(); + return Description; +} + +void addMemOperand(rcc::MachineFunction &Function, rcc::MachineInstr &Instruction, + rcc::MachineMemOperandDesc Description) { + auto Memory = Function.createMemOperand(std::move(Description)); + if (!Memory || !Instruction.addMemOperand(**Memory)) + std::terminate(); +} + +void testCompleteRegistry(TestContext &Context) { + static_assert(rcc::CorePseudoOpcodeCount == 52); + static_assert(rcc::GenericOpcodeCount == 278); + + const auto Descriptors = rcc::genericInstructionDescriptors(); + Context.expect(Descriptors.size() == 330, + "the target-independent registry must contain 52 core and 278 generic opcodes"); + + std::unordered_set<std::string> Names; + std::unordered_set<rcc::Opcode> Opcodes; + bool ClassesAreCorrect = true; + for (const rcc::InstructionDescriptor &Descriptor : Descriptors) { + ClassesAreCorrect &= Descriptor.InstructionOpcode.isValid() && !Descriptor.Name.empty() && + Names.insert(Descriptor.Name).second && + Opcodes.insert(Descriptor.InstructionOpcode).second; + if (rcc::isGenericOpcode(Descriptor.InstructionOpcode)) { + ClassesAreCorrect &= Descriptor.Flags.has(rcc::InstructionFlag::Generic) && + !Descriptor.Flags.has(rcc::InstructionFlag::Pseudo); + } else { + ClassesAreCorrect &= rcc::isCorePseudoOpcode(Descriptor.InstructionOpcode) && + Descriptor.Flags.has(rcc::InstructionFlag::Pseudo) && + !Descriptor.Flags.has(rcc::InstructionFlag::Generic); + } + } + Context.expect(ClassesAreCorrect, + "every target-independent opcode must have one valid name, value, and class"); + Context.expect(rcc::getGenericOpcodeName(rcc::GenericOpcode::PHI) == "PHI" && + rcc::getGenericOpcodeName(rcc::GenericOpcode::G_ASSERT_SEXT) == + "G_ASSERT_SEXT" && + rcc::getGenericOpcodeName(rcc::GenericOpcode::G_UBFX) == "G_UBFX" && + rcc::getGenericOpcodeName(rcc::GenericOpcode::Invalid).empty(), + "opcode names must cover both ranges without assigning marker names"); + + auto Set = rcc::InstructionSet::create(copyDescriptors()); + Context.expect(Set.has_value(), "the complete target-independent descriptor set must validate"); +} + +void testCoreOpcodeContracts(TestContext &Context) { + struct Contract final { + rcc::GenericOpcode OpcodeValue; + std::size_t DescriptorOperands; + std::size_t ExplicitDefs; + bool Variadic; + std::size_t MinimumRepetitions; + bool Meta; + bool TerminalEmission; + rcc::PseudoOwner Owner; + rcc::PipelineStage Deadline; + }; + +#define RCC_META(Name, Operands, Defs, Variadic, Minimum) \ + Contract{rcc::GenericOpcode::Name, \ + Operands, \ + Defs, \ + Variadic, \ + Minimum, \ + true, \ + false, \ + rcc::PseudoOwner::None, \ + rcc::PipelineStage::Emission} +#define RCC_PSEUDO(Name, Operands, Defs, Variadic, Minimum, Owner, Deadline) \ + Contract{rcc::GenericOpcode::Name, \ + Operands, \ + Defs, \ + Variadic, \ + Minimum, \ + false, \ + false, \ + rcc::PseudoOwner::Owner, \ + rcc::PipelineStage::Deadline} +#define RCC_TERMINAL(Name, Operands, Defs, Variadic, Minimum) \ + Contract { \ + rcc::GenericOpcode::Name, Operands, Defs, Variadic, Minimum, false, true, \ + rcc::PseudoOwner::AsmPrinter, rcc::PipelineStage::Emission \ + } + + const std::array Contracts{ + RCC_PSEUDO(PHI, 3, 1, true, 1, PhiElimination, SSAEliminated), + RCC_TERMINAL(INLINEASM, 1, 0, true, 0), + RCC_TERMINAL(INLINEASM_BR, 1, 0, true, 0), + RCC_META(CFI_INSTRUCTION, 1, 0, false, 0), + RCC_META(EH_LABEL, 1, 0, false, 0), + RCC_META(GC_LABEL, 1, 0, false, 0), + RCC_META(ANNOTATION_LABEL, 1, 0, false, 0), + RCC_META(KILL, 1, 0, true, 0), + RCC_PSEUDO(EXTRACT_SUBREG, 3, 1, false, 0, PostRAExpansion, PostRAFinalized), + RCC_PSEUDO(INSERT_SUBREG, 4, 1, false, 0, TwoAddressRewrite, TiedOpsRewritten), + RCC_META(IMPLICIT_DEF, 1, 1, false, 0), + RCC_PSEUDO(INIT_UNDEF, 1, 1, false, 0, RegisterAllocator, RegisterAllocated), + RCC_PSEUDO(SUBREG_TO_REG, 3, 1, false, 0, PostRAExpansion, PostRAFinalized), + RCC_PSEUDO(COPY_TO_REGCLASS, 3, 1, false, 0, RegisterAllocator, RegisterAllocated), + RCC_META(DBG_VALUE, 1, 0, true, 0), + RCC_META(DBG_VALUE_LIST, 1, 0, true, 0), + RCC_META(DBG_INSTR_REF, 1, 0, true, 0), + RCC_META(DBG_PHI, 1, 0, true, 0), + RCC_META(DBG_LABEL, 1, 0, false, 0), + RCC_PSEUDO(REG_SEQUENCE, 3, 1, true, 1, TwoAddressRewrite, TiedOpsRewritten), + RCC_PSEUDO(COPY, 2, 1, false, 0, PostRAExpansion, PostRAFinalized), + RCC_PSEUDO(COPY_LANEMASK, 3, 1, false, 0, PostRAExpansion, PostRAFinalized), + RCC_META(BUNDLE, 1, 0, true, 0), + RCC_META(LIFETIME_START, 1, 0, false, 0), + RCC_META(LIFETIME_END, 1, 0, false, 0), + RCC_META(PSEUDO_PROBE, 4, 0, false, 0), + RCC_META(ARITH_FENCE, 2, 1, false, 0), + RCC_TERMINAL(STACKMAP, 3, 0, true, 0), + RCC_TERMINAL(FENTRY_CALL, 0, 0, false, 0), + RCC_TERMINAL(PATCHPOINT, 7, 1, true, 0), + RCC_PSEUDO(LOAD_STACK_GUARD, 1, 1, false, 0, InstructionSelect, Selected), + RCC_PSEUDO(PREALLOCATED_SETUP, 1, 0, false, 0, InstructionSelect, Selected), + RCC_PSEUDO(PREALLOCATED_ARG, 3, 1, false, 0, InstructionSelect, Selected), + RCC_TERMINAL(STATEPOINT, 1, 0, true, 0), + RCC_META(LOCAL_ESCAPE, 2, 0, false, 0), + RCC_TERMINAL(FAULTING_OP, 2, 1, true, 0), + RCC_TERMINAL(PATCHABLE_OP, 1, 0, true, 0), + RCC_TERMINAL(PATCHABLE_FUNCTION_ENTER, 0, 0, false, 0), + RCC_TERMINAL(PATCHABLE_RET, 1, 0, true, 0), + RCC_TERMINAL(PATCHABLE_FUNCTION_EXIT, 0, 0, false, 0), + RCC_TERMINAL(PATCHABLE_TAIL_CALL, 1, 0, true, 0), + RCC_TERMINAL(PATCHABLE_EVENT_CALL, 2, 0, false, 0), + RCC_TERMINAL(PATCHABLE_TYPED_EVENT_CALL, 3, 0, false, 0), + RCC_PSEUDO(ICALL_BRANCH_FUNNEL, 1, 0, true, 0, PostRAExpansion, PostRAFinalized), + RCC_META(FAKE_USE, 1, 0, true, 0), + RCC_META(MEMBARRIER, 0, 0, false, 0), + RCC_META(JUMP_TABLE_DEBUG_INFO, 1, 0, false, 0), + RCC_META(RELOC_NONE, 1, 0, false, 0), + RCC_META(CONVERGENCECTRL_ENTRY, 1, 1, false, 0), + RCC_META(CONVERGENCECTRL_ANCHOR, 1, 1, false, 0), + RCC_META(CONVERGENCECTRL_LOOP, 2, 1, false, 0), + RCC_META(CONVERGENCECTRL_GLUE, 1, 0, false, 0), + }; +#undef RCC_TERMINAL +#undef RCC_PSEUDO +#undef RCC_META + + static_assert(Contracts.size() == rcc::CorePseudoOpcodeCount); + const auto Descriptors = rcc::genericInstructionDescriptors(); + bool Matches = true; + for (const Contract &Expected : Contracts) { + const auto Found = std::ranges::find_if( + Descriptors, [&Expected](const rcc::InstructionDescriptor &Descriptor) { + return Descriptor.InstructionOpcode == rcc::opcode(Expected.OpcodeValue); + }); + if (Found == Descriptors.end()) { + Matches = false; + continue; + } + Matches &= + Found->Operands.size() == Expected.DescriptorOperands && + Found->NumExplicitDefs == Expected.ExplicitDefs && Found->Variadic == Expected.Variadic && + Found->MinimumVariadicRepetitions == Expected.MinimumRepetitions && + Found->Flags.has(rcc::InstructionFlag::Meta) == Expected.Meta && + Found->Flags.has(rcc::InstructionFlag::TerminalEmission) == Expected.TerminalEmission && + Found->Owner == Expected.Owner && Found->Deadline == Expected.Deadline; + } + Context.expect(Matches, + "all 52 core opcodes must retain their reviewed shape and lifecycle contracts"); + + const auto InlineAsm = std::ranges::find_if(Descriptors, [](const auto &Descriptor) { + return Descriptor.InstructionOpcode == rcc::opcode(rcc::GenericOpcode::INLINEASM); + }); + const auto InlineAsmBr = std::ranges::find_if(Descriptors, [](const auto &Descriptor) { + return Descriptor.InstructionOpcode == rcc::opcode(rcc::GenericOpcode::INLINEASM_BR); + }); + const auto Copy = std::ranges::find_if(Descriptors, [](const auto &Descriptor) { + return Descriptor.InstructionOpcode == rcc::opcode(rcc::GenericOpcode::COPY); + }); + Context.expect(InlineAsm != Descriptors.end() && InlineAsmBr != Descriptors.end() && + !InlineAsm->Flags.has(rcc::InstructionFlag::HasSideEffects) && + InlineAsmBr->Flags.has(rcc::InstructionFlag::HasSideEffects), + "INLINEASM_BR must be intrinsically side-effecting while INLINEASM uses its " + "operand flags"); + Context.expect(Copy != Descriptors.end() && Copy->AllowExtraImplicitOperands, + "COPY must allow dynamic implicit super-register operands"); +} + +void testShapesAndTypes(TestContext &Context) { + auto Set = rcc::InstructionSet::create(copyDescriptors()); + auto Function = + rcc::MachineFunction::create("generic", rcc::MachineFunctionStableID::fromValue(1)); + Context.expect(Set.has_value() && Function.has_value(), + "generic opcode tests require a descriptor set and machine function"); + if (!Set || !Function) + return; + + auto I32 = rcc::LowLevelType::makeCategoryNeutralScalar(32); + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto I1 = rcc::LowLevelType::makeCategoryNeutralScalar(1); + auto S16 = rcc::LowLevelType::makeCategoryNeutralScalar(16); + auto F32 = rcc::LowLevelType::makeCategoryNeutralScalar(32); + auto F64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto P64 = rcc::LowLevelType::makePointer(0, 64); + auto V2I32 = + I32 ? rcc::LowLevelType::makeFixedVector(2, *I32) : rcc::Expected<rcc::LowLevelType>{}; + auto V4I32 = + I32 ? rcc::LowLevelType::makeFixedVector(4, *I32) : rcc::Expected<rcc::LowLevelType>{}; + auto V2P64 = + P64 ? rcc::LowLevelType::makeFixedVector(2, *P64) : rcc::Expected<rcc::LowLevelType>{}; + if (!I1 || !I32 || !I64 || !S16 || !F32 || !F64 || !P64 || !V2I32 || !V4I32 || !V2P64) + std::terminate(); + auto Dst32 = (*Function)->registerInfo().createVirtualRegister(*I32); + auto Src32A = (*Function)->registerInfo().createVirtualRegister(*I32); + auto Src32B = (*Function)->registerInfo().createVirtualRegister(*I32); + auto Wide = (*Function)->registerInfo().createVirtualRegister(*I64); + auto Address = (*Function)->registerInfo().createVirtualRegister(*P64); + auto NewAddress = (*Function)->registerInfo().createVirtualRegister(*P64); + const rcc::Register Cond = createVirtualRegister(**Function, *I1); + const rcc::Register Float32 = createVirtualRegister(**Function, *F32); + const rcc::Register Float64 = createVirtualRegister(**Function, *F64); + const rcc::Register Scalar16 = createVirtualRegister(**Function, *S16); + const rcc::Register Vec2A = createVirtualRegister(**Function, *V2I32); + const rcc::Register Vec2B = createVirtualRegister(**Function, *V2I32); + const rcc::Register Vec4 = createVirtualRegister(**Function, *V4I32); + const rcc::Register PointerVector = createVirtualRegister(**Function, *V2P64); + if (!Dst32 || !Src32A || !Src32B || !Wide || !Address || !NewAddress) + std::terminate(); + + std::vector<rcc::MachineOperand> AddOperands; + AddOperands.push_back(registerOperand(*Dst32, true)); + AddOperands.push_back(registerOperand(*Src32A, false)); + AddOperands.push_back(registerOperand(*Src32B, false)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ADD), AddOperands, + (*Function)->registerInfo()) + .has_value(), + "same-typed integer operands must satisfy G_ADD"); + AddOperands.back() = registerOperand(*Wide, false); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ADD), AddOperands, + (*Function)->registerInfo()), + "G_ADD must reject a source from a different LowLevelType equality group"); + + auto ZeroImmediate = rcc::APInt::fromUnsigned(64, 0); + std::vector<rcc::MachineOperand> PointerConstant; + PointerConstant.push_back(registerOperand(*Address, true)); + if (ZeroImmediate) + PointerConstant.push_back(rcc::MachineOperand::integerImmediate(std::move(*ZeroImmediate))); + Context.expect(PointerConstant.size() == 2 && + Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_CONSTANT), + PointerConstant, (*Function)->registerInfo()) + .has_value(), + "G_CONSTANT must accept a scalar pointer destination"); + + auto Block0 = (*Function)->createBasicBlock(); + auto Block1 = (*Function)->createBasicBlock(); + if (!Block0 || !Block1 || !(*Function)->appendBlock(**Block0) || + !(*Function)->appendBlock(**Block1)) { + std::terminate(); + } + std::vector<rcc::MachineOperand> PhiOperands; + PhiOperands.push_back(registerOperand(*Dst32, true)); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_PHI), PhiOperands, + (*Function)->registerInfo()), + "G_PHI must require at least one value and predecessor pair"); + PhiOperands.push_back(registerOperand(*Src32A, false)); + PhiOperands.push_back(rcc::MachineOperand::basicBlock(**Block0)); + PhiOperands.push_back(registerOperand(*Src32B, false)); + PhiOperands.push_back(rcc::MachineOperand::basicBlock(**Block1)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_PHI), PhiOperands, + (*Function)->registerInfo()) + .has_value(), + "G_PHI must accept repeated value and predecessor pairs"); + PhiOperands.pop_back(); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_PHI), PhiOperands, + (*Function)->registerInfo()), + "G_PHI must reject an incomplete variadic operand pair"); + + std::vector<rcc::MachineOperand> IndexedLoad; + IndexedLoad.push_back(registerOperand(*Dst32, true)); + IndexedLoad.push_back(registerOperand(*NewAddress, true)); + IndexedLoad.push_back(registerOperand(*Address, false)); + IndexedLoad.push_back(registerOperand(*Wide, false)); + IndexedLoad.push_back(rcc::MachineOperand::immediate(1)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_INDEXED_LOAD), + IndexedLoad, (*Function)->registerInfo()) + .has_value(), + "indexed loads must keep address and integer offset type groups independent"); + + std::vector<rcc::MachineOperand> UnmergeOperands; + UnmergeOperands.push_back(registerOperand(*Dst32, true)); + UnmergeOperands.push_back(registerOperand(*Wide, false)); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_UNMERGE_VALUES), + UnmergeOperands, (*Function)->registerInfo()), + "G_UNMERGE_VALUES must require at least two definitions"); + UnmergeOperands.insert(UnmergeOperands.begin() + 1, registerOperand(*Src32B, true)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_UNMERGE_VALUES), + UnmergeOperands, (*Function)->registerInfo()) + .has_value(), + "two homogeneous halves must exactly cover a G_UNMERGE_VALUES source"); + + std::vector<rcc::MachineOperand> MergeOperands; + MergeOperands.push_back(registerOperand(*Wide, true)); + MergeOperands.push_back(registerOperand(*Src32A, false)); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_MERGE_VALUES), + MergeOperands, (*Function)->registerInfo()), + "G_MERGE_VALUES must require at least two sources"); + MergeOperands.push_back(registerOperand(*Src32B, false)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_MERGE_VALUES), + MergeOperands, (*Function)->registerInfo()) + .has_value(), + "two homogeneous sources must exactly fill a G_MERGE_VALUES result"); + + std::vector<rcc::MachineOperand> ConcatOperands; + ConcatOperands.push_back(registerOperand(Vec4, true)); + ConcatOperands.push_back(registerOperand(Vec2A, false)); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_CONCAT_VECTORS), + ConcatOperands, (*Function)->registerInfo()), + "G_CONCAT_VECTORS must require at least two source vectors"); + ConcatOperands.push_back(registerOperand(Vec2B, false)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_CONCAT_VECTORS), + ConcatOperands, (*Function)->registerInfo()) + .has_value(), + "two vector sources must exactly tile a G_CONCAT_VECTORS result"); + + std::vector<rcc::MachineOperand> FrameIndexOperands; + FrameIndexOperands.push_back(registerOperand(PointerVector, true)); + FrameIndexOperands.push_back(rcc::MachineOperand::frameIndex(rcc::FrameIndex::fromValue(0))); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_FRAME_INDEX), + FrameIndexOperands, (*Function)->registerInfo()), + "G_FRAME_INDEX must reject a vector-of-pointers result"); + + std::vector<rcc::MachineOperand> PtrAuthOperands; + PtrAuthOperands.push_back(registerOperand(*NewAddress, true)); + PtrAuthOperands.push_back(registerOperand(*Address, false)); + PtrAuthOperands.push_back(rcc::MachineOperand::immediate(0)); + PtrAuthOperands.push_back(registerOperand(*Wide, false)); + PtrAuthOperands.push_back(rcc::MachineOperand::immediate(7)); + Context.expect( + Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_PTRAUTH_GLOBAL_VALUE), + PtrAuthOperands, (*Function)->registerInfo()) + .has_value(), + "G_PTRAUTH_GLOBAL_VALUE must accept pointer and discriminator registers"); + auto Symbol = rcc::MachineOperand::globalSymbol("global"); + if (!Symbol) + std::terminate(); + PtrAuthOperands[1] = std::move(*Symbol); + Context.expect( + !Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_PTRAUTH_GLOBAL_VALUE), + PtrAuthOperands, (*Function)->registerInfo()), + "G_PTRAUTH_GLOBAL_VALUE must reject a symbol in place of its address register"); + + std::vector<rcc::MachineOperand> ExtensionOperands; + ExtensionOperands.push_back(registerOperand(*Wide, true)); + ExtensionOperands.push_back(registerOperand(*Src32A, false)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ZEXT), + ExtensionOperands, (*Function)->registerInfo()) + .has_value(), + "G_ZEXT must accept a wider destination with matching shape"); + ExtensionOperands[0] = registerOperand(*Dst32, true); + ExtensionOperands[1] = registerOperand(*Wide, false); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ZEXT), + ExtensionOperands, (*Function)->registerInfo()), + "G_ZEXT must reject a narrower destination"); + + auto Predicate = rcc::MachineOperand::integerPredicate(rcc::IntegerPredicate::Equal); + if (!Predicate) + std::terminate(); + std::vector<rcc::MachineOperand> CompareOperands; + CompareOperands.push_back(registerOperand(Cond, true)); + CompareOperands.push_back(std::move(*Predicate)); + CompareOperands.push_back(registerOperand(*Src32A, false)); + CompareOperands.push_back(registerOperand(*Src32B, false)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ICMP), + CompareOperands, (*Function)->registerInfo()) + .has_value(), + "G_ICMP must accept an i1 result and homogeneous integer sources"); + CompareOperands[0] = registerOperand(*Dst32, true); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ICMP), + CompareOperands, (*Function)->registerInfo()) + .has_value(), + "G_ICMP must accept a legalized integer result container"); + CompareOperands[2] = registerOperand(*Address, false); + CompareOperands[3] = registerOperand(*NewAddress, false); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ICMP), + CompareOperands, (*Function)->registerInfo()) + .has_value(), + "G_ICMP must accept homogeneous pointer sources"); + CompareOperands[3] = registerOperand(*Src32B, false); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_ICMP), + CompareOperands, (*Function)->registerInfo()), + "G_ICMP must reject mixed pointer and integer sources"); + + std::vector<rcc::MachineOperand> FloatingLoad; + FloatingLoad.push_back(registerOperand(Float64, true)); + FloatingLoad.push_back(registerOperand(*Address, false)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_FPEXTLOAD), + FloatingLoad, (*Function)->registerInfo()) + .has_value(), + "G_FPEXTLOAD must accept a scalar floating result and pointer address"); + + const std::array<std::uint64_t, 1> FloatWords{0}; + auto FloatBits = rcc::APFloat::create(rcc::FloatSemantics::IEEESingle, FloatWords); + if (!FloatBits) + std::terminate(); + std::vector<rcc::MachineOperand> FConstantOperands; + FConstantOperands.push_back(registerOperand(Float32, true)); + FConstantOperands.push_back(rcc::MachineOperand::floatingImmediate(*FloatBits)); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_FCONSTANT), + FConstantOperands, (*Function)->registerInfo()) + .has_value(), + "G_FCONSTANT payload semantics must match its scalar destination"); + FConstantOperands[0] = registerOperand(Float64, true); + Context.expect(!Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_FCONSTANT), + FConstantOperands, (*Function)->registerInfo()), + "G_FCONSTANT must reject mismatched floating semantics"); + + const std::array<std::uint64_t, 1> BFloatWords{0}; + auto BFloatBits = rcc::APFloat::create(rcc::FloatSemantics::BFloat, BFloatWords); + if (!BFloatBits) + std::terminate(); + FConstantOperands[0] = registerOperand(Scalar16, true); + FConstantOperands[1] = rcc::MachineOperand::floatingImmediate(*BFloatBits); + Context.expect(Set->validateExplicitOperands(rcc::opcode(rcc::GenericOpcode::G_FCONSTANT), + FConstantOperands, (*Function)->registerInfo()) + .has_value(), + "G_FCONSTANT must accept a bit-width-compatible LLVM scalar LowLevelType"); +} + +void testInstructionMemorySemantics(TestContext &Context) { + auto Set = rcc::InstructionSet::create(copyDescriptors()); + auto Function = + rcc::MachineFunction::create("generic_memory", rcc::MachineFunctionStableID::fromValue(2)); + Context.expect(Set && Function, "generic memory tests require an instruction set and function"); + if (!Set || !Function) + return; + + const rcc::LowLevelType I1 = *rcc::LowLevelType::makeCategoryNeutralScalar(1); + const rcc::LowLevelType I32 = *rcc::LowLevelType::makeCategoryNeutralScalar(32); + const rcc::LowLevelType I64 = *rcc::LowLevelType::makeCategoryNeutralScalar(64); + const rcc::LowLevelType F64 = *rcc::LowLevelType::makeCategoryNeutralScalar(64); + const rcc::LowLevelType P0 = *rcc::LowLevelType::makePointer(0, 64); + const rcc::LowLevelType P1 = *rcc::LowLevelType::makePointer(1, 64); + const rcc::Register Dst32 = createVirtualRegister(**Function, I32); + const rcc::Register Dst64 = createVirtualRegister(**Function, I64); + const rcc::Register Src32 = createVirtualRegister(**Function, I32); + const rcc::Register Src64 = createVirtualRegister(**Function, I64); + const rcc::Register Float64 = createVirtualRegister(**Function, F64); + const rcc::Register Status = createVirtualRegister(**Function, I1); + const rcc::Register Address0 = createVirtualRegister(**Function, P0); + const rcc::Register Address1 = createVirtualRegister(**Function, P1); + + std::vector<rcc::MachineOperand> LoadOperands; + LoadOperands.push_back(registerOperand(Dst64, true)); + LoadOperands.push_back(registerOperand(Address0, false)); + rcc::MachineInstr &Load = + createInstruction(**Function, rcc::GenericOpcode::G_LOAD, std::move(LoadOperands)); + addMemOperand(**Function, Load, memoryDescription({rcc::MemoryAccessFlag::Load}, 8)); + Context.expect(Set->validateInstruction(Load, (*Function)->registerInfo()).has_value(), + "G_LOAD must accept one matching load MMO"); + + std::vector<rcc::MachineOperand> OversizedLoadOperands; + OversizedLoadOperands.push_back(registerOperand(Dst32, true)); + OversizedLoadOperands.push_back(registerOperand(Address0, false)); + rcc::MachineInstr &OversizedLoad = + createInstruction(**Function, rcc::GenericOpcode::G_LOAD, std::move(OversizedLoadOperands)); + addMemOperand(**Function, OversizedLoad, memoryDescription({rcc::MemoryAccessFlag::Load}, 8)); + Context.expect(!Set->validateInstruction(OversizedLoad, (*Function)->registerInfo()), + "G_LOAD must reject an MMO larger than its result LowLevelType"); + + std::vector<rcc::MachineOperand> WrongAddressSpaceOperands; + WrongAddressSpaceOperands.push_back(registerOperand(Dst64, true)); + WrongAddressSpaceOperands.push_back(registerOperand(Address1, false)); + rcc::MachineInstr &WrongAddressSpace = createInstruction(**Function, rcc::GenericOpcode::G_LOAD, + std::move(WrongAddressSpaceOperands)); + addMemOperand(**Function, WrongAddressSpace, + memoryDescription({rcc::MemoryAccessFlag::Load}, 8, 0)); + Context.expect(!Set->validateInstruction(WrongAddressSpace, (*Function)->registerInfo()), + "generic memory access must match its pointer and MMO address spaces"); + + std::vector<rcc::MachineOperand> ExtLoadOperands; + ExtLoadOperands.push_back(registerOperand(Dst64, true)); + ExtLoadOperands.push_back(registerOperand(Address0, false)); + rcc::MachineInstr &ExtLoad = + createInstruction(**Function, rcc::GenericOpcode::G_ZEXTLOAD, std::move(ExtLoadOperands)); + addMemOperand(**Function, ExtLoad, memoryDescription({rcc::MemoryAccessFlag::Load}, 4)); + Context.expect(Set->validateInstruction(ExtLoad, (*Function)->registerInfo()).has_value(), + "G_ZEXTLOAD must accept a strictly narrower memory access"); + + std::vector<rcc::MachineOperand> NonExtendingLoadOperands; + NonExtendingLoadOperands.push_back(registerOperand(Dst64, true)); + NonExtendingLoadOperands.push_back(registerOperand(Address0, false)); + rcc::MachineInstr &NonExtendingLoad = createInstruction( + **Function, rcc::GenericOpcode::G_ZEXTLOAD, std::move(NonExtendingLoadOperands)); + addMemOperand(**Function, NonExtendingLoad, memoryDescription({rcc::MemoryAccessFlag::Load}, 8)); + Context.expect(!Set->validateInstruction(NonExtendingLoad, (*Function)->registerInfo()), + "G_ZEXTLOAD must reject a non-narrowing MMO"); + + std::vector<rcc::MachineOperand> CompareExchangeOperands; + CompareExchangeOperands.push_back(registerOperand(Dst64, true)); + CompareExchangeOperands.push_back(registerOperand(Status, true)); + CompareExchangeOperands.push_back(registerOperand(Address0, false)); + CompareExchangeOperands.push_back(registerOperand(Src64, false)); + CompareExchangeOperands.push_back(registerOperand(Src64, false)); + rcc::MachineInstr &CompareExchange = + createInstruction(**Function, rcc::GenericOpcode::G_ATOMIC_CMPXCHG_WITH_SUCCESS, + std::move(CompareExchangeOperands)); + addMemOperand(**Function, CompareExchange, + memoryDescription({rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Store}, 8, 0, + rcc::AtomicOperation::CompareExchange)); + Context.expect(Set->validateInstruction(CompareExchange, (*Function)->registerInfo()).has_value(), + "generic compare-exchange must accept a matching atomic MMO"); + + std::vector<rcc::MachineOperand> WrongAtomicOperands; + WrongAtomicOperands.push_back(registerOperand(Dst64, true)); + WrongAtomicOperands.push_back(registerOperand(Address0, false)); + WrongAtomicOperands.push_back(registerOperand(Src64, false)); + rcc::MachineInstr &WrongAtomic = createInstruction( + **Function, rcc::GenericOpcode::G_ATOMICRMW_ADD, std::move(WrongAtomicOperands)); + addMemOperand(**Function, WrongAtomic, + memoryDescription({rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Store}, 8, 0, + rcc::AtomicOperation::CompareExchange)); + Context.expect(!Set->validateInstruction(WrongAtomic, (*Function)->registerInfo()), + "generic atomic RMW must reject a compare-exchange MMO"); + + std::vector<rcc::MachineOperand> NarrowAtomicOperands; + NarrowAtomicOperands.push_back(registerOperand(Dst64, true)); + NarrowAtomicOperands.push_back(registerOperand(Address0, false)); + NarrowAtomicOperands.push_back(registerOperand(Src64, false)); + rcc::MachineInstr &NarrowAtomic = createInstruction( + **Function, rcc::GenericOpcode::G_ATOMICRMW_ADD, std::move(NarrowAtomicOperands)); + addMemOperand(**Function, NarrowAtomic, + memoryDescription({rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Store}, 1, 0, + rcc::AtomicOperation::ReadModifyWrite)); + Context.expect(Set->validateInstruction(NarrowAtomic, (*Function)->registerInfo()).has_value(), + "generic atomic RMW must accept an MMO narrower than its register container"); + + std::vector<rcc::MachineOperand> OversizedAtomicOperands; + OversizedAtomicOperands.push_back(registerOperand(Dst32, true)); + OversizedAtomicOperands.push_back(registerOperand(Address0, false)); + OversizedAtomicOperands.push_back(registerOperand(Src32, false)); + rcc::MachineInstr &OversizedAtomic = createInstruction( + **Function, rcc::GenericOpcode::G_ATOMICRMW_ADD, std::move(OversizedAtomicOperands)); + addMemOperand(**Function, OversizedAtomic, + memoryDescription({rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Store}, 8, 0, + rcc::AtomicOperation::ReadModifyWrite)); + Context.expect(!Set->validateInstruction(OversizedAtomic, (*Function)->registerInfo()), + "generic atomic RMW must reject an MMO wider than its register container"); + + std::vector<rcc::MachineOperand> FloatingAtomicOperands; + FloatingAtomicOperands.push_back(registerOperand(Float64, true)); + FloatingAtomicOperands.push_back(registerOperand(Address0, false)); + FloatingAtomicOperands.push_back(registerOperand(Float64, false)); + rcc::MachineInstr &FloatingAtomic = createInstruction( + **Function, rcc::GenericOpcode::G_ATOMICRMW_FADD, std::move(FloatingAtomicOperands)); + addMemOperand(**Function, FloatingAtomic, + memoryDescription({rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Store}, 8, 0, + rcc::AtomicOperation::ReadModifyWrite)); + Context.expect(Set->validateInstruction(FloatingAtomic, (*Function)->registerInfo()).has_value(), + "floating atomic RMW must accept a scalar floating value and RMW MMO"); + + std::vector<rcc::MachineOperand> PrefetchOperands; + PrefetchOperands.push_back(registerOperand(Address0, false)); + PrefetchOperands.push_back(rcc::MachineOperand::immediate(0)); + PrefetchOperands.push_back(rcc::MachineOperand::immediate(3)); + PrefetchOperands.push_back(rcc::MachineOperand::immediate(1)); + rcc::MachineInstr &Prefetch = + createInstruction(**Function, rcc::GenericOpcode::G_PREFETCH, std::move(PrefetchOperands)); + Context.expect(!Set->validateInstruction(Prefetch, (*Function)->registerInfo()), + "G_PREFETCH must require an MMO"); + addMemOperand(**Function, Prefetch, memoryDescription({rcc::MemoryAccessFlag::Load}, 1)); + Context.expect(Set->validateInstruction(Prefetch, (*Function)->registerInfo()).has_value(), + "read-prefetch selector must match a load-only MMO"); + + std::vector<rcc::MachineOperand> VarArgOperands; + VarArgOperands.push_back(registerOperand(Dst64, true)); + VarArgOperands.push_back(registerOperand(Address0, false)); + VarArgOperands.push_back(rcc::MachineOperand::immediate(8)); + rcc::MachineInstr &VarArg = + createInstruction(**Function, rcc::GenericOpcode::G_VAARG, std::move(VarArgOperands)); + Context.expect(Set->validateInstruction(VarArg, (*Function)->registerInfo()).has_value(), + "G_VAARG must accept LLVM's canonical form without MMOs"); + addMemOperand(**Function, VarArg, memoryDescription({rcc::MemoryAccessFlag::Load}, 8)); + addMemOperand(**Function, VarArg, memoryDescription({rcc::MemoryAccessFlag::Store}, 8)); + Context.expect(!Set->validateInstruction(VarArg, (*Function)->registerInfo()), + "G_VAARG must reject MMOs before legalization"); + + std::vector<rcc::MachineOperand> MemoryTransferOperands; + MemoryTransferOperands.push_back(registerOperand(Address0, false)); + MemoryTransferOperands.push_back(registerOperand(Address1, false)); + MemoryTransferOperands.push_back(registerOperand(Src64, false)); + MemoryTransferOperands.push_back(rcc::MachineOperand::immediate(0)); + rcc::MachineInstr &MemoryTransfer = createInstruction(**Function, rcc::GenericOpcode::G_MEMCPY, + std::move(MemoryTransferOperands)); + addMemOperand(**Function, MemoryTransfer, + memoryDescription({rcc::MemoryAccessFlag::Store}, 8, 0)); + addMemOperand(**Function, MemoryTransfer, memoryDescription({rcc::MemoryAccessFlag::Load}, 8, 1)); + Context.expect(Set->validateInstruction(MemoryTransfer, (*Function)->registerInfo()).has_value(), + "G_MEMCPY must accept equal-sized store/load MMOs in canonical order"); + + std::vector<rcc::MachineOperand> ReversedTransferOperands; + ReversedTransferOperands.push_back(registerOperand(Address0, false)); + ReversedTransferOperands.push_back(registerOperand(Address1, false)); + ReversedTransferOperands.push_back(registerOperand(Src64, false)); + ReversedTransferOperands.push_back(rcc::MachineOperand::immediate(0)); + rcc::MachineInstr &ReversedTransfer = createInstruction(**Function, rcc::GenericOpcode::G_MEMCPY, + std::move(ReversedTransferOperands)); + addMemOperand(**Function, ReversedTransfer, + memoryDescription({rcc::MemoryAccessFlag::Load}, 8, 0)); + addMemOperand(**Function, ReversedTransfer, + memoryDescription({rcc::MemoryAccessFlag::Store}, 8, 1)); + Context.expect(!Set->validateInstruction(ReversedTransfer, (*Function)->registerInfo()), + "G_MEMCPY must reject reversed MMO access directions"); +} + +void testSemanticFlags(TestContext &Context) { + const auto Descriptors = rcc::genericInstructionDescriptors(); + const auto Find = [Descriptors](rcc::GenericOpcode OpcodeValue) { + const rcc::Opcode Value = rcc::opcode(OpcodeValue); + const auto Position = + std::ranges::find_if(Descriptors, [Value](const rcc::InstructionDescriptor &Descriptor) { + return Descriptor.InstructionOpcode == Value; + }); + return Position == Descriptors.end() ? nullptr : &*Position; + }; + + const auto *Load = Find(rcc::GenericOpcode::G_LOAD); + const auto *Atomic = Find(rcc::GenericOpcode::G_ATOMIC_CMPXCHG); + const auto *VarArg = Find(rcc::GenericOpcode::G_VAARG); + const auto *Branch = Find(rcc::GenericOpcode::G_BRINDIRECT); + const auto *Strict = Find(rcc::GenericOpcode::G_STRICT_FMA); + const auto *InlineAsm = Find(rcc::GenericOpcode::INLINEASM); + Context.expect( + Load != nullptr && Load->Flags.has(rcc::InstructionFlag::MayLoad) && + Load->RequiresMemoryOperands && Atomic != nullptr && + Atomic->Flags.has(rcc::InstructionFlag::MayLoad) && + Atomic->Flags.has(rcc::InstructionFlag::MayStore) && Atomic->RequiresMemoryOperands && + VarArg != nullptr && VarArg->Flags.has(rcc::InstructionFlag::MayLoad) && + VarArg->Flags.has(rcc::InstructionFlag::MayStore) && !VarArg->RequiresMemoryOperands && + Branch != nullptr && Branch->Flags.has(rcc::InstructionFlag::Terminator) && + Branch->Flags.has(rcc::InstructionFlag::IndirectBranch) && + Branch->Flags.has(rcc::InstructionFlag::Barrier) && Strict != nullptr && + Strict->Flags.has(rcc::InstructionFlag::HasSideEffects) && InlineAsm != nullptr && + InlineAsm->Flags.has(rcc::InstructionFlag::TerminalEmission) && + InlineAsm->Owner == rcc::PseudoOwner::AsmPrinter, + "memory, control-flow, constrained-FP, and terminal flags must be explicit"); +} + +} // namespace + +int main() { + TestContext Context; + testCompleteRegistry(Context); + testCoreOpcodeContracts(Context); + testShapesAndTypes(Context); + testInstructionMemorySemantics(Context); + testSemanticFlags(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/GlobalISel/CMakeLists.txt b/backend/unittests/CodeGen/GlobalISel/CMakeLists.txt new file mode 100644 index 0000000..ba38de1 --- /dev/null +++ b/backend/unittests/CodeGen/GlobalISel/CMakeLists.txt @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +include_directories("${RCC_BACKEND_SOURCE_DIR}/lib/Target/RISCV") + +add_executable(RCCBackendCodeGenLegalizerTests LegalizerTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenLegalizerTests) +target_link_libraries( + RCCBackendCodeGenLegalizerTests + PRIVATE RCCBackend::CodeGen RCCBackend::RISCV RCCBackend::TestTarget +) +add_test(NAME RCCBackend.CodeGen.GlobalISel.Legalizer COMMAND RCCBackendCodeGenLegalizerTests) + +add_executable(RCCBackendCodeGenRegBankSelectTests RegBankSelectTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenRegBankSelectTests) +target_link_libraries( + RCCBackendCodeGenRegBankSelectTests + PRIVATE RCCBackend::CodeGen RCCBackend::RISCV RCCBackend::TestTarget +) +add_test( + NAME RCCBackend.CodeGen.GlobalISel.RegBankSelect + COMMAND RCCBackendCodeGenRegBankSelectTests +) + +add_executable(RCCBackendCodeGenInstructionSelectTests InstructionSelectTest.cpp) +rcc_backend_configure_target(RCCBackendCodeGenInstructionSelectTests) +target_link_libraries( + RCCBackendCodeGenInstructionSelectTests + PRIVATE RCCBackend::CodeGen RCCBackend::RISCV RCCBackend::TestTarget +) +add_test( + NAME RCCBackend.CodeGen.GlobalISel.InstructionSelect + COMMAND RCCBackendCodeGenInstructionSelectTests +) + +rcc_backend_register_format_files( + "${CMAKE_CURRENT_SOURCE_DIR}/GISelMITest.h" + "${CMAKE_CURRENT_SOURCE_DIR}/InstructionSelectTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/LegalizerTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/RegBankSelectTest.cpp" +) diff --git a/backend/unittests/CodeGen/GlobalISel/GISelMITest.h b/backend/unittests/CodeGen/GlobalISel/GISelMITest.h new file mode 100644 index 0000000..20e8854 --- /dev/null +++ b/backend/unittests/CodeGen/GlobalISel/GISelMITest.h @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RCC_UNITTESTS_CODEGEN_GLOBALISEL_GISELMITEST_H +#define RCC_UNITTESTS_CODEGEN_GLOBALISEL_GISELMITEST_H + +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <string> +#include <string_view> +#include <utility> + +namespace rcc::test { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +struct FunctionFixture final { + std::unique_ptr<rcc::MachineFunction> Function; + rcc::MachineBasicBlock *Entry = nullptr; +}; + +[[nodiscard]] inline FunctionFixture createFixture(TestContext &Context, std::string_view Name, + std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::string(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "machine pipeline function creation must succeed"); + if (!Function) + return {}; + auto Entry = (*Function)->createBasicBlock(); + Context.expect(Entry.has_value(), "machine pipeline block creation must succeed"); + if (!Entry || !(*Function)->appendBlock(**Entry)) + return {}; + return {std::move(*Function), *Entry}; +} + +[[nodiscard]] inline rcc::Register +createRegister(TestContext &Context, rcc::MachineFunction &Function, std::uint32_t Bits) { + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(Bits); + Context.expect(Type.has_value(), "integer LowLevelType construction must succeed"); + if (!Type) + return {}; + auto Reg = Function.registerInfo().createVirtualRegister(*Type); + Context.expect(Reg.has_value(), "virtual register construction must succeed"); + return Reg ? *Reg : rcc::Register{}; +} + +[[nodiscard]] inline rcc::Register createFloatRegister(TestContext &Context, + rcc::MachineFunction &Function) { + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + Context.expect(Type.has_value(), "floating LowLevelType construction must succeed"); + if (!Type) + return {}; + auto Reg = Function.registerInfo().createVirtualRegister(*Type); + Context.expect(Reg.has_value(), "floating virtual register construction must succeed"); + return Reg ? *Reg : rcc::Register{}; +} + +[[nodiscard]] inline bool assignGPR(rcc::MachineFunction &Function, rcc::Register Reg) { + const auto Bank = + rcc::RegisterBankID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterBank::GPR)); + const auto RegClass = + rcc::RegisterClassID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterClass::GPR)); + return Function.registerInfo().setRegisterBank(Reg, Bank).has_value() && + Function.registerInfo().setRegisterClass(Reg, RegClass).has_value(); +} + +[[nodiscard]] inline bool buildConstant(TestContext &Context, rcc::MachineIRBuilder &Builder, + rcc::Register Destination, std::int64_t Value) { + auto Pending = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_CONSTANT)); + if (!Pending || !Pending->addDef(Destination) || !Pending->addImmediate(Value) || + !Pending->build()) { + Context.expect(false, "generic constant construction must succeed"); + return false; + } + return true; +} + +[[nodiscard]] inline bool buildAdd(TestContext &Context, rcc::MachineIRBuilder &Builder, + rcc::Register Destination, rcc::Register Left, + rcc::Register Right) { + auto Pending = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_ADD)); + if (!Pending || !Pending->addDef(Destination) || !Pending->addUse(Left) || + !Pending->addUse(Right) || !Pending->build()) { + Context.expect(false, "generic add construction must succeed"); + return false; + } + return true; +} + +} // namespace rcc::test + +#endif // RCC_UNITTESTS_CODEGEN_GLOBALISEL_GISELMITEST_H diff --git a/backend/unittests/CodeGen/GlobalISel/InstructionSelectTest.cpp b/backend/unittests/CodeGen/GlobalISel/InstructionSelectTest.cpp new file mode 100644 index 0000000..9f3f0b5 --- /dev/null +++ b/backend/unittests/CodeGen/GlobalISel/InstructionSelectTest.cpp @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/GlobalISel/InstructionSelect.h" + +#include "GISel/RISCVRegisterBankInfo.h" +#include "GISelMITest.h" +#include "rcc/CodeGen/GlobalISel/Legalizer.h" +#include "rcc/CodeGen/GlobalISel/RegBankSelect.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/Target/RISCV/RISCVSubtarget.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +#include <algorithm> +#include <array> +#include <cstdint> +#include <memory> +#include <ranges> +#include <vector> + +namespace { + +using rcc::test::assignGPR; +using rcc::test::buildAdd; +using rcc::test::buildConstant; +using rcc::test::createFixture; +using rcc::test::createRegister; +using rcc::test::TestContext; + +class MutatingFailingSelector final : public rcc::InstructionSelector { +public: + [[nodiscard]] rcc::Expected<void> canSelect(const rcc::MachineInstr &, + const rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return {}; + } + + [[nodiscard]] rcc::Expected<void> select(rcc::MachineInstr &Instruction, rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + auto Mutation = Instruction.setOpcode(rcc::opcode(rcc::GenericOpcode::G_SUB)); + if (!Mutation) + return std::unexpected(Mutation.error()); + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, "test selector rejected its renderer"); + } +}; + +class RecordingSelector final : public rcc::InstructionSelector { +public: + explicit RecordingSelector(const rcc::InstructionSelector &Delegate) : Delegate(&Delegate) {} + + [[nodiscard]] rcc::Expected<void> canSelect(const rcc::MachineInstr &Instruction, + const rcc::MachineFunction &Function, + const rcc::TargetDescription &Target) const override { + return Delegate->canSelect(Instruction, Function, Target); + } + + [[nodiscard]] rcc::Expected<void> select(rcc::MachineInstr &Instruction, + rcc::MachineFunction &Function, + const rcc::TargetDescription &Target) const override { + SelectedIDs.push_back(Instruction.stableID().value()); + return Delegate->select(Instruction, Function, Target); + } + + [[nodiscard]] const std::vector<std::uint64_t> &selectedIDs() const noexcept { + return SelectedIDs; + } + +private: + const rcc::InstructionSelector *Delegate; + mutable std::vector<std::uint64_t> SelectedIDs; +}; + +class TwoStepSelector final : public rcc::InstructionSelector { +public: + [[nodiscard]] rcc::Expected<void> canSelect(const rcc::MachineInstr &, + const rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return {}; + } + + [[nodiscard]] rcc::Expected<void> select(rcc::MachineInstr &Instruction, rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + ++Selections; + if (Instruction.opcode() == rcc::opcode(rcc::GenericOpcode::G_ADD)) + return Instruction.setOpcode(rcc::opcode(rcc::GenericOpcode::G_SUB)); + if (Instruction.opcode() == rcc::opcode(rcc::GenericOpcode::G_SUB)) { + return Instruction.setOpcode( + rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::ADD))); + } + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, + "two-step selector received an unexpected opcode"); + } + + [[nodiscard]] std::size_t selections() const noexcept { + return Selections; + } + +private: + mutable std::size_t Selections = 0; +}; + +void testPipeline(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFixture(Context, "pipeline_success", 2); + Context.expect(Target.has_value() && Fixture.Function != nullptr && Fixture.Entry != nullptr, + "pipeline success fixture must be complete"); + if (!Target || !Fixture.Function || Fixture.Entry == nullptr) + return; + + const rcc::Register Left = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Right = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Result = createRegister(Context, *Fixture.Function, 64); + rcc::MachineIRBuilder Builder(*Fixture.Function, *Target); + Context.expect(Builder.setInsertPoint(*Fixture.Entry).has_value(), + "pipeline insertion point must be valid"); + if (Left.isNoRegister() || Right.isNoRegister() || Result.isNoRegister() || + !buildConstant(Context, Builder, Left, 42) || !buildConstant(Context, Builder, Right, 1) || + !buildAdd(Context, Builder, Result, Left, Right)) + return; + + Fixture.Function->properties() = {rcc::MachineFunctionProperty::IsSSA, + rcc::MachineFunctionProperty::TracksLiveness}; + const rcc::riscv::RISCVSubtarget Subtarget(rcc::riscv::RISCVSubtargetFeatures::rv64gc()); + rcc::MachineFunctionPassManager Pipeline; + Context.expect(Pipeline + .addPass(std::make_unique<rcc::Legalizer>(Subtarget.getLegalizerInfo(), + rcc::OptimizationLevel::O2)) + .has_value(), + "legalizer must be accepted by the pass manager"); + Context.expect(Pipeline.addPass(std::make_unique<rcc::RegBankSelect>(Subtarget.getRegBankInfo())) + .has_value(), + "register-bank selector must be accepted by the pass manager"); + Context.expect( + Pipeline.addPass(std::make_unique<rcc::InstructionSelect>(Subtarget.getInstructionSelector())) + .has_value(), + "instruction selector must be accepted by the pass manager"); + rcc::MachineFunctionAnalysisManager Analyses; + auto Run = Pipeline.run(*Fixture.Function, *Target, Analyses); + Context.expect(Run.has_value(), "RV64GC generic pipeline must complete"); + Context.expect(Fixture.Function->properties().has(rcc::MachineFunctionProperty::Selected), + "successful instruction selection must publish Selected"); + Context.expect(Fixture.Function->firstBlock()->firstInstruction() != nullptr, + "selected instructions must remain attached to their block"); + for (const rcc::MachineInstr *Instruction : Fixture.Function->allInstructions()) + Context.expect(!rcc::isGenericOpcode(Instruction->opcode()), + "successful instruction selection must remove generic opcodes"); +} + +void testInstructionSelectionTransaction(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFixture(Context, "pipeline_selection_transaction", 8); + if (!Target || !Fixture.Function || Fixture.Entry == nullptr) + return; + const rcc::Register Left = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Right = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Result = createRegister(Context, *Fixture.Function, 64); + rcc::MachineIRBuilder Builder(*Fixture.Function, *Target); + if (Left.isNoRegister() || Right.isNoRegister() || Result.isNoRegister() || + !Builder.setInsertPoint(*Fixture.Entry) || !buildAdd(Context, Builder, Result, Left, Right) || + !assignGPR(*Fixture.Function, Left) || !assignGPR(*Fixture.Function, Right) || + !assignGPR(*Fixture.Function, Result)) { + return; + } + rcc::MachineInstr *Original = Fixture.Entry->firstInstruction(); + if (Original == nullptr) + return; + const rcc::Opcode OriginalOpcode = Original->opcode(); + const std::size_t OriginalOperandCount = Original->operandCount(); + Fixture.Function->properties() = {rcc::MachineFunctionProperty::Legalized, + rcc::MachineFunctionProperty::RegBankSelected}; + MutatingFailingSelector Selector; + rcc::InstructionSelect Pass(Selector); + rcc::MachineFunctionAnalysisManager Analyses; + auto Run = Pass.run(*Fixture.Function, *Target, Analyses); + Context.expect(!Run && Run.error().code() == rcc::ErrorCode::Unsupported, + "a renderer failure must be reported before publication"); + Context.expect(Original->opcode() == OriginalOpcode && + Original->operandCount() == OriginalOperandCount && + Fixture.Entry->instructionCount() == 1, + "a renderer failure must leave the original MachineIR unchanged"); +} + +void testInstructionSelectionOrder(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFixture(Context, "pipeline_selection_order", 9); + if (!Target || !Fixture.Function || Fixture.Entry == nullptr) + return; + const rcc::Register Left = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Right = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Result = createRegister(Context, *Fixture.Function, 64); + rcc::MachineIRBuilder Builder(*Fixture.Function, *Target); + if (Left.isNoRegister() || Right.isNoRegister() || Result.isNoRegister() || + !Builder.setInsertPoint(*Fixture.Entry) || !buildConstant(Context, Builder, Left, 6) || + !buildConstant(Context, Builder, Right, 7) || + !buildAdd(Context, Builder, Result, Left, Right) || !assignGPR(*Fixture.Function, Left) || + !assignGPR(*Fixture.Function, Right) || !assignGPR(*Fixture.Function, Result)) { + return; + } + std::vector<std::uint64_t> LayoutIDs; + for (const rcc::MachineInstr *Instruction = Fixture.Entry->firstInstruction(); + Instruction != nullptr; Instruction = Instruction->next()) { + LayoutIDs.push_back(Instruction->stableID().value()); + } + Fixture.Function->properties() = {rcc::MachineFunctionProperty::Legalized, + rcc::MachineFunctionProperty::RegBankSelected}; + const rcc::riscv::RISCVSubtarget Subtarget(rcc::riscv::RISCVSubtargetFeatures::rv64gc()); + RecordingSelector Selector(*Subtarget.getInstructionSelector()); + rcc::InstructionSelect Pass(Selector); + rcc::MachineFunctionAnalysisManager Analyses; + auto Run = Pass.run(*Fixture.Function, *Target, Analyses); + Context.expect(Run.has_value(), "recorded instruction selection must complete"); + std::ranges::reverse(LayoutIDs); + std::vector<std::uint64_t> Expected = LayoutIDs; + Expected.insert(Expected.end(), LayoutIDs.begin(), LayoutIDs.end()); + Context.expect( + Selector.selectedIDs() == Expected, + "instruction selection must use stable block-postorder and reverse instruction order"); +} + +void testInstructionSelectionWorklist(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFixture(Context, "pipeline_selection_worklist", 10); + if (!Target || !Fixture.Function || Fixture.Entry == nullptr) + return; + const rcc::Register Left = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Right = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Result = createRegister(Context, *Fixture.Function, 64); + rcc::MachineIRBuilder Builder(*Fixture.Function, *Target); + if (Left.isNoRegister() || Right.isNoRegister() || Result.isNoRegister() || + !Builder.setInsertPoint(*Fixture.Entry) || !buildAdd(Context, Builder, Result, Left, Right) || + !assignGPR(*Fixture.Function, Left) || !assignGPR(*Fixture.Function, Right) || + !assignGPR(*Fixture.Function, Result)) { + return; + } + Fixture.Function->properties() = {rcc::MachineFunctionProperty::Legalized, + rcc::MachineFunctionProperty::RegBankSelected}; + TwoStepSelector Selector; + rcc::InstructionSelect Pass(Selector); + rcc::MachineFunctionAnalysisManager Analyses; + auto Run = Pass.run(*Fixture.Function, *Target, Analyses); + Context.expect(Run.has_value(), "a progressing multi-step selection must complete"); + Context.expect(Selector.selections() == 4, + "transaction preflight and publication must each select both generic states"); + Context.expect(Fixture.Entry->firstInstruction() != nullptr && + Fixture.Entry->firstInstruction()->opcode() == + rcc::Opcode::fromValue( + static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::ADD)), + "the selection worklist must revisit a progressing generic root"); +} + +void testExtendedRV64GCPipeline(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFixture(Context, "pipeline_extended_rv64gc", 7); + Context.expect(Target.has_value() && Fixture.Function != nullptr && Fixture.Entry != nullptr, + "extended pipeline fixture must be complete"); + if (!Target || !Fixture.Function || Fixture.Entry == nullptr) + return; + auto I1 = rcc::LowLevelType::makeCategoryNeutralScalar(1); + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Pointer = rcc::LowLevelType::makePointer(0, 64); + if (!I1 || !I64 || !Pointer) + return; + const rcc::Register Left = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Right = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Condition = + Fixture.Function->registerInfo().createVirtualRegister(*I1).value_or(rcc::Register{}); + const rcc::Register Selected = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Sum = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Overflow = + Fixture.Function->registerInfo().createVirtualRegister(*I1).value_or(rcc::Register{}); + const rcc::Register Quotient = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Remainder = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Address = + Fixture.Function->registerInfo().createVirtualRegister(*Pointer).value_or(rcc::Register{}); + const rcc::Register AtomicResult = createRegister(Context, *Fixture.Function, 64); + const std::array Registers{Left, Right, Condition, Selected, Sum, + Overflow, Quotient, Remainder, Address, AtomicResult}; + if (std::ranges::any_of(Registers, [](rcc::Register Reg) { return Reg.isNoRegister(); })) + return; + rcc::MachineIRBuilder Builder(*Fixture.Function, *Target); + if (!Builder.setInsertPoint(*Fixture.Entry) || !buildConstant(Context, Builder, Left, 41) || + !buildConstant(Context, Builder, Right, 7)) { + return; + } + auto Compare = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_ICMP)); + if (!Compare || !Compare->addDef(Condition) || + !Compare->addIntegerPredicate(rcc::IntegerPredicate::UnsignedGreaterThan) || + !Compare->addUse(Left) || !Compare->addUse(Right) || !Compare->build()) { + return; + } + auto Select = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_SELECT)); + if (!Select || !Select->addDef(Selected) || !Select->addUse(Condition) || !Select->addUse(Left) || + !Select->addUse(Right) || !Select->build()) { + return; + } + auto AddOverflow = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_UADDO)); + if (!AddOverflow || !AddOverflow->addDef(Sum) || !AddOverflow->addDef(Overflow) || + !AddOverflow->addUse(Left) || !AddOverflow->addUse(Right) || !AddOverflow->build()) { + return; + } + auto DivRem = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_SDIVREM)); + if (!DivRem || !DivRem->addDef(Quotient) || !DivRem->addDef(Remainder) || !DivRem->addUse(Left) || + !DivRem->addUse(Right) || !DivRem->build()) { + return; + } + auto AddressDefinition = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_IMPLICIT_DEF)); + if (!AddressDefinition || !AddressDefinition->addDef(Address) || !AddressDefinition->build()) + return; + auto Size = rcc::MemorySize::fixed(8); + auto Align = rcc::Alignment::create(8); + if (!Size || !Align) + return; + rcc::MachineMemOperandDesc Description; + Description.Accesses = {rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Store}; + Description.AtomicKind = rcc::AtomicOperation::ReadModifyWrite; + Description.SuccessOrdering = rcc::AtomicOrdering::SequentiallyConsistent; + Description.Size = *Size; + Description.Align = *Align; + Description.Base = rcc::MemoryBase::stack(); + auto Memory = Fixture.Function->createMemOperand(std::move(Description)); + if (!Memory) + return; + auto Atomic = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_ATOMICRMW_ADD)); + if (!Atomic || !Atomic->addDef(AtomicResult) || !Atomic->addUse(Address) || + !Atomic->addUse(Selected) || !Atomic->addMemOperand(**Memory) || !Atomic->build()) { + return; + } + + Fixture.Function->properties() = {rcc::MachineFunctionProperty::IsSSA, + rcc::MachineFunctionProperty::TracksLiveness}; + const rcc::riscv::RISCVSubtarget Subtarget(rcc::riscv::RISCVSubtargetFeatures::rv64gc()); + rcc::MachineFunctionPassManager Pipeline; + if (!Pipeline.addPass(std::make_unique<rcc::Legalizer>(Subtarget.getLegalizerInfo(), + rcc::OptimizationLevel::O2)) || + !Pipeline.addPass(std::make_unique<rcc::RegBankSelect>(Subtarget.getRegBankInfo())) || + !Pipeline.addPass( + std::make_unique<rcc::InstructionSelect>(Subtarget.getInstructionSelector()))) { + Context.expect(false, "extended RV64GC pipeline construction must succeed"); + return; + } + rcc::MachineFunctionAnalysisManager Analyses; + auto Run = Pipeline.run(*Fixture.Function, *Target, Analyses); + Context.expect(Run.has_value(), "extended RV64GC pipeline must complete"); + Context.expect(Run && Fixture.Function->properties().has(rcc::MachineFunctionProperty::Selected), + "extended pipeline must publish Selected"); + for (const rcc::MachineInstr *Instruction : Fixture.Function->allInstructions()) { + Context.expect(!rcc::isGenericOpcode(Instruction->opcode()), + "extended pipeline must eliminate every generic opcode"); + Context.expect( + Target->validateInstruction(*Instruction, Fixture.Function->registerInfo()).has_value(), + "extended pipeline output must satisfy target descriptors"); + } +} + +} // namespace + +int main() { + TestContext Context; + testPipeline(Context); + testInstructionSelectionTransaction(Context); + testInstructionSelectionOrder(Context); + testInstructionSelectionWorklist(Context); + testExtendedRV64GCPipeline(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/GlobalISel/LegalizerTest.cpp b/backend/unittests/CodeGen/GlobalISel/LegalizerTest.cpp new file mode 100644 index 0000000..eaf3741 --- /dev/null +++ b/backend/unittests/CodeGen/GlobalISel/LegalizerTest.cpp @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/GlobalISel/Legalizer.h" + +#include "GISelMITest.h" +#include "rcc/CodeGen/GlobalISel/GISelChangeObserver.h" +#include "rcc/CodeGen/GlobalISel/LegalizerHelper.h" +#include "rcc/Target/RISCV/RISCVRegisters.h" +#include "rcc/Target/RISCV/RISCVSubtarget.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +#include <cstdint> +#include <memory> +#include <string> + +namespace { + +using rcc::test::buildAdd; +using rcc::test::createFixture; +using rcc::test::createRegister; +using rcc::test::TestContext; + +class BackwardInvalidatingLegalizer final : public rcc::LegalizerInfo { +public: + BackwardInvalidatingLegalizer() { + getActionDefinitionsBuilder(rcc::opcode(rcc::GenericOpcode::G_ADD)).alwaysLegal(); + getActionDefinitionsBuilder(rcc::opcode(rcc::GenericOpcode::G_SUB)).custom(); + } + + [[nodiscard]] rcc::Expected<void> + legalizeCustom(rcc::LegalizerHelper &Helper, rcc::MachineInstr &Instruction, + const rcc::LegalizationDecision &) const override { + rcc::MachineFunction &Function = Helper.MIRBuilder.function(); + for (rcc::MachineInstr *Candidate : Function.allInstructions()) { + if (Candidate != &Instruction && + Candidate->opcode() == rcc::opcode(rcc::GenericOpcode::G_ADD)) { + Helper.Observer.changingInstr(*Candidate); + auto Invalidated = Candidate->setOpcode(rcc::opcode(rcc::GenericOpcode::G_SUB)); + if (!Invalidated) + return std::unexpected(Invalidated.error()); + Helper.Observer.changedInstr(*Candidate); + break; + } + } + Helper.Observer.changingInstr(Instruction); + auto Changed = Instruction.setOpcode( + rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::ADD))); + if (!Changed) + return std::unexpected(Changed.error()); + Helper.Observer.changedInstr(Instruction); + return {}; + } +}; + +void testNullAndMissingContracts(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFixture(Context, "pipeline_contracts", 1); + Context.expect(Target.has_value() && Fixture.Function != nullptr && Fixture.Entry != nullptr, + "pipeline contract fixture must be complete"); + if (!Target || !Fixture.Function) + return; + + rcc::Legalizer NullPass(nullptr, rcc::OptimizationLevel::O2); + rcc::MachineFunctionAnalysisManager Analyses; + auto NullResult = NullPass.run(*Fixture.Function, *Target, Analyses); + Context.expect(!NullResult && NullResult.error().code() == rcc::ErrorCode::InvalidArgument, + "a null legalizer interface must be rejected"); + Context.expect(Fixture.Function->properties().empty(), + "a rejected pass must not publish function properties"); + + const rcc::riscv::RISCVSubtarget Subtarget(rcc::riscv::RISCVSubtargetFeatures::rv64gc()); + rcc::Legalizer ValidPass(Subtarget.getLegalizerInfo(), rcc::OptimizationLevel::O2); + auto MissingResult = ValidPass.run(*Fixture.Function, *Target, Analyses); + Context.expect(!MissingResult && + MissingResult.error().code() == rcc::ErrorCode::VerificationError, + "a pass with missing prerequisites must be rejected"); +} + +void testUnsupportedRollback(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + if (!Target) + return; + auto Unsupported = createFixture(Context, "pipeline_unsupported", 3); + Context.expect(Unsupported.Function != nullptr && Unsupported.Entry != nullptr, + "unsupported pipeline fixture must be complete"); + if (!Unsupported.Function || Unsupported.Entry == nullptr) + return; + const rcc::Register UnsupportedReg = createRegister(Context, *Unsupported.Function, 64); + rcc::MachineIRBuilder UnsupportedBuilder(*Unsupported.Function, *Target); + Context.expect(UnsupportedBuilder.setInsertPoint(*Unsupported.Entry).has_value(), + "unsupported pipeline insertion point must be valid"); + if (UnsupportedReg.isNoRegister()) + return; + auto Pending = UnsupportedBuilder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_GET_FPENV)); + Context.expect(Pending.has_value(), "unsupported generic instruction must be constructible"); + if (!Pending) + return; + auto Built = Pending->addDef(UnsupportedReg); + Context.expect(Built.has_value(), "unsupported generic definition must be accepted"); + if (!Built) + return; + Context.expect(Pending->build().has_value(), + "unsupported floating-point environment operation must be constructible"); + Unsupported.Function->properties() = {rcc::MachineFunctionProperty::IsSSA, + rcc::MachineFunctionProperty::TracksLiveness}; + const rcc::riscv::RISCVSubtarget Subtarget(rcc::riscv::RISCVSubtargetFeatures::rv64gc()); + rcc::MachineFunctionPassManager OnlyLegalizer; + Context.expect(OnlyLegalizer + .addPass(std::make_unique<rcc::Legalizer>(Subtarget.getLegalizerInfo(), + rcc::OptimizationLevel::O2)) + .has_value(), + "standalone legalizer must be accepted"); + rcc::MachineFunctionAnalysisManager UnsupportedAnalyses; + auto Failure = OnlyLegalizer.run(*Unsupported.Function, *Target, UnsupportedAnalyses); + Context.expect(!Failure && Failure.error().code() == rcc::ErrorCode::Unsupported, + "unsupported generic instructions must produce a target diagnostic"); + Context.expect(!Failure && Failure.error().message().find("G_GET_FPENV") != std::string::npos, + "unsupported legalization diagnostics must name the rejected opcode"); + Context.expect( + Unsupported.Function->properties().has(rcc::MachineFunctionProperty::IsSSA) && + !Unsupported.Function->properties().has(rcc::MachineFunctionProperty::Legalized), + "failed legalizer execution must not publish Legalized"); +} + +void testLegalizerRevisitsInvalidatedInstructions(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFixture(Context, "pipeline_legalizer_revisit", 7); + if (!Target || !Fixture.Function || Fixture.Entry == nullptr) + return; + const rcc::Register Left = createRegister(Context, *Fixture.Function, 64); + const rcc::Register Right = createRegister(Context, *Fixture.Function, 64); + const rcc::Register FirstResult = createRegister(Context, *Fixture.Function, 64); + const rcc::Register SecondResult = createRegister(Context, *Fixture.Function, 64); + rcc::MachineIRBuilder Builder(*Fixture.Function, *Target); + if (Left.isNoRegister() || Right.isNoRegister() || FirstResult.isNoRegister() || + SecondResult.isNoRegister() || !Builder.setInsertPoint(*Fixture.Entry) || + !buildAdd(Context, Builder, FirstResult, Left, Right) || + !buildAdd(Context, Builder, SecondResult, Left, Right)) + return; + rcc::MachineInstr *First = Fixture.Entry->firstInstruction(); + rcc::MachineInstr *Second = First == nullptr ? nullptr : First->next(); + if (Second == nullptr || !Second->setOpcode(rcc::opcode(rcc::GenericOpcode::G_SUB))) + return; + auto X10 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X10); + auto X11 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X11); + auto FirstCopy = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::COPY)); + auto SecondCopy = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::COPY)); + if (!X10 || !X11 || !FirstCopy || !FirstCopy->addDef(rcc::Register::fromPhysical(*X10)) || + !FirstCopy->addUse(FirstResult) || !FirstCopy->build() || !SecondCopy || + !SecondCopy->addDef(rcc::Register::fromPhysical(*X11)) || !SecondCopy->addUse(SecondResult) || + !SecondCopy->build()) + return; + + Fixture.Function->properties() = {rcc::MachineFunctionProperty::IsSSA, + rcc::MachineFunctionProperty::TracksLiveness}; + BackwardInvalidatingLegalizer Legalizer; + rcc::Legalizer Pass(Legalizer, rcc::OptimizationLevel::O2); + rcc::MachineFunctionAnalysisManager Analyses; + auto Run = Pass.run(*Fixture.Function, *Target, Analyses); + const rcc::Opcode LegalOpcode = + rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::ADD)); + Context.expect(Run && First->opcode() == LegalOpcode && Second->opcode() == LegalOpcode, + "legalizer must revisit an earlier instruction invalidated by a later lowering"); +} + +} // namespace + +int main() { + TestContext Context; + testNullAndMissingContracts(Context); + testUnsupportedRollback(Context); + testLegalizerRevisitsInvalidatedInstructions(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/GlobalISel/RegBankSelectTest.cpp b/backend/unittests/CodeGen/GlobalISel/RegBankSelectTest.cpp new file mode 100644 index 0000000..785844f --- /dev/null +++ b/backend/unittests/CodeGen/GlobalISel/RegBankSelectTest.cpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/GlobalISel/RegBankSelect.h" + +#include "GISel/RISCVRegisterBankInfo.h" +#include "GISelMITest.h" +#include "rcc/Target/RISCV/RISCVSubtarget.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +#include <cstdint> + +namespace { + +using rcc::test::buildConstant; +using rcc::test::createFixture; +using rcc::test::createRegister; +using rcc::test::TestContext; + +void testBankConflict(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFixture(Context, "pipeline_bank_conflict", 4); + Context.expect(Target.has_value() && Fixture.Function != nullptr && Fixture.Entry != nullptr, + "bank conflict fixture must be complete"); + if (!Target || !Fixture.Function || Fixture.Entry == nullptr) + return; + const rcc::Register Value = createRegister(Context, *Fixture.Function, 64); + Context.expect( + Fixture.Function->registerInfo() + .setRegisterBank(Value, rcc::RegisterBankID::fromValue( + static_cast<std::uint32_t>(rcc::riscv::RegisterBank::FPR))) + .has_value(), + "conflicting bank assignment must be constructible"); + Fixture.Function->properties() = {rcc::MachineFunctionProperty::Legalized}; + const rcc::riscv::RISCVSubtarget Subtarget(rcc::riscv::RISCVSubtargetFeatures::rv64gc()); + rcc::RegBankSelect Pass(Subtarget.getRegBankInfo()); + rcc::MachineFunctionAnalysisManager Analyses; + auto Result = Pass.run(*Fixture.Function, *Target, Analyses); + Context.expect(!Result && Result.error().code() == rcc::ErrorCode::VerificationError, + "bank selection must reject a conflicting pre-existing bank"); + auto Info = Fixture.Function->registerInfo().virtualRegisterInfo(Value); + Context.expect(Info && Info->get().Bank, "failed bank selection must preserve a bank assignment"); + if (!Info || !Info->get().Bank) + return; + const rcc::RegisterBankID Bank = Info->get().Bank.value_or(rcc::RegisterBankID{}); + Context.expect(Bank == rcc::RegisterBankID::fromValue( + static_cast<std::uint32_t>(rcc::riscv::RegisterBank::FPR)), + "failed bank selection must preserve the original bank assignment"); +} + +void testBankSelectionDoesNotAssignAClass(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFixture(Context, "pipeline_bank_class", 5); + Context.expect(Target.has_value() && Fixture.Function != nullptr && Fixture.Entry != nullptr, + "bank class fixture must be complete"); + if (!Target || !Fixture.Function || Fixture.Entry == nullptr) + return; + const rcc::Register Value = createRegister(Context, *Fixture.Function, 64); + rcc::MachineIRBuilder Builder(*Fixture.Function, *Target); + if (Value.isNoRegister() || !Builder.setInsertPoint(*Fixture.Entry) || + !buildConstant(Context, Builder, Value, 7)) + return; + const auto GPR = + rcc::RegisterBankID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterBank::GPR)); + Context.expect(Fixture.Function->registerInfo().setRegisterBank(Value, GPR).has_value(), + "pre-existing GPR bank must be assignable"); + Fixture.Function->properties() = {rcc::MachineFunctionProperty::Legalized}; + const rcc::riscv::RISCVSubtarget Subtarget(rcc::riscv::RISCVSubtargetFeatures::rv64gc()); + rcc::RegBankSelect Pass(Subtarget.getRegBankInfo()); + rcc::MachineFunctionAnalysisManager Analyses; + auto Result = Pass.run(*Fixture.Function, *Target, Analyses); + Context.expect(Result.has_value() && !Result->Changed, + "bank selection must preserve an existing bank-only mapping"); + auto Info = Fixture.Function->registerInfo().virtualRegisterInfo(Value); + Context.expect(Info && Info->get().Bank && !Info->get().RegClass, + "bank selection must not assign a register class"); + if (!Info || !Info->get().Bank) + return; + Context.expect(Info->get().Bank.value_or(rcc::RegisterBankID{}) == GPR, + "bank selection must preserve the GPR bank"); +} + +} // namespace + +int main() { + TestContext Context; + testBankConflict(Context); + testBankSelectionDoesNotAssignAClass(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/InstructionDescriptorTest.cpp b/backend/unittests/CodeGen/InstructionDescriptorTest.cpp new file mode 100644 index 0000000..d263725 --- /dev/null +++ b/backend/unittests/CodeGen/InstructionDescriptorTest.cpp @@ -0,0 +1,368 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/InstructionDescriptor.h" + +#include "rcc/CodeGen/LowLevelType.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/CodeGen/MachineOperand.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::OperandDescriptor registerDescriptor(rcc::RegisterOperandRole Role) { + rcc::OperandDescriptor Descriptor; + Descriptor.Kinds = {rcc::MachineOperand::Kind::Register}; + Descriptor.RegisterRole = Role; + return Descriptor; +} + +[[nodiscard]] rcc::InstructionDescriptor minimalDescriptor(std::uint32_t OpcodeValue, + std::string Name) { + rcc::InstructionDescriptor Descriptor; + Descriptor.InstructionOpcode = rcc::Opcode::fromValue(OpcodeValue); + Descriptor.Name = std::move(Name); + Descriptor.Flags = {rcc::InstructionFlag::ProducesCode}; + return Descriptor; +} + +[[nodiscard]] rcc::Expected<rcc::MachineOperand> registerOperand(rcc::Register Reg, + rcc::RegisterOperandFlags Flags, + rcc::SubRegIndex SubReg = {}, + std::uint32_t TargetFlags = 0) { + return rcc::MachineOperand::registerOperand(Reg, Flags, SubReg, TargetFlags); +} + +[[nodiscard]] rcc::MachineMemOperandDesc loadDescription() { + rcc::MachineMemOperandDesc Description; + Description.Accesses = {rcc::MemoryAccessFlag::Load}; + Description.Size = *rcc::MemorySize::fixed(8); + Description.Align = *rcc::Alignment::create(8); + Description.Base = rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)); + return Description; +} + +[[nodiscard]] rcc::Expected<rcc::InstructionSet> createValidInstructionSet() { + constexpr std::uint32_t AddOpcode = 1; + constexpr std::uint32_t TwoAddressOpcode = 2; + constexpr std::uint32_t LoadOpcode = 3; + + rcc::InstructionDescriptor Add = minimalDescriptor(AddOpcode, "TEST_ADDri"); + auto Def = registerDescriptor(rcc::RegisterOperandRole::Def); + Def.RegBank = rcc::RegisterBankID::fromValue(7); + Def.RegClass = rcc::RegisterClassID::fromValue(8); + Def.Name = rcc::NamedOperandID::fromValue(1); + + auto Source = registerDescriptor(rcc::RegisterOperandRole::Use); + Source.Name = rcc::NamedOperandID::fromValue(2); + + rcc::OperandDescriptor RegisterOrImmediate; + RegisterOrImmediate.Kinds = { + rcc::MachineOperand::Kind::Register, + rcc::MachineOperand::Kind::Immediate, + rcc::MachineOperand::Kind::GlobalSymbol, + }; + RegisterOrImmediate.RegisterRole = rcc::RegisterOperandRole::Use; + RegisterOrImmediate.ImmediatePredicate = rcc::ImmediatePredicateID::fromValue(1); + RegisterOrImmediate.Name = rcc::NamedOperandID::fromValue(3); + RegisterOrImmediate.AllowedTargetFlags = 0x3; + + Add.Operands.push_back(Def); + Add.Operands.push_back(Source); + Add.Operands.push_back(RegisterOrImmediate); + Add.NumExplicitDefs = 1; + Add.ImplicitDefs.push_back( + rcc::ImplicitRegisterDescriptor{*rcc::PhysicalRegister::create(1), false, true}); + Add.ImplicitUses.push_back(rcc::ImplicitRegisterDescriptor{*rcc::PhysicalRegister::create(2)}); + + rcc::InstructionDescriptor TwoAddress = minimalDescriptor(TwoAddressOpcode, "TEST_TWOADDR"); + auto TiedDef = registerDescriptor(rcc::RegisterOperandRole::Def); + auto TiedUse = registerDescriptor(rcc::RegisterOperandRole::Use); + TiedDef.DefaultTiedOperand = 1; + TiedUse.DefaultTiedOperand = 0; + TwoAddress.Operands.push_back(TiedDef); + TwoAddress.Operands.push_back(TiedUse); + TwoAddress.NumExplicitDefs = 1; + + rcc::InstructionDescriptor Load = minimalDescriptor(LoadOpcode, "TEST_LOAD"); + Load.Flags.set(rcc::InstructionFlag::MayLoad); + Load.RequiresMemoryOperands = true; + + rcc::ImmediatePredicate Signed12; + Signed12.ID = rcc::ImmediatePredicateID::fromValue(1); + Signed12.Name = "register-or-simm12"; + Signed12.Predicate = [](const rcc::MachineOperand &Operand) { + if (Operand.isRegister()) + return true; + const auto *Value = Operand.getIf<std::int64_t>(); + return Value != nullptr && *Value >= -2048 && *Value <= 2047; + }; + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Add)); + Descriptors.push_back(std::move(TwoAddress)); + Descriptors.push_back(std::move(Load)); + std::vector<rcc::ImmediatePredicate> Predicates; + Predicates.push_back(std::move(Signed12)); + return rcc::InstructionSet::create(std::move(Descriptors), std::move(Predicates)); +} + +void testDescriptorStructure(TestContext &Context) { + Context.expect(!rcc::InstructionSet::create({}), "an empty instruction set must be rejected"); + + auto DefAfterUses = minimalDescriptor(10, "BAD_DEF_ORDER"); + DefAfterUses.Operands.push_back(registerDescriptor(rcc::RegisterOperandRole::Def)); + Context.expect(!rcc::InstructionSet::create({DefAfterUses}), + "a definition outside the explicit def prefix must be rejected"); + + auto AmbiguousVariadic = minimalDescriptor(11, "BAD_VARIADIC"); + auto Optional = registerDescriptor(rcc::RegisterOperandRole::Use); + Optional.Optional = true; + auto Variadic = registerDescriptor(rcc::RegisterOperandRole::Use); + Variadic.VariadicGroup = true; + AmbiguousVariadic.Operands.push_back(Optional); + AmbiguousVariadic.Operands.push_back(Variadic); + AmbiguousVariadic.Variadic = true; + Context.expect(!rcc::InstructionSet::create({AmbiguousVariadic}), + "optional and variadic operand tails must not be combined"); + + auto BadTie = minimalDescriptor(12, "BAD_TIE"); + auto Def = registerDescriptor(rcc::RegisterOperandRole::Def); + auto Use = registerDescriptor(rcc::RegisterOperandRole::Use); + Def.DefaultTiedOperand = 1; + BadTie.Operands.push_back(Def); + BadTie.Operands.push_back(Use); + BadTie.NumExplicitDefs = 1; + Context.expect(!rcc::InstructionSet::create({BadTie}), "default operand ties must be reciprocal"); + + auto DuplicateNames = minimalDescriptor(13, "BAD_NAMES"); + auto FirstUse = registerDescriptor(rcc::RegisterOperandRole::Use); + auto SecondUse = registerDescriptor(rcc::RegisterOperandRole::Use); + FirstUse.Name = rcc::NamedOperandID::fromValue(9); + SecondUse.Name = rcc::NamedOperandID::fromValue(9); + DuplicateNames.Operands.push_back(FirstUse); + DuplicateNames.Operands.push_back(SecondUse); + Context.expect(!rcc::InstructionSet::create({DuplicateNames}), + "named operand IDs must be unique within an instruction"); + + auto BadBranch = minimalDescriptor(14, "BAD_BRANCH"); + BadBranch.Flags.set(rcc::InstructionFlag::Branch); + Context.expect(!rcc::InstructionSet::create({BadBranch}), + "branch descriptors must also be terminators"); + + auto UnknownPredicate = minimalDescriptor(15, "BAD_PREDICATE"); + rcc::OperandDescriptor Immediate; + Immediate.Kinds = {rcc::MachineOperand::Kind::Immediate}; + Immediate.ImmediatePredicate = rcc::ImmediatePredicateID::fromValue(99); + UnknownPredicate.Operands.push_back(Immediate); + Context.expect(!rcc::InstructionSet::create({UnknownPredicate}), + "operand descriptors must not reference unknown predicates"); +} + +void testOperandValidation(TestContext &Context) { + auto InstructionSet = createValidInstructionSet(); + Context.expect(InstructionSet.has_value(), "the complete test instruction set must be valid"); + if (!InstructionSet) + return; + + auto Function = rcc::MachineFunction::create("descriptor_operands", + rcc::MachineFunctionStableID::fromValue(1)); + Context.expect(Function.has_value(), "descriptor test function must be valid"); + if (!Function) + return; + + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Destination = (*Function)->registerInfo().createVirtualRegister(*I64); + auto Source = (*Function)->registerInfo().createVirtualRegister(*I64); + Context.expect(Destination && Source, "descriptor test virtual registers must be allocated"); + if (!Destination || !Source) + return; + + auto &RegInfo = (*Function)->registerInfo(); + Context.expect( + RegInfo.setRegisterBank(*Destination, rcc::RegisterBankID::fromValue(7)).has_value() && + RegInfo.setRegisterClass(*Destination, rcc::RegisterClassID::fromValue(8)).has_value(), + "descriptor test destination constraints must be recorded"); + + std::vector<rcc::MachineOperand> ValidOperands; + ValidOperands.push_back(*registerOperand(*Destination, {rcc::RegisterOperandFlag::Define})); + ValidOperands.push_back(*registerOperand(*Source, {rcc::RegisterOperandFlag::Use})); + ValidOperands.push_back(rcc::MachineOperand::immediate(42, 0x1)); + Context.expect( + InstructionSet->validateExplicitOperands(rcc::Opcode::fromValue(1), ValidOperands, RegInfo) + .has_value(), + "matching roles, constraints, flags, and predicate must validate"); + + std::vector<rcc::MachineOperand> SymbolOperands; + SymbolOperands.push_back(*registerOperand(*Destination, {rcc::RegisterOperandFlag::Define})); + SymbolOperands.push_back(*registerOperand(*Source, {rcc::RegisterOperandFlag::Use})); + SymbolOperands.push_back(*rcc::MachineOperand::globalSymbol("object", 0, 0x1)); + Context.expect( + InstructionSet->validateExplicitOperands(rcc::Opcode::fromValue(1), SymbolOperands, RegInfo) + .has_value(), + "an immediate predicate must not reject another allowed symbolic operand kind"); + + std::vector<rcc::MachineOperand> WrongRole; + WrongRole.push_back(*registerOperand(*Destination, {rcc::RegisterOperandFlag::Use})); + WrongRole.push_back(*registerOperand(*Source, {rcc::RegisterOperandFlag::Use})); + WrongRole.push_back(rcc::MachineOperand::immediate(1)); + Context.expect( + !InstructionSet->validateExplicitOperands(rcc::Opcode::fromValue(1), WrongRole, RegInfo), + "a register role mismatch must be rejected"); + + std::vector<rcc::MachineOperand> BadImmediate; + BadImmediate.push_back(*registerOperand(*Destination, {rcc::RegisterOperandFlag::Define})); + BadImmediate.push_back(*registerOperand(*Source, {rcc::RegisterOperandFlag::Use})); + BadImmediate.push_back(rcc::MachineOperand::immediate(4096)); + Context.expect( + !InstructionSet->validateExplicitOperands(rcc::Opcode::fromValue(1), BadImmediate, RegInfo), + "an operand outside its immediate predicate must be rejected"); + + std::vector<rcc::MachineOperand> BadTargetFlags; + BadTargetFlags.push_back(*registerOperand(*Destination, {rcc::RegisterOperandFlag::Define})); + BadTargetFlags.push_back(*registerOperand(*Source, {rcc::RegisterOperandFlag::Use})); + BadTargetFlags.push_back(rcc::MachineOperand::immediate(1, 0x4)); + Context.expect( + !InstructionSet->validateExplicitOperands(rcc::Opcode::fromValue(1), BadTargetFlags, RegInfo), + "unregistered target operand flags must be rejected"); +} + +void testInstructionValidation(TestContext &Context) { + auto InstructionSet = createValidInstructionSet(); + auto Function = rcc::MachineFunction::create("descriptor_instructions", + rcc::MachineFunctionStableID::fromValue(2)); + Context.expect(InstructionSet && Function, "instruction validation setup must succeed"); + if (!InstructionSet || !Function) + return; + + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Reg = (*Function)->registerInfo().createVirtualRegister(*I64); + Context.expect(Reg.has_value(), "instruction validation register must be allocated"); + if (!Reg) + return; + + auto Tied = (*Function)->createInstruction(rcc::Opcode::fromValue(2)); + Context.expect(Tied.has_value(), "tied instruction must be allocated"); + if (!Tied) + return; + Context.expect( + (*Tied)->addOperand(*registerOperand(*Reg, {rcc::RegisterOperandFlag::Define})).has_value() && + (*Tied) + ->addOperand(*registerOperand(*Reg, {rcc::RegisterOperandFlag::Use})) + .has_value() && + (*Tied)->tieOperands(0, 1).has_value(), + "tied instruction operands must be materialized"); + Context.expect( + InstructionSet->validateInstruction(**Tied, (*Function)->registerInfo()).has_value(), + "a reciprocal descriptor-required tie must validate"); + + auto Untied = (*Function)->createInstruction(rcc::Opcode::fromValue(2)); + Context.expect(Untied.has_value(), "untied instruction must be allocated"); + if (Untied) { + Context.expect((*Untied) + ->addOperand(*registerOperand(*Reg, {rcc::RegisterOperandFlag::Define})) + .has_value() && + (*Untied) + ->addOperand(*registerOperand(*Reg, {rcc::RegisterOperandFlag::Use})) + .has_value(), + "untied instruction operands must be added"); + Context.expect(!InstructionSet->validateInstruction(**Untied, (*Function)->registerInfo()), + "a missing descriptor-required tie must be rejected"); + } + + auto Destination = (*Function)->registerInfo().createVirtualRegister(*I64); + auto Source = (*Function)->registerInfo().createVirtualRegister(*I64); + Context.expect(Destination && Source, "implicit operand validation registers must be allocated"); + if (!Destination || !Source) + return; + auto &RegInfo = (*Function)->registerInfo(); + Context.expect( + RegInfo.setRegisterBank(*Destination, rcc::RegisterBankID::fromValue(7)).has_value() && + RegInfo.setRegisterClass(*Destination, rcc::RegisterClassID::fromValue(8)).has_value(), + "implicit validation destination constraints must be recorded"); + + auto WithImplicit = (*Function)->createInstruction(rcc::Opcode::fromValue(1)); + const rcc::InstructionDescriptor *AddDescriptor = + InstructionSet->descriptor(rcc::Opcode::fromValue(1)); + Context.expect(WithImplicit && AddDescriptor != nullptr, + "implicit instruction setup must find its descriptor"); + if (!WithImplicit || AddDescriptor == nullptr) + return; + Context.expect( + (*WithImplicit) + ->addOperand(*registerOperand(*Destination, {rcc::RegisterOperandFlag::Define})) + .has_value() && + (*WithImplicit) + ->addOperand(*registerOperand(*Source, {rcc::RegisterOperandFlag::Use})) + .has_value() && + (*WithImplicit)->addOperand(rcc::MachineOperand::immediate(7)).has_value(), + "explicit operands for implicit validation must be added"); + auto DefaultImplicit = InstructionSet->createDefaultImplicitOperands(*AddDescriptor); + Context.expect(DefaultImplicit.has_value(), "default implicit operands must be constructible"); + if (!DefaultImplicit) + return; + for (rcc::MachineOperand &Operand : *DefaultImplicit) { + Context.expect((*WithImplicit)->addOperand(std::move(Operand)).has_value(), + "default implicit operand insertion must succeed"); + } + Context.expect(InstructionSet->validateInstruction(**WithImplicit, RegInfo).has_value(), + "exact default implicit operands must validate"); + + rcc::MachineOperand *ImplicitDef = (*WithImplicit)->operandAt(3); + Context.expect(ImplicitDef != nullptr && + ImplicitDef + ->setRegisterFlags( + {rcc::RegisterOperandFlag::Define, rcc::RegisterOperandFlag::Implicit}) + .has_value(), + "implicit definition flags must be mutable for a negative test"); + Context.expect(!InstructionSet->validateInstruction(**WithImplicit, RegInfo), + "implicit renamable state must match the descriptor exactly"); + + auto Load = (*Function)->createInstruction(rcc::Opcode::fromValue(3)); + Context.expect(Load.has_value(), "load instruction must be allocated"); + if (!Load) + return; + Context.expect(!InstructionSet->validateInstruction(**Load, RegInfo), + "an MMO-required load without an MMO must be rejected"); + auto MemOperand = (*Function)->createMemOperand(loadDescription()); + Context.expect(MemOperand.has_value() && (*Load)->addMemOperand(**MemOperand).has_value(), + "a function-owned load MMO must attach"); + Context.expect(InstructionSet->validateInstruction(**Load, RegInfo).has_value(), + "an MMO matching mayLoad must validate"); +} + +} // namespace + +int main() { + TestContext Context; + testDescriptorStructure(Context); + testOperandValidation(Context); + testInstructionValidation(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/LiveIntervalTest.cpp b/backend/unittests/CodeGen/LiveIntervalTest.cpp new file mode 100644 index 0000000..34ebd76 --- /dev/null +++ b/backend/unittests/CodeGen/LiveIntervalTest.cpp @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/LiveInterval.h" + +#include "LiveTestSupport.h" +#include "rcc/CodeGen/LiveIntervals.h" +#include "rcc/CodeGen/LiveVariables.h" + +namespace { + +using namespace rcc::test::liveness; + +void testLiveRange(TestContext &Context) { + const rcc::SlotIndex S16 = rcc::SlotIndex::fromValue(16).registerSlot(); + const rcc::SlotIndex S24 = rcc::SlotIndex::fromValue(24).registerSlot(); + const rcc::SlotIndex S32 = rcc::SlotIndex::fromValue(32).registerSlot(); + const rcc::SlotIndex S40 = rcc::SlotIndex::fromValue(40).registerSlot(); + const rcc::SlotIndex S48 = rcc::SlotIndex::fromValue(48).registerSlot(); + + rcc::LiveRange Range; + auto First = Range.createValue(S16); + Context.expect(First.has_value(), "live value creation must succeed"); + if (!First) + return; + Context.expect(Range.addSegment({S16, S32, *First}).has_value() && + Range.addSegment({S32, S48, *First}).has_value() && + Range.segments().size() == 1, + "adjacent same-value live segments must merge"); + auto Second = Range.createValue(S40, rcc::LiveValueState::Redefined); + Context.expect(Second.has_value() && !Range.addSegment({S24, S40, *Second}), + "overlapping different live values must be rejected"); + Context.expect(Range.valueAt(S24) != nullptr && Range.liveAt(S40) && + Range.remove(S24, S40).has_value() && Range.segments().size() == 2 && + Range.verify().has_value(), + "live-range lookup and removal must preserve ordered half-open segments"); + + rcc::LiveRange Shrunk; + auto Value = Shrunk.createValue(S16); + if (!Value) { + Context.expect(false, "shrink test value creation must succeed"); + return; + } + const std::array Uses{S32}; + Context.expect(Shrunk.addSegment({S16, S48.deadSlot(), *Value}).has_value() && + Shrunk.shrinkToUses(Uses).has_value() && Shrunk.segments().size() == 1 && + Shrunk.segments().front().End == S32.deadSlot(), + "shrinkToUses must retain exactly through the final use dead slot"); +} + +void testLaneSubrangesAndEarlyClobber(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction("lane-subranges", 3, 1); + if (!Fixture) { + Context.expect(false, "lane-subrange fixture creation must succeed"); + return; + } + auto Reg = createVReg(*Fixture->Function); + auto Early = createVReg(*Fixture->Function); + if (!Reg || !Early) { + Context.expect(false, "lane-subrange registers must be created"); + return; + } + auto LowDef = + buildDef(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg, rcc::test::TestSubReg::Low32); + auto LowUse = + buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg, rcc::test::TestSubReg::Low32); + auto HighDef = buildDef(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg, + rcc::test::TestSubReg::High32); + auto HighUse = buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg, + rcc::test::TestSubReg::High32); + auto EarlyDef = buildDef(*Fixture->Function, Target, *Fixture->Blocks[0], *Early, {}, true); + auto EarlyUse = buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], *Early); + if (!LowDef || !LowUse || !HighDef || !HighUse || !EarlyDef || !EarlyUse) { + Context.expect(false, "lane-subrange instructions must be built"); + return; + } + + rcc::MachineFunctionAnalysisManager Analyses; + auto Registered = rcc::registerStage11MachineAnalyses(Analyses); + auto Intervals = Registered + ? Analyses.getResult<rcc::LiveIntervalsAnalysis>(*Fixture->Function, Target) + : rcc::Expected<rcc::LiveIntervalsAnalysis::Result *>{ + std::unexpected(Registered.error())}; + if (!Intervals) { + Context.expect(false, "lane-subrange intervals must materialize"); + return; + } + const rcc::LiveInterval *Interval = (*Intervals)->interval(*Reg); + const rcc::LiveInterval *EarlyInterval = (*Intervals)->interval(*Early); + const bool HasLow = Interval != nullptr && + std::ranges::any_of(Interval->SubRanges, [](const rcc::LiveSubRange &Range) { + return Range.Lanes == rcc::LaneMask::fromRaw(0x3); + }); + const bool HasHigh = Interval != nullptr && + std::ranges::any_of(Interval->SubRanges, [](const rcc::LiveSubRange &Range) { + return Range.Lanes == rcc::LaneMask::fromRaw(0xC); + }); + Context.expect(Interval != nullptr && Interval->SubRanges.size() == 2 && HasLow && HasHigh, + "partial virtual-register accesses must form disjoint lane subranges"); + Context.expect(EarlyInterval != nullptr && !EarlyInterval->MainRange.values().empty() && + EarlyInterval->MainRange.values().front().Definition.kind() == + rcc::SlotIndexKind::EarlyClobber, + "early-clobber definitions must use the early-clobber slot"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "liveness TestTarget must be created"); + if (!Target) + return Context.result(); + testLiveRange(Context); + testLaneSubrangesAndEarlyClobber(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/LiveIntervalsTest.cpp b/backend/unittests/CodeGen/LiveIntervalsTest.cpp new file mode 100644 index 0000000..a080e4d --- /dev/null +++ b/backend/unittests/CodeGen/LiveIntervalsTest.cpp @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/LiveIntervals.h" + +#include "LiveTestSupport.h" +#include "rcc/CodeGen/LiveRegUnits.h" +#include "rcc/CodeGen/LiveVariables.h" +#include "rcc/Target/RISCV/RISCVInstructions.h" +#include "rcc/Target/RISCV/RISCVRegisters.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +namespace { + +using namespace rcc::test::liveness; + +void testCFGAndValueNumbers(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction("cfg-liveness", 2, 8); + if (!Fixture) { + Context.expect(false, "CFG liveness fixture creation must succeed"); + return; + } + auto &Function = *Fixture->Function; + auto &Entry = *Fixture->Blocks[0]; + auto &Left = *Fixture->Blocks[1]; + auto &Right = *Fixture->Blocks[2]; + auto &Join = *Fixture->Blocks[3]; + auto &Header = *Fixture->Blocks[4]; + auto &Body = *Fixture->Blocks[5]; + auto &Exit = *Fixture->Blocks[6]; + auto &Dead = *Fixture->Blocks[7]; + auto Half = rcc::BranchProbability::fromFraction(1, 2); + auto Updater = Function.cfgUpdater(); + const bool CFGValid = Half && Updater.addSuccessor(Entry, Left, *Half) && + Updater.addSuccessor(Entry, Right, *Half) && + Updater.addSuccessor(Left, Join, rcc::BranchProbability::one()) && + Updater.addSuccessor(Right, Join, rcc::BranchProbability::one()) && + Updater.addSuccessor(Join, Header, rcc::BranchProbability::one()) && + Updater.addSuccessor(Header, Body, *Half) && + Updater.addSuccessor(Header, Exit, *Half) && + Updater.addSuccessor(Body, Header, rcc::BranchProbability::one()); + Context.expect(CFGValid, "diamond and loop CFG construction must succeed"); + if (!CFGValid) + return; + + auto Across = createVReg(Function); + auto Merged = createVReg(Function); + auto Unreachable = createVReg(Function); + auto Redefined = createVReg(Function); + if (!Across || !Merged || !Unreachable || !Redefined) { + Context.expect(false, "CFG liveness virtual registers must be created"); + return; + } + auto AcrossDef = buildDef(Function, Target, Entry, *Across); + auto LeftDef = buildDef(Function, Target, Left, *Merged); + auto RightDef = buildDef(Function, Target, Right, *Merged); + auto MergedUse = buildUse(Function, Target, Join, *Merged); + auto LoopUse = buildUse(Function, Target, Body, *Across); + auto DeadUse = buildUse(Function, Target, Dead, *Unreachable); + auto FirstRedef = buildDef(Function, Target, Exit, *Redefined); + auto SecondRedef = buildDef(Function, Target, Exit, *Redefined); + if (!AcrossDef || !LeftDef || !RightDef || !MergedUse || !LoopUse || !DeadUse || !FirstRedef || + !SecondRedef) { + Context.expect(false, "CFG liveness instructions must be built"); + return; + } + + rcc::MachineFunctionAnalysisManager Analyses; + auto Registered = rcc::registerStage11MachineAnalyses(Analyses); + auto Variables = Registered ? Analyses.getResult<rcc::LiveVariablesAnalysis>(Function, Target) + : rcc::Expected<rcc::LiveVariablesAnalysis::Result *>{ + std::unexpected(Registered.error())}; + auto Intervals = Registered ? Analyses.getResult<rcc::LiveIntervalsAnalysis>(Function, Target) + : rcc::Expected<rcc::LiveIntervalsAnalysis::Result *>{ + std::unexpected(Registered.error())}; + if (!Variables || !Intervals) { + Context.expect(false, "CFG liveness analyses must materialize"); + return; + } + Context.expect((*Variables)->isLiveOut(Entry, *Across) && (*Variables)->isLiveIn(Left, *Across) && + (*Variables)->isLiveIn(Right, *Across) && + (*Variables)->isLiveIn(Header, *Across) && + (*Variables)->isLiveOut(Body, *Across), + "CFG fixed point must propagate a value through a diamond and loop backedge"); + Context.expect( + (*Variables)->isLiveOut(Left, *Merged) && (*Variables)->isLiveOut(Right, *Merged) && + (*Variables)->isLiveIn(Join, *Merged) && (*Variables)->isLiveIn(Dead, *Unreachable), + "liveness must merge predecessor definitions and retain unreachable uses"); + + const rcc::LiveInterval *MergedInterval = (*Intervals)->interval(*Merged); + const rcc::LiveInterval *AcrossInterval = (*Intervals)->interval(*Across); + const rcc::LiveInterval *RedefinedInterval = (*Intervals)->interval(*Redefined); + const bool HasPhi = MergedInterval != nullptr && + std::ranges::any_of(MergedInterval->MainRange.values(), + [](const rcc::LiveValueNumber &Value) { + return Value.State == rcc::LiveValueState::Phi; + }); + const bool HasRedefined = + RedefinedInterval != nullptr && + std::ranges::any_of(RedefinedInterval->MainRange.values(), + [](const rcc::LiveValueNumber &Value) { + return Value.State == rcc::LiveValueState::Redefined; + }); + Context.expect(MergedInterval != nullptr && MergedInterval->MainRange.values().size() >= 3 && + HasPhi && HasRedefined, + "non-SSA intervals must retain multiple definitions, PHI, and redefined values"); + Context.expect(AcrossInterval != nullptr && (*Intervals)->interferes(*Across, *Merged), + "live intervals must expose CFG-derived interference"); +} + +void testPhysicalUnitsAndRegMasks(TestContext &Context, const rcc::TargetDescription &Target) { + const rcc::RegisterUnitID MultiLaneUnit = rcc::RegisterUnitID::fromValue(9); + rcc::LiveRegUnits PartialLanes; + PartialLanes.add(MultiLaneUnit, rcc::LaneMask::fromRaw(0x3)); + rcc::LiveRegUnits RemovedLanes; + RemovedLanes.add(MultiLaneUnit, rcc::LaneMask::fromRaw(0x2)); + PartialLanes.subtract(RemovedLanes); + Context.expect(PartialLanes.lanes(MultiLaneUnit) == rcc::LaneMask::fromRaw(0x1) && + PartialLanes.contains(MultiLaneUnit, rcc::LaneMask::fromRaw(0x1)) && + !PartialLanes.intersects(MultiLaneUnit, rcc::LaneMask::fromRaw(0x2)), + "RegisterUnit lane sets must preserve partial union and subtraction"); + + auto AliasFixture = createFunction("physical-alias", 4, 1); + if (!AliasFixture) { + Context.expect(false, "physical-alias fixture creation must succeed"); + return; + } + const rcc::Register G0 = rcc::Register::fromPhysical(physical(rcc::test::TestRegister::G0)); + const rcc::Register G0High = + rcc::Register::fromPhysical(physical(rcc::test::TestRegister::G0High32)); + auto G0Def = buildDef(*AliasFixture->Function, Target, *AliasFixture->Blocks[0], G0); + auto G0HighUse = buildUse(*AliasFixture->Function, Target, *AliasFixture->Blocks[0], G0High); + if (!G0Def || !G0HighUse) { + Context.expect(false, "physical-alias instructions must be built"); + return; + } + rcc::MachineFunctionAnalysisManager AliasAnalyses; + auto AliasRegistered = rcc::registerStage11MachineAnalyses(AliasAnalyses); + auto AliasIntervals = + AliasRegistered + ? AliasAnalyses.getResult<rcc::LiveIntervalsAnalysis>(*AliasFixture->Function, Target) + : rcc::Expected<rcc::LiveIntervalsAnalysis::Result *>{ + std::unexpected(AliasRegistered.error())}; + const rcc::RegisterUnitID HighUnit = rcc::RegisterUnitID::fromValue(3); + Context.expect(AliasIntervals && (*AliasIntervals)->regUnitRange(HighUnit) != nullptr && + (*AliasIntervals)->regUnitRange(HighUnit)->segments().size() == 1, + "shared RegisterUnits must connect super-register defs to subregister uses"); + + auto MaskFixture = createFunction("regmask", 5, 1); + if (!MaskFixture) { + Context.expect(false, "RegMask fixture creation must succeed"); + return; + } + auto Across = createVReg(*MaskFixture->Function); + auto Output = createVReg(*MaskFixture->Function); + if (!Across || !Output) { + Context.expect(false, "RegMask virtual registers must be created"); + return; + } + const auto Mask = rcc::test::createTestCallPreservedMask(); + auto AcrossDef = buildDef(*MaskFixture->Function, Target, *MaskFixture->Blocks[0], *Across); + auto FirstMask = buildMask(*MaskFixture->Function, Target, *MaskFixture->Blocks[0], Mask); + auto AcrossUse = buildUse(*MaskFixture->Function, Target, *MaskFixture->Blocks[0], *Across); + auto OutputDef = + buildMaskedDef(*MaskFixture->Function, Target, *MaskFixture->Blocks[0], *Output, Mask); + auto OutputUse = buildUse(*MaskFixture->Function, Target, *MaskFixture->Blocks[0], *Output); + if (!AcrossDef || !FirstMask || !AcrossUse || !OutputDef || !OutputUse) { + Context.expect(false, "RegMask instructions must be built"); + return; + } + rcc::MachineFunctionAnalysisManager MaskAnalyses; + auto MaskRegistered = rcc::registerStage11MachineAnalyses(MaskAnalyses); + auto MaskIntervals = + MaskRegistered + ? MaskAnalyses.getResult<rcc::LiveIntervalsAnalysis>(*MaskFixture->Function, Target) + : rcc::Expected<rcc::LiveIntervalsAnalysis::Result *>{ + std::unexpected(MaskRegistered.error())}; + if (!MaskIntervals) { + Context.expect(false, "RegMask intervals must materialize"); + return; + } + const rcc::PhysicalRegister Candidate = physical(rcc::test::TestRegister::G0); + const rcc::LiveInterval *AcrossInterval = (*MaskIntervals)->interval(*Across); + const rcc::LiveInterval *OutputInterval = (*MaskIntervals)->interval(*Output); + Context.expect((*MaskIntervals)->regMaskPositions().size() == 2 && + std::ranges::all_of((*MaskIntervals)->regMaskPositions(), + [](const rcc::RegMaskPosition &Position) { + return Position.Position.kind() == + rcc::SlotIndexKind::EarlyClobber; + }), + "RegMask interference positions must use early-clobber slots"); + Context.expect(AcrossInterval != nullptr && OutputInterval != nullptr && + (*MaskIntervals)->regMaskClobbers(*AcrossInterval, Candidate) && + !(*MaskIntervals)->regMaskClobbers(*OutputInterval, Candidate), + "RegMask queries must reject live-through values but accept same-call outputs"); + Context.expect((*MaskIntervals)->regUnitRange(rcc::RegisterUnitID::fromValue(1)) == nullptr, + "unused RegMask clobbers must not expand into physical live ranges"); +} + +void testRISCVLiveness(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFunction("riscv-liveness", 8, 1); + if (!Target || !Fixture) { + Context.expect(false, "RV64GC liveness fixture creation must succeed"); + return; + } + auto X5 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X5); + auto X6 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X6); + auto X7 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X7); + if (!X5 || !X6 || !X7) { + Context.expect(false, "RV64GC physical registers must resolve"); + return; + } + auto BuildADDI = [&](rcc::PhysicalRegister Destination, rcc::PhysicalRegister Source, + std::int64_t Immediate) -> rcc::Expected<rcc::MachineInstr *> { + rcc::MachineIRBuilder Builder(*Fixture->Function, *Target); + auto Point = Builder.setInsertPoint(*Fixture->Blocks[0]); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr( + rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::ADDI))); + if (!Pending) + return std::unexpected(Pending.error()); + auto Def = Pending->addDef(rcc::Register::fromPhysical(Destination)); + if (!Def) + return std::unexpected(Def.error()); + auto Use = Pending->addUse(rcc::Register::fromPhysical(Source)); + if (!Use) + return std::unexpected(Use.error()); + auto Imm = Pending->addImmediate(Immediate); + if (!Imm) + return std::unexpected(Imm.error()); + return Pending->build(); + }; + auto First = BuildADDI(*X6, *X5, 1); + auto Second = BuildADDI(*X7, *X6, 2); + if (!First || !Second) { + Context.expect(false, "RV64GC ADDI instructions must be built"); + return; + } + rcc::MachineFunctionAnalysisManager Analyses; + auto Registered = rcc::registerStage11MachineAnalyses(Analyses); + auto Intervals = Registered + ? Analyses.getResult<rcc::LiveIntervalsAnalysis>(*Fixture->Function, *Target) + : rcc::Expected<rcc::LiveIntervalsAnalysis::Result *>{ + std::unexpected(Registered.error())}; + const auto Unit = + rcc::RegisterUnitID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterUnit::U_X6)); + Context.expect(Intervals && (*Intervals)->regUnitRange(Unit) != nullptr && + (*Intervals)->regUnitRange(Unit)->segments().size() == 1, + "production RV64GC register units must participate in LiveIntervals"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "liveness TestTarget must be created"); + if (!Target) + return Context.result(); + testCFGAndValueNumbers(Context, *Target); + testPhysicalUnitsAndRegMasks(Context, *Target); + testRISCVLiveness(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/LivePhysRegsTest.cpp b/backend/unittests/CodeGen/LivePhysRegsTest.cpp new file mode 100644 index 0000000..6eea796 --- /dev/null +++ b/backend/unittests/CodeGen/LivePhysRegsTest.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/LivePhysRegs.h" + +#include "LiveTestSupport.h" +#include "rcc/CodeGen/LiveVariables.h" + +namespace { + +using namespace rcc::test::liveness; + +void testPostRALivenessRepair(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction("post-ra-repair", 12, 1); + if (!Fixture) { + Context.expect(false, "post-RA repair fixture creation must succeed"); + return; + } + const rcc::Register G0 = rcc::Register::fromPhysical(physical(rcc::test::TestRegister::G0)); + auto Def = buildDef(*Fixture->Function, Target, *Fixture->Blocks[0], G0); + auto FirstUse = buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], G0); + auto LastUse = buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], G0); + if (!Def || !FirstUse || !LastUse) { + Context.expect(false, "post-RA repair instructions must be built"); + return; + } + + rcc::MachineFunctionAnalysisManager Analyses; + auto Registered = rcc::registerStage11MachineAnalyses(Analyses); + auto Liveness = Registered + ? Analyses.getResult<rcc::LiveVariablesAnalysis>(*Fixture->Function, Target) + : rcc::Expected<rcc::LiveVariablesAnalysis::Result *>{ + std::unexpected(Registered.error())}; + if (!Liveness) { + Context.expect(false, "post-RA repair liveness must materialize"); + return; + } + const std::array Blocks{Fixture->Blocks[0]}; + auto Repaired = rcc::repairPostRAMachineBlockLiveness(Blocks, Target, **Liveness); + Context.expect(Repaired && *Repaired, "post-RA repair must update stale kill/dead flags"); + Context.expect(!hasFlag(**Def, 0, rcc::RegisterOperandFlag::Dead) && + !hasFlag(**FirstUse, 0, rcc::RegisterOperandFlag::Kill) && + hasFlag(**LastUse, 0, rcc::RegisterOperandFlag::Kill), + "post-RA repair must derive precise physical kill/dead flags"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "liveness TestTarget must be created"); + if (!Target) + return Context.result(); + testPostRALivenessRepair(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/LiveRegMatrixTest.cpp b/backend/unittests/CodeGen/LiveRegMatrixTest.cpp new file mode 100644 index 0000000..423da50 --- /dev/null +++ b/backend/unittests/CodeGen/LiveRegMatrixTest.cpp @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/LiveRegMatrix.h" + +#include "CodeGen/RegisterAllocationTestSupport.h" + +#include <algorithm> +#include <iostream> + +namespace { + +using namespace rcc; +using namespace rcc::test; +using namespace rcc::test::ra; + +void testLaneAndAssignment(TestContext &Context, const TargetDescription &Target) { + auto Fixture = createFunction("matrix-lanes", 1); + if (!Fixture) + return; + auto Low = createVReg(*Fixture->Function); + auto High = createVReg(*Fixture->Function); + if (!Low || !High || + !buildDef(*Fixture->Function, Target, *Fixture->Block, *Low, 1, TestSubReg::Low32) || + !buildDef(*Fixture->Function, Target, *Fixture->Block, *High, 2, TestSubReg::High32) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *High, TestSubReg::High32) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Low, TestSubReg::Low32)) + return; + + MachineFunctionAnalysisManager Analyses; + if (!registerStage11MachineAnalyses(Analyses)) + return; + auto Intervals = Analyses.getResult<LiveIntervalsAnalysis>(*Fixture->Function, Target); + if (!Intervals) + return; + const PhysicalRegister G0 = physical(TestRegister::G0); + LiveRegMatrix Matrix(*Fixture->Function, Target, **Intervals); + Context.expect(Matrix.assign(*Low, G0).has_value() && + Matrix.checkInterference(*High, G0).Kind == RegisterInterferenceKind::Free && + Matrix.assign(*High, G0).has_value() && Matrix.assignment(*Low) == G0, + "LiveRegMatrix must allow disjoint lanes and publish assignments"); + Context.expect(Matrix.unassign(*Low).has_value() && !Matrix.assignment(*Low), + "LiveRegMatrix must remove an assignment without affecting other lanes"); +} + +void testFixedAndMaskInterference(TestContext &Context, const TargetDescription &Target) { + auto Fixture = createFunction("matrix-fixed", 2); + if (!Fixture) + return; + auto Value = createVReg(*Fixture->Function); + const PhysicalRegister G0 = physical(TestRegister::G0); + const PhysicalRegister G1 = physical(TestRegister::G1); + if (!Value || !buildDef(*Fixture->Function, Target, *Fixture->Block, *Value, 7) || + !buildMask(*Fixture->Function, Target, *Fixture->Block, createTestCallPreservedMask()) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, Register::fromPhysical(G0)) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value)) + return; + + MachineFunctionAnalysisManager Analyses; + if (!registerStage11MachineAnalyses(Analyses)) + return; + auto Intervals = Analyses.getResult<LiveIntervalsAnalysis>(*Fixture->Function, Target); + if (!Intervals) + return; + LiveRegMatrix Matrix(*Fixture->Function, Target, **Intervals); + const auto G0Interferences = Matrix.collectInterferences(*Value, G0); + const auto G1Interferences = Matrix.collectInterferences(*Value, G1); + const auto HasKind = [](std::span<const RegisterInterference> Values, + RegisterInterferenceKind Kind) { + return std::ranges::any_of( + Values, [Kind](const RegisterInterference &Value) { return Value.Kind == Kind; }); + }; + Context.expect(HasKind(G0Interferences, RegisterInterferenceKind::FixedRegister) && + HasKind(G0Interferences, RegisterInterferenceKind::RegisterMask) && + !HasKind(G1Interferences, RegisterInterferenceKind::FixedRegister) && + Matrix.assign(*Value, G1).has_value(), + "LiveRegMatrix must distinguish fixed-register and RegMask interference"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "LiveRegMatrix test target must be constructible"); + if (!Target) { + std::cerr << Target.error().message() << '\n'; + return Context.result(); + } + testLaneAndAssignment(Context, *Target); + testFixedAndMaskInterference(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/LiveRegUnitsTest.cpp b/backend/unittests/CodeGen/LiveRegUnitsTest.cpp new file mode 100644 index 0000000..7f224ac --- /dev/null +++ b/backend/unittests/CodeGen/LiveRegUnitsTest.cpp @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/LiveRegUnits.h" + +#include "LiveTestSupport.h" + +namespace { + +using namespace rcc::test::liveness; + +void testLiveRegUnits(TestContext &Context) { + const rcc::RegisterUnitID Unit = rcc::RegisterUnitID::fromValue(9); + rcc::LiveRegUnits Present; + rcc::LiveRegUnits Removed; + Present.add(Unit, rcc::LaneMask::fromRaw(0x3)); + Removed.add(Unit, rcc::LaneMask::fromRaw(0x2)); + Present.subtract(Removed); + Context.expect(Present.lanes(Unit) == rcc::LaneMask::fromRaw(0x1) && + Present.contains(Unit, rcc::LaneMask::fromRaw(0x1)) && + !Present.intersects(Unit, rcc::LaneMask::fromRaw(0x2)), + "LiveRegUnits must preserve partial union and subtraction"); + Present.unite(Removed); + Context.expect(Present.contains(Unit, rcc::LaneMask::fromRaw(0x3)), + "LiveRegUnits union must restore all selected lanes"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "liveness TestTarget must be created"); + if (!Target) + return Context.result(); + testLiveRegUnits(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/LiveTestSupport.h b/backend/unittests/CodeGen/LiveTestSupport.h new file mode 100644 index 0000000..f6a3566 --- /dev/null +++ b/backend/unittests/CodeGen/LiveTestSupport.h @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef RCC_BACKEND_LIVE_TEST_SUPPORT_H +#define RCC_BACKEND_LIVE_TEST_SUPPORT_H + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineCFGUpdater.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/MachineVerifier.h" +#include "rcc/CodeGen/TargetDescription.h" + +#include <algorithm> +#include <array> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <memory> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace rcc::test::liveness { + +constexpr std::uint32_t DefOpcode = 2048; +constexpr std::uint32_t UseOpcode = 2049; +constexpr std::uint32_t EarlyDefOpcode = 2050; +constexpr std::uint32_t MaskOpcode = 2051; +constexpr std::uint32_t MaskedDefOpcode = 2052; +constexpr std::uint32_t LiveOutMaskOpcode = 2053; + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +[[nodiscard]] inline rcc::Opcode testOpcode(std::uint32_t Value) { + return rcc::Opcode::fromValue(Value); +} + +[[nodiscard]] inline rcc::OperandDescriptor registerOperand(rcc::RegisterOperandRole Role, + bool EarlyClobber = false) { + rcc::OperandDescriptor Result; + Result.Kinds = {rcc::MachineOperand::Kind::Register}; + Result.RegisterRole = Role; + Result.EarlyClobber = EarlyClobber; + Result.AllowSubReg = true; + return Result; +} + +[[nodiscard]] inline rcc::OperandDescriptor plainOperand(rcc::MachineOperand::Kind Kind) { + rcc::OperandDescriptor Result; + Result.Kinds = {Kind}; + return Result; +} + +[[nodiscard]] inline rcc::InstructionDescriptor instruction(std::uint32_t Value, std::string Name) { + rcc::InstructionDescriptor Result; + Result.InstructionOpcode = testOpcode(Value); + Result.Name = std::move(Name); + Result.Flags = {rcc::InstructionFlag::ProducesCode}; + return Result; +} + +[[nodiscard]] inline rcc::Expected<rcc::TargetDescription> createTarget() { + std::vector<rcc::InstructionDescriptor> Descriptors; + std::vector<rcc::ImmediatePredicate> Predicates; + auto Generic = rcc::appendGenericInstructionInfo(Descriptors, Predicates); + if (!Generic) + return std::unexpected(Generic.error()); + + auto Def = instruction(DefOpcode, "LIVENESS_DEF"); + Def.Operands = {registerOperand(rcc::RegisterOperandRole::Def)}; + Def.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Def)); + + auto Use = instruction(UseOpcode, "LIVENESS_USE"); + Use.Operands = {registerOperand(rcc::RegisterOperandRole::Use)}; + Descriptors.push_back(std::move(Use)); + + auto EarlyDef = instruction(EarlyDefOpcode, "LIVENESS_EARLY_DEF"); + EarlyDef.Operands = {registerOperand(rcc::RegisterOperandRole::Def, true)}; + EarlyDef.NumExplicitDefs = 1; + Descriptors.push_back(std::move(EarlyDef)); + + auto Mask = instruction(MaskOpcode, "LIVENESS_MASK"); + Mask.Operands = {plainOperand(rcc::MachineOperand::Kind::RegisterMask)}; + Mask.Flags.set(rcc::InstructionFlag::Call); + Mask.Flags.set(rcc::InstructionFlag::HasSideEffects); + Descriptors.push_back(std::move(Mask)); + + auto MaskedDef = instruction(MaskedDefOpcode, "LIVENESS_MASKED_DEF"); + MaskedDef.Operands = {registerOperand(rcc::RegisterOperandRole::Def), + plainOperand(rcc::MachineOperand::Kind::RegisterMask)}; + MaskedDef.NumExplicitDefs = 1; + MaskedDef.Flags.set(rcc::InstructionFlag::Call); + MaskedDef.Flags.set(rcc::InstructionFlag::HasSideEffects); + Descriptors.push_back(std::move(MaskedDef)); + + auto LiveOutMask = instruction(LiveOutMaskOpcode, "LIVENESS_LIVEOUT_MASK"); + LiveOutMask.Operands = {plainOperand(rcc::MachineOperand::Kind::LiveOutRegisterMask)}; + Descriptors.push_back(std::move(LiveOutMask)); + + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors), std::move(Predicates)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +struct FunctionFixture final { + std::unique_ptr<rcc::MachineFunction> Function; + std::vector<rcc::MachineBasicBlock *> Blocks; +}; + +[[nodiscard]] inline rcc::Expected<FunctionFixture> +createFunction(std::string Name, std::uint64_t ID, std::size_t BlockCount) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + if (!Function) + return std::unexpected(Function.error()); + FunctionFixture Result{std::move(*Function), {}}; + Result.Blocks.reserve(BlockCount); + for (std::size_t Index = 0; Index < BlockCount; ++Index) { + auto Block = Result.Function->createBasicBlock(); + if (!Block) + return std::unexpected(Block.error()); + auto Appended = Result.Function->appendBlock(**Block); + if (!Appended) + return std::unexpected(Appended.error()); + Result.Blocks.push_back(*Block); + } + return Result; +} + +[[nodiscard]] inline rcc::Expected<rcc::Register> createVReg(rcc::MachineFunction &Function) { + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + if (!Type) + return std::unexpected(Type.error()); + auto Reg = Function.registerInfo().createVirtualRegister(*Type); + if (!Reg) + return std::unexpected(Reg.error()); + auto Bank = Function.registerInfo().setRegisterBank(*Reg, rcc::test::TestRegisterBank::GPR); + if (!Bank) + return std::unexpected(Bank.error()); + auto Class = Function.registerInfo().setRegisterClass(*Reg, rcc::test::TestRegisterClass::GPR64); + if (!Class) + return std::unexpected(Class.error()); + return *Reg; +} + +[[nodiscard]] inline rcc::Expected<rcc::MachineInstr *> +buildDef(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Register Reg, rcc::SubRegIndex SubReg = {}, + bool EarlyClobber = false) { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = Builder.setInsertPoint(Block); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(testOpcode(EarlyClobber ? EarlyDefOpcode : DefOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + rcc::RegisterOperandFlags Flags; + if (EarlyClobber) + Flags.set(rcc::RegisterOperandFlag::EarlyClobber); + auto Added = Pending->addDef(Reg, Flags, SubReg); + if (!Added) + return std::unexpected(Added.error()); + return Pending->build(); +} + +[[nodiscard]] inline rcc::Expected<rcc::MachineInstr *> +buildUse(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Register Reg, rcc::SubRegIndex SubReg = {}, + rcc::MachineInstr *Before = nullptr) { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = + Before == nullptr ? Builder.setInsertPoint(Block) : Builder.setInsertPointBefore(*Before); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(testOpcode(UseOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + auto Added = Pending->addUse(Reg, {}, SubReg); + if (!Added) + return std::unexpected(Added.error()); + return Pending->build(); +} + +[[nodiscard]] inline rcc::Expected<rcc::MachineInstr *> +buildMask(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, std::shared_ptr<const rcc::RegisterMask> Mask) { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = Builder.setInsertPoint(Block); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(testOpcode(MaskOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + auto Added = Pending->addRegisterMask(std::move(Mask)); + if (!Added) + return std::unexpected(Added.error()); + return Pending->build(); +} + +[[nodiscard]] inline rcc::Expected<rcc::MachineInstr *> +buildMaskedDef(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Register Reg, + std::shared_ptr<const rcc::RegisterMask> Mask) { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = Builder.setInsertPoint(Block); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(testOpcode(MaskedDefOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + auto Def = Pending->addDef(Reg); + if (!Def) + return std::unexpected(Def.error()); + auto MaskResult = Pending->addRegisterMask(std::move(Mask)); + if (!MaskResult) + return std::unexpected(MaskResult.error()); + return Pending->build(); +} + +[[nodiscard]] inline rcc::PhysicalRegister physical(std::uint32_t Number) { + return *rcc::PhysicalRegister::create(Number); +} + +[[nodiscard]] inline bool hasFlag(const rcc::MachineInstr &Instruction, std::size_t OperandIndex, + rcc::RegisterOperandFlag Flag) { + const rcc::MachineOperand *Operand = Instruction.operandAt(OperandIndex); + return Operand != nullptr && Operand->registerFlags().has(Flag); +} + +inline void setRebuildProperties(rcc::MachineFunction &Function) { + Function.properties().set(rcc::MachineFunctionProperty::Legalized); + Function.properties().set(rcc::MachineFunctionProperty::RegBankSelected); + Function.properties().set(rcc::MachineFunctionProperty::Selected); + Function.properties().set(rcc::MachineFunctionProperty::NoPHIs); + Function.properties().set(rcc::MachineFunctionProperty::TiedOpsRewritten); +} + +} // namespace rcc::test::liveness + +#endif // RCC_BACKEND_LIVE_TEST_SUPPORT_H diff --git a/backend/unittests/CodeGen/LiveVariablesTest.cpp b/backend/unittests/CodeGen/LiveVariablesTest.cpp new file mode 100644 index 0000000..1cc4e91 --- /dev/null +++ b/backend/unittests/CodeGen/LiveVariablesTest.cpp @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/LiveVariables.h" + +#include "LiveTestSupport.h" +#include "rcc/CodeGen/LiveIntervals.h" +#include "rcc/Target/RISCV/RISCVInstructions.h" +#include "rcc/Target/RISCV/RISCVRegisters.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +namespace { + +using namespace rcc::test::liveness; + +void testLivenessRebuildAndVerifier(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction("rebuild", 6, 2); + if (!Fixture) { + Context.expect(false, "liveness-rebuild fixture creation must succeed"); + return; + } + auto &Function = *Fixture->Function; + auto &Entry = *Fixture->Blocks[0]; + auto &Exit = *Fixture->Blocks[1]; + auto Edge = Function.cfgUpdater().addSuccessor(Entry, Exit, rcc::BranchProbability::one()); + const rcc::Register G0 = rcc::Register::fromPhysical(physical(rcc::test::TestRegister::G0)); + const rcc::Register G1 = rcc::Register::fromPhysical(physical(rcc::test::TestRegister::G1)); + auto LiveDef = buildDef(Function, Target, Entry, G0); + auto FirstUse = buildUse(Function, Target, Exit, G0); + auto LastUse = buildUse(Function, Target, Exit, G0); + auto DeadDef = buildDef(Function, Target, Exit, G1); + if (!Edge || !LiveDef || !FirstUse || !LastUse || !DeadDef) { + Context.expect(false, "liveness-rebuild MachineIR must be constructed"); + return; + } + auto StaleLiveIn = Entry.addLiveIn(physical(rcc::test::TestRegister::F0)); + Context.expect(StaleLiveIn.has_value(), "stale live-in setup must succeed"); + setRebuildProperties(Function); + + rcc::MachineFunctionPassManager Pipeline; + auto Added = Pipeline.addPass(std::make_unique<rcc::RebuildMachineLivenessPass>()); + rcc::MachineFunctionAnalysisManager Analyses; + auto Run = Added ? Pipeline.run(Function, Target, Analyses) + : rcc::Expected<void>{std::unexpected(Added.error())}; + if (!Run) + std::cerr << "liveness rebuild pipeline error: " << Run.error().message() << '\n'; + Context.expect(Run.has_value() && + Function.properties().has(rcc::MachineFunctionProperty::TracksLiveness), + "RebuildMachineLiveness must publish TracksLiveness through the pass manager"); + if (!Run) + return; + Context.expect( + Entry.liveIns().empty() && Exit.liveIns().size() == 1 && + Exit.liveIns().front().PhysReg == physical(rcc::test::TestRegister::G0) && + Exit.liveIns().front().Lanes == rcc::LaneMask::all(), + "liveness rebuild must replace stale live-ins with canonical whole-register input"); + Context.expect(!hasFlag(**LiveDef, 0, rcc::RegisterOperandFlag::Dead) && + !hasFlag(**FirstUse, 0, rcc::RegisterOperandFlag::Kill) && + hasFlag(**LastUse, 0, rcc::RegisterOperandFlag::Kill) && + hasFlag(**DeadDef, 0, rcc::RegisterOperandFlag::Dead), + "reverse reconstruction must derive conservative kill and dead flags"); + Context.expect(rcc::MachineVerifier::verify(Function, Target).has_value(), + "rebuilt tracked liveness must pass the MachineVerifier"); + + auto ConservativeLiveIn = Entry.addLiveIn(physical(rcc::test::TestRegister::F0)); + Context.expect(ConservativeLiveIn.has_value() && + rcc::MachineVerifier::verify(Function, Target).has_value(), + "MachineVerifier must accept conservative physical live-ins"); + Entry.clearLiveIns(); + + rcc::MachineOperand *FirstUseOperand = (*FirstUse)->operandAt(0); + if (FirstUseOperand == nullptr) { + Context.expect(false, "first use operand must exist"); + return; + } + rcc::RegisterOperandFlags StaleFlags = FirstUseOperand->registerFlags(); + StaleFlags.set(rcc::RegisterOperandFlag::Kill); + auto StaleKill = FirstUseOperand->setRegisterFlags(StaleFlags); + Context.expect(StaleKill.has_value() && !rcc::MachineVerifier::verify(Function, Target), + "MachineVerifier must reject an overstrong tracked kill flag"); + StaleFlags.clear(rcc::RegisterOperandFlag::Kill); + auto ClearedKill = FirstUseOperand->setRegisterFlags(StaleFlags); + Context.expect(ClearedKill.has_value(), "stale kill flag cleanup must succeed"); + Exit.clearLiveIns(); + Context.expect(!rcc::MachineVerifier::verify(Function, Target), + "MachineVerifier must reject stale tracked physical live-ins"); + + rcc::RebuildMachineLivenessPass Rebuild; + auto Repaired = Rebuild.run(Function, Target, Analyses); + Context.expect(Repaired && rcc::MachineVerifier::verify(Function, Target), + "a second liveness rebuild must repair stale derived state"); +} + +void testTrackedLiveThroughPhysicalRegister(TestContext &Context, + const rcc::TargetDescription &Target) { + auto Fixture = createFunction("tracked-live-through", 11, 2); + if (!Fixture) { + Context.expect(false, "tracked live-through fixture creation must succeed"); + return; + } + auto &Function = *Fixture->Function; + auto &Predecessor = *Fixture->Blocks[0]; + auto &Successor = *Fixture->Blocks[1]; + const rcc::Register G0 = rcc::Register::fromPhysical(physical(rcc::test::TestRegister::G0)); + auto Edge = + Function.cfgUpdater().addSuccessor(Predecessor, Successor, rcc::BranchProbability::one()); + auto Use = buildUse(Function, Target, Successor, G0); + auto LiveIn = Successor.addLiveIn(physical(rcc::test::TestRegister::G0)); + if (!Edge || !Use || !LiveIn) { + Context.expect(false, "tracked live-through MachineIR must be constructed"); + return; + } + + Function.properties().set(rcc::MachineFunctionProperty::TracksLiveness); + Context.expect(rcc::verifyTrackedMachineLiveness(Function, Target).has_value() && + Predecessor.liveIns().empty(), + "a predecessor must not declare a physical register used only by its successor"); + Successor.clearLiveIns(); + Context.expect(!rcc::verifyTrackedMachineLiveness(Function, Target), + "a block must declare a physical register that it reads before defining"); +} + +void testFailedRebuildIsAtomic(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction("failed-rebuild", 9, 2); + if (!Fixture) { + Context.expect(false, "failed-rebuild fixture creation must succeed"); + return; + } + auto Destination = createVReg(*Fixture->Function); + auto Source = createVReg(*Fixture->Function); + if (!Destination || !Source) { + Context.expect(false, "failed-rebuild virtual registers must be created"); + return; + } + + auto &Predecessor = *Fixture->Blocks[0]; + auto &PhiBlock = *Fixture->Blocks[1]; + const auto LiveIn = + PhiBlock.addLiveIn(physical(rcc::test::TestRegister::G0), rcc::LaneMask::fromRaw(0x1)); + rcc::RegisterOperandFlags KillFlags; + KillFlags.set(rcc::RegisterOperandFlag::Kill); + rcc::MachineIRBuilder Builder(*Fixture->Function, Target); + auto Point = Builder.setInsertPoint(PhiBlock); + auto Pending = Point ? Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::PHI)) + : rcc::Expected<rcc::MachineInstrBuilder>{std::unexpected(Point.error())}; + auto Def = Pending ? Pending->addDef(*Destination) + : rcc::Expected<void>{std::unexpected(Pending.error())}; + auto Use = + Def ? Pending->addUse(*Source, KillFlags) : rcc::Expected<void>{std::unexpected(Def.error())}; + auto Incoming = + Use ? Pending->addBasicBlock(Predecessor) : rcc::Expected<void>{std::unexpected(Use.error())}; + auto Phi = Incoming ? Pending->build() + : rcc::Expected<rcc::MachineInstr *>{std::unexpected(Incoming.error())}; + if (!LiveIn || !Phi) { + Context.expect(false, "malformed-edge PHI setup must succeed"); + return; + } + + const std::vector<rcc::MachineBasicBlockLiveIn> OriginalLiveIns(PhiBlock.liveIns().begin(), + PhiBlock.liveIns().end()); + const rcc::RegisterOperandFlags OriginalFlags = (*Phi)->operandAt(1)->registerFlags(); + auto Rebuilt = rcc::rebuildMachineLiveness(*Fixture->Function, Target); + Context.expect(!Rebuilt && Rebuilt.error().code() == rcc::ErrorCode::VerificationError && + std::ranges::equal(PhiBlock.liveIns(), OriginalLiveIns) && + (*Phi)->operandAt(1)->registerFlags() == OriginalFlags, + "malformed PHI edges must fail without changing live-ins or operand flags"); +} + +class DummyAnalysis final { +public: + struct Result final {}; + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + return Result{}; + } +}; + +void testAnalysisRegistrationAndInvalidation(TestContext &Context, + const rcc::TargetDescription &Target) { + auto Fixture = createFunction("analysis-invalidation", 7, 1); + if (!Fixture) { + Context.expect(false, "analysis-invalidation fixture creation must succeed"); + return; + } + auto Reg = createVReg(*Fixture->Function); + auto Def = Reg ? buildDef(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg) + : rcc::Expected<rcc::MachineInstr *>{std::unexpected(Reg.error())}; + auto Use = Reg ? buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg) + : rcc::Expected<rcc::MachineInstr *>{std::unexpected(Reg.error())}; + if (!Reg || !Def || !Use) { + Context.expect(false, "analysis-invalidation instructions must be built"); + return; + } + + rcc::MachineFunctionAnalysisManager Analyses; + auto FirstRegistration = rcc::registerStage11MachineAnalyses(Analyses); + auto SecondRegistration = rcc::registerStage11MachineAnalyses(Analyses); + auto Intervals = FirstRegistration + ? Analyses.getResult<rcc::LiveIntervalsAnalysis>(*Fixture->Function, Target) + : rcc::Expected<rcc::LiveIntervalsAnalysis::Result *>{ + std::unexpected(FirstRegistration.error())}; + Context.expect(FirstRegistration && SecondRegistration && Intervals && + Analyses.cachedResultCount(*Fixture->Function) == 3, + "Stage 11 analyses must register idempotently and materialize dependencies"); + Analyses.invalidate(*Fixture->Function, rcc::PreservedAnalyses::none(), + {rcc::MachinePassChange::Frame}); + Context.expect(Analyses.cachedResultCount(*Fixture->Function) == 3, + "debug-only changes must preserve all liveness analyses"); + Analyses.invalidate(*Fixture->Function, rcc::PreservedAnalyses::none(), + {rcc::MachinePassChange::Operands}); + Context.expect( + Analyses.cachedResultCount(*Fixture->Function) == 1 && + Analyses.hasCached(*Fixture->Function, + rcc::MachineFunctionAnalysisManager::id<rcc::SlotIndexesAnalysis>()), + "operand changes must retain SlotIndexes while invalidating liveness dependents"); + auto Recomputed = Analyses.getResult<rcc::LiveIntervalsAnalysis>(*Fixture->Function, Target); + Context.expect(Recomputed && Analyses.cachedResultCount(*Fixture->Function) == 3, + "invalidated liveness dependencies must recompute on demand"); + Analyses.invalidate(*Fixture->Function, rcc::PreservedAnalyses::none(), + {rcc::MachinePassChange::BlockLayout}); + Context.expect( + Analyses.cachedResultCount(*Fixture->Function) == 1 && + Analyses.hasCached(*Fixture->Function, + rcc::MachineFunctionAnalysisManager::id<rcc::LiveVariablesAnalysis>()), + "layout changes must invalidate SlotIndexes and LiveIntervals only"); + + rcc::MachineFunctionAnalysisManager Atomic; + rcc::MachineAnalysisRegistration DummyRegistration; + DummyRegistration.Name = "SlotIndexes"; + auto Dummy = Atomic.registerAnalysis<DummyAnalysis>(std::move(DummyRegistration)); + auto Rejected = rcc::registerStage11MachineAnalyses(Atomic); + Context.expect( + Dummy && !Rejected && + Atomic.isRegistered(rcc::MachineFunctionAnalysisManager::id<DummyAnalysis>()) && + !Atomic.isRegistered( + rcc::MachineFunctionAnalysisManager::id<rcc::SlotIndexesAnalysis>()) && + !Atomic.isRegistered( + rcc::MachineFunctionAnalysisManager::id<rcc::LiveVariablesAnalysis>()), + "failed standard-analysis registration must leave the manager unchanged"); +} + +void testRISCVReservedRegisterLiveness(TestContext &Context) { + auto Target = rcc::riscv::createTargetDescription(); + auto Fixture = createFunction("riscv-reserved-liveness", 10, 1); + if (!Target || !Fixture) { + Context.expect(false, "RV64GC reserved-register liveness fixture creation must succeed"); + return; + } + auto X0 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X0); + auto X5 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X5); + if (!X0 || !X5) { + Context.expect(false, "RV64GC reserved-register fixture registers must resolve"); + return; + } + + rcc::MachineIRBuilder Builder(*Fixture->Function, *Target); + auto Point = Builder.setInsertPoint(*Fixture->Blocks[0]); + auto Pending = Point ? Builder.buildInstr(rcc::Opcode::fromValue( + static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::ADDI))) + : rcc::Expected<rcc::MachineInstrBuilder>{std::unexpected(Point.error())}; + auto Def = Pending ? Pending->addDef(rcc::Register::fromPhysical(*X5)) + : rcc::Expected<void>{std::unexpected(Pending.error())}; + auto Use = Def ? Pending->addUse(rcc::Register::fromPhysical(*X0)) + : rcc::Expected<void>{std::unexpected(Def.error())}; + auto Immediate = + Use ? Pending->addImmediate(1) : rcc::Expected<void>{std::unexpected(Use.error())}; + auto Instruction = Immediate + ? Pending->build() + : rcc::Expected<rcc::MachineInstr *>{std::unexpected(Immediate.error())}; + auto DeadDef = + Instruction ? Builder.buildInstr(rcc::Opcode::fromValue( + static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::ADDI))) + : rcc::Expected<rcc::MachineInstrBuilder>{std::unexpected(Instruction.error())}; + auto DeadDefDestination = DeadDef ? DeadDef->addDef(rcc::Register::fromPhysical(*X0)) + : rcc::Expected<void>{std::unexpected(DeadDef.error())}; + auto DeadDefSource = DeadDefDestination + ? DeadDef->addUse(rcc::Register::fromPhysical(*X0)) + : rcc::Expected<void>{std::unexpected(DeadDefDestination.error())}; + auto DeadDefImmediate = DeadDefSource + ? DeadDef->addImmediate(0) + : rcc::Expected<void>{std::unexpected(DeadDefSource.error())}; + auto DeadDefInstruction = + DeadDefImmediate + ? DeadDef->build() + : rcc::Expected<rcc::MachineInstr *>{std::unexpected(DeadDefImmediate.error())}; + if (!Instruction || !DeadDefInstruction) { + Context.expect(false, "RV64GC reserved-register liveness instruction must be built"); + return; + } + + auto Rebuilt = rcc::rebuildMachineLiveness(*Fixture->Function, *Target); + const rcc::MachineOperand *ZeroUse = (*Instruction)->operandAt(1); + const rcc::MachineOperand *ZeroDef = (*DeadDefInstruction)->operandAt(0); + const rcc::MachineOperand *DeadDefZeroUse = (*DeadDefInstruction)->operandAt(1); + Context.expect(Rebuilt && Fixture->Blocks[0]->liveIns().empty() && ZeroUse != nullptr && + !ZeroUse->registerFlags().has(rcc::RegisterOperandFlag::Kill) && + ZeroDef != nullptr && + ZeroDef->registerFlags().has(rcc::RegisterOperandFlag::Dead) && + DeadDefZeroUse != nullptr && + !DeadDefZeroUse->registerFlags().has(rcc::RegisterOperandFlag::Kill), + "reserved uses must remain available while dead defs retain precise liveness"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "liveness TestTarget must be created"); + if (!Target) + return Context.result(); + + testLivenessRebuildAndVerifier(Context, *Target); + testTrackedLiveThroughPhysicalRegister(Context, *Target); + testFailedRebuildIsAtomic(Context, *Target); + testAnalysisRegistrationAndInvalidation(Context, *Target); + testRISCVReservedRegisterLiveness(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineAnalysesTest.cpp b/backend/unittests/CodeGen/MachineAnalysesTest.cpp new file mode 100644 index 0000000..53cd847 --- /dev/null +++ b/backend/unittests/CodeGen/MachineAnalysesTest.cpp @@ -0,0 +1,582 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineAnalyses.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/MachineCFGUpdater.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/CodeGen/MachineVerifier.h" + +#include <algorithm> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + std::vector<rcc::InstructionDescriptor> Descriptors; + std::vector<rcc::ImmediatePredicate> Predicates; + auto Appended = rcc::appendGenericInstructionInfo(Descriptors, Predicates); + if (!Appended) + return std::unexpected(Appended.error()); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors), std::move(Predicates)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] rcc::Expected<rcc::MachineBasicBlock *> appendBlock(rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + if (!Block) + return std::unexpected(Block.error()); + auto Appended = Function.appendBlock(**Block); + if (!Appended) + return std::unexpected(Appended.error()); + return *Block; +} + +void testCFGAnalyses(TestContext &Context, const rcc::TargetDescription &Target) { + auto Function = rcc::MachineFunction::create("cfg", rcc::MachineFunctionStableID::fromValue(1)); + Context.expect(Function.has_value(), "CFG analysis function creation must succeed"); + if (!Function) + return; + + auto Entry = appendBlock(**Function); + auto Header = appendBlock(**Function); + auto Body = appendBlock(**Function); + auto Exit = appendBlock(**Function); + auto Dead = appendBlock(**Function); + Context.expect(Entry && Header && Body && Exit && Dead, + "CFG analysis blocks must be appended successfully"); + if (!Entry || !Header || !Body || !Exit || !Dead) + return; + + auto ThreeQuarters = rcc::BranchProbability::fromFraction(3, 4); + auto OneQuarter = rcc::BranchProbability::fromFraction(1, 4); + auto Updater = (*Function)->cfgUpdater(); + Context.expect(ThreeQuarters && OneQuarter && + Updater.addSuccessor(**Entry, **Header, rcc::BranchProbability::one()) && + Updater.addSuccessor(**Header, **Body, *ThreeQuarters) && + Updater.addSuccessor(**Header, **Exit, *OneQuarter) && + Updater.addSuccessor(**Body, **Header, rcc::BranchProbability::one()), + "reducible loop CFG construction must succeed"); + + rcc::MachineFunctionAnalysisManager Analyses; + Context.expect(rcc::registerStage8MachineAnalyses(Analyses).has_value(), + "standard Stage 8 analyses must register atomically"); + auto Dominators = Analyses.getResult<rcc::MachineDominatorTreeAnalysis>(**Function, Target); + auto Loops = Analyses.getResult<rcc::MachineLoopAnalysis>(**Function, Target); + auto Probabilities = + Analyses.getResult<rcc::MachineBranchProbabilityAnalysis>(**Function, Target); + auto Frequencies = Analyses.getResult<rcc::MachineBlockFrequencyAnalysis>(**Function, Target); + Context.expect(Dominators && Loops && Probabilities && Frequencies, + "CFG analyses and their dependencies must materialize"); + if (!Dominators || !Loops || !Probabilities || !Frequencies) + return; + + Context.expect((*Dominators)->dominates(**Entry, **Exit) && + (*Dominators)->dominates(**Header, **Body) && + (*Dominators)->immediateDominator(**Body) == *Header && + (*Dominators)->nearestCommonDominator(**Body, **Exit) == *Header && + !(*Dominators)->isReachable(**Dead), + "dominator analysis must model reachability, IDom, and common dominators"); + Context.expect((*Loops)->loops().size() == 1 && (*Loops)->loopFor(**Header) != nullptr && + (*Loops)->loopFor(**Body) != nullptr && (*Loops)->loopDepth(**Body) == 1, + "loop analysis must recover the natural loop and loop depth"); + Context.expect((*Probabilities)->probability(**Header, **Body) == *ThreeQuarters && + (*Probabilities)->probability(**Header, **Exit) == *OneQuarter, + "branch-probability analysis must retain exact integer edge probabilities"); + Context.expect((*Frequencies)->frequency(**Entry) != 0 && + (*Frequencies)->frequency(**Header) > (*Frequencies)->frequency(**Entry) && + (*Frequencies)->frequency(**Dead) == 0, + "block-frequency analysis must distinguish loop and unreachable blocks"); + + auto Alias = Analyses.getResult<rcc::MachineAliasAnalysis>(**Function, Target); + auto MemoryDependence = + Analyses.getResult<rcc::MachineMemoryDependenceAnalysis>(**Function, Target); + Context.expect(Alias && MemoryDependence && Analyses.cachedResultCount(**Function) == 6, + "the complete Stage 8 analysis set must materialize into one function cache"); + Analyses.invalidate(**Function, rcc::PreservedAnalyses::none(), + {rcc::MachinePassChange::Registers}); + Context.expect(Analyses.cachedResultCount(**Function) == 6, + "register-only changes must preserve CFG and memory analyses"); + Analyses.invalidate(**Function, rcc::PreservedAnalyses::none(), {rcc::MachinePassChange::CFG}); + Context.expect( + Analyses.cachedResultCount(**Function) == 1 && + Analyses.hasCached(**Function, + rcc::MachineFunctionAnalysisManager::id<rcc::MachineAliasAnalysis>()), + "CFG changes must invalidate CFG dependents and memory dependence but preserve aliasing"); + + auto RefreshedDominators = + Analyses.getResult<rcc::MachineDominatorTreeAnalysis>(**Function, Target); + Context.expect( + RefreshedDominators && + Updater.addSuccessor(**Entry, **Exit, rcc::BranchProbability::zero()).has_value() && + (*RefreshedDominators)->updateAfterCFGChange(**Function).has_value() && + (*RefreshedDominators)->immediateDominator(**Exit) == *Entry, + "dominator result must support exact updates after CFG edge changes"); +} + +void testIrreducibleCFG(TestContext &Context, const rcc::TargetDescription &Target) { + auto Function = + rcc::MachineFunction::create("irreducible", rcc::MachineFunctionStableID::fromValue(2)); + if (!Function) { + Context.expect(false, "irreducible CFG function creation must succeed"); + return; + } + auto Entry = appendBlock(**Function); + auto Left = appendBlock(**Function); + auto Right = appendBlock(**Function); + auto Exit = appendBlock(**Function); + if (!Entry || !Left || !Right || !Exit) { + Context.expect(false, "irreducible CFG blocks must be appended"); + return; + } + auto Half = rcc::BranchProbability::fromFraction(1, 2); + auto Updater = (*Function)->cfgUpdater(); + Context.expect(Half && Updater.addSuccessor(**Entry, **Left, *Half) && + Updater.addSuccessor(**Entry, **Right, *Half) && + Updater.addSuccessor(**Left, **Right, *Half) && + Updater.addSuccessor(**Left, **Exit, *Half) && + Updater.addSuccessor(**Right, **Left, *Half) && + Updater.addSuccessor(**Right, **Exit, *Half), + "irreducible CFG construction must succeed"); + + rcc::MachineFunctionAnalysisManager Analyses; + auto Registered = rcc::registerStage8MachineAnalyses(Analyses); + auto Loops = + Registered + ? Analyses.getResult<rcc::MachineLoopAnalysis>(**Function, Target) + : rcc::Expected<rcc::MachineLoopAnalysis::Result *>{std::unexpected(Registered.error())}; + Context.expect(Loops && (*Loops)->irreducibleRegions().size() == 1 && + (*Loops)->irreducibleRegions().front().Entries.size() == 2 && + (*Loops)->isIrreducible(**Left) && (*Loops)->isIrreducible(**Right), + "loop analysis must explicitly identify multi-entry irreducible regions"); +} + +[[nodiscard]] rcc::MachineMemOperandDesc +memoryDescription(rcc::MemoryBase Base, std::int64_t Offset, rcc::MemoryAccessFlag Access) { + rcc::MachineMemOperandDesc Description; + Description.Accesses = {Access}; + Description.Size = *rcc::MemorySize::fixed(8); + Description.Align = *rcc::Alignment::create(8); + Description.Base = std::move(Base); + Description.Offset = Offset; + return Description; +} + +void testAliasAnalysis(TestContext &Context) { + auto First = rcc::MachineMemOperand::create(memoryDescription( + rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)), 0, rcc::MemoryAccessFlag::Load)); + auto Same = rcc::MachineMemOperand::create(memoryDescription( + rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)), 0, rcc::MemoryAccessFlag::Store)); + auto Partial = rcc::MachineMemOperand::create(memoryDescription( + rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)), 4, rcc::MemoryAccessFlag::Store)); + auto DisjointRange = rcc::MachineMemOperand::create( + memoryDescription(rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)), 16, + rcc::MemoryAccessFlag::Store)); + auto DistinctObject = rcc::MachineMemOperand::create(memoryDescription( + rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(1)), 0, rcc::MemoryAccessFlag::Store)); + auto DifferentObjectKind = rcc::MachineMemOperand::create( + memoryDescription(rcc::MemoryBase::constantPool(rcc::ConstantPoolIndex::fromValue(0)), 0, + rcc::MemoryAccessFlag::Store)); + Context.expect(First && Same && Partial && DisjointRange && DistinctObject && DifferentObjectKind, + "alias-analysis memory operands must be valid"); + if (!First || !Same || !Partial || !DisjointRange || !DistinctObject || !DifferentObjectKind) + return; + + rcc::MachineAliasAnalysis::Result Alias; + Context.expect(Alias.alias(*First, *Same) == rcc::MachineAliasResult::MustAlias && + Alias.alias(*First, *Partial) == rcc::MachineAliasResult::PartialAlias && + Alias.alias(*First, *DisjointRange) == rcc::MachineAliasResult::NoAlias && + Alias.alias(*First, *DistinctObject) == rcc::MachineAliasResult::NoAlias && + Alias.alias(*First, *DifferentObjectKind) == rcc::MachineAliasResult::NoAlias, + "alias analysis must distinguish exact, partial, range, and object aliasing"); +} + +void testMemoryDependence(TestContext &Context, const rcc::TargetDescription &Target) { + auto Function = + rcc::MachineFunction::create("memory", rcc::MachineFunctionStableID::fromValue(3)); + if (!Function) { + Context.expect(false, "memory-dependence function creation must succeed"); + return; + } + auto Block = appendBlock(**Function); + auto OtherBlock = appendBlock(**Function); + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Pointer = rcc::LowLevelType::makePointer(0, 64); + auto Value = I64 ? (*Function)->registerInfo().createVirtualRegister(*I64) + : rcc::Expected<rcc::Register>{std::unexpected(I64.error())}; + auto Address = Pointer ? (*Function)->registerInfo().createVirtualRegister(*Pointer) + : rcc::Expected<rcc::Register>{std::unexpected(Pointer.error())}; + if (!Block || !OtherBlock || !Value || !Address) { + Context.expect(false, "memory-dependence MachineIR setup must succeed"); + return; + } + + auto StoreMemory = (*Function)->createMemOperand( + memoryDescription(rcc::MemoryBase::stack(), 0, rcc::MemoryAccessFlag::Store)); + auto AliasingLoadMemory = (*Function)->createMemOperand( + memoryDescription(rcc::MemoryBase::stack(), 0, rcc::MemoryAccessFlag::Load)); + auto DisjointLoadMemory = (*Function)->createMemOperand( + memoryDescription(rcc::MemoryBase::stack(), 32, rcc::MemoryAccessFlag::Load)); + auto VolatileStoreDescription = + memoryDescription(rcc::MemoryBase::stack(), 64, rcc::MemoryAccessFlag::Store); + VolatileStoreDescription.Accesses.set(rcc::MemoryAccessFlag::Volatile); + auto VolatileLoadDescription = + memoryDescription(rcc::MemoryBase::stack(), 96, rcc::MemoryAccessFlag::Load); + VolatileLoadDescription.Accesses.set(rcc::MemoryAccessFlag::Volatile); + auto VolatileStoreMemory = (*Function)->createMemOperand(std::move(VolatileStoreDescription)); + auto VolatileLoadMemory = (*Function)->createMemOperand(std::move(VolatileLoadDescription)); + auto OtherBlockLoadMemory = (*Function)->createMemOperand( + memoryDescription(rcc::MemoryBase::stack(), 0, rcc::MemoryAccessFlag::Load)); + rcc::MachineIRBuilder Builder(**Function, Target); + Context.expect(StoreMemory && AliasingLoadMemory && DisjointLoadMemory && VolatileStoreMemory && + VolatileLoadMemory && OtherBlockLoadMemory && Builder.setInsertPoint(**Block), + "memory-dependence builder setup must succeed"); + if (!StoreMemory || !AliasingLoadMemory || !DisjointLoadMemory || !VolatileStoreMemory || + !VolatileLoadMemory || !OtherBlockLoadMemory) + return; + + auto Store = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_STORE)); + if (!Store || !Store->addUse(*Value) || !Store->addUse(*Address) || + !Store->addMemOperand(**StoreMemory)) { + Context.expect(false, "generic store construction must succeed"); + return; + } + auto StoreInstruction = Store->build(); + auto Load = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_LOAD)); + if (!Load || !Load->addDef(*Value) || !Load->addUse(*Address) || + !Load->addMemOperand(**AliasingLoadMemory)) { + Context.expect(false, "aliasing generic load construction must succeed"); + return; + } + auto LoadInstruction = Load->build(); + auto DisjointLoad = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_LOAD)); + if (!DisjointLoad || !DisjointLoad->addDef(*Value) || !DisjointLoad->addUse(*Address) || + !DisjointLoad->addMemOperand(**DisjointLoadMemory)) { + Context.expect(false, "disjoint generic load construction must succeed"); + return; + } + auto DisjointLoadInstruction = DisjointLoad->build(); + auto VolatileStore = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_STORE)); + if (!VolatileStore || !VolatileStore->addUse(*Value) || !VolatileStore->addUse(*Address) || + !VolatileStore->addMemOperand(**VolatileStoreMemory)) { + Context.expect(false, "volatile store construction must succeed"); + return; + } + auto VolatileStoreInstruction = VolatileStore->build(); + auto VolatileLoad = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_LOAD)); + if (!VolatileLoad || !VolatileLoad->addDef(*Value) || !VolatileLoad->addUse(*Address) || + !VolatileLoad->addMemOperand(**VolatileLoadMemory)) { + Context.expect(false, "volatile load construction must succeed"); + return; + } + auto VolatileLoadInstruction = VolatileLoad->build(); + if (!Builder.setInsertPoint(**OtherBlock)) { + Context.expect(false, "second memory region insertion point must be valid"); + return; + } + auto OtherBlockLoad = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_LOAD)); + if (!OtherBlockLoad || !OtherBlockLoad->addDef(*Value) || !OtherBlockLoad->addUse(*Address) || + !OtherBlockLoad->addMemOperand(**OtherBlockLoadMemory)) { + Context.expect(false, "second-region generic load construction must succeed"); + return; + } + auto OtherBlockLoadInstruction = OtherBlockLoad->build(); + Context.expect(StoreInstruction && LoadInstruction && DisjointLoadInstruction && + VolatileStoreInstruction && VolatileLoadInstruction && + OtherBlockLoadInstruction, + "memory instructions must be published transactionally"); + if (!StoreInstruction || !LoadInstruction || !DisjointLoadInstruction || + !VolatileStoreInstruction || !VolatileLoadInstruction || !OtherBlockLoadInstruction) + return; + + rcc::MachineFunctionAnalysisManager Analyses; + auto Registered = rcc::registerStage8MachineAnalyses(Analyses); + auto Dependencies = + Registered ? Analyses.getResult<rcc::MachineMemoryDependenceAnalysis>(**Function, Target) + : rcc::Expected<rcc::MachineMemoryDependenceAnalysis::Result *>{ + std::unexpected(Registered.error())}; + Context.expect(Dependencies && (*Dependencies)->dependencies(**LoadInstruction).size() == 1 && + (*Dependencies)->dependencies(**LoadInstruction).front().Source == + (*StoreInstruction)->stableID() && + (*Dependencies)->dependencies(**DisjointLoadInstruction).empty(), + "memory dependence must add flow edges only for potentially aliasing accesses"); + Context.expect( + Dependencies && (*Dependencies)->regions().size() == 2 && + (*Dependencies)->regionFor(**Block) != nullptr && + (*Dependencies)->regionFor(**Block)->Instructions.size() == 5 && + (*Dependencies)->regionFor(**OtherBlock) != nullptr && + (*Dependencies)->regionFor(**OtherBlock)->Instructions.size() == 1 && + (*Dependencies)->dependencies(**OtherBlockLoadInstruction).empty() && + (*Dependencies)->dependencies(**OtherBlock).empty(), + "memory dependence must expose deterministic block regions without cross-CFG edges"); + bool HasOrderedVolatileDependency = false; + if (Dependencies) { + HasOrderedVolatileDependency = + std::ranges::any_of((*Dependencies)->dependencies(**VolatileLoadInstruction), + [&](const rcc::MachineMemoryDependency &Dependency) { + return Dependency.Source == (*VolatileStoreInstruction)->stableID(); + }); + } + Context.expect(HasOrderedVolatileDependency, + "ordered memory accesses must retain dependencies across disjoint addresses"); +} + +class ConflictingAnalysis final { +public: + struct Result final {}; + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + return Result{}; + } +}; + +void testAtomicStandardRegistration(TestContext &Context) { + rcc::MachineFunctionAnalysisManager Analyses; + Context.expect(Analyses + .registerAnalysis<ConflictingAnalysis>({ + .Name = "MachineLoopInfo", + .Dependencies = {}, + }) + .has_value(), + "analysis-name conflict setup must succeed"); + Context.expect( + !rcc::registerStage8MachineAnalyses(Analyses) && + !Analyses.isRegistered( + rcc::MachineFunctionAnalysisManager::id<rcc::MachineDominatorTreeAnalysis>()), + "failed standard-analysis registration must not publish a partial set"); +} + +void testCFGUpdaterPHIs(TestContext &Context, const rcc::TargetDescription &Target) { + auto Function = + rcc::MachineFunction::create("cfg_phi", rcc::MachineFunctionStableID::fromValue(4)); + if (!Function) { + Context.expect(false, "CFG PHI function creation must succeed"); + return; + } + auto Source = appendBlock(**Function); + auto Other = appendBlock(**Function); + auto Merge = appendBlock(**Function); + auto Unconnected = appendBlock(**Function); + if (!Source || !Other || !Merge || !Unconnected) { + Context.expect(false, "CFG PHI blocks must be appended"); + return; + } + auto Updater = (*Function)->cfgUpdater(); + if (!Updater.addSuccessor(**Source, **Merge, rcc::BranchProbability::one()) || + !Updater.addSuccessor(**Other, **Merge, rcc::BranchProbability::one())) { + Context.expect(false, "CFG PHI predecessor edges must be created"); + return; + } + + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Destination = I64 ? (*Function)->registerInfo().createVirtualRegister(*I64) + : rcc::Expected<rcc::Register>{std::unexpected(I64.error())}; + auto SourceValue = I64 ? (*Function)->registerInfo().createVirtualRegister(*I64) + : rcc::Expected<rcc::Register>{std::unexpected(I64.error())}; + auto OtherValue = I64 ? (*Function)->registerInfo().createVirtualRegister(*I64) + : rcc::Expected<rcc::Register>{std::unexpected(I64.error())}; + if (!Destination || !SourceValue || !OtherValue) { + Context.expect(false, "CFG PHI registers must be created"); + return; + } + + rcc::MachineIRBuilder Builder(**Function, Target); + if (!Builder.setInsertPoint(**Source)) + return; + auto SourceDefinition = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_IMPLICIT_DEF)); + if (!SourceDefinition || !SourceDefinition->addDef(*SourceValue) || !SourceDefinition->build()) { + Context.expect(false, "CFG PHI source definition must build"); + return; + } + auto Branch = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_BR)); + if (!Branch || !Branch->addBasicBlock(**Merge)) { + Context.expect(false, "CFG PHI branch must be assembled"); + return; + } + auto BranchInstruction = Branch->build(); + if (!BranchInstruction || !Builder.setInsertPoint(**Merge)) { + Context.expect(false, "CFG PHI branch must publish"); + return; + } + auto Phi = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_PHI)); + if (!Phi || !Phi->addDef(*Destination) || !Phi->addUse(*SourceValue) || + !Phi->addBasicBlock(**Source) || !Phi->addUse(*OtherValue) || !Phi->addBasicBlock(**Other)) { + Context.expect(false, "CFG PHI instruction must be assembled"); + return; + } + auto PhiInstruction = Phi->build(); + Context.expect(PhiInstruction && rcc::MachineVerifier::verify(**Function, Target), + "initial CFG PHI function must verify"); + if (!PhiInstruction) + return; + + auto Split = Updater.splitBlock(**Source, **BranchInstruction); + Context.expect( + Split && (*Split)->predecessors().size() == 1 && + (*Split)->predecessors().front() == *Source && (*Split)->successors().size() == 1 && + (*PhiInstruction)->operandAt(2)->getIf<rcc::BasicBlockOperand>()->Block == *Split && + rcc::MachineVerifier::verify(**Function, Target), + "block split must redirect CFG edges and PHI predecessor identity"); + if (!Split) + return; + + Context.expect(Updater.removeSuccessor(**Other, **Merge) && + (*PhiInstruction)->operandCount() == 3 && + rcc::MachineVerifier::verify(**Function, Target), + "edge deletion must remove the corresponding PHI incoming pair"); + Context.expect(!Updater.addSuccessor(**Unconnected, **Merge, rcc::BranchProbability::one()), + "new PHI predecessors without incoming values must be rejected"); + Context.expect(Updater.deleteBlock(**Other) && !(*Other)->isLinked() && + rcc::MachineVerifier::verify(**Function, Target), + "CFG block deletion must detach a disconnected block cleanly"); +} + +void testCFGUpdaterFailureAtomicity(TestContext &Context, const rcc::TargetDescription &Target) { + auto SplitFunction = + rcc::MachineFunction::create("cfg_split_atomic", rcc::MachineFunctionStableID::fromValue(5)); + if (!SplitFunction) { + Context.expect(false, "CFG split atomicity function creation must succeed"); + return; + } + auto Source = appendBlock(**SplitFunction); + auto Merge = appendBlock(**SplitFunction); + if (!Source || !Merge) + return; + auto SplitUpdater = (*SplitFunction)->cfgUpdater(); + if (!SplitUpdater.addSuccessor(**Source, **Merge, rcc::BranchProbability::one())) + return; + + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Destination = I64 ? (*SplitFunction)->registerInfo().createVirtualRegister(*I64) + : rcc::Expected<rcc::Register>{std::unexpected(I64.error())}; + auto Value = I64 ? (*SplitFunction)->registerInfo().createVirtualRegister(*I64) + : rcc::Expected<rcc::Register>{std::unexpected(I64.error())}; + if (!Destination || !Value) + return; + + rcc::MachineIRBuilder Builder(**SplitFunction, Target); + if (!Builder.setInsertPoint(**Source)) + return; + auto Branch = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_BR)); + auto BranchInstruction = + Branch && Branch->addBasicBlock(**Merge) + ? Branch->build() + : rcc::Expected<rcc::MachineInstr *>{rcc::makeUnexpected( + rcc::ErrorCode::InternalError, "failed to build CFG split branch")}; + if (!BranchInstruction || !Builder.setInsertPoint(**Merge)) + return; + auto Phi = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_PHI)); + auto PhiInstruction = + Phi && Phi->addDef(*Destination) && Phi->addUse(*Value) && Phi->addBasicBlock(**Source) + ? Phi->build() + : rcc::Expected<rcc::MachineInstr *>{rcc::makeUnexpected( + rcc::ErrorCode::InternalError, "failed to build CFG split PHI")}; + if (!PhiInstruction || !(*PhiInstruction)->eraseOperand(2)) + return; + + const std::size_t LinkedBlocksBefore = (*SplitFunction)->blockCount(); + const std::size_t AllocatedBlocksBefore = (*SplitFunction)->allBlocks().size(); + auto FailedSplit = SplitUpdater.splitBlock(**Source, **BranchInstruction); + Context.expect( + !FailedSplit && (*SplitFunction)->blockCount() == LinkedBlocksBefore && + (*SplitFunction)->allBlocks().size() == AllocatedBlocksBefore && + (*BranchInstruction)->parent() == *Source && (*Source)->successors().size() == 1 && + (*Source)->successors().front().Block == *Merge && (*Merge)->predecessors().size() == 1 && + (*Merge)->predecessors().front() == *Source, + "failed block split must not mutate layout, instructions, or CFG edges"); + + auto FailedDelete = SplitUpdater.deleteBlock(**Source); + Context.expect(!FailedDelete && (*Source)->isLinked() && + (*SplitFunction)->blockCount() == LinkedBlocksBefore && + (*Source)->successors().size() == 1 && (*Merge)->predecessors().size() == 1, + "failed block deletion must not partially disconnect CFG edges"); + + auto TransferFunction = rcc::MachineFunction::create("cfg_transfer_atomic", + rcc::MachineFunctionStableID::fromValue(6)); + if (!TransferFunction) + return; + auto From = appendBlock(**TransferFunction); + auto To = appendBlock(**TransferFunction); + auto Successor = appendBlock(**TransferFunction); + if (!From || !To || !Successor) + return; + auto TransferUpdater = (*TransferFunction)->cfgUpdater(); + if (!TransferUpdater.addSuccessor(**From, **Successor, rcc::BranchProbability::one()) || + !TransferUpdater.addSuccessor(**To, **Successor, rcc::BranchProbability::one())) { + return; + } + auto TransferDestination = I64 ? (*TransferFunction)->registerInfo().createVirtualRegister(*I64) + : rcc::Expected<rcc::Register>{std::unexpected(I64.error())}; + auto TransferValue = I64 ? (*TransferFunction)->registerInfo().createVirtualRegister(*I64) + : rcc::Expected<rcc::Register>{std::unexpected(I64.error())}; + if (!TransferDestination || !TransferValue) + return; + rcc::MachineIRBuilder TransferBuilder(**TransferFunction, Target); + if (!TransferBuilder.setInsertPoint(**Successor)) + return; + auto TransferPhi = TransferBuilder.buildInstr(rcc::opcode(rcc::GenericOpcode::G_PHI)); + auto TransferPhiInstruction = + TransferPhi && TransferPhi->addDef(*TransferDestination) && + TransferPhi->addUse(*TransferValue) && TransferPhi->addBasicBlock(**From) + ? TransferPhi->build() + : rcc::Expected<rcc::MachineInstr *>{rcc::makeUnexpected( + rcc::ErrorCode::InternalError, "failed to build CFG transfer PHI")}; + if (!TransferPhiInstruction) + return; + + auto FailedTransfer = TransferUpdater.transferSuccessors(**From, **To); + Context.expect( + !FailedTransfer && (*From)->successors().size() == 1 && (*To)->successors().size() == 1 && + (*Successor)->predecessors().size() == 2 && + (*TransferPhiInstruction)->operandCount() == 3 && + (*TransferPhiInstruction)->operandAt(2)->getIf<rcc::BasicBlockOperand>()->Block == *From, + "failed successor transfer must preserve both CFG edges and PHI operands"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "machine analysis tests require a valid target"); + if (!Target) + return Context.result(); + + testCFGAnalyses(Context, *Target); + testIrreducibleCFG(Context, *Target); + testAliasAnalysis(Context); + testMemoryDependence(Context, *Target); + testCFGUpdaterPHIs(Context, *Target); + testCFGUpdaterFailureAtomicity(Context, *Target); + testAtomicStandardRegistration(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineCompilationStatusTest.cpp b/backend/unittests/CodeGen/MachineCompilationStatusTest.cpp new file mode 100644 index 0000000..84cada0 --- /dev/null +++ b/backend/unittests/CodeGen/MachineCompilationStatusTest.cpp @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineCompilationStatus.h" + +#include <array> +#include <cstdint> +#include <iostream> +#include <string> +#include <string_view> +#include <utility> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::MachineCompilationFailure +makeFailure(std::string Message = "selection did not match") { + rcc::MachineCompilationFailure Failure; + Failure.Diagnostic.Severity = rcc::MachineCompilationDiagnosticSeverity::Error; + Failure.Diagnostic.Code = rcc::ErrorCode::Unsupported; + Failure.Diagnostic.Message = std::move(Message); + Failure.Diagnostic.Location = rcc::SourceLocation{"input.mir", 7, 3, 0}; + Failure.FailurePass = "instruction-select"; + Failure.FailureInstruction = rcc::MachineInstrStableID::fromValue(17); + Failure.FailureStage = rcc::PipelineStage::Selected; + Failure.FailureRule = "rv64-test-pattern"; + Failure.Target = "riscv64-unknown-linux-gnu"; + Failure.Subtarget = "rv64gc"; + return Failure; +} + +void testStateNames(TestContext &Context) { + constexpr std::array States{ + rcc::MachineCompilationState::Success, + rcc::MachineCompilationState::Unsupported, + rcc::MachineCompilationState::MIRParseFailed, + rcc::MachineCompilationState::LegalizationFailed, + rcc::MachineCompilationState::SelectionFailed, + rcc::MachineCompilationState::RegisterAllocationFailed, + rcc::MachineCompilationState::CallLoweringFailed, + rcc::MachineCompilationState::FrameLayoutFailed, + rcc::MachineCompilationState::FinalizationFailed, + rcc::MachineCompilationState::AssemblyLoweringFailed, + rcc::MachineCompilationState::InternalError, + }; + for (const rcc::MachineCompilationState State : States) { + Context.expect(rcc::isKnownMachineCompilationState(State) && + rcc::getMachineCompilationStateName(State) != "Unknown", + "every public compilation state must have a stable name"); + } + Context.expect(!rcc::isKnownMachineCompilationState(rcc::MachineCompilationState::Count) && + rcc::getMachineCompilationStateName(rcc::MachineCompilationState::Count) == + "Unknown", + "compilation state sentinel must not be accepted as a public state"); +} + +void testSuccessfulAndFailedStatus(TestContext &Context) { + rcc::MachineCompilationStatus Status; + Context.expect(Status.succeeded() && Status.state() == rcc::MachineCompilationState::Success && + Status.failure() == nullptr && Status.verify().has_value(), + "default compilation status must represent verified success"); + + Context.expect( + Status.fail(rcc::MachineCompilationState::SelectionFailed, makeFailure()).has_value(), + "valid structured failure must atomically replace success"); + const rcc::MachineCompilationFailure *Failure = Status.failure(); + Context.expect(!Status.succeeded() && + Status.state() == rcc::MachineCompilationState::SelectionFailed && + Failure != nullptr && + Failure->FailureInstruction == rcc::MachineInstrStableID::fromValue(17) && + Failure->FailureStage == rcc::PipelineStage::Selected && + Failure->Target == "riscv64-unknown-linux-gnu" && Status.verify().has_value(), + "failed compilation status must preserve all structured context"); + + Context.expect( + !Status.fail(rcc::MachineCompilationState::InternalError, makeFailure("second failure")), + "first compilation failure must remain sticky"); + Context.expect(Status.state() == rcc::MachineCompilationState::SelectionFailed && + Status.failure() != nullptr && + Status.failure()->Diagnostic.Message == "selection did not match", + "a rejected later failure must not mutate the original status"); +} + +void testInvalidFailures(TestContext &Context) { + rcc::MachineCompilationStatus SuccessState; + Context.expect(!SuccessState.fail(rcc::MachineCompilationState::Success, makeFailure()), + "failure API must reject the success state"); + + rcc::MachineCompilationStatus UnknownState; + Context.expect(!UnknownState.fail(rcc::MachineCompilationState::Count, makeFailure()), + "failure API must reject an unknown state"); + + rcc::MachineCompilationStatus EmptyDiagnostic; + Context.expect( + !EmptyDiagnostic.fail(rcc::MachineCompilationState::SelectionFailed, makeFailure({})), + "failure API must reject an empty diagnostic message"); + + auto InvalidLocationFailure = makeFailure(); + InvalidLocationFailure.Diagnostic.Location = rcc::SourceLocation{"", 0, 0, 0}; + rcc::MachineCompilationStatus InvalidLocation; + Context.expect(!InvalidLocation.fail(rcc::MachineCompilationState::SelectionFailed, + std::move(InvalidLocationFailure)), + "failure API must reject an invalid diagnostic location"); + + auto InvalidInstructionFailure = makeFailure(); + InvalidInstructionFailure.FailureInstruction = rcc::MachineInstrStableID{}; + rcc::MachineCompilationStatus InvalidInstruction; + Context.expect(!InvalidInstruction.fail(rcc::MachineCompilationState::SelectionFailed, + std::move(InvalidInstructionFailure)), + "failure API must reject an invalid instruction StableID"); + + auto InvalidStageFailure = makeFailure(); + InvalidStageFailure.FailureStage = static_cast<rcc::PipelineStage>(255); + rcc::MachineCompilationStatus InvalidStage; + Context.expect(!InvalidStage.fail(rcc::MachineCompilationState::SelectionFailed, + std::move(InvalidStageFailure)), + "failure API must reject an unknown pipeline stage"); + + auto EmptyPassFailure = makeFailure(); + EmptyPassFailure.FailurePass = std::string{}; + rcc::MachineCompilationStatus EmptyPass; + Context.expect( + !EmptyPass.fail(rcc::MachineCompilationState::SelectionFailed, std::move(EmptyPassFailure)), + "failure API must reject empty optional text fields"); + + auto InvalidSeverityFailure = makeFailure(); + InvalidSeverityFailure.Diagnostic.Severity = rcc::MachineCompilationDiagnosticSeverity::Count; + rcc::MachineCompilationStatus InvalidSeverity; + Context.expect(!InvalidSeverity.fail(rcc::MachineCompilationState::SelectionFailed, + std::move(InvalidSeverityFailure)), + "failure API must reject an unknown diagnostic severity"); + + auto InvalidCodeFailure = makeFailure(); + InvalidCodeFailure.Diagnostic.Code = static_cast<rcc::ErrorCode>(255); + rcc::MachineCompilationStatus InvalidCode; + Context.expect(!InvalidCode.fail(rcc::MachineCompilationState::SelectionFailed, + std::move(InvalidCodeFailure)), + "failure API must reject an unknown diagnostic code"); +} + +} // namespace + +int main() { + TestContext Context; + testStateNames(Context); + testSuccessfulAndFailedStatus(Context); + testInvalidFailures(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineConstantPoolTest.cpp b/backend/unittests/CodeGen/MachineConstantPoolTest.cpp new file mode 100644 index 0000000..39323d5 --- /dev/null +++ b/backend/unittests/CodeGen/MachineConstantPoolTest.cpp @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineConstantPool.h" + +#include <bit> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <optional> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +struct PoolTargetConstant final { + std::uint64_t Value = 0; + std::string Symbol; + + [[nodiscard]] static constexpr rcc::TargetConstantTypeID stableTypeID() noexcept { + return rcc::TargetConstantTypeID::fromValue(0x5253434350430001ULL); + } + + [[nodiscard]] static constexpr std::uint32_t schemaVersion() noexcept { + return 1; + } + + [[nodiscard]] rcc::Expected<void> verify() const { + if (Value == 0) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "pool target constant value must be nonzero"); + } + return {}; + } + + [[nodiscard]] rcc::Expected<PoolTargetConstant> clone() const { + return PoolTargetConstant{Value, Symbol}; + } + + [[nodiscard]] std::uint64_t stableHash() const noexcept { + std::uint64_t Hash = Value; + for (const char Character : Symbol) + Hash = (Hash * 16777619U) ^ static_cast<unsigned char>(Character); + return Hash; + } + + [[nodiscard]] std::string print() const { + return Symbol.empty() ? std::to_string(Value) : Symbol + "+" + std::to_string(Value); + } + + [[nodiscard]] std::vector<std::byte> bytes() const { + std::vector<std::byte> Bytes(8); + for (std::size_t Index = 0; Index < Bytes.size(); ++Index) + Bytes[Index] = static_cast<std::byte>((Value >> (Index * 8U)) & 0xffU); + return Bytes; + } + + [[nodiscard]] rcc::Expected<std::vector<std::byte>> encode() const { + std::vector<std::byte> Payload = bytes(); + for (const char Character : Symbol) + Payload.push_back(static_cast<std::byte>(static_cast<unsigned char>(Character))); + return Payload; + } + + [[nodiscard]] rcc::Expected<rcc::TargetConstantEmission> emit() const { + rcc::TargetConstantEmission Emission; + Emission.Bytes = bytes(); + if (!Symbol.empty()) { + Emission.Fixups.push_back({0, rcc::TargetRelocationKindID::fromValue(3), Symbol, + std::bit_cast<std::int64_t>(Value)}); + } + return Emission; + } + + [[nodiscard]] rcc::Alignment requiredAlignment() const noexcept { + return *rcc::Alignment::create(8); + } + + [[nodiscard]] std::optional<rcc::TargetRelocationKindID> relocationKind() const noexcept { + if (Symbol.empty()) + return std::nullopt; + return rcc::TargetRelocationKindID::fromValue(3); + } + + [[nodiscard]] static rcc::Expected<PoolTargetConstant> parse(std::string_view Text) { + if (Text.empty()) + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, "pool constant text is empty"); + return PoolTargetConstant{1, std::string(Text)}; + } + + [[nodiscard]] static rcc::Expected<PoolTargetConstant> + decode(std::uint32_t Version, std::span<const std::byte> Payload) { + if (Version != schemaVersion() || Payload.size() < 8) { + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, + "pool target constant encoding is invalid"); + } + std::uint64_t Value = 0; + for (std::size_t Index = 0; Index < 8; ++Index) + Value |= std::to_integer<std::uint64_t>(Payload[Index]) << (Index * 8U); + std::string Symbol; + for (const std::byte Character : Payload.subspan(8)) + Symbol.push_back(static_cast<char>(std::to_integer<unsigned char>(Character))); + return PoolTargetConstant{Value, std::move(Symbol)}; + } + + friend bool operator==(const PoolTargetConstant &Left, const PoolTargetConstant &Right) = default; +}; + +[[nodiscard]] rcc::MachineConstantPoolEntryDesc +genericDescription(rcc::ConstantPoolMergeKind Merge) { + rcc::MachineConstantPoolEntryDesc Description; + auto Integer = rcc::APInt::fromUnsigned(32, 0x04030201U); + Description.Value = std::move(*rcc::GenericConstantData::integer(std::move(*Integer))); + Description.Size = 4; + Description.Align = *rcc::Alignment::create(4); + Description.Merge = Merge; + if (Merge == rcc::ConstantPoolMergeKind::Exact) + Description.Placement.Section = rcc::ConstantPoolSectionKind::MergeableReadOnly; + return Description; +} + +[[nodiscard]] rcc::MachineConstantPoolEntryDesc targetDescription(bool Expression) { + rcc::MachineConstantPoolEntryDesc Description; + auto Constant = rcc::TargetConstant::create( + PoolTargetConstant{Expression ? 17U : 9U, Expression ? "symbol" : ""}); + if (Expression) + Description.Value = rcc::TargetExpressionConstant{std::move(*Constant)}; + else + Description.Value = std::move(*Constant); + Description.Size = 8; + Description.Align = *rcc::Alignment::create(8); + Description.Relocation = + Expression ? rcc::ConstantPoolRelocationMode::Target : rcc::ConstantPoolRelocationMode::None; + return Description; +} + +[[nodiscard]] rcc::MachineConstantPoolEntryDesc symbolDifferenceDescription() { + rcc::MachineConstantPoolEntryDesc Description; + Description.Value = rcc::SymbolDifferenceConstant{"upper", "lower", 4}; + Description.Size = 4; + Description.Align = *rcc::Alignment::create(4); + Description.Relocation = rcc::ConstantPoolRelocationMode::PositionIndependent; + return Description; +} + +[[nodiscard]] rcc::MachineConstantPoolEntryDesc blockAddressDescription() { + rcc::MachineConstantPoolEntryDesc Description; + Description.Value = rcc::BlockAddressConstant{"function", "block", 0}; + Description.Size = 8; + Description.Align = *rcc::Alignment::create(8); + Description.Relocation = rcc::ConstantPoolRelocationMode::Static; + return Description; +} + +void testPoolLifecycle(TestContext &Context) { + rcc::MachineConstantPool Pool; + + auto MergedA = Pool.addEntry(genericDescription(rcc::ConstantPoolMergeKind::Exact)); + auto MergedB = Pool.addEntry(genericDescription(rcc::ConstantPoolMergeKind::Exact)); + auto Unmerged = Pool.addEntry(genericDescription(rcc::ConstantPoolMergeKind::Never)); + Context.expect(MergedA && MergedB && Unmerged && *MergedA == *MergedB && MergedA->value() == 0 && + Unmerged->value() == 1 && Pool.entries().size() == 2 && + Pool.liveEntryCount() == 2, + "exact constants must merge while nonmergeable constants retain identity"); + if (!MergedA || !Unmerged) + return; + + Context.expect( + !Pool.updateEntry(*Unmerged, genericDescription(rcc::ConstantPoolMergeKind::Exact)), + "pool update must reject duplicate mergeable entries"); + Context.expect(Pool.eraseEntry(*Unmerged).has_value() && Pool.entry(*Unmerged) == nullptr, + "constant pool erase must leave a tombstone"); + + auto Target = Pool.addEntry(targetDescription(false)); + auto Difference = Pool.addEntry(symbolDifferenceDescription()); + auto Address = Pool.addEntry(blockAddressDescription()); + auto Expression = Pool.addEntry(targetDescription(true)); + Context.expect(Target && Difference && Address && Expression && Target->value() == 2 && + Expression->value() == 5 && Pool.liveEntryCount() == 5, + "all constant pool value kinds must receive monotonic indices"); + if (!Target || !Expression) + return; + + const rcc::MachineConstantPoolEntry *TargetEntry = Pool.entry(*Target); + auto ClonedDescription = + TargetEntry == nullptr ? rcc::Expected<rcc::MachineConstantPoolEntryDesc>(rcc::makeUnexpected( + rcc::ErrorCode::InternalError, "missing target entry")) + : TargetEntry->cloneDescription(); + Context.expect(ClonedDescription && + TargetEntry->description()->semanticallyEquals(*ClonedDescription), + "target constant pool descriptions must deep-clone"); + + Context.expect(Pool.verify().has_value() && Pool.freeze().has_value() && Pool.isFrozen(), + "valid constant pool must freeze after verification"); + Context.expect(!Pool.addEntry(blockAddressDescription()) && !Pool.eraseEntry(*Target) && + !Pool.updateEntry(*Expression, targetDescription(true)) && !Pool.freeze(), + "frozen constant pool must reject every mutation"); +} + +void testInvalidEntries(TestContext &Context) { + rcc::MachineConstantPool Pool; + + auto SizeMismatch = genericDescription(rcc::ConstantPoolMergeKind::Never); + SizeMismatch.Size = 8; + Context.expect(!Pool.addEntry(std::move(SizeMismatch)), + "generic constant byte size must match its entry size"); + + auto RelocatingBytes = genericDescription(rcc::ConstantPoolMergeKind::Never); + RelocatingBytes.Relocation = rcc::ConstantPoolRelocationMode::Static; + Context.expect(!Pool.addEntry(std::move(RelocatingBytes)), + "raw generic bytes must reject implicit relocation semantics"); + + auto WeakTargetAlignment = targetDescription(false); + WeakTargetAlignment.Align = *rcc::Alignment::create(4); + Context.expect(!Pool.addEntry(std::move(WeakTargetAlignment)), + "target constant must enforce its required alignment"); + + auto MissingTargetRelocation = targetDescription(true); + MissingTargetRelocation.Relocation = rcc::ConstantPoolRelocationMode::None; + Context.expect(!Pool.addEntry(std::move(MissingTargetRelocation)), + "target expression must agree with its relocation contract"); + + auto InvalidPlacement = blockAddressDescription(); + InvalidPlacement.Placement.Section = rcc::ConstantPoolSectionKind::Target; + Context.expect(!Pool.addEntry(std::move(InvalidPlacement)), + "target placement must carry target flags"); + + auto ZeroDistance = blockAddressDescription(); + ZeroDistance.Placement.MaximumDistance = 0; + Context.expect(!Pool.addEntry(std::move(ZeroDistance)), + "constant pool maximum distance must be nonzero"); +} + +} // namespace + +int main() { + TestContext Context; + testPoolLifecycle(Context); + testInvalidEntries(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineDebugInfoTest.cpp b/backend/unittests/CodeGen/MachineDebugInfoTest.cpp new file mode 100644 index 0000000..ae4d539 --- /dev/null +++ b/backend/unittests/CodeGen/MachineDebugInfoTest.cpp @@ -0,0 +1,518 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineDebugInfo.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineFrameInfo.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineFunctionAttributes.h" +#include "rcc/CodeGen/MachineFunctionCloner.h" +#include "rcc/CodeGen/MachineFunctionRecords.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineVerifier.h" +#include "rcc/CodeGen/TargetDescription.h" + +#include <cstdint> +#include <iostream> +#include <limits> +#include <memory> +#include <optional> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t ValueOpcode = 1024; +constexpr std::uint32_t MetaOpcode = 1025; + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + rcc::InstructionDescriptor Value; + Value.InstructionOpcode = rcc::Opcode::fromValue(ValueOpcode); + Value.Name = "TEST_VALUE"; + Value.Flags = {rcc::InstructionFlag::ProducesCode}; + rcc::OperandDescriptor OptionalMetadata; + OptionalMetadata.Kinds = {rcc::MachineOperand::Kind::Metadata}; + OptionalMetadata.Optional = true; + Value.Operands.push_back(OptionalMetadata); + + rcc::InstructionDescriptor Meta; + Meta.InstructionOpcode = rcc::Opcode::fromValue(MetaOpcode); + Meta.Name = "TEST_META"; + Meta.Flags = {rcc::InstructionFlag::Meta}; + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Value)); + Descriptors.push_back(std::move(Meta)); + std::vector<rcc::ImmediatePredicate> Predicates; + auto Generic = rcc::appendGenericInstructionInfo(Descriptors, Predicates); + if (!Generic) + return std::unexpected(Generic.error()); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors), std::move(Predicates)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> +createFunction(TestContext &Context, std::uint64_t StableID, std::string Name) { + auto Function = rcc::MachineFunction::create(std::move(Name), + rcc::MachineFunctionStableID::fromValue(StableID)); + Context.expect(Function.has_value(), "machine function creation must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineBasicBlock *appendBlock(TestContext &Context, + rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + Context.expect(Block.has_value() && Function.appendBlock(**Block).has_value(), + "machine basic block creation and linking must succeed"); + return Block ? *Block : nullptr; +} + +[[nodiscard]] rcc::MachineInstr *appendInstruction(TestContext &Context, + rcc::MachineFunction &Function, + rcc::MachineBasicBlock &Block, + std::uint32_t OpcodeValue) { + auto Instruction = Function.createInstruction(rcc::Opcode::fromValue(OpcodeValue)); + Context.expect(Instruction.has_value() && Block.appendInstruction(**Instruction).has_value(), + "machine instruction creation and linking must succeed"); + return Instruction ? *Instruction : nullptr; +} + +void testMetadataValuesAndLocations(TestContext &Context) { + rcc::MachineMetadataInfo Metadata; + Context.expect(Metadata.addNode({.Slot = 0, .IsDistinct = false, .Elements = {}}).has_value(), + "function-local debug metadata setup must succeed"); + + const rcc::MachineDIExpression Expression( + std::vector<std::uint64_t>{0x1005, 0, 0x22, 0x1000, 0, 64}); + Context.expect(Expression.elements().size() == 6 && Expression.elements()[0] == 0x1005, + "inline DIExpression elements must preserve their exact unsigned encoding"); + auto ExpressionValue = rcc::MachineMetadataValue::expression(Expression); + Context.expect(ExpressionValue.verify(Metadata).has_value() && + ExpressionValue.expression() != nullptr && + *ExpressionValue.expression() == Expression, + "inline DIExpression metadata values must remain structured"); + Context.expect(!rcc::MachineMetadataValue{}.verify(Metadata), + "an unrepresented metadata value must be rejected"); + + Context.expect(!rcc::MachineDILocation::create({}), + "an inline DILocation must contain a root frame"); + Context.expect(!rcc::MachineDILocation::create({{}}), + "an inline DILocation frame must contain a valid scope"); + Context.expect(!rcc::MachineDILocation::create({ + { + .Line = 1, + .Column = 0, + .Scope = rcc::MachineMetadataReference::embeddedIR(1), + .IsImplicitCode = false, + .AtomGroup = 0, + .AtomRank = 8, + }, + }), + "an inline DILocation atom rank must fit the fixed three-bit field"); + Context.expect(!rcc::MachineDILocation::create({ + { + .Line = 1, + .Column = 0, + .Scope = rcc::MachineMetadataReference::embeddedIR(1), + .IsImplicitCode = false, + .AtomGroup = rcc::MachineDILocation::MaxAtomGroup + 1, + .AtomRank = 0, + }, + }), + "an inline DILocation atom group must fit the fixed 61-bit field"); + + auto InlineLocation = rcc::MachineDILocation::create( + { + { + .Line = 7, + .Column = 65'535, + .Scope = rcc::MachineMetadataReference::embeddedIR(3), + .IsImplicitCode = true, + .AtomGroup = rcc::MachineDILocation::MaxAtomGroup, + .AtomRank = 7, + }, + { + .Line = 9, + .Column = 4, + .Scope = rcc::MachineMetadataReference::embeddedIR(0), + .IsImplicitCode = false, + .AtomGroup = 0, + .AtomRank = 0, + }, + }, + rcc::MachineMetadataReference::embeddedIR(4)); + Context.expect(InlineLocation.has_value() && InlineLocation->verify(Metadata).has_value() && + InlineLocation->inlineChain().size() == 2 && + InlineLocation->terminalInlinedAt() == + rcc::MachineMetadataReference::embeddedIR(4), + "inline DILocation chains must preserve nested and terminal references"); + if (!InlineLocation) + return; + + auto DebugLocation = rcc::MachineDebugLocation::inlineLocation(*InlineLocation); + Context.expect(DebugLocation.verify(Metadata).has_value() && + DebugLocation.inlineLocation() != nullptr, + "a structured inline DILocation must form a debug-location attachment"); + rcc::MachineVariableDebugInfo VariableInfo{ + .Variable = rcc::MachineMetadataReference::embeddedIR(5), + .Expression = ExpressionValue, + .Location = DebugLocation, + }; + Context.expect(VariableInfo.verify(Metadata).has_value() && + !rcc::MachineVariableDebugInfo{}.verify(Metadata), + "variable debug records must carry at least one verified metadata component"); + + auto Undefined = + rcc::MachineDebugLocation::reference(rcc::MachineMetadataReference::machineFunction(99)); + Context.expect(!Undefined, "typed debug locations must not accept function-local tuple metadata"); +} + +void testInstructionMetadataAndClone(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 20, "debug-metadata"); + if (!Target || Function == nullptr) + return; + Context.expect( + Function->metadata().addNode({.Slot = 0, .IsDistinct = false, .Elements = {}}).has_value(), + "instruction metadata graph setup must succeed"); + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + rcc::MachineInstr *Instruction = appendInstruction(Context, *Function, *Block, ValueOpcode); + if (Instruction == nullptr) + return; + + auto MetadataValue = + rcc::MachineMetadataValue::reference(rcc::MachineMetadataReference::machineFunction(0)); + Context.expect(MetadataValue.has_value(), "instruction metadata reference must be valid"); + if (!MetadataValue) + return; + auto Operand = rcc::MachineOperand::metadata(std::move(*MetadataValue)); + auto InlineLocation = rcc::MachineDILocation::create({ + { + .Line = 12, + .Column = 8, + .Scope = rcc::MachineMetadataReference::embeddedIR(7), + .IsImplicitCode = false, + .AtomGroup = 0, + .AtomRank = 0, + }, + }); + Context.expect(Operand.has_value() && InlineLocation.has_value(), + "instruction metadata values must be constructible"); + if (!Operand || !InlineLocation) + return; + const rcc::MachineDebugLocation DebugLocation = + rcc::MachineDebugLocation::inlineLocation(*InlineLocation); + Context.expect(Instruction->addOperand(std::move(*Operand)).has_value() && + Instruction->setDebugLocation(DebugLocation).has_value(), + "instruction metadata operand and debug location must attach independently"); + Instruction->setSourceLocation({"debug-input.mir", 3, 5, 0}); + const rcc::MachineVariableDebugInfo StackDebugInfo{ + .Variable = rcc::MachineMetadataReference::embeddedIR(8), + .Expression = rcc::MachineMetadataValue::expression( + rcc::MachineDIExpression(std::vector<std::uint64_t>{0x1000, 0, 64})), + .Location = DebugLocation, + }; + rcc::StackObjectDesc StackDescription; + StackDescription.Size = 8; + StackDescription.Align = *rcc::Alignment::create(8); + StackDescription.DebugInfo = StackDebugInfo; + auto StackIndex = Function->frameInfo().createObject(std::move(StackDescription)); + Context.expect(StackIndex.has_value(), + "structured variable debug information must attach to a stack object"); + if (!StackIndex) + return; + Context.expect(rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "well-formed instruction metadata must pass unified verification"); + + auto Clone = + rcc::cloneMachineFunction(*Function, *Target, + { + .NewStableID = rcc::MachineFunctionStableID::fromValue(21), + .NewName = std::string("debug-metadata.clone"), + }); + Context.expect(Clone.has_value(), "instruction debug metadata must support deep cloning"); + if (!Clone) + return; + const rcc::MachineInstr *ClonedInstruction = + Clone->ClonedFunction->firstBlock()->firstInstruction(); + const auto ClonedStackIndex = Clone->FrameIndices.find(*StackIndex); + const rcc::StackObject *ClonedStack = + ClonedStackIndex == Clone->FrameIndices.end() + ? nullptr + : Clone->ClonedFunction->frameInfo().object(ClonedStackIndex->second); + Context.expect( + ClonedInstruction->debugLocation() == DebugLocation && + ClonedInstruction->sourceLocation() == rcc::SourceLocation{"debug-input.mir", 3, 5, 0} && + ClonedInstruction->operandAt(0)->getIf<rcc::MetadataOperand>()->Value.reference() != + nullptr && + ClonedStack != nullptr && ClonedStack->description() != nullptr && + ClonedStack->description()->DebugInfo == StackDebugInfo, + "clone must preserve distinct parser source and semantic debug information"); + + auto InvalidFunction = createFunction(Context, 22, "invalid-debug-metadata"); + if (InvalidFunction == nullptr) + return; + rcc::MachineBasicBlock *InvalidBlock = appendBlock(Context, *InvalidFunction); + if (InvalidBlock == nullptr) + return; + rcc::MachineInstr *InvalidInstruction = + appendInstruction(Context, *InvalidFunction, *InvalidBlock, ValueOpcode); + if (InvalidInstruction == nullptr) + return; + auto InvalidValue = + rcc::MachineMetadataValue::reference(rcc::MachineMetadataReference::machineFunction(9)); + Context.expect(InvalidValue.has_value(), "undefined scoped references remain representable"); + if (!InvalidValue) + return; + auto InvalidOperand = rcc::MachineOperand::metadata(std::move(*InvalidValue)); + Context.expect(InvalidOperand.has_value() && + InvalidInstruction->addOperand(std::move(*InvalidOperand)).has_value() && + !rcc::MachineVerifier::verify(*InvalidFunction, *Target), + "undefined instruction metadata references must fail unified verification"); + + auto InvalidFrameFunction = createFunction(Context, 23, "invalid-frame-debug-metadata"); + if (InvalidFrameFunction == nullptr) + return; + rcc::StackObjectDesc InvalidStackDescription; + InvalidStackDescription.Size = 8; + InvalidStackDescription.Align = *rcc::Alignment::create(8); + InvalidStackDescription.DebugInfo = rcc::MachineVariableDebugInfo{ + .Variable = rcc::MachineMetadataReference::machineFunction(9), + .Expression = std::nullopt, + .Location = std::nullopt, + }; + Context.expect( + !InvalidFrameFunction->frameInfo().createObject(std::move(InvalidStackDescription)), + "typed stack debug information must reject function-local tuple metadata"); +} + +void testInstructionNumbering(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 1, "debug-numbering"); + if (!Target || Function == nullptr) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + rcc::MachineInstr *First = appendInstruction(Context, *Function, *Block, ValueOpcode); + rcc::MachineInstr *Second = appendInstruction(Context, *Function, *Block, ValueOpcode); + rcc::MachineInstr *Third = appendInstruction(Context, *Function, *Block, ValueOpcode); + if (First == nullptr || Second == nullptr || Third == nullptr) + return; + + Context.expect(!First->setDebugInstrNumber(0) && + !First->setDebugInstrNumber( + static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max()) + 1), + "debug instruction numbers must fit in nonzero uint32"); + Context.expect(First->setDebugInstrNumber(7).has_value(), + "an explicit debug instruction number must be accepted"); + auto SecondNumber = Second->assignDebugInstrNumber(); + auto ExistingSecondNumber = Second->assignDebugInstrNumber(); + Context.expect(SecondNumber.has_value() && *SecondNumber == 8 && + ExistingSecondNumber.has_value() && *ExistingSecondNumber == *SecondNumber, + "generated debug numbers must continue after the largest observed number"); + Second->clearDebugInstrNumber(); + auto ReassignedSecondNumber = Second->assignDebugInstrNumber(); + Context.expect(ReassignedSecondNumber.has_value() && *ReassignedSecondNumber == 9, + "cleared debug numbers must not rewind the monotonic allocator"); + Context.expect(Third->setDebugInstrNumber(7).has_value() && + !rcc::MachineVerifier::verify(*Function, *Target), + "the unified verifier must reject duplicate live debug numbers"); + Context.expect(!Third->setDebugInstrNumber(10), + "an existing debug identity must be cleared before replacement"); + Third->clearDebugInstrNumber(); + Context.expect(Third->setDebugInstrNumber(10).has_value() && + rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "unique debug numbers must pass unified verification"); +} + +void testSubstitutionsAndClone(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 2, "debug-substitutions"); + if (!Target || Function == nullptr) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + rcc::MachineInstr *Instruction = appendInstruction(Context, *Function, *Block, ValueOpcode); + if (Instruction == nullptr) + return; + Context.expect(Instruction->setDebugInstrNumber(7).has_value(), + "tracked instruction numbering must succeed"); + + const rcc::MachineDebugValueSubstitution Substitution{ + .Source = {.InstructionNumber = 1, .OperandNumber = 0}, + .Destination = {.InstructionNumber = 7, .OperandNumber = 1}, + .Subregister = rcc::test::TestSubReg::Low32, + }; + Context.expect(Function->records().addDebugValueSubstitution(Substitution).has_value(), + "a structured debug value substitution must be stored"); + Context.expect(!Function->records().addDebugValueSubstitution(Substitution), + "a debug value source must have at most one replacement"); + Context.expect(!Function->records().addDebugValueSubstitution({ + .Source = + { + .InstructionNumber = 2, + .OperandNumber = rcc::MachineDebugMemoryOperandNumber, + }, + .Destination = {.InstructionNumber = 7, .OperandNumber = 0}, + .Subregister = {}, + }), + "a substitution cannot originate from the reserved memory operand"); + Context.expect(!Function->records().addDebugValueSubstitution({ + .Source = {.InstructionNumber = 3, .OperandNumber = 0}, + .Destination = {.InstructionNumber = 3, .OperandNumber = 1}, + .Subregister = {}, + }), + "a substitution cannot replace a value within one instruction"); + + Context.expect(!rcc::MachineVerifier::verify(*Function, *Target), + "substitutions must require the DebugInstrRef function policy"); + Function->attributes().set(rcc::MachineFunctionAttribute::DebugInstrRef); + Context.expect(rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "well-formed substitutions must pass unified verification"); + + auto Clone = + rcc::cloneMachineFunction(*Function, *Target, + { + .NewStableID = rcc::MachineFunctionStableID::fromValue(3), + .NewName = std::string("debug-substitutions.clone"), + }); + Context.expect(Clone.has_value(), "tracked debug state must support explicit deep cloning"); + if (!Clone) + return; + const rcc::MachineBasicBlock *ClonedBlock = Clone->ClonedFunction->firstBlock(); + const rcc::MachineInstr *ClonedInstruction = + ClonedBlock == nullptr ? nullptr : ClonedBlock->firstInstruction(); + Context.expect(ClonedInstruction != nullptr && ClonedInstruction->debugInstrNumber() == 7 && + Clone->ClonedFunction->records().debugValueSubstitutions().size() == 1 && + Clone->ClonedFunction->records().debugValueSubstitutions()[0] == Substitution, + "deep clone must preserve debug numbers and substitution identities"); +} + +void testVerifierFailures(TestContext &Context) { + auto Target = createTarget(); + if (!Target) + return; + + { + auto Function = createFunction(Context, 9, "missing-debug-location"); + if (Function == nullptr) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + auto Instruction = Function->createInstruction(rcc::opcode(rcc::GenericOpcode::DBG_VALUE)); + if (!Instruction) + return; + for (const std::int64_t Value : {1, 2, 3, 4}) { + if (!(*Instruction)->addOperand(rcc::MachineOperand::immediate(Value))) + return; + } + Context.expect(Block->appendInstruction(**Instruction).has_value(), + "four-operand DBG_VALUE must be linkable for verifier coverage"); + auto Verification = rcc::MachineVerifier::verify(*Function, *Target); + Context.expect(!Verification && + Verification.error().message().find( + "Bad machine code: Missing DebugLoc for debug instruction\n" + "- instruction: DBG_VALUE 1, 2, 3, 4") != std::string_view::npos, + "complete DBG_VALUE must require a DebugLoc with LLVM-compatible diagnostics"); + } + + { + auto Function = createFunction(Context, 10, "debug-meta"); + if (Function == nullptr) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + rcc::MachineInstr *Meta = appendInstruction(Context, *Function, *Block, MetaOpcode); + if (Meta == nullptr) + return; + Context.expect(Meta->setDebugInstrNumber(1).has_value() && + !rcc::MachineVerifier::verify(*Function, *Target), + "metadata instructions must not carry debug value tracking numbers"); + } + + { + auto Function = createFunction(Context, 11, "debug-subreg"); + if (Function == nullptr) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + rcc::MachineInstr *Instruction = appendInstruction(Context, *Function, *Block, ValueOpcode); + if (Instruction == nullptr) + return; + Function->attributes().set(rcc::MachineFunctionAttribute::DebugInstrRef); + Context.expect(Function->records() + .addDebugValueSubstitution({ + .Source = {.InstructionNumber = 1, .OperandNumber = 0}, + .Destination = {.InstructionNumber = 2, .OperandNumber = 0}, + .Subregister = rcc::SubRegIndex::fromValue(999), + }) + .has_value() && + !rcc::MachineVerifier::verify(*Function, *Target), + "debug substitutions must reject target-unknown subregister indices"); + } + + { + rcc::MachineFunctionRecords Records; + Context.expect(Records + .addDebugValueSubstitution({ + .Source = {.InstructionNumber = 1, .OperandNumber = 0}, + .Destination = {.InstructionNumber = 2, .OperandNumber = 0}, + .Subregister = {}, + }) + .has_value() && + Records + .addDebugValueSubstitution({ + .Source = {.InstructionNumber = 2, .OperandNumber = 0}, + .Destination = {.InstructionNumber = 1, .OperandNumber = 0}, + .Subregister = {}, + }) + .has_value() && + !Records.verify(), + "debug substitution chains must be acyclic"); + } +} + +} // namespace + +int main() { + TestContext Context; + testMetadataValuesAndLocations(Context); + testInstructionMetadataAndClone(Context); + testInstructionNumbering(Context); + testSubstitutionsAndClone(Context); + testVerifierFailures(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineFrameInfoTest.cpp b/backend/unittests/CodeGen/MachineFrameInfoTest.cpp new file mode 100644 index 0000000..b0464a6 --- /dev/null +++ b/backend/unittests/CodeGen/MachineFrameInfoTest.cpp @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineFrameInfo.h" + +#include "rcc/CodeGen/MachineDebugInfo.h" + +#include <cstdint> +#include <iostream> +#include <optional> +#include <string_view> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::StackObjectDesc fixedObject(std::uint64_t Size, std::uint64_t Align) { + rcc::StackObjectDesc Description; + Description.Size = Size; + Description.Align = *rcc::Alignment::create(Align); + return Description; +} + +void testObjectLifecycle(TestContext &Context) { + rcc::MachineFrameInfo Frame; + + auto LocalDescription = fixedObject(8, 8); + LocalDescription.IsAliased = true; + LocalDescription.AddressTaken = true; + LocalDescription.Lifetime.push_back( + {rcc::MachineInstrStableID::fromValue(3), rcc::MachineInstrStableID::fromValue(7)}); + auto Local = Frame.createObject(LocalDescription); + + auto FixedDescription = fixedObject(16, 16); + FixedDescription.Kind = rcc::StackObjectKind::FixedABI; + FixedDescription.Offset = -32; + auto Fixed = Frame.createObject(FixedDescription); + + auto SpillDescription = fixedObject(8, 8); + SpillDescription.Kind = rcc::StackObjectKind::SpillSlot; + auto Spill = Frame.createObject(SpillDescription); + + rcc::StackObjectDesc VariableDescription; + VariableDescription.Kind = rcc::StackObjectKind::VariableSized; + VariableDescription.Align = *rcc::Alignment::create(32); + VariableDescription.Offset = 0; + VariableDescription.LocalOffset = 0; + auto Variable = Frame.createObject(VariableDescription); + + Context.expect(Local && Fixed && Spill && Variable, "valid stack object kinds must be created"); + if (!Local || !Fixed || !Spill || !Variable) + return; + + Context.expect(Local->value() == 0 && Fixed->value() == 1 && Spill->value() == 2 && + Variable->value() == 3 && Frame.liveObjectCount() == 4, + "frame indices must be monotonic table identities"); + Context.expect(Frame.object(*Fixed)->isFixed() && Frame.object(*Spill)->isSpill() && + Frame.object(*Variable)->isVariableSized(), + "stack object semantic categories must be derived exactly"); + + Context.expect(Frame.setObjectOffset(*Local, -8).has_value() && + Frame.setObjectOffset(*Spill, -16).has_value() && + Frame.setObjectOffset(*Variable, -64).has_value() && + Frame.markObjectDead(*Spill).has_value(), + "live stack objects must support controlled pre-freeze updates"); + Context.expect(Frame.object(*Spill)->description()->IsDead, + "dead stack object state must remain explicit"); + Context.expect(Frame.object(*Variable)->description()->Offset == -64 && + Frame.object(*Variable)->description()->LocalOffset == 0, + "variable-sized stack objects must preserve LLVM MIR frame offsets"); + + Context.expect(Frame.eraseObject(*Fixed).has_value() && Frame.object(*Fixed) == nullptr && + Frame.liveObjectCount() == 3, + "erasing a stack object must leave an inaccessible tombstone"); + auto Replacement = Frame.createObject(fixedObject(4, 4)); + Context.expect(Replacement && Replacement->value() == 4, + "erased frame indices must never be reused"); + if (!Replacement) + return; + Context.expect(Frame.setObjectOffset(*Replacement, -24).has_value(), + "replacement stack object must accept a final offset"); + + Context.expect(Frame.setMaxCallFrameSize(24).has_value() && + Frame.freeze(64, *rcc::Alignment::create(16)).has_value() && + Frame.isFrozen() && Frame.staticStackSize() == 64 && + Frame.stackAlignment().bytes() == 16 && Frame.maxCallFrameSize() == 24 && + Frame.verify().has_value(), + "complete frame layout must freeze atomically"); + + Context.expect(!Frame.createObject(fixedObject(8, 8)) && !Frame.setObjectOffset(*Local, -40) && + !Frame.eraseObject(*Local) && !Frame.setMaxCallFrameSize(32) && + !Frame.freeze(64, *rcc::Alignment::create(16)), + "frozen frames must reject every semantic mutation"); +} + +void testInvalidDescriptions(TestContext &Context) { + rcc::MachineFrameInfo Frame; + + rcc::StackObjectDesc MissingSize; + Context.expect(!Frame.createObject(MissingSize), + "fixed-size stack object must reject a missing size"); + + auto ZeroSize = fixedObject(0, 1); + Context.expect(Frame.createObject(ZeroSize).has_value(), + "fixed-size stack objects must preserve LLVM MIR zero-sized objects"); + + rcc::StackObjectDesc InvalidVariable; + InvalidVariable.Kind = rcc::StackObjectKind::DynamicAlloca; + InvalidVariable.Size = 8; + Context.expect(!Frame.createObject(InvalidVariable), + "variable-sized stack object must reject a fixed size"); + + auto MissingFixedOffset = fixedObject(8, 8); + MissingFixedOffset.Kind = rcc::StackObjectKind::FixedABI; + Context.expect(!Frame.createObject(MissingFixedOffset), + "fixed ABI stack object must require an offset"); + + auto InvalidTarget = fixedObject(8, 8); + InvalidTarget.Kind = rcc::StackObjectKind::Target; + Context.expect(!Frame.createObject(InvalidTarget), + "target stack object must carry target identity"); + + auto InvalidLifetime = fixedObject(8, 8); + InvalidLifetime.Lifetime.push_back( + {rcc::MachineInstrStableID::fromValue(4), rcc::MachineInstrStableID::fromValue(4)}); + Context.expect(!Frame.createObject(InvalidLifetime), + "stack object lifetime must reject equal boundaries"); + + auto InvalidDebugInfo = fixedObject(8, 8); + InvalidDebugInfo.DebugInfo.emplace(); + Context.expect(!Frame.createObject(InvalidDebugInfo), + "stack object debug information must not be structurally empty"); + + auto DebugObject = fixedObject(8, 8); + DebugObject.DebugInfo = rcc::MachineVariableDebugInfo{ + .Variable = rcc::MachineMetadataReference::embeddedIR(1), + .Expression = rcc::MachineMetadataValue::expression(rcc::MachineDIExpression{}), + .Location = std::nullopt, + }; + auto DebugIndex = Frame.createObject(DebugObject); + Context.expect(DebugIndex.has_value() && + Frame.object(*DebugIndex)->description()->DebugInfo == DebugObject.DebugInfo, + "stack object debug information must remain structured"); + + Context.expect( + rcc::StackID::fromValue(0) && rcc::StackID::fromValue(4) && rcc::StackID::fromValue(255) && + !rcc::StackID::fromValue(5) && !rcc::StackID::fromValue(256) && + rcc::parseStackIDName("sgpr-spill") == rcc::StackID::sgprSpillStack() && + rcc::parseStackIDName("scalable-vector") == rcc::StackID::scalableVectorStack() && + rcc::parseStackIDName("scalable-predicate-vector") == + rcc::StackID::scalablePredicateVectorStack() && + rcc::parseStackIDName("wasm-local") == rcc::StackID::wasmLocalStack() && + rcc::parseStackIDName("noalloc") == rcc::StackID::noAllocStack() && + !rcc::parseStackIDName("safe") && + rcc::getStackIDName(rcc::StackID::noAllocStack()) == "noalloc", + "stack IDs and names must match the fixed LLVM MIR enumeration"); + + auto Target = fixedObject(8, 8); + Target.Kind = rcc::StackObjectKind::Target; + Target.Stack = rcc::StackID::sgprSpillStack(); + auto TargetIndex = Frame.createObject(Target); + Context.expect(TargetIndex.has_value(), "special stack identity must admit target stack objects"); + + auto Unresolved = Frame.createObject(fixedObject(8, 8)); + Context.expect(Unresolved && !Frame.freeze(16, *rcc::Alignment::create(8)) && !Frame.isFrozen() && + !Frame.staticStackSize() && Frame.verify().has_value(), + "failed frame freeze must not mutate frame state"); +} + +} // namespace + +int main() { + TestContext Context; + testObjectLifecycle(Context); + testInvalidDescriptions(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineFunctionClonerTest.cpp b/backend/unittests/CodeGen/MachineFunctionClonerTest.cpp new file mode 100644 index 0000000..82bcce1 --- /dev/null +++ b/backend/unittests/CodeGen/MachineFunctionClonerTest.cpp @@ -0,0 +1,846 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineFunctionCloner.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/LowLevelType.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineCFGUpdater.h" +#include "rcc/CodeGen/MachineCompilationStatus.h" +#include "rcc/CodeGen/MachineConstantPool.h" +#include "rcc/CodeGen/MachineFrameInfo.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineFunctionProperties.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineJumpTableInfo.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/MachineVerifier.h" +#include "rcc/CodeGen/TargetFunctionState.h" + +#include <algorithm> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t NopOpcode = 1; +constexpr std::uint32_t DefOpcode = 2; +constexpr std::uint32_t BranchOpcode = 3; +constexpr std::uint32_t CopyOpcode = 4; +constexpr std::uint32_t FrameOpcode = 5; +constexpr std::uint32_t BlockAddressOpcode = 6; +constexpr std::uint32_t LoadOpcode = 7; +constexpr std::uint32_t ConstantPoolOpcode = 8; +constexpr std::uint32_t JumpTableOpcode = 9; + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +void appendLittleEndian(std::vector<std::byte> &Bytes, std::uint64_t Value, std::size_t Width) { + for (std::size_t Index = 0; Index < Width; ++Index) + Bytes.push_back(static_cast<std::byte>((Value >> (Index * 8U)) & 0xffU)); +} + +[[nodiscard]] std::uint64_t readLittleEndian(std::span<const std::byte> Bytes, std::size_t Offset, + std::size_t Width) { + std::uint64_t Value = 0; + for (std::size_t Index = 0; Index < Width; ++Index) { + Value |= std::to_integer<std::uint64_t>(Bytes[Offset + Index]) << (Index * 8U); + } + return Value; +} + +struct CloneTargetState final { + rcc::MachineBlockStableID Block; + rcc::MachineInstrStableID Instruction; + rcc::Register TrackedRegister; + rcc::FrameIndex Frame; + rcc::ConstantPoolIndex Constant; + rcc::JumpTableIndex JumpTable; + + [[nodiscard]] static constexpr rcc::TargetFunctionStateTypeID stableTypeID() noexcept { + return rcc::TargetFunctionStateTypeID::fromValue(0x524343434c4f4e45ULL); + } + + [[nodiscard]] static constexpr std::uint32_t schemaVersion() noexcept { + return 1; + } + + [[nodiscard]] rcc::Expected<void> verify() const { + if (!Block.isValid() || !Instruction.isValid() || TrackedRegister.isNoRegister()) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "clone target state has an invalid identity"); + } + return {}; + } + + [[nodiscard]] rcc::Expected<CloneTargetState> + clone(const rcc::MachineFunctionCloneContext &Context) const { + auto MappedBlock = Context.mapBlockStableID(Block); + if (!MappedBlock) + return std::unexpected(MappedBlock.error()); + auto MappedInstruction = Context.mapInstructionStableID(Instruction); + if (!MappedInstruction) + return std::unexpected(MappedInstruction.error()); + auto MappedRegister = Context.mapRegister(TrackedRegister); + if (!MappedRegister) + return std::unexpected(MappedRegister.error()); + auto MappedFrame = Context.mapFrameIndex(Frame); + if (!MappedFrame) + return std::unexpected(MappedFrame.error()); + auto MappedConstant = Context.mapConstantPoolIndex(Constant); + if (!MappedConstant) + return std::unexpected(MappedConstant.error()); + auto MappedJumpTable = Context.mapJumpTableIndex(JumpTable); + if (!MappedJumpTable) + return std::unexpected(MappedJumpTable.error()); + return CloneTargetState{*MappedBlock, *MappedInstruction, *MappedRegister, + *MappedFrame, *MappedConstant, *MappedJumpTable}; + } + + [[nodiscard]] std::string print() const { + return "clone-target-state"; + } + + [[nodiscard]] rcc::Expected<std::vector<std::byte>> encode() const { + std::vector<std::byte> Payload; + Payload.reserve(32); + appendLittleEndian(Payload, Block.value(), 8); + appendLittleEndian(Payload, Instruction.value(), 8); + appendLittleEndian(Payload, TrackedRegister.opaqueValue(), 4); + appendLittleEndian(Payload, Frame.value(), 4); + appendLittleEndian(Payload, Constant.value(), 4); + appendLittleEndian(Payload, JumpTable.value(), 4); + return Payload; + } + + [[nodiscard]] static rcc::Expected<CloneTargetState> decode(std::uint32_t Version, + std::span<const std::byte> Payload) { + if (Version != schemaVersion() || Payload.size() != 32) { + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, + "clone target state encoding is invalid"); + } + return CloneTargetState{ + rcc::MachineBlockStableID::fromValue(readLittleEndian(Payload, 0, 8)), + rcc::MachineInstrStableID::fromValue(readLittleEndian(Payload, 8, 8)), + rcc::Register(static_cast<std::uint32_t>(readLittleEndian(Payload, 16, 4))), + rcc::FrameIndex::fromValue(static_cast<std::uint32_t>(readLittleEndian(Payload, 20, 4))), + rcc::ConstantPoolIndex::fromValue( + static_cast<std::uint32_t>(readLittleEndian(Payload, 24, 4))), + rcc::JumpTableIndex::fromValue( + static_cast<std::uint32_t>(readLittleEndian(Payload, 28, 4))), + }; + } + + friend bool operator==(const CloneTargetState &Left, const CloneTargetState &Right) = default; +}; + +[[nodiscard]] rcc::OperandDescriptor registerDescriptor(rcc::RegisterOperandRole Role) { + rcc::OperandDescriptor Descriptor; + Descriptor.Kinds = {rcc::MachineOperand::Kind::Register}; + Descriptor.RegisterRole = Role; + return Descriptor; +} + +[[nodiscard]] rcc::InstructionDescriptor instructionDescriptor(std::uint32_t OpcodeValue, + std::string Name) { + rcc::InstructionDescriptor Descriptor; + Descriptor.InstructionOpcode = rcc::Opcode::fromValue(OpcodeValue); + Descriptor.Name = std::move(Name); + Descriptor.Flags = {rcc::InstructionFlag::ProducesCode}; + return Descriptor; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + auto Nop = instructionDescriptor(NopOpcode, "TEST_NOP"); + + auto Def = instructionDescriptor(DefOpcode, "TEST_DEF"); + Def.Operands.push_back(registerDescriptor(rcc::RegisterOperandRole::Def)); + Def.NumExplicitDefs = 1; + + auto Branch = instructionDescriptor(BranchOpcode, "TEST_BRANCH"); + rcc::OperandDescriptor Block; + Block.Kinds = {rcc::MachineOperand::Kind::BasicBlock}; + Branch.Operands.push_back(Block); + Branch.Flags.set(rcc::InstructionFlag::Branch); + Branch.Flags.set(rcc::InstructionFlag::Terminator); + + auto Copy = instructionDescriptor(CopyOpcode, "TEST_COPY"); + auto CopyDef = registerDescriptor(rcc::RegisterOperandRole::Def); + auto CopyUse = registerDescriptor(rcc::RegisterOperandRole::Use); + CopyDef.DefaultTiedOperand = 1; + CopyUse.DefaultTiedOperand = 0; + Copy.Operands.push_back(CopyDef); + Copy.Operands.push_back(CopyUse); + Copy.NumExplicitDefs = 1; + + auto Frame = instructionDescriptor(FrameOpcode, "TEST_FRAME"); + rcc::OperandDescriptor FrameIndex; + FrameIndex.Kinds = {rcc::MachineOperand::Kind::FrameIndex}; + Frame.Operands.push_back(FrameIndex); + + auto BlockAddress = instructionDescriptor(BlockAddressOpcode, "TEST_BLOCK_ADDRESS"); + rcc::OperandDescriptor Address; + Address.Kinds = {rcc::MachineOperand::Kind::BlockAddress}; + BlockAddress.Operands.push_back(Address); + + auto Load = instructionDescriptor(LoadOpcode, "TEST_LOAD"); + Load.Flags.set(rcc::InstructionFlag::MayLoad); + Load.RequiresMemoryOperands = true; + + auto ConstantPool = instructionDescriptor(ConstantPoolOpcode, "TEST_CONSTANT_POOL"); + rcc::OperandDescriptor ConstantPoolIndex; + ConstantPoolIndex.Kinds = {rcc::MachineOperand::Kind::ConstantPoolIndex}; + ConstantPool.Operands.push_back(ConstantPoolIndex); + + auto JumpTable = instructionDescriptor(JumpTableOpcode, "TEST_JUMP_TABLE"); + rcc::OperandDescriptor JumpTableIndex; + JumpTableIndex.Kinds = {rcc::MachineOperand::Kind::JumpTableIndex}; + JumpTable.Operands.push_back(JumpTableIndex); + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Nop)); + Descriptors.push_back(std::move(Def)); + Descriptors.push_back(std::move(Branch)); + Descriptors.push_back(std::move(Copy)); + Descriptors.push_back(std::move(Frame)); + Descriptors.push_back(std::move(BlockAddress)); + Descriptors.push_back(std::move(Load)); + Descriptors.push_back(std::move(ConstantPool)); + Descriptors.push_back(std::move(JumpTable)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> +createFunction(TestContext &Context, std::string Name, std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "machine function creation must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineBasicBlock *createBlock(TestContext &Context, + rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + Context.expect(Block.has_value(), "machine basic block creation must succeed"); + if (!Block) + return nullptr; + Context.expect(Function.appendBlock(**Block).has_value(), + "machine basic block linking must succeed"); + return *Block; +} + +[[nodiscard]] rcc::MachineMemOperandDesc loadDescription(rcc::MemoryBase Base) { + rcc::MachineMemOperandDesc Description; + Description.Accesses = {rcc::MemoryAccessFlag::Load, rcc::MemoryAccessFlag::Dereferenceable}; + Description.Size = *rcc::MemorySize::fixed(8); + Description.Align = *rcc::Alignment::create(8); + Description.BaseAlign = *rcc::Alignment::create(16); + Description.Base = std::move(Base); + Description.Offset = -8; + Description.TBAA = rcc::MachineMetadataReference::embeddedIR(10); + Description.AliasScope = rcc::MachineMetadataReference::embeddedIR(11); + Description.NoAlias = rcc::MachineMetadataReference::embeddedIR(12); + Description.NoAliasAddressSpace = rcc::MachineMetadataReference::embeddedIR(13); + Description.RangeMetadata = rcc::MachineMetadataReference::embeddedIR(14); + Description.TargetFlags = 0x3; + return Description; +} + +[[nodiscard]] rcc::MachineInstr *findInstruction(rcc::MachineBasicBlock &Block, + std::uint32_t OpcodeValue) { + for (rcc::MachineInstr *Instruction = Block.firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + if (Instruction->opcode() == rcc::Opcode::fromValue(OpcodeValue)) + return Instruction; + } + return nullptr; +} + +struct SourceFixture final { + rcc::MachineBasicBlock *Entry = nullptr; + rcc::MachineBasicBlock *Exit = nullptr; + rcc::Register Value; + rcc::Register CopyValue; + rcc::FrameIndex StackSlot; + rcc::ConstantPoolIndex Constant; + rcc::JumpTableIndex JumpTable; + std::vector<const rcc::MachineMemOperand *> LoadMMOs; +}; + +[[nodiscard]] bool populateSource(TestContext &Context, rcc::MachineFunction &Function, + const rcc::TargetDescription &Target, SourceFixture &Fixture) { + auto HistoricalBlock = Function.createBasicBlock(); + auto HistoricalInstruction = Function.createInstruction(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(HistoricalBlock && HistoricalInstruction, "source arena history must be created"); + + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Tombstone = Function.registerInfo().createVirtualRegister(*Type); + Context.expect(Tombstone && Function.registerInfo().eraseVirtualRegister(*Tombstone).has_value(), + "source vreg history must contain a tombstone"); + + rcc::StackObjectDesc HistoricalFrame; + HistoricalFrame.Size = 4; + HistoricalFrame.Align = *rcc::Alignment::create(4); + auto HistoricalFrameIndex = Function.frameInfo().createObject(HistoricalFrame); + Context.expect(HistoricalFrameIndex && + Function.frameInfo().eraseObject(*HistoricalFrameIndex).has_value(), + "source frame history must contain a tombstone"); + + rcc::StackObjectDesc StackSlot; + StackSlot.Kind = rcc::StackObjectKind::SpillSlot; + StackSlot.Size = 8; + StackSlot.Align = *rcc::Alignment::create(8); + StackSlot.Offset = -16; + auto StackSlotIndex = Function.frameInfo().createObject(StackSlot); + Context.expect(StackSlotIndex.has_value(), "source live stack object must be created"); + if (!StackSlotIndex) + return false; + Fixture.StackSlot = *StackSlotIndex; + + Fixture.Entry = createBlock(Context, Function); + Fixture.Exit = createBlock(Context, Function); + if (Fixture.Entry == nullptr || Fixture.Exit == nullptr) + return false; + + rcc::MachineConstantPoolEntryDesc HistoricalConstant; + auto HistoricalInteger = rcc::APInt::fromUnsigned(32, 0x01010101U); + HistoricalConstant.Value = + std::move(*rcc::GenericConstantData::integer(std::move(*HistoricalInteger))); + HistoricalConstant.Size = 4; + HistoricalConstant.Align = *rcc::Alignment::create(4); + auto HistoricalConstantIndex = Function.constantPool().addEntry(std::move(HistoricalConstant)); + Context.expect(HistoricalConstantIndex && + Function.constantPool().eraseEntry(*HistoricalConstantIndex).has_value(), + "source constant pool history must contain a tombstone"); + + rcc::MachineConstantPoolEntryDesc Constant; + Constant.Value = rcc::BlockAddressConstant{std::string(Function.name()), "exit", 5}; + Constant.Size = 8; + Constant.Align = *rcc::Alignment::create(8); + Constant.Relocation = rcc::ConstantPoolRelocationMode::Static; + auto ConstantIndex = Function.constantPool().addEntry(std::move(Constant)); + Context.expect(ConstantIndex && Function.constantPool().freeze().has_value(), + "source live constant pool entry must be frozen"); + if (!ConstantIndex) + return false; + Fixture.Constant = *ConstantIndex; + + rcc::MachineJumpTableDesc HistoricalJumpTable; + HistoricalJumpTable.EntrySize = 4; + HistoricalJumpTable.Align = *rcc::Alignment::create(4); + HistoricalJumpTable.Targets = {Fixture.Entry}; + auto HistoricalJumpTableIndex = + Function.jumpTableInfo().createTable(std::move(HistoricalJumpTable)); + Context.expect(HistoricalJumpTableIndex && + Function.jumpTableInfo().eraseTable(*HistoricalJumpTableIndex).has_value(), + "source jump table history must contain a tombstone"); + + rcc::MachineJumpTableDesc JumpTable; + JumpTable.Encoding = rcc::JumpTableEntryEncoding::GPRelative32BlockAddress; + JumpTable.EntrySize = 4; + JumpTable.Align = *rcc::Alignment::create(4); + JumpTable.Targets = {Fixture.Exit, Fixture.Entry, Fixture.Exit}; + auto JumpTableIndex = Function.jumpTableInfo().createTable(std::move(JumpTable)); + Context.expect(JumpTableIndex && Function.jumpTableInfo().freeze().has_value(), + "source live jump table must be frozen"); + if (!JumpTableIndex) + return false; + Fixture.JumpTable = *JumpTableIndex; + + auto Value = Function.registerInfo().createVirtualRegister(*Type); + auto CopyValue = Function.registerInfo().createVirtualRegister(*Type); + Context.expect(Value && CopyValue, "source live vregs must be created"); + if (!Value || !CopyValue) + return false; + Fixture.Value = *Value; + Fixture.CopyValue = *CopyValue; + + const auto Bank = rcc::RegisterBankID::fromValue(1); + const auto RegClass = rcc::RegisterClassID::fromValue(2); + for (const rcc::Register Reg : {Fixture.Value, Fixture.CopyValue}) { + Context.expect(Function.registerInfo().setRegisterBank(Reg, Bank).has_value() && + Function.registerInfo().setRegisterClass(Reg, RegClass).has_value() && + Function.registerInfo().setTargetRegisterFlags(Reg, 0x3).has_value(), + "source vreg constraints must be recorded"); + } + + const auto LiveIn = *rcc::PhysicalRegister::create(1); + const auto Reserved = *rcc::PhysicalRegister::create(2); + const auto Hint = *rcc::PhysicalRegister::create(5); + Context.expect(Function.registerInfo().addAllocationHint(Fixture.Value, Hint).has_value() && + Function.registerInfo().addLiveIn(LiveIn, Fixture.Value).has_value() && + Function.registerInfo().reservePhysicalRegister(Reserved).has_value() && + Fixture.Entry->addLiveIn(LiveIn).has_value(), + "source register configuration must be recorded"); + + rcc::MachineIRBuilder Builder(Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Entry).has_value(), + "source entry insertion point must be valid"); + auto Def = Builder.buildInstr(rcc::Opcode::fromValue(DefOpcode)); + Context.expect(Def && Def->addDef(Fixture.Value).has_value(), + "source definition must be assembled"); + if (!Def) + return false; + Context.expect(Def->setFlags({rcc::MachineInstrFlag::NoMerge}).has_value() && + Def->setSourceLocation({"source.mir", 9, 4, 2}).has_value() && + Def->setOrigin(rcc::OriginID::fromValue(77)).has_value() && Def->build(), + "source definition metadata must build"); + + auto Branch = Builder.buildInstr(rcc::Opcode::fromValue(BranchOpcode)); + Context.expect(Branch && Branch->addBasicBlock(*Fixture.Exit).has_value() && Branch->build(), + "source branch must build"); + Context.expect(Function.cfgUpdater() + .addSuccessor(*Fixture.Entry, *Fixture.Exit, rcc::BranchProbability::one()) + .has_value(), + "source CFG edge must build"); + + Context.expect(Builder.setInsertPoint(*Fixture.Exit).has_value(), + "source exit insertion point must be valid"); + auto Copy = Builder.buildInstr(rcc::Opcode::fromValue(CopyOpcode)); + Context.expect(Copy && Copy->addDef(Fixture.CopyValue).has_value() && + Copy->addUse(Fixture.Value).has_value() && Copy->build(), + "source tied copy must build"); + + auto Frame = Builder.buildInstr(rcc::Opcode::fromValue(FrameOpcode)); + Context.expect(Frame && Frame->addFrameIndex(Fixture.StackSlot, -8).has_value() && Frame->build(), + "source frame-index instruction must build"); + + auto ConstantPool = Builder.buildInstr(rcc::Opcode::fromValue(ConstantPoolOpcode)); + Context.expect(ConstantPool && ConstantPool->addConstantPool(Fixture.Constant, 8).has_value() && + ConstantPool->build(), + "source constant-pool instruction must build"); + + auto JumpTableInstruction = Builder.buildInstr(rcc::Opcode::fromValue(JumpTableOpcode)); + Context.expect(JumpTableInstruction && + JumpTableInstruction->addJumpTable(Fixture.JumpTable).has_value() && + JumpTableInstruction->build(), + "source jump-table instruction must build"); + + auto Address = Builder.buildInstr(rcc::Opcode::fromValue(BlockAddressOpcode)); + Context.expect( + Address && Address->addBlockAddress(std::string(Function.name()), "exit", 3).has_value() && + Address->build(), + "source self block-address instruction must build"); + + for (rcc::MemoryBase Base : {rcc::MemoryBase::frameIndex(Fixture.StackSlot), + rcc::MemoryBase::constantPool(Fixture.Constant), + rcc::MemoryBase::jumpTable(Fixture.JumpTable)}) { + auto MMO = Function.createMemOperand(loadDescription(std::move(Base))); + Context.expect(MMO.has_value(), "source MMO must be created"); + if (!MMO) + return false; + Fixture.LoadMMOs.push_back(*MMO); + } + auto Load = Builder.buildInstr(rcc::Opcode::fromValue(LoadOpcode)); + if (!Load) + return false; + bool AddedAllMMOs = true; + for (const rcc::MachineMemOperand *MMO : Fixture.LoadMMOs) + AddedAllMMOs &= Load->addMemOperand(*MMO).has_value(); + Context.expect(AddedAllMMOs && Load->build(), "source load with MMOs must build"); + + rcc::MachineInstr *Definition = findInstruction(*Fixture.Entry, DefOpcode); + rcc::MachineInstr *LoadInstruction = findInstruction(*Fixture.Exit, LoadOpcode); + const rcc::StackObject *Object = Function.frameInfo().object(Fixture.StackSlot); + if (Definition == nullptr || LoadInstruction == nullptr || Object == nullptr) + return false; + Context.expect( + Definition->setPreInstrSymbol("before").has_value() && + Definition->setPostInstrSymbol("after").has_value() && + Definition->setHeapAllocMarker(rcc::MachineMetadataReference::embeddedIR(20)) + .has_value() && + Definition->setPCSections(rcc::MachineMetadataReference::embeddedIR(21)).has_value() && + Definition->setMMRA(rcc::MachineMetadataReference::embeddedIR(22)).has_value() && + Definition->setCFIType(23).has_value() && + Definition->setDeactivationSymbol("guard").has_value(), + "source instruction attachments must be recorded"); + rcc::StackObjectDesc UpdatedStackSlot = *Object->description(); + UpdatedStackSlot.Lifetime.push_back({Definition->stableID(), LoadInstruction->stableID()}); + Context.expect(Function.frameInfo() + .updateObject(Fixture.StackSlot, std::move(UpdatedStackSlot)) + .has_value() && + Function.frameInfo().setMaxCallFrameSize(16).has_value() && + Function.frameInfo().freeze(32, *rcc::Alignment::create(16)).has_value(), + "source frame metadata must be finalized"); + + auto TargetState = rcc::TargetFunctionState::create( + CloneTargetState{Fixture.Entry->stableID(), LoadInstruction->stableID(), Fixture.Value, + Fixture.StackSlot, Fixture.Constant, Fixture.JumpTable}); + Context.expect(TargetState.has_value(), "source target function state must be created"); + if (!TargetState) + return false; + Context.expect(Function.setTargetState(std::move(*TargetState)).has_value(), + "source target function state must be installed"); + + rcc::MachineCompilationFailure Failure; + Failure.Diagnostic.Code = rcc::ErrorCode::Unsupported; + Failure.Diagnostic.Message = "test instruction selection failed"; + Failure.Diagnostic.Location = rcc::SourceLocation{"source.mir", 12, 5, 0}; + Failure.FailurePass = "test-instruction-select"; + Failure.FailureInstruction = LoadInstruction->stableID(); + Failure.FailureStage = rcc::PipelineStage::Selected; + Failure.FailureRule = "test-load-pattern"; + Failure.Target = "test64"; + Failure.Subtarget = "test64+feature"; + Context.expect(Function.compilationStatus() + .fail(rcc::MachineCompilationState::SelectionFailed, std::move(Failure)) + .has_value(), + "source structured compilation failure must be recorded"); + + Function.properties().set(rcc::MachineFunctionProperty::IsSSA); + Function.properties().set(rcc::MachineFunctionProperty::TracksLiveness); + Function.properties().set(rcc::MachineFunctionProperty::NoPHIs); + return true; +} + +void testDeepClone(TestContext &Context) { + auto Target = createTarget(); + auto Source = createFunction(Context, "source_function", 1); + SourceFixture Fixture; + if (!Target || !Source || !populateSource(Context, *Source, *Target, Fixture)) { + return; + } + Context.expect(rcc::MachineVerifier::verify(*Source, *Target).has_value(), + "source function must verify before cloning"); + + auto Clone = rcc::cloneMachineFunction( + *Source, *Target, + rcc::MachineFunctionCloneOptions{rcc::MachineFunctionStableID::fromValue(99), + std::string("cloned_function")}); + Context.expect(Clone.has_value(), "valid source function must deep-clone"); + if (!Clone) + return; + rcc::MachineFunction &Destination = *Clone->ClonedFunction; + + Context.expect(Destination.name() == "cloned_function" && + Destination.stableID() == rcc::MachineFunctionStableID::fromValue(99), + "clone must use its explicit function identity"); + Context.expect(Destination.properties() == Source->properties() && + rcc::MachineVerifier::verify(Destination, *Target).has_value() && + rcc::MachineVerifier::verify(*Source, *Target).has_value(), + "source and destination must independently preserve valid properties"); + Context.expect(Source->allBlocks().size() == 3 && Destination.allBlocks().size() == 2 && + Source->allInstructions().size() == 9 && + Destination.allInstructions().size() == 8 && + Source->frameInfo().objects().size() == 2 && + Destination.frameInfo().objects().size() == 1 && + Source->constantPool().entries().size() == 2 && + Destination.constantPool().entries().size() == 1 && + Source->jumpTableInfo().tables().size() == 2 && + Destination.jumpTableInfo().tables().size() == 1, + "clone must omit detached arena history"); + Context.expect(Clone->BlockStableIDs.size() == 2 && Clone->InstructionStableIDs.size() == 8 && + Clone->VirtualRegisters.size() == 2 && Clone->FrameIndices.size() == 1 && + Clone->ConstantPoolIndices.size() == 1 && Clone->JumpTableIndices.size() == 1, + "clone must publish complete live identity mappings"); + + rcc::MachineBasicBlock *DestinationEntry = Destination.firstBlock(); + rcc::MachineBasicBlock *DestinationExit = + DestinationEntry == nullptr ? nullptr : DestinationEntry->next(); + Context.expect(DestinationEntry != nullptr && DestinationExit != nullptr && + DestinationExit->next() == nullptr && DestinationEntry != Fixture.Entry && + DestinationExit != Fixture.Exit, + "clone blocks must be distinct and preserve layout"); + if (DestinationEntry == nullptr || DestinationExit == nullptr) + return; + + const auto EntryID = Clone->BlockStableIDs.find(Fixture.Entry->stableID()); + const auto ExitID = Clone->BlockStableIDs.find(Fixture.Exit->stableID()); + Context.expect(EntryID != Clone->BlockStableIDs.end() && ExitID != Clone->BlockStableIDs.end() && + EntryID->second == DestinationEntry->stableID() && + ExitID->second == DestinationExit->stableID(), + "block StableID mapping must identify cloned layout objects"); + Context.expect(DestinationEntry->successors().size() == 1 && + DestinationEntry->successors()[0].Block == DestinationExit && + DestinationExit->predecessors().size() == 1 && + DestinationExit->predecessors()[0] == DestinationEntry, + "clone must rebuild reciprocal CFG edges"); + Context.expect(std::ranges::equal(DestinationEntry->liveIns(), Fixture.Entry->liveIns()), + "clone must preserve block live-ins"); + + const auto ValuePosition = Clone->VirtualRegisters.find(Fixture.Value); + const auto CopyValuePosition = Clone->VirtualRegisters.find(Fixture.CopyValue); + Context.expect(ValuePosition != Clone->VirtualRegisters.end() && + CopyValuePosition != Clone->VirtualRegisters.end(), + "clone must map every live source vreg"); + if (ValuePosition == Clone->VirtualRegisters.end() || + CopyValuePosition == Clone->VirtualRegisters.end()) { + return; + } + const rcc::Register NewValue = ValuePosition->second; + const rcc::Register NewCopyValue = CopyValuePosition->second; + Context.expect(NewValue.virtualIndex() == 0 && NewCopyValue.virtualIndex() == 1, + "clone must deterministically compact source vreg tombstones"); + + auto SourceInfo = Source->registerInfo().virtualRegisterInfo(Fixture.Value); + auto DestinationInfo = Destination.registerInfo().virtualRegisterInfo(NewValue); + Context.expect(SourceInfo && DestinationInfo && + SourceInfo->get().Type == DestinationInfo->get().Type && + SourceInfo->get().Bank == DestinationInfo->get().Bank && + SourceInfo->get().RegClass == DestinationInfo->get().RegClass && + SourceInfo->get().AllocationHints == DestinationInfo->get().AllocationHints && + SourceInfo->get().TargetFlags == DestinationInfo->get().TargetFlags, + "clone must preserve vreg type and allocation constraints"); + Context.expect(Destination.registerInfo().liveIns().size() == 1 && + Destination.registerInfo().liveIns()[0].InitialVirtualRegister == NewValue && + std::ranges::equal(Destination.registerInfo().reservedRegisters(), + Source->registerInfo().reservedRegisters()), + "clone must remap function live-ins and preserve reserved registers"); + + rcc::MachineInstr *DestinationDef = findInstruction(*DestinationEntry, DefOpcode); + rcc::MachineInstr *DestinationBranch = findInstruction(*DestinationEntry, BranchOpcode); + rcc::MachineInstr *DestinationCopy = findInstruction(*DestinationExit, CopyOpcode); + rcc::MachineInstr *DestinationFrame = findInstruction(*DestinationExit, FrameOpcode); + rcc::MachineInstr *DestinationConstantPool = + findInstruction(*DestinationExit, ConstantPoolOpcode); + rcc::MachineInstr *DestinationJumpTable = findInstruction(*DestinationExit, JumpTableOpcode); + rcc::MachineInstr *DestinationAddress = findInstruction(*DestinationExit, BlockAddressOpcode); + rcc::MachineInstr *DestinationLoad = findInstruction(*DestinationExit, LoadOpcode); + Context.expect(DestinationDef && DestinationBranch && DestinationCopy && DestinationFrame && + DestinationConstantPool && DestinationJumpTable && DestinationAddress && + DestinationLoad, + "clone must preserve every live instruction"); + if (!DestinationDef || !DestinationBranch || !DestinationCopy || !DestinationFrame || + !DestinationConstantPool || !DestinationJumpTable || !DestinationAddress || + !DestinationLoad) { + return; + } + + Context.expect( + DestinationDef->operandAt(0)->registerValue() == NewValue && + DestinationDef->flags().has(rcc::MachineInstrFlag::NoMerge) && + DestinationDef->sourceLocation() == rcc::SourceLocation{"source.mir", 9, 4, 2} && + DestinationDef->origin() == rcc::OriginID::fromValue(77) && + DestinationDef->preInstrSymbol() == "before" && + DestinationDef->postInstrSymbol() == "after" && + DestinationDef->heapAllocMarker() == rcc::MachineMetadataReference::embeddedIR(20) && + DestinationDef->pcSections() == rcc::MachineMetadataReference::embeddedIR(21) && + DestinationDef->mmra() == rcc::MachineMetadataReference::embeddedIR(22) && + DestinationDef->cfiType() == 23 && DestinationDef->deactivationSymbol() == "guard", + "clone must remap register operands and preserve instruction metadata"); + Context.expect(DestinationBranch->operandAt(0)->getIf<rcc::BasicBlockOperand>()->Block == + DestinationExit, + "clone must remap basic-block operands"); + Context.expect(DestinationCopy->operandAt(0)->registerValue() == NewCopyValue && + DestinationCopy->operandAt(1)->registerValue() == NewValue && + DestinationCopy->operandAt(0)->tiedOperandIndex() == 1 && + DestinationCopy->operandAt(1)->tiedOperandIndex() == 0, + "clone must remap tied registers and rebuild ties"); + const auto FramePosition = Clone->FrameIndices.find(Fixture.StackSlot); + Context.expect(FramePosition != Clone->FrameIndices.end() && + FramePosition->second == rcc::FrameIndex::fromValue(0) && + DestinationFrame->operandAt(0)->getIf<rcc::FrameIndexOperand>()->Index == + FramePosition->second, + "clone must compact and remap frame indices"); + + const auto ConstantPosition = Clone->ConstantPoolIndices.find(Fixture.Constant); + const auto JumpTablePosition = Clone->JumpTableIndices.find(Fixture.JumpTable); + Context.expect( + ConstantPosition != Clone->ConstantPoolIndices.end() && + JumpTablePosition != Clone->JumpTableIndices.end() && + ConstantPosition->second == rcc::ConstantPoolIndex::fromValue(0) && + JumpTablePosition->second == rcc::JumpTableIndex::fromValue(0) && + DestinationConstantPool->operandAt(0)->getIf<rcc::ConstantPoolOperand>()->Index == + ConstantPosition->second && + DestinationJumpTable->operandAt(0)->getIf<rcc::JumpTableOperand>()->Index == + JumpTablePosition->second, + "clone must compact and remap constant-pool and jump-table indices"); + + const rcc::MachineConstantPoolEntry *DestinationConstantEntry = + ConstantPosition == Clone->ConstantPoolIndices.end() + ? nullptr + : Destination.constantPool().entry(ConstantPosition->second); + const auto *DestinationPoolAddress = + DestinationConstantEntry == nullptr + ? nullptr + : std::get_if<rcc::BlockAddressConstant>(&DestinationConstantEntry->description()->Value); + Context.expect(Destination.constantPool().isFrozen() && DestinationPoolAddress != nullptr && + DestinationPoolAddress->FunctionName == "cloned_function" && + DestinationPoolAddress->BlockName == "exit", + "clone must preserve frozen constant pool state and rewrite self addresses"); + + const rcc::MachineJumpTableEntry *DestinationJumpTableEntry = + JumpTablePosition == Clone->JumpTableIndices.end() + ? nullptr + : Destination.jumpTableInfo().table(JumpTablePosition->second); + const rcc::MachineJumpTableDesc *DestinationJumpDescription = + DestinationJumpTableEntry == nullptr ? nullptr : DestinationJumpTableEntry->description(); + Context.expect(Destination.jumpTableInfo().isFrozen() && DestinationJumpDescription != nullptr && + DestinationJumpDescription->Targets.size() == 3 && + DestinationJumpDescription->Targets[0] == DestinationExit && + DestinationJumpDescription->Targets[1] == DestinationEntry && + DestinationJumpDescription->Targets[2] == DestinationExit, + "clone must preserve jump-table order while remapping block targets"); + + Context.expect( + DestinationAddress->operandAt(0)->getIf<rcc::BlockAddressOperand>()->FunctionName == + "cloned_function", + "clone must rewrite self block-address function names"); + + bool MMOsMatch = DestinationLoad->memOperands().size() == Fixture.LoadMMOs.size(); + for (std::size_t Index = 0; MMOsMatch && Index < DestinationLoad->memOperands().size(); ++Index) { + rcc::MachineMemOperandDesc ExpectedMMO = Fixture.LoadMMOs[Index]->description(); + if (const auto Frame = ExpectedMMO.Base.frameIndex(); + Frame && FramePosition != Clone->FrameIndices.end()) { + ExpectedMMO.Base = rcc::MemoryBase::frameIndex(FramePosition->second); + } + if (const auto Constant = ExpectedMMO.Base.constantPoolIndex(); + Constant && ConstantPosition != Clone->ConstantPoolIndices.end()) { + ExpectedMMO.Base = rcc::MemoryBase::constantPool(ConstantPosition->second); + } + if (const auto JumpTable = ExpectedMMO.Base.jumpTableIndex(); + JumpTable && JumpTablePosition != Clone->JumpTableIndices.end()) { + ExpectedMMO.Base = rcc::MemoryBase::jumpTable(JumpTablePosition->second); + } + + const rcc::MachineMemOperand *DestinationMMO = DestinationLoad->memOperands()[Index]; + MMOsMatch = + DestinationMMO != Fixture.LoadMMOs[Index] && DestinationMMO->description() == ExpectedMMO && + Destination.ownsMemOperand(*DestinationMMO) && !Source->ownsMemOperand(*DestinationMMO); + } + Context.expect(MMOsMatch, "clone must deep-copy MMO ownership while remapping every table base"); + + const rcc::StackObject *DestinationStackSlot = + FramePosition == Clone->FrameIndices.end() + ? nullptr + : Destination.frameInfo().object(FramePosition->second); + Context.expect( + DestinationStackSlot != nullptr && Destination.frameInfo().isFrozen() && + Destination.frameInfo().staticStackSize() == 32 && + Destination.frameInfo().maxCallFrameSize() == 16 && + DestinationStackSlot->description()->Lifetime.size() == 1 && + DestinationStackSlot->description()->Lifetime[0].Begin == DestinationDef->stableID() && + DestinationStackSlot->description()->Lifetime[0].End == DestinationLoad->stableID(), + "clone must preserve frozen frame layout and remap instruction lifetimes"); + + rcc::MachineInstr *SourceDef = findInstruction(*Fixture.Entry, DefOpcode); + const auto InstructionID = SourceDef == nullptr + ? Clone->InstructionStableIDs.end() + : Clone->InstructionStableIDs.find(SourceDef->stableID()); + Context.expect(SourceDef != nullptr && InstructionID != Clone->InstructionStableIDs.end() && + InstructionID->second == DestinationDef->stableID(), + "instruction StableID mapping must identify cloned instructions"); + + rcc::MachineInstr *SourceLoad = findInstruction(*Fixture.Exit, LoadOpcode); + const auto LoadID = SourceLoad == nullptr + ? Clone->InstructionStableIDs.end() + : Clone->InstructionStableIDs.find(SourceLoad->stableID()); + const CloneTargetState *SourceTarget = Source->getTargetInfo<CloneTargetState>(); + const CloneTargetState *DestinationTarget = Destination.getTargetInfo<CloneTargetState>(); + Context.expect(SourceLoad != nullptr && LoadID != Clone->InstructionStableIDs.end() && + SourceTarget != nullptr && DestinationTarget != nullptr && + SourceTarget != DestinationTarget && EntryID != Clone->BlockStableIDs.end() && + FramePosition != Clone->FrameIndices.end() && + ConstantPosition != Clone->ConstantPoolIndices.end() && + JumpTablePosition != Clone->JumpTableIndices.end() && + DestinationTarget->Block == EntryID->second && + DestinationTarget->Instruction == LoadID->second && + DestinationTarget->TrackedRegister == NewValue && + DestinationTarget->Frame == FramePosition->second && + DestinationTarget->Constant == ConstantPosition->second && + DestinationTarget->JumpTable == JumpTablePosition->second, + "clone must strongly clone target state through every identity mapping"); + + const rcc::MachineCompilationFailure *SourceFailure = Source->compilationStatus().failure(); + const rcc::MachineCompilationFailure *DestinationFailure = + Destination.compilationStatus().failure(); + Context.expect(SourceFailure != nullptr && DestinationFailure != nullptr && + Destination.compilationStatus().state() == + rcc::MachineCompilationState::SelectionFailed && + DestinationFailure->FailureInstruction == DestinationLoad->stableID() && + DestinationFailure->Diagnostic == SourceFailure->Diagnostic && + DestinationFailure->FailurePass == SourceFailure->FailurePass && + DestinationFailure->FailureStage == SourceFailure->FailureStage && + DestinationFailure->FailureRule == SourceFailure->FailureRule && + DestinationFailure->Target == SourceFailure->Target && + DestinationFailure->Subtarget == SourceFailure->Subtarget, + "clone must preserve compilation failure details and remap its instruction"); +} + +void testCloneFailures(TestContext &Context) { + auto Target = createTarget(); + auto Source = createFunction(Context, "failure_source", 2); + if (!Target || !Source) + return; + + Context.expect( + !rcc::cloneMachineFunction(*Source, *Target, + rcc::MachineFunctionCloneOptions{ + rcc::MachineFunctionStableID::fromValue(0), std::nullopt}), + "clone must reject an invalid destination StableID"); + auto EmptyName = rcc::cloneMachineFunction( + *Source, *Target, + rcc::MachineFunctionCloneOptions{rcc::MachineFunctionStableID::fromValue(3), std::string()}); + Context.expect(EmptyName && EmptyName->ClonedFunction->name().empty(), + "clone must preserve an explicitly empty destination name"); + + auto Invalid = createFunction(Context, "invalid_source", 4); + rcc::MachineBasicBlock *Block = Invalid ? createBlock(Context, *Invalid) : nullptr; + if (Invalid && Block != nullptr) { + auto Unknown = Invalid->createInstruction(rcc::Opcode::fromValue(999)); + Context.expect(Unknown && Block->appendInstruction(**Unknown).has_value(), + "invalid clone source must contain an unknown opcode"); + Context.expect( + !rcc::cloneMachineFunction(*Invalid, *Target, + rcc::MachineFunctionCloneOptions{ + rcc::MachineFunctionStableID::fromValue(5), std::nullopt}), + "clone must verify and reject an invalid source function"); + } + + auto UnmappedState = createFunction(Context, "unmapped_target_state", 6); + auto Physical = rcc::Register::makePhysical(1); + if (UnmappedState && Physical) { + auto State = rcc::TargetFunctionState::create(CloneTargetState{ + rcc::MachineBlockStableID::fromValue(99), rcc::MachineInstrStableID::fromValue(99), + *Physical, rcc::FrameIndex::fromValue(0), rcc::ConstantPoolIndex::fromValue(0), + rcc::JumpTableIndex::fromValue(0)}); + Context.expect(State && UnmappedState->setTargetState(std::move(*State)).has_value(), + "unmapped target state negative test must be installed"); + Context.expect( + !rcc::cloneMachineFunction(*UnmappedState, *Target, + rcc::MachineFunctionCloneOptions{ + rcc::MachineFunctionStableID::fromValue(7), std::nullopt}), + "clone must reject target state that does not map its identities"); + } +} + +} // namespace + +int main() { + TestContext Context; + testDeepClone(Context); + testCloneFailures(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineFunctionRecordsTest.cpp b/backend/unittests/CodeGen/MachineFunctionRecordsTest.cpp new file mode 100644 index 0000000..621bd21 --- /dev/null +++ b/backend/unittests/CodeGen/MachineFunctionRecordsTest.cpp @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineFunctionRecords.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineDebugInfo.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineFunctionCloner.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineVerifier.h" +#include "rcc/CodeGen/TargetDescription.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t NopOpcode = 1; +constexpr std::uint32_t CallOpcode = 2; + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::PhysicalRegister phys(std::uint32_t Number) { + auto Reg = rcc::PhysicalRegister::create(Number); + return Reg ? *Reg : rcc::PhysicalRegister{}; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + rcc::InstructionDescriptor Nop; + Nop.InstructionOpcode = rcc::Opcode::fromValue(NopOpcode); + Nop.Name = "TEST_NOP"; + Nop.Flags = {rcc::InstructionFlag::ProducesCode}; + + rcc::InstructionDescriptor Call; + Call.InstructionOpcode = rcc::Opcode::fromValue(CallOpcode); + Call.Name = "TEST_CALL"; + Call.Flags = {rcc::InstructionFlag::Call, rcc::InstructionFlag::HasSideEffects, + rcc::InstructionFlag::ProducesCode}; + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Nop)); + Descriptors.push_back(std::move(Call)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> +createFunction(TestContext &Context, std::uint64_t StableID, std::string Name) { + auto Function = rcc::MachineFunction::create(std::move(Name), + rcc::MachineFunctionStableID::fromValue(StableID)); + Context.expect(Function.has_value(), "machine function creation must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineBasicBlock *appendBlock(TestContext &Context, + rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + Context.expect(Block.has_value(), "machine basic block creation must succeed"); + if (!Block) + return nullptr; + Context.expect(Function.appendBlock(**Block).has_value(), "machine block linking must succeed"); + return *Block; +} + +[[nodiscard]] rcc::MachineInstr *appendInstruction(TestContext &Context, + rcc::MachineFunction &Function, + rcc::MachineBasicBlock &Block, + std::uint32_t OpcodeValue) { + auto Instruction = Function.createInstruction(rcc::Opcode::fromValue(OpcodeValue)); + Context.expect(Instruction.has_value(), "machine instruction creation must succeed"); + if (!Instruction) + return nullptr; + Context.expect(Block.appendInstruction(**Instruction).has_value(), + "machine instruction linking must succeed"); + return *Instruction; +} + +void testValueValidation(TestContext &Context) { + rcc::MachineFunctionRecords Records; + Context.expect(!Records.hasCalleeSavedRegisters() && Records.calleeSavedRegisters().empty(), + "an absent callee-saved list must remain distinguishable"); + Context.expect(Records.setCalleeSavedRegisters({}).has_value() && + Records.hasCalleeSavedRegisters() && Records.calleeSavedRegisters().empty(), + "an explicit empty callee-saved list must remain present"); + Records.clearCalleeSavedRegisters(); + + const rcc::PhysicalRegister G0 = phys(rcc::test::TestRegister::G0); + Context.expect(!Records.setCalleeSavedRegisters({G0, G0}), + "duplicate callee-saved registers must be rejected"); + Context.expect(!Records.addCallSite({}), + "a call-site record must require a valid instruction StableID"); + Context.expect(!Records.addCalledGlobal({ + .CallInstruction = rcc::MachineInstrStableID::fromValue(1), + .Callee = {}, + .Flags = 0, + }), + "a called-global record must require a nonempty callee"); + Context.expect(!Records.addEntryValue({}), + "an entry-value record must require a physical register and debug metadata"); + + const rcc::MachinePrefetchTarget Target{ + .Block = {.BaseID = 7, .CloneID = 3}, + .CallsiteIndex = 4, + }; + Context.expect(Records.addPrefetchTarget(Target).has_value() && + !Records.addPrefetchTarget(Target), + "duplicate structured prefetch targets must be rejected"); +} + +void testVerifierAndClone(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 1, "records"); + if (!Target || Function == nullptr) + return; + Context.expect( + Function->metadata().addNode({.Slot = 0, .IsDistinct = false, .Elements = {}}).has_value(), + "function record metadata setup must succeed"); + + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + Block->attributes().setProfileID({.BaseID = 19, .CloneID = 2}); + rcc::MachineInstr *Call = appendInstruction(Context, *Function, *Block, CallOpcode); + if (Call == nullptr) + return; + + const rcc::PhysicalRegister G0 = phys(rcc::test::TestRegister::G0); + const rcc::PhysicalRegister G1 = phys(rcc::test::TestRegister::G1); + Context.expect(Function->records().setCalleeSavedRegisters({G0, G1}).has_value(), + "callee-saved registers must be stored"); + Context.expect(Function->records() + .addCallSite({ + .CallInstruction = Call->stableID(), + .ForwardedArguments = + { + {.Reg = G0, .ArgumentNumber = 0}, + {.Reg = G1, .ArgumentNumber = 0}, + }, + .CalleeTypeIDs = {0x11, 0x22}, + }) + .has_value(), + "structured call-site information must be stored"); + Context.expect(Function->records() + .addCalledGlobal({ + .CallInstruction = Call->stableID(), + .Callee = "callee", + .Flags = 5, + }) + .has_value(), + "structured called-global information must be stored"); + Context.expect(Function->records() + .addPrefetchTarget({ + .Block = {.BaseID = 19, .CloneID = 2}, + .CallsiteIndex = 8, + }) + .has_value(), + "structured prefetch target information must be stored"); + auto EntryLocation = + rcc::MachineDebugLocation::reference(rcc::MachineMetadataReference::embeddedIR(12)); + Context.expect(EntryLocation.has_value(), "entry-value debug location must be representable"); + if (!EntryLocation) + return; + const rcc::MachineEntryValueInfo EntryValue{ + .Reg = G0, + .DebugInfo = + { + .Variable = rcc::MachineMetadataReference::embeddedIR(10), + .Expression = rcc::MachineMetadataValue::expression(rcc::MachineDIExpression{}), + .Location = *EntryLocation, + }, + }; + Context.expect(Function->records().addEntryValue(EntryValue).has_value(), + "structured entry-value debug information must be stored"); + Context.expect(rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "well-formed function records must pass unified verification"); + + auto Clone = + rcc::cloneMachineFunction(*Function, *Target, + { + .NewStableID = rcc::MachineFunctionStableID::fromValue(2), + .NewName = std::string("records.clone"), + }); + Context.expect(Clone.has_value(), "machine function records must support deep cloning"); + if (!Clone) + return; + + const auto InstructionID = Clone->InstructionStableIDs.find(Call->stableID()); + const rcc::MachineFunctionRecords &ClonedRecords = Clone->ClonedFunction->records(); + Context.expect( + InstructionID != Clone->InstructionStableIDs.end() && + ClonedRecords.hasCalleeSavedRegisters() && + ClonedRecords.calleeSavedRegisters().size() == 2 && + ClonedRecords.callSites().size() == 1 && + ClonedRecords.callSites()[0].CallInstruction == InstructionID->second && + ClonedRecords.callSites()[0].ForwardedArguments.size() == 2 && + ClonedRecords.callSites()[0].ForwardedArguments[1].ArgumentNumber == 0 && + ClonedRecords.callSites()[0].CalleeTypeIDs == std::vector<std::uint64_t>({0x11, 0x22}) && + ClonedRecords.calledGlobals().size() == 1 && + ClonedRecords.calledGlobals()[0].CallInstruction == InstructionID->second && + ClonedRecords.calledGlobals()[0].Callee == "callee" && + ClonedRecords.calledGlobals()[0].Flags == 5 && + ClonedRecords.prefetchTargets().size() == 1 && + ClonedRecords.prefetchTargets()[0].Block == + rcc::MachineBasicBlockProfileID{.BaseID = 19, .CloneID = 2} && + ClonedRecords.prefetchTargets()[0].CallsiteIndex == 8 && + ClonedRecords.entryValues().size() == 1 && ClonedRecords.entryValues()[0] == EntryValue, + "deep clone must preserve records and remap instruction StableIDs"); +} + +void testCrossReferenceFailures(TestContext &Context) { + auto Target = createTarget(); + if (!Target) + return; + + { + auto Function = createFunction(Context, 10, "noncall"); + if (Function == nullptr) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + rcc::MachineInstr *Nop = appendInstruction(Context, *Function, *Block, NopOpcode); + if (Nop == nullptr) + return; + Context.expect(Function->records() + .addCallSite({ + .CallInstruction = Nop->stableID(), + .ForwardedArguments = {}, + .CalleeTypeIDs = {}, + }) + .has_value() && + !rcc::MachineVerifier::verify(*Function, *Target), + "call-site records must reference instructions described as calls"); + } + + { + auto Function = createFunction(Context, 11, "detached"); + if (Function == nullptr) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + auto Detached = Function->createInstruction(rcc::Opcode::fromValue(CallOpcode)); + Context.expect(Detached.has_value() && + Function->records() + .addCalledGlobal({ + .CallInstruction = (*Detached)->stableID(), + .Callee = "callee", + }) + .has_value() && + !rcc::MachineVerifier::verify(*Function, *Target), + "function records must not reference detached instructions"); + } + + { + auto Function = createFunction(Context, 12, "unknown-register"); + if (Function == nullptr) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + rcc::MachineInstr *Call = appendInstruction(Context, *Function, *Block, CallOpcode); + if (Call == nullptr) + return; + Context.expect(Function->records() + .addCallSite({ + .CallInstruction = Call->stableID(), + .ForwardedArguments = {{.Reg = phys(999), .ArgumentNumber = 0}}, + .CalleeTypeIDs = {}, + }) + .has_value() && + !rcc::MachineVerifier::verify(*Function, *Target), + "call-site records must reject target-unknown physical registers"); + } + + { + auto Function = createFunction(Context, 13, "dangling-prefetch-target"); + if (Function == nullptr) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (Block == nullptr) + return; + Context.expect(Function->records() + .addPrefetchTarget({ + .Block = {.BaseID = 1, .CloneID = 0}, + .CallsiteIndex = 0, + }) + .has_value() && + rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "prefetch targets may reference a bb_id without a machine block"); + } + + { + auto Function = createFunction(Context, 14, "duplicate-bbid"); + if (Function == nullptr) + return; + rcc::MachineBasicBlock *First = appendBlock(Context, *Function); + rcc::MachineBasicBlock *Second = appendBlock(Context, *Function); + if (First == nullptr || Second == nullptr) + return; + First->attributes().setProfileID({.BaseID = 3, .CloneID = 1}); + Second->attributes().setProfileID({.BaseID = 3, .CloneID = 1}); + Context.expect(!rcc::MachineVerifier::verify(*Function, *Target), + "linked block bb_id values must be unique"); + } + + { + auto Function = createFunction(Context, 13, "entry-value-register"); + if (Function == nullptr) + return; + Context.expect(Function->records() + .addEntryValue({ + .Reg = phys(999), + .DebugInfo = + { + .Variable = rcc::MachineMetadataReference::embeddedIR(1), + .Expression = std::nullopt, + .Location = std::nullopt, + }, + }) + .has_value() && + !rcc::MachineVerifier::verify(*Function, *Target), + "entry-value records must reject target-unknown physical registers"); + } +} + +} // namespace + +int main() { + TestContext Context; + testValueValidation(Context); + testVerifierAndClone(Context); + testCrossReferenceFailures(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineIRAttributesTest.cpp b/backend/unittests/CodeGen/MachineIRAttributesTest.cpp new file mode 100644 index 0000000..5619210 --- /dev/null +++ b/backend/unittests/CodeGen/MachineIRAttributesTest.cpp @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineBasicBlockAttributes.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineFunctionAttributes.h" +#include "rcc/CodeGen/MachineFunctionCloner.h" +#include "rcc/CodeGen/MachineIRTypes.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineVerifier.h" +#include "rcc/CodeGen/TargetDescription.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t NopOpcode = 1; + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + rcc::InstructionDescriptor Nop; + Nop.InstructionOpcode = rcc::Opcode::fromValue(NopOpcode); + Nop.Name = "TEST_NOP"; + Nop.Flags = {rcc::InstructionFlag::ProducesCode}; + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Nop)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> createFunction(TestContext &Context, + std::uint64_t StableID) { + auto Function = + rcc::MachineFunction::create("attributes", rcc::MachineFunctionStableID::fromValue(StableID)); + Context.expect(Function.has_value(), "machine function creation must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +void testAttributeValueTypes(TestContext &Context) { + rcc::MachineFunctionAttributes FunctionAttributes; + FunctionAttributes.set(rcc::MachineFunctionAttribute::ExposesReturnsTwice); + FunctionAttributes.set(rcc::MachineFunctionAttribute::TracksDebugUserValues); + Context.expect(FunctionAttributes.has(rcc::MachineFunctionAttribute::ExposesReturnsTwice) && + FunctionAttributes.has(rcc::MachineFunctionAttribute::TracksDebugUserValues) && + rcc::getMachineFunctionAttributeName( + rcc::MachineFunctionAttribute::TracksDebugUserValues) == + "TracksDebugUserValues", + "function attribute flags and names must be stable"); + + auto DecodedFunctionAttributes = + rcc::MachineFunctionAttributes::fromRawFlags(FunctionAttributes.rawFlags()); + Context.expect(DecodedFunctionAttributes.has_value() && + DecodedFunctionAttributes->rawFlags() == FunctionAttributes.rawFlags(), + "function attribute raw flags must round trip"); + Context.expect(!rcc::MachineFunctionAttributes::fromRawFlags(std::uint16_t{1} << 15U), + "unknown function attribute bits must be rejected"); + + rcc::MachineBasicBlockAttributes BlockAttributes; + Context.expect(!BlockAttributes.setName("") && !BlockAttributes.setIRBlockName(""), + "empty block and IR block names must be rejected"); + Context.expect(BlockAttributes.setName("entry").has_value() && + BlockAttributes.setIRBlockName("entry").has_value(), + "decoded block and IR block names must be accepted"); + BlockAttributes.set(rcc::MachineBasicBlockAttribute::IRBlockAddressTaken); + BlockAttributes.setCallFrameSize(0); + Context.expect(BlockAttributes.verify().has_value() && BlockAttributes.callFrameSize() == 0, + "zero call-frame size must be the semantic default"); + BlockAttributes.setSectionID(rcc::MachineBasicBlockSectionID::exception()); + BlockAttributes.setProfileID({.BaseID = 7, .CloneID = 2}); + Context.expect( + BlockAttributes.sectionID().kind() == rcc::MachineBasicBlockSectionKind::Exception && + BlockAttributes.sectionID().number() == 0 && + BlockAttributes.profileID() == rcc::MachineBasicBlockProfileID{.BaseID = 7, .CloneID = 2}, + "special section and fixed profile identities must remain structured"); + Context.expect(!rcc::MachineBasicBlockAttributes::fromRawFlags(std::uint8_t{1} << 7U), + "unknown block attribute bits must be rejected"); + + rcc::MachineInstrFlags Flags{ + rcc::MachineInstrFlag::NoConvergent, rcc::MachineInstrFlag::NonNeg, + rcc::MachineInstrFlag::Disjoint, rcc::MachineInstrFlag::NoUSWrap, + rcc::MachineInstrFlag::SameSign, rcc::MachineInstrFlag::InBounds, + }; + Context.expect( + Flags.has(rcc::MachineInstrFlag::NoConvergent) && Flags.has(rcc::MachineInstrFlag::NonNeg) && + Flags.has(rcc::MachineInstrFlag::Disjoint) && + Flags.has(rcc::MachineInstrFlag::NoUSWrap) && + Flags.has(rcc::MachineInstrFlag::SameSign) && Flags.has(rcc::MachineInstrFlag::InBounds), + "all fixed MIR instruction flags must have independent storage"); +} + +void testVerifierAndClone(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 1); + if (!Target || Function == nullptr) + return; + + auto FunctionAlignment = rcc::Alignment::create(32); + auto BlockAlignment = rcc::Alignment::create(16); + Context.expect(FunctionAlignment.has_value() && BlockAlignment.has_value(), + "power-of-two alignments must be accepted"); + if (!FunctionAlignment || !BlockAlignment) + return; + + Function->attributes().set(rcc::MachineFunctionAttribute::ExposesReturnsTwice); + Function->attributes().set(rcc::MachineFunctionAttribute::HasEHScopes); + Function->attributes().set(rcc::MachineFunctionAttribute::DebugInstrRef); + Function->attributes().setAlignment(*FunctionAlignment); + + auto BlockResult = Function->createBasicBlock(); + Context.expect(BlockResult.has_value(), "machine basic block creation must succeed"); + if (!BlockResult) + return; + rcc::MachineBasicBlock *Block = *BlockResult; + Context.expect(Function->appendBlock(*Block).has_value(), + "machine basic block linking must succeed"); + Context.expect(Block->attributes().setName("entry").has_value(), + "machine basic block name must be stored"); + Block->attributes().set(rcc::MachineBasicBlockAttribute::IRBlockAddressTaken); + Context.expect(!rcc::MachineVerifier::verify(*Function, *Target), + "IR-block-address-taken must require an IR block association"); + + Context.expect(Block->attributes().setIRBlockName("entry").has_value(), + "IR block association must be stored"); + Block->attributes().set(rcc::MachineBasicBlockAttribute::LandingPad); + Block->attributes().setAlignment(*BlockAlignment); + Block->attributes().setSectionID(rcc::MachineBasicBlockSectionID::numbered(3)); + Block->attributes().setProfileID({.BaseID = 11, .CloneID = 4}); + Block->attributes().setCallFrameSize(64); + + auto InstructionResult = Function->createInstruction(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(InstructionResult.has_value(), "machine instruction creation must succeed"); + if (!InstructionResult) + return; + rcc::MachineInstr *Instruction = *InstructionResult; + Instruction->setFlags({ + rcc::MachineInstrFlag::NoConvergent, + rcc::MachineInstrFlag::NonNeg, + rcc::MachineInstrFlag::Disjoint, + rcc::MachineInstrFlag::NoUSWrap, + rcc::MachineInstrFlag::SameSign, + rcc::MachineInstrFlag::InBounds, + }); + Context.expect(Block->appendInstruction(*Instruction).has_value(), + "machine instruction linking must succeed"); + Context.expect(rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "function and block MIR attributes must pass the unified verifier"); + + auto Clone = + rcc::cloneMachineFunction(*Function, *Target, + rcc::MachineFunctionCloneOptions{ + .NewStableID = rcc::MachineFunctionStableID::fromValue(2), + .NewName = std::string("attributes.clone"), + }); + Context.expect(Clone.has_value(), "attributed machine function clone must succeed"); + if (!Clone) + return; + + const rcc::MachineFunction &ClonedFunction = *Clone->ClonedFunction; + const rcc::MachineBasicBlock *ClonedBlock = ClonedFunction.firstBlock(); + const rcc::MachineInstr *ClonedInstruction = + ClonedBlock == nullptr ? nullptr : ClonedBlock->firstInstruction(); + Context.expect(ClonedFunction.attributes() == Function->attributes(), + "function MIR attributes must survive deep cloning"); + Context.expect(ClonedBlock != nullptr && ClonedBlock->attributes() == Block->attributes(), + "basic block MIR attributes must survive deep cloning"); + Context.expect(ClonedInstruction != nullptr && ClonedInstruction->flags() == Instruction->flags(), + "complete MIR instruction flags must survive deep cloning"); +} + +} // namespace + +int main() { + TestContext Context; + testAttributeValueTypes(Context); + testVerifierAndClone(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineIRBuilderTest.cpp b/backend/unittests/CodeGen/MachineIRBuilderTest.cpp new file mode 100644 index 0000000..17e9874 --- /dev/null +++ b/backend/unittests/CodeGen/MachineIRBuilderTest.cpp @@ -0,0 +1,556 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineIRBuilder.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/LowLevelType.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineConstantPool.h" +#include "rcc/CodeGen/MachineDebugInfo.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineJumpTableInfo.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" + +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <memory> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t CopyOpcode = 1; +constexpr std::uint32_t AddOpcode = 2; +constexpr std::uint32_t LoadOpcode = 3; +constexpr std::uint32_t NopOpcode = 4; +constexpr std::uint32_t BranchOpcode = 5; +constexpr std::uint32_t DynamicImplicitOpcode = 6; +constexpr std::uint32_t ConstantPoolOpcode = 7; +constexpr std::uint32_t JumpTableOpcode = 8; + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::OperandDescriptor registerDescriptor(rcc::RegisterOperandRole Role) { + rcc::OperandDescriptor Descriptor; + Descriptor.Kinds = {rcc::MachineOperand::Kind::Register}; + Descriptor.RegisterRole = Role; + return Descriptor; +} + +[[nodiscard]] rcc::InstructionDescriptor instructionDescriptor(std::uint32_t OpcodeValue, + std::string Name) { + rcc::InstructionDescriptor Descriptor; + Descriptor.InstructionOpcode = rcc::Opcode::fromValue(OpcodeValue); + Descriptor.Name = std::move(Name); + Descriptor.Flags = {rcc::InstructionFlag::ProducesCode}; + return Descriptor; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + auto Copy = instructionDescriptor(CopyOpcode, "TEST_COPY"); + auto CopyDef = registerDescriptor(rcc::RegisterOperandRole::Def); + auto CopyUse = registerDescriptor(rcc::RegisterOperandRole::Use); + CopyDef.DefaultTiedOperand = 1; + CopyUse.DefaultTiedOperand = 0; + Copy.Operands.push_back(CopyDef); + Copy.Operands.push_back(CopyUse); + Copy.NumExplicitDefs = 1; + + auto Add = instructionDescriptor(AddOpcode, "TEST_ADD"); + Add.Operands.push_back(registerDescriptor(rcc::RegisterOperandRole::Def)); + Add.Operands.push_back(registerDescriptor(rcc::RegisterOperandRole::Use)); + rcc::OperandDescriptor RegisterOrImmediate; + RegisterOrImmediate.Kinds = { + rcc::MachineOperand::Kind::Register, + rcc::MachineOperand::Kind::Immediate, + }; + RegisterOrImmediate.RegisterRole = rcc::RegisterOperandRole::Use; + Add.Operands.push_back(RegisterOrImmediate); + Add.NumExplicitDefs = 1; + Add.ImplicitDefs.push_back( + rcc::ImplicitRegisterDescriptor{*rcc::PhysicalRegister::create(1), false, true}); + Add.ImplicitUses.push_back(rcc::ImplicitRegisterDescriptor{*rcc::PhysicalRegister::create(2)}); + + auto Load = instructionDescriptor(LoadOpcode, "TEST_LOAD"); + Load.Flags.set(rcc::InstructionFlag::MayLoad); + Load.RequiresMemoryOperands = true; + + auto Nop = instructionDescriptor(NopOpcode, "TEST_NOP"); + + auto Branch = instructionDescriptor(BranchOpcode, "TEST_BRANCH"); + rcc::OperandDescriptor Block; + Block.Kinds = {rcc::MachineOperand::Kind::BasicBlock}; + Branch.Operands.push_back(Block); + Branch.Flags.set(rcc::InstructionFlag::Branch); + Branch.Flags.set(rcc::InstructionFlag::Terminator); + + auto Dynamic = instructionDescriptor(DynamicImplicitOpcode, "TEST_DYNAMIC_IMPLICIT"); + Dynamic.AllowExtraImplicitOperands = true; + + auto ConstantPool = instructionDescriptor(ConstantPoolOpcode, "TEST_CONSTANT_POOL"); + rcc::OperandDescriptor ConstantPoolIndex; + ConstantPoolIndex.Kinds = {rcc::MachineOperand::Kind::ConstantPoolIndex}; + ConstantPool.Operands.push_back(ConstantPoolIndex); + + auto JumpTable = instructionDescriptor(JumpTableOpcode, "TEST_JUMP_TABLE"); + rcc::OperandDescriptor JumpTableIndex; + JumpTableIndex.Kinds = {rcc::MachineOperand::Kind::JumpTableIndex}; + JumpTable.Operands.push_back(JumpTableIndex); + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Copy)); + Descriptors.push_back(std::move(Add)); + Descriptors.push_back(std::move(Load)); + Descriptors.push_back(std::move(Nop)); + Descriptors.push_back(std::move(Branch)); + Descriptors.push_back(std::move(Dynamic)); + Descriptors.push_back(std::move(ConstantPool)); + Descriptors.push_back(std::move(JumpTable)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> +createFunction(TestContext &Context, std::string Name, std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "machine function creation must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineBasicBlock *createBlock(TestContext &Context, + rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + Context.expect(Block.has_value(), "machine basic block creation must succeed"); + if (!Block) + return nullptr; + Context.expect(Function.appendBlock(**Block).has_value(), + "machine basic block linking must succeed"); + return *Block; +} + +[[nodiscard]] rcc::MachineMemOperandDesc memoryDescription(rcc::MemoryAccessFlag Access) { + rcc::MachineMemOperandDesc Description; + Description.Accesses = {Access}; + Description.Size = *rcc::MemorySize::fixed(8); + Description.Align = *rcc::Alignment::create(8); + Description.Base = rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)); + return Description; +} + +[[nodiscard]] rcc::MachineInstr *buildNop(TestContext &Context, rcc::MachineIRBuilder &Builder) { + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(Pending.has_value(), "NOP builder creation must succeed"); + if (!Pending) + return nullptr; + auto Instruction = Pending->build(); + Context.expect(Instruction.has_value(), "NOP construction must succeed"); + if (!Instruction) + return nullptr; + return *Instruction; +} + +void testTransactionalBuild(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, "transactional", 1); + Context.expect(Target.has_value(), "builder target description must be valid"); + if (!Target || !Function) + return; + rcc::MachineBasicBlock *Block = createBlock(Context, *Function); + if (Block == nullptr) + return; + + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Destination = Function->registerInfo().createVirtualRegister(*Type); + auto Source = Function->registerInfo().createVirtualRegister(*Type); + Context.expect(Destination && Source, "builder virtual registers must be allocated"); + if (!Destination || !Source) + return; + + rcc::MachineIRBuilder Builder(*Function, *Target); + Context.expect(!Builder.buildInstr(rcc::Opcode::fromValue(CopyOpcode)), + "an unset insertion point must reject instruction construction"); + Context.expect(Function->allInstructions().empty(), + "rejected builder setup must not allocate an instruction"); + Context.expect(Builder.setInsertPoint(*Block).has_value(), + "a linked block must be a valid insertion point"); + Context.expect(!Builder.buildInstr(rcc::Opcode::fromValue(99)), + "an unregistered opcode must be rejected"); + + auto Incomplete = Builder.buildInstr(rcc::Opcode::fromValue(CopyOpcode)); + Context.expect(Incomplete.has_value() && Incomplete->addDef(*Destination).has_value(), + "incomplete instruction setup must accept its first operand"); + if (!Incomplete) + return; + Context.expect(!Incomplete->tieOperands(0, 1) && !Incomplete->setSourceLocation({"", 0, 0, 0}) && + !Incomplete->setDebugLocation({}) && + !Incomplete->setOrigin(rcc::OriginID::fromValue(0)), + "invalid pending metadata and tie indices must be rejected"); + Context.expect(!Incomplete->build(), "descriptor validation must reject a missing operand"); + Context.expect(Function->allInstructions().empty() && Block->instructionCount() == 0, + "failed validation must not allocate or publish an instruction"); + + auto Complete = Builder.buildInstr(rcc::Opcode::fromValue(CopyOpcode)); + Context.expect(Complete.has_value(), "complete instruction builder must be created"); + if (!Complete) + return; + Context.expect(Complete->addDef(*Destination).has_value() && + Complete->addUse(*Source).has_value(), + "complete instruction operands must be accepted"); + Context.expect(Complete->tieOperands(0, 1).has_value(), + "an explicit tie matching the descriptor default must be idempotent"); + Context.expect(Complete->setFlags({rcc::MachineInstrFlag::FrameSetup}).has_value(), + "valid instruction flags must be accepted"); + auto InlineDebugLocation = rcc::MachineDILocation::create({ + { + .Line = 11, + .Column = 2, + .Scope = rcc::MachineMetadataReference::embeddedIR(3), + .IsImplicitCode = false, + .AtomGroup = 0, + .AtomRank = 0, + }, + }); + Context.expect(InlineDebugLocation.has_value(), + "builder debug location setup must be structurally valid"); + if (!InlineDebugLocation) + return; + const rcc::MachineDebugLocation DebugLocation = + rcc::MachineDebugLocation::inlineLocation(*InlineDebugLocation); + Context.expect( + Complete->setSourceLocation(rcc::SourceLocation{"builder.mir", 7, 3, 1}).has_value() && + Complete->setDebugLocation(DebugLocation).has_value() && + Complete->setOrigin(rcc::OriginID::fromValue(19)).has_value(), + "valid parser source, debug location, and origin metadata must be accepted"); + auto Built = Complete->build(); + Context.expect(Built.has_value(), "valid pending instruction must build"); + if (!Built) + return; + + rcc::MachineInstr *Instruction = *Built; + Context.expect(Instruction->stableID().value() == 1, + "failed builds must not consume instruction StableIDs"); + Context.expect(Instruction->parent() == Block && Block->firstInstruction() == Instruction, + "successful build must publish at the insertion point"); + Context.expect(Instruction->operandAt(0)->tiedOperandIndex() == 1 && + Instruction->operandAt(1)->tiedOperandIndex() == 0, + "descriptor-default tie must be materialized bidirectionally"); + Context.expect(Instruction->flags().has(rcc::MachineInstrFlag::FrameSetup) && + Instruction->sourceLocation() == rcc::SourceLocation{"builder.mir", 7, 3, 1} && + Instruction->debugLocation() == DebugLocation && + Instruction->origin() == rcc::OriginID::fromValue(19), + "builder metadata must be preserved on the published instruction"); + Context.expect(Target->validateInstruction(*Instruction, Function->registerInfo()).has_value(), + "builder output must satisfy the shared instruction descriptor"); + Context.expect(!Complete->build(), "a successful pending builder must be single-use"); + Context.expect(!Complete->setFlags({}) && !Complete->clearSourceLocation() && + !Complete->clearDebugLocation() && !Complete->clearOrigin(), + "all pending-builder mutators must reject use after publication"); + Context.expect(Function->registerInfo().verifyDefUseChains().has_value(), + "published builder operands must enter exact def-use chains"); + + auto MoveSource = Builder.buildInstr(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(MoveSource.has_value(), "movable pending builder must be created"); + if (!MoveSource) + return; + rcc::MachineInstrBuilder MoveDestination(std::move(*MoveSource)); + Context.expect(!MoveSource->build(), "a moved-from pending builder must be consumed"); + auto MovedResult = MoveDestination.build(); + Context.expect(MovedResult && (*MovedResult)->stableID().value() == 2, + "a moved-to pending builder must retain its construction state"); +} + +void testInsertionOrdering(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, "ordering", 2); + if (!Target || !Function) + return; + rcc::MachineBasicBlock *Block = createBlock(Context, *Function); + if (Block == nullptr) + return; + + rcc::MachineIRBuilder Builder(*Function, *Target); + Context.expect(Builder.setInsertPoint(*Block).has_value(), + "ordering builder insertion point must be valid"); + rcc::MachineInstr *First = buildNop(Context, Builder); + rcc::MachineInstr *Tail = buildNop(Context, Builder); + if (First == nullptr || Tail == nullptr) + return; + + Context.expect(Builder.setInsertPointAfter(*First).has_value(), + "after-instruction insertion point must be accepted"); + rcc::MachineInstr *MiddleA = buildNop(Context, Builder); + rcc::MachineInstr *MiddleB = buildNop(Context, Builder); + Context.expect(MiddleA != nullptr && MiddleB != nullptr && First->next() == MiddleA && + MiddleA->next() == MiddleB && MiddleB->next() == Tail, + "repeated insertion after a cursor must preserve build order"); + + Context.expect(Builder.setInsertPointBefore(*First).has_value(), + "before-instruction insertion point must be accepted"); + rcc::MachineInstr *PrefixA = buildNop(Context, Builder); + rcc::MachineInstr *PrefixB = buildNop(Context, Builder); + Context.expect(PrefixA != nullptr && PrefixB != nullptr && Block->firstInstruction() == PrefixA && + PrefixA->next() == PrefixB && PrefixB->next() == First, + "repeated insertion before a cursor must preserve build order"); +} + +void testImplicitAndMemoryValidation(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, "implicit_memory", 3); + auto OtherFunction = createFunction(Context, "foreign", 4); + if (!Target || !Function || !OtherFunction) + return; + rcc::MachineBasicBlock *Block = createBlock(Context, *Function); + rcc::MachineBasicBlock *OtherBlock = createBlock(Context, *OtherFunction); + if (Block == nullptr || OtherBlock == nullptr) + return; + + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Destination = Function->registerInfo().createVirtualRegister(*Type); + auto Source = Function->registerInfo().createVirtualRegister(*Type); + Context.expect(Destination && Source, "implicit test virtual registers must be allocated"); + if (!Destination || !Source) + return; + + rcc::MachineIRBuilder Builder(*Function, *Target); + Context.expect(Builder.setInsertPoint(*Block).has_value(), + "implicit test insertion point must be accepted"); + + auto Add = Builder.buildInstr(rcc::Opcode::fromValue(AddOpcode)); + Context.expect(Add && Add->addDef(*Destination).has_value() && Add->addUse(*Source).has_value() && + Add->addImmediate(7).has_value(), + "ADD operands must be accepted by the pending builder"); + if (!Add) + return; + auto BuiltAdd = Add->build(); + Context.expect(BuiltAdd.has_value(), "ADD with default implicits must build"); + if (!BuiltAdd) + return; + Context.expect( + (*BuiltAdd)->operandCount() == 5 && (*BuiltAdd)->operandAt(3)->isImplicit() && + (*BuiltAdd)->operandAt(3)->registerFlags().has(rcc::RegisterOperandFlag::Renamable) && + (*BuiltAdd)->operandAt(4)->isImplicit(), + "default implicit defs and uses must follow explicit operands"); + Context.expect(Target->validateInstruction(**BuiltAdd, Function->registerInfo()).has_value(), + "default implicit builder output must pass shared validation"); + + const std::size_t BeforeRejectedImplicit = Function->allInstructions().size(); + auto ExtraImplicit = Builder.buildInstr(rcc::Opcode::fromValue(AddOpcode)); + Context.expect( + ExtraImplicit && ExtraImplicit->addDef(*Destination).has_value() && + ExtraImplicit->addUse(*Source).has_value() && + ExtraImplicit->addImmediate(1).has_value() && + ExtraImplicit + ->addImplicitUse(rcc::Register::fromPhysical(*rcc::PhysicalRegister::create(3))) + .has_value(), + "dynamic implicit negative test must be assembled"); + Context.expect(ExtraImplicit && !ExtraImplicit->build() && + Function->allInstructions().size() == BeforeRejectedImplicit, + "descriptor-rejected dynamic implicits must not allocate instructions"); + + auto Dynamic = Builder.buildInstr(rcc::Opcode::fromValue(DynamicImplicitOpcode)); + Context.expect( + Dynamic && + Dynamic->addImplicitUse(rcc::Register::fromPhysical(*rcc::PhysicalRegister::create(3))) + .has_value(), + "descriptor-permitted dynamic implicit operand must be accepted"); + if (!Dynamic) + return; + auto BuiltDynamic = Dynamic->build(); + Context.expect( + BuiltDynamic.has_value() && + Target->validateInstruction(**BuiltDynamic, Function->registerInfo()).has_value(), + "dynamic implicit builder output must validate"); + + const std::size_t BeforeMissingMemory = Function->allInstructions().size(); + auto MissingMemory = Builder.buildInstr(rcc::Opcode::fromValue(LoadOpcode)); + Context.expect(MissingMemory && !MissingMemory->build() && + Function->allInstructions().size() == BeforeMissingMemory, + "missing required MMO must fail before instruction allocation"); + + rcc::StackObjectDesc StackSlot; + StackSlot.Size = 8; + StackSlot.Align = *rcc::Alignment::create(8); + auto FrameIndex = Function->frameInfo().createObject(StackSlot); + auto ForeignFrameIndex = OtherFunction->frameInfo().createObject(StackSlot); + Context.expect(FrameIndex && ForeignFrameIndex, + "memory validation stack objects must be created"); + if (!FrameIndex || !ForeignFrameIndex) + return; + + auto LoadMemory = Function->createMemOperand(memoryDescription(rcc::MemoryAccessFlag::Load)); + auto StoreMemory = Function->createMemOperand(memoryDescription(rcc::MemoryAccessFlag::Store)); + auto ForeignMemory = + OtherFunction->createMemOperand(memoryDescription(rcc::MemoryAccessFlag::Load)); + Context.expect(LoadMemory && StoreMemory && ForeignMemory, + "memory validation operands must be created"); + if (!LoadMemory || !StoreMemory || !ForeignMemory) + return; + + const std::size_t BeforeUnknownTableReferences = Function->allInstructions().size(); + auto UnknownConstant = Builder.buildInstr(rcc::Opcode::fromValue(ConstantPoolOpcode)); + auto UnknownJumpTable = Builder.buildInstr(rcc::Opcode::fromValue(JumpTableOpcode)); + Context.expect( + UnknownConstant && + UnknownConstant->addConstantPool(rcc::ConstantPoolIndex::fromValue(99)).has_value() && + !UnknownConstant->build() && UnknownJumpTable && + UnknownJumpTable->addJumpTable(rcc::JumpTableIndex::fromValue(99)).has_value() && + !UnknownJumpTable->build() && + Function->allInstructions().size() == BeforeUnknownTableReferences, + "builder must reject unknown constant-pool and jump-table indices before allocation"); + + rcc::MachineConstantPoolEntryDesc ConstantDescription; + auto Integer = rcc::APInt::fromUnsigned(64, 0x2a2a2a2a2a2a2a2aULL); + ConstantDescription.Value = std::move(*rcc::GenericConstantData::integer(std::move(*Integer))); + ConstantDescription.Size = 8; + ConstantDescription.Align = *rcc::Alignment::create(8); + auto ConstantIndex = Function->constantPool().addEntry(std::move(ConstantDescription)); + rcc::MachineJumpTableDesc JumpDescription; + JumpDescription.Targets = {Block}; + auto JumpIndex = Function->jumpTableInfo().createTable(std::move(JumpDescription)); + Context.expect(ConstantIndex && JumpIndex, "builder table references must have owned entries"); + if (!ConstantIndex || !JumpIndex) + return; + + auto ConstantInstruction = Builder.buildInstr(rcc::Opcode::fromValue(ConstantPoolOpcode)); + auto JumpInstruction = Builder.buildInstr(rcc::Opcode::fromValue(JumpTableOpcode)); + Context.expect( + ConstantInstruction && ConstantInstruction->addConstantPool(*ConstantIndex, 4).has_value() && + ConstantInstruction->build() && JumpInstruction && + JumpInstruction->addJumpTable(*JumpIndex).has_value() && JumpInstruction->build(), + "builder must publish valid owned table references"); + + auto ConstantMemoryDescription = memoryDescription(rcc::MemoryAccessFlag::Load); + ConstantMemoryDescription.Base = rcc::MemoryBase::constantPool(*ConstantIndex); + auto JumpMemoryDescription = memoryDescription(rcc::MemoryAccessFlag::Load); + JumpMemoryDescription.Base = rcc::MemoryBase::jumpTable(*JumpIndex); + auto ConstantMemory = Function->createMemOperand(std::move(ConstantMemoryDescription)); + auto JumpMemory = Function->createMemOperand(std::move(JumpMemoryDescription)); + auto ConstantLoad = Builder.buildInstr(rcc::Opcode::fromValue(LoadOpcode)); + auto JumpLoad = Builder.buildInstr(rcc::Opcode::fromValue(LoadOpcode)); + Context.expect(ConstantMemory && JumpMemory && ConstantLoad && + ConstantLoad->addMemOperand(**ConstantMemory).has_value() && + ConstantLoad->build() && JumpLoad && + JumpLoad->addMemOperand(**JumpMemory).has_value() && JumpLoad->build(), + "builder must validate constant-pool and jump-table MMO bases"); + + auto Load = Builder.buildInstr(rcc::Opcode::fromValue(LoadOpcode)); + Context.expect(Load && Load->addMemOperand(**LoadMemory).has_value(), + "function-owned load MMO must be accepted"); + if (!Load) + return; + auto BuiltLoad = Load->build(); + Context.expect(BuiltLoad.has_value() && + Target->validateInstruction(**BuiltLoad, Function->registerInfo()).has_value(), + "matching load MMO must build and validate"); + + const std::size_t BeforeStoreMismatch = Function->allInstructions().size(); + auto StoreMismatch = Builder.buildInstr(rcc::Opcode::fromValue(LoadOpcode)); + Context.expect(StoreMismatch && StoreMismatch->addMemOperand(**StoreMemory).has_value() && + !StoreMismatch->build() && + Function->allInstructions().size() == BeforeStoreMismatch, + "MMO access mismatch must fail before instruction allocation"); + auto ForeignMMO = Builder.buildInstr(rcc::Opcode::fromValue(LoadOpcode)); + Context.expect(ForeignMMO && !ForeignMMO->addMemOperand(**ForeignMemory), + "foreign-function MMO must be rejected immediately"); + + const std::size_t BeforeForeignBlock = Function->allInstructions().size(); + auto ForeignBranch = Builder.buildInstr(rcc::Opcode::fromValue(BranchOpcode)); + Context.expect(ForeignBranch && ForeignBranch->addBasicBlock(*OtherBlock).has_value() && + !ForeignBranch->build() && + Function->allInstructions().size() == BeforeForeignBlock, + "foreign basic block operand must fail before instruction allocation"); +} + +void testStaleCursorsAndOwnedOperands(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, "ownership", 5); + if (!Target || !Function) + return; + rcc::MachineBasicBlock *Block = createBlock(Context, *Function); + if (Block == nullptr) + return; + + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Destination = Function->registerInfo().createVirtualRegister(*Type); + auto Source = Function->registerInfo().createVirtualRegister(*Type); + if (!Destination || !Source) + return; + + rcc::MachineIRBuilder Builder(*Function, *Target); + Context.expect(Builder.setInsertPoint(*Block).has_value(), + "ownership test insertion point must be valid"); + auto Copy = Builder.buildInstr(rcc::Opcode::fromValue(CopyOpcode)); + Context.expect(Copy && Copy->addDef(*Destination).has_value() && + Copy->addUse(*Source).has_value(), + "ownership source instruction must be assembled"); + if (!Copy) + return; + auto BuiltCopy = Copy->build(); + if (!BuiltCopy) + return; + + auto DetachedTarget = Function->createInstruction(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(DetachedTarget.has_value(), "detached target instruction must be allocated"); + if (!DetachedTarget) + return; + rcc::MachineOperand *OwnedOperand = (*BuiltCopy)->operandAt(0); + Context.expect( + OwnedOperand != nullptr && !(*DetachedTarget)->addOperand(std::move(*OwnedOperand)) && + OwnedOperand->parent() == *BuiltCopy && OwnedOperand->registerValue() == *Destination, + "controlled instruction API must reject an owned operand without moving it"); + + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(Pending && OwnedOperand != nullptr && + !Pending->addOperand(std::move(*OwnedOperand)) && + OwnedOperand->parent() == *BuiltCopy, + "controlled builder API must reject an owned operand without moving it"); + + rcc::MachineInstr *Cursor = buildNop(Context, Builder); + if (Cursor == nullptr) + return; + Context.expect(Builder.setInsertPointBefore(*Cursor).has_value(), "cursor setup must succeed"); + auto Stale = Builder.buildInstr(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(Stale.has_value(), "pending stale-cursor test must be created"); + const std::size_t BeforeStaleBuild = Function->allInstructions().size(); + Context.expect(Block->removeInstruction(*Cursor).has_value(), "cursor removal must succeed"); + Context.expect(Stale && !Stale->build() && Function->allInstructions().size() == BeforeStaleBuild, + "a stale cursor must fail before instruction allocation"); +} + +} // namespace + +int main() { + TestContext Context; + testTransactionalBuild(Context); + testInsertionOrdering(Context); + testImplicitAndMemoryValidation(Context); + testStaleCursorsAndOwnedOperands(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineIRCoreTest.cpp b/backend/unittests/CodeGen/MachineIRCoreTest.cpp new file mode 100644 index 0000000..875eeb8 --- /dev/null +++ b/backend/unittests/CodeGen/MachineIRCoreTest.cpp @@ -0,0 +1,484 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/LowLevelType.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineCFGUpdater.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/RegisterMask.h" + +#include <array> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <memory> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> +createFunction(TestContext &Context, std::string Name, std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "valid machine function construction must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineBasicBlock *createLinkedBlock(TestContext &Context, + rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + Context.expect(Block.has_value(), "basic block allocation must succeed"); + if (!Block) + return nullptr; + auto LinkResult = Function.appendBlock(**Block); + Context.expect(LinkResult.has_value(), "basic block insertion must succeed"); + if (!LinkResult) + return nullptr; + return *Block; +} + +[[nodiscard]] bool addRegisterOperand(TestContext &Context, rcc::MachineInstr &Instruction, + rcc::Register Reg, rcc::RegisterOperandFlags Flags) { + auto Operand = rcc::MachineOperand::registerOperand(Reg, Flags); + Context.expect(Operand.has_value(), "valid register operand construction must succeed"); + if (!Operand) + return false; + auto AddResult = Instruction.addOperand(std::move(*Operand)); + Context.expect(AddResult.has_value(), "register operand insertion must succeed"); + return AddResult.has_value(); +} + +[[nodiscard]] std::size_t countOperands(rcc::RegOperandRange Range) { + std::size_t Count = 0; + for ([[maybe_unused]] const rcc::MachineOperand &Operand : Range) + ++Count; + return Count; +} + +rcc::MachineMemOperandDesc makeLoadDescription() { + rcc::MachineMemOperandDesc Description; + Description.Accesses = rcc::MemoryAccessFlags{ + rcc::MemoryAccessFlag::Load, + rcc::MemoryAccessFlag::Dereferenceable, + }; + Description.Size = *rcc::MemorySize::fixed(8); + Description.Align = *rcc::Alignment::create(8); + Description.Base = rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(0)); + return Description; +} + +void testOperandKinds(TestContext &Context) { + auto Function = createFunction(Context, "operands", 1); + if (!Function) + return; + auto Block = Function->createBasicBlock(); + Context.expect(Block.has_value(), "operand test block allocation must succeed"); + if (!Block) + return; + + const std::array<std::uint64_t, 1> HalfWords{0x3c00}; + auto HalfBits = rcc::APFloat::create(rcc::FloatSemantics::IEEEHalf, HalfWords); + Context.expect(HalfBits.has_value(), "well-sized floating immediate must be accepted"); + if (!HalfBits) + return; + Context.expect(HalfBits->bitWidth() == 16 && HalfBits->words()[0] == 0x3c00, + "floating immediate must preserve semantics and raw bits"); + + const std::array<std::uint64_t, 1> OverflowHalfWords{0x10000}; + Context.expect(!rcc::APFloat::create(rcc::FloatSemantics::IEEEHalf, OverflowHalfWords), + "floating immediate bits above semantics width must be rejected"); + const std::array<std::uint64_t, 2> TooManyHalfWords{0, 0}; + Context.expect(!rcc::APFloat::create(rcc::FloatSemantics::IEEEHalf, TooManyHalfWords), + "floating immediate word-count mismatch must be rejected"); + + auto Virtual = Function->registerInfo().createVirtualRegister( + *rcc::LowLevelType::makeCategoryNeutralScalar(32)); + Context.expect(Virtual.has_value(), "operand test virtual register must be created"); + if (!Virtual) + return; + + auto RegOperand = rcc::MachineOperand::registerOperand( + *Virtual, rcc::RegisterOperandFlags{rcc::RegisterOperandFlag::Use}); + Context.expect(RegOperand && RegOperand->kind() == rcc::MachineOperand::Kind::Register, + "register operand must preserve its kind"); + Context.expect(!rcc::MachineOperand::registerOperand(*Virtual, {}), + "register operand without def/use role must be rejected"); + Context.expect(!rcc::MachineOperand::registerOperand( + *Virtual, rcc::RegisterOperandFlags{rcc::RegisterOperandFlag::Define, + rcc::RegisterOperandFlag::Kill}), + "kill flag on register definition must be rejected"); + + auto Immediate = rcc::MachineOperand::immediate(-9, 17); + Context.expect(Immediate.kind() == rcc::MachineOperand::Kind::Immediate && + *Immediate.getIf<std::int64_t>() == -9 && Immediate.targetFlags() == 17, + "immediate operand must preserve value and target flags"); + + auto Floating = rcc::MachineOperand::floatingImmediate(*HalfBits); + Context.expect(Floating.kind() == rcc::MachineOperand::Kind::FloatingImmediate, + "floating operand must preserve its kind"); + auto BlockRef = rcc::MachineOperand::basicBlock(**Block); + Context.expect(BlockRef.getIf<rcc::BasicBlockOperand>()->Block == *Block, + "basic block operand must preserve its target"); + auto Frame = rcc::MachineOperand::frameIndex(rcc::FrameIndex::fromValue(4), -8); + Context.expect(Frame.getIf<rcc::FrameIndexOperand>()->Offset == -8, + "frame index operand must preserve its offset"); + auto Constant = rcc::MachineOperand::constantPool(rcc::ConstantPoolIndex::fromValue(2), 3); + Context.expect(Constant.kind() == rcc::MachineOperand::Kind::ConstantPoolIndex, + "constant pool operand must preserve its kind"); + auto Jump = rcc::MachineOperand::jumpTable(rcc::JumpTableIndex::fromValue(5)); + Context.expect(Jump.kind() == rcc::MachineOperand::Kind::JumpTableIndex, + "jump table operand must preserve its kind"); + + auto Global = rcc::MachineOperand::globalSymbol("global", 12); + auto External = rcc::MachineOperand::externalSymbol("external", -4); + auto BlockAddress = rcc::MachineOperand::blockAddress("function", "block", 2); + Context.expect(Global && External && BlockAddress, "valid symbol operands must be accepted"); + Context.expect(!rcc::MachineOperand::globalSymbol(""), + "empty global symbol operand must be rejected"); + Context.expect(!rcc::MachineOperand::blockAddress("", "block"), + "incomplete block address operand must be rejected"); + + auto Mask = std::make_shared<const rcc::RegisterMask>(std::vector<std::uint64_t>{0xffff}); + auto MaskOperand = rcc::MachineOperand::registerMask(Mask); + Context.expect(MaskOperand && MaskOperand->kind() == rcc::MachineOperand::Kind::RegisterMask, + "register mask operand must preserve shared immutable mask"); + Context.expect(!rcc::MachineOperand::registerMask(nullptr), + "null register mask operand must be rejected"); + + auto MetadataReference = + rcc::MachineMetadataValue::reference(rcc::MachineMetadataReference::embeddedIR(0)); + Context.expect(MetadataReference.has_value(), + "a valid scoped metadata reference must form an operand value"); + if (!MetadataReference) + return; + auto Metadata = rcc::MachineOperand::metadata(std::move(*MetadataReference)); + Context.expect(Metadata && Metadata->kind() == rcc::MachineOperand::Kind::Metadata && + Metadata->getIf<rcc::MetadataOperand>()->Value.reference() != nullptr, + "metadata operand must preserve its scoped semantic value"); + Context.expect(!rcc::MachineOperand::metadata({}), + "an unrepresented metadata operand value must be rejected"); + + auto Target = rcc::MachineOperand::targetIndex(rcc::TargetIndex::fromValue(8), 9); + Context.expect(Target.getIf<rcc::TargetIndexOperand>()->Offset == 9, + "target index operand must preserve its offset"); + auto MCSymbol = rcc::MachineOperand::mcSymbol(".Ltmp0", 1); + Context.expect(MCSymbol && MCSymbol->kind() == rcc::MachineOperand::Kind::MCSymbol, + "MC symbol precursor must preserve its kind"); + + const auto Clone = Immediate.cloneDetached(); + Context.expect(Clone.parent() == nullptr && + *Clone.getIf<std::int64_t>() == *Immediate.getIf<std::int64_t>(), + "detached operand clone must preserve payload without ownership"); + Context.expect(!Immediate.setRegister(*Virtual), + "register mutation on non-register operand must be rejected"); +} + +void testRegisterInfoAndOperandMovement(TestContext &Context) { + auto Function = createFunction(Context, "def_use", 2); + if (!Function) + return; + rcc::MachineBasicBlock *Block = createLinkedBlock(Context, *Function); + if (Block == nullptr) + return; + + auto I32 = rcc::LowLevelType::makeCategoryNeutralScalar(32); + Context.expect(I32.has_value(), "i32 construction for MRI test must succeed"); + if (!I32) + return; + auto V0 = Function->registerInfo().createVirtualRegister(*I32); + auto V1 = Function->registerInfo().createVirtualRegister(*I32); + Context.expect(V0 && V1, "MRI must allocate monotonic virtual registers"); + if (!V0 || !V1) + return; + Context.expect(V0->virtualIndex() == 0 && V1->virtualIndex() == 1, + "MRI virtual register indices must start at zero and increase"); + + auto PhysReg = rcc::PhysicalRegister::create(7); + Context.expect(PhysReg.has_value(), "MRI test physical register must be valid"); + if (!PhysReg) + return; + const rcc::Register Physical = rcc::Register::fromPhysical(*PhysReg); + + auto Instruction = Function->createInstruction(rcc::Opcode::fromValue(1)); + Context.expect(Instruction.has_value(), "machine instruction allocation must succeed"); + if (!Instruction) + return; + rcc::MachineInstr *Instr = *Instruction; + + if (!addRegisterOperand(Context, *Instr, *V0, + rcc::RegisterOperandFlags{rcc::RegisterOperandFlag::Define}) || + !addRegisterOperand(Context, *Instr, *V0, + rcc::RegisterOperandFlags{rcc::RegisterOperandFlag::Use}) || + !addRegisterOperand(Context, *Instr, *V0, + rcc::RegisterOperandFlags{rcc::RegisterOperandFlag::Use, + rcc::RegisterOperandFlag::Debug}) || + !addRegisterOperand(Context, *Instr, Physical, + rcc::RegisterOperandFlags{rcc::RegisterOperandFlag::Use})) { + return; + } + + Context.expect(Function->registerInfo().useEmpty(*V0), + "detached instruction operands must not enter def-use chains"); + auto LinkInstruction = Block->appendInstruction(*Instr); + Context.expect(LinkInstruction.has_value(), "instruction insertion must succeed"); + if (!LinkInstruction) + return; + + Context.expect(countOperands(Function->registerInfo().regOperands(*V0)) == 3, + "def-use chain must include definition, use, and debug use"); + Context.expect(countOperands(Function->registerInfo().useOperands(*V0)) == 2, + "use iterator must include ordinary and debug uses"); + Context.expect(countOperands(Function->registerInfo().useOperands(*V0, false)) == 1, + "nodbg use iterator must filter debug operands"); + Context.expect(Function->registerInfo().getUniqueVRegDef(*V0) == Instr->operandAt(0), + "unique virtual register definition must be discoverable"); + Context.expect(countOperands(Function->registerInfo().regOperands(Physical)) == 1, + "physical registers must participate in exact def-use chains"); + + rcc::MachineOperand *const OldOperandAddress = Instr->operandAt(0); + Instr->reserveOperands(128); + Context.expect(Instr->operandAt(0) != nullptr && Instr->operandAt(0)->parent() == Instr, + "operand relocation must preserve instruction ownership"); + Context.expect(Function->registerInfo().verifyDefUseChains().has_value(), + "operand relocation must repair intrusive def-use links"); + Context.expect(OldOperandAddress != Instr->operandAt(0) || + Function->registerInfo().getUniqueVRegDef(*V0) == Instr->operandAt(0), + "def-use head must follow an operand when its address changes"); + + auto TieResult = Instr->tieOperands(0, 1); + Context.expect(TieResult.has_value(), "valid def-use operand tie must succeed"); + const auto DetachedTieClone = Instr->operandAt(0)->cloneDetached(); + Context.expect(!DetachedTieClone.tiedOperandIndex(), + "single detached operand clone must not retain an instruction-local tie"); + auto InsertResult = Instr->insertOperand(0, rcc::MachineOperand::immediate(5)); + Context.expect(InsertResult.has_value(), "operand insertion before a tie must succeed"); + Context.expect(Instr->operandAt(1)->tiedOperandIndex() == 2 && + Instr->operandAt(2)->tiedOperandIndex() == 1, + "operand insertion must rewrite both tie indices"); + auto EraseResult = Instr->eraseOperand(0); + Context.expect(EraseResult.has_value(), "temporary operand erasure must succeed"); + Context.expect(Instr->operandAt(0)->tiedOperandIndex() == 1 && + Instr->operandAt(1)->tiedOperandIndex() == 0, + "operand erasure must rewrite both tie indices"); + Context.expect(Instr->untieOperand(0).has_value(), "reciprocal operand tie must be removable"); + + auto SetRegisterResult = Instr->operandAt(1)->setRegister(*V1); + Context.expect(SetRegisterResult.has_value(), "register operand rebinding must succeed"); + Context.expect(Function->registerInfo().useEmpty(*V0, false), + "register rebinding must unlink the old ordinary use"); + Context.expect(Function->registerInfo().hasOneUse(*V1), + "register rebinding must link the new use"); + + auto ReplaceResult = Function->registerInfo().replaceRegWith(*V1, *V0); + Context.expect(ReplaceResult && *ReplaceResult == 1, + "replaceRegWith must rewrite every linked operand exactly once"); + Context.expect(Function->registerInfo().verifyDefUseChains().has_value(), + "register replacement must preserve exact def-use chains"); + + for (std::int64_t Value = 0; Value < 64; ++Value) { + auto Result = Instr->addOperand(rcc::MachineOperand::immediate(Value)); + Context.expect(Result.has_value(), "variadic immediate operand insertion must succeed"); + } + Context.expect(Function->registerInfo().verifyDefUseChains().has_value(), + "repeated operand growth must preserve def-use links"); + + auto &MRI = Function->registerInfo(); + Context.expect(MRI.setRegisterBank(*V0, rcc::RegisterBankID::fromValue(3)).has_value(), + "MRI must record a register bank"); + Context.expect(MRI.setRegisterClass(*V0, rcc::RegisterClassID::fromValue(4)).has_value(), + "MRI must record a register class"); + Context.expect(MRI.addAllocationHint(*V0, *PhysReg).has_value(), + "MRI must record a physical allocation hint"); + auto V0Info = MRI.virtualRegisterInfo(*V0); + Context.expect(V0Info && V0Info->get().Type == I32 && + V0Info->get().Bank == rcc::RegisterBankID::fromValue(3) && + V0Info->get().RegClass == rcc::RegisterClassID::fromValue(4), + "MRI virtual register properties must evolve without losing LowLevelType"); + + Context.expect(MRI.addLiveIn(*PhysReg, *V0).has_value(), + "MRI must record function physical live-ins"); + Context.expect(!MRI.addLiveIn(*PhysReg, *V0), + "duplicate function physical live-ins must be rejected"); + Context.expect(MRI.reservePhysicalRegister(*PhysReg).has_value(), + "MRI must record reserved physical registers"); + Context.expect(MRI.isReserved(*PhysReg), + "MRI reserved-register query must find recorded register"); + + auto DeadVReg = MRI.createVirtualRegister(*I32); + Context.expect(DeadVReg && MRI.eraseVirtualRegister(*DeadVReg).has_value(), + "unused virtual register must become a tombstone"); + auto LaterVReg = MRI.createVirtualRegister(*I32); + Context.expect(LaterVReg && LaterVReg->virtualIndex() != DeadVReg->virtualIndex(), + "MRI must not reuse tombstoned virtual register indices"); + Context.expect(!MRI.eraseVirtualRegister(*V0), + "virtual register with linked operands must not be erased"); + + auto ZombieVReg = MRI.createVirtualRegister(*I32); + auto ZombieInstr = Function->createInstruction(rcc::Opcode::fromValue(2)); + Context.expect(ZombieVReg && ZombieInstr, + "detached tombstone validation setup must allocate objects"); + if (ZombieVReg && ZombieInstr) { + Context.expect(addRegisterOperand(Context, **ZombieInstr, *ZombieVReg, + rcc::RegisterOperandFlags{rcc::RegisterOperandFlag::Use}), + "detached instruction must initially accept its live virtual register"); + Context.expect(MRI.eraseVirtualRegister(*ZombieVReg).has_value(), + "detached operand must not make an unpublished vreg live"); + const std::size_t OriginalInstructionCount = Block->instructionCount(); + Context.expect(!Block->appendInstruction(**ZombieInstr), + "instruction insertion must reject a tombstoned virtual register"); + Context.expect((*ZombieInstr)->parent() == nullptr && + Block->instructionCount() == OriginalInstructionCount, + "failed instruction insertion must not mutate block layout"); + Context.expect(!(*ZombieInstr)->operandAt(0)->setRegister(*ZombieVReg), + "detached register mutation must reject a tombstoned vreg"); + } + + auto UnlinkResult = Block->removeInstruction(*Instr); + Context.expect(UnlinkResult.has_value(), "instruction removal must succeed"); + Context.expect(MRI.useEmpty(*V0) && MRI.getUniqueVRegDef(*V0) == nullptr, + "instruction removal must unlink every register operand"); + Context.expect(MRI.verifyDefUseChains().has_value(), + "empty def-use lists after removal must remain structurally valid"); + Context.expect(Block->appendInstruction(*Instr).has_value(), + "detached instruction must be reinsertable"); + Context.expect(MRI.getUniqueVRegDef(*V0) == Instr->operandAt(0), + "instruction reinsertion must rebuild register links"); +} + +void testOwnershipCFGAndMemory(TestContext &Context) { + auto Function = createFunction(Context, "cfg", 3); + auto OtherFunction = createFunction(Context, "other", 4); + if (!Function || !OtherFunction) + return; + + rcc::MachineBasicBlock *Entry = createLinkedBlock(Context, *Function); + rcc::MachineBasicBlock *Left = createLinkedBlock(Context, *Function); + rcc::MachineBasicBlock *Right = createLinkedBlock(Context, *Function); + rcc::MachineBasicBlock *Foreign = createLinkedBlock(Context, *OtherFunction); + if (Entry == nullptr || Left == nullptr || Right == nullptr || Foreign == nullptr) + return; + + Context.expect(Function->blockCount() == 3, + "machine function block list must preserve layout count"); + Context.expect(Entry->layoutIndex() == 0 && Left->layoutIndex() == 1 && Right->layoutIndex() == 2, + "block layout indices must follow intrusive layout order"); + + auto Half = rcc::BranchProbability::fromFraction(1, 2); + Context.expect(Half.has_value(), "CFG test probability must be valid"); + if (!Half) + return; + auto CFG = Function->cfgUpdater(); + Context.expect(CFG.addSuccessor(*Entry, *Left, *Half).has_value() && + CFG.addSuccessor(*Entry, *Left, *Half).has_value(), + "duplicate semantic branches must merge into one CFG successor"); + Context.expect(Entry->successors().size() == 1 && Left->predecessors().size() == 1, + "CFG must not contain duplicate successor or predecessor entries"); + Context.expect(CFG.addSuccessor(*Entry, *Right, *Half).has_value(), + "second CFG successor must be insertable"); + Context.expect(!CFG.addSuccessor(*Entry, *Foreign, *Half), + "cross-function CFG edge must be rejected"); + Context.expect(!CFG.replaceSuccessor(*Entry, *Left, *Foreign) && + Entry->successors().size() == 2 && Left->predecessors().size() == 1, + "cross-function CFG replacement must fail without mutating existing edges"); + + Context.expect(CFG.replaceSuccessor(*Entry, *Left, *Right).has_value(), + "CFG successor replacement must succeed"); + Context.expect(Entry->successors().size() == 1 && Left->predecessors().empty() && + Right->predecessors().size() == 1, + "CFG replacement must merge duplicate target and repair predecessors"); + Context.expect(CFG.transferSuccessors(*Entry, *Left).has_value(), + "CFG successor transfer must succeed"); + Context.expect(Entry->successors().empty() && Left->successors().size() == 1 && + Right->predecessors()[0] == Left, + "CFG successor transfer must update both directions"); + Context.expect(CFG.disconnectBlock(*Left).has_value(), + "CFG block disconnection must remove incoming and outgoing edges"); + Context.expect(Left->successors().empty() && Right->predecessors().empty(), + "CFG disconnection must leave no stale reciprocal edges"); + + auto Instr = Function->createInstruction(rcc::Opcode::fromValue(9)); + auto NextInstr = Function->createInstruction(rcc::Opcode::fromValue(10)); + Context.expect(Instr && NextInstr, "instruction arena allocation must succeed"); + if (!Instr || !NextInstr) + return; + Context.expect(Entry->appendInstruction(**Instr).has_value(), + "first block instruction must be inserted"); + Context.expect(Entry->insertInstructionBefore(**Instr, **NextInstr).has_value(), + "instruction insertion before a stable node must succeed"); + Context.expect(Entry->firstInstruction() == *NextInstr && Entry->lastInstruction() == *Instr && + Entry->instructionCount() == 2, + "intrusive instruction list must preserve order and endpoints"); + + auto CrossBlockOperand = rcc::MachineOperand::basicBlock(*Foreign); + Context.expect(!(*Instr)->addOperand(std::move(CrossBlockOperand)), + "instruction must reject a block operand from another function"); + + auto MemOperand = Function->createMemOperand(makeLoadDescription()); + auto ReplacementMemOperand = Function->createMemOperand(makeLoadDescription()); + auto ForeignMemOperand = OtherFunction->createMemOperand(makeLoadDescription()); + Context.expect(MemOperand && ReplacementMemOperand && ForeignMemOperand, + "function arena must allocate immutable memory operands"); + if (MemOperand && ReplacementMemOperand && ForeignMemOperand) { + Context.expect((*Instr)->addMemOperand(**MemOperand).has_value(), + "instruction must accept a memory operand owned by its function"); + Context.expect((*Instr)->replaceMemOperand(0, **ReplacementMemOperand).has_value() && + (*Instr)->memOperands().front() == *ReplacementMemOperand, + "instruction must replace an owned memory operand in place"); + Context.expect(!(*Instr)->replaceMemOperand(1, **MemOperand) && + !(*Instr)->replaceMemOperand(0, **ForeignMemOperand) && + (*Instr)->memOperands().front() == *ReplacementMemOperand, + "invalid memory replacement must fail without mutation"); + Context.expect(!(*Instr)->addMemOperand(**ForeignMemOperand), + "instruction must reject a memory operand from another function"); + } + + const rcc::MachineBasicBlock *StableEntryAddress = Entry; + const rcc::MachineInstr *StableInstrAddress = *Instr; + for (std::uint32_t Index = 0; Index < 128; ++Index) { + auto NewBlock = Function->createBasicBlock(); + auto NewInstr = Function->createInstruction(rcc::Opcode::fromValue(Index + 100)); + Context.expect(NewBlock && NewInstr, "arena growth must continue to allocate objects"); + } + Context.expect(StableEntryAddress == Entry && StableInstrAddress == *Instr, + "arena growth must preserve block and instruction addresses"); + + Context.expect(Function->removeBlock(*Right).has_value(), + "disconnected empty block must be removable from layout"); + Context.expect(!Right->isLinked() && Function->blockCount() == 2, + "removed block must leave layout while retaining stable storage"); +} + +} // namespace + +int main() { + TestContext Context; + testOperandKinds(Context); + testRegisterInfoAndOperandMovement(Context); + testOwnershipCFGAndMemory(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineJumpTableInfoTest.cpp b/backend/unittests/CodeGen/MachineJumpTableInfoTest.cpp new file mode 100644 index 0000000..106a2a6 --- /dev/null +++ b/backend/unittests/CodeGen/MachineJumpTableInfoTest.cpp @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineJumpTableInfo.h" + +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineFunction.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <string> +#include <string_view> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> +createFunction(TestContext &Context, std::string Name, std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "machine function creation must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineBasicBlock *createBlock(TestContext &Context, + rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + Context.expect(Block.has_value() && Function.appendBlock(**Block).has_value(), + "jump table test block creation must succeed"); + return Block ? *Block : nullptr; +} + +[[nodiscard]] rcc::MachineJumpTableDesc tableDescription(rcc::MachineBasicBlock &First, + rcc::MachineBasicBlock &Second) { + rcc::MachineJumpTableDesc Description; + Description.Encoding = rcc::JumpTableEntryEncoding::GPRelative32BlockAddress; + Description.EntrySize = 4; + Description.Align = *rcc::Alignment::create(4); + Description.Targets = {&First, &Second, &Second}; + Description.Placement.MaximumDistance = 4096; + return Description; +} + +void testTableLifecycle(TestContext &Context) { + auto Function = createFunction(Context, "jump_tables", 1); + if (!Function) + return; + rcc::MachineBasicBlock *Entry = createBlock(Context, *Function); + rcc::MachineBasicBlock *Exit = createBlock(Context, *Function); + if (Entry == nullptr || Exit == nullptr) + return; + + auto First = Function->jumpTableInfo().createTable(tableDescription(*Entry, *Exit)); + auto TargetDescription = tableDescription(*Exit, *Entry); + TargetDescription.Encoding = rcc::JumpTableEntryEncoding::Target; + TargetDescription.TargetEncoding = 19; + TargetDescription.Placement.TargetFlags = 7; + auto Target = Function->jumpTableInfo().createTable(std::move(TargetDescription)); + Context.expect(First && Target && First->value() == 0 && Target->value() == 1 && + Function->jumpTableInfo().liveTableCount() == 2, + "valid jump tables must receive monotonic indices"); + if (!First || !Target) + return; + + const rcc::MachineJumpTableEntry *FirstEntry = Function->jumpTableInfo().table(*First); + Context.expect(FirstEntry != nullptr && FirstEntry->description()->Targets.size() == 3 && + FirstEntry->description()->Targets[1] == Exit && + FirstEntry->description()->Targets[2] == Exit, + "jump table target order and duplicate cases must be preserved"); + + Context.expect(Function->jumpTableInfo().eraseTable(*First).has_value() && + Function->jumpTableInfo().table(*First) == nullptr, + "erasing a jump table must leave a tombstone"); + auto CompressedDescription = tableDescription(*Entry, *Exit); + CompressedDescription.Encoding = rcc::JumpTableEntryEncoding::Custom32; + CompressedDescription.EntrySize = 4; + auto Compressed = Function->jumpTableInfo().createTable(std::move(CompressedDescription)); + Context.expect(Compressed && Compressed->value() == 2, + "jump table tombstone indices must never be reused"); + + auto Updated = tableDescription(*Entry, *Exit); + Updated.Encoding = rcc::JumpTableEntryEncoding::LabelDifference64; + Updated.EntrySize = 8; + Context.expect(Function->jumpTableInfo().updateTable(*Target, std::move(Updated)).has_value() && + Function->jumpTableInfo().verify().has_value() && + Function->jumpTableInfo().freeze().has_value() && + Function->jumpTableInfo().isFrozen(), + "valid jump table updates must verify and freeze"); + Context.expect( + !Function->jumpTableInfo().eraseTable(*Target) && + !Function->jumpTableInfo().updateTable(*Target, tableDescription(*Entry, *Exit)) && + !Function->jumpTableInfo().createTable(tableDescription(*Entry, *Exit)) && + !Function->jumpTableInfo().freeze(), + "frozen jump table info must reject every mutation"); +} + +void testInvalidTables(TestContext &Context) { + auto Function = createFunction(Context, "invalid_tables", 2); + auto Foreign = createFunction(Context, "foreign_tables", 3); + if (!Function || !Foreign) + return; + rcc::MachineBasicBlock *Block = createBlock(Context, *Function); + rcc::MachineBasicBlock *ForeignBlock = createBlock(Context, *Foreign); + if (Block == nullptr || ForeignBlock == nullptr) + return; + + rcc::MachineJumpTableDesc Empty; + Context.expect(!Function->jumpTableInfo().createTable(std::move(Empty)), + "jump table must reject an empty target list"); + + auto ForeignTarget = tableDescription(*Block, *ForeignBlock); + Context.expect(!Function->jumpTableInfo().createTable(std::move(ForeignTarget)), + "jump table must reject a foreign target block"); + + auto InvalidSize = tableDescription(*Block, *Block); + InvalidSize.EntrySize = 3; + Context.expect(!Function->jumpTableInfo().createTable(std::move(InvalidSize)), + "jump table entry size must be a bounded power of two"); + + auto MissingTargetEncoding = tableDescription(*Block, *Block); + MissingTargetEncoding.Encoding = rcc::JumpTableEntryEncoding::Target; + Context.expect(!Function->jumpTableInfo().createTable(std::move(MissingTargetEncoding)), + "target jump table must carry a target encoding ID"); + + auto UnexpectedTargetEncoding = tableDescription(*Block, *Block); + UnexpectedTargetEncoding.TargetEncoding = 1; + Context.expect(!Function->jumpTableInfo().createTable(std::move(UnexpectedTargetEncoding)), + "generic jump table encoding must reject target encoding IDs"); + + auto OversizedCompressed = tableDescription(*Block, *Block); + OversizedCompressed.Encoding = rcc::JumpTableEntryEncoding::Custom32; + OversizedCompressed.EntrySize = 8; + OversizedCompressed.EntrySize = 8; + Context.expect(!Function->jumpTableInfo().createTable(std::move(OversizedCompressed)), + "compressed jump table must reject oversized entries"); + + auto ZeroDistance = tableDescription(*Block, *Block); + ZeroDistance.Placement.MaximumDistance = 0; + Context.expect(!Function->jumpTableInfo().createTable(std::move(ZeroDistance)), + "jump table maximum distance must be nonzero"); + + auto LiveTable = Function->jumpTableInfo().createTable(tableDescription(*Block, *Block)); + Context.expect(LiveTable && Function->removeBlock(*Block).has_value() && + !Function->jumpTableInfo().verify(), + "jump table verification must detect a detached target"); +} + +} // namespace + +int main() { + TestContext Context; + testTableLifecycle(Context); + testInvalidTables(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineMetadataTest.cpp b/backend/unittests/CodeGen/MachineMetadataTest.cpp new file mode 100644 index 0000000..6669e2a --- /dev/null +++ b/backend/unittests/CodeGen/MachineMetadataTest.cpp @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineMetadata.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineFunctionCloner.h" +#include "rcc/CodeGen/MachineVerifier.h" +#include "rcc/CodeGen/TargetDescription.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t NopOpcode = 1; + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + rcc::InstructionDescriptor Nop; + Nop.InstructionOpcode = rcc::Opcode::fromValue(NopOpcode); + Nop.Name = "TEST_NOP"; + Nop.Flags = {rcc::InstructionFlag::ProducesCode}; + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Nop)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> +createFunction(TestContext &Context, std::uint64_t StableID, std::string Name) { + auto Function = rcc::MachineFunction::create(std::move(Name), + rcc::MachineFunctionStableID::fromValue(StableID)); + Context.expect(Function.has_value(), "machine function creation must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineMetadataElement +referenceElement(TestContext &Context, rcc::MachineMetadataReference Reference) { + auto Element = rcc::MachineMetadataElement::reference(Reference); + Context.expect(Element.has_value(), "valid machine metadata references must form elements"); + return Element ? std::move(*Element) : rcc::MachineMetadataElement{}; +} + +void testReferenceIdentity(TestContext &Context) { + const rcc::MachineMetadataReference Invalid; + const rcc::MachineMetadataReference EmbeddedZero = rcc::MachineMetadataReference::embeddedIR(0); + const rcc::MachineMetadataReference FunctionZero = + rcc::MachineMetadataReference::machineFunction(0); + + Context.expect(!Invalid.isValid() && EmbeddedZero.isValid() && FunctionZero.isValid(), + "metadata slot zero must be valid in both explicit reference scopes"); + Context.expect(EmbeddedZero != FunctionZero && + EmbeddedZero.scope() == rcc::MachineMetadataScope::EmbeddedIR && + FunctionZero.scope() == rcc::MachineMetadataScope::MachineFunction && + EmbeddedZero.slot() == 0 && FunctionZero.slot() == 0, + "embedded and function-local metadata slots must have distinct identities"); + Context.expect(!rcc::MachineMetadataElement::reference(Invalid), + "an invalid metadata reference must not form an element"); +} + +void testOrderedGraphValidation(TestContext &Context) { + rcc::MachineMetadataInfo Metadata; + rcc::MachineMetadataNode First{ + .Slot = 0, + .IsDistinct = true, + .Elements = + { + referenceElement(Context, rcc::MachineMetadataReference::embeddedIR(0)), + rcc::MachineMetadataElement::string(""), + referenceElement(Context, rcc::MachineMetadataReference::machineFunction(1)), + }, + }; + Context.expect(Metadata.addNode(std::move(First)).has_value() && !Metadata.verify(), + "forward references must be accepted before graph closure is verified"); + + rcc::MachineMetadataNode Second{ + .Slot = 1, + .IsDistinct = false, + .Elements = + { + referenceElement(Context, rcc::MachineMetadataReference::machineFunction(0)), + rcc::MachineMetadataElement::string("payload"), + }, + }; + Context.expect(Metadata.addNode(std::move(Second)).has_value() && Metadata.verify().has_value(), + "closed cyclic metadata tuple graphs must be valid"); + Context.expect(Metadata.nodes().size() == 2 && Metadata.nodes()[0].Slot == 0 && + Metadata.nodes()[0].IsDistinct && Metadata.nodes()[1].Slot == 1 && + Metadata.node(0) == &Metadata.nodes()[0] && + Metadata.node(1) == &Metadata.nodes()[1] && Metadata.node(2) == nullptr, + "metadata definitions and lookup must preserve deterministic source order"); + Context.expect(Metadata.nodes()[0].Elements[1].string() != nullptr && + Metadata.nodes()[0].Elements[1].string()->empty(), + "empty metadata strings must remain valid tuple elements"); + + const std::size_t OriginalSize = Metadata.nodes().size(); + Context.expect(!Metadata.addNode({.Slot = 0, .IsDistinct = false, .Elements = {}}) && + Metadata.nodes().size() == OriginalSize, + "duplicate metadata definitions must be rejected without changing the graph"); + + auto Clone = Metadata.clone(); + Context.expect(Clone.has_value() && *Clone == Metadata && + Clone->nodes().data() != Metadata.nodes().data(), + "metadata graphs must support independent deterministic deep cloning"); + + rcc::MachineMetadataInfo InvalidMetadata; + rcc::MachineMetadataNode InvalidNode; + InvalidNode.Elements.emplace_back(); + Context.expect(!InvalidMetadata.addNode(std::move(InvalidNode)) && + InvalidMetadata.nodes().empty(), + "invalid tuple elements must be rejected before publication"); +} + +void testFunctionVerifierAndClone(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 1, "metadata"); + if (!Target || Function == nullptr) + return; + + Context.expect( + Function->metadata() + .addNode({ + .Slot = 7, + .IsDistinct = false, + .Elements = + { + referenceElement(Context, rcc::MachineMetadataReference::embeddedIR(3)), + rcc::MachineMetadataElement::string("root"), + }, + }) + .has_value() && + Function->metadata() + .addNode({ + .Slot = 2, + .IsDistinct = true, + .Elements = + { + referenceElement(Context, + rcc::MachineMetadataReference::machineFunction(7)), + }, + }) + .has_value() && + rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "well-formed machine metadata must pass unified function verification"); + + auto Clone = + rcc::cloneMachineFunction(*Function, *Target, + { + .NewStableID = rcc::MachineFunctionStableID::fromValue(2), + .NewName = std::string("metadata.clone"), + }); + Context.expect(Clone.has_value() && Clone->ClonedFunction->metadata() == Function->metadata() && + Clone->ClonedFunction->metadata().nodes().data() != + Function->metadata().nodes().data(), + "function cloning must preserve an independent metadata graph"); + + auto InvalidFunction = createFunction(Context, 3, "invalid-metadata"); + if (InvalidFunction == nullptr) + return; + Context.expect( + InvalidFunction->metadata() + .addNode({ + .Slot = 0, + .Elements = + { + referenceElement(Context, + rcc::MachineMetadataReference::machineFunction(99)), + }, + }) + .has_value() && + !rcc::MachineVerifier::verify(*InvalidFunction, *Target), + "the unified verifier must reject undefined function-local metadata references"); +} + +} // namespace + +int main() { + TestContext Context; + testReferenceIdentity(Context); + testOrderedGraphValidation(Context); + testFunctionVerifierAndClone(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineOperandTest.cpp b/backend/unittests/CodeGen/MachineOperandTest.cpp new file mode 100644 index 0000000..96f1f19 --- /dev/null +++ b/backend/unittests/CodeGen/MachineOperandTest.cpp @@ -0,0 +1,524 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineOperand.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineCFIInfo.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineFunctionAttributes.h" +#include "rcc/CodeGen/MachineFunctionCloner.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineVerifier.h" +#include "rcc/CodeGen/RegisterMask.h" +#include "rcc/CodeGen/TargetDescription.h" + +#include <array> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <limits> +#include <memory> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t OperandOpcode = 1; + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::OperandDescriptor operandDescriptor(rcc::MachineOperand::Kind Kind) { + rcc::OperandDescriptor Descriptor; + Descriptor.Kinds = {Kind}; + return Descriptor; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + rcc::InstructionDescriptor Descriptor; + Descriptor.InstructionOpcode = rcc::Opcode::fromValue(OperandOpcode); + Descriptor.Name = "TEST_MIR_OPERANDS"; + Descriptor.Flags = {rcc::InstructionFlag::ProducesCode}; + Descriptor.Operands = { + operandDescriptor(rcc::MachineOperand::Kind::IntegerImmediate), + operandDescriptor(rcc::MachineOperand::Kind::LiveOutRegisterMask), + operandDescriptor(rcc::MachineOperand::Kind::CFIIndex), + operandDescriptor(rcc::MachineOperand::Kind::Intrinsic), + operandDescriptor(rcc::MachineOperand::Kind::Predicate), + operandDescriptor(rcc::MachineOperand::Kind::Predicate), + operandDescriptor(rcc::MachineOperand::Kind::ShuffleMask), + operandDescriptor(rcc::MachineOperand::Kind::DebugInstrRef), + operandDescriptor(rcc::MachineOperand::Kind::LaneMask), + }; + + auto Instructions = rcc::InstructionSet::create({std::move(Descriptor)}); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> +createFunction(TestContext &Context, std::uint64_t ID, std::string Name) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "machine function construction must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineBasicBlock *appendBlock(TestContext &Context, + rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + Context.expect(Block.has_value(), "machine basic block construction must succeed"); + if (!Block) + return nullptr; + auto AppendResult = Function.appendBlock(**Block); + Context.expect(AppendResult.has_value(), "machine basic block insertion must succeed"); + if (!AppendResult) + return nullptr; + return *Block; +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> +buildOperandInstruction(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::CFIInstructionIndex CFIIndex, + std::shared_ptr<const rcc::RegisterMask> LiveOutMask) { + const std::array<std::uint64_t, 2> Words{ + 0x0123456789abcdefULL, + 0xfedcba9876543210ULL, + }; + auto Integer = rcc::APInt::create(128, Words); + if (!Integer) + return std::unexpected(Integer.error()); + + rcc::MachineIRBuilder Builder(Function, Target); + auto InsertResult = Builder.setInsertPoint(Block); + if (!InsertResult) + return std::unexpected(InsertResult.error()); + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(OperandOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addIntegerImmediate(std::move(*Integer)); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addLiveOutRegisterMask(std::move(LiveOutMask)); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addCFIInstruction(CFIIndex); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addIntrinsic(rcc::IntrinsicID::fromValue(7)); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addIntegerPredicate(rcc::IntegerPredicate::SignedLessThan); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addFloatingPredicate(rcc::FloatingPredicate::UnorderedGreaterEqual); + !Result) { + return std::unexpected(Result.error()); + } + if (auto Result = Pending->addShuffleMask({1, rcc::ShuffleMaskOperand::Undef, 0}); !Result) { + return std::unexpected(Result.error()); + } + if (auto Result = Pending->addDebugInstrRef(9, 3); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addLaneMask(rcc::LaneMask::fromRaw(0x8000000000000001ULL)); !Result) { + return std::unexpected(Result.error()); + } + return Pending->build(); +} + +void testIntegerImmediate(TestContext &Context) { + const std::array<std::uint64_t, 2> Words{ + 0x0123456789abcdefULL, + 0x0000000000000001ULL, + }; + auto Integer = rcc::APInt::create(65, Words); + Context.expect(Integer.has_value() && Integer->bitWidth() == 65 && Integer->words().size() == 2 && + Integer->words()[0] == Words[0] && Integer->words()[1] == Words[1] && + Integer->isNegative() && !Integer->isZero(), + "arbitrary-width integer must preserve exact little-endian words"); + + Context.expect( + !rcc::APInt::create(0, std::span<const std::uint64_t>{}) && + !rcc::APInt::create(rcc::APInt::MaxBitWidth + 1, std::span<const std::uint64_t>{}), + "integer immediate must reject unsupported bit widths"); + const std::array<std::uint64_t, 2> TwoWords{1, 0}; + const std::array<std::uint64_t, 1> HighBits{0x100}; + Context.expect(!rcc::APInt::create(64, TwoWords) && !rcc::APInt::create(8, HighBits), + "integer immediate must reject word-count and high-bit mismatches"); + + auto UnsignedMaximum = rcc::APInt::fromUnsigned(8, 255); + auto UnsignedOverflow = rcc::APInt::fromUnsigned(8, 256); + auto SignedMinimum = rcc::APInt::fromSigned(8, -128); + auto SignedMaximum = rcc::APInt::fromSigned(8, 127); + auto SignedUnderflow = rcc::APInt::fromSigned(8, -129); + auto SignedOverflow = rcc::APInt::fromSigned(8, 128); + Context.expect(UnsignedMaximum && UnsignedMaximum->words()[0] == 255 && !UnsignedOverflow && + SignedMinimum && SignedMinimum->words()[0] == 128 && SignedMaximum && + SignedMaximum->words()[0] == 127 && !SignedUnderflow && !SignedOverflow, + "integer immediate scalar factories must enforce signedness and width"); + + auto SignExtended = rcc::APInt::fromSigned(128, -1); + auto Zero = rcc::APInt::fromUnsigned(128, 0); + Context.expect(SignExtended && SignExtended->isNegative() && SignExtended->words().size() == 2 && + SignExtended->words()[0] == std::numeric_limits<std::uint64_t>::max() && + SignExtended->words()[1] == std::numeric_limits<std::uint64_t>::max() && + Zero && Zero->isZero() && !Zero->isNegative(), + "wide scalar factories must sign-extend and zero-extend exactly"); + + if (Integer) { + auto Operand = rcc::MachineOperand::integerImmediate(std::move(*Integer), 5); + const auto *Payload = Operand.getIf<rcc::IntegerImmediateOperand>(); + Context.expect(Operand.kind() == rcc::MachineOperand::Kind::IntegerImmediate && + Operand.targetFlags() == 5 && Payload != nullptr && + Payload->Value.bitWidth() == 65, + "typed integer operand must retain its distinct kind and target flags"); + } +} + +void testOperandFactories(TestContext &Context) { + auto Mask = rcc::test::createTestCallPreservedMask(); + auto LiveOut = rcc::MachineOperand::liveOutRegisterMask(Mask, 3); + Context.expect(LiveOut && LiveOut->kind() == rcc::MachineOperand::Kind::LiveOutRegisterMask && + LiveOut->targetFlags() == 3 && + LiveOut->getIf<rcc::LiveOutRegisterMaskOperand>()->Mask == Mask && + !rcc::MachineOperand::liveOutRegisterMask(nullptr), + "live-out mask must be distinct from a call-preserved register mask"); + + auto CFI = rcc::MachineOperand::cfiIndex(rcc::CFIInstructionIndex::fromValue(0), 4); + Context.expect(CFI.kind() == rcc::MachineOperand::Kind::CFIIndex && + CFI.getIf<rcc::CFIIndexOperand>()->Index.value() == 0 && + CFI.targetFlags() == 4, + "CFI operand must preserve index zero and target flags"); + + auto Intrinsic = rcc::MachineOperand::intrinsic(rcc::IntrinsicID::fromValue(17), 6); + Context.expect(Intrinsic && Intrinsic->kind() == rcc::MachineOperand::Kind::Intrinsic && + Intrinsic->getIf<rcc::IntrinsicOperand>()->ID.value() == 17 && + Intrinsic->targetFlags() == 6 && + !rcc::MachineOperand::intrinsic(rcc::IntrinsicID{}), + "intrinsic operand must use a nonzero strong ID"); + + auto IntegerPredicate = + rcc::MachineOperand::integerPredicate(rcc::IntegerPredicate::UnsignedGreaterEqual); + auto FloatingPredicate = + rcc::MachineOperand::floatingPredicate(rcc::FloatingPredicate::OrderedNotEqual); + Context.expect( + IntegerPredicate && FloatingPredicate && + IntegerPredicate->getIf<rcc::PredicateOperand>()->integerPredicate() != nullptr && + FloatingPredicate->getIf<rcc::PredicateOperand>()->floatingPredicate() != nullptr && + !rcc::MachineOperand::integerPredicate(rcc::IntegerPredicate::Count) && + !rcc::MachineOperand::floatingPredicate(rcc::FloatingPredicate::Count), + "integer and floating predicates must share one tagged operand without invalid sentinels"); + + auto Shuffle = rcc::MachineOperand::shuffleMask({1, rcc::ShuffleMaskOperand::Undef, -2, 0}, 7); + Context.expect(Shuffle && Shuffle->kind() == rcc::MachineOperand::Kind::ShuffleMask && + Shuffle->getIf<rcc::ShuffleMaskOperand>()->Elements == + std::vector<std::int32_t>({1, -1, -2, 0}) && + Shuffle->targetFlags() == 7 && !rcc::MachineOperand::shuffleMask({0}), + "shuffle mask must preserve int32 elements and the canonical undef sentinel"); + + auto Reference = rcc::MachineOperand::debugInstrRef(9, 1'000'000, 8); + auto ZeroReference = rcc::MachineOperand::debugInstrRef(0, 0); + Context.expect( + Reference && Reference->kind() == rcc::MachineOperand::Kind::DebugInstrRef && + Reference->getIf<rcc::DebugInstrRefOperand>()->InstructionNumber == 9 && + Reference->getIf<rcc::DebugInstrRefOperand>()->OperandNumber == 1'000'000 && + Reference->targetFlags() == 8 && ZeroReference && + ZeroReference->getIf<rcc::DebugInstrRefOperand>()->InstructionNumber == 0 && + !rcc::MachineOperand::debugInstrRef( + static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max()) + 1, 0) && + !rcc::MachineOperand::debugInstrRef( + 1, static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max()) + 1), + "debug instruction reference must preserve zero and enforce its two uint32 domains"); + + auto LaneMask = rcc::MachineOperand::laneMask(rcc::LaneMask::fromRaw(0xf00000000000000fULL), 9); + Context.expect(LaneMask.kind() == rcc::MachineOperand::Kind::LaneMask && + LaneMask.getIf<rcc::LaneMaskOperand>()->Mask.rawValue() == + 0xf00000000000000fULL && + LaneMask.targetFlags() == 9, + "lane-mask operand must preserve all 64 bits"); + + auto Clone = LaneMask.cloneDetached(); + Context.expect(Clone.parent() == nullptr && Clone.kind() == LaneMask.kind() && + Clone.targetFlags() == LaneMask.targetFlags() && + Clone.getIf<rcc::LaneMaskOperand>()->Mask == + LaneMask.getIf<rcc::LaneMaskOperand>()->Mask, + "detached operand cloning must preserve the new payload kinds"); +} + +void testFrameIndexRewrite(TestContext &Context) { + auto Function = createFunction(Context, 5, "frame-index-rewrite"); + if (!Function) + return; + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + auto Align = rcc::Alignment::create(8); + if (Block == nullptr || !Align) + return; + + rcc::StackObjectDesc Description; + Description.Kind = rcc::StackObjectKind::SpillSlot; + Description.Size = 8; + Description.Align = *Align; + auto First = Function->frameInfo().createObject(Description); + auto Second = Function->frameInfo().createObject(Description); + Context.expect(First.has_value() && Second.has_value(), + "frame-index rewrite objects must be created"); + if (!First || !Second) + return; + + auto Instruction = Function->createInstruction(rcc::Opcode::fromValue(OperandOpcode)); + Context.expect(Instruction.has_value() && + (*Instruction)->addOperand(rcc::MachineOperand::frameIndex(*First, 4)) && + Block->appendInstruction(**Instruction), + "frame-index rewrite operand must be linked to its function"); + if (!Instruction || (*Instruction)->operandCount() == 0) + return; + rcc::MachineOperand *Operand = (*Instruction)->operandAt(0); + Context.expect(Operand != nullptr && Operand->setFrameIndex(*Second).has_value() && + Operand->getIf<rcc::FrameIndexOperand>()->Index == *Second && + Operand->getIf<rcc::FrameIndexOperand>()->Offset == 4, + "setFrameIndex must rewrite only the object identity"); + + const rcc::FrameIndex Unknown = rcc::FrameIndex::fromValue(99); + Context.expect(Operand != nullptr && !Operand->setFrameIndex(Unknown) && + Operand->getIf<rcc::FrameIndexOperand>()->Index == *Second, + "setFrameIndex must reject an unknown attached object atomically"); + auto Immediate = rcc::MachineOperand::immediate(0); + Context.expect(!Immediate.setFrameIndex(*First), + "setFrameIndex must reject non-frame-index operands"); +} + +void testCFIInfo(TestContext &Context) { + const auto R1 = rcc::DwarfRegisterNumber::fromValue(1); + const auto R2 = rcc::DwarfRegisterNumber::fromValue(2); + const auto R3 = rcc::DwarfRegisterNumber::fromValue(3); + rcc::MachineCFIInfo Info; + + auto Add = [&Context, &Info](rcc::MachineCFIInstruction Instruction) { + auto Index = Info.addInstruction(std::move(Instruction)); + Context.expect(Index.has_value(), "valid CFI instruction insertion must succeed"); + }; + auto AddExpected = [&Context, &Info](rcc::Expected<rcc::MachineCFIInstruction> Instruction) { + Context.expect(Instruction.has_value(), "valid structured CFI factory must succeed"); + if (!Instruction) + return; + auto Index = Info.addInstruction(std::move(*Instruction)); + Context.expect(Index.has_value(), "valid structured CFI insertion must succeed"); + }; + + Add(rcc::MachineCFIInstruction::sameValue(R1)); + Add(rcc::MachineCFIInstruction::rememberState()); + Add(rcc::MachineCFIInstruction::restoreState()); + Add(rcc::MachineCFIInstruction::offset(R1, -8)); + Add(rcc::MachineCFIInstruction::llvmDefAddressSpaceCFA(R1, 16, 5)); + Add(rcc::MachineCFIInstruction::defCFARegister(R2)); + Add(rcc::MachineCFIInstruction::defCFAOffset(32)); + Add(rcc::MachineCFIInstruction::defCFA(R2, 24)); + Add(rcc::MachineCFIInstruction::relativeOffset(R3, -16)); + Add(rcc::MachineCFIInstruction::adjustCFAOffset(8)); + AddExpected(rcc::MachineCFIInstruction::escape({0x0c, 0x07, 0x08})); + Add(rcc::MachineCFIInstruction::restore(R1)); + Add(rcc::MachineCFIInstruction::undefined(R2)); + Add(rcc::MachineCFIInstruction::registerRule(R1, R3)); + Add(rcc::MachineCFIInstruction::windowSave()); + Add(rcc::MachineCFIInstruction::negateReturnAddressState()); + Add(rcc::MachineCFIInstruction::negateReturnAddressStateWithPC()); + Add(rcc::MachineCFIInstruction::gnuArgsSize(48)); + AddExpected(rcc::MachineCFIInstruction::label("cfi.label")); + Add(rcc::MachineCFIInstruction::valueOffset(R3, 64)); + Add(rcc::MachineCFIInstruction::llvmRegisterPair({ + .Register = R1, + .FirstRegister = R2, + .FirstSizeInBits = 32, + .SecondRegister = R3, + .SecondSizeInBits = 32, + })); + AddExpected(rcc::MachineCFIInstruction::llvmVectorRegisters({ + .Register = R1, + .VectorRegisters = {{R2, 0, 64}, {R3, 1, 64}}, + })); + Add(rcc::MachineCFIInstruction::llvmVectorOffset({ + .Register = R1, + .RegisterSizeInBits = 64, + .MaskRegister = R2, + .MaskRegisterSizeInBits = 8, + .Offset = -32, + })); + Add(rcc::MachineCFIInstruction::llvmVectorRegisterMask({ + .Register = R1, + .SpillRegister = R2, + .SpillRegisterLaneSizeInBits = 64, + .MaskRegister = R3, + .MaskRegisterSizeInBits = 8, + })); + + Context.expect(Info.instructions().size() == 24 && Info.verify().has_value() && + Info.instruction(rcc::CFIInstructionIndex::fromValue(0))->operation() == + rcc::MachineCFIOperation::SameValue && + Info.instruction(rcc::CFIInstructionIndex::fromValue(23))->operation() == + rcc::MachineCFIOperation::LLVMVectorRegisterMask && + Info.instruction(rcc::CFIInstructionIndex::fromValue(24)) == nullptr, + "CFI table must preserve deterministic zero-based definition order"); + + Context.expect(!rcc::MachineCFIInstruction::escape({}) && + !rcc::MachineCFIInstruction::label("") && + !rcc::MachineCFIInstruction::llvmVectorRegisters({ + .Register = R1, + .VectorRegisters = {}, + }), + "CFI factories must reject structurally incomplete payloads"); + + auto ZeroWidthVectorRegisters = rcc::MachineCFIInstruction::llvmVectorRegisters({ + .Register = R1, + .VectorRegisters = {{R2, 0, 0}}, + }); + Context.expect( + rcc::MachineCFIInstruction::llvmRegisterPair({ + .Register = R1, + .FirstRegister = R2, + .FirstSizeInBits = 0, + .SecondRegister = R3, + .SecondSizeInBits = 0, + }) + .verify() && + ZeroWidthVectorRegisters && ZeroWidthVectorRegisters->verify() && + rcc::MachineCFIInstruction::llvmVectorOffset({ + .Register = R1, + .RegisterSizeInBits = 0, + .MaskRegister = R2, + .MaskRegisterSizeInBits = 0, + .Offset = 0, + }) + .verify() && + rcc::MachineCFIInstruction::llvmVectorRegisterMask({ + .Register = R1, + .SpillRegister = R2, + .SpillRegisterLaneSizeInBits = 0, + .MaskRegister = R3, + .MaskRegisterSizeInBits = 0, + }) + .verify(), + "CFI unsigned size fields must preserve every value accepted by the fixed MIR grammar"); + + auto Clone = Info.clone(); + Context.expect(Clone && *Clone == Info, "CFI information clone must preserve every rule"); + if (Clone) { + auto Extra = Clone->addInstruction(rcc::MachineCFIInstruction::rememberState()); + Context.expect(Extra && Clone->instructions().size() == 25 && Info.instructions().size() == 24, + "CFI information clone must own independent storage"); + } +} + +void testBuilderVerifierAndClone(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "operand test target construction must succeed"); + auto Function = createFunction(Context, 1, "operand-integration"); + if (!Target || !Function) + return; + Function->attributes().set(rcc::MachineFunctionAttribute::DebugInstrRef); + auto CFIIndex = Function->cfiInfo().addInstruction(rcc::MachineCFIInstruction::defCFAOffset(16)); + Context.expect(CFIIndex.has_value(), "function CFI instruction insertion must succeed"); + rcc::MachineBasicBlock *Block = appendBlock(Context, *Function); + if (!CFIIndex || Block == nullptr) + return; + + auto Instruction = buildOperandInstruction(*Function, *Target, *Block, *CFIIndex, + rcc::test::createTestCallPreservedMask()); + Context.expect(Instruction.has_value() && + rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "builder and verifier must accept every newly modeled operand kind"); + if (!Instruction) + return; + + const auto Operands = (*Instruction)->operands(); + Context.expect( + Operands.size() == 9 && + Operands[0].getIf<rcc::IntegerImmediateOperand>()->Value.bitWidth() == 128 && + Operands[1].kind() == rcc::MachineOperand::Kind::LiveOutRegisterMask && + Operands[2].getIf<rcc::CFIIndexOperand>()->Index == *CFIIndex && + Operands[3].getIf<rcc::IntrinsicOperand>()->ID.value() == 7 && + *Operands[4].getIf<rcc::PredicateOperand>()->integerPredicate() == + rcc::IntegerPredicate::SignedLessThan && + *Operands[5].getIf<rcc::PredicateOperand>()->floatingPredicate() == + rcc::FloatingPredicate::UnorderedGreaterEqual && + Operands[6].getIf<rcc::ShuffleMaskOperand>()->Elements[1] == + rcc::ShuffleMaskOperand::Undef && + Operands[7].getIf<rcc::DebugInstrRefOperand>()->InstructionNumber == 9 && + Operands[8].getIf<rcc::LaneMaskOperand>()->Mask.rawValue() == 0x8000000000000001ULL, + "built instruction must expose all new payloads without string or immediate overloading"); + + auto Clone = + rcc::cloneMachineFunction(*Function, *Target, + { + .NewStableID = rcc::MachineFunctionStableID::fromValue(2), + .NewName = std::string("operand-integration.clone"), + }); + Context.expect(Clone.has_value(), "operand-rich function must support explicit deep cloning"); + if (Clone) { + const rcc::MachineInstr *ClonedInstruction = + Clone->ClonedFunction->firstBlock()->firstInstruction(); + Context.expect( + Clone->ClonedFunction->cfiInfo() == Function->cfiInfo() && ClonedInstruction != nullptr && + ClonedInstruction->operands().size() == 9 && + ClonedInstruction->operands()[0].getIf<rcc::IntegerImmediateOperand>()->Value == + Operands[0].getIf<rcc::IntegerImmediateOperand>()->Value && + ClonedInstruction->operands()[6].getIf<rcc::ShuffleMaskOperand>()->Elements == + Operands[6].getIf<rcc::ShuffleMaskOperand>()->Elements, + "deep clone must preserve CFI indices and owned variable-size operand data"); + } + + auto MissingCFIFunction = createFunction(Context, 3, "missing-cfi"); + if (!MissingCFIFunction) + return; + rcc::MachineBasicBlock *MissingCFIBlock = appendBlock(Context, *MissingCFIFunction); + if (MissingCFIBlock == nullptr) + return; + auto MissingCFI = buildOperandInstruction(*MissingCFIFunction, *Target, *MissingCFIBlock, + rcc::CFIInstructionIndex::fromValue(0), + rcc::test::createTestCallPreservedMask()); + Context.expect(!MissingCFI, "builder must reject an operand that references an unknown CFI rule"); + + auto InvalidMaskFunction = createFunction(Context, 4, "invalid-liveout"); + if (!InvalidMaskFunction) + return; + auto ValidCFI = + InvalidMaskFunction->cfiInfo().addInstruction(rcc::MachineCFIInstruction::rememberState()); + rcc::MachineBasicBlock *InvalidMaskBlock = appendBlock(Context, *InvalidMaskFunction); + if (!ValidCFI || InvalidMaskBlock == nullptr) + return; + auto InvalidMask = std::make_shared<const rcc::RegisterMask>( + std::vector<std::uint64_t>{std::uint64_t{1} << 63U}); + auto InvalidLiveOut = buildOperandInstruction(*InvalidMaskFunction, *Target, *InvalidMaskBlock, + *ValidCFI, std::move(InvalidMask)); + Context.expect(!InvalidLiveOut, + "target validation must reject a live-out mask with unknown physical registers"); +} + +} // namespace + +int main() { + TestContext Context; + testIntegerImmediate(Context); + testOperandFactories(Context); + testFrameIndexRewrite(Context); + testCFIInfo(Context); + testBuilderVerifierAndClone(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineOptimizationTest.cpp b/backend/unittests/CodeGen/MachineOptimizationTest.cpp new file mode 100644 index 0000000..cffd4e1 --- /dev/null +++ b/backend/unittests/CodeGen/MachineOptimizationTest.cpp @@ -0,0 +1,798 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "RISCV.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/LiveVariables.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineCFGUpdater.h" +#include "rcc/CodeGen/MachineCSE.h" +#include "rcc/CodeGen/MachineCombiner.h" +#include "rcc/CodeGen/MachineDCE.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineLICM.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/CodeGen/MachineMemoryFolding.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/MachineSink.h" +#include "rcc/CodeGen/TargetMemoryFolding.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#include <string> +#include <string_view> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +struct Fixture final { + std::unique_ptr<rcc::MachineFunction> Function; + rcc::MachineBasicBlock *Entry = nullptr; +}; + +[[nodiscard]] rcc::Opcode opcode(rcc::riscv::InstructionOpcode Value) noexcept; + +class TestMemoryFoldingInfo final : public rcc::TargetMemoryFoldingInfo { +public: + explicit TestMemoryFoldingInfo(bool DropMemory = false) noexcept : DropMemory(DropMemory) {} + + [[nodiscard]] rcc::Expected<bool> + canFoldMemoryOperand(const rcc::MachineInstr &Consumer, std::size_t OperandIndex, + const rcc::MachineInstr &MemoryInstruction, const rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return Consumer.opcode() == opcode(rcc::riscv::InstructionOpcode::ADDI) && OperandIndex == 1 && + MemoryInstruction.opcode() == opcode(rcc::riscv::InstructionOpcode::LD); + } + + [[nodiscard]] rcc::Expected<rcc::MachineInstr *> + foldMemoryOperand(rcc::MachineInstr &Consumer, std::size_t, rcc::MachineInstr &MemoryInstruction, + rcc::MachineFunction &Function, + const rcc::TargetDescription &Target) const override { + const rcc::MachineOperand *DestinationOperand = Consumer.operandAt(0); + const rcc::MachineOperand *BaseOperand = MemoryInstruction.operandAt(1); + const rcc::MachineOperand *OffsetOperand = MemoryInstruction.operandAt(2); + const rcc::Register *Destination = + DestinationOperand == nullptr ? nullptr : DestinationOperand->getIf<rcc::Register>(); + const rcc::Register *Base = + BaseOperand == nullptr ? nullptr : BaseOperand->getIf<rcc::Register>(); + const std::int64_t *Offset = + OffsetOperand == nullptr ? nullptr : OffsetOperand->getIf<std::int64_t>(); + rcc::MachineBasicBlock *Block = Consumer.parent(); + if (Destination == nullptr || Base == nullptr || Offset == nullptr || Block == nullptr || + MemoryInstruction.parent() != Block) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "test memory fold received an invalid candidate"); + } + + rcc::MachineIRBuilder Builder(Function, Target); + if (auto Point = Builder.setInsertPointBefore(Consumer); !Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::LD)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Added = Pending->addDef(*Destination); !Added) + return std::unexpected(Added.error()); + if (auto Added = Pending->addUse(*Base); !Added) + return std::unexpected(Added.error()); + if (auto Added = Pending->addImmediate(*Offset); !Added) + return std::unexpected(Added.error()); + for (const rcc::MachineMemOperand *Memory : MemoryInstruction.memOperands()) { + if (Memory == nullptr) + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "test memory fold received a null MMO"); + if (auto Added = Pending->addMemOperand(*Memory); !Added) + return std::unexpected(Added.error()); + } + auto Folded = Pending->build(); + if (!Folded) + return std::unexpected(Folded.error()); + if (DropMemory) { + while (!(*Folded)->memOperands().empty()) { + if (auto Erased = (*Folded)->eraseMemOperand(0); !Erased) + return std::unexpected(Erased.error()); + } + } + if (auto Removed = Block->removeInstruction(MemoryInstruction); !Removed) + return std::unexpected(Removed.error()); + if (auto Removed = Block->removeInstruction(Consumer); !Removed) + return std::unexpected(Removed.error()); + return *Folded; + } + + [[nodiscard]] rcc::Expected<bool> + unfoldMemoryOperand(rcc::MachineInstr &, rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return false; + } + + [[nodiscard]] rcc::Expected<std::optional<rcc::MemorySize>> + getFoldedMemorySize(const rcc::MachineInstr &, const rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + auto Size = rcc::MemorySize::fixed(8); + if (!Size) + return std::unexpected(Size.error()); + return std::optional<rcc::MemorySize>(*Size); + } + +private: + bool DropMemory; +}; + +[[nodiscard]] rcc::Opcode opcode(rcc::riscv::InstructionOpcode Value) noexcept { + return rcc::Opcode::fromValue(static_cast<std::uint32_t>(Value)); +} + +[[nodiscard]] Fixture makeFixture(TestContext &Context, std::string Name, std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "optimization fixture function creation must succeed"); + if (!Function) + return {}; + auto Entry = (*Function)->createBasicBlock(); + Context.expect(Entry.has_value(), "optimization fixture block creation must succeed"); + if (!Entry || !(*Function)->appendBlock(**Entry)) + return {}; + (*Function)->properties().set(rcc::MachineFunctionProperty::IsSSA); + (*Function)->properties().set(rcc::MachineFunctionProperty::Legalized); + (*Function)->properties().set(rcc::MachineFunctionProperty::RegBankSelected); + (*Function)->properties().set(rcc::MachineFunctionProperty::Selected); + return {std::move(*Function), *Entry}; +} + +void setPostRAProperties(rcc::MachineFunction &Function) { + Function.properties().clear(rcc::MachineFunctionProperty::IsSSA); + Function.properties().set(rcc::MachineFunctionProperty::NoPHIs); + Function.properties().set(rcc::MachineFunctionProperty::TiedOpsRewritten); + Function.properties().set(rcc::MachineFunctionProperty::NoVRegs); + Function.properties().set(rcc::MachineFunctionProperty::TracksLiveness); +} + +[[nodiscard]] rcc::Register makeGPR(TestContext &Context, rcc::MachineFunction &Function) { + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Reg = Type ? Function.registerInfo().createVirtualRegister(*Type) + : rcc::Expected<rcc::Register>(std::unexpected(rcc::Error( + rcc::ErrorCode::InternalError, "test LowLevelType creation failed"))); + Context.expect(Reg.has_value(), "optimization virtual register creation must succeed"); + if (!Reg) + return {}; + const auto Bank = + rcc::RegisterBankID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterBank::GPR)); + const auto Class = + rcc::RegisterClassID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterClass::GPR)); + Context.expect(Function.registerInfo().setRegisterBank(*Reg, Bank).has_value() && + Function.registerInfo().setRegisterClass(*Reg, Class).has_value(), + "optimization register constraints must be assigned"); + return *Reg; +} + +[[nodiscard]] rcc::PhysicalRegister physical(TestContext &Context, + rcc::riscv::PhysicalRegisterID ID) { + auto Reg = rcc::riscv::physicalRegister(ID); + Context.expect(Reg.has_value(), "optimization physical register lookup must succeed"); + return Reg.value_or(rcc::PhysicalRegister{}); +} + +[[nodiscard]] rcc::MachineInstr *buildADDI(TestContext &Context, rcc::MachineIRBuilder &Builder, + rcc::Register Destination, rcc::Register Source, + std::int64_t Immediate) { + auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::ADDI)); + if (!Pending || !Pending->addDef(Destination) || !Pending->addUse(Source) || + !Pending->addImmediate(Immediate)) { + Context.expect(false, "ADDI construction must succeed"); + return nullptr; + } + auto Built = Pending->build(); + Context.expect(Built.has_value(), "ADDI insertion must succeed"); + return Built.value_or(nullptr); +} + +[[nodiscard]] rcc::MachineInstr *buildADD(TestContext &Context, rcc::MachineIRBuilder &Builder, + rcc::Register Destination, rcc::Register Left, + rcc::Register Right) { + auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::ADD)); + if (!Pending || !Pending->addDef(Destination) || !Pending->addUse(Left) || + !Pending->addUse(Right)) { + Context.expect(false, "ADD construction must succeed"); + return nullptr; + } + auto Built = Pending->build(); + Context.expect(Built.has_value(), "ADD insertion must succeed"); + return Built.value_or(nullptr); +} + +[[nodiscard]] const rcc::MachineMemOperand * +makeMemory(TestContext &Context, rcc::MachineFunction &Function, bool Store, bool Invariant) { + auto Size = rcc::MemorySize::fixed(8); + auto Align = rcc::Alignment::create(8); + Context.expect(Size.has_value() && Align.has_value(), "memory shape creation must succeed"); + if (!Size || !Align) + return nullptr; + rcc::MemoryAccessFlags Flags{Store ? rcc::MemoryAccessFlag::Store : rcc::MemoryAccessFlag::Load}; + if (Invariant) { + Flags.set(rcc::MemoryAccessFlag::Invariant); + Flags.set(rcc::MemoryAccessFlag::Dereferenceable); + } + rcc::MachineMemOperandDesc Description; + Description.Accesses = Flags; + Description.Size = *Size; + Description.Align = *Align; + Description.Base = Invariant ? rcc::MemoryBase::constantPoolPseudo() : rcc::MemoryBase::stack(); + auto Memory = Function.createMemOperand(std::move(Description)); + Context.expect(Memory.has_value(), "memory operand creation must succeed"); + return Memory.value_or(nullptr); +} + +[[nodiscard]] rcc::MachineInstr *buildStore(TestContext &Context, rcc::MachineIRBuilder &Builder, + rcc::Register Value, rcc::PhysicalRegister StackPointer, + const rcc::MachineMemOperand &Memory) { + auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::SD)); + if (!Pending || !Pending->addUse(Value) || + !Pending->addUse(rcc::Register::fromPhysical(StackPointer)) || !Pending->addImmediate(0) || + !Pending->addMemOperand(Memory)) { + Context.expect(false, "store construction must succeed"); + return nullptr; + } + auto Built = Pending->build(); + Context.expect(Built.has_value(), "store insertion must succeed"); + return Built.value_or(nullptr); +} + +[[nodiscard]] rcc::MachineInstr *buildLoad(TestContext &Context, rcc::MachineIRBuilder &Builder, + rcc::Register Destination, rcc::Register Address, + const rcc::MachineMemOperand &Memory) { + auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::LD)); + if (!Pending || !Pending->addDef(Destination) || !Pending->addUse(Address) || + !Pending->addImmediate(0) || !Pending->addMemOperand(Memory)) { + Context.expect(false, "load construction must succeed"); + return nullptr; + } + auto Built = Pending->build(); + Context.expect(Built.has_value(), "load insertion must succeed"); + return Built.value_or(nullptr); +} + +void testDCEFixedPointAndDebugUses(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "dce", 1); + if (!Fixture.Function || Fixture.Entry == nullptr) + return; + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Entry).has_value(), + "DCE insert point must be established"); + const rcc::PhysicalRegister X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + const rcc::PhysicalRegister X2 = physical(Context, rcc::riscv::PhysicalRegisterID::X2); + + const rcc::Register LiveRoot = makeGPR(Context, *Fixture.Function); + const rcc::Register LiveValue = makeGPR(Context, *Fixture.Function); + const rcc::Register DeadRoot = makeGPR(Context, *Fixture.Function); + const rcc::Register DeadValue = makeGPR(Context, *Fixture.Function); + (void)buildADDI(Context, Builder, LiveRoot, rcc::Register::fromPhysical(X0), 1); + (void)buildADDI(Context, Builder, LiveValue, LiveRoot, 2); + (void)buildADDI(Context, Builder, DeadRoot, rcc::Register::fromPhysical(X0), 3); + (void)buildADDI(Context, Builder, DeadValue, DeadRoot, 4); + + auto Debug = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::DBG_VALUE)); + Context.expect(Debug.has_value() && + Debug->addUse(DeadValue, {rcc::RegisterOperandFlag::Debug}).has_value() && + Debug->build().has_value(), + "debug-only use construction must succeed"); + const rcc::MachineMemOperand *Memory = makeMemory(Context, *Fixture.Function, true, false); + if (Memory == nullptr) + return; + rcc::MachineInstr *Store = buildStore(Context, Builder, LiveValue, X2, *Memory); + rcc::MachineInstr *FrameSetup = buildADDI(Context, Builder, rcc::Register::fromPhysical(X2), + rcc::Register::fromPhysical(X2), -16); + rcc::MachineInstr *FrameDestroy = buildADDI(Context, Builder, rcc::Register::fromPhysical(X2), + rcc::Register::fromPhysical(X2), 16); + Context.expect(FrameSetup != nullptr && FrameDestroy != nullptr, + "DCE frame instruction construction must succeed"); + if (FrameSetup == nullptr || FrameDestroy == nullptr) + return; + FrameSetup->setFlags({rcc::MachineInstrFlag::FrameSetup}); + FrameDestroy->setFlags({rcc::MachineInstrFlag::FrameDestroy}); + for (rcc::MachineInstr *Instruction : {FrameSetup, FrameDestroy}) { + rcc::MachineOperand *Definition = Instruction->operandAt(0); + if (Definition == nullptr) + return; + rcc::RegisterOperandFlags Flags = Definition->registerFlags(); + Flags.set(rcc::RegisterOperandFlag::Dead); + Context.expect(Definition->setRegisterFlags(Flags).has_value(), + "DCE frame definition must accept a dead marker"); + } + const std::size_t Before = Fixture.Entry->instructionCount(); + + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineDCEPass Pass; + auto Result = Pass.run(*Fixture.Function, Target, Analyses); + if (!Result) + std::cerr << "machine DCE failure: " << Result.error().message() << '\n'; + Context.expect(Result.has_value() && Result->Changed, + "machine DCE must remove the debug-only dead chain"); + Context.expect(Fixture.Entry->instructionCount() + 2 == Before, + "machine DCE must reach a fixed point across the dead chain"); + Context.expect(Store != nullptr && Store->isLinked(), "machine DCE must preserve a store"); + Context.expect(FrameSetup->isLinked() && FrameDestroy->isLinked(), + "machine DCE must preserve frame setup and destroy instructions"); + Context.expect(!Fixture.Function->registerInfo().isKnownVirtualRegister(DeadRoot) && + !Fixture.Function->registerInfo().isKnownVirtualRegister(DeadValue), + "machine DCE must retire removed virtual registers"); + rcc::MachineInstr *DebugInstruction = nullptr; + for (rcc::MachineInstr *Instruction = Fixture.Entry->firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + if (Instruction->opcode() == rcc::opcode(rcc::GenericOpcode::DBG_VALUE)) + DebugInstruction = Instruction; + } + const rcc::MachineOperand *DebugOperand = + DebugInstruction == nullptr ? nullptr : DebugInstruction->operandAt(0); + Context.expect(DebugOperand != nullptr && DebugOperand->registerValue().has_value() && + DebugOperand->registerValue()->isNoRegister(), + "machine DCE must clear a dangling debug register use"); +} + +void testMachineCombinerIdentityRules(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "combiner", 6); + if (!Fixture.Function || Fixture.Entry == nullptr) + return; + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Entry).has_value(), + "combiner insert point must be established"); + const rcc::PhysicalRegister X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + const rcc::PhysicalRegister X2 = physical(Context, rcc::riscv::PhysicalRegisterID::X2); + const rcc::Register Seed = makeGPR(Context, *Fixture.Function); + const rcc::Register Identity = makeGPR(Context, *Fixture.Function); + const rcc::Register Consumer = makeGPR(Context, *Fixture.Function); + (void)buildADDI(Context, Builder, Seed, rcc::Register::fromPhysical(X0), 7); + rcc::MachineInstr *IdentityInstruction = buildADDI(Context, Builder, Identity, Seed, 0); + rcc::MachineInstr *ConsumerInstruction = buildADD(Context, Builder, Consumer, Identity, Seed); + const rcc::MachineMemOperand *Memory = makeMemory(Context, *Fixture.Function, true, false); + if (Memory == nullptr) + return; + (void)buildStore(Context, Builder, Consumer, X2, *Memory); + + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineCombinerPass Pass(rcc::riscv::machineCombinerInfo(), + rcc::MachineCombinerStage::SelectedSSA); + auto Result = Pass.run(*Fixture.Function, Target, Analyses); + if (!Result) + std::cerr << "machine combiner failure: " << Result.error().message() << '\n'; + Context.expect(Result.has_value() && Result->Changed, + "machine combiner must apply a registered RV64GC identity rule"); + Context.expect(IdentityInstruction != nullptr && !IdentityInstruction->isLinked() && + !Fixture.Function->registerInfo().isKnownVirtualRegister(Identity), + "machine combiner must erase the identity and retire its result"); + const rcc::MachineOperand *ConsumerUse = + ConsumerInstruction == nullptr ? nullptr : ConsumerInstruction->operandAt(1); + Context.expect(ConsumerUse != nullptr && ConsumerUse->registerValue() == Seed, + "machine combiner executor must rewrite all result uses"); +} + +void testPostRAMachineCombinerIdentity(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "post_ra_combiner", 7); + if (!Fixture.Function || Fixture.Entry == nullptr) + return; + Fixture.Function->properties().clear(rcc::MachineFunctionProperty::IsSSA); + Fixture.Function->properties().set(rcc::MachineFunctionProperty::NoPHIs); + Fixture.Function->properties().set(rcc::MachineFunctionProperty::TiedOpsRewritten); + Fixture.Function->properties().set(rcc::MachineFunctionProperty::NoVRegs); + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Entry).has_value(), + "post-RA combiner insert point must be established"); + const rcc::PhysicalRegister X10 = physical(Context, rcc::riscv::PhysicalRegisterID::X10); + rcc::MachineInstr *Identity = buildADDI(Context, Builder, rcc::Register::fromPhysical(X10), + rcc::Register::fromPhysical(X10), 0); + rcc::MachineInstr *FrameIdentity = buildADDI(Context, Builder, rcc::Register::fromPhysical(X10), + rcc::Register::fromPhysical(X10), 0); + if (FrameIdentity != nullptr) + FrameIdentity->setFlags({rcc::MachineInstrFlag::FrameDestroy}); + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineCombinerPass Pass(rcc::riscv::machineCombinerInfo(), + rcc::MachineCombinerStage::PostRegisterAllocation); + auto Result = Pass.run(*Fixture.Function, Target, Analyses); + Context.expect( + Result.has_value() && Result->Changed && Identity != nullptr && !Identity->isLinked(), + "post-RA combiner must erase a physical-register no-op only when identities match"); + Context.expect(FrameIdentity != nullptr && FrameIdentity->isLinked(), + "post-frame semantics must protect an otherwise removable identity"); +} + +void testCSERegisterAndMemoryRules(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "cse", 2); + if (!Fixture.Function || Fixture.Entry == nullptr) + return; + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Entry).has_value(), + "CSE insert point must be established"); + const rcc::PhysicalRegister X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + + const rcc::Register Seed = makeGPR(Context, *Fixture.Function); + const rcc::Register First = makeGPR(Context, *Fixture.Function); + const rcc::Register Redundant = makeGPR(Context, *Fixture.Function); + const rcc::Register Consumer = makeGPR(Context, *Fixture.Function); + (void)buildADDI(Context, Builder, Seed, rcc::Register::fromPhysical(X0), 9); + rcc::MachineInstr *FirstInstruction = buildADDI(Context, Builder, First, Seed, 7); + rcc::MachineInstr *RedundantInstruction = buildADDI(Context, Builder, Redundant, Seed, 7); + rcc::MachineInstr *ConsumerInstruction = buildADD(Context, Builder, Consumer, Redundant, Seed); + + const rcc::Register MutableLoadOne = makeGPR(Context, *Fixture.Function); + const rcc::Register MutableLoadTwo = makeGPR(Context, *Fixture.Function); + const rcc::Register InvariantLoadOne = makeGPR(Context, *Fixture.Function); + const rcc::Register InvariantLoadTwo = makeGPR(Context, *Fixture.Function); + const rcc::MachineMemOperand *MutableMemory = + makeMemory(Context, *Fixture.Function, false, false); + const rcc::MachineMemOperand *InvariantMemory = + makeMemory(Context, *Fixture.Function, false, true); + if (MutableMemory == nullptr || InvariantMemory == nullptr) + return; + rcc::MachineInstr *MutableOne = buildLoad(Context, Builder, MutableLoadOne, Seed, *MutableMemory); + rcc::MachineInstr *MutableTwo = buildLoad(Context, Builder, MutableLoadTwo, Seed, *MutableMemory); + rcc::MachineInstr *InvariantOne = + buildLoad(Context, Builder, InvariantLoadOne, Seed, *InvariantMemory); + rcc::MachineInstr *InvariantTwo = + buildLoad(Context, Builder, InvariantLoadTwo, Seed, *InvariantMemory); + + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineCSEPass Pass; + auto Result = Pass.run(*Fixture.Function, Target, Analyses); + if (!Result) + std::cerr << "machine CSE failure: " << Result.error().message() << '\n'; + Context.expect(Result.has_value() && Result->Changed, + "machine CSE must eliminate safe redundant expressions"); + Context.expect(FirstInstruction != nullptr && FirstInstruction->isLinked() && + RedundantInstruction != nullptr && !RedundantInstruction->isLinked(), + "machine CSE must keep the dominating expression only"); + const rcc::MachineOperand *ConsumerUse = + ConsumerInstruction == nullptr ? nullptr : ConsumerInstruction->operandAt(1); + Context.expect(ConsumerUse != nullptr && ConsumerUse->registerValue() == First, + "machine CSE must rewrite uses to the dominating result"); + Context.expect(MutableOne != nullptr && MutableOne->isLinked() && MutableTwo != nullptr && + MutableTwo->isLinked(), + "machine CSE must not merge mutable loads"); + Context.expect(InvariantOne != nullptr && InvariantOne->isLinked() && InvariantTwo != nullptr && + !InvariantTwo->isLinked(), + "machine CSE must merge invariant dereferenceable loads"); + Context.expect(!Fixture.Function->registerInfo().isKnownVirtualRegister(Redundant) && + !Fixture.Function->registerInfo().isKnownVirtualRegister(InvariantLoadTwo), + "machine CSE must retire redundant result registers"); +} + +void testCSEBudgetPreservesLegalMIR(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "cse_budget", 3); + if (!Fixture.Function || Fixture.Entry == nullptr) + return; + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Entry).has_value(), + "CSE budget insert point must be established"); + const rcc::PhysicalRegister X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + const rcc::Register Seed = makeGPR(Context, *Fixture.Function); + const rcc::Register First = makeGPR(Context, *Fixture.Function); + const rcc::Register Second = makeGPR(Context, *Fixture.Function); + (void)buildADDI(Context, Builder, Seed, rcc::Register::fromPhysical(X0), 1); + (void)buildADDI(Context, Builder, First, Seed, 2); + rcc::MachineInstr *SecondInstruction = buildADDI(Context, Builder, Second, Seed, 2); + + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineCSEPass Pass({.MaximumComparisons = 0}); + auto Result = Pass.run(*Fixture.Function, Target, Analyses); + if (!Result) + std::cerr << "machine CSE budget failure: " << Result.error().message() << '\n'; + Context.expect(Result.has_value() && !Result->Changed && SecondInstruction != nullptr && + SecondInstruction->isLinked(), + "machine CSE budget exhaustion must stop without rejecting or mutating MIR"); +} + +void testLICMHoistsOnlyInvariantInstructions(TestContext &Context, + const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "licm", 4); + if (!Fixture.Function || Fixture.Entry == nullptr) + return; + auto Header = Fixture.Function->createBasicBlock(); + auto Body = Fixture.Function->createBasicBlock(); + auto Exit = Fixture.Function->createBasicBlock(); + Context.expect(Header.has_value() && Body.has_value() && Exit.has_value() && + Fixture.Function->appendBlock(**Header).has_value() && + Fixture.Function->appendBlock(**Body).has_value() && + Fixture.Function->appendBlock(**Exit).has_value(), + "LICM loop blocks must be created"); + if (!Header || !Body || !Exit) + return; + auto Half = rcc::BranchProbability::fromFraction(1, 2); + Context.expect(Half.has_value(), "LICM branch probability must be created"); + if (!Half) + return; + auto Updater = Fixture.Function->cfgUpdater(); + Context.expect( + Updater.addSuccessor(*Fixture.Entry, **Header, rcc::BranchProbability::one()).has_value() && + Updater.addSuccessor(**Header, **Body, *Half).has_value() && + Updater.addSuccessor(**Header, **Exit, *Half).has_value() && + Updater.addSuccessor(**Body, **Header, rcc::BranchProbability::one()).has_value(), + "LICM loop CFG must be connected"); + + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Entry).has_value(), + "LICM preheader insertion point must be established"); + const rcc::PhysicalRegister X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + const rcc::Register Seed = makeGPR(Context, *Fixture.Function); + (void)buildADDI(Context, Builder, Seed, rcc::Register::fromPhysical(X0), 1); + + Context.expect(Builder.setInsertPoint(**Body).has_value(), + "LICM body insertion point must be established"); + const rcc::Register HoistedValue = makeGPR(Context, *Fixture.Function); + const rcc::Register DependentValue = makeGPR(Context, *Fixture.Function); + rcc::MachineInstr *Hoisted = buildADDI(Context, Builder, HoistedValue, Seed, 8); + rcc::MachineInstr *Dependent = buildADDI(Context, Builder, DependentValue, HoistedValue, 9); + const rcc::Register MutableLoad = makeGPR(Context, *Fixture.Function); + const rcc::MachineMemOperand *Memory = makeMemory(Context, *Fixture.Function, false, false); + rcc::MachineInstr *Load = + Memory == nullptr ? nullptr : buildLoad(Context, Builder, MutableLoad, Seed, *Memory); + + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineLICMPass Pass; + auto Result = Pass.run(*Fixture.Function, Target, Analyses); + if (!Result) + std::cerr << "machine LICM failure: " << Result.error().message() << '\n'; + Context.expect(Result.has_value() && Result->Changed, + "machine LICM must hoist loop-invariant instructions"); + Context.expect(Hoisted != nullptr && Hoisted->parent() == Fixture.Entry && Dependent != nullptr && + Dependent->parent() == Fixture.Entry, + "machine LICM must reach a fixed point for dependent invariants"); + Context.expect(Load != nullptr && Load->parent() == *Body, + "machine LICM must keep mutable loads in the loop"); +} + +void testMachineSinkUsesColdUniqueSuccessor(TestContext &Context, + const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "sink", 5); + if (!Fixture.Function || Fixture.Entry == nullptr) + return; + auto Cold = Fixture.Function->createBasicBlock(); + auto Hot = Fixture.Function->createBasicBlock(); + Context.expect(Cold.has_value() && Hot.has_value() && + Fixture.Function->appendBlock(**Cold).has_value() && + Fixture.Function->appendBlock(**Hot).has_value(), + "machine sink successor blocks must be created"); + if (!Cold || !Hot) + return; + auto ColdProbability = rcc::BranchProbability::fromFraction(1, 10); + auto HotProbability = rcc::BranchProbability::fromFraction(9, 10); + Context.expect(ColdProbability.has_value() && HotProbability.has_value(), + "machine sink probabilities must be created"); + if (!ColdProbability || !HotProbability) + return; + auto Updater = Fixture.Function->cfgUpdater(); + Context.expect(Updater.addSuccessor(*Fixture.Entry, **Cold, *ColdProbability).has_value() && + Updater.addSuccessor(*Fixture.Entry, **Hot, *HotProbability).has_value(), + "machine sink CFG must be connected"); + + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Entry).has_value(), + "machine sink source insertion point must be established"); + const rcc::PhysicalRegister X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + const rcc::Register Seed = makeGPR(Context, *Fixture.Function); + const rcc::Register SunkValue = makeGPR(Context, *Fixture.Function); + (void)buildADDI(Context, Builder, Seed, rcc::Register::fromPhysical(X0), 1); + rcc::MachineInstr *Sunk = buildADDI(Context, Builder, SunkValue, Seed, 4); + + Context.expect(Builder.setInsertPoint(**Cold).has_value(), + "machine sink destination insertion point must be established"); + const rcc::Register Consumer = makeGPR(Context, *Fixture.Function); + rcc::MachineInstr *Use = buildADD(Context, Builder, Consumer, SunkValue, Seed); + + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineSinkPass Pass; + auto Result = Pass.run(*Fixture.Function, Target, Analyses); + if (!Result) + std::cerr << "machine sink failure: " << Result.error().message() << '\n'; + Context.expect(Result.has_value() && Result->Changed, + "machine sink must move a cold-path-only expression"); + Context.expect(Sunk != nullptr && Sunk->parent() == *Cold && Use != nullptr && + Sunk->next() == Use, + "machine sink must place the definition immediately before its first use"); +} + +void testPostRACodeMotion(TestContext &Context, const rcc::TargetDescription &Target) { + auto LICMFixture = makeFixture(Context, "post-ra-licm", 10); + if (!LICMFixture.Function || LICMFixture.Entry == nullptr) + return; + auto Header = LICMFixture.Function->createBasicBlock(); + auto Body = LICMFixture.Function->createBasicBlock(); + auto Exit = LICMFixture.Function->createBasicBlock(); + if (!Header || !Body || !Exit || !LICMFixture.Function->appendBlock(**Header) || + !LICMFixture.Function->appendBlock(**Body) || !LICMFixture.Function->appendBlock(**Exit)) { + Context.expect(false, "post-RA LICM blocks must be created"); + return; + } + auto Half = rcc::BranchProbability::fromFraction(1, 2); + if (!Half) + return; + auto LICMUpdater = LICMFixture.Function->cfgUpdater(); + if (!LICMUpdater.addSuccessor(*LICMFixture.Entry, **Header, rcc::BranchProbability::one()) || + !LICMUpdater.addSuccessor(**Header, **Body, *Half) || + !LICMUpdater.addSuccessor(**Header, **Exit, *Half) || + !LICMUpdater.addSuccessor(**Body, **Header, rcc::BranchProbability::one())) { + Context.expect(false, "post-RA LICM CFG must be connected"); + return; + } + rcc::MachineIRBuilder LICMBuilder(*LICMFixture.Function, Target); + if (!LICMBuilder.setInsertPoint(*LICMFixture.Entry)) + return; + const rcc::Register X0 = + rcc::Register::fromPhysical(physical(Context, rcc::riscv::PhysicalRegisterID::X0)); + const rcc::PhysicalRegister X2Physical = physical(Context, rcc::riscv::PhysicalRegisterID::X2); + const rcc::Register X10 = + rcc::Register::fromPhysical(physical(Context, rcc::riscv::PhysicalRegisterID::X10)); + const rcc::Register X12 = + rcc::Register::fromPhysical(physical(Context, rcc::riscv::PhysicalRegisterID::X12)); + const rcc::Register X13 = + rcc::Register::fromPhysical(physical(Context, rcc::riscv::PhysicalRegisterID::X13)); + (void)buildADDI(Context, LICMBuilder, X10, X0, 1); + if (!LICMBuilder.setInsertPoint(**Header)) + return; + rcc::MachineInstr *Hoisted = buildADDI(Context, LICMBuilder, X12, X10, 4); + if (Hoisted == nullptr) + return; + Hoisted->setFlags({rcc::MachineInstrFlag::NoFPExcept}); + if (!LICMBuilder.setInsertPoint(**Body)) + return; + (void)buildADDI(Context, LICMBuilder, X13, X12, 1); + if (!LICMBuilder.setInsertPoint(**Exit)) + return; + const rcc::MachineMemOperand *LICMMemory = + makeMemory(Context, *LICMFixture.Function, true, false); + if (LICMMemory == nullptr) + return; + (void)buildStore(Context, LICMBuilder, X12, X2Physical, *LICMMemory); + setPostRAProperties(*LICMFixture.Function); + Context.expect(rcc::rebuildMachineLiveness(*LICMFixture.Function, Target).has_value(), + "post-RA LICM fixture liveness must be built"); + rcc::MachineFunctionAnalysisManager LICMAnalyses; + rcc::MachineLICMPass PostRALICM(rcc::MachineCodeMotionStage::PostRegisterAllocation); + auto LICMResult = PostRALICM.run(*LICMFixture.Function, Target, LICMAnalyses); + Context.expect(LICMResult && LICMResult->Changed && Hoisted->parent() == LICMFixture.Entry && + rcc::verifyTrackedMachineLiveness(*LICMFixture.Function, Target).has_value(), + "post-RA LICM must hoist a safe physical-register invariant and repair liveness"); + + auto SinkFixture = makeFixture(Context, "post-ra-sink", 11); + if (!SinkFixture.Function || SinkFixture.Entry == nullptr) + return; + auto Cold = SinkFixture.Function->createBasicBlock(); + auto Hot = SinkFixture.Function->createBasicBlock(); + if (!Cold || !Hot || !SinkFixture.Function->appendBlock(**Cold) || + !SinkFixture.Function->appendBlock(**Hot)) { + return; + } + auto ColdProbability = rcc::BranchProbability::fromFraction(1, 10); + auto HotProbability = rcc::BranchProbability::fromFraction(9, 10); + if (!ColdProbability || !HotProbability) + return; + auto SinkUpdater = SinkFixture.Function->cfgUpdater(); + if (!SinkUpdater.addSuccessor(*SinkFixture.Entry, **Cold, *ColdProbability) || + !SinkUpdater.addSuccessor(*SinkFixture.Entry, **Hot, *HotProbability)) { + return; + } + rcc::MachineIRBuilder SinkBuilder(*SinkFixture.Function, Target); + if (!SinkBuilder.setInsertPoint(*SinkFixture.Entry)) + return; + (void)buildADDI(Context, SinkBuilder, X10, X0, 2); + rcc::MachineInstr *Sunk = buildADDI(Context, SinkBuilder, X12, X10, 8); + if (Sunk == nullptr) + return; + Sunk->setFlags({rcc::MachineInstrFlag::NoFPExcept}); + if (!SinkBuilder.setInsertPoint(**Cold)) + return; + rcc::MachineInstr *ColdUse = buildADDI(Context, SinkBuilder, X13, X12, 1); + setPostRAProperties(*SinkFixture.Function); + Context.expect(rcc::rebuildMachineLiveness(*SinkFixture.Function, Target).has_value(), + "post-RA sink fixture liveness must be built"); + rcc::MachineFunctionAnalysisManager SinkAnalyses; + rcc::MachineSinkPass PostRASink(rcc::MachineCodeMotionStage::PostRegisterAllocation); + auto SinkResult = PostRASink.run(*SinkFixture.Function, Target, SinkAnalyses); + Context.expect(SinkResult && SinkResult->Changed && Sunk->parent() == *Cold && + Sunk->next() == ColdUse && + rcc::verifyTrackedMachineLiveness(*SinkFixture.Function, Target).has_value(), + "post-RA sinking must move a physical definition to its sole cold use"); +} + +void testMemoryFoldingContract(TestContext &Context, const rcc::TargetDescription &Target) { + auto BuildCandidate = [&](std::string Name, std::uint64_t ID) { + Fixture Candidate = makeFixture(Context, std::move(Name), ID); + if (!Candidate.Function || Candidate.Entry == nullptr) + return Candidate; + rcc::MachineIRBuilder Builder(*Candidate.Function, Target); + if (!Builder.setInsertPoint(*Candidate.Entry)) + return Candidate; + const rcc::PhysicalRegister X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + const rcc::PhysicalRegister X2 = physical(Context, rcc::riscv::PhysicalRegisterID::X2); + const rcc::Register Address = makeGPR(Context, *Candidate.Function); + const rcc::Register Loaded = makeGPR(Context, *Candidate.Function); + const rcc::Register FoldedResult = makeGPR(Context, *Candidate.Function); + (void)buildADDI(Context, Builder, Address, rcc::Register::fromPhysical(X0), 0); + const rcc::MachineMemOperand *LoadMemory = + makeMemory(Context, *Candidate.Function, false, false); + const rcc::MachineMemOperand *StoreMemory = + makeMemory(Context, *Candidate.Function, true, false); + if (LoadMemory == nullptr || StoreMemory == nullptr) + return Candidate; + (void)buildLoad(Context, Builder, Loaded, Address, *LoadMemory); + (void)buildADDI(Context, Builder, FoldedResult, Loaded, 0); + (void)buildStore(Context, Builder, FoldedResult, X2, *StoreMemory); + return Candidate; + }; + + Fixture Folded = BuildCandidate("memory-fold", 8); + if (!Folded.Function || Folded.Entry == nullptr) + return; + TestMemoryFoldingInfo GoodInfo; + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineMemoryFoldingPass Pass(GoodInfo); + auto Result = Pass.run(*Folded.Function, Target, Analyses); + if (!Result) + std::cerr << "machine memory folding failure: " << Result.error().message() << '\n'; + std::size_t LoadCount = 0; + rcc::MachineInstr *FoldedLoad = nullptr; + for (rcc::MachineInstr *Instruction = Folded.Entry->firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + if (Instruction->opcode() == opcode(rcc::riscv::InstructionOpcode::LD)) { + ++LoadCount; + FoldedLoad = Instruction; + } + } + Context.expect(Result && Result->Changed && LoadCount == 1 && FoldedLoad != nullptr && + FoldedLoad->memOperands().size() == 1, + "memory folding must replace an adjacent load-use pair and migrate its MMO"); + + Fixture Rejected = BuildCandidate("memory-fold-missing-mmo", 9); + if (!Rejected.Function || Rejected.Entry == nullptr) + return; + rcc::MachineInstr *OriginalLoad = Rejected.Entry->firstInstruction(); + OriginalLoad = OriginalLoad == nullptr ? nullptr : OriginalLoad->next(); + rcc::MachineInstr *OriginalConsumer = OriginalLoad == nullptr ? nullptr : OriginalLoad->next(); + const std::size_t OriginalCount = Rejected.Entry->instructionCount(); + TestMemoryFoldingInfo BadInfo(true); + rcc::MachineMemoryFoldingPass BadPass(BadInfo); + auto Failure = BadPass.run(*Rejected.Function, Target, Analyses); + Context.expect(!Failure && Failure.error().code() == rcc::ErrorCode::VerificationError && + Rejected.Entry->instructionCount() == OriginalCount && + OriginalLoad != nullptr && OriginalLoad->isLinked() && + OriginalConsumer != nullptr && OriginalConsumer->isLinked(), + "memory folding must reject missing MMO migration without mutating the function"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = rcc::riscv::createTargetDescription(); + Context.expect(Target.has_value(), "optimization target construction must succeed"); + if (!Target) + return Context.result(); + testDCEFixedPointAndDebugUses(Context, *Target); + testMachineCombinerIdentityRules(Context, *Target); + testPostRAMachineCombinerIdentity(Context, *Target); + testCSERegisterAndMemoryRules(Context, *Target); + testCSEBudgetPreservesLegalMIR(Context, *Target); + testLICMHoistsOnlyInvariantInstructions(Context, *Target); + testMachineSinkUsesColdUniqueSuccessor(Context, *Target); + testPostRACodeMotion(Context, *Target); + testMemoryFoldingContract(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachinePassManagerTest.cpp b/backend/unittests/CodeGen/MachinePassManagerTest.cpp new file mode 100644 index 0000000..6e6c44d --- /dev/null +++ b/backend/unittests/CodeGen/MachinePassManagerTest.cpp @@ -0,0 +1,426 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachinePassManager.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/CompilationUnit.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/MachineFunction.h" + +#include <atomic> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <memory> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + std::vector<rcc::InstructionDescriptor> Descriptors; + std::vector<rcc::ImmediatePredicate> Predicates; + auto Appended = rcc::appendGenericInstructionInfo(Descriptors, Predicates); + if (!Appended) + return std::unexpected(Appended.error()); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors), std::move(Predicates)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +class BlockCountAnalysis final { +public: + struct Result final { + std::size_t Count = 0; + }; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "BlockCount"; + } + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &Function, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + return Result{Function.blockCount()}; + } +}; + +class NonThreadSafeAnalysis final { +public: + struct Result final {}; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "NonThreadSafe"; + } + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + return Result{}; + } +}; + +class RequiredAnalysisPass final : public rcc::MachineFunctionPass { +public: + [[nodiscard]] const rcc::MachinePassContract &contract() const noexcept override { + static const rcc::MachinePassContract Contract{ + .Name = "RequiredAnalysis", + .Requires = {}, + .Sets = {}, + .Clears = {}, + .RequiredAnalyses = {rcc::MachineFunctionAnalysisManager::id<BlockCountAnalysis>()}, + .VerificationStage = std::nullopt, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected<rcc::MachinePassResult> + run(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineFunctionAnalysisManager &Analyses) override { + auto Blocks = Analyses.getResult<BlockCountAnalysis>(Function, Target); + if (!Blocks) + return std::unexpected(Blocks.error()); + rcc::MachinePassResult Result; + Result.Preserved = rcc::PreservedAnalyses::all(); + return Result; + } +}; + +class UnreportedChangePass final : public rcc::MachineFunctionPass { +public: + [[nodiscard]] const rcc::MachinePassContract &contract() const noexcept override { + static const rcc::MachinePassContract Contract{ + .Name = "UnreportedChange", + .Requires = {}, + .Sets = {}, + .Clears = {}, + .RequiredAnalyses = {}, + .VerificationStage = std::nullopt, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected<rcc::MachinePassResult> + run(rcc::MachineFunction &Function, const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) override { + auto Block = Function.createBasicBlock(); + if (!Block) + return std::unexpected(Block.error()); + auto Appended = Function.appendBlock(**Block); + if (!Appended) + return std::unexpected(Appended.error()); + return rcc::MachinePassResult{}; + } +}; + +class MutatingFailurePass final : public rcc::MachineFunctionPass { +public: + [[nodiscard]] const rcc::MachinePassContract &contract() const noexcept override { + static const rcc::MachinePassContract Contract{ + .Name = "MutatingFailure", + .Requires = {}, + .Sets = {}, + .Clears = {}, + .RequiredAnalyses = {rcc::MachineFunctionAnalysisManager::id<BlockCountAnalysis>()}, + .VerificationStage = std::nullopt, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected<rcc::MachinePassResult> + run(rcc::MachineFunction &Function, const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) override { + auto Block = Function.createBasicBlock(); + if (!Block) + return std::unexpected(Block.error()); + auto Appended = Function.appendBlock(**Block); + if (!Appended) + return std::unexpected(Appended.error()); + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "intentional mutating pass failure"); + } +}; + +class ParallelCountingPass final : public rcc::MachineFunctionPass { +public: + explicit ParallelCountingPass(std::atomic<std::uint32_t> &Count) : Count(&Count) {} + + [[nodiscard]] const rcc::MachinePassContract &contract() const noexcept override { + static const rcc::MachinePassContract Contract = [] { + rcc::MachinePassContract Value{ + .Name = "ParallelCounting", + .Requires = {}, + .Sets = {}, + .Clears = {}, + .RequiredAnalyses = {}, + .VerificationStage = std::nullopt, + }; + Value.ThreadSafety = rcc::PassThreadSafety::ThreadSafe; + return Value; + }(); + return Contract; + } + + [[nodiscard]] rcc::Expected<rcc::MachinePassResult> + run(rcc::MachineFunction &, const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) override { + Count->fetch_add(1, std::memory_order_relaxed); + return rcc::MachinePassResult{}; + } + +private: + std::atomic<std::uint32_t> *Count; +}; + +class RequiresNonThreadSafeAnalysisPass final : public rcc::MachineFunctionPass { +public: + [[nodiscard]] const rcc::MachinePassContract &contract() const noexcept override { + static const rcc::MachinePassContract Contract = [] { + rcc::MachinePassContract Value{ + .Name = "RequiresNonThreadSafe", + .Requires = {}, + .Sets = {}, + .Clears = {}, + .RequiredAnalyses = + { + rcc::MachineFunctionAnalysisManager::id<NonThreadSafeAnalysis>(), + }, + .VerificationStage = std::nullopt, + }; + Value.ThreadSafety = rcc::PassThreadSafety::ThreadSafe; + return Value; + }(); + return Contract; + } + + [[nodiscard]] rcc::Expected<rcc::MachinePassResult> + run(rcc::MachineFunction &, const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) override { + return rcc::MachinePassResult{}; + } +}; + +class UnitFunctionCountAnalysis final { +public: + struct Result final { + std::size_t Count = 0; + }; + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "UnitFunctionCount"; + } + [[nodiscard]] static rcc::Expected<Result> run(rcc::CompilationUnit &Unit, + const rcc::TargetDescription &, + rcc::CompilationUnitAnalysisManager &) { + return Result{Unit.functionCount()}; + } +}; + +class ObserveUnitPass final : public rcc::CompilationUnitPass { +public: + [[nodiscard]] const rcc::CompilationUnitPassContract &contract() const noexcept override { + static const rcc::CompilationUnitPassContract Contract{ + .Name = "ObserveUnit", + .RequiredAnalyses = {rcc::CompilationUnitAnalysisManager::id<UnitFunctionCountAnalysis>()}, + .Parameters = {}, + .DeclaredChanges = {}, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected<rcc::CompilationUnitPassResult> + run(rcc::CompilationUnit &Unit, const rcc::TargetDescription &Target, + rcc::CompilationUnitAnalysisManager &Analyses, + rcc::MachineFunctionAnalysisManager &) override { + auto Count = Analyses.getResult<UnitFunctionCountAnalysis>(Unit, Target); + if (!Count) + return std::unexpected(Count.error()); + if ((*Count)->Count != Unit.functionCount()) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "unit analysis returned a stale function count"); + } + return rcc::CompilationUnitPassResult{}; + } +}; + +class MutatingUnitFailurePass final : public rcc::CompilationUnitPass { +public: + [[nodiscard]] const rcc::CompilationUnitPassContract &contract() const noexcept override { + static const rcc::CompilationUnitPassContract Contract{ + .Name = "MutatingUnitFailure", + .RequiredAnalyses = {rcc::CompilationUnitAnalysisManager::id<UnitFunctionCountAnalysis>()}, + .Parameters = {}, + .DeclaredChanges = {rcc::MachinePassChange::BlockLayout}, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected<rcc::CompilationUnitPassResult> + run(rcc::CompilationUnit &Unit, const rcc::TargetDescription &, + rcc::CompilationUnitAnalysisManager &, rcc::MachineFunctionAnalysisManager &) override { + auto Created = Unit.createMachineFunction("added-before-failure"); + if (!Created) + return std::unexpected(Created.error()); + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "intentional compilation-unit pass failure"); + } +}; + +void testMachinePassManager(TestContext &Context, const rcc::TargetDescription &Target) { + auto Function = rcc::MachineFunction::create("pass", rcc::MachineFunctionStableID::fromValue(2)); + Context.expect(Function.has_value(), "pass function creation must succeed"); + if (!Function) + return; + + rcc::MachineFunctionAnalysisManager Analyses; + Context.expect(Analyses + .registerAnalysis<BlockCountAnalysis>({ + .Name = "BlockCount", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + .ThreadSafety = rcc::PassThreadSafety::ThreadSafe, + }) + .has_value(), + "pass analysis registration must succeed"); + + rcc::MachineFunctionPassManager Pipeline; + Context.expect(Pipeline.addPass(std::make_unique<RequiredAnalysisPass>()).has_value(), + "a valid pass must be accepted"); + auto Run = Pipeline.run(**Function, Target, Analyses); + Context.expect(Run.has_value(), "a pass with a materialized required analysis must run"); + + auto ChangedFunction = + rcc::MachineFunction::create("unreported", rcc::MachineFunctionStableID::fromValue(3)); + rcc::MachineFunctionAnalysisManager EmptyAnalyses; + rcc::MachineFunctionPassManager Detecting( + {.VerifyBefore = true, .VerifyEach = true, .DetectUnreportedChanges = true}); + Context.expect(ChangedFunction && + Detecting.addPass(std::make_unique<UnreportedChangePass>()).has_value() && + !Detecting.run(**ChangedFunction, Target, EmptyAnalyses), + "change detection must reject an unreported MachineIR mutation"); + + auto FailedFunction = + rcc::MachineFunction::create("failed", rcc::MachineFunctionStableID::fromValue(4)); + rcc::MachineFunctionAnalysisManager FailureAnalyses; + Context.expect(FailedFunction && FailureAnalyses + .registerAnalysis<BlockCountAnalysis>({ + .Name = "BlockCount", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + }) + .has_value(), + "mutating failure analysis fixtures must be valid"); + rcc::MachineFunctionPassManager FailingPipeline; + Context.expect( + FailedFunction && + FailingPipeline.addPass(std::make_unique<MutatingFailurePass>()).has_value() && + !FailingPipeline.run(**FailedFunction, Target, FailureAnalyses) && + !FailureAnalyses.hasCached(**FailedFunction, + rcc::MachineFunctionAnalysisManager::id<BlockCountAnalysis>()), + "a pass that mutates and fails must not leave stale function analyses cached"); +} + +void testCompilationUnitAndParallelism(TestContext &Context, const rcc::TargetDescription &Target) { + rcc::CompilationUnit Unit; + for (std::size_t Index = 0; Index < 8; ++Index) { + auto Function = Unit.createMachineFunction("f" + std::to_string(Index)); + Context.expect(Function.has_value(), "parallel test function creation must succeed"); + } + + std::atomic<std::uint32_t> Runs = 0; + rcc::MachineFunctionPassManager Parallel; + Context.expect(Parallel.addPass(std::make_unique<ParallelCountingPass>(Runs)).has_value(), + "thread-safe pass registration must succeed"); + rcc::MachineFunctionAnalysisManager Prototype; + auto ParallelRun = Parallel.runFunctions(Unit, Target, Prototype, std::size_t{4}); + Context.expect(ParallelRun.has_value() && Runs == Unit.functionCount(), + "parallel function execution must visit every function exactly once"); + + rcc::MachineFunctionAnalysisManager UnsafePrototype; + Context.expect(UnsafePrototype + .registerAnalysis<NonThreadSafeAnalysis>({ + .Name = "NonThreadSafe", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + .ThreadSafety = rcc::PassThreadSafety::NotThreadSafe, + }) + .has_value(), + "non-thread-safe analysis registration must be explicit"); + rcc::MachineFunctionPassManager UnsafeParallel; + Context.expect( + UnsafeParallel.addPass(std::make_unique<RequiresNonThreadSafeAnalysisPass>()).has_value() && + !UnsafeParallel.runFunctions(Unit, Target, UnsafePrototype, std::size_t{4}), + "parallel pipelines must reject a required analysis not marked thread-safe"); + + rcc::CompilationUnitAnalysisManager UnitAnalyses; + Context.expect(UnitAnalyses + .registerAnalysis<UnitFunctionCountAnalysis>({ + .Name = "UnitFunctionCount", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + }) + .has_value(), + "compilation-unit analysis registration must succeed"); + rcc::MachineFunctionAnalysisManager FunctionAnalyses; + rcc::CompilationUnitPassManager UnitPipeline; + Context.expect(UnitPipeline.addPass(std::make_unique<ObserveUnitPass>()).has_value(), + "compilation-unit pass registration must succeed"); + Context.expect(UnitPipeline.run(Unit, Target, UnitAnalyses, FunctionAnalyses).has_value(), + "compilation-unit pass and analysis must run through their own manager"); + + rcc::MachineFunction *FirstFunction = Unit.functionAt(0); + Context.expect( + FirstFunction != nullptr && + FunctionAnalyses + .registerAnalysis<BlockCountAnalysis>({ + .Name = "BlockCount", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + }) + .has_value() && + FunctionAnalyses.getResult<BlockCountAnalysis>(*FirstFunction, Target).has_value(), + "compilation-unit failure cache fixtures must materialize"); + rcc::CompilationUnitPassManager FailingUnitPipeline; + Context.expect( + FirstFunction != nullptr && + FailingUnitPipeline.addPass(std::make_unique<MutatingUnitFailurePass>()).has_value() && + !FailingUnitPipeline.run(Unit, Target, UnitAnalyses, FunctionAnalyses) && + !UnitAnalyses.hasCached( + Unit, rcc::CompilationUnitAnalysisManager::id<UnitFunctionCountAnalysis>()) && + !FunctionAnalyses.hasCached( + *FirstFunction, rcc::MachineFunctionAnalysisManager::id<BlockCountAnalysis>()), + "a mutating compilation-unit failure must clear unit and function analysis caches"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "machine pass tests require a valid target description"); + if (!Target) + return Context.result(); + + testMachinePassManager(Context, *Target); + testCompilationUnitAndParallelism(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineSchedulingTest.cpp b/backend/unittests/CodeGen/MachineSchedulingTest.cpp new file mode 100644 index 0000000..a3d1de9 --- /dev/null +++ b/backend/unittests/CodeGen/MachineSchedulingTest.cpp @@ -0,0 +1,518 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "RISCV.h" +#include "rcc/CodeGen/LiveVariables.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/MachineScheduler.h" +#include "rcc/CodeGen/PostRAScheduler.h" +#include "rcc/CodeGen/ScheduleDAG.h" +#include "rcc/CodeGen/ScheduleHazardRecognizer.h" +#include "rcc/CodeGen/TargetSchedulingModel.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +#include <array> +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#include <string> +#include <string_view> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +struct Fixture final { + std::unique_ptr<rcc::MachineFunction> Function; + rcc::MachineBasicBlock *Block = nullptr; +}; + +class OnePerCycleHazard final : public rcc::TargetScheduleHazardRecognizer { +public: + [[nodiscard]] std::unique_ptr<rcc::TargetScheduleHazardRecognizer> clone() const override { + return std::make_unique<OnePerCycleHazard>(*this); + } + + [[nodiscard]] rcc::Expected<rcc::ScheduleHazardType> + getHazardType(const rcc::ScheduleUnit &) const override { + return Emitted ? rcc::ScheduleHazardType::Hazard : rcc::ScheduleHazardType::NoHazard; + } + + [[nodiscard]] rcc::Expected<void> emitInstruction(const rcc::ScheduleUnit &) override { + Emitted = true; + return {}; + } + + [[nodiscard]] rcc::Expected<void> advanceCycle() override { + Emitted = false; + return {}; + } + + [[nodiscard]] rcc::Expected<void> recedeCycle() override { + Emitted = false; + return {}; + } + + [[nodiscard]] rcc::Expected<void> reset() override { + Emitted = false; + return {}; + } + +private: + bool Emitted = false; +}; + +class TestSchedulingHooks final : public rcc::TargetSchedulingHooks { +public: + bool MakeADDIBoundary = false; + bool AddMutation = false; + bool PreferSecondUnit = false; + bool EnableDynamicHazards = false; + + [[nodiscard]] bool isSchedulingBoundary(const rcc::MachineInstr &Instruction, + const rcc::TargetDescription &) const noexcept override { + return MakeADDIBoundary && + Instruction.opcode() == rcc::Opcode::fromValue(static_cast<std::uint32_t>( + rcc::riscv::InstructionOpcode::ADDI)); + } + + [[nodiscard]] rcc::Expected<void> mutateScheduleDAG(rcc::ScheduleDAG &DAG, + const rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::ScheduleDAGMode) const override { + if (!AddMutation || DAG.units().size() < 2) + return {}; + return DAG.addArtificialDependency(0, 1, 2); + } + + [[nodiscard]] std::int64_t candidatePreference(const rcc::ScheduleUnit &Unit, + rcc::ScheduleDirection, + const rcc::MachineBasicBlock &, + std::uint64_t) const noexcept override { + return PreferSecondUnit && Unit.ID == 1 ? 100 : 0; + } + + [[nodiscard]] std::unique_ptr<rcc::TargetScheduleHazardRecognizer> + createHazardRecognizer(rcc::ScheduleDirection) const override { + return EnableDynamicHazards ? std::make_unique<OnePerCycleHazard>() : nullptr; + } +}; + +[[nodiscard]] rcc::Opcode opcode(rcc::riscv::InstructionOpcode Value) noexcept { + return rcc::Opcode::fromValue(static_cast<std::uint32_t>(Value)); +} + +[[nodiscard]] rcc::Expected<rcc::TargetSchedulingModel> +makeSchedulingModel(const rcc::TargetSchedulingHooks *Hooks = nullptr) { + rcc::TargetSchedulingModelDesc Description; + Description.IssueWidth = 2; + Description.DispatchLimit = 2; + for (const rcc::riscv::SchedulingClassInfo &Class : rcc::riscv::schedulingClasses()) { + Description.SchedulingClasses.push_back({ + .ID = Class.ID, + .Name = std::string(Class.Name), + .MicroOps = Class.MicroOps, + .Latency = Class.Latency, + .Resources = {}, + .WriteLatencies = {{0, Class.Latency}}, + .ReadAdvances = {}, + }); + } + Description.PressureSets.push_back({ + .ID = rcc::RegisterPressureSetID::fromValue(1), + .Name = "gpr", + .RegisterClasses = + { + rcc::RegisterClassID::fromValue( + static_cast<std::uint32_t>(rcc::riscv::RegisterClass::GPR)), + }, + .Limit = 27, + }); + Description.Hooks = Hooks; + return rcc::TargetSchedulingModel::create(std::move(Description)); +} + +[[nodiscard]] rcc::Register physical(TestContext &Context, rcc::riscv::PhysicalRegisterID ID) { + auto Value = rcc::riscv::physicalRegister(ID); + Context.expect(Value.has_value(), "physical register creation must succeed"); + return Value ? rcc::Register::fromPhysical(*Value) : rcc::Register{}; +} + +[[nodiscard]] Fixture makeFixture(TestContext &Context, std::string Name, std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "scheduler function creation must succeed"); + if (!Function) + return {}; + auto Block = (*Function)->createBasicBlock(); + Context.expect(Block.has_value() && (*Function)->appendBlock(**Block).has_value(), + "scheduler block creation must succeed"); + if (!Block) + return {}; + (*Function)->properties().set(rcc::MachineFunctionProperty::Legalized); + (*Function)->properties().set(rcc::MachineFunctionProperty::RegBankSelected); + (*Function)->properties().set(rcc::MachineFunctionProperty::Selected); + (*Function)->properties().set(rcc::MachineFunctionProperty::NoPHIs); + (*Function)->properties().set(rcc::MachineFunctionProperty::TiedOpsRewritten); + return {std::move(*Function), *Block}; +} + +[[nodiscard]] rcc::Register makeGPR(TestContext &Context, rcc::MachineFunction &Function) { + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Reg = Type ? Function.registerInfo().createVirtualRegister(*Type) + : rcc::Expected<rcc::Register>(std::unexpected(rcc::Error( + rcc::ErrorCode::InternalError, "test LowLevelType creation failed"))); + Context.expect(Reg.has_value(), "scheduler virtual register creation must succeed"); + if (!Reg) + return {}; + const auto Bank = + rcc::RegisterBankID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterBank::GPR)); + const auto Class = + rcc::RegisterClassID::fromValue(static_cast<std::uint32_t>(rcc::riscv::RegisterClass::GPR)); + Context.expect(Function.registerInfo().setRegisterBank(*Reg, Bank).has_value() && + Function.registerInfo().setRegisterClass(*Reg, Class).has_value(), + "scheduler virtual register constraints must be assigned"); + return *Reg; +} + +[[nodiscard]] rcc::MachineInstr *buildADDI(TestContext &Context, rcc::MachineIRBuilder &Builder, + rcc::Register Destination, rcc::Register Source, + std::int64_t Immediate) { + auto Pending = Builder.buildInstr(opcode(rcc::riscv::InstructionOpcode::ADDI)); + if (!Pending || !Pending->addDef(Destination) || !Pending->addUse(Source) || + !Pending->addImmediate(Immediate)) { + Context.expect(false, "scheduler ADDI construction must succeed"); + return nullptr; + } + auto Built = Pending->build(); + Context.expect(Built.has_value(), "scheduler ADDI insertion must succeed"); + return Built.value_or(nullptr); +} + +[[nodiscard]] rcc::MachineInstr *buildBinary(TestContext &Context, rcc::MachineIRBuilder &Builder, + rcc::riscv::InstructionOpcode Opcode, + rcc::Register Destination, rcc::Register Left, + rcc::Register Right) { + auto Pending = Builder.buildInstr(opcode(Opcode)); + if (!Pending || !Pending->addDef(Destination) || !Pending->addUse(Left) || + !Pending->addUse(Right)) { + Context.expect(false, "scheduler binary instruction construction must succeed"); + return nullptr; + } + auto Built = Pending->build(); + Context.expect(Built.has_value(), "scheduler binary instruction insertion must succeed"); + return Built.value_or(nullptr); +} + +[[nodiscard]] bool hasDependency(const rcc::ScheduleUnit &Unit, std::size_t Destination, + rcc::ScheduleDependencyKind Kind) { + for (const rcc::ScheduleDependency &Dependency : Unit.Successors) { + if (Dependency.Destination == Destination && Dependency.Kind == Kind) + return true; + } + return false; +} + +void testDAGRegisterDependencies(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "schedule_dag", 1); + if (!Fixture.Function || Fixture.Block == nullptr) + return; + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Block).has_value(), + "schedule DAG insertion point must be established"); + const rcc::Register X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + const rcc::Register X10 = physical(Context, rcc::riscv::PhysicalRegisterID::X10); + const rcc::Register Value = makeGPR(Context, *Fixture.Function); + const rcc::Register Consumer = makeGPR(Context, *Fixture.Function); + rcc::MachineInstr *Definition = buildADDI(Context, Builder, Value, X0, 1); + rcc::MachineInstr *Use = + buildBinary(Context, Builder, rcc::riscv::InstructionOpcode::ADD, Consumer, Value, X10); + rcc::MachineInstr *Redefinition = buildADDI(Context, Builder, Value, X0, 2); + std::array<rcc::MachineInstr *, 3> Instructions{Definition, Use, Redefinition}; + auto DAG = rcc::ScheduleDAG::build(*Fixture.Block, Instructions, *Fixture.Function, Target, + rcc::riscv::schedulingModel(), + rcc::ScheduleDAGMode::PreRegisterAllocation); + if (!DAG) + std::cerr << "schedule DAG failure: " << DAG.error().message() << '\n'; + Context.expect(DAG.has_value() && DAG->units().size() == 3, + "schedule DAG must create one unit per instruction"); + if (!DAG) + return; + Context.expect(hasDependency(DAG->units()[0], 1, rcc::ScheduleDependencyKind::Data), + "schedule DAG must model virtual-register data dependencies"); + Context.expect(hasDependency(DAG->units()[0], 2, rcc::ScheduleDependencyKind::Output), + "schedule DAG must model virtual-register output dependencies"); + Context.expect(hasDependency(DAG->units()[1], 2, rcc::ScheduleDependencyKind::Anti), + "schedule DAG must model virtual-register anti dependencies"); + Context.expect(DAG->units()[0].Latency != 0 && DAG->units()[0].MicroOps != 0, + "schedule DAG must attach target latency and resource facts"); +} + +void testHazardScoreboard(TestContext &Context) { + auto Hazards = rcc::ScheduleHazardRecognizer::create(rcc::riscv::schedulingModel()); + Context.expect(Hazards.has_value(), "hazard recognizer construction must succeed"); + if (!Hazards) + return; + const auto Memory = rcc::ProcessorResourceID::fromValue(3); + rcc::ScheduleUnit Load; + Load.MicroOps = 1; + Load.Resources = {{Memory, 1}}; + Context.expect(Hazards->issue(Load, 0).has_value(), + "first memory operation must issue in cycle zero"); + auto SecondCycle = Hazards->earliestIssueCycle(Load, 0); + Context.expect(SecondCycle.has_value() && *SecondCycle == 1, + "single-unit memory resource must serialize operations"); + + rcc::ScheduleUnit Wide; + Wide.MicroOps = 5; + auto Fresh = rcc::ScheduleHazardRecognizer::create(rcc::riscv::schedulingModel()); + Context.expect(Fresh.has_value() && Fresh->issue(Wide, 0).has_value(), + "multi-cycle dispatch must accept instructions wider than issue width"); + auto AfterWide = Fresh ? Fresh->earliestIssueCycle(Load, 0) + : rcc::Expected<std::uint64_t>(std::unexpected( + rcc::Error(rcc::ErrorCode::InternalError, "no recognizer"))); + Context.expect(AfterWide.has_value() && *AfterWide == 3, + "multi-cycle dispatch must block the complete dispatch interval"); +} + +void testOperandLatencyAndModelValidation(TestContext &Context) { + const rcc::SchedulingClassID ALU = rcc::SchedulingClassID::fromValue( + static_cast<std::uint32_t>(rcc::riscv::SchedulingClass::ALU)); + const rcc::SchedulingClassID Load = rcc::SchedulingClassID::fromValue( + static_cast<std::uint32_t>(rcc::riscv::SchedulingClass::LOAD)); + auto Forwarded = rcc::riscv::schedulingModel().operandLatency(Load, 0, ALU, 1); + auto Unforwarded = rcc::riscv::schedulingModel().operandLatency(Load, 0, ALU, 0); + auto ALUChain = rcc::riscv::schedulingModel().operandLatency(ALU, 0, ALU, 1); + Context.expect(Forwarded.has_value() && *Forwarded == 2 && Unforwarded.has_value() && + *Unforwarded == 3 && ALUChain.has_value() && *ALUChain == 1, + "operand latency must combine write latency with exact read advance"); + + rcc::TargetSchedulingModelDesc Invalid; + Invalid.SchedulingClasses.push_back({ + .ID = ALU, + .Name = "invalid", + .MicroOps = 1, + .Latency = 1, + .Resources = {}, + .WriteLatencies = {{0, 1}, {0, 2}}, + .ReadAdvances = {}, + }); + auto Rejected = rcc::TargetSchedulingModel::create(std::move(Invalid)); + Context.expect(!Rejected && Rejected.error().code() == rcc::ErrorCode::InvalidArgument, + "scheduling model must reject duplicate operand latency facts"); +} + +void testDynamicTargetHazards(TestContext &Context) { + TestSchedulingHooks Hooks; + Hooks.EnableDynamicHazards = true; + auto Model = makeSchedulingModel(&Hooks); + Context.expect(Model.has_value(), "dynamic-hazard scheduling model construction must succeed"); + if (!Model) + return; + + rcc::ScheduleUnit First; + First.ID = 1; + First.MicroOps = 1; + rcc::ScheduleUnit Second; + Second.ID = 2; + Second.MicroOps = 1; + auto Top = rcc::ScheduleHazardRecognizer::create(*Model, rcc::ScheduleDirection::TopDown); + Context.expect(Top.has_value() && Top->issue(First, 0).has_value(), + "target dynamic hazard must accept the first top-down issue"); + auto TopCycle = Top ? Top->earliestIssueCycle(Second, 0) + : rcc::Expected<std::uint64_t>(std::unexpected( + rcc::Error(rcc::ErrorCode::InternalError, "no top-down recognizer"))); + Context.expect(TopCycle.has_value() && *TopCycle == 1, + "target dynamic hazard must advance the top-down cycle"); + + auto Bottom = rcc::ScheduleHazardRecognizer::create(*Model, rcc::ScheduleDirection::BottomUp); + Context.expect(Bottom.has_value() && Bottom->issue(First, 0).has_value(), + "target dynamic hazard must accept the first bottom-up issue"); + auto BottomCycle = Bottom ? Bottom->earliestIssueCycle(Second, 0) + : rcc::Expected<std::uint64_t>(std::unexpected(rcc::Error( + rcc::ErrorCode::InternalError, "no bottom-up recognizer"))); + Context.expect(BottomCycle.has_value() && *BottomCycle == 1, + "target dynamic hazard must recede the bottom-up cycle"); +} + +void testTargetSchedulingHooks(TestContext &Context, const rcc::TargetDescription &Target) { + TestSchedulingHooks MutationHooks; + MutationHooks.AddMutation = true; + auto MutationModel = makeSchedulingModel(&MutationHooks); + auto Fixture = makeFixture(Context, "schedule_mutation", 4); + if (!MutationModel || !Fixture.Function || Fixture.Block == nullptr) { + Context.expect(false, "target schedule mutation fixture must be created"); + return; + } + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Block).has_value(), + "target schedule mutation insertion point must be established"); + const rcc::Register X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + rcc::MachineInstr *First = + buildADDI(Context, Builder, makeGPR(Context, *Fixture.Function), X0, 1); + rcc::MachineInstr *Second = + buildADDI(Context, Builder, makeGPR(Context, *Fixture.Function), X0, 2); + std::array<rcc::MachineInstr *, 2> Instructions{First, Second}; + auto DAG = rcc::ScheduleDAG::build(*Fixture.Block, Instructions, *Fixture.Function, Target, + *MutationModel, rcc::ScheduleDAGMode::PreRegisterAllocation); + Context.expect(DAG.has_value() && + hasDependency(DAG->units()[0], 1, rcc::ScheduleDependencyKind::Artificial), + "target schedule mutation must add a checked artificial dependency"); + + TestSchedulingHooks PreferenceHooks; + PreferenceHooks.PreferSecondUnit = true; + auto PreferenceModel = makeSchedulingModel(&PreferenceHooks); + if (!PreferenceModel) { + Context.expect(false, "target scheduling preference model must be created"); + return; + } + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineSchedulerPass PreferencePass( + *PreferenceModel, {.MaximumRegionSize = 8, .EnableBidirectionalScheduling = false}); + auto Preferred = PreferencePass.run(*Fixture.Function, Target, Analyses); + Context.expect(Preferred.has_value() && Preferred->Changed && + Fixture.Block->firstInstruction() == Second, + "target candidate preference must affect the production scheduler"); + + auto BoundaryFixture = makeFixture(Context, "schedule_boundary", 5); + TestSchedulingHooks BoundaryHooks; + BoundaryHooks.MakeADDIBoundary = true; + auto BoundaryModel = makeSchedulingModel(&BoundaryHooks); + if (!BoundaryFixture.Function || BoundaryFixture.Block == nullptr || !BoundaryModel) { + Context.expect(false, "target schedule boundary fixture must be created"); + return; + } + rcc::MachineIRBuilder BoundaryBuilder(*BoundaryFixture.Function, Target); + Context.expect(BoundaryBuilder.setInsertPoint(*BoundaryFixture.Block).has_value(), + "target schedule boundary insertion point must be established"); + rcc::MachineInstr *BoundaryFirst = + buildADDI(Context, BoundaryBuilder, makeGPR(Context, *BoundaryFixture.Function), X0, 1); + (void)buildADDI(Context, BoundaryBuilder, makeGPR(Context, *BoundaryFixture.Function), X0, 2); + rcc::MachineFunctionAnalysisManager BoundaryAnalyses; + rcc::MachineSchedulerPass BoundaryPass( + *BoundaryModel, {.MaximumRegionSize = 8, .EnableBidirectionalScheduling = false}); + auto BoundaryResult = BoundaryPass.run(*BoundaryFixture.Function, Target, BoundaryAnalyses); + Context.expect(BoundaryResult.has_value() && !BoundaryResult->Changed && + BoundaryFixture.Block->firstInstruction() == BoundaryFirst, + "target scheduling boundary must split production schedule regions"); +} + +void buildSchedulingChain(TestContext &Context, Fixture &Fixture, + const rcc::TargetDescription &Target, bool PhysicalRegisters, + rcc::MachineInstr *&Short, rcc::MachineInstr *&Long) { + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Block).has_value(), + "scheduler chain insertion point must be established"); + const rcc::Register X0 = physical(Context, rcc::riscv::PhysicalRegisterID::X0); + const rcc::Register X10 = physical(Context, rcc::riscv::PhysicalRegisterID::X10); + const rcc::Register X11 = physical(Context, rcc::riscv::PhysicalRegisterID::X11); + const rcc::Register ShortValue = PhysicalRegisters + ? physical(Context, rcc::riscv::PhysicalRegisterID::X12) + : makeGPR(Context, *Fixture.Function); + const rcc::Register LongValue = PhysicalRegisters + ? physical(Context, rcc::riscv::PhysicalRegisterID::X13) + : makeGPR(Context, *Fixture.Function); + const rcc::Register LongResult = PhysicalRegisters + ? physical(Context, rcc::riscv::PhysicalRegisterID::X14) + : makeGPR(Context, *Fixture.Function); + const rcc::Register ShortResult = PhysicalRegisters + ? physical(Context, rcc::riscv::PhysicalRegisterID::X15) + : makeGPR(Context, *Fixture.Function); + Short = buildADDI(Context, Builder, ShortValue, X0, 1); + Long = buildBinary(Context, Builder, rcc::riscv::InstructionOpcode::DIV, LongValue, X10, X11); + (void)buildBinary(Context, Builder, rcc::riscv::InstructionOpcode::ADD, LongResult, LongValue, + X10); + (void)buildBinary(Context, Builder, rcc::riscv::InstructionOpcode::ADD, ShortResult, ShortValue, + X11); +} + +void testPreRAScheduler(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "pre_ra_schedule", 2); + if (!Fixture.Function || Fixture.Block == nullptr) + return; + rcc::MachineInstr *Short = nullptr; + rcc::MachineInstr *Long = nullptr; + buildSchedulingChain(Context, Fixture, Target, false, Short, Long); + Context.expect(Fixture.Block->firstInstruction() == Short, + "pre-RA fixture must begin in intentionally suboptimal order"); + rcc::MachineFunctionAnalysisManager Analyses; + rcc::MachineSchedulerPass Pass(rcc::riscv::schedulingModel(), + {.MaximumRegionSize = 64, .EnableBidirectionalScheduling = false}); + auto Result = Pass.run(*Fixture.Function, Target, Analyses); + if (!Result) + std::cerr << "pre-RA scheduler failure: " << Result.error().message() << '\n'; + Context.expect(Result.has_value() && Result->Changed, + "pre-RA scheduler must reorder the latency-critical chain"); + Context.expect(Fixture.Block->firstInstruction() == Long, + "pre-RA scheduler must prioritize the long critical path"); +} + +void testPostRASchedulerAndLivenessRepair(TestContext &Context, + const rcc::TargetDescription &Target) { + auto Fixture = makeFixture(Context, "post_ra_schedule", 3); + if (!Fixture.Function || Fixture.Block == nullptr) + return; + Fixture.Function->properties().set(rcc::MachineFunctionProperty::NoVRegs); + Fixture.Function->properties().set(rcc::MachineFunctionProperty::TracksLiveness); + rcc::MachineInstr *Short = nullptr; + rcc::MachineInstr *Long = nullptr; + buildSchedulingChain(Context, Fixture, Target, true, Short, Long); + auto Rebuilt = rcc::rebuildMachineLiveness(*Fixture.Function, Target); + Context.expect(Rebuilt.has_value(), "post-RA fixture liveness rebuild must succeed"); + if (!Rebuilt) + return; + + rcc::MachineFunctionAnalysisManager Analyses; + rcc::PostRASchedulerPass Pass(rcc::riscv::schedulingModel(), + {.MaximumRegionSize = 64, .EnableBidirectionalScheduling = false}); + auto Result = Pass.run(*Fixture.Function, Target, Analyses); + if (!Result) + std::cerr << "post-RA scheduler failure: " << Result.error().message() << '\n'; + Context.expect(Result.has_value() && Result->Changed, + "post-RA scheduler must reorder the latency-critical chain"); + Context.expect(Fixture.Block->firstInstruction() == Long, + "post-RA scheduler must prioritize the long critical path"); + auto Verified = rcc::verifyTrackedMachineLiveness(*Fixture.Function, Target); + if (!Verified) + std::cerr << "post-RA liveness verification failure: " << Verified.error().message() << '\n'; + Context.expect(Verified.has_value(), + "post-RA scheduler must locally repair exact kill/dead state"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = rcc::riscv::createTargetDescription(); + Context.expect(Target.has_value(), "scheduler target creation must succeed"); + if (!Target) + return Context.result(); + testDAGRegisterDependencies(Context, *Target); + testHazardScoreboard(Context); + testOperandLatencyAndModelValidation(Context); + testDynamicTargetHazards(Context); + testTargetSchedulingHooks(Context, *Target); + testPreRAScheduler(Context, *Target); + testPostRASchedulerAndLivenessRepair(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineValueTypesTest.cpp b/backend/unittests/CodeGen/MachineValueTypesTest.cpp new file mode 100644 index 0000000..315d56e --- /dev/null +++ b/backend/unittests/CodeGen/MachineValueTypesTest.cpp @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineIRTypes.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/CodeGen/RegisterMask.h" + +#include <cstdint> +#include <iostream> +#include <string_view> +#include <type_traits> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +void testStrongValues(TestContext &Context) { + static_assert(!std::is_same_v<rcc::FrameIndex, rcc::ConstantPoolIndex>); + static_assert(!std::is_same_v<rcc::ConstantPoolIndex, rcc::JumpTableIndex>); + static_assert(!std::is_same_v<rcc::RegisterBankID, rcc::RegisterClassID>); + + const auto Frame = rcc::FrameIndex::fromValue(7); + const auto Constant = rcc::ConstantPoolIndex::fromValue(7); + Context.expect(Frame.value() == 7 && Constant.value() == 7, + "strong machine indices must preserve their values"); + + const auto Opcode = rcc::Opcode::fromValue(19); + Context.expect(Opcode.isValid() && Opcode.value() == 19, "Opcode must preserve its opaque value"); + Context.expect(!rcc::Opcode{}.isValid(), "Opcode zero must remain invalid"); + + const auto LowLanes = rcc::LaneMask::fromRaw(0x3); + const auto HighLanes = rcc::LaneMask::fromRaw(0x6); + Context.expect(LowLanes.intersects(HighLanes), "overlapping lane masks must intersect"); + Context.expect((LowLanes | HighLanes).rawValue() == 0x7, + "lane mask union must preserve all lanes"); +} + +void testBranchProbability(TestContext &Context) { + auto Half = rcc::BranchProbability::fromFraction(1, 2); + Context.expect(Half.has_value(), "valid branch probability fraction must be accepted"); + if (Half) { + const auto Sum = Half->saturatingAdd(*Half); + const auto RawDistance = rcc::BranchProbability::one().rawValue() - Sum.rawValue(); + Context.expect(RawDistance <= 1, + "two rounded half probabilities must sum to one within one unit"); + Context.expect(Half->complement().rawValue() == Half->rawValue() || + Half->complement().rawValue() + 1 == Half->rawValue(), + "half probability complement must preserve fixed-point rounding"); + } + + Context.expect(!rcc::BranchProbability::fromFraction(1, 0), + "zero branch probability denominator must be rejected"); + Context.expect(!rcc::BranchProbability::fromFraction(3, 2), + "branch probability greater than one must be rejected"); +} + +void testRegisterMask(TestContext &Context) { + std::vector<std::uint64_t> Words(2); + Words[0] = std::uint64_t{1} << 7; + Words[1] = std::uint64_t{1} << 6; + const rcc::RegisterMask Mask(std::move(Words)); + + auto Reg7 = rcc::PhysicalRegister::create(7); + auto Reg70 = rcc::PhysicalRegister::create(70); + auto Reg71 = rcc::PhysicalRegister::create(71); + Context.expect(Reg7 && Reg70 && Reg71, "physical registers for mask test must be valid"); + if (Reg7 && Reg70 && Reg71) { + Context.expect(Mask.preserves(*Reg7), "preserved register mask bit must be one"); + Context.expect(Mask.preserves(*Reg70), "preserved register mask must cross word boundaries"); + Context.expect(Mask.clobbers(*Reg71), "clear register mask bit must mean clobbered"); + } +} + +rcc::MachineMemOperandDesc makeBasicLoadDescription() { + rcc::MachineMemOperandDesc Description; + Description.Accesses = rcc::MemoryAccessFlags{ + rcc::MemoryAccessFlag::Load, + rcc::MemoryAccessFlag::Dereferenceable, + }; + Description.Size = *rcc::MemorySize::fixed(8); + Description.Align = *rcc::Alignment::create(8); + Description.AddressSpace = 0; + Description.Base = rcc::MemoryBase::frameIndex(rcc::FrameIndex::fromValue(3)); + Description.Offset = 16; + Description.AliasSource = rcc::MemoryAliasSourceID::fromValue(11); + return Description; +} + +void testMemoryValueTypes(TestContext &Context) { + Context.expect(!rcc::Alignment::create(0), "zero alignment must be rejected"); + Context.expect(!rcc::Alignment::create(3), "non-power-of-two alignment must be rejected"); + Context.expect(rcc::Alignment::create(16).has_value(), "power-of-two alignment must be accepted"); + Context.expect(!rcc::MemorySize::fixed(0), "zero fixed memory size must be rejected"); + Context.expect(!rcc::MemorySize::scalable(0), "zero scalable memory minimum must be rejected"); + + auto Range = rcc::MemoryOffsetRange::create(-4, 12); + Context.expect(Range && Range->Lower == -4 && Range->Upper == 12, + "valid memory offset range must preserve both bounds"); + Context.expect(!rcc::MemoryOffsetRange::create(1, -1), + "reversed memory offset range must be rejected"); + + const auto Symbol = rcc::MemoryBase::globalSymbol("global"); + Context.expect(Symbol.kind() == rcc::MemoryBase::Kind::GlobalSymbol, + "global memory base must preserve its kind"); + Context.expect(Symbol.symbolName() == "global", "global memory base must preserve its symbol"); + + rcc::GenericConstantData PoisonPointer; + PoisonPointer.Type = rcc::GenericConstantType::pointer(); + PoisonPointer.Kind = rcc::GenericConstantKind::Poison; + auto ConstantBase = rcc::MemoryBase::irConstant(PoisonPointer); + Context.expect(ConstantBase && ConstantBase->kind() == rcc::MemoryBase::Kind::IRConstant && + ConstantBase->constantValue() != nullptr && + *ConstantBase->constantValue() == PoisonPointer, + "IR-constant memory base must preserve a structured pointer constant"); + + rcc::GenericConstantData InvalidIntegerBase; + InvalidIntegerBase.Type = *rcc::GenericConstantType::integer(8); + InvalidIntegerBase.Kind = rcc::GenericConstantKind::Poison; + Context.expect(!rcc::MemoryBase::irConstant(std::move(InvalidIntegerBase)), + "IR-constant memory base must reject a non-pointer constant"); +} + +void testMachineMemOperand(TestContext &Context) { + auto Load = rcc::MachineMemOperand::create(makeBasicLoadDescription()); + Context.expect(Load.has_value(), "complete ordinary load MMO must be accepted"); + if (Load) { + Context.expect(Load->isLoad() && !Load->isStore(), + "ordinary load MMO must preserve access direction"); + Context.expect(!Load->isAtomic() && !Load->isVolatile(), + "ordinary load MMO must remain unordered and non-volatile"); + } + + rcc::MachineMemOperandDesc NoAccess; + Context.expect(!rcc::MachineMemOperand::create(NoAccess), + "MMO without load or store access must be rejected"); + + auto InvariantStore = makeBasicLoadDescription(); + InvariantStore.Accesses.set(rcc::MemoryAccessFlag::Store); + InvariantStore.Accesses.set(rcc::MemoryAccessFlag::Invariant); + Context.expect(!rcc::MachineMemOperand::create(InvariantStore), + "invariant store MMO must be rejected"); + + auto AtomicLoad = makeBasicLoadDescription(); + AtomicLoad.AtomicKind = rcc::AtomicOperation::Load; + AtomicLoad.SuccessOrdering = rcc::AtomicOrdering::Acquire; + Context.expect(rcc::MachineMemOperand::create(AtomicLoad).has_value(), + "acquire atomic load MMO must be accepted"); + + AtomicLoad.SuccessOrdering = rcc::AtomicOrdering::Release; + Context.expect(!rcc::MachineMemOperand::create(AtomicLoad), + "release ordering on atomic load must be rejected"); + + auto CompareExchange = makeBasicLoadDescription(); + CompareExchange.Accesses.set(rcc::MemoryAccessFlag::Store); + CompareExchange.AtomicKind = rcc::AtomicOperation::CompareExchange; + CompareExchange.SuccessOrdering = rcc::AtomicOrdering::AcquireRelease; + CompareExchange.FailureOrdering = rcc::AtomicOrdering::Acquire; + Context.expect(rcc::MachineMemOperand::create(CompareExchange).has_value(), + "compatible compare-exchange orderings must be accepted"); + + CompareExchange.FailureOrdering = rcc::AtomicOrdering::NotAtomic; + Context.expect(rcc::MachineMemOperand::create(CompareExchange).has_value(), + "LLVM MIR compare-exchange MMO without failure ordering must be accepted"); + + CompareExchange.FailureOrdering = rcc::AtomicOrdering::Acquire; + CompareExchange.SuccessOrdering = rcc::AtomicOrdering::Release; + Context.expect(!rcc::MachineMemOperand::create(CompareExchange), + "failure ordering stronger than success must be rejected"); + + auto BadAddressSpace = makeBasicLoadDescription(); + BadAddressSpace.AddressSpace = rcc::LowLevelType::MaxAddressSpace + 1; + Context.expect(!rcc::MachineMemOperand::create(BadAddressSpace), + "MMO address space wider than LowLevelType encoding must be rejected"); + + auto EmptySymbol = makeBasicLoadDescription(); + EmptySymbol.Base = rcc::MemoryBase::externalSymbol(""); + Context.expect(!rcc::MachineMemOperand::create(EmptySymbol), + "empty MMO symbol base must be rejected"); + + auto OffsetAlignment = makeBasicLoadDescription(); + OffsetAlignment.Offset = 12; + OffsetAlignment.Align = *rcc::Alignment::create(4); + OffsetAlignment.BaseAlign = *rcc::Alignment::create(8); + auto OffsetAligned = rcc::MachineMemOperand::create(OffsetAlignment); + Context.expect(OffsetAligned && + OffsetAligned->description().BaseAlign == *rcc::Alignment::create(8), + "MMO base alignment must derive effective alignment through its offset"); + + OffsetAlignment.Align = *rcc::Alignment::create(8); + Context.expect(!rcc::MachineMemOperand::create(OffsetAlignment), + "MMO alignment inconsistent with its base and offset must be rejected"); + + OffsetAlignment.Offset = 16; + OffsetAlignment.Align = *rcc::Alignment::create(8); + OffsetAlignment.BaseAlign = *rcc::Alignment::create(8); + auto RedundantBase = rcc::MachineMemOperand::create(OffsetAlignment); + Context.expect(RedundantBase && !RedundantBase->description().BaseAlign, + "MMO base alignment equal to effective alignment must canonicalize away"); +} + +} // namespace + +int main() { + TestContext Context; + testStrongValues(Context); + testBranchProbability(Context); + testRegisterMask(Context); + testMemoryValueTypes(Context); + testMachineMemOperand(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineVerifierTargetTest.cpp b/backend/unittests/CodeGen/MachineVerifierTargetTest.cpp new file mode 100644 index 0000000..3cc2b56 --- /dev/null +++ b/backend/unittests/CodeGen/MachineVerifierTargetTest.cpp @@ -0,0 +1,485 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/LowLevelType.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/MachineVerifier.h" +#include "rcc/CodeGen/RegisterMask.h" +#include "rcc/CodeGen/TargetDescription.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t DefGPR64Opcode = 1; +constexpr std::uint32_t DefGPR32Opcode = 2; +constexpr std::uint32_t EarlyClobberOpcode = 3; +constexpr std::uint32_t TiedAnyOpcode = 4; +constexpr std::uint32_t CallOpcode = 5; +constexpr std::uint32_t RenamableImplicitOpcode = 6; + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +struct FunctionFixture final { + std::unique_ptr<rcc::MachineFunction> Function; + rcc::MachineBasicBlock *Block = nullptr; +}; + +[[nodiscard]] rcc::PhysicalRegister phys(std::uint32_t Number) { + return *rcc::PhysicalRegister::create(Number); +} + +[[nodiscard]] rcc::OperandDescriptor +registerOperand(rcc::RegisterOperandRole Role, + std::optional<rcc::RegisterClassID> Class = std::nullopt, + std::optional<rcc::RegisterBankID> Bank = std::nullopt, bool AllowSubReg = false, + bool EarlyClobber = false) { + rcc::OperandDescriptor Operand; + Operand.Kinds = {rcc::MachineOperand::Kind::Register}; + Operand.RegisterRole = Role; + Operand.RegClass = Class; + Operand.RegBank = Bank; + Operand.AllowSubReg = AllowSubReg; + Operand.EarlyClobber = EarlyClobber; + return Operand; +} + +[[nodiscard]] rcc::InstructionDescriptor instruction(std::uint32_t OpcodeValue, std::string Name) { + rcc::InstructionDescriptor Instruction; + Instruction.InstructionOpcode = rcc::Opcode::fromValue(OpcodeValue); + Instruction.Name = std::move(Name); + Instruction.Flags = {rcc::InstructionFlag::ProducesCode}; + return Instruction; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + auto DefGPR64 = instruction(DefGPR64Opcode, "TEST_DEF_GPR64"); + DefGPR64.Operands.push_back(registerOperand(rcc::RegisterOperandRole::Def, + rcc::test::TestRegisterClass::GPR64, + rcc::test::TestRegisterBank::GPR, true)); + DefGPR64.NumExplicitDefs = 1; + + auto DefGPR32 = instruction(DefGPR32Opcode, "TEST_DEF_GPR32"); + DefGPR32.Operands.push_back(registerOperand(rcc::RegisterOperandRole::Def, + rcc::test::TestRegisterClass::GPR32, + rcc::test::TestRegisterBank::GPR, true)); + DefGPR32.NumExplicitDefs = 1; + + auto EarlyClobber = instruction(EarlyClobberOpcode, "TEST_EARLY_CLOBBER"); + EarlyClobber.Operands.push_back(registerOperand(rcc::RegisterOperandRole::Def, + rcc::test::TestRegisterClass::GPR64, + rcc::test::TestRegisterBank::GPR, false, true)); + EarlyClobber.Operands.push_back(registerOperand(rcc::RegisterOperandRole::Use, + rcc::test::TestRegisterClass::GPR64, + rcc::test::TestRegisterBank::GPR)); + EarlyClobber.NumExplicitDefs = 1; + + auto TiedAny = instruction(TiedAnyOpcode, "TEST_TIED_ANY"); + auto TiedDef = registerOperand(rcc::RegisterOperandRole::Def); + auto TiedUse = registerOperand(rcc::RegisterOperandRole::Use); + TiedDef.DefaultTiedOperand = 1; + TiedUse.DefaultTiedOperand = 0; + TiedAny.Operands.push_back(TiedDef); + TiedAny.Operands.push_back(TiedUse); + TiedAny.NumExplicitDefs = 1; + + auto Call = instruction(CallOpcode, "TEST_CALL"); + rcc::OperandDescriptor Mask; + Mask.Kinds = {rcc::MachineOperand::Kind::RegisterMask}; + Call.Operands.push_back(Mask); + Call.Flags.set(rcc::InstructionFlag::Call); + Call.Flags.set(rcc::InstructionFlag::HasSideEffects); + + auto RenamableImplicit = instruction(RenamableImplicitOpcode, "TEST_RENAMABLE_IMPLICIT"); + RenamableImplicit.ImplicitDefs.push_back({phys(rcc::test::TestRegister::G0), false, true}); + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(DefGPR64)); + Descriptors.push_back(std::move(DefGPR32)); + Descriptors.push_back(std::move(EarlyClobber)); + Descriptors.push_back(std::move(TiedAny)); + Descriptors.push_back(std::move(Call)); + Descriptors.push_back(std::move(RenamableImplicit)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] FunctionFixture createFunction(TestContext &Context, std::string Name, + std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "target verifier function must be created"); + if (!Function) + return {}; + + auto Block = (*Function)->createBasicBlock(); + Context.expect(Block && (*Function)->appendBlock(**Block).has_value(), + "target verifier block must be linked"); + if (!Block) + return {}; + return FunctionFixture{std::move(*Function), *Block}; +} + +[[nodiscard]] rcc::Expected<void> classify(rcc::MachineRegisterInfo &RegInfo, rcc::Register Reg, + rcc::RegisterBankID Bank, rcc::RegisterClassID Class) { + auto BankResult = RegInfo.setRegisterBank(Reg, Bank); + if (!BankResult) + return std::unexpected(BankResult.error()); + return RegInfo.setRegisterClass(Reg, Class); +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> +appendInstruction(FunctionFixture &Fixture, std::uint32_t OpcodeValue, + std::vector<rcc::MachineOperand> Operands, + std::optional<rcc::MachineOperandTie> Tie = std::nullopt) { + auto Instruction = Fixture.Function->createInstruction(rcc::Opcode::fromValue(OpcodeValue)); + if (!Instruction) + return std::unexpected(Instruction.error()); + for (rcc::MachineOperand &Operand : Operands) { + auto AddResult = (*Instruction)->addOperand(std::move(Operand)); + if (!AddResult) + return std::unexpected(AddResult.error()); + } + if (Tie) { + auto TieResult = (*Instruction)->tieOperands(Tie->DefOperand, Tie->UseOperand); + if (!TieResult) + return std::unexpected(TieResult.error()); + } + auto AppendResult = Fixture.Block->appendInstruction(**Instruction); + if (!AppendResult) + return std::unexpected(AppendResult.error()); + return *Instruction; +} + +void testValidTargetFacts(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction(Context, "valid_target_facts", 1); + if (!Fixture.Function) + return; + + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Reg = Fixture.Function->registerInfo().createVirtualRegister(*Type); + if (!Reg) + return; + Context.expect(classify(Fixture.Function->registerInfo(), *Reg, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64Alloc) + .has_value(), + "valid target verifier vreg must be classified"); + + const auto G0 = phys(rcc::test::TestRegister::G0); + const auto G1 = phys(rcc::test::TestRegister::G1); + Context.expect(Fixture.Function->registerInfo().addLiveIn(G0, *Reg).has_value() && + Fixture.Function->registerInfo() + .reservePhysicalRegister(phys(rcc::test::TestRegister::StackPointer)) + .has_value() && + Fixture.Block->addLiveIn(G0, rcc::LaneMask::fromRaw(0x3)).has_value(), + "valid target live-in and reserved register facts must be recorded"); + + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Block).has_value(), + "valid target builder insertion point must be set"); + auto Def = Builder.buildInstr(rcc::Opcode::fromValue(DefGPR64Opcode)); + Context.expect(Def && Def->addDef(*Reg).has_value() && Def->build(), + "register subclass must satisfy a target operand superclass"); + + auto Early = Builder.buildInstr(rcc::Opcode::fromValue(EarlyClobberOpcode)); + Context.expect( + Early && + Early->addDef(rcc::Register::fromPhysical(G0), {rcc::RegisterOperandFlag::EarlyClobber}) + .has_value() && + Early->addUse(rcc::Register::fromPhysical(G1)).has_value() && Early->build(), + "disjoint physical registers must satisfy early-clobber"); + + auto Call = Builder.buildInstr(rcc::Opcode::fromValue(CallOpcode)); + Context.expect(Call && + Call->addRegisterMask(rcc::test::createTestCallPreservedMask()).has_value() && + Call->build(), + "valid target call mask must build"); + Context.expect(rcc::MachineVerifier::verify(*Fixture.Function, Target).has_value(), + "complete valid target facts must pass MachineVerifier"); +} + +void testSubRegisterProjection(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction(Context, "subregister_projection", 2); + if (!Fixture.Function) + return; + + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Reg = Fixture.Function->registerInfo().createVirtualRegister(*Type); + if (!Reg) + return; + Context.expect(classify(Fixture.Function->registerInfo(), *Reg, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64) + .has_value(), + "subregister source vreg must be classified"); + + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Block).has_value(), + "subregister builder insertion point must be set"); + auto Def = Builder.buildInstr(rcc::Opcode::fromValue(DefGPR32Opcode)); + Context.expect(Def && Def->addDef(*Reg, {}, rcc::test::TestSubReg::Low32).has_value() && + Def->build(), + "projected GPR32Low class must satisfy the GPR32 operand class"); + Context.expect(rcc::MachineVerifier::verify(*Fixture.Function, Target).has_value(), + "valid virtual subregister projection must verify"); +} + +void testUnknownAndMalformedTargetFacts(TestContext &Context, + const rcc::TargetDescription &Target) { + { + auto Fixture = createFunction(Context, "unknown_physreg", 3); + auto Operand = rcc::MachineOperand::registerOperand(rcc::Register::fromPhysical(phys(99)), + {rcc::RegisterOperandFlag::Define}); + if (Fixture.Function && Operand) { + std::vector<rcc::MachineOperand> Operands; + Operands.push_back(std::move(*Operand)); + Context.expect(appendInstruction(Fixture, DefGPR64Opcode, std::move(Operands)) && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject unknown physical registers"); + } + } + { + auto Fixture = createFunction(Context, "physical_subreg", 4); + auto Operand = rcc::MachineOperand::registerOperand( + rcc::Register::fromPhysical(phys(rcc::test::TestRegister::G0)), + {rcc::RegisterOperandFlag::Define}, rcc::test::TestSubReg::Low32); + if (Fixture.Function && Operand) { + std::vector<rcc::MachineOperand> Operands; + Operands.push_back(std::move(*Operand)); + Context.expect(appendInstruction(Fixture, DefGPR64Opcode, std::move(Operands)) && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject physical operands with SubRegIndex"); + } + } + { + auto Fixture = createFunction(Context, "unknown_bank", 5); + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + if (!Fixture.Function || !Type) + return; + auto Reg = Fixture.Function->registerInfo().createVirtualRegister(*Type); + Context.expect(Reg && + Fixture.Function->registerInfo() + .setRegisterBank(*Reg, rcc::RegisterBankID::fromValue(99)) + .has_value() && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject unknown virtual register bank IDs"); + } + { + auto Fixture = createFunction(Context, "wide_bank_value", 6); + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(128); + if (!Fixture.Function || !Type) + return; + auto Reg = Fixture.Function->registerInfo().createVirtualRegister(*Type); + Context.expect(Reg && + Fixture.Function->registerInfo() + .setRegisterBank(*Reg, rcc::test::TestRegisterBank::GPR) + .has_value() && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject LLTs wider than their register bank"); + } + { + auto Fixture = createFunction(Context, "bad_function_livein", 7); + Context.expect(Fixture.Function && + Fixture.Function->registerInfo().addLiveIn(phys(99)).has_value() && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject unknown function physical live-ins"); + } + { + auto Fixture = createFunction(Context, "bad_block_lanes", 8); + Context.expect( + Fixture.Function && + Fixture.Block + ->addLiveIn(phys(rcc::test::TestRegister::G0), rcc::LaneMask::fromRaw(0x10)) + .has_value() && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject target-invalid block live-in lanes"); + } + { + auto Fixture = createFunction(Context, "bad_allocation_hint", 9); + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Reg = Fixture.Function->registerInfo().createVirtualRegister(*Type); + Context.expect(Reg && + classify(Fixture.Function->registerInfo(), *Reg, + rcc::test::TestRegisterBank::FPR, + rcc::test::TestRegisterClass::FPR64) + .has_value() && + Fixture.Function->registerInfo() + .addAllocationHint(*Reg, phys(rcc::test::TestRegister::G0)) + .has_value() && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject allocation hints outside a vreg class"); + } + { + auto Fixture = createFunction(Context, "reserved_allocation_hint", 14); + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Reg = Fixture.Function->registerInfo().createVirtualRegister(*Type); + const auto G0 = phys(rcc::test::TestRegister::G0); + Context.expect(Reg && + classify(Fixture.Function->registerInfo(), *Reg, + rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64) + .has_value() && + Fixture.Function->registerInfo().addAllocationHint(*Reg, G0).has_value() && + Fixture.Function->registerInfo().reservePhysicalRegister(G0).has_value() && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject allocation hints that overlap reserved registers"); + } + { + auto Fixture = createFunction(Context, "incompatible_function_livein", 15); + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Reg = Fixture.Function->registerInfo().createVirtualRegister(*Type); + Context.expect( + Reg && + classify(Fixture.Function->registerInfo(), *Reg, rcc::test::TestRegisterBank::FPR, + rcc::test::TestRegisterClass::FPR64) + .has_value() && + Fixture.Function->registerInfo() + .addLiveIn(phys(rcc::test::TestRegister::G0), *Reg) + .has_value() && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject a function live-in outside its initial vreg constraints"); + } +} + +void testRegMaskEarlyClobberAndTies(TestContext &Context, const rcc::TargetDescription &Target) { + { + auto Fixture = createFunction(Context, "bad_regmask", 10); + auto Mask = std::make_shared<const rcc::RegisterMask>( + std::vector<std::uint64_t>{std::uint64_t{1} << 63U}); + auto Operand = rcc::MachineOperand::registerMask(std::move(Mask)); + if (Fixture.Function && Operand) { + std::vector<rcc::MachineOperand> Operands; + Operands.push_back(std::move(*Operand)); + Context.expect(appendInstruction(Fixture, CallOpcode, std::move(Operands)) && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject RegMask bits for unknown registers"); + } + } + { + auto Fixture = createFunction(Context, "early_overlap", 11); + const auto G0 = phys(rcc::test::TestRegister::G0); + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Block).has_value(), + "early-clobber builder insertion point must be set"); + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(EarlyClobberOpcode)); + Context.expect( + Pending && + Pending + ->addDef(rcc::Register::fromPhysical(G0), {rcc::RegisterOperandFlag::EarlyClobber}) + .has_value() && + Pending->addUse(rcc::Register::fromPhysical(G0)).has_value() && !Pending->build() && + Fixture.Function->allInstructions().empty(), + "Builder must reject overlapping early-clobber before publication"); + + auto Def = rcc::MachineOperand::registerOperand( + rcc::Register::fromPhysical(G0), + {rcc::RegisterOperandFlag::Define, rcc::RegisterOperandFlag::EarlyClobber}); + auto Use = rcc::MachineOperand::registerOperand(rcc::Register::fromPhysical(G0), + {rcc::RegisterOperandFlag::Use}); + if (Def && Use) { + std::vector<rcc::MachineOperand> Operands; + Operands.push_back(std::move(*Def)); + Operands.push_back(std::move(*Use)); + Context.expect(appendInstruction(Fixture, EarlyClobberOpcode, std::move(Operands)) && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject overlapping early-clobber"); + } + } + { + auto Fixture = createFunction(Context, "incompatible_tie", 12); + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto GPR = Fixture.Function->registerInfo().createVirtualRegister(*Type); + auto FPR = Fixture.Function->registerInfo().createVirtualRegister(*Type); + if (!GPR || !FPR) + return; + Context.expect(classify(Fixture.Function->registerInfo(), *GPR, + rcc::test::TestRegisterBank::GPR, rcc::test::TestRegisterClass::GPR64) + .has_value() && + classify(Fixture.Function->registerInfo(), *FPR, + rcc::test::TestRegisterBank::FPR, + rcc::test::TestRegisterClass::FPR64) + .has_value(), + "incompatible tied vregs must have individually valid classes"); + + auto Def = rcc::MachineOperand::registerOperand(*GPR, {rcc::RegisterOperandFlag::Define}); + auto Use = rcc::MachineOperand::registerOperand(*FPR, {rcc::RegisterOperandFlag::Use}); + if (Def && Use) { + std::vector<rcc::MachineOperand> Operands; + Operands.push_back(std::move(*Def)); + Operands.push_back(std::move(*Use)); + Context.expect(appendInstruction(Fixture, TiedAnyOpcode, std::move(Operands), + rcc::MachineOperandTie{0, 1}) && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject tied classes without a physical intersection"); + } + } + { + auto Fixture = createFunction(Context, "reserved_renamable", 13); + const auto G0 = phys(rcc::test::TestRegister::G0); + Context.expect(Fixture.Function->registerInfo().reservePhysicalRegister(G0).has_value(), + "reserved renamable test register must be reserved"); + + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + Context.expect(Builder.setInsertPoint(*Fixture.Block).has_value(), + "reserved renamable builder insertion point must be set"); + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(RenamableImplicitOpcode)); + Context.expect(Pending && !Pending->build(), + "Builder must reject a renamable reserved implicit register"); + + const rcc::InstructionDescriptor *Descriptor = + Target.instructionDescriptor(rcc::Opcode::fromValue(RenamableImplicitOpcode)); + if (Descriptor == nullptr) + return; + auto Implicits = Target.createDefaultImplicitOperands(*Descriptor); + if (Implicits) { + Context.expect(appendInstruction(Fixture, RenamableImplicitOpcode, std::move(*Implicits)) && + !rcc::MachineVerifier::verify(*Fixture.Function, Target), + "MachineVerifier must reject a renamable reserved implicit register"); + } + } +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "target-aware verifier test target must be valid"); + if (!Target) + return Context.result(); + + testValidTargetFacts(Context, *Target); + testSubRegisterProjection(Context, *Target); + testUnknownAndMalformedTargetFacts(Context, *Target); + testRegMaskEarlyClobberAndTies(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/MachineVerifierTest.cpp b/backend/unittests/CodeGen/MachineVerifierTest.cpp new file mode 100644 index 0000000..5327148 --- /dev/null +++ b/backend/unittests/CodeGen/MachineVerifierTest.cpp @@ -0,0 +1,738 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachineVerifier.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/LowLevelType.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineCFGUpdater.h" +#include "rcc/CodeGen/MachineCompilationStatus.h" +#include "rcc/CodeGen/MachineConstantPool.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineFunctionProperties.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineJumpTableInfo.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t GenericDefOpcode = 1; +constexpr std::uint32_t GenericUseOpcode = 2; +constexpr std::uint32_t CopyOpcode = 3; +constexpr std::uint32_t BranchOpcode = 4; +constexpr std::uint32_t PhiOpcode = 5; +constexpr std::uint32_t FrameOpcode = 6; +constexpr std::uint32_t PseudoOpcode = 7; +constexpr std::uint32_t NopOpcode = 8; +constexpr std::uint32_t ConstantPoolOpcode = 9; +constexpr std::uint32_t JumpTableOpcode = 10; + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::OperandDescriptor registerDescriptor(rcc::RegisterOperandRole Role) { + rcc::OperandDescriptor Descriptor; + Descriptor.Kinds = {rcc::MachineOperand::Kind::Register}; + Descriptor.RegisterRole = Role; + return Descriptor; +} + +[[nodiscard]] rcc::InstructionDescriptor instructionDescriptor(std::uint32_t OpcodeValue, + std::string Name) { + rcc::InstructionDescriptor Descriptor; + Descriptor.InstructionOpcode = rcc::Opcode::fromValue(OpcodeValue); + Descriptor.Name = std::move(Name); + Descriptor.Flags = {rcc::InstructionFlag::ProducesCode}; + return Descriptor; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + auto GenericDef = instructionDescriptor(GenericDefOpcode, "TEST_GENERIC_DEF"); + GenericDef.Operands.push_back(registerDescriptor(rcc::RegisterOperandRole::Def)); + GenericDef.NumExplicitDefs = 1; + GenericDef.Flags.set(rcc::InstructionFlag::Generic); + GenericDef.Flags.set(rcc::InstructionFlag::AllowedPreLegalizer); + + auto GenericUse = instructionDescriptor(GenericUseOpcode, "TEST_GENERIC_USE"); + GenericUse.Operands.push_back(registerDescriptor(rcc::RegisterOperandRole::Use)); + GenericUse.Flags.set(rcc::InstructionFlag::Generic); + GenericUse.Flags.set(rcc::InstructionFlag::AllowedPreLegalizer); + + auto Copy = instructionDescriptor(CopyOpcode, "TEST_COPY"); + auto CopyDef = registerDescriptor(rcc::RegisterOperandRole::Def); + auto CopyUse = registerDescriptor(rcc::RegisterOperandRole::Use); + CopyDef.DefaultTiedOperand = 1; + CopyUse.DefaultTiedOperand = 0; + Copy.Operands.push_back(CopyDef); + Copy.Operands.push_back(CopyUse); + Copy.NumExplicitDefs = 1; + + auto Branch = instructionDescriptor(BranchOpcode, "TEST_BRANCH"); + rcc::OperandDescriptor Block; + Block.Kinds = {rcc::MachineOperand::Kind::BasicBlock}; + Branch.Operands.push_back(Block); + Branch.Flags.set(rcc::InstructionFlag::Terminator); + Branch.Flags.set(rcc::InstructionFlag::Branch); + + auto Phi = instructionDescriptor(PhiOpcode, "TEST_PHI"); + Phi.Operands.push_back(registerDescriptor(rcc::RegisterOperandRole::Def)); + auto PhiValue = registerDescriptor(rcc::RegisterOperandRole::Use); + PhiValue.VariadicGroup = true; + Phi.Operands.push_back(PhiValue); + rcc::OperandDescriptor PhiBlock; + PhiBlock.Kinds = {rcc::MachineOperand::Kind::BasicBlock}; + PhiBlock.VariadicGroup = true; + Phi.Operands.push_back(PhiBlock); + Phi.NumExplicitDefs = 1; + Phi.Variadic = true; + Phi.Flags.set(rcc::InstructionFlag::Pseudo); + Phi.Flags.set(rcc::InstructionFlag::Phi); + Phi.Owner = rcc::PseudoOwner::PhiElimination; + Phi.Deadline = rcc::PipelineStage::SSAEliminated; + + auto Frame = instructionDescriptor(FrameOpcode, "TEST_FRAME"); + rcc::OperandDescriptor FrameIndex; + FrameIndex.Kinds = {rcc::MachineOperand::Kind::FrameIndex}; + Frame.Operands.push_back(FrameIndex); + + auto Pseudo = instructionDescriptor(PseudoOpcode, "TEST_PSEUDO"); + Pseudo.Flags.set(rcc::InstructionFlag::Pseudo); + Pseudo.Owner = rcc::PseudoOwner::InstructionSelect; + Pseudo.Deadline = rcc::PipelineStage::Selected; + + auto Nop = instructionDescriptor(NopOpcode, "TEST_NOP"); + + auto ConstantPool = instructionDescriptor(ConstantPoolOpcode, "TEST_CONSTANT_POOL"); + rcc::OperandDescriptor ConstantPoolIndex; + ConstantPoolIndex.Kinds = {rcc::MachineOperand::Kind::ConstantPoolIndex}; + ConstantPool.Operands.push_back(ConstantPoolIndex); + + auto JumpTable = instructionDescriptor(JumpTableOpcode, "TEST_JUMP_TABLE"); + rcc::OperandDescriptor JumpTableIndex; + JumpTableIndex.Kinds = {rcc::MachineOperand::Kind::JumpTableIndex}; + JumpTable.Operands.push_back(JumpTableIndex); + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(GenericDef)); + Descriptors.push_back(std::move(GenericUse)); + Descriptors.push_back(std::move(Copy)); + Descriptors.push_back(std::move(Branch)); + Descriptors.push_back(std::move(Phi)); + Descriptors.push_back(std::move(Frame)); + Descriptors.push_back(std::move(Pseudo)); + Descriptors.push_back(std::move(Nop)); + Descriptors.push_back(std::move(ConstantPool)); + Descriptors.push_back(std::move(JumpTable)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> +createFunction(TestContext &Context, std::string Name, std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "machine function creation must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +[[nodiscard]] rcc::MachineBasicBlock *createBlock(TestContext &Context, + rcc::MachineFunction &Function) { + auto Block = Function.createBasicBlock(); + Context.expect(Block.has_value(), "machine basic block creation must succeed"); + if (!Block) + return nullptr; + Context.expect(Function.appendBlock(**Block).has_value(), + "machine basic block linking must succeed"); + return *Block; +} + +[[nodiscard]] rcc::MachineInstr * +buildNoOperand(TestContext &Context, rcc::MachineIRBuilder &Builder, std::uint32_t OpcodeValue) { + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(OpcodeValue)); + Context.expect(Pending.has_value(), "pending no-operand instruction must be created"); + if (!Pending) + return nullptr; + auto Result = Pending->build(); + Context.expect(Result.has_value(), "no-operand instruction must build"); + if (!Result) + return nullptr; + return *Result; +} + +[[nodiscard]] bool populateValidPreLegalizer(TestContext &Context, rcc::MachineFunction &Function, + const rcc::TargetDescription &Target) { + rcc::MachineBasicBlock *Entry = createBlock(Context, Function); + rcc::MachineBasicBlock *Exit = createBlock(Context, Function); + if (Entry == nullptr || Exit == nullptr) + return false; + + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Value = Function.registerInfo().createVirtualRegister(*Type); + Context.expect(Value.has_value(), "typed verifier virtual register must be allocated"); + if (!Value) + return false; + + rcc::MachineIRBuilder Builder(Function, Target); + Context.expect(Builder.setInsertPoint(*Entry).has_value(), "entry insertion point must be valid"); + auto Def = Builder.buildInstr(rcc::Opcode::fromValue(GenericDefOpcode)); + Context.expect(Def && Def->addDef(*Value).has_value(), "generic definition must be assembled"); + if (!Def || !Def->build()) + return false; + + auto Branch = Builder.buildInstr(rcc::Opcode::fromValue(BranchOpcode)); + Context.expect(Branch && Branch->addBasicBlock(*Exit).has_value(), + "branch operand must be assembled"); + if (!Branch || !Branch->build()) + return false; + + Context.expect( + Function.cfgUpdater().addSuccessor(*Entry, *Exit, rcc::BranchProbability::one()).has_value(), + "valid verifier CFG edge must be created"); + + Context.expect(Builder.setInsertPoint(*Exit).has_value(), "exit insertion point must be valid"); + auto Use = Builder.buildInstr(rcc::Opcode::fromValue(GenericUseOpcode)); + Context.expect(Use && Use->addUse(*Value).has_value(), "generic use must be assembled"); + if (!Use || !Use->build()) + return false; + + Function.properties().set(rcc::MachineFunctionProperty::IsSSA); + Function.properties().set(rcc::MachineFunctionProperty::TracksLiveness); + return true; +} + +void testValidFunctionAndModes(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, "valid", 1); + if (!Target || !Function || !populateValidPreLegalizer(Context, *Function, *Target)) { + return; + } + + Context.expect(rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "well-formed function must pass generic verification"); + Context.expect( + rcc::MachineVerifier::verify( + *Function, *Target, + rcc::MachineVerifierOptions{rcc::MachineVerifierMode::PreLegalizer, std::nullopt, true}) + .has_value(), + "typed SSA input must pass pre-legalizer verification"); + + auto DetachedBlock = Function->createBasicBlock(); + auto DetachedInstruction = Function->createInstruction(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(DetachedBlock && DetachedInstruction, + "detached arena history must be allocatable"); + Context.expect(rcc::MachineVerifier::verify(*Function, *Target).has_value(), + "empty detached arena objects must not invalidate live layout"); + + Function->properties().set(rcc::MachineFunctionProperty::Legalized); + Context.expect( + !rcc::MachineVerifier::verify( + *Function, *Target, + rcc::MachineVerifierOptions{rcc::MachineVerifierMode::PreLegalizer, std::nullopt, true}), + "pre-legalizer mode must reject later-stage properties"); +} + +void testReachableStructuralFailures(TestContext &Context) { + auto Target = createTarget(); + if (!Target) + return; + + auto Unknown = createFunction(Context, "unknown_opcode", 2); + rcc::MachineBasicBlock *UnknownBlock = Unknown ? createBlock(Context, *Unknown) : nullptr; + if (Unknown && UnknownBlock != nullptr) { + auto Instruction = Unknown->createInstruction(rcc::Opcode::fromValue(999)); + Context.expect(Instruction && UnknownBlock->appendInstruction(**Instruction).has_value(), + "unknown opcode negative test must be linked"); + Context.expect(!rcc::MachineVerifier::verify(*Unknown, *Target), + "live unregistered opcode must fail verification"); + } + + auto WrongOperands = createFunction(Context, "wrong_operands", 3); + rcc::MachineBasicBlock *WrongBlock = + WrongOperands ? createBlock(Context, *WrongOperands) : nullptr; + if (WrongOperands && WrongBlock != nullptr) { + auto Instruction = WrongOperands->createInstruction(rcc::Opcode::fromValue(GenericDefOpcode)); + Context.expect(Instruction && WrongBlock->appendInstruction(**Instruction).has_value(), + "wrong-operand negative test must be linked"); + Context.expect(!rcc::MachineVerifier::verify(*WrongOperands, *Target), + "descriptor operand-count mismatch must fail verification"); + } + + auto DetachedChild = createFunction(Context, "detached_child", 4); + if (DetachedChild) { + auto Block = DetachedChild->createBasicBlock(); + auto Instruction = DetachedChild->createInstruction(rcc::Opcode::fromValue(NopOpcode)); + Context.expect(Block && Instruction && (*Block)->appendInstruction(**Instruction).has_value(), + "detached block negative test must contain an instruction"); + Context.expect(!rcc::MachineVerifier::verify(*DetachedChild, *Target), + "detached block retaining instructions must fail verification"); + } + + auto DetachedCFG = createFunction(Context, "detached_cfg", 5); + rcc::MachineBasicBlock *Linked = DetachedCFG ? createBlock(Context, *DetachedCFG) : nullptr; + if (DetachedCFG && Linked != nullptr) { + auto Detached = DetachedCFG->createBasicBlock(); + Context.expect(Detached && DetachedCFG->cfgUpdater() + .addSuccessor(**Detached, *Linked, rcc::BranchProbability::one()) + .has_value(), + "detached CFG negative test edge must be created"); + Context.expect(!rcc::MachineVerifier::verify(*DetachedCFG, *Target), + "detached block retaining CFG state must fail verification"); + } +} + +enum class PhiTestShape : std::uint8_t { + Valid, + MissingPredecessor, + DuplicatePredecessor, + NonPredecessor, + AfterInstruction, +}; + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> +createPhiTestFunction(TestContext &Context, const rcc::TargetDescription &Target, std::string Name, + std::uint64_t ID, PhiTestShape Shape) { + auto Function = createFunction(Context, std::move(Name), ID); + if (!Function) + return nullptr; + rcc::MachineBasicBlock *Left = createBlock(Context, *Function); + rcc::MachineBasicBlock *Right = createBlock(Context, *Function); + rcc::MachineBasicBlock *Merge = createBlock(Context, *Function); + rcc::MachineBasicBlock *Other = createBlock(Context, *Function); + if (Left == nullptr || Right == nullptr || Merge == nullptr || Other == nullptr) + return nullptr; + + auto CFG = Function->cfgUpdater(); + if (!CFG.addSuccessor(*Left, *Merge, rcc::BranchProbability::one()) || + !CFG.addSuccessor(*Right, *Merge, rcc::BranchProbability::one())) { + Context.expect(false, "PHI verifier test CFG must be constructed"); + return nullptr; + } + + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto Destination = Type ? Function->registerInfo().createVirtualRegister(*Type) + : rcc::Expected<rcc::Register>{std::unexpected(Type.error())}; + auto LeftValue = Type ? Function->registerInfo().createVirtualRegister(*Type) + : rcc::Expected<rcc::Register>{std::unexpected(Type.error())}; + auto RightValue = Type ? Function->registerInfo().createVirtualRegister(*Type) + : rcc::Expected<rcc::Register>{std::unexpected(Type.error())}; + if (!Destination || !LeftValue || !RightValue) { + Context.expect(false, "PHI verifier test registers must be created"); + return nullptr; + } + + rcc::MachineIRBuilder Builder(*Function, Target); + if (!Builder.setInsertPoint(*Merge)) { + Context.expect(false, "PHI verifier insertion point must be valid"); + return nullptr; + } + if (Shape == PhiTestShape::AfterInstruction && + buildNoOperand(Context, Builder, NopOpcode) == nullptr) + return nullptr; + + auto Phi = Builder.buildInstr(rcc::Opcode::fromValue(PhiOpcode)); + if (!Phi || !Phi->addDef(*Destination) || !Phi->addUse(*LeftValue) || + !Phi->addBasicBlock(*Left)) { + Context.expect(false, "PHI verifier instruction prefix must build"); + return nullptr; + } + if (Shape == PhiTestShape::DuplicatePredecessor) { + if (!Phi->addUse(*RightValue) || !Phi->addBasicBlock(*Left)) + return nullptr; + } else if (Shape == PhiTestShape::NonPredecessor) { + if (!Phi->addUse(*RightValue) || !Phi->addBasicBlock(*Other)) + return nullptr; + } else if (Shape != PhiTestShape::MissingPredecessor) { + if (!Phi->addUse(*RightValue) || !Phi->addBasicBlock(*Right)) + return nullptr; + } + if (!Phi->build()) { + Context.expect(false, "PHI verifier test instruction must publish"); + return nullptr; + } + return Function; +} + +void testPHIStructure(TestContext &Context) { + auto Target = createTarget(); + if (!Target) + return; + auto Valid = createPhiTestFunction(Context, *Target, "valid_phi", 60, PhiTestShape::Valid); + auto Missing = + createPhiTestFunction(Context, *Target, "missing_phi", 61, PhiTestShape::MissingPredecessor); + auto Duplicate = createPhiTestFunction(Context, *Target, "duplicate_phi", 62, + PhiTestShape::DuplicatePredecessor); + auto NonPredecessor = + createPhiTestFunction(Context, *Target, "foreign_phi", 63, PhiTestShape::NonPredecessor); + auto AfterInstruction = + createPhiTestFunction(Context, *Target, "late_phi", 64, PhiTestShape::AfterInstruction); + Context.expect(Valid && rcc::MachineVerifier::verify(*Valid, *Target), + "PHI with one incoming value per predecessor must verify"); + Context.expect(Missing && !rcc::MachineVerifier::verify(*Missing, *Target), + "PHI missing a predecessor must fail verification"); + Context.expect(Duplicate && !rcc::MachineVerifier::verify(*Duplicate, *Target), + "PHI with duplicate incoming blocks must fail verification"); + Context.expect(NonPredecessor && !rcc::MachineVerifier::verify(*NonPredecessor, *Target), + "PHI naming a non-predecessor must fail verification"); + Context.expect(AfterInstruction && !rcc::MachineVerifier::verify(*AfterInstruction, *Target), + "PHI after a code instruction must fail verification"); +} + +void testPropertyFailures(TestContext &Context) { + auto Target = createTarget(); + if (!Target) + return; + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + + auto SSA = createFunction(Context, "ssa", 6); + rcc::MachineBasicBlock *SSABlock = SSA ? createBlock(Context, *SSA) : nullptr; + if (SSA && SSABlock != nullptr) { + auto Value = SSA->registerInfo().createVirtualRegister(*Type); + rcc::MachineIRBuilder Builder(*SSA, *Target); + Context.expect(Value && Builder.setInsertPoint(*SSABlock).has_value(), + "SSA negative test setup must succeed"); + for (int Index = 0; Index != 2 && Value; ++Index) { + auto Def = Builder.buildInstr(rcc::Opcode::fromValue(GenericDefOpcode)); + Context.expect(Def && Def->addDef(*Value).has_value() && Def->build(), + "SSA negative test definition must build"); + } + SSA->properties().set(rcc::MachineFunctionProperty::IsSSA); + Context.expect(!rcc::MachineVerifier::verify(*SSA, *Target), + "IsSSA must reject a multiply-defined virtual register"); + } + + auto PHI = createFunction(Context, "phi", 7); + rcc::MachineBasicBlock *PHIBlock = PHI ? createBlock(Context, *PHI) : nullptr; + if (PHI && PHIBlock != nullptr) { + auto Value = PHI->registerInfo().createVirtualRegister(*Type); + rcc::MachineIRBuilder Builder(*PHI, *Target); + Context.expect(Value && Builder.setInsertPoint(*PHIBlock).has_value(), + "PHI negative test setup must succeed"); + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(PhiOpcode)); + Context.expect(Pending && Pending->addDef(*Value).has_value() && Pending->build(), + "PHI negative test instruction must build"); + PHI->properties().set(rcc::MachineFunctionProperty::NoPHIs); + Context.expect(!rcc::MachineVerifier::verify(*PHI, *Target), + "NoPHIs must reject a PHI instruction"); + } + + auto NoVRegs = createFunction(Context, "no_vregs", 8); + if (NoVRegs) { + rcc::MachineBasicBlock *Block = createBlock(Context, *NoVRegs); + auto Value = NoVRegs->registerInfo().createVirtualRegister(*Type); + rcc::MachineIRBuilder Builder(*NoVRegs, *Target); + Context.expect(Block != nullptr && Builder.setInsertPoint(*Block).has_value(), + "NoVRegs negative test setup must succeed"); + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(GenericDefOpcode)); + Context.expect(Value && Pending && Pending->addDef(*Value).has_value(), + "NoVRegs linked history must be assembled"); + if (!Value || !Pending || Block == nullptr) + return; + auto Built = Pending->build(); + NoVRegs->properties().set(rcc::MachineFunctionProperty::NoVRegs); + Context.expect(Value && Built && !rcc::MachineVerifier::verify(*NoVRegs, *Target), + "NoVRegs must reject a live virtual register"); + Context.expect(Value && Built && Block != nullptr && + Block->removeInstruction(**Built).has_value() && + NoVRegs->registerInfo().eraseVirtualRegister(*Value).has_value() && + rcc::MachineVerifier::verify(*NoVRegs, *Target).has_value(), + "NoVRegs must accept tombstoned def-use history"); + } + + auto Frame = createFunction(Context, "frame_index", 9); + rcc::MachineBasicBlock *FrameBlock = Frame ? createBlock(Context, *Frame) : nullptr; + if (Frame && FrameBlock != nullptr) { + rcc::StackObjectDesc StackSlot; + StackSlot.Size = 8; + StackSlot.Align = *rcc::Alignment::create(8); + auto FrameIndex = Frame->frameInfo().createObject(StackSlot); + rcc::MachineIRBuilder Builder(*Frame, *Target); + Context.expect(FrameIndex && Builder.setInsertPoint(*FrameBlock).has_value(), + "frame-index negative test setup must succeed"); + if (!FrameIndex) + return; + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(FrameOpcode)); + Context.expect(Pending && Pending->addFrameIndex(*FrameIndex).has_value() && Pending->build(), + "frame-index negative test instruction must build"); + Frame->properties().set(rcc::MachineFunctionProperty::NoFrameIndices); + Context.expect(!rcc::MachineVerifier::verify(*Frame, *Target), + "NoFrameIndices must reject a frame-index operand"); + } + + auto Pseudo = createFunction(Context, "pseudo", 10); + rcc::MachineBasicBlock *PseudoBlock = Pseudo ? createBlock(Context, *Pseudo) : nullptr; + if (Pseudo && PseudoBlock != nullptr) { + rcc::MachineIRBuilder Builder(*Pseudo, *Target); + Context.expect(Builder.setInsertPoint(*PseudoBlock).has_value() && + buildNoOperand(Context, Builder, PseudoOpcode) != nullptr, + "pseudo negative test instruction must build"); + Pseudo->properties().set(rcc::MachineFunctionProperty::NoExecutablePseudos); + Context.expect(!rcc::MachineVerifier::verify(*Pseudo, *Target), + "NoExecutablePseudos must reject an executable pseudo"); + } +} + +void testSelectedStateAndPseudoDeadline(TestContext &Context) { + auto Target = createTarget(); + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + if (!Target) + return; + + auto Tied = createFunction(Context, "tied", 11); + rcc::MachineBasicBlock *TiedBlock = Tied ? createBlock(Context, *Tied) : nullptr; + if (Tied && TiedBlock != nullptr) { + auto Def = Tied->registerInfo().createVirtualRegister(*Type); + auto Use = Tied->registerInfo().createVirtualRegister(*Type); + if (!Def || !Use) + return; + for (const rcc::Register Reg : {*Def, *Use}) { + Context.expect(Tied->registerInfo() + .setRegisterBank(Reg, rcc::RegisterBankID::fromValue(1)) + .has_value() && + Tied->registerInfo() + .setRegisterClass(Reg, rcc::RegisterClassID::fromValue(1)) + .has_value(), + "selected-state register constraints must be recorded"); + } + + rcc::MachineIRBuilder Builder(*Tied, *Target); + Context.expect(Builder.setInsertPoint(*TiedBlock).has_value(), + "tied-state insertion point must be valid"); + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(CopyOpcode)); + Context.expect(Pending && Pending->addDef(*Def).has_value() && + Pending->addUse(*Use).has_value() && Pending->build(), + "tied-state instruction must build"); + for (const rcc::MachineFunctionProperty Property : + {rcc::MachineFunctionProperty::Legalized, rcc::MachineFunctionProperty::RegBankSelected, + rcc::MachineFunctionProperty::Selected, rcc::MachineFunctionProperty::NoPHIs, + rcc::MachineFunctionProperty::TiedOpsRewritten}) { + Tied->properties().set(Property); + } + Context.expect(!rcc::MachineVerifier::verify(*Tied, *Target), + "TiedOpsRewritten must reject unequal tied registers"); + rcc::MachineInstr *Instruction = TiedBlock->firstInstruction(); + Context.expect(Instruction != nullptr && + Instruction->operandAt(1)->setRegister(*Def).has_value() && + rcc::MachineVerifier::verify(*Tied, *Target).has_value(), + "equal tied registers must satisfy TiedOpsRewritten"); + } + + auto Pseudo = createFunction(Context, "pseudo_deadline", 12); + rcc::MachineBasicBlock *Block = Pseudo ? createBlock(Context, *Pseudo) : nullptr; + if (Pseudo && Block != nullptr) { + rcc::MachineIRBuilder Builder(*Pseudo, *Target); + Context.expect(Builder.setInsertPoint(*Block).has_value() && + buildNoOperand(Context, Builder, PseudoOpcode) != nullptr, + "deadline pseudo must build"); + Context.expect( + rcc::MachineVerifier::verify(*Pseudo, *Target, + rcc::MachineVerifierOptions{rcc::MachineVerifierMode::Generic, + rcc::PipelineStage::Selected}) + .has_value(), + "pseudo may remain at its declared deadline"); + Context.expect(!rcc::MachineVerifier::verify( + *Pseudo, *Target, + rcc::MachineVerifierOptions{rcc::MachineVerifierMode::Generic, + rcc::PipelineStage::RegisterAllocated}), + "pseudo must not survive beyond its declared deadline"); + } + + auto Dependency = createFunction(Context, "property_dependency", 13); + if (Dependency) { + Dependency->properties().set(rcc::MachineFunctionProperty::Selected); + Context.expect(!rcc::MachineVerifier::verify(*Dependency, *Target), + "Selected must require prior pipeline properties"); + Dependency->properties().clear(rcc::MachineFunctionProperty::Selected); + Dependency->properties().set(rcc::MachineFunctionProperty::FrameFinalized); + Context.expect(!rcc::MachineVerifier::verify(*Dependency, *Target), + "FrameFinalized must require NoVRegs and NoFrameIndices"); + } + + auto SSAExitDependency = createFunction(Context, "ssa_exit_property_dependency", 65); + if (SSAExitDependency) { + for (const rcc::MachineFunctionProperty Property : + {rcc::MachineFunctionProperty::Legalized, rcc::MachineFunctionProperty::RegBankSelected, + rcc::MachineFunctionProperty::Selected, rcc::MachineFunctionProperty::TiedOpsRewritten}) { + SSAExitDependency->properties().set(Property); + } + Context.expect(!rcc::MachineVerifier::verify(*SSAExitDependency, *Target), + "TiedOpsRewritten must require NoPHIs"); + SSAExitDependency->properties().set(rcc::MachineFunctionProperty::NoPHIs); + Context.expect(rcc::MachineVerifier::verify(*SSAExitDependency, *Target).has_value(), + "TiedOpsRewritten dependency must be satisfied by NoPHIs"); + } +} + +void testFrameStateFailures(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, "frame_references", 14); + rcc::MachineBasicBlock *Block = Function ? createBlock(Context, *Function) : nullptr; + if (!Target || !Function || Block == nullptr) + return; + + rcc::MachineIRBuilder Builder(*Function, *Target); + Context.expect(Builder.setInsertPoint(*Block).has_value(), + "frame reference insertion point must be valid"); + auto Unknown = Builder.buildInstr(rcc::Opcode::fromValue(FrameOpcode)); + Context.expect(Unknown && Unknown->addFrameIndex(rcc::FrameIndex::fromValue(99)).has_value() && + !Unknown->build() && Function->allInstructions().empty(), + "builder must reject an unknown frame index before instruction allocation"); + + rcc::StackObjectDesc StackSlot; + StackSlot.Size = 8; + StackSlot.Align = *rcc::Alignment::create(8); + auto FrameIndex = Function->frameInfo().createObject(StackSlot); + if (!FrameIndex) + return; + auto Live = Builder.buildInstr(rcc::Opcode::fromValue(FrameOpcode)); + Context.expect(Live && Live->addFrameIndex(*FrameIndex).has_value() && Live->build(), + "known live frame index must build"); + Context.expect(Function->frameInfo().markObjectDead(*FrameIndex).has_value() && + !rcc::MachineVerifier::verify(*Function, *Target), + "verifier must reject an instruction that references a dead stack object"); + + auto Lifetime = createFunction(Context, "frame_lifetime", 15); + if (!Lifetime) + return; + rcc::StackObjectDesc LifetimeSlot; + LifetimeSlot.Size = 8; + LifetimeSlot.Align = *rcc::Alignment::create(8); + LifetimeSlot.Lifetime.push_back( + {rcc::MachineInstrStableID::fromValue(1), rcc::MachineInstrStableID::fromValue(2)}); + Context.expect(Lifetime->frameInfo().createObject(LifetimeSlot).has_value() && + !rcc::MachineVerifier::verify(*Lifetime, *Target), + "verifier must reject stack lifetimes that reference unknown instructions"); +} + +void testTableReferenceFailures(TestContext &Context) { + auto Target = createTarget(); + auto ConstantFunction = createFunction(Context, "constant_reference", 16); + rcc::MachineBasicBlock *ConstantBlock = + ConstantFunction ? createBlock(Context, *ConstantFunction) : nullptr; + if (!Target || !ConstantFunction || ConstantBlock == nullptr) + return; + + rcc::MachineConstantPoolEntryDesc ConstantDescription; + auto Integer = rcc::APInt::fromUnsigned(32, 0x01010101U); + ConstantDescription.Value = std::move(*rcc::GenericConstantData::integer(std::move(*Integer))); + ConstantDescription.Size = 4; + ConstantDescription.Align = *rcc::Alignment::create(4); + auto ConstantIndex = ConstantFunction->constantPool().addEntry(std::move(ConstantDescription)); + rcc::MachineIRBuilder ConstantBuilder(*ConstantFunction, *Target); + Context.expect(ConstantIndex && ConstantBuilder.setInsertPoint(*ConstantBlock).has_value(), + "constant-pool verifier setup must succeed"); + if (!ConstantIndex) + return; + auto ConstantInstruction = ConstantBuilder.buildInstr(rcc::Opcode::fromValue(ConstantPoolOpcode)); + Context.expect(ConstantInstruction && + ConstantInstruction->addConstantPool(*ConstantIndex).has_value() && + ConstantInstruction->build() && + ConstantFunction->constantPool().eraseEntry(*ConstantIndex).has_value() && + !rcc::MachineVerifier::verify(*ConstantFunction, *Target), + "verifier must reject a tombstoned constant-pool reference"); + + auto JumpFunction = createFunction(Context, "jump_reference", 17); + rcc::MachineBasicBlock *JumpBlock = JumpFunction ? createBlock(Context, *JumpFunction) : nullptr; + if (!JumpFunction || JumpBlock == nullptr) + return; + rcc::MachineJumpTableDesc JumpDescription; + JumpDescription.Targets = {JumpBlock}; + auto JumpIndex = JumpFunction->jumpTableInfo().createTable(std::move(JumpDescription)); + rcc::MachineIRBuilder JumpBuilder(*JumpFunction, *Target); + Context.expect(JumpIndex && JumpBuilder.setInsertPoint(*JumpBlock).has_value(), + "jump-table verifier setup must succeed"); + if (!JumpIndex) + return; + auto JumpInstruction = JumpBuilder.buildInstr(rcc::Opcode::fromValue(JumpTableOpcode)); + Context.expect(JumpInstruction && JumpInstruction->addJumpTable(*JumpIndex).has_value() && + JumpInstruction->build() && + JumpFunction->jumpTableInfo().eraseTable(*JumpIndex).has_value() && + !rcc::MachineVerifier::verify(*JumpFunction, *Target), + "verifier must reject a tombstoned jump-table reference"); +} + +void testCompilationStatusReferences(TestContext &Context) { + auto Target = createTarget(); + auto Unknown = createFunction(Context, "unknown_failure_instruction", 18); + if (!Target || !Unknown) + return; + + rcc::MachineCompilationFailure UnknownFailure; + UnknownFailure.Diagnostic.Code = rcc::ErrorCode::Unsupported; + UnknownFailure.Diagnostic.Message = "selection failed"; + UnknownFailure.FailureInstruction = rcc::MachineInstrStableID::fromValue(99); + UnknownFailure.FailureStage = rcc::PipelineStage::Selected; + Context.expect( + Unknown->compilationStatus() + .fail(rcc::MachineCompilationState::SelectionFailed, std::move(UnknownFailure)) + .has_value() && + !rcc::MachineVerifier::verify(*Unknown, *Target), + "verifier must reject compilation status that references an unknown instruction"); + + auto Live = createFunction(Context, "live_failure_instruction", 19); + rcc::MachineBasicBlock *Block = Live ? createBlock(Context, *Live) : nullptr; + if (!Live || Block == nullptr) + return; + rcc::MachineIRBuilder Builder(*Live, *Target); + Context.expect(Builder.setInsertPoint(*Block).has_value(), + "live failure instruction insertion point must be valid"); + rcc::MachineInstr *Instruction = buildNoOperand(Context, Builder, NopOpcode); + if (Instruction == nullptr) + return; + + rcc::MachineCompilationFailure LiveFailure; + LiveFailure.Diagnostic.Code = rcc::ErrorCode::Unsupported; + LiveFailure.Diagnostic.Message = "finalization failed"; + LiveFailure.FailureInstruction = Instruction->stableID(); + LiveFailure.FailureStage = rcc::PipelineStage::PostRAFinalized; + Context.expect( + Live->compilationStatus() + .fail(rcc::MachineCompilationState::FinalizationFailed, std::move(LiveFailure)) + .has_value() && + rcc::MachineVerifier::verify(*Live, *Target).has_value(), + "verifier must accept a structurally valid failed function"); +} + +} // namespace + +int main() { + TestContext Context; + testValidFunctionAndModes(Context); + testReachableStructuralFailures(Context); + testPHIStructure(Context); + testPropertyFailures(Context); + testSelectedStateAndPseudoDeadline(Context); + testFrameStateFailures(Context); + testTableReferenceFailures(Context); + testCompilationStatusReferences(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/PBQPTest.cpp b/backend/unittests/CodeGen/PBQPTest.cpp new file mode 100644 index 0000000..073de16 --- /dev/null +++ b/backend/unittests/CodeGen/PBQPTest.cpp @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/PBQP/Graph.h" +#include "rcc/CodeGen/PBQP/Math.h" +#include "rcc/CodeGen/PBQP/Solver.h" + +#include <algorithm> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <string_view> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +void testMath(TestContext &Context) { + using namespace rcc::pbqp; + Context.expect( + addCosts(InfiniteCost, -4) == InfiniteCost && addCosts(InfiniteCost - 1, 2) == InfiniteCost && + addCosts(NegativeInfiniteCost + 1, -2) == NegativeInfiniteCost && addCosts(7, -3) == 4, + "PBQP cost arithmetic must saturate deterministically"); + + CostVector Vector(std::vector<Cost>{8, -2, 4}); + Context.expect(Vector.minimumIndex() == 1, + "PBQP cost vectors must choose the first stable minimum"); + CostMatrix Matrix(2, 3); + Matrix.at(0, 0) = 1; + Matrix.at(0, 1) = 2; + Matrix.at(0, 2) = 3; + Matrix.at(1, 0) = 4; + Matrix.at(1, 1) = 5; + Matrix.at(1, 2) = 6; + const CostMatrix Transposed = Matrix.transpose(); + Context.expect(Transposed.rows() == 3 && Transposed.columns() == 2 && Transposed.at(2, 1) == 6 && + Transposed.at(1, 0) == 2, + "PBQP matrix transposition must preserve orientation"); +} + +void testGraphValidation(TestContext &Context) { + using namespace rcc::pbqp; + Graph Problem; + auto Empty = Problem.addNode(CostVector(0)); + auto First = Problem.addNode(CostVector(std::vector<Cost>{0, 1}), 20); + auto Second = Problem.addNode(CostVector(std::vector<Cost>{2, 0, 3}), 10); + Context.expect(!Empty && First && Second, + "PBQP graphs must reject empty nodes and accept stable nonempty nodes"); + if (!First || !Second) + return; + auto Self = Problem.addEdge(*First, *First, CostMatrix(2, 2)); + auto WrongShape = Problem.addEdge(*First, *Second, CostMatrix(2, 2)); + CostMatrix EdgeCosts(2, 3); + EdgeCosts.at(1, 2) = 7; + auto Edge = Problem.addEdge(*Second, *First, EdgeCosts.transpose(), 99); + auto Duplicate = Problem.addEdge(*First, *Second, CostMatrix(2, 3)); + Context.expect(!Self && !WrongShape && Edge && !Duplicate && Problem.verify().has_value(), + "PBQP graph edges must enforce identity, dimensions, orientation, and uniqueness"); + if (!Edge) + return; + const rcc::pbqp::Edge *Stored = Problem.edge(*Edge); + Context.expect(Stored != nullptr && Stored->First == *First && Stored->Second == *Second && + Stored->Costs.rows() == 2 && Stored->Costs.columns() == 3 && + Stored->Costs.at(1, 2) == 7, + "PBQP graph insertion must normalize reversed edge orientation"); +} + +void testReductions(TestContext &Context) { + using namespace rcc::pbqp; + DeterministicSolver Solver; + + Graph DegreeZero; + auto Only = DegreeZero.addNode(CostVector(std::vector<Cost>{4, 1, 1}), 3); + auto ZeroSolution = Solver.solve(DegreeZero); + Context.expect(Only && ZeroSolution && ZeroSolution->selection(*Only) == 1 && + ZeroSolution->totalCost() == 1, + "PBQP R0 reduction must select the first minimum-cost option"); + + Graph DegreeOne; + auto Left = DegreeOne.addNode(CostVector(std::vector<Cost>{3, 0}), 1); + auto Right = DegreeOne.addNode(CostVector(std::vector<Cost>{0, 2}), 2); + CostMatrix Pair(2, 2); + Pair.at(1, 0) = InfiniteCost; + auto PairEdge = DegreeOne.addEdge(*Left, *Right, std::move(Pair)); + auto OneSolution = Solver.solve(DegreeOne); + Context.expect(Left && Right && PairEdge && OneSolution && OneSolution->selection(*Left) == 1 && + OneSolution->selection(*Right) == 1 && OneSolution->totalCost() == 2, + "PBQP R1 reduction must propagate unary costs to its neighbor"); + + Graph DegreeTwo; + auto A = DegreeTwo.addNode(CostVector(std::vector<Cost>{0, 2}), 1); + auto B = DegreeTwo.addNode(CostVector(std::vector<Cost>{0, 0}), 2); + auto C = DegreeTwo.addNode(CostVector(std::vector<Cost>{2, 0}), 3); + CostMatrix AB(2, 2); + CostMatrix BC(2, 2); + AB.at(0, 1) = 5; + AB.at(1, 0) = 5; + BC.at(0, 1) = 5; + BC.at(1, 0) = 5; + auto ABEdge = DegreeTwo.addEdge(*A, *B, std::move(AB)); + auto BCEdge = DegreeTwo.addEdge(*B, *C, std::move(BC)); + auto TwoSolution = Solver.solve(DegreeTwo); + Context.expect(A && B && C && ABEdge && BCEdge && TwoSolution && + TwoSolution->selection(*A) == 0 && TwoSolution->selection(*B) == 0 && + TwoSolution->selection(*C) == 0 && TwoSolution->totalCost() == 2, + "PBQP R2 reduction must add the induced neighbor edge cost"); +} + +[[nodiscard]] rcc::Expected<rcc::pbqp::Solution> solveClique() { + using namespace rcc::pbqp; + Graph Problem; + std::vector<NodeID> Nodes; + for (std::uint64_t Index = 0; Index < 4; ++Index) { + auto Node = Problem.addNode(CostVector(std::vector<Cost>{9, 0, 0}), 100 - Index); + if (!Node) + return std::unexpected(Node.error()); + Nodes.push_back(*Node); + } + for (std::size_t Left = 0; Left < Nodes.size(); ++Left) { + for (std::size_t Right = Left + 1; Right < Nodes.size(); ++Right) { + CostMatrix Costs(3, 3); + Costs.at(1, 1) = InfiniteCost; + Costs.at(2, 2) = InfiniteCost; + auto Edge = Problem.addEdge(Nodes[Left], Nodes[Right], std::move(Costs), + static_cast<std::uint64_t>(Left * Nodes.size() + Right)); + if (!Edge) + return std::unexpected(Edge.error()); + } + } + return DeterministicSolver{}.solve(Problem); +} + +void testHeuristicAndDeterminism(TestContext &Context) { + auto First = solveClique(); + auto Second = solveClique(); + Context.expect(First && Second && std::ranges::equal(First->selections(), Second->selections()) && + First->totalCost() == Second->totalCost(), + "PBQP higher-degree reduction must be deterministic"); + if (First) { + Context.expect(First->totalCost() == 18, + "a four-node two-register clique must spill exactly two nodes"); + } + + rcc::pbqp::Graph Impossible; + auto Node = Impossible.addNode( + rcc::pbqp::CostVector(std::vector<rcc::pbqp::Cost>{rcc::pbqp::InfiniteCost})); + auto Failure = rcc::pbqp::DeterministicSolver{}.solve(Impossible); + Context.expect(Node && !Failure && + Failure.error().code() == rcc::ErrorCode::RegisterAllocationFailed, + "PBQP solver must diagnose a problem with no finite assignment"); +} + +} // namespace + +int main() { + TestContext Context; + testMath(Context); + testGraphValidation(Context); + testReductions(Context); + testHeuristicAndDeterminism(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/PHIEliminationTest.cpp b/backend/unittests/CodeGen/PHIEliminationTest.cpp new file mode 100644 index 0000000..bbc834f --- /dev/null +++ b/backend/unittests/CodeGen/PHIEliminationTest.cpp @@ -0,0 +1,479 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/PHIElimination.h" + +#include "SSAExitTestSupport.h" +#include "rcc/CodeGen/MachineCFGUpdater.h" +#include "rcc/CodeGen/MachinePassManager.h" +#include "rcc/CodeGen/MachineVerifier.h" + +#include <array> +#include <memory> +#include <utility> + +namespace { + +using namespace rcc::test::ssa; + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> buildJump(rcc::MachineFunction &Function, + const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, + rcc::MachineBasicBlock &Destination) { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = Builder.setInsertPoint(Block); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(testOpcode(JumpOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + auto TargetOperand = Pending->addBasicBlock(Destination); + if (!TargetOperand) + return std::unexpected(TargetOperand.error()); + return Pending->build(); +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> +buildPhi(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Register Destination, + std::span<const std::pair<rcc::Register, rcc::MachineBasicBlock *>> Incoming, + rcc::RegisterOperandFlags DestinationFlags = {}, std::uint32_t DestinationTargetFlags = 0, + rcc::RegisterOperandFlags IncomingFlags = {}, std::uint32_t IncomingTargetFlags = 0) { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = Builder.setInsertPoint(Block); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::PHI)); + if (!Pending) + return std::unexpected(Pending.error()); + auto Def = Pending->addDef(Destination, DestinationFlags, {}, DestinationTargetFlags); + if (!Def) + return std::unexpected(Def.error()); + for (const auto &[Value, Predecessor] : Incoming) { + auto Use = Pending->addUse(Value, IncomingFlags, {}, IncomingTargetFlags); + if (!Use) + return std::unexpected(Use.error()); + auto IncomingBlock = Pending->addBasicBlock(*Predecessor); + if (!IncomingBlock) + return std::unexpected(IncomingBlock.error()); + } + return Pending->build(); +} + +[[nodiscard]] rcc::Expected<void> runPHIElimination(rcc::MachineFunction &Function, + const rcc::TargetDescription &Target, + const rcc::TargetInstrInfo &Info) { + rcc::MachineFunctionPassManager Pipeline; + auto Added = Pipeline.addPass(std::make_unique<rcc::PHIEliminationPass>(Info)); + if (!Added) + return std::unexpected(Added.error()); + rcc::MachineFunctionAnalysisManager Analyses; + return Pipeline.run(Function, Target, Analyses); +} + +[[nodiscard]] std::vector<rcc::MachineInstr *> linkedInstructions(rcc::MachineBasicBlock &Block) { + std::vector<rcc::MachineInstr *> Result; + for (rcc::MachineInstr *Instruction = Block.firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + Result.push_back(Instruction); + } + return Result; +} + +[[nodiscard]] bool hasLiveIn(const rcc::MachineBasicBlock &Block, rcc::PhysicalRegister Reg) { + return std::ranges::any_of(Block.liveIns(), [Reg](const rcc::MachineBasicBlockLiveIn &LiveIn) { + return LiveIn.PhysReg == Reg; + }); +} + +void testParallelPHIsAndPlacement(TestContext &Context, const rcc::TargetDescription &Target, + const TestInstrInfo &Info) { + auto Fixture = createFunction(Context, "parallel_phis", 1, 3); + if (!Fixture.Function || Fixture.Blocks.size() != 3) + return; + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + if (!I64) + return; + auto &Function = *Fixture.Function; + rcc::MachineBasicBlock &Preheader = *Fixture.Blocks[0]; + rcc::MachineBasicBlock &Header = *Fixture.Blocks[1]; + rcc::MachineBasicBlock &Backedge = *Fixture.Blocks[2]; + const rcc::PhysicalRegister Hint = *rcc::PhysicalRegister::create(rcc::test::TestRegister::G1); + + auto InitA = createVReg(Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + auto InitB = createVReg(Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + auto DestinationA = createVReg(Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64, 0x1, Hint); + auto DestinationB = createVReg(Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + Context.expect(InitA && InitB && DestinationA && DestinationB, + "parallel PHI registers must be created"); + if (!InitA || !InitB || !DestinationA || !DestinationB) + return; + + auto CFG = Function.cfgUpdater(); + Context.expect( + CFG.addSuccessor(Preheader, Header, rcc::BranchProbability::one()).has_value() && + CFG.addSuccessor(Header, Backedge, rcc::BranchProbability::one()).has_value() && + CFG.addSuccessor(Backedge, Header, rcc::BranchProbability::one()).has_value(), + "parallel PHI loop CFG must be connected"); + + const std::array InitADef{*InitA}; + const std::array InitBDef{*InitB}; + Context.expect(buildRegisterInstruction(Function, Target, Preheader, testOpcode(DefOpcode), + InitADef, {}, {rcc::RegisterOperandFlag::Dead}) && + buildRegisterInstruction(Function, Target, Preheader, testOpcode(DefOpcode), + InitBDef, {}) && + buildJump(Function, Target, Preheader, Header), + "parallel PHI preheader must be populated"); + + const std::array IncomingA{ + std::pair<rcc::Register, rcc::MachineBasicBlock *>{*InitA, &Preheader}, + std::pair<rcc::Register, rcc::MachineBasicBlock *>{*DestinationB, &Backedge}}; + const std::array IncomingB{ + std::pair<rcc::Register, rcc::MachineBasicBlock *>{*InitB, &Preheader}, + std::pair<rcc::Register, rcc::MachineBasicBlock *>{*DestinationA, &Backedge}}; + Context.expect(buildPhi(Function, Target, Header, *DestinationA, IncomingA, {}, 0x4, {}, 0x8) && + buildPhi(Function, Target, Header, *DestinationB, IncomingB), + "parallel PHIs must be built"); + + rcc::MachineIRBuilder HeaderBuilder(Function, Target); + if (!HeaderBuilder.setInsertPoint(Header)) + return; + auto Meta = HeaderBuilder.buildInstr(testOpcode(MetaOpcode)); + const std::array HeaderUse{*DestinationA}; + Context.expect(Meta && Meta->build() && + buildRegisterInstruction(Function, Target, Header, testOpcode(UseOpcode), {}, + HeaderUse) && + buildJump(Function, Target, Header, Backedge), + "parallel PHI header must contain meta, code, and terminator instructions"); + + const std::array KilledUse{*DestinationA}; + Context.expect(buildRegisterInstruction(Function, Target, Backedge, testOpcode(UseOpcode), {}, + KilledUse, {}, {rcc::RegisterOperandFlag::Kill}) && + buildJump(Function, Target, Backedge, Header), + "parallel PHI backedge must be populated"); + auto InitialLiveness = setSelectedSSAProperties(Function, Target); + Context.expect(InitialLiveness.has_value(), "parallel PHI liveness rebuild must succeed"); + if (!InitialLiveness) + return; + + auto Run = runPHIElimination(Function, Target, Info); + Context.expect(Run.has_value(), "parallel PHI elimination must succeed"); + if (!Run) + return; + Context.expect(!Function.properties().has(rcc::MachineFunctionProperty::IsSSA) && + Function.properties().has(rcc::MachineFunctionProperty::NoPHIs) && + Function.properties().has(rcc::MachineFunctionProperty::TracksLiveness), + "PHI elimination must publish its exact property transition"); + Context.expect(instructionsWithOpcode(Function, rcc::opcode(rcc::GenericOpcode::PHI)).empty(), + "PHI elimination must remove every PHI"); + + const auto HeaderInstructions = linkedInstructions(Header); + Context.expect(HeaderInstructions.size() == 5 && + HeaderInstructions[0]->opcode() == testOpcode(MetaOpcode) && + HeaderInstructions[1]->opcode() == rcc::opcode(rcc::GenericOpcode::COPY) && + HeaderInstructions[2]->opcode() == rcc::opcode(rcc::GenericOpcode::COPY) && + HeaderInstructions[3]->opcode() == testOpcode(UseOpcode) && + HeaderInstructions[4]->opcode() == testOpcode(JumpOpcode), + "destination COPYs must follow the full leading PHI/meta region"); + if (HeaderInstructions.size() < 3) + return; + + const rcc::Register *FreshA = HeaderInstructions[1]->operandAt(1)->getIf<rcc::Register>(); + const rcc::Register *FreshB = HeaderInstructions[2]->operandAt(1)->getIf<rcc::Register>(); + Context.expect(FreshA != nullptr && FreshB != nullptr && *FreshA != *FreshB && + *FreshA != *DestinationA && *FreshB != *DestinationB, + "each PHI must receive an independent fresh incoming vreg"); + if (FreshA == nullptr || FreshB == nullptr) + return; + + const auto BackedgeInstructions = linkedInstructions(Backedge); + Context.expect(BackedgeInstructions.size() == 4 && + BackedgeInstructions[1]->opcode() == rcc::opcode(rcc::GenericOpcode::COPY) && + BackedgeInstructions[2]->opcode() == rcc::opcode(rcc::GenericOpcode::COPY) && + BackedgeInstructions[3]->opcode() == testOpcode(JumpOpcode), + "incoming COPYs must be placed together before the first terminator"); + if (BackedgeInstructions.size() >= 3) { + const auto FirstDef = BackedgeInstructions[1]->operandAt(0)->registerValue(); + const auto FirstUse = BackedgeInstructions[1]->operandAt(1)->registerValue(); + const auto SecondDef = BackedgeInstructions[2]->operandAt(0)->registerValue(); + const auto SecondUse = BackedgeInstructions[2]->operandAt(1)->registerValue(); + Context.expect(FirstDef == *FreshA && FirstUse == *DestinationB && SecondDef == *FreshB && + SecondUse == *DestinationA, + "backedge COPYs must read old PHI destinations into distinct temporaries"); + } + + auto FreshInfo = Function.registerInfo().virtualRegisterInfo(*FreshA); + Context.expect(FreshInfo.has_value(), "fresh PHI vreg must remain registered"); + if (!FreshInfo) + return; + Context.expect(FreshInfo->get().Type == I64, "fresh PHI vreg must clone its LowLevelType"); + Context.expect(FreshInfo->get().Bank == rcc::test::TestRegisterBank::GPR, + "fresh PHI vreg must clone its register bank"); + Context.expect(FreshInfo->get().RegClass == rcc::test::TestRegisterClass::GPR64, + "fresh PHI vreg must clone its register class"); + Context.expect(FreshInfo->get().TargetFlags == 0x1, + "fresh PHI vreg must clone registered target flags"); + Context.expect(FreshInfo->get().AllocationHints.size() == 1, + "fresh PHI vreg must clone the allocation-hint count"); + Context.expect(!FreshInfo->get().AllocationHints.empty() && + FreshInfo->get().AllocationHints.front() == Hint, + "fresh PHI vreg must clone the allocation-hint register"); + Context.expect(HeaderInstructions[1]->operandAt(0)->targetFlags() == 0x4 && + HeaderInstructions[1]->operandAt(1)->targetFlags() == 0x4, + "successor COPY must preserve registered destination target flags"); + + const rcc::MachineInstr *InitDefinition = Preheader.firstInstruction(); + Context.expect( + InitDefinition != nullptr && + !InitDefinition->operandAt(0)->registerFlags().has(rcc::RegisterOperandFlag::Dead) && + !BackedgeInstructions[0]->operandAt(0)->registerFlags().has( + rcc::RegisterOperandFlag::Kill), + "liveness-preserving PHI elimination must conservatively clear kill/dead flags"); + Context.expect(rcc::MachineVerifier::verify(Function, Target).has_value(), + "parallel PHI result must pass MachineVerifier"); +} + +void testCriticalEdgeAndPhysicalLiveIn(TestContext &Context, const rcc::TargetDescription &Target, + const TestInstrInfo &Info) { + auto Fixture = createFunction(Context, "critical_edge", 2, 4); + if (!Fixture.Function || Fixture.Blocks.size() != 4) + return; + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + if (!I64) + return; + auto &Function = *Fixture.Function; + rcc::MachineBasicBlock &From = *Fixture.Blocks[0]; + rcc::MachineBasicBlock &Side = *Fixture.Blocks[1]; + rcc::MachineBasicBlock &Other = *Fixture.Blocks[2]; + rcc::MachineBasicBlock &Merge = *Fixture.Blocks[3]; + auto Condition = createVReg(Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + auto OtherValue = createVReg(Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + auto Destination = createVReg(Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + if (!Condition || !OtherValue || !Destination) + return; + + auto Half = rcc::BranchProbability::fromFraction(1, 2); + auto CFG = Function.cfgUpdater(); + Context.expect(Half && CFG.addSuccessor(From, Side, *Half) && + CFG.addSuccessor(From, Merge, *Half) && + CFG.addSuccessor(Other, Merge, rcc::BranchProbability::one()), + "critical-edge CFG must be connected"); + + const std::array ConditionDef{*Condition}; + Context.expect( + buildRegisterInstruction(Function, Target, From, testOpcode(DefOpcode), ConditionDef, {}) + .has_value(), + "critical-edge condition must be defined"); + rcc::MachineIRBuilder FromBuilder(Function, Target); + if (!FromBuilder.setInsertPoint(From)) + return; + auto Branch = FromBuilder.buildInstr(testOpcode(ConditionalBranchOpcode)); + Context.expect(Branch && Branch->addUse(*Condition) && Branch->addBasicBlock(Merge) && + Branch->build(), + "critical-edge conditional branch must be built"); + const std::array OtherDef{*OtherValue}; + Context.expect( + buildRegisterInstruction(Function, Target, Other, testOpcode(DefOpcode), OtherDef, {}) + .has_value(), + "unrelated fallthrough predecessor must define its incoming value"); + + const rcc::PhysicalRegister G0 = *rcc::PhysicalRegister::create(rcc::test::TestRegister::G0); + const rcc::PhysicalRegister G1 = *rcc::PhysicalRegister::create(rcc::test::TestRegister::G1); + Context.expect(Merge.addLiveIn(G1, rcc::LaneMask::fromRaw(0xF)).has_value(), + "merge block live-in setup must succeed"); + const std::array Incoming{ + std::pair<rcc::Register, rcc::MachineBasicBlock *>{rcc::Register::fromPhysical(G0), &From}, + std::pair<rcc::Register, rcc::MachineBasicBlock *>{*OtherValue, &Other}}; + Context.expect(buildPhi(Function, Target, Merge, *Destination, Incoming).has_value(), + "critical-edge PHI must be built"); + const std::array G1Use{rcc::Register::fromPhysical(G1)}; + Context.expect(buildRegisterInstruction(Function, Target, Merge, testOpcode(UseOpcode), {}, G1Use) + .has_value(), + "merge block must consume its inherited physical live-in"); + auto InitialLiveness = setSelectedSSAProperties(Function, Target); + Context.expect(InitialLiveness.has_value(), "critical-edge PHI liveness rebuild must succeed"); + if (!InitialLiveness) + return; + + auto Run = runPHIElimination(Function, Target, Info); + Context.expect(Run.has_value(), "critical-edge PHI elimination must succeed"); + if (!Run) + return; + + rcc::MachineBasicBlock *Edge = Merge.previous(); + Context.expect( + Edge != nullptr && Edge != &Other && Edge->previous() == &Other && + From.successors().size() == 2 && + std::ranges::any_of(From.successors(), + [Edge](const rcc::MachineSuccessor &S) { return S.Block == Edge; }) && + Edge->predecessors().size() == 1 && Edge->predecessors()[0] == &From && + Edge->successors().size() == 1 && Edge->successors()[0].Block == &Merge, + "critical edge must become a reciprocal From-to-edge-to-Merge CFG path"); + if (Edge == nullptr || Edge == &Other) + return; + + const rcc::MachineOperand *BranchTarget = From.lastInstruction()->operandAt(1); + const auto *BranchBlock = + BranchTarget == nullptr ? nullptr : BranchTarget->getIf<rcc::BasicBlockOperand>(); + Context.expect(BranchBlock != nullptr && BranchBlock->Block == Edge, + "explicit critical-edge branch must target the split block"); + const auto OtherInstructions = linkedInstructions(Other); + const auto EdgeInstructions = linkedInstructions(*Edge); + Context.expect(OtherInstructions.size() == 3 && + OtherInstructions.back()->opcode() == testOpcode(JumpOpcode) && + EdgeInstructions.size() == 2 && + EdgeInstructions[0]->opcode() == rcc::opcode(rcc::GenericOpcode::COPY) && + EdgeInstructions[1]->opcode() == testOpcode(JumpOpcode), + "edge split must preserve unrelated fallthrough and terminator placement"); + Context.expect(hasLiveIn(*Edge, G0) && hasLiveIn(*Edge, G1), + "split edge must inherit successor live-ins and its physical PHI source"); + Context.expect(rcc::MachineVerifier::verify(Function, Target).has_value(), + "critical-edge result must pass MachineVerifier"); +} + +void testUndefAndFailureAtomicity(TestContext &Context, const rcc::TargetDescription &Target, + const TestInstrInfo &Info) { + auto BuildFixture = [&](std::uint64_t ID) { + auto Fixture = createFunction(Context, "undef_phi", ID, 2); + if (!Fixture.Function || Fixture.Blocks.size() != 2) + return Fixture; + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + if (!I64) + return Fixture; + auto Source = createVReg(*Fixture.Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + auto Destination = createVReg(*Fixture.Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + if (!Source || !Destination) + return Fixture; + auto CFG = Fixture.Function->cfgUpdater(); + if (!CFG.addSuccessor(*Fixture.Blocks[0], *Fixture.Blocks[1], rcc::BranchProbability::one()) || + !buildJump(*Fixture.Function, Target, *Fixture.Blocks[0], *Fixture.Blocks[1])) { + return Fixture; + } + const std::array Incoming{ + std::pair<rcc::Register, rcc::MachineBasicBlock *>{*Source, Fixture.Blocks[0]}}; + if (!buildPhi(*Fixture.Function, Target, *Fixture.Blocks[1], *Destination, Incoming, {}, 0, + {rcc::RegisterOperandFlag::Undef})) { + return Fixture; + } + Context.expect(setSelectedSSAProperties(*Fixture.Function, Target).has_value(), + "undef PHI liveness rebuild must succeed"); + return Fixture; + }; + + auto Undef = BuildFixture(3); + if (!Undef.Function) + return; + auto Run = runPHIElimination(*Undef.Function, Target, Info); + Context.expect(Run.has_value(), "undef PHI elimination must succeed"); + if (Run) { + const auto PredecessorInstructions = linkedInstructions(*Undef.Blocks[0]); + Context.expect(PredecessorInstructions.size() == 2 && + PredecessorInstructions[0]->opcode() == + rcc::opcode(rcc::GenericOpcode::IMPLICIT_DEF) && + PredecessorInstructions[1]->opcode() == testOpcode(JumpOpcode), + "undef PHI incoming values must become edge IMPLICIT_DEF instructions"); + } + + auto Atomic = BuildFixture(4); + if (!Atomic.Function) + return; + const std::size_t BlocksBefore = Atomic.Function->blockCount(); + const std::size_t InstructionsBefore = + instructionsWithOpcode(*Atomic.Function, rcc::opcode(rcc::GenericOpcode::PHI)).size(); + const std::size_t RegistersBefore = Atomic.Function->registerInfo().virtualRegisters().size(); + const std::uint64_t PropertiesBefore = Atomic.Function->properties().rawValue(); + TestInstrInfo RejectingInfo(true); + rcc::MachineFunctionAnalysisManager Analyses; + rcc::PHIEliminationPass FailingPass(RejectingInfo); + auto Failed = FailingPass.run(*Atomic.Function, Target, Analyses); + Context.expect(!Failed && Failed.error().code() == rcc::ErrorCode::Unsupported, + "COPY rejection must produce a structured PHI-elimination failure"); + Context.expect( + Atomic.Function->blockCount() == BlocksBefore && + instructionsWithOpcode(*Atomic.Function, rcc::opcode(rcc::GenericOpcode::PHI)).size() == + InstructionsBefore && + Atomic.Function->registerInfo().virtualRegisters().size() == RegistersBefore && + Atomic.Function->properties().rawValue() == PropertiesBefore, + "failed PHI transaction preflight must leave the original function unchanged"); +} + +void testMalformedPHIs(TestContext &Context, const rcc::TargetDescription &Target) { + auto BuildBadPhi = [&](std::uint64_t ID, rcc::LowLevelType DestinationType, + rcc::RegisterBankID DestinationBank, rcc::RegisterClassID DestinationClass, + rcc::LowLevelType SourceType, rcc::RegisterBankID SourceBank, + rcc::RegisterClassID SourceClass, rcc::SubRegIndex SourceSubReg = {}) { + auto Fixture = createFunction(Context, "bad_phi", ID, 2); + if (!Fixture.Function || Fixture.Blocks.size() != 2) + return Fixture; + auto Destination = + createVReg(*Fixture.Function, DestinationType, DestinationBank, DestinationClass); + auto Source = createVReg(*Fixture.Function, SourceType, SourceBank, SourceClass); + if (!Destination || !Source) + return Fixture; + auto CFG = Fixture.Function->cfgUpdater(); + if (!CFG.addSuccessor(*Fixture.Blocks[0], *Fixture.Blocks[1], rcc::BranchProbability::one())) { + return Fixture; + } + const std::array SourceDef{*Source}; + if (!buildRegisterInstruction(*Fixture.Function, Target, *Fixture.Blocks[0], + testOpcode(DefOpcode), SourceDef, {})) { + return Fixture; + } + rcc::MachineIRBuilder Builder(*Fixture.Function, Target); + if (!Builder.setInsertPoint(*Fixture.Blocks[1])) + return Fixture; + auto Phi = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::PHI)); + if (!Phi || !Phi->addDef(*Destination) || !Phi->addUse(*Source, {}, SourceSubReg) || + !Phi->addBasicBlock(*Fixture.Blocks[0]) || !Phi->build()) { + return Fixture; + } + return Fixture; + }; + + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto I32 = rcc::LowLevelType::makeCategoryNeutralScalar(32); + if (!I64 || !I32) + return; + auto BadType = + BuildBadPhi(6, *I64, rcc::test::TestRegisterBank::GPR, rcc::test::TestRegisterClass::GPR64, + *I32, rcc::test::TestRegisterBank::GPR, rcc::test::TestRegisterClass::GPR32); + Context.expect(BadType.Function && !rcc::MachineVerifier::verify(*BadType.Function, Target), + "MachineVerifier must reject incompatible PHI LLTs"); + + auto BadClass = + BuildBadPhi(7, *I64, rcc::test::TestRegisterBank::GPR, rcc::test::TestRegisterClass::GPR64, + *I64, rcc::test::TestRegisterBank::FPR, rcc::test::TestRegisterClass::FPR64); + Context.expect(BadClass.Function && !rcc::MachineVerifier::verify(*BadClass.Function, Target), + "MachineVerifier must reject nonoverlapping PHI register classes"); + + auto BadSubReg = + BuildBadPhi(8, *I32, rcc::test::TestRegisterBank::GPR, rcc::test::TestRegisterClass::GPR32Low, + *I64, rcc::test::TestRegisterBank::GPR, rcc::test::TestRegisterClass::GPR64, + rcc::test::TestSubReg::High32); + Context.expect(BadSubReg.Function && !rcc::MachineVerifier::verify(*BadSubReg.Function, Target), + "MachineVerifier must reject incompatible PHI subregister projections"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "PHI-elimination TestTarget must be created"); + if (!Target) + return Context.result(); + TestInstrInfo Info; + testParallelPHIsAndPlacement(Context, *Target, Info); + testCriticalEdgeAndPhysicalLiveIn(Context, *Target, Info); + testUndefAndFailureAtomicity(Context, *Target, Info); + testMalformedPHIs(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/PassInstrumentationTest.cpp b/backend/unittests/CodeGen/PassInstrumentationTest.cpp new file mode 100644 index 0000000..97ce445 --- /dev/null +++ b/backend/unittests/CodeGen/PassInstrumentationTest.cpp @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachinePassManager.h" + +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <memory> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + std::vector<rcc::InstructionDescriptor> Descriptors; + std::vector<rcc::ImmediatePredicate> Predicates; + auto Appended = rcc::appendGenericInstructionInfo(Descriptors, Predicates); + if (!Appended) + return std::unexpected(Appended.error()); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors), std::move(Predicates)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +class BlockCountAnalysis final { +public: + struct Result final { + std::size_t Count = 0; + }; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "BlockCount"; + } + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &Function, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + return Result{Function.blockCount()}; + } +}; + +class InstrumentedPass final : public rcc::MachineFunctionPass { +public: + [[nodiscard]] const rcc::MachinePassContract &contract() const noexcept override { + static const rcc::MachinePassContract Contract{ + .Name = "Instrumented", + .Requires = {}, + .Sets = {}, + .Clears = {}, + .RequiredAnalyses = + { + rcc::MachineFunctionAnalysisManager::id<BlockCountAnalysis>(), + }, + .VerificationStage = std::nullopt, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected<rcc::MachinePassResult> + run(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineFunctionAnalysisManager &Analyses) override { + auto First = Analyses.getResult<BlockCountAnalysis>(Function, Target); + auto Second = Analyses.getResult<BlockCountAnalysis>(Function, Target); + auto FirstAction = Analyses.requestAction("first", Function); + auto SecondAction = Analyses.requestAction("second", Function); + if (!First || !Second || !FirstAction || !SecondAction || !*FirstAction || *SecondAction) + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "instrumentation action policy was not enforced"); + rcc::MachinePassResult Result; + Result.Preserved = rcc::PreservedAnalyses::all(); + return Result; + } +}; + +void testInstrumentation(TestContext &Context, const rcc::TargetDescription &Target) { + auto Function = + rcc::MachineFunction::create("instrumentation", rcc::MachineFunctionStableID::fromValue(1)); + Context.expect(Function.has_value(), "instrumentation function creation must succeed"); + if (!Function) + return; + + rcc::MachineFunctionAnalysisManager Analyses; + Context.expect(Analyses + .registerAnalysis<BlockCountAnalysis>({ + .Name = "BlockCount", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + .ThreadSafety = rcc::PassThreadSafety::ThreadSafe, + }) + .has_value(), + "instrumentation analysis registration must succeed"); + + rcc::MachinePassInstrumentation Instrumentation; + Instrumentation.setPerFunctionFuel(1); + std::uint32_t BeforePass = 0; + std::uint32_t AfterPass = 0; + std::uint32_t BeforeAnalysis = 0; + std::uint32_t AfterAnalysis = 0; + std::uint32_t CacheHits = 0; + std::uint32_t Printed = 0; + std::uint32_t Changes = 0; + Instrumentation.addBeforeCallback( + [&BeforePass](std::string_view, const rcc::MachineFunction &) { ++BeforePass; }); + Instrumentation.addAfterCallback([&AfterPass](std::string_view, const rcc::MachineFunction &, + const rcc::MachinePassResult &) { ++AfterPass; }); + Instrumentation.addBeforeAnalysisCallback( + [&BeforeAnalysis](std::string_view, const rcc::MachineFunction &) { ++BeforeAnalysis; }); + Instrumentation.addAfterAnalysisCallback( + [&AfterAnalysis, &CacheHits](std::string_view, const rcc::MachineFunction &, bool CacheHit, + std::uint64_t) { + ++AfterAnalysis; + CacheHits += CacheHit ? 1U : 0U; + }); + Instrumentation.addIRPrintCallback([&Printed](rcc::InstrumentationPhase, std::string_view, + const rcc::MachineFunction &) { ++Printed; }); + Instrumentation.addChangeCallback([&Changes](std::string_view, const rcc::MachineFunction &, + rcc::MachinePassChanges ReportedChanges, + bool FingerprintChanged) { + if (ReportedChanges.empty() && !FingerprintChanged) + ++Changes; + }); + + rcc::MachineFunctionPassManager Pipeline; + Context.expect(Pipeline.addPass(std::make_unique<InstrumentedPass>()).has_value() && + Pipeline.run(**Function, Target, Analyses, &Instrumentation).has_value(), + "instrumented pass must run"); + Context.expect(BeforePass == 1 && AfterPass == 1 && BeforeAnalysis == 1 && AfterAnalysis == 3 && + CacheHits == 2 && Printed == 2 && Changes == 1, + "all pass and analysis instrumentation points must run"); + + const auto Events = Instrumentation.actionEvents(); + Context.expect(Events.size() == 2 && Events[0].Accepted && !Events[1].Accepted, + "action instrumentation must record accepted and rejected decisions"); + const auto Statistics = Instrumentation.statistics(); + Context.expect(Statistics.size() == 2, + "instrumentation must collect one pass and one analysis statistic"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), + "pass instrumentation tests require a valid target description"); + if (!Target) + return Context.result(); + + testInstrumentation(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/PassManagerTest.cpp b/backend/unittests/CodeGen/PassManagerTest.cpp new file mode 100644 index 0000000..56c30f3 --- /dev/null +++ b/backend/unittests/CodeGen/PassManagerTest.cpp @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineFunctionAnalysisManager.h" + +#include <atomic> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + std::vector<rcc::InstructionDescriptor> Descriptors; + std::vector<rcc::ImmediatePredicate> Predicates; + auto Appended = rcc::appendGenericInstructionInfo(Descriptors, Predicates); + if (!Appended) + return std::unexpected(Appended.error()); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors), std::move(Predicates)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +class BlockCountAnalysis final { +public: + struct Result final { + std::size_t Count = 0; + }; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "BlockCount"; + } + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &Function, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + ++Runs; + return Result{Function.blockCount()}; + } + + static inline std::atomic<std::uint32_t> Runs = 0; +}; + +class DependentAnalysis final { +public: + struct Result final { + std::size_t Count = 0; + }; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "Dependent"; + } + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &Function, + const rcc::TargetDescription &Target, + rcc::MachineFunctionAnalysisManager &Analyses) { + auto Blocks = Analyses.getResult<BlockCountAnalysis>(Function, Target); + if (!Blocks) + return std::unexpected(Blocks.error()); + return Result{(*Blocks)->Count}; + } +}; + +class StickyAnalysis final { +public: + struct Result final { + [[nodiscard]] bool invalidate(const rcc::MachineFunction &, + const rcc::PreservedAnalyses &) noexcept { + return false; + } + }; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "Sticky"; + } + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + return Result{}; + } +}; + +class ExpiringAnalysis final { +public: + struct Result final { + [[nodiscard]] bool invalidate(const rcc::MachineFunction &, + const rcc::PreservedAnalyses &) noexcept { + return true; + } + }; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "Expiring"; + } + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + return Result{}; + } +}; + +class CycleA final { +public: + struct Result final {}; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "CycleA"; + } + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + return Result{}; + } +}; + +class CycleB final { +public: + struct Result final {}; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "CycleB"; + } + + [[nodiscard]] static rcc::Expected<Result> run(rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::MachineFunctionAnalysisManager &) { + return Result{}; + } +}; + +void testAnalysisManager(TestContext &Context, const rcc::TargetDescription &Target) { + auto Function = + rcc::MachineFunction::create("analysis", rcc::MachineFunctionStableID::fromValue(1)); + Context.expect(Function.has_value(), "analysis function creation must succeed"); + if (!Function) + return; + + rcc::MachineFunctionAnalysisManager Analyses; + Context.expect(Analyses + .registerAnalysis<BlockCountAnalysis>({ + .Name = "BlockCount", + .Dependencies = {}, + .InvalidatedBy = {rcc::MachinePassChange::CFG}, + }) + .has_value(), + "base analysis registration must succeed"); + Context.expect(Analyses + .registerAnalysis<DependentAnalysis>({ + .Name = "Dependent", + .Dependencies = + { + rcc::MachineFunctionAnalysisManager::id<BlockCountAnalysis>(), + }, + .InvalidatedBy = {rcc::MachinePassChange::Operands}, + }) + .has_value(), + "dependent analysis registration must succeed"); + + BlockCountAnalysis::Runs = 0; + auto First = Analyses.getResult<DependentAnalysis>(**Function, Target); + auto Second = Analyses.getResult<DependentAnalysis>(**Function, Target); + Context.expect(First && Second && *First == *Second && BlockCountAnalysis::Runs == 1, + "analysis dependencies must materialize once and then use the cache"); + + Analyses.invalidate(**Function, rcc::PreservedAnalyses::none(), + {rcc::MachinePassChange::Registers}); + Context.expect(Analyses.cachedResultCount(**Function) == 2, + "unrelated changes must preserve cached analyses precisely"); + Analyses.invalidate(**Function, rcc::PreservedAnalyses::none(), {rcc::MachinePassChange::CFG}); + Context.expect(Analyses.cachedResultCount(**Function) == 0, + "invalidating a dependency must invalidate its dependent analysis"); + + rcc::MachineFunctionAnalysisManager Cyclic; + Context.expect( + Cyclic.registerAnalysis<CycleA>({ + .Name = "CycleA", + .Dependencies = + { + rcc::MachineFunctionAnalysisManager::id<CycleB>(), + }, + }) + .has_value() && + Cyclic + .registerAnalysis<CycleB>({ + .Name = "CycleB", + .Dependencies = + { + rcc::MachineFunctionAnalysisManager::id<CycleA>(), + }, + }) + .has_value(), + "cyclic analysis registrations must be accepted until requested"); + Context.expect(!Cyclic.getResult<CycleA>(**Function, Target), + "analysis dependency cycles must produce a structured error"); + + rcc::MachineFunctionAnalysisManager CustomInvalidation; + Context.expect(CustomInvalidation + .registerAnalysis<StickyAnalysis>({ + .Name = "Sticky", + .Dependencies = {}, + .InvalidatedBy = {rcc::MachinePassChange::CFG}, + }) + .has_value() && + CustomInvalidation + .registerAnalysis<ExpiringAnalysis>({ + .Name = "Expiring", + .Dependencies = {}, + .InvalidatedBy = {rcc::MachinePassChange::CFG}, + }) + .has_value() && + CustomInvalidation.getResult<StickyAnalysis>(**Function, Target) && + CustomInvalidation.getResult<ExpiringAnalysis>(**Function, Target), + "custom invalidation analyses must materialize"); + CustomInvalidation.invalidate(**Function, rcc::PreservedAnalyses::none(), + {rcc::MachinePassChange::CFG}); + Context.expect(CustomInvalidation.hasCached( + **Function, rcc::MachineFunctionAnalysisManager::id<StickyAnalysis>()) && + !CustomInvalidation.hasCached( + **Function, rcc::MachineFunctionAnalysisManager::id<ExpiringAnalysis>()), + "analysis invalidate() must return false to preserve and true to invalidate"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "pass manager tests require a valid target description"); + if (!Target) + return Context.result(); + + testAnalysisManager(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/RegAllocBasicTest.cpp b/backend/unittests/CodeGen/RegAllocBasicTest.cpp new file mode 100644 index 0000000..015fe37 --- /dev/null +++ b/backend/unittests/CodeGen/RegAllocBasicTest.cpp @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "CodeGen/RegisterAllocationTestSupport.h" + +#include <iostream> + +namespace { + +using namespace rcc; +using namespace rcc::test; +using namespace rcc::test::ra; + +[[nodiscard]] Expected<MachineInstr *> buildEarly(MachineFunction &Function, + const TargetDescription &Target, + MachineBasicBlock &Block, Register Destination, + Register Source) { + MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(testOpcode(EarlyOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + RegisterOperandFlags Flags; + Flags.set(RegisterOperandFlag::EarlyClobber); + if (auto Result = Pending->addDef(Destination, Flags); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addUse(Source); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +void testHintsAndPressure(TestContext &Context, const TargetDescription &Target) { + auto HintFixture = createFunction("basic-hint", 30); + if (!HintFixture) + return; + auto HintValue = createVReg(*HintFixture->Function); + if (!HintValue || !buildDef(*HintFixture->Function, Target, *HintFixture->Block, *HintValue, 1) || + !buildUse(*HintFixture->Function, Target, *HintFixture->Block, *HintValue) || + !HintFixture->Function->registerInfo().addAllocationHint(*HintValue, + physical(TestRegister::G1)) || + !prepareForAllocation(*HintFixture->Function, Target)) + return; + TestRegisterAllocationInfo Info; + auto Allocated = runAllocator(*HintFixture->Function, Target, Info, AllocatorKind::Basic); + const MachineInstr *Use = HintFixture->Block->lastInstruction(); + Context.expect(Allocated && Use != nullptr && + Use->operandAt(0)->registerValue() == + Register::fromPhysical(physical(TestRegister::G1)), + "BasicAllocator must honor the first allocation hint"); + + auto Pressure = createFunction("basic-pressure", 31); + if (!Pressure) + return; + std::vector<Register> Values; + for (std::int64_t Value = 0; Value < 3; ++Value) { + auto Reg = createVReg(*Pressure->Function); + if (!Reg || !buildDef(*Pressure->Function, Target, *Pressure->Block, *Reg, Value)) + return; + Values.push_back(*Reg); + } + for (const Register Reg : Values) + if (!buildUse(*Pressure->Function, Target, *Pressure->Block, Reg)) + return; + if (!prepareForAllocation(*Pressure->Function, Target)) + return; + Allocated = runAllocator(*Pressure->Function, Target, Info, AllocatorKind::Basic, + {.EnableRematerialization = false}); + Context.expect(Allocated && + Pressure->Function->properties().has(MachineFunctionProperty::NoVRegs) && + Pressure->Function->frameInfo().liveObjectCount() != 0, + "BasicAllocator must close spill/reload pressure"); +} + +void testEarlyClobberAndRewrite(TestContext &Context, const TargetDescription &Target) { + auto Fixture = createFunction("basic-constraints", 32); + if (!Fixture) + return; + auto Source = createVReg(*Fixture->Function); + auto Destination = createVReg(*Fixture->Function); + if (!Source || !Destination || + !buildDef(*Fixture->Function, Target, *Fixture->Block, *Source, 1) || + !buildEarly(*Fixture->Function, Target, *Fixture->Block, *Destination, *Source) || + !buildTied(*Fixture->Function, Target, *Fixture->Block, *Destination) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Destination) || + !prepareForAllocation(*Fixture->Function, Target)) + return; + TestRegisterAllocationInfo Info; + auto Allocated = runAllocator(*Fixture->Function, Target, Info, AllocatorKind::Basic); + const MachineInstr *Early = Fixture->Block->firstInstruction()->next(); + const MachineInstr *Tied = Early == nullptr ? nullptr : Early->next(); + Context.expect(Allocated && Early != nullptr && Tied != nullptr && + Early->operandAt(0)->registerValue() != Early->operandAt(1)->registerValue() && + Tied->operandAt(0)->registerValue() == Tied->operandAt(1)->registerValue(), + "BasicAllocator must preserve early-clobber and tied constraints"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "BasicAllocator test target must be constructible"); + if (!Target) + return Context.result(); + testHintsAndPressure(Context, *Target); + testEarlyClobberAndRewrite(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/RegAllocFastTest.cpp b/backend/unittests/CodeGen/RegAllocFastTest.cpp new file mode 100644 index 0000000..cf5498f --- /dev/null +++ b/backend/unittests/CodeGen/RegAllocFastTest.cpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "CodeGen/RegisterAllocationTestSupport.h" +#include "rcc/CodeGen/MachineCFGUpdater.h" + +#include <iostream> + +namespace { + +using namespace rcc; +using namespace rcc::test; +using namespace rcc::test::ra; + +void testBlockBoundary(TestContext &Context, const TargetDescription &Target) { + auto Fixture = createFunction("fast-boundary", 40); + if (!Fixture) + return; + auto Exit = Fixture->Function->createBasicBlock(); + if (!Exit || !Fixture->Function->appendBlock(**Exit) || + !Fixture->Function->cfgUpdater().addSuccessor(*Fixture->Block, **Exit, + BranchProbability::one())) + return; + auto Value = createVReg(*Fixture->Function); + if (!Value || !buildDef(*Fixture->Function, Target, *Fixture->Block, *Value, 9) || + !buildUse(*Fixture->Function, Target, **Exit, *Value) || + !buildUse(*Fixture->Function, Target, **Exit, + Register::fromPhysical(physical(TestRegister::G0))) || + !prepareForAllocation(*Fixture->Function, Target)) + return; + TestRegisterAllocationInfo Info; + auto Allocated = runAllocator(*Fixture->Function, Target, Info, AllocatorKind::Fast); + Context.expect(Allocated && countOpcode(*Fixture->Function, testOpcode(SpillStoreOpcode)) == 1 && + countOpcode(*Fixture->Function, testOpcode(SpillLoadOpcode)) == 1 && + Fixture->Function->properties().has(MachineFunctionProperty::NoVRegs), + "FastAllocator must preserve values across a block boundary"); +} + +void testFunctionLiveInAndDebug(TestContext &Context, const TargetDescription &Target) { + auto Fixture = createFunction("fast-live-in", 41); + if (!Fixture) + return; + auto Value = createVReg(*Fixture->Function); + const PhysicalRegister G0 = physical(TestRegister::G0); + if (!Value || !Fixture->Function->registerInfo().addLiveIn(G0, *Value) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value, {}, true) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value) || + !prepareForAllocation(*Fixture->Function, Target)) + return; + TestRegisterAllocationInfo Info; + auto Allocated = runAllocator(*Fixture->Function, Target, Info, AllocatorKind::Fast); + const MachineOperand *DebugOperand = nullptr; + for (const MachineInstr *Instruction = Fixture->Block->firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + for (const MachineOperand &Operand : Instruction->operands()) { + if (Operand.isDebugUse()) { + DebugOperand = &Operand; + break; + } + } + if (DebugOperand != nullptr) + break; + } + Context.expect( + Allocated && DebugOperand != nullptr && DebugOperand->registerValue() && + (DebugOperand->registerValue()->isNoRegister() || + DebugOperand->registerValue() == Register::fromPhysical(G0)) && + std::ranges::none_of(Fixture->Function->registerInfo().virtualRegisters(), + [](const VirtualRegisterInfo &Info) { return !Info.IsTombstone; }), + "FastAllocator must preserve function live-ins and debug locations"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "FastAllocator test target must be constructible"); + if (!Target) + return Context.result(); + testBlockBoundary(Context, *Target); + testFunctionLiveInAndDebug(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/RegAllocPBQPTest.cpp b/backend/unittests/CodeGen/RegAllocPBQPTest.cpp new file mode 100644 index 0000000..9b70a9b --- /dev/null +++ b/backend/unittests/CodeGen/RegAllocPBQPTest.cpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "CodeGen/RegisterAllocationTestSupport.h" + +#include <iostream> + +namespace { + +using namespace rcc; +using namespace rcc::test; +using namespace rcc::test::ra; + +[[nodiscard]] Expected<FunctionFixture> createPressureFunction(const TargetDescription &Target, + std::uint64_t ID, bool WithCopy) { + auto Fixture = createFunction("pbqp-pressure", ID); + if (!Fixture) + return std::unexpected(Fixture.error()); + std::vector<Register> Values; + for (std::int64_t Value = 0; Value < 3; ++Value) { + auto Reg = createVReg(*Fixture->Function); + if (!Reg || !buildDef(*Fixture->Function, Target, *Fixture->Block, *Reg, Value)) + return makeUnexpected(ErrorCode::InternalError, "failed to construct PBQP pressure value"); + Values.push_back(*Reg); + } + if (WithCopy && !buildCopy(*Fixture->Function, Target, *Fixture->Block, Values[1], Values[0])) + return makeUnexpected(ErrorCode::InternalError, "failed to construct PBQP copy"); + for (const Register Reg : Values) + if (!buildUse(*Fixture->Function, Target, *Fixture->Block, Reg)) + return makeUnexpected(ErrorCode::InternalError, "failed to construct PBQP use"); + auto Prepared = prepareForAllocation(*Fixture->Function, Target); + if (!Prepared) + return std::unexpected(Prepared.error()); + return Fixture; +} + +[[nodiscard]] std::vector<std::uint64_t> signature(const MachineFunction &Function) { + std::vector<std::uint64_t> Result; + Result.push_back(Function.frameInfo().liveObjectCount()); + for (const MachineBasicBlock *Block = Function.firstBlock(); Block != nullptr; + Block = Block->next()) { + for (const MachineInstr *Instruction = Block->firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + Result.push_back(Instruction->opcode().value()); + for (const MachineOperand &Operand : Instruction->operands()) { + Result.push_back(static_cast<std::uint64_t>(Operand.kind())); + if (const Register *Reg = Operand.getIf<Register>()) + Result.push_back(Reg->opaqueValue()); + } + } + } + return Result; +} + +void testHintsAndDeterminism(TestContext &Context, const TargetDescription &Target) { + auto HintFixture = createFunction("pbqp-hint", 50); + if (!HintFixture) + return; + auto Value = createVReg(*HintFixture->Function); + if (!Value || !buildDef(*HintFixture->Function, Target, *HintFixture->Block, *Value, 1) || + !buildUse(*HintFixture->Function, Target, *HintFixture->Block, *Value) || + !HintFixture->Function->registerInfo().addAllocationHint(*Value, + physical(TestRegister::G1)) || + !HintFixture->Function->registerInfo().addAllocationHint(*Value, + physical(TestRegister::G0)) || + !prepareForAllocation(*HintFixture->Function, Target)) + return; + TestRegisterAllocationInfo Info; + auto Allocated = runAllocator(*HintFixture->Function, Target, Info, AllocatorKind::PBQP); + const MachineInstr *Use = HintFixture->Block->lastInstruction(); + Context.expect(Allocated && Use != nullptr && + Use->operandAt(0)->registerValue() == + Register::fromPhysical(physical(TestRegister::G1)), + "PBQP must preserve allocation-hint preference order"); + + auto First = createPressureFunction(Target, 51, false); + auto Second = createPressureFunction(Target, 52, false); + if (!First || !Second) + return; + auto FirstResult = runAllocator(*First->Function, Target, Info, AllocatorKind::PBQP, + {.EnableRematerialization = false}); + auto SecondResult = runAllocator(*Second->Function, Target, Info, AllocatorKind::PBQP, + {.EnableRematerialization = false}); + Context.expect(FirstResult && SecondResult && + signature(*First->Function) == signature(*Second->Function), + "PBQP allocation and spill/rebuild order must be deterministic"); +} + +void testCopyCoalescingSwitch(TestContext &Context, const TargetDescription &Target) { + TestRegisterAllocationInfo Info; + auto Coalesced = createPressureFunction(Target, 53, true); + auto Uncoalesced = createPressureFunction(Target, 54, true); + if (!Coalesced || !Uncoalesced) + return; + auto WithCoalescing = + runAllocator(*Coalesced->Function, Target, Info, AllocatorKind::PBQP, + {.EnableRematerialization = false, .EnableCopyCoalescing = true}); + auto WithoutCoalescing = + runAllocator(*Uncoalesced->Function, Target, Info, AllocatorKind::PBQP, + {.EnableRematerialization = false, .EnableCopyCoalescing = false}); + Context.expect(WithCoalescing && WithoutCoalescing && + Coalesced->Function->properties().has(MachineFunctionProperty::NoVRegs) && + Uncoalesced->Function->properties().has(MachineFunctionProperty::NoVRegs), + "PBQP copy-coalescing modes must both produce valid physical mappings"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "PBQP allocator test target must be constructible"); + if (!Target) + return Context.result(); + testHintsAndDeterminism(Context, *Target); + testCopyCoalescingSwitch(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/RegisterAllocationTest.cpp b/backend/unittests/CodeGen/RegisterAllocationTest.cpp new file mode 100644 index 0000000..443d00a --- /dev/null +++ b/backend/unittests/CodeGen/RegisterAllocationTest.cpp @@ -0,0 +1,1044 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/LiveIntervals.h" +#include "rcc/CodeGen/LiveRangeEdit.h" +#include "rcc/CodeGen/LiveRegMatrix.h" +#include "rcc/CodeGen/LiveVariables.h" +#include "rcc/CodeGen/LowLevelType.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineCFGUpdater.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachinePassManager.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/RegAllocCommon.h" +#include "rcc/CodeGen/RegAllocFast.h" +#include "rcc/CodeGen/RegAllocPBQP.h" +#include "rcc/CodeGen/RegisterMask.h" +#include "rcc/CodeGen/Spiller.h" +#include "rcc/CodeGen/TargetDescription.h" +#include "rcc/CodeGen/TargetRegisterAllocationInfo.h" +#include "rcc/CodeGen/VirtRegMap.h" + +#include <algorithm> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#include <ranges> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t DefOpcode = 3000; +constexpr std::uint32_t RematerializableDefOpcode = 3001; +constexpr std::uint32_t UseOpcode = 3002; +constexpr std::uint32_t BinaryOpcode = 3003; +constexpr std::uint32_t SpillLoadOpcode = 3004; +constexpr std::uint32_t SpillStoreOpcode = 3005; +constexpr std::uint32_t MaskOpcode = 3006; +constexpr std::uint32_t EarlyOpcode = 3007; +constexpr std::uint32_t TiedOpcode = 3008; +constexpr std::uint32_t CallDefOpcode = 3009; + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +[[nodiscard]] constexpr rcc::Opcode testOpcode(std::uint32_t Value) noexcept { + return rcc::Opcode::fromValue(Value); +} + +[[nodiscard]] rcc::PhysicalRegister physical(std::uint32_t Number) { + return *rcc::PhysicalRegister::create(Number); +} + +[[nodiscard]] rcc::OperandDescriptor registerOperand(rcc::RegisterOperandRole Role, + bool EarlyClobber = false) { + rcc::OperandDescriptor Result; + Result.Kinds = {rcc::MachineOperand::Kind::Register}; + Result.RegisterRole = Role; + Result.AllowSubReg = true; + Result.EarlyClobber = EarlyClobber; + return Result; +} + +[[nodiscard]] rcc::OperandDescriptor plainOperand(rcc::MachineOperand::Kind Kind) { + rcc::OperandDescriptor Result; + Result.Kinds = {Kind}; + return Result; +} + +[[nodiscard]] rcc::InstructionDescriptor descriptor(std::uint32_t Value, std::string Name) { + rcc::InstructionDescriptor Result; + Result.InstructionOpcode = testOpcode(Value); + Result.Name = std::move(Name); + Result.Flags = {rcc::InstructionFlag::ProducesCode}; + return Result; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> +createTarget(bool AddSharedMultiLaneUnit = false) { + std::vector<rcc::InstructionDescriptor> Descriptors; + + auto Def = descriptor(DefOpcode, "RA_DEF"); + Def.Operands = {registerOperand(rcc::RegisterOperandRole::Def), + plainOperand(rcc::MachineOperand::Kind::Immediate)}; + Def.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Def)); + + auto Remat = descriptor(RematerializableDefOpcode, "RA_REMAT_DEF"); + Remat.Operands = {registerOperand(rcc::RegisterOperandRole::Def), + plainOperand(rcc::MachineOperand::Kind::Immediate)}; + Remat.NumExplicitDefs = 1; + Remat.Flags.set(rcc::InstructionFlag::Rematerializable); + Descriptors.push_back(std::move(Remat)); + + auto Use = descriptor(UseOpcode, "RA_USE"); + Use.Operands = {registerOperand(rcc::RegisterOperandRole::Use)}; + Use.Flags.set(rcc::InstructionFlag::HasSideEffects); + Descriptors.push_back(std::move(Use)); + + auto Binary = descriptor(BinaryOpcode, "RA_BINARY"); + Binary.Operands = {registerOperand(rcc::RegisterOperandRole::Def), + registerOperand(rcc::RegisterOperandRole::Use), + registerOperand(rcc::RegisterOperandRole::Use)}; + Binary.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Binary)); + + auto Reload = descriptor(SpillLoadOpcode, "RA_RELOAD"); + Reload.Operands = {registerOperand(rcc::RegisterOperandRole::Def), + plainOperand(rcc::MachineOperand::Kind::FrameIndex)}; + Reload.NumExplicitDefs = 1; + Reload.Flags.set(rcc::InstructionFlag::MayLoad); + Reload.RequiresMemoryOperands = true; + Descriptors.push_back(std::move(Reload)); + + auto Spill = descriptor(SpillStoreOpcode, "RA_SPILL"); + Spill.Operands = {registerOperand(rcc::RegisterOperandRole::Use), + plainOperand(rcc::MachineOperand::Kind::FrameIndex)}; + Spill.Flags.set(rcc::InstructionFlag::MayStore); + Spill.RequiresMemoryOperands = true; + Descriptors.push_back(std::move(Spill)); + + auto Mask = descriptor(MaskOpcode, "RA_MASK"); + Mask.Operands = {plainOperand(rcc::MachineOperand::Kind::RegisterMask)}; + Mask.Flags.set(rcc::InstructionFlag::Call); + Mask.Flags.set(rcc::InstructionFlag::HasSideEffects); + Descriptors.push_back(std::move(Mask)); + + auto Early = descriptor(EarlyOpcode, "RA_EARLY"); + Early.Operands = {registerOperand(rcc::RegisterOperandRole::Def, true), + registerOperand(rcc::RegisterOperandRole::Use)}; + Early.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Early)); + + auto Tied = descriptor(TiedOpcode, "RA_TIED"); + auto TiedDef = registerOperand(rcc::RegisterOperandRole::Def); + auto TiedUse = registerOperand(rcc::RegisterOperandRole::Use); + TiedDef.DefaultTiedOperand = 1; + TiedUse.DefaultTiedOperand = 0; + Tied.Operands = {TiedDef, TiedUse}; + Tied.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Tied)); + + auto CallDef = descriptor(CallDefOpcode, "RA_CALL_DEF"); + CallDef.Operands = {registerOperand(rcc::RegisterOperandRole::Def), + plainOperand(rcc::MachineOperand::Kind::RegisterMask)}; + CallDef.NumExplicitDefs = 1; + CallDef.Flags.set(rcc::InstructionFlag::Call); + CallDef.Flags.set(rcc::InstructionFlag::HasSideEffects); + Descriptors.push_back(std::move(CallDef)); + + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors), {}); + if (!Instructions) + return std::unexpected(Instructions.error()); + if (AddSharedMultiLaneUnit) { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + const rcc::RegisterUnitID SharedUnit = rcc::RegisterUnitID::fromValue(12); + Description.RegisterUnits.push_back({SharedUnit, "shared.multi-lane"}); + for (rcc::PhysicalRegisterDesc &Register : Description.PhysicalRegisters) { + if (Register.Reg == physical(rcc::test::TestRegister::G0) || + Register.Reg == physical(rcc::test::TestRegister::G1)) { + Register.Units.push_back({SharedUnit, rcc::LaneMask::fromRaw(0xF)}); + } + } + auto Registers = rcc::TargetRegisterInfo::create(std::move(Description)); + if (!Registers) + return std::unexpected(Registers.error()); + return rcc::TargetDescription::create(std::move(*Instructions), std::move(*Registers), + rcc::test::createTestTargetMIRTextInfoDescription()); + } + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] rcc::Expected<const rcc::MachineMemOperand *> +createStackMemory(rcc::MachineIRBuilder &Builder, rcc::FrameIndex StackSlot, + rcc::MemoryAccessFlag Access) { + auto Size = rcc::MemorySize::fixed(8); + auto Align = rcc::Alignment::create(8); + if (!Size) + return std::unexpected(Size.error()); + if (!Align) + return std::unexpected(Align.error()); + rcc::MachineMemOperandDesc Description; + Description.Accesses = {Access}; + Description.Size = *Size; + Description.Align = *Align; + Description.Base = rcc::MemoryBase::frameIndex(StackSlot); + return Builder.function().createMemOperand(std::move(Description)); +} + +class TestRegisterAllocationInfo final : public rcc::TargetRegisterAllocationInfo { +public: + [[nodiscard]] rcc::Expected<void> + validateSpillReload(rcc::RegisterClassID Class, + const rcc::TargetDescription &Target) const override { + const rcc::RegisterClassDesc *Description = Target.registers().registerClass(Class); + if (Description == nullptr || !Description->Allocatable || Description->SpillSizeInBits != 64 || + Description->SpillAlignmentInBits != 64) { + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, + "test target register class is not spillable"); + } + return {}; + } + + [[nodiscard]] rcc::Expected<rcc::MachineInstr *> + emitReload(rcc::MachineIRBuilder &Builder, rcc::Register Destination, rcc::RegisterClassID Class, + rcc::FrameIndex StackSlot, rcc::LaneMask Lanes) const override { + auto Validated = validateSpillReload(Class, Builder.target()); + if (!Validated) + return std::unexpected(Validated.error()); + if (Lanes.empty()) + return rcc::makeUnexpected(rcc::ErrorCode::InvalidArgument, "test reload requires lanes"); + auto Memory = createStackMemory(Builder, StackSlot, rcc::MemoryAccessFlag::Load); + if (!Memory) + return std::unexpected(Memory.error()); + auto Pending = Builder.buildInstr(testOpcode(SpillLoadOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Destination); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addFrameIndex(StackSlot); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addMemOperand(**Memory); !Result) + return std::unexpected(Result.error()); + return Pending->build(); + } + + [[nodiscard]] rcc::Expected<rcc::MachineInstr *> + emitSpill(rcc::MachineIRBuilder &Builder, rcc::Register Source, rcc::RegisterClassID Class, + rcc::FrameIndex StackSlot, rcc::LaneMask Lanes) const override { + auto Validated = validateSpillReload(Class, Builder.target()); + if (!Validated) + return std::unexpected(Validated.error()); + if (Lanes.empty()) + return rcc::makeUnexpected(rcc::ErrorCode::InvalidArgument, "test spill requires lanes"); + auto Memory = createStackMemory(Builder, StackSlot, rcc::MemoryAccessFlag::Store); + if (!Memory) + return std::unexpected(Memory.error()); + auto Pending = Builder.buildInstr(testOpcode(SpillStoreOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addUse(Source); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addFrameIndex(StackSlot); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addMemOperand(**Memory); !Result) + return std::unexpected(Result.error()); + return Pending->build(); + } + + [[nodiscard]] rcc::Expected<bool> + isRematerializable(const rcc::MachineInstr &Instruction, rcc::Register VirtualReg, + const rcc::MachineFunction &, const rcc::TargetDescription &) const override { + const rcc::MachineOperand *Operand = Instruction.operandAt(0); + const rcc::Register *Reg = Operand == nullptr ? nullptr : Operand->getIf<rcc::Register>(); + return Instruction.opcode() == testOpcode(RematerializableDefOpcode) && Reg != nullptr && + *Reg == VirtualReg; + } + + [[nodiscard]] rcc::Expected<rcc::MachineInstr *> + rematerialize(rcc::MachineIRBuilder &Builder, const rcc::MachineInstr &Original, + rcc::Register Destination, rcc::Register OriginalRegister, + const rcc::TargetDescription &Target) const override { + auto Accepted = isRematerializable(Original, OriginalRegister, Builder.function(), Target); + if (!Accepted) + return std::unexpected(Accepted.error()); + if (!*Accepted) + return rcc::makeUnexpected(rcc::ErrorCode::InvalidArgument, + "test instruction is not rematerializable"); + auto Pending = Builder.buildInstr(testOpcode(RematerializableDefOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Destination); !Result) + return std::unexpected(Result.error()); + const rcc::MachineOperand *Immediate = Original.operandAt(1); + if (Immediate == nullptr) + return rcc::makeUnexpected(rcc::ErrorCode::InternalError, + "test rematerialization lost its immediate"); + if (auto Result = Pending->addOperand(Immediate->cloneDetached()); !Result) + return std::unexpected(Result.error()); + return Pending->build(); + } +}; + +class RejectingRegisterAllocationInfo final : public rcc::TargetRegisterAllocationInfo { +public: + [[nodiscard]] rcc::Expected<void> + validateSpillReload(rcc::RegisterClassID, const rcc::TargetDescription &) const override { + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, + "test target intentionally rejects spills"); + } + + [[nodiscard]] rcc::Expected<rcc::MachineInstr *> emitReload(rcc::MachineIRBuilder &, + rcc::Register, rcc::RegisterClassID, + rcc::FrameIndex, + rcc::LaneMask) const override { + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, + "test target intentionally rejects reloads"); + } + + [[nodiscard]] rcc::Expected<rcc::MachineInstr *> emitSpill(rcc::MachineIRBuilder &, rcc::Register, + rcc::RegisterClassID, rcc::FrameIndex, + rcc::LaneMask) const override { + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, + "test target intentionally rejects spills"); + } +}; + +struct FunctionFixture final { + std::unique_ptr<rcc::MachineFunction> Function; + rcc::MachineBasicBlock *Block = nullptr; +}; + +[[nodiscard]] rcc::Expected<FunctionFixture> createFunction(std::string Name, std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + if (!Function) + return std::unexpected(Function.error()); + auto Block = (*Function)->createBasicBlock(); + if (!Block) + return std::unexpected(Block.error()); + auto Appended = (*Function)->appendBlock(**Block); + if (!Appended) + return std::unexpected(Appended.error()); + return FunctionFixture{std::move(*Function), *Block}; +} + +[[nodiscard]] rcc::Expected<rcc::Register> +createVReg(rcc::MachineFunction &Function, + rcc::RegisterClassID Class = rcc::test::TestRegisterClass::GPR64) { + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + if (!Type) + return std::unexpected(Type.error()); + auto Reg = Function.registerInfo().createVirtualRegister(*Type); + if (!Reg) + return std::unexpected(Reg.error()); + if (auto Result = Function.registerInfo().setRegisterBank(*Reg, rcc::test::TestRegisterBank::GPR); + !Result) { + return std::unexpected(Result.error()); + } + if (auto Result = Function.registerInfo().setRegisterClass(*Reg, Class); !Result) + return std::unexpected(Result.error()); + return *Reg; +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> +buildDef(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Register Reg, std::int64_t Value, + rcc::SubRegIndex SubReg = {}, bool Rematerializable = false) { + rcc::MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = + Builder.buildInstr(testOpcode(Rematerializable ? RematerializableDefOpcode : DefOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Reg, {}, SubReg); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addImmediate(Value); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> +buildUse(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Register Reg, rcc::SubRegIndex SubReg = {}) { + rcc::MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(testOpcode(UseOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addUse(Reg, {}, SubReg); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> +buildMask(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, std::shared_ptr<const rcc::RegisterMask> Mask) { + rcc::MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(testOpcode(MaskOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addRegisterMask(std::move(Mask)); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> +buildCallDef(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Register Destination, + std::shared_ptr<const rcc::RegisterMask> Mask) { + rcc::MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(testOpcode(CallDefOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Destination); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addRegisterMask(std::move(Mask)); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> +buildEarly(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Register Destination, rcc::Register Source) { + rcc::MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(testOpcode(EarlyOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + rcc::RegisterOperandFlags Flags; + Flags.set(rcc::RegisterOperandFlag::EarlyClobber); + if (auto Result = Pending->addDef(Destination, Flags); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addUse(Source); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] rcc::Expected<rcc::MachineInstr *> buildTied(rcc::MachineFunction &Function, + const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, + rcc::Register Reg) { + rcc::MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(testOpcode(TiedOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Reg); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addUse(Reg); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->tieOperands(0, 1); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] rcc::Expected<void> prepareForAllocation(rcc::MachineFunction &Function, + const rcc::TargetDescription &Target) { + auto Rebuilt = rcc::rebuildMachineLiveness(Function, Target); + if (!Rebuilt) + return std::unexpected(Rebuilt.error()); + for (const rcc::MachineFunctionProperty Property : { + rcc::MachineFunctionProperty::TracksLiveness, + rcc::MachineFunctionProperty::Legalized, + rcc::MachineFunctionProperty::RegBankSelected, + rcc::MachineFunctionProperty::Selected, + rcc::MachineFunctionProperty::NoPHIs, + rcc::MachineFunctionProperty::TiedOpsRewritten, + }) { + Function.properties().set(Property); + } + return {}; +} + +enum class AllocatorKind : std::uint8_t { + Basic, + Fast, + PBQP, +}; + +[[nodiscard]] rcc::Expected<void> runAllocator(rcc::MachineFunction &Function, + const rcc::TargetDescription &Target, + const rcc::TargetRegisterAllocationInfo &Info, + AllocatorKind Kind, + rcc::RegisterAllocationOptions Options = {}) { + rcc::MachineFunctionAnalysisManager Analyses; + auto Registered = rcc::registerStage11MachineAnalyses(Analyses); + if (!Registered) + return std::unexpected(Registered.error()); + std::unique_ptr<rcc::MachineFunctionPass> Pass; + switch (Kind) { + case AllocatorKind::Basic: + Pass = rcc::createBasicAllocatorPass(Info, Options); + break; + case AllocatorKind::Fast: + Pass = std::make_unique<rcc::FastAllocatorPass>(Info); + break; + case AllocatorKind::PBQP: + Pass = std::make_unique<rcc::PBQPAllocatorPass>(Info, Options); + break; + } + rcc::MachineFunctionPassManager Pipeline; + auto Added = Pipeline.addPass(std::move(Pass)); + if (!Added) + return std::unexpected(Added.error()); + return Pipeline.run(Function, Target, Analyses); +} + +[[nodiscard]] std::size_t countOpcode(const rcc::MachineFunction &Function, std::uint32_t Value) { + std::size_t Count = 0; + for (const rcc::MachineBasicBlock *Block = Function.firstBlock(); Block != nullptr; + Block = Block->next()) { + for (const rcc::MachineInstr *Instruction = Block->firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + Count += Instruction->opcode() == testOpcode(Value) ? 1U : 0U; + } + } + return Count; +} + +[[nodiscard]] bool hasInterference(std::span<const rcc::RegisterInterference> Values, + rcc::RegisterInterferenceKind Kind) { + return std::ranges::any_of( + Values, [Kind](const rcc::RegisterInterference &Value) { return Value.Kind == Kind; }); +} + +[[nodiscard]] std::vector<std::uint64_t> functionSignature(const rcc::MachineFunction &Function) { + std::vector<std::uint64_t> Result; + Result.push_back(Function.frameInfo().liveObjectCount()); + for (const rcc::MachineBasicBlock *Block = Function.firstBlock(); Block != nullptr; + Block = Block->next()) { + Result.push_back(Block->stableID().value()); + for (const rcc::MachineInstr *Instruction = Block->firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + Result.push_back(Instruction->opcode().value()); + Result.push_back(Instruction->operandCount()); + for (const rcc::MachineOperand &Operand : Instruction->operands()) { + Result.push_back(static_cast<std::uint64_t>(Operand.kind())); + const rcc::Register *Reg = Operand.getIf<rcc::Register>(); + Result.push_back(Reg == nullptr ? 0 : Reg->opaqueValue()); + } + } + } + return Result; +} + +void testVirtRegMapAndEdit(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction("virtreg-map", 1); + if (!Fixture) + return; + auto Parent = createVReg(*Fixture->Function); + if (!Parent) + return; + rcc::VirtRegMap Mapping(*Fixture->Function); + Context.expect(Mapping.add(*Parent).has_value(), + "VirtRegMap must accept a known virtual register"); + rcc::LiveRangeEdit Edit(*Fixture->Function, Target, Mapping); + auto Child = Edit.createFrom(*Parent, false); + auto Slot = Edit.getOrCreateSpillSlot(*Parent); + Context.expect(Child && Slot && Mapping.stackSlot(*Parent) == Slot && + Mapping.entry(*Child) != nullptr && + Mapping.entry(*Child)->SplitParent == *Parent && + Mapping.entry(*Child)->Origin == *Parent && Mapping.verify().has_value(), + "LiveRangeEdit must preserve class, origin, split, and spill-slot state"); + if (!Child) + return; + const auto G0 = physical(rcc::test::TestRegister::G0); + Context.expect(Mapping.assign(*Child, G0).has_value() && Mapping.physicalRegister(*Child) == G0 && + !Mapping.isSpillable(*Child), + "VirtRegMap must publish deterministic child assignments"); +} + +void testLiveRegMatrix(TestContext &Context, const rcc::TargetDescription &Target) { + auto LaneFixture = createFunction("lane-matrix", 2); + if (!LaneFixture) + return; + auto Low = createVReg(*LaneFixture->Function); + auto High = createVReg(*LaneFixture->Function); + if (!Low || !High) + return; + if (!buildDef(*LaneFixture->Function, Target, *LaneFixture->Block, *Low, 1, + rcc::test::TestSubReg::Low32) || + !buildDef(*LaneFixture->Function, Target, *LaneFixture->Block, *High, 2, + rcc::test::TestSubReg::High32) || + !buildUse(*LaneFixture->Function, Target, *LaneFixture->Block, *High, + rcc::test::TestSubReg::High32) || + !buildUse(*LaneFixture->Function, Target, *LaneFixture->Block, *Low, + rcc::test::TestSubReg::Low32)) { + return; + } + if (!rcc::rebuildMachineLiveness(*LaneFixture->Function, Target)) + return; + rcc::MachineFunctionAnalysisManager LaneAnalyses; + if (!rcc::registerStage11MachineAnalyses(LaneAnalyses)) + return; + auto LaneIntervals = + LaneAnalyses.getResult<rcc::LiveIntervalsAnalysis>(*LaneFixture->Function, Target); + if (!LaneIntervals) + return; + const auto G0 = physical(rcc::test::TestRegister::G0); + rcc::LiveRegMatrix LaneMatrix(*LaneFixture->Function, Target, **LaneIntervals); + Context.expect(LaneMatrix.assign(*Low, G0).has_value() && + LaneMatrix.checkInterference(*High, G0).Kind == + rcc::RegisterInterferenceKind::Free && + LaneMatrix.assign(*High, G0).has_value(), + "LiveRegMatrix must permit overlapping intervals on disjoint subregister lanes"); + + auto FixedFixture = createFunction("fixed-matrix", 3); + if (!FixedFixture) + return; + auto Value = createVReg(*FixedFixture->Function); + if (!Value || !buildDef(*FixedFixture->Function, Target, *FixedFixture->Block, *Value, 7) || + !buildMask(*FixedFixture->Function, Target, *FixedFixture->Block, + rcc::test::createTestCallPreservedMask()) || + !buildUse(*FixedFixture->Function, Target, *FixedFixture->Block, + rcc::Register::fromPhysical(G0)) || + !buildUse(*FixedFixture->Function, Target, *FixedFixture->Block, *Value)) { + return; + } + if (!rcc::rebuildMachineLiveness(*FixedFixture->Function, Target)) + return; + rcc::MachineFunctionAnalysisManager FixedAnalyses; + if (!rcc::registerStage11MachineAnalyses(FixedAnalyses)) + return; + auto FixedIntervals = + FixedAnalyses.getResult<rcc::LiveIntervalsAnalysis>(*FixedFixture->Function, Target); + if (!FixedIntervals) + return; + rcc::LiveRegMatrix FixedMatrix(*FixedFixture->Function, Target, **FixedIntervals); + const auto G0Interferences = FixedMatrix.collectInterferences(*Value, G0); + const auto G1 = physical(rcc::test::TestRegister::G1); + Context.expect( + hasInterference(G0Interferences, rcc::RegisterInterferenceKind::RegisterMask) && + (hasInterference(G0Interferences, rcc::RegisterInterferenceKind::FixedRegister) || + hasInterference(G0Interferences, rcc::RegisterInterferenceKind::RegisterUnit)) && + FixedMatrix.checkInterference(*Value, G1).Kind == rcc::RegisterInterferenceKind::Free, + "LiveRegMatrix must classify fixed, unit, and RegMask interference separately"); + + auto FixedLaneFixture = createFunction("fixed-lane-matrix", 4); + if (!FixedLaneFixture) + return; + auto HighValue = createVReg(*FixedLaneFixture->Function); + const auto G0Low32 = physical(rcc::test::TestRegister::G0Low32); + if (!HighValue || + !buildDef(*FixedLaneFixture->Function, Target, *FixedLaneFixture->Block, *HighValue, 9, + rcc::test::TestSubReg::High32) || + !buildUse(*FixedLaneFixture->Function, Target, *FixedLaneFixture->Block, + rcc::Register::fromPhysical(G0Low32)) || + !buildUse(*FixedLaneFixture->Function, Target, *FixedLaneFixture->Block, *HighValue, + rcc::test::TestSubReg::High32) || + !rcc::rebuildMachineLiveness(*FixedLaneFixture->Function, Target)) { + return; + } + rcc::MachineFunctionAnalysisManager FixedLaneAnalyses; + if (!rcc::registerStage11MachineAnalyses(FixedLaneAnalyses)) + return; + auto FixedLaneIntervals = + FixedLaneAnalyses.getResult<rcc::LiveIntervalsAnalysis>(*FixedLaneFixture->Function, Target); + if (!FixedLaneIntervals) + return; + rcc::LiveRegMatrix FixedLaneMatrix(*FixedLaneFixture->Function, Target, **FixedLaneIntervals); + Context.expect(FixedLaneMatrix.checkInterference(*HighValue, G0).Kind == + rcc::RegisterInterferenceKind::Free, + "a fixed subregister must not interfere with a disjoint virtual lane"); +} + +void testSharedUnitLaneMatrix(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction("shared-unit-lane-matrix", 5); + if (!Fixture) + return; + auto Low = createVReg(*Fixture->Function); + auto High = createVReg(*Fixture->Function); + if (!Low || !High || + !buildDef(*Fixture->Function, Target, *Fixture->Block, *Low, 1, + rcc::test::TestSubReg::Low32) || + !buildDef(*Fixture->Function, Target, *Fixture->Block, *High, 2, + rcc::test::TestSubReg::High32) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *High, + rcc::test::TestSubReg::High32) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Low, rcc::test::TestSubReg::Low32) || + !rcc::rebuildMachineLiveness(*Fixture->Function, Target)) { + return; + } + rcc::MachineFunctionAnalysisManager Analyses; + if (!rcc::registerStage11MachineAnalyses(Analyses)) + return; + auto Intervals = Analyses.getResult<rcc::LiveIntervalsAnalysis>(*Fixture->Function, Target); + if (!Intervals) + return; + const auto G0 = physical(rcc::test::TestRegister::G0); + const auto G1 = physical(rcc::test::TestRegister::G1); + rcc::LiveRegMatrix Matrix(*Fixture->Function, Target, **Intervals); + Context.expect(Target.registers().regsOverlap(G0, G1) && Matrix.assign(*Low, G0).has_value() && + Matrix.checkInterference(*High, G1).Kind == + rcc::RegisterInterferenceKind::Free && + Matrix.assign(*High, G1).has_value(), + "disjoint lanes must coexist on a shared multi-lane RegisterUnit"); +} + +[[nodiscard]] rcc::Expected<FunctionFixture> +createPressureFunction(const rcc::TargetDescription &Target, std::uint64_t ID) { + auto Fixture = createFunction("pressure-" + std::to_string(ID), ID); + if (!Fixture) + return std::unexpected(Fixture.error()); + std::vector<rcc::Register> Values; + for (std::int64_t Index = 0; Index < 3; ++Index) { + auto Reg = createVReg(*Fixture->Function); + if (!Reg) + return std::unexpected(Reg.error()); + if (auto Built = buildDef(*Fixture->Function, Target, *Fixture->Block, *Reg, Index); !Built) + return std::unexpected(Built.error()); + Values.push_back(*Reg); + } + for (const rcc::Register Reg : Values) { + if (auto Built = buildUse(*Fixture->Function, Target, *Fixture->Block, Reg); !Built) + return std::unexpected(Built.error()); + } + auto Prepared = prepareForAllocation(*Fixture->Function, Target); + if (!Prepared) + return std::unexpected(Prepared.error()); + return Fixture; +} + +void testAllocatorsUnderPressure(TestContext &Context, const rcc::TargetDescription &Target, + const TestRegisterAllocationInfo &Info) { + for (const auto [Kind, ID, Name] : { + std::tuple{AllocatorKind::Basic, std::uint64_t{10}, std::string_view{"Basic"}}, + std::tuple{AllocatorKind::Fast, std::uint64_t{11}, std::string_view{"Fast"}}, + std::tuple{AllocatorKind::PBQP, std::uint64_t{12}, std::string_view{"PBQP"}}, + }) { + auto Fixture = createPressureFunction(Target, ID); + if (!Fixture) + continue; + auto Allocated = + runAllocator(*Fixture->Function, Target, Info, Kind, {.EnableRematerialization = false}); + if (!Allocated) + std::cerr << Name << " allocation error: " << Allocated.error().message() << '\n'; + Context.expect(Allocated.has_value() && + Fixture->Function->properties().has(rcc::MachineFunctionProperty::NoVRegs) && + Fixture->Function->frameInfo().liveObjectCount() != 0 && + countOpcode(*Fixture->Function, SpillLoadOpcode) != 0 && + countOpcode(*Fixture->Function, SpillStoreOpcode) != 0, + "all allocators must close a deterministic spill/reload cycle under pressure"); + } +} + +void testFailureAtomicity(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createPressureFunction(Target, 13); + if (!Fixture) + return; + const std::vector<std::uint64_t> Before = functionSignature(*Fixture->Function); + const std::size_t KnownBefore = static_cast<std::size_t>(std::ranges::count_if( + Fixture->Function->registerInfo().virtualRegisters(), + [](const rcc::VirtualRegisterInfo &Info) { return !Info.IsTombstone; })); + RejectingRegisterAllocationInfo Rejecting; + auto Failed = runAllocator(*Fixture->Function, Target, Rejecting, AllocatorKind::Basic, + {.EnableRematerialization = false}); + const std::size_t KnownAfter = static_cast<std::size_t>(std::ranges::count_if( + Fixture->Function->registerInfo().virtualRegisters(), + [](const rcc::VirtualRegisterInfo &Info) { return !Info.IsTombstone; })); + Context.expect(!Failed && Failed.error().code() == rcc::ErrorCode::InternalError && + functionSignature(*Fixture->Function) == Before && KnownAfter == KnownBefore && + !Fixture->Function->properties().has(rcc::MachineFunctionProperty::NoVRegs), + "failed register allocation must leave the original MachineFunction unchanged"); +} + +void testFastBlockBoundary(TestContext &Context, const rcc::TargetDescription &Target, + const TestRegisterAllocationInfo &Info) { + auto Fixture = createFunction("fast-block-boundary", 14); + if (!Fixture) + return; + auto Exit = Fixture->Function->createBasicBlock(); + if (!Exit || !Fixture->Function->appendBlock(**Exit) || + !Fixture->Function->cfgUpdater().addSuccessor(*Fixture->Block, **Exit, + rcc::BranchProbability::one())) { + return; + } + auto Value = createVReg(*Fixture->Function); + const rcc::Register G0 = rcc::Register::fromPhysical(physical(rcc::test::TestRegister::G0)); + if (!Value || !buildDef(*Fixture->Function, Target, *Fixture->Block, *Value, 9) || + !buildUse(*Fixture->Function, Target, **Exit, *Value) || + !buildUse(*Fixture->Function, Target, **Exit, G0) || + !prepareForAllocation(*Fixture->Function, Target)) { + return; + } + auto Allocated = runAllocator(*Fixture->Function, Target, Info, AllocatorKind::Fast); + Context.expect( + Allocated && countOpcode(*Fixture->Function, SpillStoreOpcode) == 1 && + countOpcode(*Fixture->Function, SpillLoadOpcode) == 1 && + (*Exit)->firstInstruction()->next() != nullptr && + (*Exit)->firstInstruction()->next()->operandAt(0)->registerValue() != G0, + "FastAllocator must preserve block live-ins and virtual values across a CFG boundary"); +} + +void testFastRegMaskCalls(TestContext &Context, const rcc::TargetDescription &Target, + const TestRegisterAllocationInfo &Info) { + auto LiveThrough = createFunction("fast-regmask-live-through", 16); + if (!LiveThrough) + return; + auto Value = createVReg(*LiveThrough->Function); + if (!Value || !buildDef(*LiveThrough->Function, Target, *LiveThrough->Block, *Value, 11) || + !buildMask(*LiveThrough->Function, Target, *LiveThrough->Block, + rcc::test::createTestCallPreservedMask()) || + !buildUse(*LiveThrough->Function, Target, *LiveThrough->Block, *Value) || + !prepareForAllocation(*LiveThrough->Function, Target)) { + return; + } + auto LiveThroughAllocated = + runAllocator(*LiveThrough->Function, Target, Info, AllocatorKind::Fast); + Context.expect(LiveThroughAllocated && + countOpcode(*LiveThrough->Function, SpillStoreOpcode) == 1 && + countOpcode(*LiveThrough->Function, SpillLoadOpcode) == 1, + "FastAllocator must spill and reload a value live through a clobbering RegMask"); + + auto CallResult = createFunction("fast-regmask-call-result", 17); + if (!CallResult) + return; + auto Result = createVReg(*CallResult->Function); + if (!Result || + !buildCallDef(*CallResult->Function, Target, *CallResult->Block, *Result, + rcc::test::createTestCallPreservedMask()) || + !buildUse(*CallResult->Function, Target, *CallResult->Block, *Result) || + !prepareForAllocation(*CallResult->Function, Target)) { + return; + } + auto ResultAllocated = runAllocator(*CallResult->Function, Target, Info, AllocatorKind::Fast); + Context.expect(ResultAllocated && countOpcode(*CallResult->Function, SpillStoreOpcode) == 0 && + countOpcode(*CallResult->Function, SpillLoadOpcode) == 0, + "FastAllocator must retain a call-defined result without reloading an " + "uninitialized spill slot"); +} + +void testTiedAndEarlyClobber(TestContext &Context, const rcc::TargetDescription &Target, + const TestRegisterAllocationInfo &Info) { + auto Fixture = createFunction("tied-early", 15); + if (!Fixture) + return; + auto Source = createVReg(*Fixture->Function); + auto Destination = createVReg(*Fixture->Function); + if (!Source || !Destination || + !buildDef(*Fixture->Function, Target, *Fixture->Block, *Source, 1)) { + return; + } + auto Early = buildEarly(*Fixture->Function, Target, *Fixture->Block, *Destination, *Source); + auto Tied = buildTied(*Fixture->Function, Target, *Fixture->Block, *Destination); + if (!Early || !Tied || !buildUse(*Fixture->Function, Target, *Fixture->Block, *Destination) || + !prepareForAllocation(*Fixture->Function, Target)) { + return; + } + auto Allocated = runAllocator(*Fixture->Function, Target, Info, AllocatorKind::Basic); + const std::optional<rcc::Register> EarlyDef = (*Early)->operandAt(0)->registerValue(); + const std::optional<rcc::Register> EarlyUse = (*Early)->operandAt(1)->registerValue(); + const std::optional<rcc::Register> TiedDef = (*Tied)->operandAt(0)->registerValue(); + const std::optional<rcc::Register> TiedUse = (*Tied)->operandAt(1)->registerValue(); + Context.expect(Allocated && EarlyDef && EarlyUse && *EarlyDef != *EarlyUse && TiedDef && + TiedUse && *TiedDef == *TiedUse, + "allocation must separate early-clobber operands and preserve rewritten ties"); +} + +void testRematerialization(TestContext &Context, const rcc::TargetDescription &Target, + const TestRegisterAllocationInfo &Info) { + auto Fixture = createFunction("rematerialization", 20); + if (!Fixture) + return; + auto Value = createVReg(*Fixture->Function); + if (!Value || !buildDef(*Fixture->Function, Target, *Fixture->Block, *Value, 42, {}, true) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value)) { + return; + } + rcc::VirtRegMap Mapping(*Fixture->Function); + if (!Mapping.add(*Value)) + return; + rcc::Spiller Spiller(*Fixture->Function, Target, Info, Mapping, true); + auto Children = Spiller.spill(*Value); + Context.expect(Children && Children->size() == 2 && + Fixture->Function->frameInfo().liveObjectCount() == 0 && + countOpcode(*Fixture->Function, RematerializableDefOpcode) == 2 && + countOpcode(*Fixture->Function, SpillLoadOpcode) == 0 && + countOpcode(*Fixture->Function, SpillStoreOpcode) == 0, + "Spiller must rematerialize each use without creating a stack object"); + + auto Unspillable = createVReg(*Fixture->Function); + if (!Unspillable || !Mapping.add(*Unspillable, false)) + return; + auto Rejected = Spiller.spill(*Unspillable); + Context.expect(!Rejected && Rejected.error().code() == rcc::ErrorCode::RegisterAllocationFailed, + "Spiller must reject pressure on a non-spillable virtual register"); +} + +void testRewriteAndConstraints(TestContext &Context, const rcc::TargetDescription &Target, + const TestRegisterAllocationInfo &Info) { + auto RewriteFixture = createFunction("subregister-rewrite", 30); + if (!RewriteFixture) + return; + auto Value = createVReg(*RewriteFixture->Function); + if (!Value || + !buildDef(*RewriteFixture->Function, Target, *RewriteFixture->Block, *Value, 1, + rcc::test::TestSubReg::Low32) || + !buildUse(*RewriteFixture->Function, Target, *RewriteFixture->Block, *Value, + rcc::test::TestSubReg::Low32)) { + return; + } + rcc::VirtRegMap Mapping(*RewriteFixture->Function); + const auto G0 = physical(rcc::test::TestRegister::G0); + if (!Mapping.add(*Value) || !Mapping.assign(*Value, G0)) + return; + auto Rewritten = rcc::rewriteVirtualRegisters(*RewriteFixture->Function, Target, Mapping); + const auto G0Low32 = physical(rcc::test::TestRegister::G0Low32); + Context.expect(Rewritten && *Rewritten && + RewriteFixture->Block->firstInstruction()->operandAt(0)->registerValue() == + rcc::Register::fromPhysical(G0Low32) && + RewriteFixture->Block->firstInstruction()->operandAt(0)->subReg().value() == 0, + "physical rewrite must project and clear virtual subregister operands"); + + auto MaskFixture = createFunction("mask-allocation", 31); + if (!MaskFixture) + return; + auto AcrossCall = createVReg(*MaskFixture->Function); + if (!AcrossCall || + !buildDef(*MaskFixture->Function, Target, *MaskFixture->Block, *AcrossCall, 5) || + !buildMask(*MaskFixture->Function, Target, *MaskFixture->Block, + rcc::test::createTestCallPreservedMask()) || + !buildUse(*MaskFixture->Function, Target, *MaskFixture->Block, *AcrossCall) || + !prepareForAllocation(*MaskFixture->Function, Target)) { + return; + } + auto Allocated = runAllocator(*MaskFixture->Function, Target, Info, AllocatorKind::Basic); + const rcc::MachineInstr *FinalUse = MaskFixture->Block->lastInstruction(); + const auto G1 = physical(rcc::test::TestRegister::G1); + Context.expect(Allocated && FinalUse != nullptr && + FinalUse->operandAt(0)->registerValue() == rcc::Register::fromPhysical(G1), + "global allocation must keep call-crossing values in a preserved register"); +} + +void testInvalidRewriteMappings(TestContext &Context, const rcc::TargetDescription &Target) { + auto ExpectAtomicFailure = [&](FunctionFixture &Fixture, const rcc::VirtRegMap &Mapping, + std::string_view Message) { + const std::vector<std::uint64_t> Before = functionSignature(*Fixture.Function); + auto Rewritten = rcc::rewriteVirtualRegisters(*Fixture.Function, Target, Mapping); + Context.expect(!Rewritten && + Rewritten.error().code() == rcc::ErrorCode::RegisterAllocationFailed && + functionSignature(*Fixture.Function) == Before, + Message); + }; + + auto ClassFixture = createFunction("invalid-class-mapping", 40); + if (!ClassFixture) + return; + auto ClassValue = createVReg(*ClassFixture->Function); + if (!ClassValue || + !buildDef(*ClassFixture->Function, Target, *ClassFixture->Block, *ClassValue, 1) || + !buildUse(*ClassFixture->Function, Target, *ClassFixture->Block, *ClassValue) || + !prepareForAllocation(*ClassFixture->Function, Target)) { + return; + } + rcc::VirtRegMap ClassMapping(*ClassFixture->Function); + const auto F0 = physical(rcc::test::TestRegister::F0); + if (!ClassMapping.add(*ClassValue) || !ClassMapping.assign(*ClassValue, F0)) + return; + ExpectAtomicFailure(*ClassFixture, ClassMapping, + "class-incompatible rewrite mappings must fail atomically"); + + auto ReservedFixture = createFunction("reserved-register-mapping", 41); + if (!ReservedFixture) + return; + auto ReservedValue = createVReg(*ReservedFixture->Function); + const auto G0 = physical(rcc::test::TestRegister::G0); + if (!ReservedValue || + !buildDef(*ReservedFixture->Function, Target, *ReservedFixture->Block, *ReservedValue, 2) || + !buildUse(*ReservedFixture->Function, Target, *ReservedFixture->Block, *ReservedValue) || + !ReservedFixture->Function->registerInfo().reservePhysicalRegister(G0) || + !prepareForAllocation(*ReservedFixture->Function, Target)) { + return; + } + rcc::VirtRegMap ReservedMapping(*ReservedFixture->Function); + if (!ReservedMapping.add(*ReservedValue) || !ReservedMapping.assign(*ReservedValue, G0)) + return; + ExpectAtomicFailure(*ReservedFixture, ReservedMapping, + "reserved-register rewrite mappings must fail atomically"); + + auto OverlapFixture = createFunction("overlapping-register-mapping", 42); + if (!OverlapFixture) + return; + auto Left = createVReg(*OverlapFixture->Function); + auto Right = createVReg(*OverlapFixture->Function); + if (!Left || !Right || + !buildDef(*OverlapFixture->Function, Target, *OverlapFixture->Block, *Left, 3) || + !buildDef(*OverlapFixture->Function, Target, *OverlapFixture->Block, *Right, 4) || + !buildUse(*OverlapFixture->Function, Target, *OverlapFixture->Block, *Left) || + !buildUse(*OverlapFixture->Function, Target, *OverlapFixture->Block, *Right) || + !prepareForAllocation(*OverlapFixture->Function, Target)) { + return; + } + rcc::VirtRegMap OverlapMapping(*OverlapFixture->Function); + if (!OverlapMapping.add(*Left) || !OverlapMapping.add(*Right) || + !OverlapMapping.assign(*Left, G0) || !OverlapMapping.assign(*Right, G0)) { + return; + } + ExpectAtomicFailure(*OverlapFixture, OverlapMapping, + "interfering rewrite mappings must fail atomically"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + auto SharedUnitTarget = createTarget(true); + Context.expect(Target.has_value(), "register-allocation TestTarget must be constructible"); + Context.expect(SharedUnitTarget.has_value(), + "shared-unit register-allocation TestTarget must be constructible"); + if (!Target) { + std::cerr << "target construction error: " << Target.error().message() << '\n'; + return Context.result(); + } + TestRegisterAllocationInfo Info; + testVirtRegMapAndEdit(Context, *Target); + testLiveRegMatrix(Context, *Target); + if (SharedUnitTarget) + testSharedUnitLaneMatrix(Context, *SharedUnitTarget); + testAllocatorsUnderPressure(Context, *Target, Info); + testFailureAtomicity(Context, *Target); + testFastBlockBoundary(Context, *Target, Info); + testFastRegMaskCalls(Context, *Target, Info); + testTiedAndEarlyClobber(Context, *Target, Info); + testRematerialization(Context, *Target, Info); + testRewriteAndConstraints(Context, *Target, Info); + testInvalidRewriteMappings(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/RegisterAllocationTestSupport.h b/backend/unittests/CodeGen/RegisterAllocationTestSupport.h new file mode 100644 index 0000000..7f0a641 --- /dev/null +++ b/backend/unittests/CodeGen/RegisterAllocationTestSupport.h @@ -0,0 +1,474 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +// The private test header has no stable include-root-relative guard name. +// NOLINTNEXTLINE(llvm-header-guard) +#ifndef RCC_BACKEND_REGISTER_ALLOCATION_TEST_SUPPORT_H +#define RCC_BACKEND_REGISTER_ALLOCATION_TEST_SUPPORT_H + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/LiveIntervals.h" +#include "rcc/CodeGen/LiveRangeEdit.h" +#include "rcc/CodeGen/LiveVariables.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/RegAllocFast.h" +#include "rcc/CodeGen/RegAllocPBQP.h" +#include "rcc/CodeGen/Spiller.h" +#include "rcc/CodeGen/TargetRegisterAllocationInfo.h" +#include "rcc/CodeGen/VirtRegMap.h" + +#include <algorithm> +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#include <ranges> +#include <span> +#include <string> +#include <string_view> +#include <tuple> +#include <utility> +#include <vector> + +namespace rcc::test::ra { + +enum class AllocatorKind : std::uint8_t { Basic, Fast, PBQP }; + +inline constexpr std::uint32_t DefOpcode = 3000; +inline constexpr std::uint32_t RematerializableDefOpcode = 3001; +inline constexpr std::uint32_t UseOpcode = 3002; +inline constexpr std::uint32_t BinaryOpcode = 3003; +inline constexpr std::uint32_t SpillLoadOpcode = 3004; +inline constexpr std::uint32_t SpillStoreOpcode = 3005; +inline constexpr std::uint32_t MaskOpcode = 3006; +inline constexpr std::uint32_t EarlyOpcode = 3007; +inline constexpr std::uint32_t TiedOpcode = 3008; +inline constexpr std::uint32_t CallDefOpcode = 3009; + +[[nodiscard]] constexpr Opcode testOpcode(std::uint32_t Value) noexcept { + return Opcode::fromValue(Value); +} + +[[nodiscard]] inline PhysicalRegister physical(std::uint32_t Number) { + return *PhysicalRegister::create(Number); +} + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +[[nodiscard]] inline OperandDescriptor registerOperand(RegisterOperandRole Role, + bool EarlyClobber = false) { + OperandDescriptor Result; + Result.Kinds = {MachineOperand::Kind::Register}; + Result.RegisterRole = Role; + Result.AllowSubReg = true; + Result.EarlyClobber = EarlyClobber; + return Result; +} + +[[nodiscard]] inline OperandDescriptor plainOperand(MachineOperand::Kind Kind) { + OperandDescriptor Result; + Result.Kinds = {Kind}; + return Result; +} + +[[nodiscard]] inline InstructionDescriptor descriptor(std::uint32_t Value, std::string Name) { + InstructionDescriptor Result; + Result.InstructionOpcode = testOpcode(Value); + Result.Name = std::move(Name); + Result.Flags = {InstructionFlag::ProducesCode}; + return Result; +} + +[[nodiscard]] inline Expected<TargetDescription> createTarget() { + std::vector<InstructionDescriptor> Descriptors; + + auto Def = descriptor(DefOpcode, "RA_DEF"); + Def.Operands = {registerOperand(RegisterOperandRole::Def), + plainOperand(MachineOperand::Kind::Immediate)}; + Def.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Def)); + + auto Remat = descriptor(RematerializableDefOpcode, "RA_REMAT_DEF"); + Remat.Operands = {registerOperand(RegisterOperandRole::Def), + plainOperand(MachineOperand::Kind::Immediate)}; + Remat.NumExplicitDefs = 1; + Remat.Flags.set(InstructionFlag::Rematerializable); + Descriptors.push_back(std::move(Remat)); + + auto Use = descriptor(UseOpcode, "RA_USE"); + Use.Operands = {registerOperand(RegisterOperandRole::Use)}; + Use.Flags.set(InstructionFlag::HasSideEffects); + Descriptors.push_back(std::move(Use)); + + auto Binary = descriptor(BinaryOpcode, "RA_BINARY"); + Binary.Operands = {registerOperand(RegisterOperandRole::Def), + registerOperand(RegisterOperandRole::Use), + registerOperand(RegisterOperandRole::Use)}; + Binary.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Binary)); + + auto Reload = descriptor(SpillLoadOpcode, "RA_RELOAD"); + Reload.Operands = {registerOperand(RegisterOperandRole::Def), + plainOperand(MachineOperand::Kind::FrameIndex)}; + Reload.NumExplicitDefs = 1; + Reload.Flags.set(InstructionFlag::MayLoad); + Reload.RequiresMemoryOperands = true; + Descriptors.push_back(std::move(Reload)); + + auto Spill = descriptor(SpillStoreOpcode, "RA_SPILL"); + Spill.Operands = {registerOperand(RegisterOperandRole::Use), + plainOperand(MachineOperand::Kind::FrameIndex)}; + Spill.Flags.set(InstructionFlag::MayStore); + Spill.RequiresMemoryOperands = true; + Descriptors.push_back(std::move(Spill)); + + auto Mask = descriptor(MaskOpcode, "RA_MASK"); + Mask.Operands = {plainOperand(MachineOperand::Kind::RegisterMask)}; + Mask.Flags.set(InstructionFlag::Call); + Mask.Flags.set(InstructionFlag::HasSideEffects); + Descriptors.push_back(std::move(Mask)); + + auto Early = descriptor(EarlyOpcode, "RA_EARLY"); + Early.Operands = {registerOperand(RegisterOperandRole::Def, true), + registerOperand(RegisterOperandRole::Use)}; + Early.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Early)); + + auto Tied = descriptor(TiedOpcode, "RA_TIED"); + auto TiedDef = registerOperand(RegisterOperandRole::Def); + auto TiedUse = registerOperand(RegisterOperandRole::Use); + TiedDef.DefaultTiedOperand = 1; + TiedUse.DefaultTiedOperand = 0; + Tied.Operands = {TiedDef, TiedUse}; + Tied.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Tied)); + + auto CallDef = descriptor(CallDefOpcode, "RA_CALL_DEF"); + CallDef.Operands = {registerOperand(RegisterOperandRole::Def), + plainOperand(MachineOperand::Kind::RegisterMask)}; + CallDef.NumExplicitDefs = 1; + CallDef.Flags.set(InstructionFlag::Call); + CallDef.Flags.set(InstructionFlag::HasSideEffects); + Descriptors.push_back(std::move(CallDef)); + + auto Copy = descriptor(static_cast<std::uint32_t>(GenericOpcode::COPY), "COPY"); + Copy.Operands = {registerOperand(RegisterOperandRole::Def), + registerOperand(RegisterOperandRole::Use)}; + Copy.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Copy)); + + auto Instructions = InstructionSet::create(std::move(Descriptors), {}); + if (!Instructions) + return std::unexpected(Instructions.error()); + return test::createTestTargetDescription(std::move(*Instructions)); +} + +struct FunctionFixture final { + std::unique_ptr<MachineFunction> Function; + MachineBasicBlock *Block = nullptr; +}; + +[[nodiscard]] inline Expected<FunctionFixture> createFunction(std::string Name, std::uint64_t ID) { + auto Function = MachineFunction::create(std::move(Name), MachineFunctionStableID::fromValue(ID)); + if (!Function) + return std::unexpected(Function.error()); + auto Block = (*Function)->createBasicBlock(); + if (!Block) + return std::unexpected(Block.error()); + auto Appended = (*Function)->appendBlock(**Block); + if (!Appended) + return std::unexpected(Appended.error()); + return FunctionFixture{std::move(*Function), *Block}; +} + +[[nodiscard]] inline Expected<Register> +createVReg(MachineFunction &Function, RegisterClassID Class = TestRegisterClass::GPR64) { + auto Type = LowLevelType::makeCategoryNeutralScalar(64); + if (!Type) + return std::unexpected(Type.error()); + auto Reg = Function.registerInfo().createVirtualRegister(*Type); + if (!Reg) + return std::unexpected(Reg.error()); + if (auto Result = Function.registerInfo().setRegisterBank(*Reg, TestRegisterBank::GPR); !Result) + return std::unexpected(Result.error()); + if (auto Result = Function.registerInfo().setRegisterClass(*Reg, Class); !Result) + return std::unexpected(Result.error()); + return *Reg; +} + +[[nodiscard]] inline Expected<MachineInstr *> +buildDef(MachineFunction &Function, const TargetDescription &Target, MachineBasicBlock &Block, + Register Reg, std::int64_t Value, SubRegIndex SubReg = {}, bool Rematerializable = false) { + MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = + Builder.buildInstr(testOpcode(Rematerializable ? RematerializableDefOpcode : DefOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Reg, {}, SubReg); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addImmediate(Value); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] inline Expected<MachineInstr *> +buildUse(MachineFunction &Function, const TargetDescription &Target, MachineBasicBlock &Block, + Register Reg, SubRegIndex SubReg = {}, bool Debug = false) { + MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(testOpcode(UseOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + RegisterOperandFlags Flags; + if (Debug) + Flags.set(RegisterOperandFlag::Debug); + if (auto Result = Pending->addUse(Reg, Flags, SubReg); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] inline Expected<MachineInstr *> buildCopy(MachineFunction &Function, + const TargetDescription &Target, + MachineBasicBlock &Block, + Register Destination, Register Source) { + MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(opcode(GenericOpcode::COPY)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Destination); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addUse(Source); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] inline Expected<MachineInstr *> buildMask(MachineFunction &Function, + const TargetDescription &Target, + MachineBasicBlock &Block, + std::shared_ptr<const RegisterMask> Mask) { + MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(testOpcode(MaskOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addRegisterMask(std::move(Mask)); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] inline Expected<MachineInstr *> buildTied(MachineFunction &Function, + const TargetDescription &Target, + MachineBasicBlock &Block, Register Reg) { + MachineIRBuilder Builder(Function, Target); + if (auto Result = Builder.setInsertPoint(Block); !Result) + return std::unexpected(Result.error()); + auto Pending = Builder.buildInstr(testOpcode(TiedOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Reg); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addUse(Reg); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->tieOperands(0, 1); !Result) + return std::unexpected(Result.error()); + return Pending->build(); +} + +[[nodiscard]] inline Expected<void> prepareForAllocation(MachineFunction &Function, + const TargetDescription &Target) { + auto Rebuilt = rebuildMachineLiveness(Function, Target); + if (!Rebuilt) + return std::unexpected(Rebuilt.error()); + for (const MachineFunctionProperty Property : { + MachineFunctionProperty::TracksLiveness, + MachineFunctionProperty::Legalized, + MachineFunctionProperty::RegBankSelected, + MachineFunctionProperty::Selected, + MachineFunctionProperty::NoPHIs, + MachineFunctionProperty::TiedOpsRewritten, + }) + Function.properties().set(Property); + return {}; +} + +class TestRegisterAllocationInfo : public TargetRegisterAllocationInfo { +public: + std::uint64_t RegisterCost = 0; + std::uint64_t SpillCost = 1; + + [[nodiscard]] Expected<void> validateSpillReload(RegisterClassID Class, + const TargetDescription &Target) const override { + const RegisterClassDesc *Description = Target.registers().registerClass(Class); + if (Description == nullptr || !Description->Allocatable || Description->SpillSizeInBits == 0 || + Description->SpillAlignmentInBits == 0) + return makeUnexpected(ErrorCode::Unsupported, "test register class is not spillable"); + return {}; + } + + [[nodiscard]] Expected<MachineInstr *> emitReload(MachineIRBuilder &Builder, Register Destination, + RegisterClassID Class, FrameIndex StackSlot, + LaneMask Lanes) const override { + auto Validated = validateSpillReload(Class, Builder.target()); + if (!Validated) + return std::unexpected(Validated.error()); + if (Lanes.empty()) + return makeUnexpected(ErrorCode::InvalidArgument, "test reload requires lanes"); + auto Size = MemorySize::fixed(8); + auto Align = Alignment::create(8); + if (!Size || !Align) + return makeUnexpected(ErrorCode::InternalError, "test memory shape is invalid"); + MachineMemOperandDesc Memory; + Memory.Accesses = {MemoryAccessFlag::Load}; + Memory.Size = *Size; + Memory.Align = *Align; + Memory.Base = MemoryBase::frameIndex(StackSlot); + auto MMO = Builder.function().createMemOperand(std::move(Memory)); + if (!MMO) + return std::unexpected(MMO.error()); + auto Pending = Builder.buildInstr(testOpcode(SpillLoadOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Destination); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addFrameIndex(StackSlot); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addMemOperand(**MMO); !Result) + return std::unexpected(Result.error()); + return Pending->build(); + } + + [[nodiscard]] Expected<MachineInstr *> emitSpill(MachineIRBuilder &Builder, Register Source, + RegisterClassID Class, FrameIndex StackSlot, + LaneMask Lanes) const override { + auto Validated = validateSpillReload(Class, Builder.target()); + if (!Validated) + return std::unexpected(Validated.error()); + if (Lanes.empty()) + return makeUnexpected(ErrorCode::InvalidArgument, "test spill requires lanes"); + auto Size = MemorySize::fixed(8); + auto Align = Alignment::create(8); + if (!Size || !Align) + return makeUnexpected(ErrorCode::InternalError, "test memory shape is invalid"); + MachineMemOperandDesc Memory; + Memory.Accesses = {MemoryAccessFlag::Store}; + Memory.Size = *Size; + Memory.Align = *Align; + Memory.Base = MemoryBase::frameIndex(StackSlot); + auto MMO = Builder.function().createMemOperand(std::move(Memory)); + if (!MMO) + return std::unexpected(MMO.error()); + auto Pending = Builder.buildInstr(testOpcode(SpillStoreOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addUse(Source); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addFrameIndex(StackSlot); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addMemOperand(**MMO); !Result) + return std::unexpected(Result.error()); + return Pending->build(); + } + + [[nodiscard]] Expected<bool> isRematerializable(const MachineInstr &Instruction, + Register VirtualReg, const MachineFunction &, + const TargetDescription &) const override { + const Register *Defined = Instruction.operandAt(0)->getIf<Register>(); + return Instruction.opcode() == testOpcode(RematerializableDefOpcode) && Defined != nullptr && + *Defined == VirtualReg; + } + + [[nodiscard]] Expected<MachineInstr *> + rematerialize(MachineIRBuilder &Builder, const MachineInstr &Original, Register Destination, + Register OriginalRegister, const TargetDescription &Target) const override { + auto Accepted = isRematerializable(Original, OriginalRegister, Builder.function(), Target); + if (!Accepted) + return std::unexpected(Accepted.error()); + if (!*Accepted) + return makeUnexpected(ErrorCode::InvalidArgument, "test instruction is not rematerializable"); + auto Pending = Builder.buildInstr(testOpcode(RematerializableDefOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + if (auto Result = Pending->addDef(Destination); !Result) + return std::unexpected(Result.error()); + if (auto Result = Pending->addOperand(Original.operandAt(1)->cloneDetached()); !Result) + return std::unexpected(Result.error()); + return Pending->build(); + } + + [[nodiscard]] std::uint64_t physicalRegisterCost(const MachineFunction &, Register, + PhysicalRegister, RegisterClassID, + bool) const noexcept override { + return RegisterCost; + } + + [[nodiscard]] std::uint64_t spillCost(const MachineFunction &, const LiveInterval &, + RegisterClassID) const noexcept override { + return SpillCost; + } +}; + +[[nodiscard]] inline Expected<void> runAllocator(MachineFunction &Function, + const TargetDescription &Target, + const TargetRegisterAllocationInfo &Info, + AllocatorKind Kind, + RegisterAllocationOptions Options = {}) { + MachineFunctionAnalysisManager Analyses; + auto Registered = registerStage11MachineAnalyses(Analyses); + if (!Registered) + return std::unexpected(Registered.error()); + std::unique_ptr<MachineFunctionPass> Pass; + switch (Kind) { + case AllocatorKind::Basic: + Pass = createBasicAllocatorPass(Info, Options); + break; + case AllocatorKind::Fast: + Pass = std::make_unique<FastAllocatorPass>(Info); + break; + case AllocatorKind::PBQP: + Pass = std::make_unique<PBQPAllocatorPass>(Info, Options); + break; + } + MachineFunctionPassManager Pipeline; + auto Added = Pipeline.addPass(std::move(Pass)); + if (!Added) + return std::unexpected(Added.error()); + return Pipeline.run(Function, Target, Analyses); +} + +[[nodiscard]] inline std::size_t countOpcode(const MachineFunction &Function, Opcode OpcodeValue) { + std::size_t Count = 0; + for (const MachineBasicBlock *Block = Function.firstBlock(); Block != nullptr; + Block = Block->next()) + for (const MachineInstr *Instruction = Block->firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) + Count += Instruction->opcode() == OpcodeValue ? 1U : 0U; + return Count; +} + +} // namespace rcc::test::ra + +#endif // RCC_BACKEND_REGISTER_ALLOCATION_TEST_SUPPORT_H diff --git a/backend/unittests/CodeGen/SSAExitTestSupport.h b/backend/unittests/CodeGen/SSAExitTestSupport.h new file mode 100644 index 0000000..cf77d36 --- /dev/null +++ b/backend/unittests/CodeGen/SSAExitTestSupport.h @@ -0,0 +1,444 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +// This private test header has no stable include-root-relative guard name. +// NOLINTNEXTLINE(llvm-header-guard) +#ifndef RCC_BACKEND_SSA_EXIT_TEST_SUPPORT_H +#define RCC_BACKEND_SSA_EXIT_TEST_SUPPORT_H + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/LiveVariables.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/TargetDescription.h" +#include "rcc/CodeGen/TargetInstrInfo.h" + +#include <algorithm> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <memory> +#include <optional> +#include <ranges> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace rcc::test::ssa { + +constexpr std::uint32_t DefOpcode = 1024; +constexpr std::uint32_t UseOpcode = 1025; +constexpr std::uint32_t ConditionalBranchOpcode = 1026; +constexpr std::uint32_t JumpOpcode = 1027; +constexpr std::uint32_t ReturnOpcode = 1028; +constexpr std::uint32_t MetaOpcode = 1029; +constexpr std::uint32_t TiedOpcode = 1030; +constexpr std::uint32_t CommutableTiedOpcode = 1031; +constexpr std::uint32_t ConvertibleTiedOpcode = 1032; +constexpr std::uint32_t ThreeAddressOpcode = 1033; + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +[[nodiscard]] inline rcc::Opcode testOpcode(std::uint32_t Value) { + return rcc::Opcode::fromValue(Value); +} + +[[nodiscard]] inline rcc::OperandDescriptor registerOperand(rcc::RegisterOperandRole Role, + bool AllowSubReg = true) { + rcc::OperandDescriptor Result; + Result.Kinds = {rcc::MachineOperand::Kind::Register}; + Result.RegisterRole = Role; + Result.AllowSubReg = AllowSubReg; + Result.AllowAnyTargetFlags = true; + return Result; +} + +[[nodiscard]] inline rcc::OperandDescriptor blockOperand() { + rcc::OperandDescriptor Result; + Result.Kinds = {rcc::MachineOperand::Kind::BasicBlock}; + return Result; +} + +[[nodiscard]] inline rcc::InstructionDescriptor instruction(std::uint32_t Value, std::string Name) { + rcc::InstructionDescriptor Result; + Result.InstructionOpcode = testOpcode(Value); + Result.Name = std::move(Name); + Result.Flags = {rcc::InstructionFlag::ProducesCode}; + return Result; +} + +[[nodiscard]] inline rcc::Expected<rcc::TargetDescription> createTarget() { + std::vector<rcc::InstructionDescriptor> Descriptors; + std::vector<rcc::ImmediatePredicate> Predicates; + auto Generic = rcc::appendGenericInstructionInfo(Descriptors, Predicates); + if (!Generic) + return std::unexpected(Generic.error()); + + auto Def = instruction(DefOpcode, "TEST_DEF"); + Def.Operands = {registerOperand(rcc::RegisterOperandRole::Def)}; + Def.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Def)); + + auto Use = instruction(UseOpcode, "TEST_USE"); + Use.Operands = {registerOperand(rcc::RegisterOperandRole::Use)}; + Descriptors.push_back(std::move(Use)); + + auto Conditional = instruction(ConditionalBranchOpcode, "TEST_BRCOND"); + Conditional.Operands = {registerOperand(rcc::RegisterOperandRole::Use, false), blockOperand()}; + Conditional.Flags.set(rcc::InstructionFlag::Branch); + Conditional.Flags.set(rcc::InstructionFlag::Terminator); + Descriptors.push_back(std::move(Conditional)); + + auto Jump = instruction(JumpOpcode, "TEST_JUMP"); + Jump.Operands = {blockOperand()}; + Jump.Flags.set(rcc::InstructionFlag::Branch); + Jump.Flags.set(rcc::InstructionFlag::Terminator); + Jump.Flags.set(rcc::InstructionFlag::Barrier); + Descriptors.push_back(std::move(Jump)); + + auto Return = instruction(ReturnOpcode, "TEST_RETURN"); + Return.Flags.set(rcc::InstructionFlag::Return); + Return.Flags.set(rcc::InstructionFlag::Terminator); + Return.Flags.set(rcc::InstructionFlag::Barrier); + Descriptors.push_back(std::move(Return)); + + auto Meta = instruction(MetaOpcode, "TEST_META"); + Meta.Flags = {rcc::InstructionFlag::Pseudo, rcc::InstructionFlag::Meta}; + Descriptors.push_back(std::move(Meta)); + + auto Tied = instruction(TiedOpcode, "TEST_TIED"); + auto TiedDef = registerOperand(rcc::RegisterOperandRole::Def); + auto TiedUse = registerOperand(rcc::RegisterOperandRole::Use); + TiedDef.DefaultTiedOperand = 1; + TiedUse.DefaultTiedOperand = 0; + Tied.Operands = {TiedDef, TiedUse}; + Tied.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Tied)); + + auto Commutable = instruction(CommutableTiedOpcode, "TEST_COMMUTABLE_TIED"); + Commutable.Operands = {TiedDef, TiedUse, registerOperand(rcc::RegisterOperandRole::Use)}; + Commutable.NumExplicitDefs = 1; + Commutable.Flags.set(rcc::InstructionFlag::Commutable); + Descriptors.push_back(std::move(Commutable)); + + auto Convertible = instruction(ConvertibleTiedOpcode, "TEST_CONVERTIBLE_TIED"); + Convertible.Operands = {TiedDef, TiedUse}; + Convertible.NumExplicitDefs = 1; + Descriptors.push_back(std::move(Convertible)); + + auto ThreeAddress = instruction(ThreeAddressOpcode, "TEST_THREE_ADDRESS"); + ThreeAddress.Operands = {registerOperand(rcc::RegisterOperandRole::Def), + registerOperand(rcc::RegisterOperandRole::Use)}; + ThreeAddress.NumExplicitDefs = 1; + Descriptors.push_back(std::move(ThreeAddress)); + + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors), std::move(Predicates)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +class TestInstrInfo final : public rcc::TargetInstrInfo { +public: + explicit TestInstrInfo(bool RejectCopies = false) : RejectCopies(RejectCopies) {} + + [[nodiscard]] rcc::Expected<rcc::MachineBranchAnalysis> + analyzeBranch(rcc::MachineBasicBlock &, rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return rcc::MachineBranchAnalysis{}; + } + + [[nodiscard]] rcc::Expected<void> + reverseBranchCondition(rcc::MachineInstr &, rcc::MachineBasicBlock &, rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, "test target has no branch reversal"); + } + + [[nodiscard]] rcc::Expected<void> removeBranch(rcc::MachineInstr &, rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, "test target has no branch removal"); + } + + [[nodiscard]] rcc::Expected<bool> + isFallthroughEdge(const rcc::MachineBasicBlock &From, const rcc::MachineBasicBlock &To, + const rcc::MachineFunction &Function, + const rcc::TargetDescription &Target) const override { + if (&From.function() != &Function || &To.function() != &Function) { + return rcc::makeUnexpected(rcc::ErrorCode::InvalidArgument, + "fallthrough query references another function"); + } + if (From.next() != &To || + std::ranges::none_of(From.successors(), [&To](const rcc::MachineSuccessor &Successor) { + return Successor.Block == &To; + })) { + return false; + } + + bool CanFallThrough = true; + for (const rcc::MachineInstr *Instruction = From.firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + const rcc::InstructionDescriptor *Descriptor = + Target.instructionDescriptor(Instruction->opcode()); + if (Descriptor == nullptr) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "fallthrough query encountered an unknown opcode"); + } + if (Descriptor->Flags.has(rcc::InstructionFlag::Terminator)) + CanFallThrough = Instruction->opcode() == testOpcode(ConditionalBranchOpcode); + } + return CanFallThrough; + } + + [[nodiscard]] rcc::Expected<bool> + rewriteBranchTarget(rcc::MachineBasicBlock &Block, rcc::MachineBasicBlock &OldTarget, + rcc::MachineBasicBlock &NewTarget, rcc::MachineFunction &Function, + const rcc::TargetDescription &Target) const override { + if (&Block.function() != &Function || &OldTarget.function() != &Function || + &NewTarget.function() != &Function) { + return rcc::makeUnexpected(rcc::ErrorCode::InvalidArgument, + "branch rewrite references another function"); + } + bool Changed = false; + for (rcc::MachineInstr *Instruction = Block.firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + const rcc::InstructionDescriptor *Descriptor = + Target.instructionDescriptor(Instruction->opcode()); + if (Descriptor == nullptr) + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, "unknown branch opcode"); + if (!Descriptor->Flags.has(rcc::InstructionFlag::Branch)) + continue; + for (std::size_t Index = 0; Index < Instruction->operandCount(); ++Index) { + rcc::MachineOperand *Operand = Instruction->operandAt(Index); + const auto *BlockOperand = + Operand == nullptr ? nullptr : Operand->getIf<rcc::BasicBlockOperand>(); + if (BlockOperand == nullptr || BlockOperand->Block != &OldTarget) + continue; + auto Rewritten = Operand->setBasicBlock(NewTarget); + if (!Rewritten) + return std::unexpected(Rewritten.error()); + Changed = true; + } + } + return Changed; + } + + [[nodiscard]] rcc::Expected<rcc::MachineInstr *> + insertBranch(rcc::MachineBasicBlock &Block, rcc::MachineBasicBlock &TargetBlock, + rcc::MachineFunction &Function, + const rcc::TargetDescription &Target) const override { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = Builder.setInsertPoint(Block); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(testOpcode(JumpOpcode)); + if (!Pending) + return std::unexpected(Pending.error()); + auto Destination = Pending->addBasicBlock(TargetBlock); + if (!Destination) + return std::unexpected(Destination.error()); + auto Built = Pending->build(); + if (!Built) + return std::unexpected(Built.error()); + return *Built; + } + + [[nodiscard]] rcc::Expected<void> + validateCopy(rcc::Register Destination, rcc::SubRegIndex DestinationSubReg, rcc::Register Source, + rcc::SubRegIndex SourceSubReg, const rcc::MachineFunction &Function, + const rcc::TargetDescription &Target) const override { + if (RejectCopies) { + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, + "test target intentionally rejects COPY"); + } + if (Destination.isNoRegister() || Source.isNoRegister()) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "COPY requires nonzero registers"); + } + if ((Destination.isPhysical() && DestinationSubReg.value() != 0) || + (Source.isPhysical() && SourceSubReg.value() != 0)) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "physical COPY operands cannot carry subregisters"); + } + auto Def = rcc::MachineOperand::registerOperand(Destination, {rcc::RegisterOperandFlag::Define}, + DestinationSubReg); + auto Use = + rcc::MachineOperand::registerOperand(Source, {rcc::RegisterOperandFlag::Use}, SourceSubReg); + if (!Def || !Use) + return rcc::makeUnexpected(rcc::ErrorCode::InternalError, "failed to model COPY operands"); + return Target.validateTieCompatibility(*Def, *Use, Function.registerInfo()); + } + + [[nodiscard]] rcc::Expected<bool> + tryConvertToThreeAddress(rcc::MachineInstr &Instruction, std::size_t DefIndex, + std::size_t UseIndex, rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + if (Instruction.opcode() != testOpcode(ConvertibleTiedOpcode)) + return false; + if (DefIndex != 0 || UseIndex != 1) + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "unexpected convertible tie indices"); + auto Untied = Instruction.untieOperand(DefIndex); + if (!Untied) + return std::unexpected(Untied.error()); + auto Opcode = Instruction.setOpcode(testOpcode(ThreeAddressOpcode)); + if (!Opcode) + return std::unexpected(Opcode.error()); + return true; + } + + [[nodiscard]] rcc::Expected<std::uint32_t> + instructionSize(const rcc::MachineInstr &, const rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return 4; + } + + [[nodiscard]] rcc::Expected<rcc::MachineBasicBlock *> + branchTarget(rcc::MachineInstr &, rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return static_cast<rcc::MachineBasicBlock *>(nullptr); + } + + [[nodiscard]] bool isBranchOffsetInRange(const rcc::MachineInstr &, + std::int64_t) const noexcept override { + return true; + } + + [[nodiscard]] rcc::Expected<bool> relaxBranch(rcc::MachineInstr &, std::int64_t, + rcc::MachineFunction &, + const rcc::TargetDescription &) const override { + return false; + } + +private: + bool RejectCopies = false; +}; + +struct FunctionFixture final { + std::unique_ptr<rcc::MachineFunction> Function; + std::vector<rcc::MachineBasicBlock *> Blocks; +}; + +[[nodiscard]] inline FunctionFixture createFunction(TestContext &Context, std::string Name, + std::uint64_t ID, std::size_t BlockCount) { + auto Function = + rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "SSA-exit function creation must succeed"); + if (!Function) + return {}; + + FunctionFixture Result{std::move(*Function), {}}; + for (std::size_t Index = 0; Index < BlockCount; ++Index) { + auto Block = Result.Function->createBasicBlock(); + Context.expect(Block && Result.Function->appendBlock(**Block).has_value(), + "SSA-exit block creation must succeed"); + if (!Block) + return {}; + Result.Blocks.push_back(*Block); + } + return Result; +} + +[[nodiscard]] inline rcc::Expected<rcc::Register> +createVReg(rcc::MachineFunction &Function, rcc::LowLevelType Type, rcc::RegisterBankID Bank, + rcc::RegisterClassID Class, std::uint8_t TargetFlags = 0, + std::optional<rcc::PhysicalRegister> Hint = std::nullopt) { + auto Reg = Function.registerInfo().createVirtualRegister(Type); + if (!Reg) + return std::unexpected(Reg.error()); + auto BankResult = Function.registerInfo().setRegisterBank(*Reg, Bank); + if (!BankResult) + return std::unexpected(BankResult.error()); + auto ClassResult = Function.registerInfo().setRegisterClass(*Reg, Class); + if (!ClassResult) + return std::unexpected(ClassResult.error()); + if (TargetFlags != 0) { + auto FlagsResult = Function.registerInfo().setTargetRegisterFlags(*Reg, TargetFlags); + if (!FlagsResult) + return std::unexpected(FlagsResult.error()); + } + if (Hint) { + auto HintResult = Function.registerInfo().addAllocationHint(*Reg, *Hint); + if (!HintResult) + return std::unexpected(HintResult.error()); + } + return *Reg; +} + +[[nodiscard]] inline rcc::Expected<rcc::MachineInstr *> +buildRegisterInstruction(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineBasicBlock &Block, rcc::Opcode Opcode, + std::span<const rcc::Register> Defs, std::span<const rcc::Register> Uses, + rcc::RegisterOperandFlags DefFlags = {}, + rcc::RegisterOperandFlags UseFlags = {}) { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = Builder.setInsertPoint(Block); + if (!Point) + return std::unexpected(Point.error()); + auto Pending = Builder.buildInstr(Opcode); + if (!Pending) + return std::unexpected(Pending.error()); + for (rcc::Register Reg : Defs) { + auto Added = Pending->addDef(Reg, DefFlags); + if (!Added) + return std::unexpected(Added.error()); + } + for (rcc::Register Reg : Uses) { + auto Added = Pending->addUse(Reg, UseFlags); + if (!Added) + return std::unexpected(Added.error()); + } + return Pending->build(); +} + +[[nodiscard]] inline rcc::Expected<void> +setSelectedSSAProperties(rcc::MachineFunction &Function, const rcc::TargetDescription &Target) { + auto Rebuilt = rcc::rebuildMachineLiveness(Function, Target); + if (!Rebuilt) + return std::unexpected(Rebuilt.error()); + for (rcc::MachineFunctionProperty Property : + {rcc::MachineFunctionProperty::IsSSA, rcc::MachineFunctionProperty::TracksLiveness, + rcc::MachineFunctionProperty::Legalized, rcc::MachineFunctionProperty::RegBankSelected, + rcc::MachineFunctionProperty::Selected}) { + Function.properties().set(Property); + } + return {}; +} + +[[nodiscard]] inline std::vector<rcc::MachineInstr *> +instructionsWithOpcode(rcc::MachineFunction &Function, rcc::Opcode Opcode) { + std::vector<rcc::MachineInstr *> Result; + for (rcc::MachineBasicBlock *Block = Function.firstBlock(); Block != nullptr; + Block = Block->next()) { + for (rcc::MachineInstr *Instruction = Block->firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + if (Instruction->opcode() == Opcode) + Result.push_back(Instruction); + } + } + return Result; +} + +} // namespace rcc::test::ssa + +#endif // RCC_BACKEND_SSA_EXIT_TEST_SUPPORT_H diff --git a/backend/unittests/CodeGen/SlotIndexesTest.cpp b/backend/unittests/CodeGen/SlotIndexesTest.cpp new file mode 100644 index 0000000..9d3f54b --- /dev/null +++ b/backend/unittests/CodeGen/SlotIndexesTest.cpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/SlotIndexes.h" + +#include "LiveTestSupport.h" +#include "rcc/CodeGen/LiveVariables.h" + +namespace { + +using namespace rcc::test::liveness; + +void testSlotIndexes(TestContext &Context, const rcc::TargetDescription &Target) { + auto Fixture = createFunction("slot-indexes", 1, 1); + if (!Fixture) { + Context.expect(false, "SlotIndexes fixture creation must succeed"); + return; + } + auto Reg = createVReg(*Fixture->Function); + auto Def = Reg ? buildDef(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg) + : rcc::Expected<rcc::MachineInstr *>{std::unexpected(Reg.error())}; + auto Use = Reg ? buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg) + : rcc::Expected<rcc::MachineInstr *>{std::unexpected(Reg.error())}; + if (!Reg || !Def || !Use) { + Context.expect(false, "SlotIndexes instructions must be built"); + return; + } + + rcc::MachineFunctionAnalysisManager Analyses; + auto Registered = rcc::registerStage11MachineAnalyses(Analyses); + auto Indexes = + Registered + ? Analyses.getResult<rcc::SlotIndexesAnalysis>(*Fixture->Function, Target) + : rcc::Expected<rcc::SlotIndexesAnalysis::Result *>{std::unexpected(Registered.error())}; + if (!Indexes) { + Context.expect(false, "SlotIndexes analysis must materialize"); + return; + } + const rcc::SlotIndex DefIndex = (*Indexes)->instructionIndex(**Def); + const rcc::SlotIndex UseIndex = (*Indexes)->instructionIndex(**Use); + Context.expect((*Indexes)->blockStart(*Fixture->Blocks[0]) < DefIndex.earlyClobberSlot() && + DefIndex.earlyClobberSlot() < DefIndex && DefIndex < DefIndex.deadSlot() && + DefIndex.deadSlot() < UseIndex && + UseIndex < (*Indexes)->blockEnd(*Fixture->Blocks[0]), + "SlotIndexes must order block, early-clobber, register, and dead slots"); + Context.expect((*Indexes)->instructionFromIndex(DefIndex.deadSlot()) == *Def && + (*Indexes)->blockFromIndex(UseIndex) == Fixture->Blocks[0], + "SlotIndexes reverse mappings must accept every instruction subslot"); + + auto Inserted = buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg, {}, *Use); + Context.expect(Inserted && (*Indexes)->insertInstruction(**Inserted).has_value(), + "local SlotIndex insertion must use reserved numbering space"); + if (Inserted) { + const rcc::SlotIndex InsertedIndex = (*Indexes)->instructionIndex(**Inserted); + Context.expect(DefIndex < InsertedIndex && InsertedIndex < UseIndex, + "locally inserted SlotIndex must lie between adjacent instructions"); + Context.expect((*Indexes)->eraseInstruction(**Inserted).has_value() && + !(*Indexes)->instructionIndex(**Inserted).isValid() && + (*Indexes)->renumber(*Fixture->Function).has_value() && + (*Indexes)->instructionIndex(**Inserted).isValid(), + "SlotIndex erasure and full renumbering must rebuild exact mappings"); + + auto Second = buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg, {}, *Inserted); + auto SecondIndexed = Second ? (*Indexes)->insertInstruction(**Second) + : rcc::Expected<void>{std::unexpected(Second.error())}; + auto Third = Second + ? buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg, {}, *Second) + : rcc::Expected<rcc::MachineInstr *>{std::unexpected(Second.error())}; + auto ThirdIndexed = Third ? (*Indexes)->insertInstruction(**Third) + : rcc::Expected<void>{std::unexpected(Third.error())}; + auto Exhausted = + Third ? buildUse(*Fixture->Function, Target, *Fixture->Blocks[0], *Reg, {}, *Third) + : rcc::Expected<rcc::MachineInstr *>{std::unexpected(Third.error())}; + auto ExhaustedIndex = Exhausted ? (*Indexes)->insertInstruction(**Exhausted) + : rcc::Expected<void>{std::unexpected(Exhausted.error())}; + Context.expect(SecondIndexed && ThirdIndexed && Exhausted && !ExhaustedIndex && + ExhaustedIndex.error().code() == rcc::ErrorCode::Unsupported && + (*Indexes)->renumber(*Fixture->Function).has_value() && + (*Indexes)->instructionIndex(**Exhausted).isValid(), + "local SlotIndex exhaustion must request and recover through full renumbering"); + } +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "liveness TestTarget must be created"); + if (!Target) + return Context.result(); + testSlotIndexes(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/SpillerTest.cpp b/backend/unittests/CodeGen/SpillerTest.cpp new file mode 100644 index 0000000..1f22d8f --- /dev/null +++ b/backend/unittests/CodeGen/SpillerTest.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/Spiller.h" + +#include "CodeGen/RegisterAllocationTestSupport.h" + +#include <iostream> +#include <optional> + +namespace { + +using namespace rcc; +using namespace rcc::test; +using namespace rcc::test::ra; + +void testRematerialization(TestContext &Context, const TargetDescription &Target) { + auto Fixture = createFunction("spiller-remat", 20); + if (!Fixture) + return; + auto Value = createVReg(*Fixture->Function); + if (!Value || !buildDef(*Fixture->Function, Target, *Fixture->Block, *Value, 42, {}, true) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value)) + return; + VirtRegMap Mapping(*Fixture->Function); + TestRegisterAllocationInfo Info; + if (!Mapping.add(*Value)) + return; + Spiller ValueSpiller(*Fixture->Function, Target, Info, Mapping, true); + auto Children = ValueSpiller.spill(*Value); + Context.expect(Children && Children->size() == 2 && + Fixture->Function->frameInfo().liveObjectCount() == 0 && + countOpcode(*Fixture->Function, testOpcode(RematerializableDefOpcode)) == 2 && + countOpcode(*Fixture->Function, testOpcode(SpillLoadOpcode)) == 0, + "Spiller must rematerialize each use without allocating a stack slot"); +} + +void testDirectSpillAndDebugUse(TestContext &Context, const TargetDescription &Target) { + auto Fixture = createFunction("spiller-debug", 21); + if (!Fixture) + return; + auto Value = createVReg(*Fixture->Function); + if (!Value || !buildDef(*Fixture->Function, Target, *Fixture->Block, *Value, 7) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value, {}, true) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value)) + return; + VirtRegMap Mapping(*Fixture->Function); + TestRegisterAllocationInfo Info; + if (!Mapping.add(*Value)) + return; + Spiller ValueSpiller(*Fixture->Function, Target, Info, Mapping, false); + auto Children = ValueSpiller.spill(*Value); + const MachineOperand *DebugOperand = nullptr; + for (const MachineInstr *Instruction = Fixture->Block->firstInstruction(); Instruction != nullptr; + Instruction = Instruction->next()) { + for (const MachineOperand &Operand : Instruction->operands()) { + if (Operand.isDebugUse()) { + DebugOperand = &Operand; + break; + } + } + if (DebugOperand != nullptr) + break; + } + const std::optional<Register> DebugRegister = + DebugOperand == nullptr ? std::nullopt : DebugOperand->registerValue(); + const bool DebugRegisterCleared = DebugRegister && DebugRegister.value().isNoRegister(); + Context.expect(Children && !Children->empty() && + countOpcode(*Fixture->Function, testOpcode(SpillLoadOpcode)) != 0 && + countOpcode(*Fixture->Function, testOpcode(SpillStoreOpcode)) != 0 && + DebugOperand != nullptr && DebugRegisterCleared && + DebugOperand->registerFlags().has(RegisterOperandFlag::Undef), + "Spiller must preserve direct spill code and clear debug-only uses"); + + auto Unspillable = createVReg(*Fixture->Function); + if (!Unspillable || !Mapping.add(*Unspillable, false)) + return; + auto Rejected = ValueSpiller.spill(*Unspillable); + Context.expect(!Rejected && Rejected.error().code() == ErrorCode::RegisterAllocationFailed, + "Spiller must reject an explicitly non-spillable value"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "Spiller test target must be constructible"); + if (!Target) + return Context.result(); + testRematerialization(Context, *Target); + testDirectSpillAndDebugUse(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/TargetCallLoweringTest.cpp b/backend/unittests/CodeGen/TargetCallLoweringTest.cpp new file mode 100644 index 0000000..22a2b0c --- /dev/null +++ b/backend/unittests/CodeGen/TargetCallLoweringTest.cpp @@ -0,0 +1,328 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/TargetCallLowering.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" + +#include <concepts> +#include <cstdint> +#include <iostream> +#include <memory> +#include <span> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::Alignment align(std::uint64_t Bytes) { + const auto Value = rcc::Alignment::create(Bytes); + if (!Value) + std::terminate(); + return *Value; +} + +[[nodiscard]] rcc::LowLevelType integer(std::uint32_t Bits) { + const auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(Bits); + if (!Type) + std::terminate(); + return *Type; +} + +[[nodiscard]] rcc::PhysicalRegister physical(std::uint32_t ID) { + const auto Register = rcc::PhysicalRegister::create(ID); + if (!Register) + std::terminate(); + return *Register; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + rcc::InstructionDescriptor Nop; + Nop.InstructionOpcode = rcc::Opcode::fromValue(1); + Nop.Name = "TEST_NOP"; + Nop.Flags = {rcc::InstructionFlag::ProducesCode}; + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Nop)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] rcc::CallLoweringPlanDesc emptyPlanDescription(rcc::CallLoweringPlanKind Kind) { + rcc::CallLoweringPlanDesc Description; + Description.Kind = Kind; + Description.Target = "test-target:test-abi"; + Description.Convention = rcc::CallingConventionID::fromValue(1); + Description.StackArguments.Align = align(8); + Description.CallFrame.Align = align(8); + if (Kind == rcc::CallLoweringPlanKind::Call) { + Description.CallFrame.EmitSetup = true; + Description.CallFrame.EmitDestroy = true; + Description.PreservedMask = rcc::test::createTestCallPreservedMask(); + Description.Callee = *rcc::MachineCallTarget::externalSymbol("callee"); + } + return Description; +} + +[[nodiscard]] rcc::Expected<rcc::CallLoweringPlan> emptyPlan(rcc::CallLoweringPlanKind Kind) { + return rcc::CallLoweringPlan::create(emptyPlanDescription(Kind)); +} + +[[nodiscard]] rcc::CCAssignment registerAssignment() { + return { + .ValueIndex = 0, + .OriginalValue = rcc::Register::fromPhysical(physical(2)), + .OriginalValueIndex = 0, + .FragmentIndex = 0, + .FragmentCount = 1, + .ValueType = integer(64), + .LocationType = integer(64), + .LocationKind = rcc::CCLocationKind::Register, + .LocationRegister = physical(1), + .StackOffsetBytes = std::nullopt, + .LocationSizeBytes = 8, + .StackSlotSizeBytes = 8, + .LocationAlignment = align(8), + .Extension = rcc::ABIExtensionKind::None, + .CustomFlags = {}, + }; +} + +void testPlanValidation(TestContext &Context) { + static_assert( + std::same_as<decltype(std::declval<const rcc::CallLoweringPlan &>().argumentAssignments()), + std::span<const rcc::CCAssignment>>); + static_assert(std::same_as<decltype(std::declval<const rcc::CallLoweringPlan &>().byValCopies()), + std::span<const rcc::ByValCopyAssignment>>); + + auto Valid = emptyPlan(rcc::CallLoweringPlanKind::FormalArguments); + Context.expect(Valid && Valid->target() == "test-target:test-abi" && + Valid->argumentAssignments().empty(), + "a valid immutable formal-argument plan must be accepted"); + + auto MissingRegister = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments); + auto Assignment = registerAssignment(); + Assignment.OriginalValue = {}; + MissingRegister.ArgumentAssignments.push_back(Assignment); + Context.expect(!rcc::CallLoweringPlan::create(std::move(MissingRegister)), + "an assignment without a machine value must be rejected"); + + auto BadStack = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments); + BadStack.StackArguments.SizeBytes = 8; + auto StackAssignment = registerAssignment(); + StackAssignment.LocationKind = rcc::CCLocationKind::Stack; + StackAssignment.LocationRegister.reset(); + StackAssignment.StackOffsetBytes = 8; + BadStack.ArgumentAssignments.push_back(StackAssignment); + Context.expect(!rcc::CallLoweringPlan::create(std::move(BadStack)), + "an assignment outside the declared stack area must be rejected"); + + auto Overlap = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments); + Overlap.StackArguments.SizeBytes = 16; + StackAssignment.StackOffsetBytes = 0; + Overlap.ArgumentAssignments.push_back(StackAssignment); + StackAssignment.OriginalValue = rcc::Register::fromPhysical(physical(3)); + StackAssignment.StackOffsetBytes = 4; + Overlap.ArgumentAssignments.push_back(StackAssignment); + Context.expect(!rcc::CallLoweringPlan::create(std::move(Overlap)), + "overlapping stack assignments must be rejected"); + + auto MisalignedStack = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments); + MisalignedStack.StackArguments.SizeBytes = 16; + StackAssignment.StackOffsetBytes = 4; + MisalignedStack.ArgumentAssignments.push_back(StackAssignment); + Context.expect(!rcc::CallLoweringPlan::create(std::move(MisalignedStack)), + "a stack assignment must satisfy its declared alignment"); + + auto OversizedLocation = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments); + auto OversizedAssignment = registerAssignment(); + OversizedAssignment.LocationSizeBytes = 16; + OversizedLocation.ArgumentAssignments.push_back(OversizedAssignment); + Context.expect(!rcc::CallLoweringPlan::create(std::move(OversizedLocation)), + "an ABI location cannot exceed its transport slot"); + + auto UnknownLocation = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments); + auto UnknownAssignment = registerAssignment(); + UnknownAssignment.LocationKind = + static_cast<rcc::CCLocationKind>(255); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange) + UnknownLocation.ArgumentAssignments.push_back(UnknownAssignment); + Context.expect(!rcc::CallLoweringPlan::create(std::move(UnknownLocation)), + "an unknown ABI location kind must be rejected"); + + auto BadFrame = emptyPlanDescription(rcc::CallLoweringPlanKind::Call); + BadFrame.CallFrame.EmitDestroy = false; + Context.expect(!rcc::CallLoweringPlan::create(std::move(BadFrame)), + "unpaired call-frame setup and destroy requirements must be rejected"); + + auto SmallFrame = emptyPlanDescription(rcc::CallLoweringPlanKind::Call); + SmallFrame.StackArguments.SizeBytes = 16; + SmallFrame.CallFrame.SizeBytes = 8; + StackAssignment.StackOffsetBytes = 0; + SmallFrame.ArgumentAssignments.push_back(StackAssignment); + Context.expect(!rcc::CallLoweringPlan::create(std::move(SmallFrame)), + "a call frame must contain its complete stack-argument area"); + + auto BadTail = emptyPlanDescription(rcc::CallLoweringPlanKind::Call); + BadTail.CallFrame.EmitSetup = false; + BadTail.CallFrame.EmitDestroy = false; + BadTail.CallFrame.ReusesIncomingArea = true; + BadTail.TailDecision = rcc::TailCallKind::Tail; + BadTail.TailRejectionReason = "inconsistent accepted tail call"; + Context.expect(!rcc::CallLoweringPlan::create(std::move(BadTail)), + "an accepted tail call cannot retain a rejection reason"); + + auto UnknownTail = emptyPlanDescription(rcc::CallLoweringPlanKind::Call); + UnknownTail.TailDecision = + static_cast<rcc::TailCallKind>(255); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange) + Context.expect(!rcc::CallLoweringPlan::create(std::move(UnknownTail)), + "an unknown tail-call decision must be rejected"); + + auto BadVarArgs = emptyPlanDescription(rcc::CallLoweringPlanKind::FormalArguments); + BadVarArgs.VarArgs.IsVariadic = true; + BadVarArgs.VarArgs.RegisterSaveAreaSizeBytes = 8; + BadVarArgs.VarArgs.RegistersToSave.push_back({physical(1), 8}); + Context.expect(!rcc::CallLoweringPlan::create(std::move(BadVarArgs)), + "a variadic register save outside its area must be rejected"); + + auto BadByVal = emptyPlanDescription(rcc::CallLoweringPlanKind::Call); + auto ByValAssignment = registerAssignment(); + ByValAssignment.ValueType = *rcc::LowLevelType::makePointer(0, 64); + ByValAssignment.LocationType = ByValAssignment.ValueType; + ByValAssignment.CustomFlags.set(rcc::CCAssignmentFlag::ByVal); + BadByVal.ArgumentAssignments.push_back(ByValAssignment); + BadByVal.ByValCopies.push_back( + {0, ByValAssignment.OriginalValue, ByValAssignment.ValueType, 0, 16, align(8)}); + Context.expect(!rcc::CallLoweringPlan::create(std::move(BadByVal)), + "a byval copy outside the call frame must be rejected"); + + auto OverlappingByVal = emptyPlanDescription(rcc::CallLoweringPlanKind::Call); + OverlappingByVal.CallFrame.SizeBytes = 32; + auto FirstByVal = registerAssignment(); + FirstByVal.ValueType = *rcc::LowLevelType::makePointer(0, 64); + FirstByVal.LocationType = FirstByVal.ValueType; + FirstByVal.CustomFlags.set(rcc::CCAssignmentFlag::ByVal); + auto SecondByVal = FirstByVal; + SecondByVal.ValueIndex = 1; + SecondByVal.OriginalValue = rcc::Register::fromPhysical(physical(3)); + SecondByVal.OriginalValueIndex = 1; + SecondByVal.LocationRegister = physical(2); + OverlappingByVal.ArgumentAssignments = {FirstByVal, SecondByVal}; + OverlappingByVal.ByValCopies = { + {0, FirstByVal.OriginalValue, FirstByVal.ValueType, 0, 16, align(8)}, + {1, SecondByVal.OriginalValue, SecondByVal.ValueType, 8, 16, align(8)}, + }; + Context.expect(!rcc::CallLoweringPlan::create(std::move(OverlappingByVal)), + "overlapping byval copy ranges must be rejected"); + + Context.expect(!rcc::MachineCallTarget::globalSymbol("") && !rcc::MachineCallTarget::indirect({}), + "call-target factories must reject missing targets"); +} + +class RecordingCallLowering final : public rcc::TargetCallLowering { +public: + enum class Behavior : std::uint8_t { + Valid, + PlannerFailure, + WrongKind, + }; + + explicit RecordingCallLowering(Behavior CurrentBehavior) : CurrentBehavior(CurrentBehavior) {} + + [[nodiscard]] rcc::Expected<rcc::CallLoweringPlan> + planFormalArguments(const rcc::FormalArgumentRequest &) const override { + return plan(rcc::CallLoweringPlanKind::FormalArguments); + } + + [[nodiscard]] rcc::Expected<rcc::CallLoweringPlan> + planCall(const rcc::CallLoweringRequest &) const override { + return plan(rcc::CallLoweringPlanKind::Call); + } + + [[nodiscard]] rcc::Expected<rcc::CallLoweringPlan> + planReturn(const rcc::ReturnLoweringRequest &) const override { + return plan(rcc::CallLoweringPlanKind::Return); + } + + [[nodiscard]] rcc::Expected<void> emitPlan(rcc::MachineIRBuilder &, + const rcc::CallLoweringPlan &) const override { + ++EmissionCount; + return {}; + } + + [[nodiscard]] std::uint32_t emissions() const noexcept { + return EmissionCount; + } + +private: + [[nodiscard]] rcc::Expected<rcc::CallLoweringPlan> + plan(rcc::CallLoweringPlanKind RequestedKind) const { + if (CurrentBehavior == Behavior::PlannerFailure) { + return rcc::makeUnexpected(rcc::ErrorCode::Unsupported, + "intentional call-lowering planner failure"); + } + if (CurrentBehavior == Behavior::WrongKind) + RequestedKind = RequestedKind == rcc::CallLoweringPlanKind::Call + ? rcc::CallLoweringPlanKind::Return + : rcc::CallLoweringPlanKind::Call; + return emptyPlan(RequestedKind); + } + + Behavior CurrentBehavior; + mutable std::uint32_t EmissionCount = 0; +}; + +void testPlanThenEmitBoundary(TestContext &Context) { + auto Target = createTarget(); + auto Function = + rcc::MachineFunction::create("plan_then_emit", rcc::MachineFunctionStableID::fromValue(1)); + Context.expect(Target && Function, "call-lowering workflow fixtures must be valid"); + if (!Target || !Function) + return; + rcc::MachineIRBuilder Builder(**Function, *Target); + rcc::CallLoweringRequest Request; + + RecordingCallLowering Failing(RecordingCallLowering::Behavior::PlannerFailure); + auto Failure = Failing.lowerCall(Builder, Request); + Context.expect(!Failure && Failing.emissions() == 0 && (*Function)->allInstructions().empty(), + "planner failure must not invoke emission or mutate MachineIR"); + + RecordingCallLowering WrongKind(RecordingCallLowering::Behavior::WrongKind); + auto Mismatch = WrongKind.lowerCall(Builder, Request); + Context.expect(!Mismatch && Mismatch.error().code() == rcc::ErrorCode::InternalError && + WrongKind.emissions() == 0, + "a target returning the wrong plan kind must be rejected before emission"); + + RecordingCallLowering Valid(RecordingCallLowering::Behavior::Valid); + Context.expect(Valid.lowerCall(Builder, Request) && Valid.emissions() == 1, + "a valid plan must be emitted exactly once"); +} + +} // namespace + +int main() { + TestContext Context; + testPlanValidation(Context); + testPlanThenEmitBoundary(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/TargetConstantTest.cpp b/backend/unittests/CodeGen/TargetConstantTest.cpp new file mode 100644 index 0000000..77338b7 --- /dev/null +++ b/backend/unittests/CodeGen/TargetConstantTest.cpp @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/TargetConstant.h" + +#include <bit> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <optional> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +struct TestRelocConstant final { + std::int64_t Addend = 0; + std::string Symbol; + + [[nodiscard]] static constexpr rcc::TargetConstantTypeID stableTypeID() noexcept { + return rcc::TargetConstantTypeID::fromValue(0x5253434354430001ULL); + } + + [[nodiscard]] static constexpr std::uint32_t schemaVersion() noexcept { + return 2; + } + + [[nodiscard]] rcc::Expected<void> verify() const { + if (Symbol.empty()) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "test target constant symbol cannot be empty"); + } + return {}; + } + + [[nodiscard]] rcc::Expected<TestRelocConstant> clone() const { + return TestRelocConstant{Addend, Symbol}; + } + + [[nodiscard]] std::uint64_t stableHash() const noexcept { + std::uint64_t Hash = 1469598103934665603ULL; + for (const char RawCharacter : Symbol) { + const auto Character = static_cast<unsigned char>(RawCharacter); + Hash ^= Character; + Hash *= 1099511628211ULL; + } + Hash ^= static_cast<std::uint64_t>(Addend); + return Hash * 1099511628211ULL; + } + + [[nodiscard]] std::string print() const { + return Symbol + "+" + std::to_string(Addend); + } + + [[nodiscard]] std::vector<std::byte> addendBytes() const { + std::vector<std::byte> Bytes(8); + const std::uint64_t Bits = static_cast<std::uint64_t>(Addend); + for (std::size_t Index = 0; Index < Bytes.size(); ++Index) + Bytes[Index] = static_cast<std::byte>((Bits >> (Index * 8U)) & 0xffU); + return Bytes; + } + + [[nodiscard]] rcc::Expected<std::vector<std::byte>> encode() const { + std::vector<std::byte> Payload = addendBytes(); + for (const char RawCharacter : Symbol) { + const auto Character = static_cast<unsigned char>(RawCharacter); + Payload.push_back(static_cast<std::byte>(Character)); + } + return Payload; + } + + [[nodiscard]] rcc::Expected<rcc::TargetConstantEmission> emit() const { + rcc::TargetConstantEmission Emission; + Emission.Bytes = addendBytes(); + Emission.Fixups.push_back({0, rcc::TargetRelocationKindID::fromValue(9), Symbol, Addend}); + return Emission; + } + + [[nodiscard]] rcc::Alignment requiredAlignment() const noexcept { + return *rcc::Alignment::create(8); + } + + [[nodiscard]] std::optional<rcc::TargetRelocationKindID> relocationKind() const noexcept { + return rcc::TargetRelocationKindID::fromValue(9); + } + + [[nodiscard]] static rcc::Expected<TestRelocConstant> parse(std::string_view Text) { + if (Text.empty()) + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, "test constant text is empty"); + return TestRelocConstant{7, std::string(Text)}; + } + + [[nodiscard]] static rcc::Expected<TestRelocConstant> decode(std::uint32_t Version, + std::span<const std::byte> Payload) { + if (Version != schemaVersion()) + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, + "unsupported test constant schema version"); + if (Payload.size() <= 8) + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, "test constant payload is incomplete"); + + std::uint64_t Bits = 0; + for (std::size_t Index = 0; Index < 8; ++Index) + Bits |= std::to_integer<std::uint64_t>(Payload[Index]) << (Index * 8U); + std::string Symbol; + Symbol.reserve(Payload.size() - 8); + for (const std::byte Character : Payload.subspan(8)) + Symbol.push_back(static_cast<char>(std::to_integer<unsigned char>(Character))); + return TestRelocConstant{std::bit_cast<std::int64_t>(Bits), std::move(Symbol)}; + } + + friend bool operator==(const TestRelocConstant &Left, const TestRelocConstant &Right) = default; +}; + +struct CollidingConstant final { + TestRelocConstant Value; + + [[nodiscard]] static constexpr rcc::TargetConstantTypeID stableTypeID() noexcept { + return TestRelocConstant::stableTypeID(); + } + + [[nodiscard]] static constexpr std::uint32_t schemaVersion() noexcept { + return TestRelocConstant::schemaVersion(); + } + + [[nodiscard]] rcc::Expected<void> verify() const { + return Value.verify(); + } + + [[nodiscard]] rcc::Expected<CollidingConstant> clone() const { + return CollidingConstant{Value}; + } + + [[nodiscard]] std::uint64_t stableHash() const noexcept { + return Value.stableHash(); + } + + [[nodiscard]] std::string print() const { + return Value.print(); + } + + [[nodiscard]] rcc::Expected<std::vector<std::byte>> encode() const { + return Value.encode(); + } + + [[nodiscard]] rcc::Expected<rcc::TargetConstantEmission> emit() const { + return Value.emit(); + } + + [[nodiscard]] rcc::Alignment requiredAlignment() const noexcept { + return Value.requiredAlignment(); + } + + [[nodiscard]] std::optional<rcc::TargetRelocationKindID> relocationKind() const noexcept { + return Value.relocationKind(); + } + + [[nodiscard]] static rcc::Expected<CollidingConstant> parse(std::string_view Text) { + auto Parsed = TestRelocConstant::parse(Text); + if (!Parsed) + return std::unexpected(Parsed.error()); + return CollidingConstant{std::move(*Parsed)}; + } + + [[nodiscard]] static rcc::Expected<CollidingConstant> decode(std::uint32_t Version, + std::span<const std::byte> Payload) { + auto Decoded = TestRelocConstant::decode(Version, Payload); + if (!Decoded) + return std::unexpected(Decoded.error()); + return CollidingConstant{std::move(*Decoded)}; + } + + friend bool operator==(const CollidingConstant &Left, const CollidingConstant &Right) = default; +}; + +void testTargetConstantContract(TestContext &Context) { + auto Constant = rcc::TargetConstant::create(TestRelocConstant{11, "symbol"}); + Context.expect(Constant.has_value(), "valid target constant must be type-erased"); + if (!Constant) + return; + + Context.expect(Constant->typeID() == TestRelocConstant::stableTypeID() && + Constant->schemaVersion() == TestRelocConstant::schemaVersion() && + Constant->getIf<TestRelocConstant>() != nullptr && + Constant->getIf<CollidingConstant>() == nullptr, + "strong query must require both stable and local type identity"); + auto Printed = Constant->print(); + auto RequiredAlignment = Constant->requiredAlignment(); + auto RelocationKind = Constant->relocationKind(); + Context.expect(Constant->verify().has_value() && Printed && *Printed == "symbol+11" && + RequiredAlignment && *RequiredAlignment == *rcc::Alignment::create(8) && + RelocationKind && *RelocationKind == rcc::TargetRelocationKindID::fromValue(9), + "target constant semantic hooks must remain available through erasure"); + + auto Cloned = Constant->clone(); + Context.expect(Cloned && Constant->equals(*Cloned) && + Cloned->getIf<TestRelocConstant>() != Constant->getIf<TestRelocConstant>(), + "target constant clone must own an equal independent value"); + + auto Encoded = Constant->encode(); + if (!Encoded) + return; + auto Decoded = rcc::TargetConstant::decode<TestRelocConstant>(*Encoded); + Context.expect(Encoded && Decoded && Constant->equals(*Decoded), + "versioned target constant encoding must round-trip losslessly"); + + auto Parsed = rcc::TargetConstant::parse<TestRelocConstant>("parsed"); + const TestRelocConstant *ParsedValue = Parsed ? Parsed->getIf<TestRelocConstant>() : nullptr; + Context.expect(ParsedValue != nullptr && ParsedValue->Addend == 7 && + ParsedValue->Symbol == "parsed", + "target constant text parser must construct the same strong type"); + + auto Emission = Constant->emit(); + Context.expect(Emission && Emission->Bytes.size() == 8 && Emission->Fixups.size() == 1 && + Emission->Fixups[0].Symbol == "symbol" && + Emission->Fixups[0].Kind == rcc::TargetRelocationKindID::fromValue(9), + "target constant emission must preserve bytes and structured fixups"); +} + +void testTargetConstantFailures(TestContext &Context) { + rcc::TargetConstant Empty; + Context.expect(!Empty.verify() && !Empty.clone() && !Empty.encode() && !Empty.emit(), + "empty target constant must reject every semantic operation"); + Context.expect(!rcc::TargetConstant::create(TestRelocConstant{0, ""}), + "target constant creation must verify its value"); + + auto Constant = rcc::TargetConstant::create(TestRelocConstant{3, "symbol"}); + if (!Constant) + return; + auto Encoded = Constant->encode(); + if (!Encoded) + return; + Encoded->Version = 99; + Context.expect(!rcc::TargetConstant::decode<TestRelocConstant>(*Encoded), + "target constant decode must reject unsupported versions"); + Encoded->Version = TestRelocConstant::schemaVersion(); + Encoded->TypeID = rcc::TargetConstantTypeID::fromValue(1); + Context.expect(!rcc::TargetConstant::decode<TestRelocConstant>(*Encoded), + "target constant decode must reject a mismatched TypeID"); + + rcc::TargetConstantEmission Invalid; + Invalid.Bytes.resize(4); + Invalid.Fixups.push_back({4, rcc::TargetRelocationKindID::fromValue(1), "symbol", 0}); + Context.expect(!Invalid.verify(), "target emission must reject out-of-range fixups"); + Invalid.Fixups[0].Offset = 0; + Invalid.Fixups[0].Kind = {}; + Context.expect(!Invalid.verify(), "target emission must reject invalid relocation kinds"); + Invalid.Fixups[0].Kind = rcc::TargetRelocationKindID::fromValue(1); + Invalid.Fixups.push_back(Invalid.Fixups[0]); + Context.expect(!Invalid.verify(), "target emission must reject duplicate fixup offsets"); +} + +} // namespace + +int main() { + TestContext Context; + testTargetConstantContract(Context); + testTargetConstantFailures(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/TargetFunctionStateTest.cpp b/backend/unittests/CodeGen/TargetFunctionStateTest.cpp new file mode 100644 index 0000000..2286400 --- /dev/null +++ b/backend/unittests/CodeGen/TargetFunctionStateTest.cpp @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/TargetFunctionState.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineFunctionCloner.h" +#include "rcc/CodeGen/MachineVerifier.h" + +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +struct TestFunctionInfo final { + std::uint64_t StackProbeSize = 0; + bool UsesShadowCallStack = false; + std::string CallingConvention; + + [[nodiscard]] static constexpr rcc::TargetFunctionStateTypeID stableTypeID() noexcept { + return rcc::TargetFunctionStateTypeID::fromValue(0x5243435446530001ULL); + } + + [[nodiscard]] static constexpr std::uint32_t schemaVersion() noexcept { + return 3; + } + + [[nodiscard]] rcc::Expected<void> verify() const { + if (StackProbeSize == 0 || CallingConvention.empty()) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "test function info is incomplete"); + } + return {}; + } + + [[nodiscard]] rcc::Expected<TestFunctionInfo> + clone(const rcc::MachineFunctionCloneContext &) const { + return TestFunctionInfo{StackProbeSize, UsesShadowCallStack, CallingConvention}; + } + + [[nodiscard]] std::string print() const { + return CallingConvention + ":" + std::to_string(StackProbeSize) + ":" + + (UsesShadowCallStack ? "shadow" : "plain"); + } + + [[nodiscard]] rcc::Expected<std::vector<std::byte>> encode() const { + std::vector<std::byte> Payload; + Payload.reserve(9 + CallingConvention.size()); + for (std::size_t Index = 0; Index < 8; ++Index) { + Payload.push_back(static_cast<std::byte>((StackProbeSize >> (Index * 8U)) & 0xffU)); + } + Payload.push_back(static_cast<std::byte>(UsesShadowCallStack ? 1 : 0)); + for (const char RawCharacter : CallingConvention) { + Payload.push_back(static_cast<std::byte>(static_cast<unsigned char>(RawCharacter))); + } + return Payload; + } + + [[nodiscard]] static rcc::Expected<TestFunctionInfo> decode(std::uint32_t Version, + std::span<const std::byte> Payload) { + if (Version != schemaVersion()) { + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, + "unsupported test function info version"); + } + if (Payload.size() <= 9 || std::to_integer<unsigned int>(Payload[8]) > 1) { + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, + "test function info payload is invalid"); + } + + std::uint64_t StackProbeSize = 0; + for (std::size_t Index = 0; Index < 8; ++Index) { + StackProbeSize |= std::to_integer<std::uint64_t>(Payload[Index]) << (Index * 8U); + } + std::string CallingConvention; + CallingConvention.reserve(Payload.size() - 9); + for (const std::byte Character : Payload.subspan(9)) { + CallingConvention.push_back(static_cast<char>(std::to_integer<unsigned char>(Character))); + } + return TestFunctionInfo{StackProbeSize, std::to_integer<unsigned int>(Payload[8]) != 0, + std::move(CallingConvention)}; + } + + friend bool operator==(const TestFunctionInfo &Left, const TestFunctionInfo &Right) = default; +}; + +struct CollidingFunctionInfo final { + TestFunctionInfo Value; + + [[nodiscard]] static constexpr rcc::TargetFunctionStateTypeID stableTypeID() noexcept { + return TestFunctionInfo::stableTypeID(); + } + + [[nodiscard]] static constexpr std::uint32_t schemaVersion() noexcept { + return TestFunctionInfo::schemaVersion(); + } + + [[nodiscard]] rcc::Expected<void> verify() const { + return Value.verify(); + } + + [[nodiscard]] rcc::Expected<CollidingFunctionInfo> + clone(const rcc::MachineFunctionCloneContext &) const { + return CollidingFunctionInfo{Value}; + } + + [[nodiscard]] std::string print() const { + return Value.print(); + } + + [[nodiscard]] rcc::Expected<std::vector<std::byte>> encode() const { + return Value.encode(); + } + + [[nodiscard]] static rcc::Expected<CollidingFunctionInfo> + decode(std::uint32_t Version, std::span<const std::byte> Payload) { + auto Decoded = TestFunctionInfo::decode(Version, Payload); + if (!Decoded) + return std::unexpected(Decoded.error()); + return CollidingFunctionInfo{std::move(*Decoded)}; + } + + friend bool operator==(const CollidingFunctionInfo &Left, + const CollidingFunctionInfo &Right) = default; +}; + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + rcc::InstructionDescriptor Descriptor; + Descriptor.InstructionOpcode = rcc::Opcode::fromValue(1); + Descriptor.Name = "TEST_NOP"; + Descriptor.Flags = {rcc::InstructionFlag::ProducesCode}; + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Descriptor)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +void testTargetFunctionStateContract(TestContext &Context) { + auto State = rcc::TargetFunctionState::create(TestFunctionInfo{4096, true, "lp64d"}); + Context.expect(State.has_value(), "valid target function state must be type-erased"); + if (!State) + return; + + Context.expect(State->typeID() == TestFunctionInfo::stableTypeID() && + State->schemaVersion() == TestFunctionInfo::schemaVersion() && + State->getIf<TestFunctionInfo>() != nullptr && + State->getIf<CollidingFunctionInfo>() == nullptr, + "strong query must require stable and local type identity"); + auto Printed = State->print(); + Context.expect(State->verify().has_value() && Printed && *Printed == "lp64d:4096:shadow", + "type-erased target state must preserve its semantic hooks"); + + auto Encoded = State->encode(); + if (!Encoded) + return; + auto Decoded = rcc::TargetFunctionState::decode<TestFunctionInfo>(*Encoded); + Context.expect(Decoded && State->equals(*Decoded) && + State->getIf<TestFunctionInfo>() != Decoded->getIf<TestFunctionInfo>(), + "versioned target function state must round-trip losslessly"); + + auto Collision = rcc::TargetFunctionState::create( + CollidingFunctionInfo{TestFunctionInfo{4096, true, "lp64d"}}); + Context.expect(Collision && !State->equals(*Collision), + "colliding stable TypeIDs must not defeat local type safety"); +} + +void testTargetFunctionStateFailures(TestContext &Context) { + rcc::TargetFunctionState Empty; + Context.expect(!Empty.verify() && !Empty.print() && !Empty.encode(), + "empty target function state must reject semantic operations"); + Context.expect(!rcc::TargetFunctionState::create(TestFunctionInfo{0, false, ""}), + "target function state creation must verify its concrete value"); + + auto State = rcc::TargetFunctionState::create(TestFunctionInfo{2048, false, "lp64"}); + if (!State) + return; + auto Encoded = State->encode(); + if (!Encoded) + return; + + Encoded->Version = 0; + Context.expect(!rcc::TargetFunctionState::decode<TestFunctionInfo>(*Encoded), + "target function state decode must reject version zero"); + Encoded->Version = 99; + Context.expect(!rcc::TargetFunctionState::decode<TestFunctionInfo>(*Encoded), + "target function state decode must reject unsupported versions"); + Encoded->Version = TestFunctionInfo::schemaVersion(); + Encoded->TypeID = rcc::TargetFunctionStateTypeID::fromValue(1); + Context.expect(!rcc::TargetFunctionState::decode<TestFunctionInfo>(*Encoded), + "target function state decode must reject a mismatched TypeID"); +} + +void testMachineFunctionOwnershipAndVerification(TestContext &Context) { + auto Target = createTarget(); + auto Function = + rcc::MachineFunction::create("target_state", rcc::MachineFunctionStableID::fromValue(1)); + if (!Target || !Function) + return; + + Context.expect(!(*Function)->setTargetState(rcc::TargetFunctionState{}), + "machine function must reject an empty target state"); + auto State = rcc::TargetFunctionState::create(TestFunctionInfo{4096, false, "lp64d"}); + Context.expect(State && (*Function)->setTargetState(std::move(*State)).has_value(), + "machine function must accept one verified target state"); + Context.expect((*Function)->getTargetInfo<TestFunctionInfo>() != nullptr && + (*Function)->getTargetInfo<CollidingFunctionInfo>() == nullptr && + rcc::MachineVerifier::verify(**Function, *Target).has_value(), + "machine function must expose and verify its strong target state"); + + auto Replacement = rcc::TargetFunctionState::create(TestFunctionInfo{8192, true, "lp64d"}); + Context.expect(Replacement && !(*Function)->setTargetState(std::move(*Replacement)), + "machine function must not accept a second current target state"); + + TestFunctionInfo *Info = (*Function)->getTargetInfo<TestFunctionInfo>(); + if (Info == nullptr) + return; + Info->CallingConvention.clear(); + Context.expect(!rcc::MachineVerifier::verify(**Function, *Target), + "machine verifier must reject invalid concrete target state"); +} + +} // namespace + +int main() { + TestContext Context; + testTargetFunctionStateContract(Context); + testTargetFunctionStateFailures(Context); + testMachineFunctionOwnershipAndVerification(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/TargetMIRTextInfoTest.cpp b/backend/unittests/CodeGen/TargetMIRTextInfoTest.cpp new file mode 100644 index 0000000..24945bd --- /dev/null +++ b/backend/unittests/CodeGen/TargetMIRTextInfoTest.cpp @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/TargetMIRTextInfo.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MachineCFIInfo.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/TargetDescription.h" +#include "rcc/CodeGen/TargetRegisterInfo.h" + +#include <array> +#include <cstdint> +#include <iostream> +#include <memory> +#include <span> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::PhysicalRegister phys(std::uint32_t Number) { + return *rcc::PhysicalRegister::create(Number); +} + +void testTargetRegisterNames(TestContext &Context) { + auto Registers = rcc::test::createTestTargetRegisterInfo(); + Context.expect(Registers.has_value(), "TestTarget register schema must be valid"); + if (!Registers) + return; + + const auto G0 = phys(rcc::test::TestRegister::G0); + Context.expect(Registers->physicalRegister("g0") != nullptr && + Registers->physicalRegister("G0") == nullptr && + Registers->physicalRegisterMIRName(G0) == "g0", + "physical register MIR names must be canonical lowercase names"); + Context.expect(Registers->subRegIndex("low32") != nullptr && + Registers->subRegIndex("LOW32") == nullptr, + "subregister MIR names must retain their exact target spelling"); + Context.expect(Registers->registerClass("gpr64") != nullptr && + Registers->registerClass("GPR64") == nullptr && + Registers->registerClassMIRName(rcc::test::TestRegisterClass::GPR64) == + "gpr64", + "register class MIR names must be canonical lowercase names"); + Context.expect(Registers->registerBank("gpr") != nullptr && + Registers->registerBank("GPR") == nullptr && + Registers->registerBankMIRName(rcc::test::TestRegisterBank::GPR) == "gpr", + "register bank MIR names must be canonical lowercase names"); +} + +void testTextSchemaQueries(TestContext &Context) { + auto Registers = rcc::test::createTestTargetRegisterInfo(); + if (!Registers) + return; + auto Text = rcc::TargetMIRTextInfo::create(rcc::test::createTestTargetMIRTextInfoDescription(), + *Registers); + Context.expect(Text.has_value(), "TestTarget MIR text schema must be valid"); + if (!Text) + return; + + const auto Preserved = rcc::test::createTestCallPreservedMask(); + const auto NamedMask = Text->registerMask("test-preserved"); + Context.expect(NamedMask && *NamedMask == *Preserved && !Text->registerMask("Test-Preserved") && + Text->registerMaskName(*Preserved) == "test-preserved", + "named register masks must use one canonical lowercase spelling"); + + Context.expect(Text->targetIndex("test-index") == rcc::TargetIndex::fromValue(0) && + Text->targetIndexName(rcc::TargetIndex::fromValue(0)) == "test-index", + "target index zero must remain a valid named target index"); + Context.expect(Text->directOperandFlag("test-direct-three") == 0x3 && + Text->bitmaskOperandFlag("test-bit-four") == 0x4, + "direct and bitmask target flags must retain distinct registries"); + + auto OperandNames = Text->operandFlagNames(0xD); + Context.expect(OperandNames && + *OperandNames == std::vector<std::string_view>( + {"test-direct-one", "test-bit-four", "test-bit-eight"}), + "operand flag decomposition must be canonical and declaration ordered"); + Context.expect(!Text->operandFlagNames(0x10), + "operand flag decomposition must reject unknown bits"); + + auto MMONames = Text->machineMemOperandFlagNames(0x3); + auto VRegNames = Text->virtualRegisterFlagNames(0x3); + Context.expect( + MMONames && *MMONames == std::vector<std::string_view>({"test-mmo-one", "test-mmo-two"}) && + VRegNames && + *VRegNames == std::vector<std::string_view>({"test-vreg-one", "test-vreg-two"}) && + !Text->machineMemOperandFlagNames(0x4) && !Text->virtualRegisterFlagNames(0x4), + "MMO and virtual-register flags must reject every unregistered bit"); + + Context.expect( + Text->intrinsic("llvm.test.seven") == rcc::IntrinsicID::fromValue(7) && + Text->intrinsicName(rcc::IntrinsicID::fromValue(17)) == "llvm.test.seventeen" && + Text->intrinsic("llvm.va_copy") == rcc::intrinsicID(rcc::GenericIntrinsic::VACopy) && + Text->intrinsicName(rcc::intrinsicID(rcc::GenericIntrinsic::VACopy)) == "llvm.va_copy", + "intrinsic names and strong IDs must round-trip"); + + const auto G0 = phys(rcc::test::TestRegister::G0); + Context.expect(Text->dwarfRegister(G0) == rcc::test::TestRegister::G0 && + Text->physicalRegister(rcc::test::TestRegister::G0) == G0, + "CFI register mappings must support both physical and DWARF lookup"); + Context.expect(Text->supportsStackID(rcc::StackID::defaultStack()) && + Text->supportsStackID(rcc::StackID::noAllocStack()) && + !Text->supportsStackID(rcc::StackID::sgprSpillStack()), + "target stack support must be explicit and include fixed baseline defaults"); +} + +void testTextSchemaRejection(TestContext &Context) { + auto Registers = rcc::test::createTestTargetRegisterInfo(); + if (!Registers) + return; + auto Reject = [&Context, &Registers](rcc::TargetMIRTextInfoDesc Description, + std::string_view Message) { + Context.expect(!rcc::TargetMIRTextInfo::create(std::move(Description), *Registers), Message); + }; + + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.RegisterMasks.push_back( + {"TEST-PRESERVED", + std::make_shared<const rcc::RegisterMask>(std::vector<std::uint64_t>{})}); + Reject(std::move(Description), "register mask names must be unique case-insensitively"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.RegisterMasks.push_back({"other-mask", rcc::test::createTestCallPreservedMask()}); + Reject(std::move(Description), "register mask values must have one canonical name"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.RegisterMasks.front().Mask = std::make_shared<const rcc::RegisterMask>( + std::vector<std::uint64_t>{std::uint64_t{1} << 63U}); + Reject(std::move(Description), "named masks must reject unknown physical registers"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.TargetIndices.push_back({"other-index", rcc::TargetIndex::fromValue(0)}); + Reject(std::move(Description), "target index values must be unique"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.DirectOperandFlagMask = 0; + Reject(std::move(Description), "direct flags require a direct flag mask"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.DirectOperandFlags.front().Value = 0x4; + Reject(std::move(Description), "direct flags must remain inside their direct field"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.BitmaskOperandFlags.front().Value = 0xC; + Reject(std::move(Description), "bitmask operand flags must contain one bit"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.MachineMemOperandFlags.front().Value = 0x3; + Reject(std::move(Description), "MMO target flags must contain one bit"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.VirtualRegisterFlags.back().Value = Description.VirtualRegisterFlags.front().Value; + Reject(std::move(Description), "virtual-register target flag values must be unique"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.Intrinsics.front().Name = "test.not-an-intrinsic"; + Reject(std::move(Description), "intrinsic names must use the llvm namespace"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.CFIRegisters[1].DwarfRegister = Description.CFIRegisters[0].DwarfRegister; + Reject(std::move(Description), + "a DWARF register must not have multiple canonical physical registers"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.CFIRegisters.front().IsCanonical = false; + Reject(std::move(Description), + "every referenced DWARF register must have a canonical physical register"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.CFIRegisters.front().Register = phys(99); + Reject(std::move(Description), "CFI mappings must reject unknown physical registers"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.FunctionStateType = rcc::TargetFunctionStateTypeID{}; + Reject(std::move(Description), "target function state schemas require a nonzero TypeID"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.SupportedStackIDs.pop_back(); + Reject(std::move(Description), "targets must support the noalloc stack ID"); + } + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.SupportedStackIDs.push_back(rcc::StackID::defaultStack()); + Reject(std::move(Description), "supported stack IDs must be unique"); + } + + { + auto Description = rcc::test::createTestTargetMIRTextInfoDescription(); + Description.CFIRegisters[1].DwarfRegister = Description.CFIRegisters[0].DwarfRegister; + Description.CFIRegisters[1].IsCanonical = false; + auto Text = rcc::TargetMIRTextInfo::create(std::move(Description), *Registers); + Context.expect(Text && + Text->dwarfRegister(phys(rcc::test::TestRegister::G0Low32)) == + rcc::test::TestRegister::G0 && + Text->physicalRegister(rcc::test::TestRegister::G0) == + phys(rcc::test::TestRegister::G0), + "noncanonical physical aliases must map to one canonical DWARF register"); + } + + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.PhysicalRegisters[1].Name = "G0"; + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "physical register names must be case-insensitively unique"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.RegisterClasses[1].Name = "gpr64"; + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "register class names must be case-insensitively unique"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.RegisterBanks[1].Name = "gpr"; + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "register bank names must be case-insensitively unique"); + } +} + +void testSemanticValidation(TestContext &Context) { + rcc::OperandDescriptor Operand; + Operand.Kinds = {rcc::MachineOperand::Kind::Immediate}; + Operand.AllowedTargetFlags = 0xF; + + rcc::InstructionDescriptor Instruction; + Instruction.InstructionOpcode = rcc::Opcode::fromValue(1); + Instruction.Name = "TEST_LOAD_IMM"; + Instruction.Operands.push_back(Operand); + Instruction.Flags = { + rcc::InstructionFlag::ProducesCode, + rcc::InstructionFlag::MayLoad, + }; + Instruction.RequiresMemoryOperands = true; + + auto Instructions = rcc::InstructionSet::create({std::move(Instruction)}); + if (!Instructions) + return; + auto Target = rcc::test::createTestTargetDescription(std::move(*Instructions)); + auto Function = + rcc::MachineFunction::create("schema_validation", rcc::MachineFunctionStableID::fromValue(1)); + Context.expect(Target.has_value() && Function.has_value(), + "semantic target schema fixture must be constructible"); + if (!Target || !Function) + return; + + auto Reg = (*Function)->registerInfo().createVirtualRegister(); + Context.expect(Reg && (*Function)->registerInfo().setTargetRegisterFlags(*Reg, 0x3).has_value() && + Target->validateRegisterInfo((*Function)->registerInfo()).has_value(), + "registered virtual-register target flags must pass target validation"); + if (Reg) { + Context.expect((*Function)->registerInfo().setTargetRegisterFlags(*Reg, 0x4).has_value() && + !Target->validateRegisterInfo((*Function)->registerInfo()), + "unregistered virtual-register target flags must fail target validation"); + Context.expect( + (*Function)->registerInfo().clearTargetRegisterFlags(*Reg).has_value(), + "semantic validation fixture must restore registered virtual-register target flags"); + } + + const rcc::MachineOperand ValidOperand = rcc::MachineOperand::immediate(7, 0x5); + const rcc::MachineOperand InvalidOperand = rcc::MachineOperand::immediate(7, 0x10); + Context.expect( + Target->validateExplicitOperands(rcc::Opcode::fromValue(1), + std::span<const rcc::MachineOperand>(&ValidOperand, 1), + (*Function)->registerInfo()) + .has_value() && + !Target->validateExplicitOperands( + rcc::Opcode::fromValue(1), std::span<const rcc::MachineOperand>(&InvalidOperand, 1), + (*Function)->registerInfo()), + "machine operands must use only descriptor-allowed serializable target flags"); + + rcc::MachineMemOperandDesc MemoryDescription; + MemoryDescription.Accesses = {rcc::MemoryAccessFlag::Load}; + MemoryDescription.Size = *rcc::MemorySize::fixed(8); + MemoryDescription.Align = *rcc::Alignment::create(8); + MemoryDescription.TargetFlags = 0x3; + auto ValidMemory = rcc::MachineMemOperand::create(MemoryDescription); + MemoryDescription.TargetFlags = 0x4; + auto InvalidMemory = rcc::MachineMemOperand::create(MemoryDescription); + if (ValidMemory && InvalidMemory) { + const std::array<const rcc::MachineMemOperand *, 1> ValidOperands{&*ValidMemory}; + const std::array<const rcc::MachineMemOperand *, 1> InvalidOperands{&*InvalidMemory}; + Context.expect( + Target->validateMemOperands(rcc::Opcode::fromValue(1), ValidOperands).has_value() && + !Target->validateMemOperands(rcc::Opcode::fromValue(1), InvalidOperands), + "machine memory operands must use only registered target flags"); + } + + rcc::MachineCFIInfo CFI; + auto CFIIndex = CFI.addInstruction( + rcc::MachineCFIInstruction::sameValue(rcc::DwarfRegisterNumber::fromValue(99))); + Context.expect(CFIIndex && !Target->validateCFIInfo(CFI), + "CFI instructions must reject unmapped DWARF registers"); + + rcc::MachineFrameInfo Frame; + rcc::StackObjectDesc StackObject; + StackObject.Kind = rcc::StackObjectKind::Target; + StackObject.Size = 8; + StackObject.Align = *rcc::Alignment::create(8); + StackObject.Stack = rcc::StackID::sgprSpillStack(); + auto StackIndex = Frame.createObject(StackObject); + Context.expect(StackIndex && !Target->validateFrameInfo(Frame), + "frame objects must reject stack IDs unsupported by the target"); + + rcc::OperandDescriptor BadOperand; + BadOperand.Kinds = {rcc::MachineOperand::Kind::Immediate}; + BadOperand.AllowedTargetFlags = 0x10; + rcc::InstructionDescriptor BadInstruction; + BadInstruction.InstructionOpcode = rcc::Opcode::fromValue(2); + BadInstruction.Name = "TEST_BAD_FLAGS"; + BadInstruction.Operands.push_back(BadOperand); + BadInstruction.Flags = {rcc::InstructionFlag::ProducesCode}; + auto BadInstructions = rcc::InstructionSet::create({std::move(BadInstruction)}); + Context.expect(BadInstructions && + !rcc::test::createTestTargetDescription(std::move(*BadInstructions)), + "target construction must reject operand flag bits outside its text schema"); + + auto PartialDescription = rcc::test::createTestTargetMIRTextInfoDescription(); + PartialDescription.DirectOperandFlags.erase(PartialDescription.DirectOperandFlags.begin() + 1, + PartialDescription.DirectOperandFlags.end()); + auto Registers = rcc::test::createTestTargetRegisterInfo(); + auto PartialText = + Registers ? rcc::TargetMIRTextInfo::create(std::move(PartialDescription), *Registers) + : rcc::Expected<rcc::TargetMIRTextInfo>(std::unexpected(Registers.error())); + Context.expect(PartialText && !PartialText->validateOperandFlags(0x2), + "unused direct-field encodings must remain invalid"); +} + +} // namespace + +int main() { + TestContext Context; + testTargetRegisterNames(Context); + testTextSchemaQueries(Context); + testTextSchemaRejection(Context); + testSemanticValidation(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/TargetRegisterInfoTest.cpp b/backend/unittests/CodeGen/TargetRegisterInfoTest.cpp new file mode 100644 index 0000000..bb9a0fc --- /dev/null +++ b/backend/unittests/CodeGen/TargetRegisterInfoTest.cpp @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/TargetRegisterInfo.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/LowLevelType.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/TargetDescription.h" + +#include <cstdint> +#include <iostream> +#include <memory> +#include <ranges> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::PhysicalRegister phys(std::uint32_t Number) { + return *rcc::PhysicalRegister::create(Number); +} + +[[nodiscard]] rcc::OperandDescriptor registerOperand(rcc::RegisterOperandRole Role, + rcc::RegisterClassID Class, + rcc::RegisterBankID Bank, + bool AllowSubReg = false) { + rcc::OperandDescriptor Operand; + Operand.Kinds = {rcc::MachineOperand::Kind::Register}; + Operand.RegisterRole = Role; + Operand.RegClass = Class; + Operand.RegBank = Bank; + Operand.AllowSubReg = AllowSubReg; + return Operand; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> +createTarget(rcc::TargetRegisterInfoDesc RegisterDescription, + rcc::InstructionDescriptor Instruction) { + auto Registers = rcc::TargetRegisterInfo::create(std::move(RegisterDescription)); + if (!Registers) + return std::unexpected(Registers.error()); + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Instruction)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::TargetDescription::create(std::move(*Instructions), std::move(*Registers)); +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> +createOperandTarget(rcc::OperandDescriptor Operand, + rcc::TargetRegisterInfoDesc RegisterDescription = + rcc::test::createTestTargetRegisterInfoDescription()) { + rcc::InstructionDescriptor Instruction; + Instruction.InstructionOpcode = rcc::Opcode::fromValue(1); + Instruction.Name = "TEST_CONSTRAINED"; + Instruction.Operands.push_back(Operand); + if (Instruction.Operands.front().RegisterRole == rcc::RegisterOperandRole::Def) { + Instruction.NumExplicitDefs = 1; + } + Instruction.Flags = {rcc::InstructionFlag::ProducesCode}; + + return createTarget(std::move(RegisterDescription), std::move(Instruction)); +} + +void testRegisterTopology(TestContext &Context) { + auto Target = rcc::test::createTestTargetRegisterInfo(); + Context.expect(Target.has_value(), "TestTarget register description must be valid"); + if (!Target) + return; + + const auto G0 = phys(rcc::test::TestRegister::G0); + const auto G0Low32 = phys(rcc::test::TestRegister::G0Low32); + const auto G0High32 = phys(rcc::test::TestRegister::G0High32); + const auto G0Low16 = phys(rcc::test::TestRegister::G0Low16); + const auto G1 = phys(rcc::test::TestRegister::G1); + const auto F0 = phys(rcc::test::TestRegister::F0); + + Context.expect(Target->registerUnits().size() == 11 && Target->physicalRegisters().size() == 11 && + Target->registerClasses().size() == 8 && Target->registerBanks().size() == 2, + "TestTarget must expose its complete static register tables"); + const rcc::PhysicalRegisterDesc *Named = Target->physicalRegister("g0"); + Context.expect(Named != nullptr && Named->Reg == G0 && Named->Encoding == 0, + "physical register name lookup must preserve identity and encoding"); + + Context.expect(Target->regsOverlap(G0, G0Low32) && Target->regsOverlap(G0, G0High32) && + Target->regsOverlap(G0Low32, G0Low16) && + !Target->regsOverlap(G0Low32, G0High32) && !Target->regsOverlap(G0, G1), + "physical overlap must be derived from shared register units"); + Context.expect(std::ranges::find(Target->aliases(G0), G0Low32) != Target->aliases(G0).end() && + std::ranges::find(Target->aliases(G0Low32), G0High32) == + Target->aliases(G0Low32).end(), + "alias queries must include only overlapping physical registers"); + + Context.expect(Target->subRegister(G0, rcc::test::TestSubReg::Low32) == G0Low32 && + Target->subRegister(G0Low32, rcc::test::TestSubReg::Low16) == G0Low16 && + Target->isSubRegister(G0, G0Low16) && + std::ranges::find(Target->superRegisters(G0Low16), G0) != + Target->superRegisters(G0Low16).end() && + Target->composeSubRegIndices(rcc::test::TestSubReg::Low32, + rcc::test::TestSubReg::Low16) == + rcc::test::TestSubReg::Low16, + "subregister queries must preserve direct, transitive, and composed mappings"); + Context.expect(Target->isValidLaneMask(G0, rcc::LaneMask::fromRaw(0x3)) && + Target->isValidLaneMask(G0, rcc::LaneMask::all()) && + !Target->isValidLaneMask(G0, rcc::LaneMask::fromRaw(0x10)), + "lane masks must be checked against target register coverage"); + + Context.expect( + Target->isSubclass(rcc::test::TestRegisterClass::GPR64Alloc, + rcc::test::TestRegisterClass::GPR64) && + !Target->isSubclass(rcc::test::TestRegisterClass::GPR64, + rcc::test::TestRegisterClass::GPR64Alloc) && + Target->registerClassesOverlap(rcc::test::TestRegisterClass::GPR64, + rcc::test::TestRegisterClass::GPR64Alloc) && + Target->subRegClass(rcc::test::TestRegisterClass::GPR64, rcc::test::TestSubReg::Low32) == + rcc::test::TestRegisterClass::GPR32Low, + "register class queries must preserve hierarchy and subregister projection"); + Context.expect(Target->bankContainsRegister(rcc::test::TestRegisterBank::GPR, G0) && + !Target->bankContainsRegister(rcc::test::TestRegisterBank::GPR, F0) && + !Target->registerBanksOverlap(rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterBank::FPR), + "register bank coverage must remain distinct from register classes"); + + const auto CallMask = rcc::test::createTestCallPreservedMask(); + Context.expect(CallMask->preserves(G1) && + CallMask->preserves(phys(rcc::test::TestRegister::StackPointer)) && + CallMask->preserves(phys(rcc::test::TestRegister::F1)) && + CallMask->clobbers(G0) && Target->validateRegisterMask(*CallMask).has_value(), + "call register mask must use preserved-bit semantics"); + Context.expect(Target->validateRegisterMask(rcc::RegisterMask({})).has_value(), + "an empty register mask must represent clobber-all"); + Context.expect(!Target->validateRegisterMask(rcc::RegisterMask({std::uint64_t{1} << 63U})) && + !Target->validateRegisterMask(rcc::RegisterMask({1})), + "register masks must reject preserved bits for unknown registers and noreg"); +} + +void testDescriptionRejection(TestContext &Context) { + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.RegisterUnits.back().ID = Description.RegisterUnits.front().ID; + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "duplicate register unit IDs must be rejected"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.PhysicalRegisters.front().AlternativeNames = {"later-name"}; + Description.PhysicalRegisters[1].Name = "later-name"; + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "canonical register names must not collide with earlier ABI aliases"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.PhysicalRegisters.front().IsReserved = true; + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "statically reserved registers must not appear in allocation orders"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.PhysicalSubRegisters.front().SubReg = phys(rcc::test::TestRegister::G1Low32); + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "subregister mappings with unrelated units must be rejected"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.PhysicalRegisters[2].Units.front().Lanes = rcc::LaneMask::fromRaw(0x3); + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "register-unit views with different lane counts must be rejected"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.RegisterClasses.front().SuperClasses.push_back( + rcc::test::TestRegisterClass::GPR64Alloc); + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "cyclic register class hierarchies must be rejected"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.RegisterBanks.front().MaximumSizeInBits = 32; + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "register banks smaller than a covered class must be rejected"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.SubRegClassMappings.pop_back(); + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "supported subregisters without class projections must be rejected"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.RegisterClasses.front().Lanes = rcc::LaneMask::fromRaw(0x10); + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "register class lanes outside a member register must be rejected"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.RegisterBanks.front().RegisterClasses.erase( + Description.RegisterBanks.front().RegisterClasses.begin()); + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "a class default bank must explicitly cover that class"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.RegisterUnits.push_back({rcc::RegisterUnitID::fromValue(99), "unused"}); + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "unreferenced register units must be rejected"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.SubRegCompositions.push_back( + {rcc::test::TestSubReg::Low16, rcc::test::TestSubReg::Low16, rcc::test::TestSubReg::Low32}); + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "subregister composition bit ranges must be internally consistent"); + } + { + auto Description = rcc::test::createTestTargetRegisterInfoDescription(); + Description.RegisterClasses[3].CoveredBySubRegs = true; + Context.expect(!rcc::TargetRegisterInfo::create(std::move(Description)), + "subregister-covered classes must cover all class lanes"); + } +} + +void testTargetDescriptionConstraints(TestContext &Context) { + auto SubclassTarget = createOperandTarget(registerOperand(rcc::RegisterOperandRole::Def, + rcc::test::TestRegisterClass::GPR64, + rcc::test::TestRegisterBank::GPR)); + auto Function = + rcc::MachineFunction::create("constraints", rcc::MachineFunctionStableID::fromValue(1)); + auto Type = rcc::LowLevelType::makeCategoryNeutralScalar(64); + if (!SubclassTarget || !Function || !Type) + return; + + auto Reg = (*Function)->registerInfo().createVirtualRegister(*Type); + if (!Reg) + return; + Context.expect((*Function) + ->registerInfo() + .setRegisterBank(*Reg, rcc::test::TestRegisterBank::GPR) + .has_value() && + (*Function) + ->registerInfo() + .setRegisterClass(*Reg, rcc::test::TestRegisterClass::GPR64Alloc) + .has_value(), + "constrained test virtual register must be classified"); + + auto Operand = rcc::MachineOperand::registerOperand(*Reg, {rcc::RegisterOperandFlag::Define}); + Context.expect( + Operand && SubclassTarget + ->validateExplicitOperands(rcc::Opcode::fromValue(1), + std::span<const rcc::MachineOperand>(&*Operand, 1), + (*Function)->registerInfo()) + .has_value(), + "an actual register subclass must satisfy a superclass operand constraint"); + + auto SubRegTarget = createOperandTarget(registerOperand(rcc::RegisterOperandRole::Def, + rcc::test::TestRegisterClass::GPR32, + rcc::test::TestRegisterBank::GPR, true)); + auto BaseClassResult = + (*Function)->registerInfo().setRegisterClass(*Reg, rcc::test::TestRegisterClass::GPR64); + auto SubRegOperand = rcc::MachineOperand::registerOperand( + *Reg, {rcc::RegisterOperandFlag::Define}, rcc::test::TestSubReg::Low32); + Context.expect( + SubRegTarget && BaseClassResult && SubRegOperand && + SubRegTarget + ->validateExplicitOperands(rcc::Opcode::fromValue(1), + std::span<const rcc::MachineOperand>(&*SubRegOperand, 1), + (*Function)->registerInfo()) + .has_value(), + "subregister class projection must satisfy a compatible superclass constraint"); + + auto PhysicalSubReg = rcc::MachineOperand::registerOperand( + rcc::Register::fromPhysical(phys(rcc::test::TestRegister::G0)), + {rcc::RegisterOperandFlag::Define}, rcc::test::TestSubReg::Low32); + Context.expect(PhysicalSubReg && !SubRegTarget->validateExplicitOperands( + rcc::Opcode::fromValue(1), + std::span<const rcc::MachineOperand>(&*PhysicalSubReg, 1), + (*Function)->registerInfo()), + "physical register operands must reject nonzero subregister indices"); + + auto WideType = rcc::LowLevelType::makeCategoryNeutralScalar(128); + if (!WideType) + return; + auto WideReg = (*Function)->registerInfo().createVirtualRegister(*WideType); + Context.expect(WideReg && + (*Function) + ->registerInfo() + .setRegisterBank(*WideReg, rcc::test::TestRegisterBank::GPR) + .has_value() && + !SubclassTarget->validateRegisterInfo((*Function)->registerInfo()), + "virtual register type must fit its selected bank"); + + auto UnknownClassOperand = + registerOperand(rcc::RegisterOperandRole::Def, rcc::RegisterClassID::fromValue(99), + rcc::test::TestRegisterBank::GPR); + Context.expect(!createOperandTarget(UnknownClassOperand), + "target creation must reject unknown operand register classes"); + auto MismatchedBankOperand = + registerOperand(rcc::RegisterOperandRole::Def, rcc::test::TestRegisterClass::GPR64, + rcc::test::TestRegisterBank::FPR); + Context.expect(!createOperandTarget(MismatchedBankOperand), + "target creation must reject incompatible class and bank constraints"); + + auto ConstantDescription = rcc::test::createTestTargetRegisterInfoDescription(); + ConstantDescription.PhysicalRegisters.front().IsConstant = true; + auto ConstantTarget = createOperandTarget(registerOperand(rcc::RegisterOperandRole::Def, + rcc::test::TestRegisterClass::GPR64, + rcc::test::TestRegisterBank::GPR), + std::move(ConstantDescription)); + auto HintFunction = + rcc::MachineFunction::create("constant_hint", rcc::MachineFunctionStableID::fromValue(2)); + if (ConstantTarget && HintFunction) { + auto HintReg = (*HintFunction)->registerInfo().createVirtualRegister(*Type); + Context.expect(HintReg && + (*HintFunction) + ->registerInfo() + .setRegisterClass(*HintReg, rcc::test::TestRegisterClass::GPR64) + .has_value() && + (*HintFunction) + ->registerInfo() + .addAllocationHint(*HintReg, phys(rcc::test::TestRegister::G0)) + .has_value() && + !ConstantTarget->validateRegisterInfo((*HintFunction)->registerInfo()), + "constant physical registers must be rejected as allocation hints"); + } + + auto RenamableDescription = rcc::test::createTestTargetRegisterInfoDescription(); + RenamableDescription.PhysicalRegisters.front().IsArtificial = true; + rcc::InstructionDescriptor RenamableImplicit; + RenamableImplicit.InstructionOpcode = rcc::Opcode::fromValue(2); + RenamableImplicit.Name = "TEST_BAD_IMPLICIT"; + RenamableImplicit.ImplicitDefs.push_back({phys(rcc::test::TestRegister::G0), false, true}); + Context.expect(!createTarget(std::move(RenamableDescription), std::move(RenamableImplicit)), + "target creation must reject renamable artificial implicit registers"); + + rcc::InstructionDescriptor ConflictingImplicit; + ConflictingImplicit.InstructionOpcode = rcc::Opcode::fromValue(3); + ConflictingImplicit.Name = "TEST_BAD_EARLY_IMPLICIT"; + ConflictingImplicit.ImplicitDefs.push_back({phys(rcc::test::TestRegister::G0), true, false}); + ConflictingImplicit.ImplicitUses.push_back( + {phys(rcc::test::TestRegister::G0Low32), false, false}); + Context.expect(!createTarget(rcc::test::createTestTargetRegisterInfoDescription(), + std::move(ConflictingImplicit)), + "target creation must reject overlapping early-clobber implicit operands"); +} + +} // namespace + +int main() { + TestContext Context; + testRegisterTopology(Context); + testDescriptionRejection(Context); + testTargetDescriptionConstraints(Context); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/TwoAddressInstructionPassTest.cpp b/backend/unittests/CodeGen/TwoAddressInstructionPassTest.cpp new file mode 100644 index 0000000..d23a2eb --- /dev/null +++ b/backend/unittests/CodeGen/TwoAddressInstructionPassTest.cpp @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/TwoAddressInstructionPass.h" + +#include "SSAExitTestSupport.h" +#include "rcc/CodeGen/MachinePassManager.h" +#include "rcc/CodeGen/MachineVerifier.h" + +#include <array> +#include <memory> +#include <optional> + +namespace { + +using namespace rcc::test::ssa; + +void testTwoAddressAndSubregisterPseudos(TestContext &Context, const rcc::TargetDescription &Target, + const TestInstrInfo &Info) { + auto Fixture = createFunction(Context, "two_address", 5, 1); + if (!Fixture.Function || Fixture.Blocks.empty()) + return; + auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64); + auto I32 = rcc::LowLevelType::makeCategoryNeutralScalar(32); + if (!I64 || !I32) + return; + auto &Function = *Fixture.Function; + rcc::MachineBasicBlock &Block = *Fixture.Blocks[0]; + auto GPR64 = [&](std::uint8_t Flags = 0) { + return createVReg(Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64, Flags); + }; + auto GPR32 = [&]() { + return createVReg(Function, *I32, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR32); + }; + + auto Satisfied = GPR64(); + auto UndefSource = GPR64(); + auto UndefDestination = GPR64(); + auto CommuteSource = GPR64(); + auto CommuteDestination = GPR64(); + auto ConvertSource = GPR64(); + auto ConvertDestination = GPR64(); + auto FallbackSource = GPR64(); + auto FallbackDestination = GPR64(); + auto SequenceDestination = GPR64(); + auto SequenceLow = GPR32(); + auto SequenceHigh = GPR32(); + auto InsertDestination = GPR64(); + auto InsertBase = GPR64(); + auto Inserted = GPR32(); + const bool Complete = Satisfied && UndefSource && UndefDestination && CommuteSource && + CommuteDestination && ConvertSource && ConvertDestination && + FallbackSource && FallbackDestination && SequenceDestination && + SequenceLow && SequenceHigh && InsertDestination && InsertBase && Inserted; + Context.expect(Complete, "two-address test registers must be created"); + if (!Complete) + return; + + for (rcc::Register Source : {*CommuteSource, *ConvertSource, *FallbackSource, *SequenceLow, + *SequenceHigh, *InsertBase, *Inserted}) { + const std::array Def{Source}; + Context.expect(buildRegisterInstruction(Function, Target, Block, testOpcode(DefOpcode), Def, {}, + {rcc::RegisterOperandFlag::Dead}) + .has_value(), + "two-address source definition must be built"); + } + const std::array UndefDef{*UndefSource}; + Context.expect(buildRegisterInstruction(Function, Target, Block, + rcc::opcode(rcc::GenericOpcode::IMPLICIT_DEF), UndefDef, + {}) + .has_value(), + "two-address undef source must be defined by IMPLICIT_DEF"); + + auto BuildTied = [&](rcc::Opcode Opcode, rcc::Register Def, rcc::Register Use, + std::optional<rcc::Register> Other = std::nullopt) { + rcc::MachineIRBuilder Builder(Function, Target); + auto Point = Builder.setInsertPoint(Block); + if (!Point) + return rcc::Expected<rcc::MachineInstr *>{std::unexpected(Point.error())}; + auto Pending = Builder.buildInstr(Opcode); + if (!Pending) + return rcc::Expected<rcc::MachineInstr *>{std::unexpected(Pending.error())}; + auto AddedDef = Pending->addDef(Def); + if (!AddedDef) + return rcc::Expected<rcc::MachineInstr *>{std::unexpected(AddedDef.error())}; + auto AddedUse = Pending->addUse(Use, {rcc::RegisterOperandFlag::Kill}); + if (!AddedUse) + return rcc::Expected<rcc::MachineInstr *>{std::unexpected(AddedUse.error())}; + if (Other) { + auto AddedOther = Pending->addUse(*Other); + if (!AddedOther) + return rcc::Expected<rcc::MachineInstr *>{std::unexpected(AddedOther.error())}; + } + return Pending->build(); + }; + + auto AlreadyTied = BuildTied(testOpcode(TiedOpcode), *Satisfied, *Satisfied); + auto UndefTied = BuildTied(testOpcode(TiedOpcode), *UndefDestination, *UndefSource); + auto Commutable = BuildTied(testOpcode(CommutableTiedOpcode), *CommuteDestination, *CommuteSource, + *CommuteDestination); + auto Convertible = + BuildTied(testOpcode(ConvertibleTiedOpcode), *ConvertDestination, *ConvertSource); + auto Fallback = BuildTied(testOpcode(TiedOpcode), *FallbackDestination, *FallbackSource); + Context.expect(AlreadyTied && UndefTied && Commutable && Convertible && Fallback, + "two-address instruction fixtures must be built"); + if (!AlreadyTied || !UndefTied || !Commutable || !Convertible || !Fallback) + return; + + rcc::MachineIRBuilder Builder(Function, Target); + if (!Builder.setInsertPoint(Block)) + return; + auto Sequence = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::REG_SEQUENCE)); + Context.expect( + Sequence && Sequence->addDef(*SequenceDestination) && Sequence->addUse(*SequenceLow) && + Sequence->addImmediate(rcc::test::TestSubReg::Low32.value()) && + Sequence->addUse(*SequenceHigh) && + Sequence->addImmediate(rcc::test::TestSubReg::High32.value()) && Sequence->build(), + "REG_SEQUENCE fixture must be built"); + auto Insert = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::INSERT_SUBREG)); + Context.expect(Insert && Insert->addDef(*InsertDestination) && Insert->addUse(*InsertBase) && + Insert->addUse(*Inserted) && + Insert->addImmediate(rcc::test::TestSubReg::Low32.value()) && Insert->build(), + "INSERT_SUBREG fixture must be built"); + auto Return = Builder.buildInstr(testOpcode(ReturnOpcode)); + Context.expect(Return && Return->build(), "two-address return must be built"); + + auto InitialLiveness = setSelectedSSAProperties(Function, Target); + Context.expect(InitialLiveness.has_value(), "two-address liveness rebuild must succeed"); + if (!InitialLiveness) + return; + Function.properties().set(rcc::MachineFunctionProperty::NoPHIs); + rcc::MachineFunctionPassManager Pipeline; + Context.expect( + Pipeline.addPass(std::make_unique<rcc::TwoAddressInstructionPass>(Info)).has_value(), + "two-address pass must be registered"); + rcc::MachineFunctionAnalysisManager Analyses; + auto Run = Pipeline.run(Function, Target, Analyses); + Context.expect(Run.has_value(), "two-address rewrite must succeed"); + if (!Run) + return; + + Context.expect(Function.properties().has(rcc::MachineFunctionProperty::TiedOpsRewritten) && + !Function.properties().has(rcc::MachineFunctionProperty::IsSSA), + "two-address rewrite must publish TiedOpsRewritten and clear IsSSA"); + Context.expect( + instructionsWithOpcode(Function, rcc::opcode(rcc::GenericOpcode::REG_SEQUENCE)).empty() && + instructionsWithOpcode(Function, rcc::opcode(rcc::GenericOpcode::INSERT_SUBREG)).empty(), + "TwoAddressInstructionPass must consume all Stage 10 subregister pseudos"); + + Context.expect((*AlreadyTied)->operandAt(1)->registerValue() == *Satisfied, + "an already-satisfied tie must remain unchanged"); + Context.expect( + (*UndefTied)->operandAt(1)->registerValue() == *UndefDestination && + (*UndefTied)->operandAt(1)->registerFlags().has(rcc::RegisterOperandFlag::Undef), + "undef tied uses must be rewritten without a COPY"); + Context.expect((*Commutable)->operandAt(1)->registerValue() == *CommuteDestination && + (*Commutable)->operandAt(2)->registerValue() == *CommuteSource, + "commutation must satisfy a tie before COPY fallback"); + Context.expect((*Convertible)->opcode() == testOpcode(ThreeAddressOpcode) && + !(*Convertible)->operandAt(0)->tiedOperandIndex() && + !(*Convertible)->operandAt(1)->tiedOperandIndex(), + "target three-address conversion must run before COPY fallback"); + Context.expect((*Fallback)->previous() != nullptr && + (*Fallback)->previous()->opcode() == rcc::opcode(rcc::GenericOpcode::COPY) && + (*Fallback)->previous()->operandAt(0)->registerValue() == + *FallbackDestination && + (*Fallback)->previous()->operandAt(1)->registerValue() == *FallbackSource && + (*Fallback)->operandAt(1)->registerValue() == *FallbackDestination, + "unsatisfied ties must receive a COPY immediately before the instruction"); + + const auto Copies = instructionsWithOpcode(Function, rcc::opcode(rcc::GenericOpcode::COPY)); + const auto ImplicitDefs = + instructionsWithOpcode(Function, rcc::opcode(rcc::GenericOpcode::IMPLICIT_DEF)); + Context.expect(Copies.size() >= 5 && ImplicitDefs.size() >= 2, + "subregister pseudo expansion must materialize base and lane definitions"); + Context.expect( + std::ranges::any_of(Copies, + [InsertDestination](const rcc::MachineInstr *Copy) { + return Copy->operandAt(0)->registerValue() == *InsertDestination && + Copy->operandAt(0)->subReg() == rcc::test::TestSubReg::Low32; + }), + "INSERT_SUBREG must copy its inserted value into the requested destination lane"); + const bool HasKill = std::ranges::any_of(Function.allInstructions(), [](const auto *MI) { + return MI->parent() != nullptr && + std::ranges::any_of(MI->operands(), [](const rcc::MachineOperand &Operand) { + return Operand.registerFlags().has(rcc::RegisterOperandFlag::Kill); + }); + }); + const bool HasDead = std::ranges::any_of(Function.allInstructions(), [](const auto *MI) { + return MI->parent() != nullptr && + std::ranges::any_of(MI->operands(), [](const rcc::MachineOperand &Operand) { + return Operand.registerFlags().has(rcc::RegisterOperandFlag::Dead); + }); + }); + Context.expect(HasKill && HasDead, + "two-address rewrite must rebuild precise kill and dead flags"); + Context.expect(rcc::MachineVerifier::verify(Function, Target).has_value(), + "two-address and subregister expansion result must verify"); + + auto AtomicFixture = createFunction(Context, "two_address_atomic", 10, 1); + if (!AtomicFixture.Function || AtomicFixture.Blocks.empty()) + return; + auto AtomicSource = createVReg(*AtomicFixture.Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + auto AtomicDestination = + createVReg(*AtomicFixture.Function, *I64, rcc::test::TestRegisterBank::GPR, + rcc::test::TestRegisterClass::GPR64); + if (!AtomicSource || !AtomicDestination) + return; + const std::array AtomicSourceDef{*AtomicSource}; + Context.expect(buildRegisterInstruction(*AtomicFixture.Function, Target, *AtomicFixture.Blocks[0], + testOpcode(DefOpcode), AtomicSourceDef, {}) + .has_value(), + "two-address atomicity source definition must build"); + rcc::MachineIRBuilder AtomicBuilder(*AtomicFixture.Function, Target); + if (!AtomicBuilder.setInsertPoint(*AtomicFixture.Blocks[0])) + return; + auto AtomicPending = AtomicBuilder.buildInstr(testOpcode(TiedOpcode)); + auto AtomicInstruction = + AtomicPending && AtomicPending->addDef(*AtomicDestination) && + AtomicPending->addUse(*AtomicSource, {rcc::RegisterOperandFlag::Kill}) + ? AtomicPending->build() + : rcc::Expected<rcc::MachineInstr *>{rcc::makeUnexpected( + rcc::ErrorCode::InternalError, "failed to build atomicity fixture")}; + Context.expect(AtomicInstruction.has_value(), + "two-address atomicity tied instruction must build"); + if (!AtomicInstruction) + return; + auto AtomicLiveness = setSelectedSSAProperties(*AtomicFixture.Function, Target); + Context.expect(AtomicLiveness.has_value(), "two-address atomicity liveness rebuild must succeed"); + if (!AtomicLiveness) + return; + AtomicFixture.Function->properties().set(rcc::MachineFunctionProperty::NoPHIs); + const std::size_t AtomicInstructionsBefore = AtomicFixture.Function->allInstructions().size(); + const std::size_t AtomicRegistersBefore = + AtomicFixture.Function->registerInfo().virtualRegisters().size(); + const std::uint64_t AtomicPropertiesBefore = AtomicFixture.Function->properties().rawValue(); + + TestInstrInfo RejectingInfo(true); + rcc::MachineFunctionAnalysisManager AtomicAnalyses; + rcc::TwoAddressInstructionPass FailingPass(RejectingInfo); + auto Failed = FailingPass.run(*AtomicFixture.Function, Target, AtomicAnalyses); + Context.expect(!Failed && Failed.error().code() == rcc::ErrorCode::Unsupported, + "COPY rejection must produce a structured two-address failure"); + Context.expect( + AtomicFixture.Function->allInstructions().size() == AtomicInstructionsBefore && + AtomicFixture.Function->registerInfo().virtualRegisters().size() == + AtomicRegistersBefore && + AtomicFixture.Function->properties().rawValue() == AtomicPropertiesBefore && + (*AtomicInstruction)->operandAt(0)->registerValue() == *AtomicDestination && + (*AtomicInstruction)->operandAt(1)->registerValue() == *AtomicSource && + (*AtomicInstruction)->operandAt(1)->registerFlags().has(rcc::RegisterOperandFlag::Kill), + "failed two-address transaction preflight must leave the original function unchanged"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "two-address TestTarget must be created"); + if (!Target) + return Context.result(); + TestInstrInfo Info; + testTwoAddressAndSubregisterPseudos(Context, *Target, Info); + return Context.result(); +} diff --git a/backend/unittests/CodeGen/VirtRegMapTest.cpp b/backend/unittests/CodeGen/VirtRegMapTest.cpp new file mode 100644 index 0000000..a3bdfc1 --- /dev/null +++ b/backend/unittests/CodeGen/VirtRegMapTest.cpp @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "CodeGen/RegisterAllocationTestSupport.h" +#include "rcc/CodeGen/LiveRangeEdit.h" + +#include <iostream> + +namespace { + +using namespace rcc; +using namespace rcc::test; +using namespace rcc::test::ra; + +void testSplitMetadata(TestContext &Context, const TargetDescription &Target) { + auto Fixture = createFunction("virt-map", 10); + if (!Fixture) + return; + auto Parent = createVReg(*Fixture->Function); + if (!Parent) + return; + VirtRegMap Mapping(*Fixture->Function); + LiveRangeEdit Edit(*Fixture->Function, Target, Mapping); + Context.expect(Mapping.add(*Parent).has_value(), "VirtRegMap must accept a known vreg"); + auto Slot = Edit.getOrCreateSpillSlot(*Parent); + auto Child = Edit.createFrom(*Parent, false); + Context.expect( + Slot && Child && Mapping.verify().has_value() && Mapping.stackSlot(*Child) == Slot && + Mapping.entry(*Child)->SplitParent == *Parent && Mapping.entry(*Child)->Origin == *Parent, + "LiveRangeEdit must preserve split origin and spill metadata"); + if (!Child) + return; + const PhysicalRegister G0 = physical(TestRegister::G0); + Context.expect(Mapping.assign(*Child, G0).has_value() && Mapping.physicalRegister(*Child) == G0 && + Mapping.entry(*Child)->State == VirtRegAllocationState::Assigned, + "VirtRegMap must transition a split child to an assigned state"); +} + +void testRewriteAndAtomicValidation(TestContext &Context, const TargetDescription &Target) { + auto Fixture = createFunction("virt-rewrite", 11); + if (!Fixture) + return; + auto Value = createVReg(*Fixture->Function); + if (!Value || + !buildDef(*Fixture->Function, Target, *Fixture->Block, *Value, 1, TestSubReg::Low32) || + !buildUse(*Fixture->Function, Target, *Fixture->Block, *Value, TestSubReg::Low32) || + !prepareForAllocation(*Fixture->Function, Target)) + return; + VirtRegMap Mapping(*Fixture->Function); + const PhysicalRegister G0 = physical(TestRegister::G0); + Context.expect(Mapping.add(*Value).has_value() && Mapping.assign(*Value, G0).has_value(), + "VirtRegMap must accept a physical assignment before rewrite"); + auto Rewritten = rewriteVirtualRegisters(*Fixture->Function, Target, Mapping); + const MachineOperand *Def = Fixture->Block->firstInstruction()->operandAt(0); + Context.expect( + Rewritten && *Rewritten && Def != nullptr && Def->registerValue() && + Def->registerValue() == Register::fromPhysical(physical(TestRegister::G0Low32)) && + Def->subReg().value() == 0 && + std::ranges::none_of(Fixture->Function->registerInfo().virtualRegisters(), + [](const VirtualRegisterInfo &Info) { return !Info.IsTombstone; }), + "VirtualRegisterRewrite must project and clear subregister operands"); + + auto InvalidFixture = createFunction("virt-rewrite-invalid", 12); + if (!InvalidFixture) + return; + auto Invalid = createVReg(*InvalidFixture->Function); + if (!Invalid || + !buildDef(*InvalidFixture->Function, Target, *InvalidFixture->Block, *Invalid, 2) || + !buildUse(*InvalidFixture->Function, Target, *InvalidFixture->Block, *Invalid) || + !prepareForAllocation(*InvalidFixture->Function, Target)) + return; + VirtRegMap InvalidMapping(*InvalidFixture->Function); + const PhysicalRegister F0 = physical(TestRegister::F0); + if (!InvalidMapping.add(*Invalid) || !InvalidMapping.assign(*Invalid, F0)) + return; + auto Failed = rewriteVirtualRegisters(*InvalidFixture->Function, Target, InvalidMapping); + Context.expect(!Failed && Failed.error().code() == ErrorCode::RegisterAllocationFailed, + "VirtualRegisterRewrite must reject class-incompatible mappings atomically"); +} + +} // namespace + +int main() { + TestContext Context; + auto Target = createTarget(); + Context.expect(Target.has_value(), "VirtRegMap test target must be constructible"); + if (!Target) + return Context.result(); + testSplitMetadata(Context, *Target); + testRewriteAndAtomicValidation(Context, *Target); + return Context.result(); +} diff --git a/backend/unittests/MC/CMakeLists.txt b/backend/unittests/MC/CMakeLists.txt new file mode 100644 index 0000000..5dfb6d0 --- /dev/null +++ b/backend/unittests/MC/CMakeLists.txt @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +add_executable(RCCBackendMCTests MCTest.cpp) +rcc_backend_configure_target(RCCBackendMCTests) +target_link_libraries(RCCBackendMCTests PRIVATE RCCBackend::MC) +add_test(NAME RCCBackend.MC.Core COMMAND RCCBackendMCTests) + +add_executable(RCCBackendMCObjectTests MCObjectTest.cpp) +rcc_backend_configure_target(RCCBackendMCObjectTests) +target_link_libraries(RCCBackendMCObjectTests PRIVATE RCCBackend::MC) +add_test(NAME RCCBackend.MC.Object COMMAND RCCBackendMCObjectTests) + +rcc_backend_register_format_files( + "${CMAKE_CURRENT_SOURCE_DIR}/MCTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MCObjectTest.cpp" +) diff --git a/backend/unittests/MC/MCObjectTest.cpp b/backend/unittests/MC/MCObjectTest.cpp new file mode 100644 index 0000000..f221e49 --- /dev/null +++ b/backend/unittests/MC/MCObjectTest.cpp @@ -0,0 +1,500 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/MC/ELFObjectWriter.h" +#include "rcc/MC/MCAsmBackend.h" +#include "rcc/MC/MCAssembler.h" +#include "rcc/MC/MCCodeEmitter.h" +#include "rcc/MC/MCContext.h" +#include "rcc/MC/MCExpr.h" +#include "rcc/MC/MCFixup.h" +#include "rcc/MC/MCInst.h" + +#include <algorithm> +#include <array> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <limits> +#include <span> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++Failures; + } + + [[nodiscard]] int result() const noexcept { + return Failures == 0 ? 0 : 1; + } + +private: + int Failures = 0; +}; + +class TestCodeEmitter final : public rcc::mc::MCCodeEmitter { +public: + [[nodiscard]] rcc::Expected<rcc::mc::MCEncodedInstruction> + encodeInstruction(const rcc::mc::MCInst &Instruction) const override { + if (FailNextEncoding) { + FailNextEncoding = false; + return rcc::makeUnexpected(rcc::ErrorCode::InstructionEncodingFailed, + "intentional test encoding failure"); + } + if (Instruction.opcode().value() != 1 || !Instruction.operands().empty()) { + return rcc::makeUnexpected(rcc::ErrorCode::InstructionEncodingFailed, + "unknown test instruction"); + } + return rcc::mc::MCEncodedInstruction{.Bytes = {0x11, 0x22, 0x33, 0x44}, .Fixups = {}}; + } + + mutable bool FailNextEncoding = false; +}; + +class TestAsmBackend final : public rcc::mc::MCAsmBackend { +public: + [[nodiscard]] rcc::Expected<rcc::mc::MCFixupKindInfo> + fixupKindInfo(rcc::mc::MCFixupKind Kind) const override { + switch (Kind) { + case rcc::mc::fixup_kind::Data1: + return rcc::mc::MCFixupKindInfo{.StorageSize = 1, .PCRelative = false}; + case rcc::mc::fixup_kind::Data2: + return rcc::mc::MCFixupKindInfo{.StorageSize = 2, .PCRelative = false}; + case rcc::mc::fixup_kind::Data4: + return rcc::mc::MCFixupKindInfo{.StorageSize = 4, .PCRelative = false}; + case rcc::mc::fixup_kind::Data8: + return rcc::mc::MCFixupKindInfo{.StorageSize = 8, .PCRelative = false}; + case rcc::mc::fixup_kind::PCRelative4: + return rcc::mc::MCFixupKindInfo{.StorageSize = 4, .PCRelative = true}; + default: + return rcc::makeUnexpected(rcc::ErrorCode::InvalidArgument, "unknown test fixup kind"); + } + } + + [[nodiscard]] rcc::Expected<void> applyFixup(rcc::mc::MCFixupKind Kind, + std::span<std::uint8_t> Data, std::uint64_t Offset, + std::int64_t Value) const override { + auto Info = fixupKindInfo(Kind); + if (!Info) + return std::unexpected(Info.error()); + if (Offset > Data.size() || + Info->StorageSize > Data.size() - static_cast<std::size_t>(Offset)) { + return rcc::makeUnexpected(rcc::ErrorCode::FixupApplicationFailed, + "test fixup exceeds its section"); + } + const std::uint64_t Bits = static_cast<std::uint64_t>(Value); + for (std::uint8_t Index = 0; Index < Info->StorageSize; ++Index) + Data[static_cast<std::size_t>(Offset) + Index] = + static_cast<std::uint8_t>(Bits >> (Index * 8U)); + return {}; + } + + [[nodiscard]] rcc::Expected<std::uint32_t> + relocationType(const rcc::mc::MCFixup &, const rcc::mc::MCValue &Value) const override { + if (!Value.AddSymbol || Value.SubtractSymbol) + return rcc::makeUnexpected(rcc::ErrorCode::RelocationFailed, + "test relocation requires one symbol"); + return 77; + } + + [[nodiscard]] std::uint32_t objectFlags() const noexcept override { + return 0x20; + } +}; + +[[nodiscard]] rcc::mc::MCInst testInstruction() { + return rcc::mc::MCInst(rcc::mc::MCOpcode::fromValue(1)); +} + +[[nodiscard]] rcc::Expected<rcc::mc::MCExprRef> +symbolExpression(const rcc::mc::MCSymbolRef &Symbol) { + return rcc::mc::MCExpr::symbol(Symbol); +} + +[[nodiscard]] rcc::Expected<rcc::mc::MCExprRef> addConstant(const rcc::mc::MCSymbolRef &Symbol, + std::int64_t Addend) { + auto Base = symbolExpression(Symbol); + if (!Base) + return std::unexpected(Base.error()); + return rcc::mc::MCExpr::binary(rcc::mc::MCBinaryOperator::Add, *Base, + rcc::mc::MCExpr::constant(Addend)); +} + +[[nodiscard]] rcc::Expected<rcc::mc::MCExprRef> subtractSymbols(const rcc::mc::MCSymbolRef &Left, + const rcc::mc::MCSymbolRef &Right) { + auto LeftExpression = symbolExpression(Left); + auto RightExpression = symbolExpression(Right); + if (!LeftExpression) + return std::unexpected(LeftExpression.error()); + if (!RightExpression) + return std::unexpected(RightExpression.error()); + return rcc::mc::MCExpr::binary(rcc::mc::MCBinaryOperator::Subtract, *LeftExpression, + *RightExpression); +} + +[[nodiscard]] rcc::Expected<rcc::mc::MCSectionRef> +createSection(rcc::mc::MCContext &Context, std::string_view Name, rcc::mc::MCSectionKind Kind, + rcc::mc::MCSectionFlags Flags, std::uint64_t Alignment) { + return Context.getOrCreateSection(rcc::mc::MCSectionDesc{ + .Name = std::string(Name), + .Kind = Kind, + .Flags = Flags, + .Alignment = Alignment, + .EntrySize = 0, + .Group = {}, + .UniqueID = std::nullopt, + }); +} + +[[nodiscard]] const rcc::mc::MCObjectSymbol *findSymbol(const rcc::mc::MCAssembledObject &Object, + std::string_view Name) { + const auto Position = std::ranges::find_if(Object.Symbols, [&](const auto &Symbol) { + return Symbol.Symbol && Symbol.Symbol->name() == Name; + }); + return Position == Object.Symbols.end() ? nullptr : &*Position; +} + +[[nodiscard]] rcc::Expected<rcc::mc::MCAssembledObject> +assembleFixture(rcc::mc::MCContext &Context, const TestCodeEmitter &Emitter, + const TestAsmBackend &Backend) { + auto Text = createSection(Context, ".text", rcc::mc::MCSectionKind::Text, + {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Executable}, 4); + auto Data = createSection(Context, ".data", rcc::mc::MCSectionKind::Data, + {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Writable}, 1); + auto BSS = createSection(Context, ".bss", rcc::mc::MCSectionKind::BSS, + {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Writable}, 1); + auto Function = Context.getOrCreateSymbol("function", rcc::mc::MCSymbolKind::Function); + auto End = Context.createTemporarySymbol(".Lend"); + auto External = Context.getOrCreateSymbol("external", rcc::mc::MCSymbolKind::Generic); + if (!Text || !Data || !BSS || !Function || !End || !External) + return rcc::makeUnexpected(rcc::ErrorCode::InternalError, + "failed to create the MC object fixture"); + auto FunctionSize = subtractSymbols(*End, *Function); + auto ExternalValue = addConstant(*External, 7); + if (!FunctionSize || !ExternalValue) + return rcc::makeUnexpected(rcc::ErrorCode::InternalError, + "failed to create the MC object expressions"); + + rcc::mc::MCAssembler Assembler(Emitter, Backend); + constexpr std::uint64_t LargeBSS = (std::uint64_t{1} << 32U) + 17U; + if (!Assembler.switchSection(*Text) || + !Assembler.emitSymbolAttribute(*Function, rcc::mc::MCSymbolAttribute::Global) || + !Assembler.emitSymbolAttribute(*Function, rcc::mc::MCSymbolAttribute::Function) || + !Assembler.emitLabel(*Function) || !Assembler.emitInstruction(testInstruction()) || + !Assembler.emitAlignment(8, 0xaa, 0) || !Assembler.emitLabel(*End) || + !Assembler.emitSymbolSize(*Function, *FunctionSize) || !Assembler.switchSection(*Data) || + !Assembler.emitAlignment(16, 0, 0) || !Assembler.emitValue(*ExternalValue, 8) || + !Assembler.switchSection(*BSS) || !Assembler.emitAlignment(4096, 0, 0) || + !Assembler.emitZeroFill(LargeBSS)) { + return rcc::makeUnexpected(rcc::ErrorCode::InternalError, + "failed to emit the MC object fixture"); + } + return Assembler.assemble(); +} + +void testAssembler(TestContext &Test, const TestCodeEmitter &Emitter, + const TestAsmBackend &Backend) { + rcc::mc::MCContext Context; + auto Object = assembleFixture(Context, Emitter, Backend); + if (!Object) { + std::cerr << "MC object fixture assembly failed: " << Object.error().message() << '\n'; + Test.expect(false, "MC assembler fixture must assemble"); + return; + } + Test.expect(Object->Flags == 0x20 && Object->Sections.size() == 3, + "MC assembler must preserve target flags and section order"); + if (Object->Sections.size() != 3) + return; + Test.expect(Object->Sections[0].Alignment == 8 && Object->Sections[0].AddressSize == 8 && + Object->Sections[0].Contents == + std::vector<std::uint8_t>({0x11, 0x22, 0x33, 0x44, 0xaa, 0xaa, 0xaa, 0xaa}), + "MC assembler must materialize instructions and alignment padding"); + Test.expect(Object->Sections[1].Alignment == 16 && Object->Sections[1].AddressSize == 8 && + Object->Sections[1].Contents == std::vector<std::uint8_t>(8) && + Object->Sections[1].Relocations.size() == 1 && + Object->Sections[1].Relocations[0].Offset == 0 && + Object->Sections[1].Relocations[0].Type == 77 && + Object->Sections[1].Relocations[0].Addend == 7, + "MC assembler must preserve unresolved symbol relocations"); + constexpr std::uint64_t LargeBSS = (std::uint64_t{1} << 32U) + 17U; + Test.expect(Object->Sections[2].Alignment == 4096 && + Object->Sections[2].AddressSize == LargeBSS && + Object->Sections[2].Contents.empty(), + "MC assembler must not materialize NOBITS storage"); + + const rcc::mc::MCObjectSymbol *Function = findSymbol(*Object, "function"); + const rcc::mc::MCObjectSymbol *External = findSymbol(*Object, "external"); + Test.expect(Function != nullptr && Function->Binding == rcc::mc::MCObjectSymbolBinding::Global && + Function->Type == rcc::mc::MCObjectSymbolType::Function && + Function->SectionIndex == 0 && Function->Value == 0 && Function->Size == 8, + "MC assembler must resolve function binding, type, value, and size"); + Test.expect(External != nullptr && External->Binding == rcc::mc::MCObjectSymbolBinding::Global && + !External->SectionIndex && External->Value == 0, + "unqualified undefined symbols must become global ELF symbols"); +} + +void testAssemblerAtomicity(TestContext &Test, TestCodeEmitter &Emitter, + const TestAsmBackend &Backend) { + rcc::mc::MCContext Context; + auto Text = createSection(Context, ".text.atomic", rcc::mc::MCSectionKind::Text, + {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Executable}, 4); + auto Symbol = Context.getOrCreateSymbol("atomic_function", rcc::mc::MCSymbolKind::Function); + if (!Text || !Symbol) { + Test.expect(false, "MC assembler atomicity fixture must be constructible"); + return; + } + rcc::mc::MCAssembler Assembler(Emitter, Backend); + if (!Assembler.switchSection(*Text) || !Assembler.emitLabel(*Symbol) || + !Assembler.emitInstruction(testInstruction())) { + Test.expect(false, "MC assembler atomicity events must succeed"); + return; + } + + Emitter.FailNextEncoding = true; + auto Failed = Assembler.assemble(); + Test.expect(!Failed && Failed.error().code() == rcc::ErrorCode::InstructionEncodingFailed, + "MC assembler must propagate an encoding failure"); + Test.expect(Assembler.emitAlignment(8, 0, 0).has_value(), + "failed assembly must leave the input event stream mutable and unchanged"); + auto Succeeded = Assembler.assemble(); + Test.expect(Succeeded.has_value() && Succeeded->Sections.size() == 1 && + Succeeded->Sections[0].AddressSize == 8, + "MC assembler must support a clean retry after a failed transaction"); + if (!Succeeded) + return; + auto Size = rcc::mc::MCExpr::constant(4); + Test.expect(!Assembler.switchSection(*Text) && !Assembler.emitLabel(*Symbol) && + !Assembler.emitInstruction(testInstruction()) && !Assembler.emitValue(Size, 4) && + !Assembler.emitZeroFill(1) && !Assembler.emitAlignment(4, 0, 0) && + !Assembler.emitSymbolAttribute(*Symbol, rcc::mc::MCSymbolAttribute::Global) && + !Assembler.emitSymbolSize(*Symbol, Size) && + !Assembler.emitCommonSymbol(*Symbol, 4, 4) && !Assembler.assemble(), + "successful assembly must make every mutation and repeated finalization terminal"); +} + +void testSymbolSizeValidation(TestContext &Test, const TestCodeEmitter &Emitter, + const TestAsmBackend &Backend) { + rcc::mc::MCContext Context; + auto Text = createSection(Context, ".text.size", rcc::mc::MCSectionKind::Text, + {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Executable}, 4); + auto Data = createSection(Context, ".data.size", rcc::mc::MCSectionKind::Data, + {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Writable}, 4); + auto Function = Context.getOrCreateSymbol("sized_function", rcc::mc::MCSymbolKind::Function); + auto DataEnd = Context.createTemporarySymbol(".Ldata_end"); + if (!Text || !Data || !Function || !DataEnd) { + Test.expect(false, "symbol-size validation fixture must be constructible"); + return; + } + + auto CrossSectionSize = subtractSymbols(*DataEnd, *Function); + auto FunctionExpression = symbolExpression(*Function); + if (!CrossSectionSize || !FunctionExpression) { + Test.expect(false, "symbol-size validation expressions must be constructible"); + return; + } + auto TargetSize = rcc::mc::MCExpr::target(1, *FunctionExpression); + if (!TargetSize) { + Test.expect(false, "target symbol-size expression must be constructible"); + return; + } + + rcc::mc::MCAssembler CrossSectionAssembler(Emitter, Backend); + if (!CrossSectionAssembler.switchSection(*Text) || !CrossSectionAssembler.emitLabel(*Function) || + !CrossSectionAssembler.emitInstruction(testInstruction()) || + !CrossSectionAssembler.switchSection(*Data) || !CrossSectionAssembler.emitLabel(*DataEnd) || + !CrossSectionAssembler.emitSymbolSize(*Function, *CrossSectionSize)) { + Test.expect(false, "cross-section symbol-size events must succeed before finalization"); + return; + } + auto CrossSectionResult = CrossSectionAssembler.assemble(); + Test.expect(!CrossSectionResult && + CrossSectionResult.error().code() == rcc::ErrorCode::ObjectWritingFailed, + "MC assembler must reject a symbol-size difference across sections"); + + rcc::mc::MCAssembler TargetExpressionAssembler(Emitter, Backend); + if (!TargetExpressionAssembler.switchSection(*Text) || + !TargetExpressionAssembler.emitLabel(*Function) || + !TargetExpressionAssembler.emitInstruction(testInstruction()) || + !TargetExpressionAssembler.emitSymbolSize(*Function, *TargetSize)) { + Test.expect(false, "target symbol-size events must succeed before finalization"); + return; + } + auto TargetExpressionResult = TargetExpressionAssembler.assemble(); + Test.expect(!TargetExpressionResult && + TargetExpressionResult.error().code() == rcc::ErrorCode::ObjectWritingFailed, + "MC assembler must reject a target expression as a symbol size"); +} + +[[nodiscard]] std::uint16_t readU16(std::span<const std::uint8_t> Bytes, std::size_t Offset) { + return static_cast<std::uint16_t>(static_cast<std::uint16_t>(Bytes[Offset]) | + static_cast<std::uint16_t>(Bytes[Offset + 1U]) << 8U); +} + +[[nodiscard]] std::uint32_t readU32(std::span<const std::uint8_t> Bytes, std::size_t Offset) { + std::uint32_t Result = 0; + for (std::uint32_t Index = 0; Index < 4; ++Index) + Result |= static_cast<std::uint32_t>(Bytes[Offset + Index]) << (Index * 8U); + return Result; +} + +[[nodiscard]] bool contains(std::span<const std::uint8_t> Bytes, std::string_view Text) { + if (Text.size() > Bytes.size()) + return false; + const auto *Data = reinterpret_cast<const char *>(Bytes.data()); + return std::string_view(Data, Bytes.size()).find(Text) != std::string_view::npos; +} + +void testELFWriter(TestContext &Test, const TestCodeEmitter &Emitter, + const TestAsmBackend &Backend) { + rcc::mc::MCContext Context; + auto Object = assembleFixture(Context, Emitter, Backend); + if (!Object) { + Test.expect(false, "ELF writer fixture must assemble"); + return; + } + rcc::mc::ELFObjectTargetInfo Target{ + .Machine = 243, + .OSABI = 3, + .ABIVersion = 1, + .Flags = 1, + .ExtraSections = {{.Name = ".note.test", + .Type = 7, + .Flags = 0, + .Alignment = 4, + .EntrySize = 0, + .Contents = {1, 2, 3, 4}}}, + }; + rcc::mc::ELF64LEObjectWriter Writer(Target); + auto First = Writer.writeObject(*Object); + auto Second = Writer.writeObject(*Object); + Test.expect(First.has_value() && Second.has_value() && *First == *Second, + "ELF writer output must be deterministic"); + if (!First) + return; + Test.expect(First->size() < 4096 && First->size() >= 64 && + std::ranges::equal(std::span<const std::uint8_t>(*First).first(4), + std::array<std::uint8_t, 4>{0x7f, 'E', 'L', 'F'}) && + readU16(*First, 16) == 1 && readU16(*First, 18) == 243 && + readU32(*First, 48) == 0x21 && contains(*First, ".bss") && + contains(*First, ".rela.data") && contains(*First, ".note.test") && + contains(*First, "function") && contains(*First, "external"), + "ELF writer must encode its header, sparse BSS, sections, symbols, and relocation"); + + rcc::mc::ELFObjectTargetInfo MissingMachine = Target; + MissingMachine.Machine = 0; + Test.expect(!rcc::mc::ELF64LEObjectWriter(MissingMachine).writeObject(*Object), + "ELF writer must reject an unconfigured target machine"); + + auto DuplicateSection = *Object; + DuplicateSection.Sections.push_back(DuplicateSection.Sections.front()); + Test.expect(!Writer.writeObject(DuplicateSection), + "ELF writer must reject duplicate section identities"); + + auto BadAlignment = *Object; + BadAlignment.Sections[0].Alignment = 3; + Test.expect(!Writer.writeObject(BadAlignment), + "ELF writer must reject a non-power-of-two effective alignment"); + + auto BadContents = *Object; + ++BadContents.Sections[0].AddressSize; + Test.expect(!Writer.writeObject(BadContents), + "ELF writer must reject PROGBITS size/content disagreement"); + + auto BadNoBits = *Object; + BadNoBits.Sections[2].Relocations.push_back(rcc::mc::MCObjectRelocation{ + .Offset = 0, .Type = 1, .Symbol = Object->Symbols.back().Symbol, .Addend = 0}); + Test.expect(!Writer.writeObject(BadNoBits), + "ELF writer must reject relocations in a NOBITS section"); + + auto DuplicateSymbol = *Object; + DuplicateSymbol.Symbols.push_back(DuplicateSymbol.Symbols.front()); + Test.expect(!Writer.writeObject(DuplicateSymbol), + "ELF writer must reject duplicate symbol identities"); + + auto OutOfRangeSymbol = *Object; + auto Defined = std::ranges::find_if( + OutOfRangeSymbol.Symbols, [](const auto &Symbol) { return Symbol.SectionIndex.has_value(); }); + if (Defined != OutOfRangeSymbol.Symbols.end()) { + const std::size_t SectionIndex = + Defined->SectionIndex.value_or(OutOfRangeSymbol.Sections.size()); + Defined->Value = OutOfRangeSymbol.Sections[SectionIndex].AddressSize + 1U; + Test.expect(!Writer.writeObject(OutOfRangeSymbol), + "ELF writer must reject a symbol outside its defining section"); + } + + auto OutOfRangeSymbolSize = *Object; + auto Sized = std::ranges::find_if(OutOfRangeSymbolSize.Symbols, [](const auto &Symbol) { + return Symbol.SectionIndex.has_value() && Symbol.Size != 0; + }); + if (Sized != OutOfRangeSymbolSize.Symbols.end()) { + const std::size_t SectionIndex = + Sized->SectionIndex.value_or(OutOfRangeSymbolSize.Sections.size()); + Sized->Size = OutOfRangeSymbolSize.Sections[SectionIndex].AddressSize - Sized->Value + 1U; + Test.expect(!Writer.writeObject(OutOfRangeSymbolSize), + "ELF writer must reject a symbol range outside its defining section"); + } + + auto BadCommon = *Object; + auto Undefined = std::ranges::find_if(BadCommon.Symbols, + [](const auto &Symbol) { return !Symbol.SectionIndex; }); + if (Undefined != BadCommon.Symbols.end()) { + Undefined->Binding = rcc::mc::MCObjectSymbolBinding::Local; + Undefined->Value = 3; + Undefined->Size = 4; + Undefined->CommonAlignment = 3; + Test.expect(!Writer.writeObject(BadCommon), + "ELF writer must reject an invalid common symbol definition"); + } + + auto BadRelocationOffset = *Object; + BadRelocationOffset.Sections[1].Relocations[0].Offset = + BadRelocationOffset.Sections[1].AddressSize; + Test.expect(!Writer.writeObject(BadRelocationOffset), + "ELF writer must reject a relocation outside its section"); + + rcc::mc::MCContext OtherContext; + auto Unknown = OtherContext.getOrCreateSymbol("unknown", rcc::mc::MCSymbolKind::Generic); + if (Unknown) { + auto UnknownRelocation = *Object; + UnknownRelocation.Sections[1].Relocations[0].Symbol = *Unknown; + Test.expect(!Writer.writeObject(UnknownRelocation), + "ELF writer must reject a relocation to an unknown symbol"); + } + + auto BadExtraTarget = Target; + BadExtraTarget.ExtraSections[0].Alignment = 3; + Test.expect(!rcc::mc::ELF64LEObjectWriter(std::move(BadExtraTarget)).writeObject(*Object), + "ELF writer must reject an invalid target extra section"); + + auto ManagedExtraTarget = Target; + ManagedExtraTarget.ExtraSections[0].Type = 2; + Test.expect(!rcc::mc::ELF64LEObjectWriter(std::move(ManagedExtraTarget)).writeObject(*Object), + "ELF writer must reject a writer-managed target extra section type"); + + auto ExtendedNumberingTarget = Target; + ExtendedNumberingTarget.ExtraSections.resize(0xff00U); + auto ExtendedNumbering = + rcc::mc::ELF64LEObjectWriter(std::move(ExtendedNumberingTarget)).writeObject(*Object); + Test.expect(!ExtendedNumbering && ExtendedNumbering.error().code() == rcc::ErrorCode::Unsupported, + "ELF writer must reject section counts that require extended numbering"); +} + +} // namespace + +int main() { + TestContext Test; + TestCodeEmitter Emitter; + TestAsmBackend Backend; + testAssembler(Test, Emitter, Backend); + testAssemblerAtomicity(Test, Emitter, Backend); + testSymbolSizeValidation(Test, Emitter, Backend); + testELFWriter(Test, Emitter, Backend); + return Test.result(); +} diff --git a/backend/unittests/MC/MCTest.cpp b/backend/unittests/MC/MCTest.cpp new file mode 100644 index 0000000..1876fdb --- /dev/null +++ b/backend/unittests/MC/MCTest.cpp @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/MC/MCCFI.h" +#include "rcc/MC/MCContext.h" +#include "rcc/MC/MCExpr.h" +#include "rcc/MC/MCInst.h" +#include "rcc/MC/MCInstPrinter.h" +#include "rcc/MC/MCSection.h" +#include "rcc/MC/MCStreamer.h" + +#include <array> +#include <cstdint> +#include <iostream> +#include <optional> +#include <string> +#include <string_view> +#include <utility> + +namespace { + +class TestPrinter final : public rcc::mc::MCInstPrinter { +public: + [[nodiscard]] rcc::Expected<std::string> + printInstruction(const rcc::mc::MCInst &Instruction) const override { + if (Instruction.opcode().value() != 7 || Instruction.operands().size() != 2) + return rcc::makeUnexpected(rcc::ErrorCode::AssemblyLoweringFailed, + "unexpected test instruction"); + const auto *Register = Instruction.operands()[0].registerValue(); + const auto *Immediate = Instruction.operands()[1].immediateValue(); + if (Register == nullptr || Immediate == nullptr) + return rcc::makeUnexpected(rcc::ErrorCode::AssemblyLoweringFailed, + "unexpected test operands"); + return "probe r" + std::to_string(Register->value()) + ", " + std::to_string(*Immediate); + } + + [[nodiscard]] rcc::Expected<std::string> + printTargetExpression(std::uint32_t Kind, std::string_view Operand) const override { + if (Kind != 3) + return rcc::makeUnexpected(rcc::ErrorCode::AssemblyLoweringFailed, "unknown test expression"); + return "%test(" + std::string(Operand) + ")"; + } +}; + +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; +} + +} // namespace + +int main() { + rcc::mc::MCContext Context; + auto Symbol = Context.getOrCreateSymbol("object", rcc::mc::MCSymbolKind::Object); + auto SameSymbol = Context.getOrCreateSymbol("object", rcc::mc::MCSymbolKind::Generic); + if (!Symbol || !SameSymbol || *Symbol != *SameSymbol || Context.symbol("object") != *Symbol) + return fail("MCContext must uniquify compatible symbols"); + if (Context.getOrCreateSymbol("object", rcc::mc::MCSymbolKind::Function) || + Context.getOrCreateSymbol("bad\nname", rcc::mc::MCSymbolKind::Generic)) { + return fail("MCContext must reject conflicting or invalid symbols"); + } + auto Temporary0 = Context.createTemporarySymbol(".Ltmp"); + auto Temporary1 = Context.createTemporarySymbol(".Ltmp"); + if (!Temporary0 || !Temporary1 || (*Temporary0)->name() != ".Ltmp0" || + (*Temporary1)->name() != ".Ltmp1") { + return fail("temporary MC symbols must be deterministic"); + } + + auto SymbolExpr = rcc::mc::MCExpr::symbol(*Symbol); + if (!SymbolExpr) + return fail("symbol MC expression creation failed"); + auto Add = rcc::mc::MCExpr::binary(rcc::mc::MCBinaryOperator::Add, *SymbolExpr, + rcc::mc::MCExpr::constant(4)); + if (!Add) + return fail("binary MC expression creation failed"); + auto TargetExpr = rcc::mc::MCExpr::target(3, *Add); + if (!TargetExpr || !(*TargetExpr)->verify()) + return fail("structured MC expression creation failed"); + auto Folded = + rcc::mc::MCExpr::binary(rcc::mc::MCBinaryOperator::ShiftLeft, rcc::mc::MCExpr::constant(-1), + rcc::mc::MCExpr::constant(63)); + if (!Folded || (*Folded)->evaluateAbsolute() != std::int64_t(-9223372036854775807LL - 1)) + return fail("absolute MC expression evaluation must use defined 64-bit semantics"); + + rcc::mc::MCSectionDesc TextDescription{ + .Name = ".text", + .Kind = rcc::mc::MCSectionKind::Text, + .Flags = {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Executable}, + .Alignment = 4, + .EntrySize = 0, + .Group = {}, + .UniqueID = std::nullopt, + }; + auto Text = Context.getOrCreateSection(TextDescription); + auto SameText = Context.getOrCreateSection(TextDescription); + if (!Text || !SameText || *Text != *SameText || Context.section(".text") != *Text) + return fail("MCContext must uniquify identical sections"); + TextDescription.Alignment = 8; + auto DifferentlyAlignedText = Context.getOrCreateSection(TextDescription); + if (!DifferentlyAlignedText || *DifferentlyAlignedText != *Text) + return fail("MCContext must merge per-fragment section alignment requirements"); + TextDescription.Flags.set(rcc::mc::MCSectionFlag::Writable); + if (Context.getOrCreateSection(std::move(TextDescription))) + return fail("MCContext must reject conflicting section descriptions"); + + TestPrinter Printer; + rcc::mc::AsmTextStreamer Streamer(Printer); + rcc::mc::MCInst Instruction(rcc::mc::MCOpcode::fromValue(7)); + Instruction.setFlags(11); + Instruction.setSourceIdentity(42); + Instruction.addOperand(rcc::mc::MCOperand::registerOperand(rcc::mc::MCRegister::fromValue(5))); + Instruction.addOperand(rcc::mc::MCOperand::immediate(-9)); + if (Streamer.emitInstruction(Instruction) || !Streamer.switchSection(*Text) || + !Streamer.emitSymbolAttribute(*Symbol, rcc::mc::MCSymbolAttribute::Global) || + !Streamer.emitLabel(*Symbol) || Streamer.emitLabel(*Symbol) || + !Streamer.emitInstruction(Instruction) || !Streamer.emitValue(*TargetExpr, 8)) { + return fail("assembly streamer event validation failed"); + } + const std::array<std::uint8_t, 3> Bytes{0x01, 0x02, 0xff}; + if (!Streamer.emitBytes(Bytes) || !Streamer.emitZeroFill(6) || + !Streamer.emitAlignment(16, 0xaa, 31) || + !Streamer.emitSymbolSize(*Symbol, rcc::mc::MCExpr::constant(17))) { + return fail("assembly streamer data events failed"); + } + rcc::mc::MCCFIInstruction CFI; + CFI.Operation = rcc::mc::MCCFIOperation::DefCFA; + CFI.Operands = {2, 16}; + if (!Streamer.emitCFI(CFI) || !Streamer.emitInlineAssembly(" nop\n") || + !Streamer.emitComment("finished") || !Streamer.finish() || Streamer.finish()) { + return fail("assembly streamer terminal events failed"); + } + auto Output = Streamer.takeOutput(); + if (!Output || Streamer.takeOutput()) + return fail("assembly output ownership must transfer exactly once"); + if (!contains(*Output, ".section .text,\"ax\",@progbits\n") || + !contains(*Output, ".globl object\nobject:\n") || !contains(*Output, " probe r5, -9\n") || + !contains(*Output, " .8byte %test((object+4))\n") || + !contains(*Output, " .byte 0x01, 0x02, 0xff\n") || !contains(*Output, " .zero 6\n") || + !contains(*Output, " .p2align 4, 170, 31\n") || !contains(*Output, ".size object, 17\n") || + !contains(*Output, ".cfi_def_cfa 2, 16\n") || !contains(*Output, " nop\n # finished\n")) { + return fail("assembly streamer output is incomplete or unstable"); + } + return 0; +} diff --git a/backend/unittests/MIR/CMakeLists.txt b/backend/unittests/MIR/CMakeLists.txt new file mode 100644 index 0000000..8ff0527 --- /dev/null +++ b/backend/unittests/MIR/CMakeLists.txt @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +add_executable(RCCBackendEmbeddedIRTests EmbeddedIRTest.cpp) +rcc_backend_configure_target(RCCBackendEmbeddedIRTests) +target_link_libraries(RCCBackendEmbeddedIRTests PRIVATE RCCBackend::MIR) + +add_test(NAME RCCBackend.MIR.EmbeddedIR COMMAND RCCBackendEmbeddedIRTests) + +add_executable(RCCBackendMILexerTests MILexerTest.cpp) +rcc_backend_configure_target(RCCBackendMILexerTests) +target_include_directories( + RCCBackendMILexerTests + PRIVATE "${RCC_BACKEND_SOURCE_DIR}/lib/CodeGen/MIRParser" +) +target_link_libraries(RCCBackendMILexerTests PRIVATE RCCBackend::MIR) + +add_test(NAME RCCBackend.MIR.MILexer COMMAND RCCBackendMILexerTests) + +add_executable(RCCBackendMIParserTests MIParserTest.cpp) +rcc_backend_configure_target(RCCBackendMIParserTests) +target_link_libraries( + RCCBackendMIParserTests + PRIVATE + RCCBackend::MIR + RCCBackend::TestTarget +) + +add_test(NAME RCCBackend.MIR.MIParser COMMAND RCCBackendMIParserTests) + +add_executable(RCCBackendMIRDocumentTests MIRDocumentTest.cpp) +rcc_backend_configure_target(RCCBackendMIRDocumentTests) +target_link_libraries( + RCCBackendMIRDocumentTests + PRIVATE + RCCBackend::MIR + RCCBackend::TestTarget +) + +add_test(NAME RCCBackend.MIR.MIRDocument COMMAND RCCBackendMIRDocumentTests) + +rcc_backend_register_format_files( + "${CMAKE_CURRENT_SOURCE_DIR}/EmbeddedIRTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MILexerTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MIRDocumentTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MIParserTest.cpp" +) diff --git a/backend/unittests/MIR/EmbeddedIRTest.cpp b/backend/unittests/MIR/EmbeddedIRTest.cpp new file mode 100644 index 0000000..bbac841 --- /dev/null +++ b/backend/unittests/MIR/EmbeddedIRTest.cpp @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/MIR/EmbeddedIR.h" + +#include <iostream> +#include <string> +#include <string_view> + +namespace { + +int fail(const char *Message) { + std::cerr << Message << '\n'; + return 1; +} + +int testReferenceIndex() { + const std::string Input = "; @commented = global i8 0\n" + "target datalayout = \"e-m:e-p:64:64\"\n" + "target triple = \"riscv64-unknown-linux-gnu\"\n" + "%pair = type { i32, i32 }\n" + "@ordinary = global i32 0\n" + "@\"space\\20global\" = external global i8\n" + "declare void @declared()\n" + "define void @\"function\\20name\"(\n" + " { i32, i32 } %argument\n" + ") {\n" + "entry:\n" + " %slot = alloca i32, align 4\n" + " %not_an_alloca = add i32 %argument, 2\n" + " br label %\"exit\\20block\"\n" + "\"exit\\20block\":\n" + " ret void\n" + "}\n" + "!0 = !DILocation(line: 1, scope: !1)\n" + "!llvm.module.flags = !{!0}\n"; + + rcc::mir::EmbeddedIRIndexResult Result = rcc::mir::indexEmbeddedIR("embedded.ll", Input); + const rcc::mir::EmbeddedIRReferenceIndex *Index = Result.index(); + if (!Result.succeeded() || Index == nullptr) + return fail("Embedded IR indexing must accept the MIR reference subset"); + if (Index->source().text() != Input) + return fail("Embedded IR indexing must preserve the original text"); + if (Index->targetTriple() != std::optional<std::string_view>("riscv64-unknown-linux-gnu") || + Index->dataLayout() != std::optional<std::string_view>("e-m:e-p:64:64")) + return fail("Embedded IR indexing must preserve target facts"); + if (Index->functions().size() != 2 || Index->function("declared") == nullptr || + Index->function("declared")->isDefinition()) + return fail("Embedded IR indexing must retain function declarations"); + + const rcc::mir::EmbeddedIRFunction *Function = Index->function("function name"); + if (Function == nullptr || !Function->isDefinition() || Function->blocks().size() != 2) + return fail("Embedded IR indexing must decode function and block identifiers"); + if (Function->block("entry") == nullptr || Function->block("exit block") == nullptr) + return fail("Embedded IR block lookup must use decoded names"); + if (Function->allocas().size() != 1 || Function->alloca("slot") == nullptr || + Function->alloca("not_an_alloca") != nullptr) + return fail("Embedded IR indexing must distinguish named allocas from other local values"); + if (Function->values().size() != 3 || Function->value("argument") == nullptr || + Function->value("slot") == nullptr || Function->value("not_an_alloca") == nullptr) + return fail("Embedded IR indexing must retain arguments and instruction results"); + + if (!Index->hasGlobalValue("ordinary") || !Index->hasGlobalValue("space global") || + !Index->hasGlobalValue("declared") || Index->hasGlobalValue("commented")) + return fail("Embedded IR global lookup must include objects and functions but ignore comments"); + if (Index->metadata("0") == nullptr || Index->metadata("llvm.module.flags") == nullptr || + Index->metadataDefinitions().size() != 2) + return fail("Embedded IR indexing must retain numeric and named metadata definitions"); + if (Index->metadata("0")->DynamicType != "DILocation" || + Index->metadata("llvm.module.flags")->DynamicType != "MDTuple") + return fail("Embedded IR indexing must classify metadata dynamic types"); + if (Index->typeDefinitions().size() != 1 || Index->typeDefinition("pair") == nullptr || + Index->typeDefinition("pair")->Definition != "{ i32, i32 }") + return fail("Embedded IR indexing must retain named type definitions"); + return 0; +} + +int testDeclarationDefinitionMerge() { + const std::string Input = "declare void @shared()\n" + "define void @shared() {\n" + "0:\n" + " ret void\n" + "}\n"; + rcc::mir::EmbeddedIRIndexResult Result = rcc::mir::indexEmbeddedIR("merge.ll", Input); + const rcc::mir::EmbeddedIRReferenceIndex *Index = Result.index(); + if (!Result.succeeded() || Index == nullptr || Index->functions().size() != 1) + return fail("Embedded IR indexing must merge a declaration with its definition"); + const rcc::mir::EmbeddedIRFunction *Function = Index->function("shared"); + if (Function == nullptr || !Function->isDefinition() || Function->block("0") == nullptr) + return fail("A later definition must complete its indexed declaration"); + return 0; +} + +bool hasSingleDiagnostic(const rcc::mir::EmbeddedIRIndexResult &Result, std::string_view Message) { + return !Result.succeeded() && Result.diagnostics().size() == 1 && + Result.diagnostics()[0].Message == Message; +} + +int testDiagnostics() { + auto DuplicateBlock = + rcc::mir::indexEmbeddedIR("blocks.ll", "define void @f() {\nentry:\nentry:\n ret void\n}\n"); + if (!hasSingleDiagnostic(DuplicateBlock, "duplicate embedded LLVM IR basic block label") || + DuplicateBlock.diagnostics()[0].Range.Begin.Line != 3) + return fail("Embedded IR indexing must reject a duplicate block at its second label"); + + auto BadEscape = rcc::mir::indexEmbeddedIR("escape.ll", "@\"bad\\q0\" = global i8 0\n"); + if (!hasSingleDiagnostic(BadEscape, + "LLVM IR identifier escape must contain two hexadecimal digits")) + return fail("Embedded IR indexing must reject malformed identifier escapes"); + + auto Unterminated = + rcc::mir::indexEmbeddedIR("function.ll", "define void @f() {\nentry:\n ret void\n"); + if (!hasSingleDiagnostic(Unterminated, + "embedded LLVM IR function definition has no closing brace")) + return fail("Embedded IR indexing must reject an unterminated function definition"); + + auto DuplicateTriple = + rcc::mir::indexEmbeddedIR("triple.ll", "target triple = \"riscv64-unknown-linux-gnu\"\n" + "target triple = \"riscv64-unknown-linux-gnu\"\n"); + if (!hasSingleDiagnostic(DuplicateTriple, "duplicate embedded LLVM IR target triple")) + return fail("Embedded IR indexing must reject duplicate target facts"); + + auto NameConflict = + rcc::mir::indexEmbeddedIR("names.ll", "@same = global i32 0\ndeclare void @same()\n"); + if (!hasSingleDiagnostic(NameConflict, + "embedded LLVM IR name is used by both a function and a global object")) + return fail("Embedded IR indexing must reject conflicting global value kinds"); + + const std::string NulInput("target triple = \"x\"\0", 20); + auto Nul = rcc::mir::indexEmbeddedIR("nul.ll", NulInput); + if (!hasSingleDiagnostic(Nul, "NUL bytes are not allowed in embedded LLVM IR")) + return fail("Embedded IR indexing must reject embedded NUL bytes"); + return 0; +} + +} // namespace + +int main() { + if (const int Result = testReferenceIndex(); Result != 0) + return Result; + if (const int Result = testDeclarationDefinitionMerge(); Result != 0) + return Result; + return testDiagnostics(); +} diff --git a/backend/unittests/MIR/MILexerTest.cpp b/backend/unittests/MIR/MILexerTest.cpp new file mode 100644 index 0000000..46b9cae --- /dev/null +++ b/backend/unittests/MIR/MILexerTest.cpp @@ -0,0 +1,417 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "MILexer.h" + +#include "rcc/Support/TextDiagnostic.h" + +#include <array> +#include <cstddef> +#include <iostream> +#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'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +using rcc::mir::MITokenKind; + +struct ExpectedToken final { + MITokenKind Kind; + std::string_view Spelling; + std::optional<std::string_view> StringValue; + std::optional<std::string_view> IntegerValue; +}; + +void expectToken(TestContext &Context, rcc::mir::MILexer &Lexer, const ExpectedToken &Expected) { + const rcc::mir::MIToken Token = Lexer.next(); + Context.expect(Token.kind() == Expected.Kind && Token.spelling() == Expected.Spelling, + "MI token kind and spelling must match the fixed grammar"); + Context.expect(Token.hasStringValue() == Expected.StringValue.has_value() && + (!Expected.StringValue || Token.stringValue() == *Expected.StringValue), + "MI token string payload must be exact"); + Context.expect(Token.hasIntegerValue() == Expected.IntegerValue.has_value() && + (!Expected.IntegerValue || Token.integerValue() == *Expected.IntegerValue), + "MI token integer spelling must remain exact"); +} + +void expectEnd(TestContext &Context, rcc::mir::MILexer &Lexer) { + const rcc::mir::MIToken Token = Lexer.next(); + Context.expect(Token.kind() == MITokenKind::EndOfFile && Token.spelling().empty(), + "MI lexer must terminate with an empty end-of-file token"); +} + +void testPunctuationAndTrivia(TestContext &Context) { + rcc::mir::MILexer Lexer("punctuation.mir", + " \t; line comment\r\n/* first */ /* second\ncomment */ , = _ : :: . ! " + "( ) { } + - < >"); + + const std::array Expected{ + ExpectedToken{MITokenKind::Newline, "\r\n", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::Comma, ",", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::Equal, "=", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::Underscore, "_", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::Colon, ":", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::DoubleColon, "::", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::Dot, ".", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::Exclaim, "!", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::LeftParen, "(", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::RightParen, ")", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::LeftBrace, "{", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::RightBrace, "}", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::Plus, "+", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::Minus, "-", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::Less, "<", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::Greater, ">", std::nullopt, std::nullopt}, + }; + for (const ExpectedToken &Token : Expected) + expectToken(Context, Lexer, Token); + expectEnd(Context, Lexer); + + Context.expect(!Lexer.failed() && Lexer.diagnostics().empty() && + Lexer.tokenCount() == Expected.size(), + "whitespace and both MIR comment forms must not create tokens"); +} + +void testKeywordCorpus(TestContext &Context) { + // Compatibility corpus from llvm/lib/CodeGen/MIRParser/MILexer.cpp at + // 2b40c303e79f64c5c7642032554eda026594d604. + constexpr std::array Keywords{ + std::string_view{"implicit"}, + std::string_view{"implicit-def"}, + std::string_view{"def"}, + std::string_view{"dead"}, + std::string_view{"killed"}, + std::string_view{"undef"}, + std::string_view{"internal"}, + std::string_view{"early-clobber"}, + std::string_view{"debug-use"}, + std::string_view{"renamable"}, + std::string_view{"tied-def"}, + std::string_view{"frame-setup"}, + std::string_view{"frame-destroy"}, + std::string_view{"nnan"}, + std::string_view{"ninf"}, + std::string_view{"nsz"}, + std::string_view{"arcp"}, + std::string_view{"contract"}, + std::string_view{"afn"}, + std::string_view{"reassoc"}, + std::string_view{"nuw"}, + std::string_view{"nsw"}, + std::string_view{"nusw"}, + std::string_view{"exact"}, + std::string_view{"nneg"}, + std::string_view{"disjoint"}, + std::string_view{"samesign"}, + std::string_view{"inbounds"}, + std::string_view{"nofpexcept"}, + std::string_view{"unpredictable"}, + std::string_view{"debug-location"}, + std::string_view{"debug-instr-number"}, + std::string_view{"dbg-instr-ref"}, + std::string_view{"same_value"}, + std::string_view{"offset"}, + std::string_view{"rel_offset"}, + std::string_view{"def_cfa_register"}, + std::string_view{"def_cfa_offset"}, + std::string_view{"adjust_cfa_offset"}, + std::string_view{"escape"}, + std::string_view{"def_cfa"}, + std::string_view{"llvm_def_aspace_cfa"}, + std::string_view{"remember_state"}, + std::string_view{"restore"}, + std::string_view{"restore_state"}, + std::string_view{"undefined"}, + std::string_view{"register"}, + std::string_view{"window_save"}, + std::string_view{"negate_ra_sign_state"}, + std::string_view{"negate_ra_sign_state_with_pc"}, + std::string_view{"llvm_register_pair"}, + std::string_view{"llvm_vector_registers"}, + std::string_view{"llvm_vector_offset"}, + std::string_view{"llvm_vector_register_mask"}, + std::string_view{"blockaddress"}, + std::string_view{"intrinsic"}, + std::string_view{"target-index"}, + std::string_view{"half"}, + std::string_view{"bfloat"}, + std::string_view{"float"}, + std::string_view{"double"}, + std::string_view{"x86_fp80"}, + std::string_view{"fp128"}, + std::string_view{"ppc_fp128"}, + std::string_view{"target-flags"}, + std::string_view{"volatile"}, + std::string_view{"non-temporal"}, + std::string_view{"dereferenceable"}, + std::string_view{"invariant"}, + std::string_view{"align"}, + std::string_view{"basealign"}, + std::string_view{"addrspace"}, + std::string_view{"stack"}, + std::string_view{"got"}, + std::string_view{"jump-table"}, + std::string_view{"constant-pool"}, + std::string_view{"call-entry"}, + std::string_view{"custom"}, + std::string_view{"lanemask"}, + std::string_view{"liveout"}, + std::string_view{"landing-pad"}, + std::string_view{"inlineasm-br-indirect-target"}, + std::string_view{"ehscope-entry"}, + std::string_view{"ehfunclet-entry"}, + std::string_view{"liveins"}, + std::string_view{"successors"}, + std::string_view{"floatpred"}, + std::string_view{"intpred"}, + std::string_view{"shufflemask"}, + std::string_view{"pre-instr-symbol"}, + std::string_view{"post-instr-symbol"}, + std::string_view{"heap-alloc-marker"}, + std::string_view{"pcsections"}, + std::string_view{"cfi-type"}, + std::string_view{"deactivation-symbol"}, + std::string_view{"bbsections"}, + std::string_view{"bb_id"}, + std::string_view{"unknown-size"}, + std::string_view{"unknown-address"}, + std::string_view{"distinct"}, + std::string_view{"ir-block-address-taken"}, + std::string_view{"machine-block-address-taken"}, + std::string_view{"call-frame-size"}, + std::string_view{"noconvergent"}, + std::string_view{"mmra"}, + }; + + std::string Source; + for (const std::string_view Keyword : Keywords) { + Source += Keyword; + Source.push_back(' '); + } + Source += "TEST_OPCODE keyword-like"; + + rcc::mir::MILexer Lexer("keywords.mir", std::move(Source)); + for (const std::string_view Keyword : Keywords) { + const rcc::mir::MIToken Token = Lexer.next(); + Context.expect(Token.kind() == MITokenKind::Keyword && Token.stringValue() == Keyword && + rcc::mir::isMIKeyword(Keyword), + "every fixed LLVM MIR keyword must be reserved"); + } + expectToken(Context, Lexer, + {MITokenKind::Identifier, "TEST_OPCODE", "TEST_OPCODE", std::nullopt}); + expectToken(Context, Lexer, + {MITokenKind::Identifier, "keyword-like", "keyword-like", std::nullopt}); + expectEnd(Context, Lexer); + Context.expect(!rcc::mir::isMIKeyword("Implicit") && !rcc::mir::isMIKeyword("unknown-keyword"), + "MIR keyword classification must remain case-sensitive"); +} + +void testMetadataKeywords(TestContext &Context) { + constexpr std::array Keywords{ + std::string_view{"!tbaa"}, std::string_view{"!alias.scope"}, + std::string_view{"!noalias"}, std::string_view{"!noalias.addrspace"}, + std::string_view{"!range"}, std::string_view{"!DIExpression"}, + std::string_view{"!DILocation"}, + }; + std::string Source; + for (const std::string_view Keyword : Keywords) { + Source += Keyword; + Source.push_back(' '); + } + Source += "! !12"; + + rcc::mir::MILexer Lexer("metadata.mir", std::move(Source)); + for (const std::string_view Keyword : Keywords) { + const rcc::mir::MIToken Token = Lexer.next(); + Context.expect(Token.kind() == MITokenKind::MetadataKeyword && Token.stringValue() == Keyword && + rcc::mir::isMIMetadataKeyword(Keyword), + "every fixed named metadata token must be recognized"); + } + expectToken(Context, Lexer, {MITokenKind::Exclaim, "!", std::nullopt, std::nullopt}); + expectToken(Context, Lexer, {MITokenKind::Exclaim, "!", std::nullopt, std::nullopt}); + expectToken(Context, Lexer, {MITokenKind::IntegerLiteral, "12", std::nullopt, "12"}); + expectEnd(Context, Lexer); +} + +void testNamedEntities(TestContext &Context) { + rcc::mir::MILexer Lexer("entities.mir", + R"(bb.12.entry %bb.13.exit %jump-table.2 %stack.3.slot )" + R"(%fixed-stack.4 %const.5 %subreg.low32 %ir-block.7 )" + R"(%ir-block."quoted\20block" %ir.8 %ir.named %12 %named-vreg $x0 )" + R"(@9 @global @"quoted\20global" &external &"quoted\20external" )" + R"(<mcsymbol local.label> <mcsymbol "quoted\20symbol"> `i32 7` )" + R"("plain\20string\\tail")"); + + const std::array Expected{ + ExpectedToken{MITokenKind::MachineBasicBlockLabel, "bb.12.entry", "entry", "12"}, + ExpectedToken{MITokenKind::MachineBasicBlock, "%bb.13.exit", "exit", "13"}, + ExpectedToken{MITokenKind::JumpTableIndex, "%jump-table.2", std::nullopt, "2"}, + ExpectedToken{MITokenKind::StackObject, "%stack.3.slot", "slot", "3"}, + ExpectedToken{MITokenKind::FixedStackObject, "%fixed-stack.4", std::nullopt, "4"}, + ExpectedToken{MITokenKind::ConstantPoolItem, "%const.5", std::nullopt, "5"}, + ExpectedToken{MITokenKind::SubRegisterIndex, "%subreg.low32", "low32", std::nullopt}, + ExpectedToken{MITokenKind::IRBlock, "%ir-block.7", std::nullopt, "7"}, + ExpectedToken{MITokenKind::NamedIRBlock, R"(%ir-block."quoted\20block")", "quoted block", + std::nullopt}, + ExpectedToken{MITokenKind::IRValue, "%ir.8", std::nullopt, "8"}, + ExpectedToken{MITokenKind::NamedIRValue, "%ir.named", "named", std::nullopt}, + ExpectedToken{MITokenKind::VirtualRegister, "%12", std::nullopt, "12"}, + ExpectedToken{MITokenKind::NamedVirtualRegister, "%named-vreg", "named-vreg", std::nullopt}, + ExpectedToken{MITokenKind::NamedRegister, "$x0", "x0", std::nullopt}, + ExpectedToken{MITokenKind::GlobalValue, "@9", std::nullopt, "9"}, + ExpectedToken{MITokenKind::NamedGlobalValue, "@global", "global", std::nullopt}, + ExpectedToken{MITokenKind::NamedGlobalValue, R"(@"quoted\20global")", "quoted global", + std::nullopt}, + ExpectedToken{MITokenKind::ExternalSymbol, "&external", "external", std::nullopt}, + ExpectedToken{MITokenKind::ExternalSymbol, R"(&"quoted\20external")", "quoted external", + std::nullopt}, + ExpectedToken{MITokenKind::MCSymbol, "<mcsymbol local.label>", "local.label", std::nullopt}, + ExpectedToken{MITokenKind::MCSymbol, R"(<mcsymbol "quoted\20symbol">)", "quoted symbol", + std::nullopt}, + ExpectedToken{MITokenKind::QuotedIRValue, "`i32 7`", "i32 7", std::nullopt}, + ExpectedToken{MITokenKind::StringConstant, R"("plain\20string\\tail")", + R"(plain string\tail)", std::nullopt}, + }; + for (const ExpectedToken &Token : Expected) + expectToken(Context, Lexer, Token); + expectEnd(Context, Lexer); + Context.expect(!Lexer.failed() && Lexer.diagnostics().empty(), + "all fixed named MIR entity forms must lex without diagnostics"); +} + +void testNumericalLiterals(TestContext &Context) { + rcc::mir::MILexer Lexer("numbers.mir", "-42 17 1. 1.25 -2.5e-3 6.02E23 0xFF 0XK3FFF000000000000 " + "f0x3F800000 1e3 0x f0x"); + + const std::array Expected{ + ExpectedToken{MITokenKind::IntegerLiteral, "-42", std::nullopt, "-42"}, + ExpectedToken{MITokenKind::IntegerLiteral, "17", std::nullopt, "17"}, + ExpectedToken{MITokenKind::FloatingPointLiteral, "1.", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::FloatingPointLiteral, "1.25", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::FloatingPointLiteral, "-2.5e-3", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::FloatingPointLiteral, "6.02E23", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::HexLiteral, "0xFF", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::FloatingPointLiteral, "0XK3FFF000000000000", std::nullopt, + std::nullopt}, + ExpectedToken{MITokenKind::FloatingPointLiteral, "f0x3F800000", std::nullopt, std::nullopt}, + ExpectedToken{MITokenKind::IntegerLiteral, "1", std::nullopt, "1"}, + ExpectedToken{MITokenKind::Identifier, "e3", "e3", std::nullopt}, + ExpectedToken{MITokenKind::IntegerLiteral, "0", std::nullopt, "0"}, + ExpectedToken{MITokenKind::Identifier, "x", "x", std::nullopt}, + ExpectedToken{MITokenKind::Identifier, "f0x", "f0x", std::nullopt}, + }; + for (const ExpectedToken &Token : Expected) + expectToken(Context, Lexer, Token); + expectEnd(Context, Lexer); + + std::string LargeNumber(10000, '9'); + rcc::mir::MILexer LargeLexer("large-number.mir", LargeNumber); + const rcc::mir::MIToken LargeToken = LargeLexer.next(); + Context.expect(LargeToken.kind() == MITokenKind::IntegerLiteral && + LargeToken.integerValue() == LargeNumber, + "integer tokenization must preserve arbitrary-width decimal spelling"); +} + +void expectSingleError(TestContext &Context, std::string Source, std::string_view MessageFragment, + rcc::mir::MILexerOptions Options = {}) { + rcc::mir::MILexer Lexer("invalid.mir", std::move(Source), Options); + const rcc::mir::MIToken Token = Lexer.next(); + Context.expect(Token.kind() == MITokenKind::Error && Lexer.failed() && + Lexer.diagnostics().size() == 1 && + Lexer.diagnostics().front().Category == "mir-lexer" && + Lexer.diagnostics().front().Message.find(MessageFragment) != std::string::npos, + "invalid MI input must produce one structured lexer diagnostic"); + expectEnd(Context, Lexer); + Context.expect(Lexer.diagnostics().size() == 1, + "reading after an MI lexer failure must not duplicate diagnostics"); +} + +void testDiagnosticsAndLimits(TestContext &Context) { + expectSingleError(Context, "/* never closed", "closing '*/'"); + expectSingleError(Context, R"("never closed)", "closing '\"'"); + expectSingleError(Context, "`never closed", "closing '`'"); + expectSingleError(Context, "<mcsymbol local", "closed by '>'"); + expectSingleError(Context, "!unknown", "unknown metadata keyword"); + expectSingleError(Context, "bb.name", "decimal block number"); + expectSingleError(Context, "#", "unexpected character '#'"); + expectSingleError(Context, std::string(1, '\0'), "unexpected byte 0x00"); + expectSingleError(Context, "four", "length limit", + rcc::mir::MILexerOptions{.MaximumTokenLength = 3}); + + rcc::mir::MILexer Limited("limited.mir", "one two", + rcc::mir::MILexerOptions{.MaximumTokenLength = 0, .MaximumTokens = 1}); + expectToken(Context, Limited, {MITokenKind::Identifier, "one", "one", std::nullopt}); + const rcc::mir::MIToken LimitError = Limited.next(); + Context.expect(LimitError.kind() == MITokenKind::Error && Limited.diagnostics().size() == 1 && + Limited.diagnostics().front().Message.find("token count") != + std::string::npos && + Limited.tokenCount() == 1, + "MI lexer token count limits must fail before publishing excess tokens"); +} + +void testRangesAndClassifiers(TestContext &Context) { + rcc::mir::MILexer Lexer("ranges.mir", "implicit\n $x0 \"flag\""); + const rcc::mir::MIToken Flag = Lexer.next(); + const rcc::mir::MIToken Newline = Lexer.next(); + const rcc::mir::MIToken Register = Lexer.next(); + const rcc::mir::MIToken StringFlag = Lexer.next(); + + Context.expect(Flag.isRegisterFlag() && !Flag.isRegister() && Flag.range().Begin.Line == 1 && + Flag.range().Begin.Column == 1 && Flag.range().End.Column == 9, + "register flags must retain exact source ranges"); + Context.expect(Newline.kind() == MITokenKind::Newline && Register.isRegister() && + Register.stringValue() == "x0" && Register.range().Begin.Line == 2 && + Register.range().Begin.Column == 3, + "line and column tracking must account for skipped indentation"); + Context.expect(StringFlag.isMemoryOperandFlag() && + rcc::mir::getMITokenKindName(StringFlag.kind()) == "string-constant", + "memory operand and token-kind classifiers must cover string flags"); + + rcc::mir::MILexer Invalid("body.mir", "ok\n #"); + static_cast<void>(Invalid.next()); + static_cast<void>(Invalid.next()); + static_cast<void>(Invalid.next()); + const std::string Formatted = + rcc::formatTextDiagnostic(Invalid.diagnostics().front(), Invalid.source()); + Context.expect(Formatted.starts_with("error: body.mir:2:3: unexpected character '#'"), + "formatted MI diagnostics must report stable input line and column"); + + rcc::mir::MILexer Original("move.mir", R"("owned\20value")"); + rcc::mir::MILexer Moved(std::move(Original)); + const rcc::mir::MIToken Owned = Moved.next(); + Context.expect(Owned.stringValue() == "owned value", + "moving an MI lexer must preserve owned decoded token values"); +} + +} // namespace + +int main() { + TestContext Context; + testPunctuationAndTrivia(Context); + testKeywordCorpus(Context); + testMetadataKeywords(Context); + testNamedEntities(Context); + testNumericalLiterals(Context); + testDiagnosticsAndLimits(Context); + testRangesAndClassifiers(Context); + return Context.result(); +} diff --git a/backend/unittests/MIR/MIParserTest.cpp b/backend/unittests/MIR/MIParserTest.cpp new file mode 100644 index 0000000..61292ac --- /dev/null +++ b/backend/unittests/MIR/MIParserTest.cpp @@ -0,0 +1,640 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MIRParser/MIParser.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MIRPrinter.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineInstr.h" +#include "rcc/CodeGen/MachineMemOperand.h" +#include "rcc/CodeGen/MachineOperand.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/TargetDescription.h" + +#include <cstdint> +#include <iostream> +#include <limits> +#include <memory> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t CopyOpcode = 1; +constexpr std::uint32_t AddImmediateOpcode = 2; +constexpr std::uint32_t BranchOpcode = 3; +constexpr std::uint32_t NopOpcode = 4; +constexpr std::uint32_t ConstantOpcode = 5; +constexpr std::uint32_t FloatingConstantOpcode = 6; +constexpr std::uint32_t MetadataOpcode = 7; +constexpr std::uint32_t LoadOpcode = 8; +constexpr std::uint32_t InlineAsmOpcode = 9; + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::OperandDescriptor +registerOperand(rcc::RegisterOperandRole Role, + rcc::RegisterClassID Class = rcc::test::TestRegisterClass::GPR64) { + rcc::OperandDescriptor Descriptor; + Descriptor.Kinds = {rcc::MachineOperand::Kind::Register}; + Descriptor.RegisterRole = Role; + Descriptor.RegClass = Class; + return Descriptor; +} + +[[nodiscard]] rcc::InstructionDescriptor descriptor(std::uint32_t OpcodeValue, std::string Name) { + rcc::InstructionDescriptor Descriptor; + Descriptor.InstructionOpcode = rcc::Opcode::fromValue(OpcodeValue); + Descriptor.Name = std::move(Name); + Descriptor.Flags = {rcc::InstructionFlag::ProducesCode}; + return Descriptor; +} + +[[nodiscard]] rcc::OperandDescriptor genericRegisterOperand(rcc::RegisterOperandRole Role) { + rcc::OperandDescriptor Descriptor; + Descriptor.Kinds = {rcc::MachineOperand::Kind::Register}; + Descriptor.RegisterRole = Role; + return Descriptor; +} + +[[nodiscard]] rcc::OperandDescriptor operand(rcc::MachineOperand::Kind Kind) { + rcc::OperandDescriptor Descriptor; + Descriptor.Kinds = {Kind}; + return Descriptor; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + auto Copy = descriptor(CopyOpcode, "TEST_COPY"); + Copy.Operands = { + registerOperand(rcc::RegisterOperandRole::Def), + registerOperand(rcc::RegisterOperandRole::Use), + }; + Copy.NumExplicitDefs = 1; + + auto AddImmediate = descriptor(AddImmediateOpcode, "TEST_ADDI"); + AddImmediate.Operands = { + registerOperand(rcc::RegisterOperandRole::Def), + registerOperand(rcc::RegisterOperandRole::Use), + }; + rcc::OperandDescriptor Immediate; + Immediate.Kinds = {rcc::MachineOperand::Kind::Immediate}; + Immediate.AllowedTargetFlags = 0xFU; + AddImmediate.Operands.push_back(Immediate); + AddImmediate.NumExplicitDefs = 1; + + auto Branch = descriptor(BranchOpcode, "TEST_BRANCH"); + rcc::OperandDescriptor Block; + Block.Kinds = {rcc::MachineOperand::Kind::BasicBlock}; + Branch.Operands.push_back(Block); + Branch.Flags.set(rcc::InstructionFlag::Branch); + Branch.Flags.set(rcc::InstructionFlag::Terminator); + Branch.Flags.set(rcc::InstructionFlag::Barrier); + + auto Nop = descriptor(NopOpcode, "TEST_NOP"); + + auto Constant = descriptor(ConstantOpcode, "TEST_CONSTANT"); + Constant.Operands = { + genericRegisterOperand(rcc::RegisterOperandRole::Def), + operand(rcc::MachineOperand::Kind::IntegerImmediate), + }; + Constant.NumExplicitDefs = 1; + Constant.Flags.set(rcc::InstructionFlag::Generic); + Constant.Flags.set(rcc::InstructionFlag::AllowedPreLegalizer); + + auto FloatingConstant = descriptor(FloatingConstantOpcode, "TEST_FCONSTANT"); + FloatingConstant.Operands = { + genericRegisterOperand(rcc::RegisterOperandRole::Def), + operand(rcc::MachineOperand::Kind::FloatingImmediate), + }; + FloatingConstant.NumExplicitDefs = 1; + FloatingConstant.Flags.set(rcc::InstructionFlag::Generic); + FloatingConstant.Flags.set(rcc::InstructionFlag::AllowedPreLegalizer); + + auto Metadata = descriptor(MetadataOpcode, "TEST_METADATA"); + Metadata.Operands = { + operand(rcc::MachineOperand::Kind::Metadata), + operand(rcc::MachineOperand::Kind::Metadata), + operand(rcc::MachineOperand::Kind::CFIIndex), + }; + + auto Load = descriptor(LoadOpcode, "TEST_LOAD"); + Load.Operands = { + registerOperand(rcc::RegisterOperandRole::Def), + registerOperand(rcc::RegisterOperandRole::Use), + }; + Load.NumExplicitDefs = 1; + Load.Flags.set(rcc::InstructionFlag::MayLoad); + Load.RequiresMemoryOperands = true; + + auto InlineAsm = descriptor(InlineAsmOpcode, "INLINEASM"); + InlineAsm.Operands = { + operand(rcc::MachineOperand::Kind::ExternalSymbol), + operand(rcc::MachineOperand::Kind::Immediate), + }; + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Copy)); + Descriptors.push_back(std::move(AddImmediate)); + Descriptors.push_back(std::move(Branch)); + Descriptors.push_back(std::move(Nop)); + Descriptors.push_back(std::move(Constant)); + Descriptors.push_back(std::move(FloatingConstant)); + Descriptors.push_back(std::move(Metadata)); + Descriptors.push_back(std::move(Load)); + Descriptors.push_back(std::move(InlineAsm)); + for (const rcc::InstructionDescriptor &Generic : rcc::genericInstructionDescriptors()) { + if (Generic.InstructionOpcode == rcc::opcode(rcc::GenericOpcode::G_ATOMIC_CMPXCHG) || + Generic.InstructionOpcode == + rcc::opcode(rcc::GenericOpcode::G_ATOMIC_CMPXCHG_WITH_SUCCESS)) { + Descriptors.push_back(Generic); + } + } + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + return rcc::test::createTestTargetDescription(std::move(*Instructions)); +} + +[[nodiscard]] std::unique_ptr<rcc::MachineFunction> createFunction(TestContext &Context, + std::uint64_t ID) { + auto Function = + rcc::MachineFunction::create("parser", rcc::MachineFunctionStableID::fromValue(ID)); + Context.expect(Function.has_value(), "machine function creation must succeed"); + if (!Function) + return nullptr; + return std::move(*Function); +} + +void testTwoPassBodyParsing(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 1); + Context.expect(Target.has_value(), "parser target description must be valid"); + if (!Target) + std::cerr << "error: " << Target.error().message() << '\n'; + if (!Target || !Function) + return; + auto MetadataNode = + Function->metadata().addNode({.Slot = 100, .IsDistinct = false, .Elements = {}}); + Context.expect(MetadataNode.has_value(), "metadata fixture must be valid"); + if (!MetadataNode) + return; + + const std::string Body = R"( +bb.0."entry block" (align 16, bb_id 7.1, call-frame-size 32): + liveins: $g0 + successors: %bb.1."exit block"(2147483648) + %0:gpr64 = TEST_COPY $g0 + %1:gpr64 = frame-setup TEST_ADDI %0, target-flags(test-direct-one, test-bit-four) 4 + %2:_(s128) = TEST_CONSTANT i128 340282366920938463463374607431768211455 + %3:_(f64) = TEST_FCONSTANT double f0x400921FB54442D18 + TEST_METADATA !100, !DIExpression(DW_OP_LLVM_arg, 0), def_cfa $g0, 16, pre-instr-symbol <mcsymbol before>, post-instr-symbol <mcsymbol after>, heap-alloc-marker !0, pcsections !0, mmra !0, cfi-type 17, deactivation-symbol @guard, debug-instr-number 9, debug-location !DILocation(line: 42, column: 7, scope: !0, inlinedAt: !DILocation(line: 8, scope: !0, inlinedAt: !0), isImplicitCode: true, atomGroup: 3, atomRank: 2) + %4:gpr64 = TEST_LOAD %0 :: (load 8 from unknown-address, align 8, addrspace 0, !tbaa !0) + TEST_BRANCH %bb.1."exit block" + +bb.1."exit block" (landing-pad, bbsections Cold): + $g1 = TEST_COPY %1 + TEST_NOP { + TEST_NOP + } +)"; + + rcc::mir::MIParserContext ParserContext; + ParserContext.Metadata.push_back( + {.TextID = 0, .Reference = rcc::MachineMetadataReference::embeddedIR(0)}); + ParserContext.PointerSizeInBits = [](std::uint32_t) -> rcc::Expected<std::uint32_t> { + return 64; + }; + rcc::mir::MIParser Parser(*Function, *Target, "body.mir", Body, ParserContext); + const auto Parsed = Parser.parse(); + const bool ParseSucceeded = + Parsed.has_value() && !Parser.failed() && Parser.diagnostics().empty(); + Context.expect(ParseSucceeded, + "valid MIR body must pass both parsing passes and final verification"); + if (!ParseSucceeded) { + for (const auto &Diagnostic : Parser.diagnostics()) + std::cerr << Diagnostic.Category << ": " << Diagnostic.Message << '\n'; + if (!Parsed) + std::cerr << "error: " << Parsed.error().message() << '\n'; + } + if (!Parsed) + return; + + Context.expect(Function->blockCount() == 2 && Function->allInstructions().size() == 10, + "parser must publish every declared block and instruction"); + rcc::MachineBasicBlock *Entry = Function->firstBlock(); + rcc::MachineBasicBlock *Exit = Entry == nullptr ? nullptr : Entry->next(); + Context.expect(Entry != nullptr && Exit != nullptr && !Exit->next(), + "parsed block layout must retain source order"); + if (Entry == nullptr || Exit == nullptr) + return; + + Context.expect(Entry->attributes().name() == "entry block" && + Entry->attributes().alignment().bytes() == 16 && + Entry->attributes().profileID() == rcc::MachineBasicBlockProfileID{7, 1} && + Entry->attributes().callFrameSize() == 32, + "first-pass block attributes must be retained"); + Context.expect(Exit->attributes().has(rcc::MachineBasicBlockAttribute::LandingPad) && + Exit->attributes().sectionID() == rcc::MachineBasicBlockSectionID::cold(), + "named block flags and section IDs must be retained"); + Context.expect(Entry->liveIns().size() == 1 && Entry->successors().size() == 1 && + Entry->successors().front().Block == Exit && + Entry->successors().front().Probability.rawValue() == 2147483648U, + "live-ins and explicitly weighted successors must be published"); + + const auto Registers = Function->registerInfo().virtualRegisters(); + Context.expect(Registers.size() == 5 && + Registers[0].RegClass == rcc::test::TestRegisterClass::GPR64 && + Registers[1].RegClass == rcc::test::TestRegisterClass::GPR64 && + Registers[2].Type == *rcc::LowLevelType::makeCategoryNeutralScalar(128) && + Registers[3].Type == *rcc::LowLevelType::makeCategoryNeutralScalar(64) && + Registers[4].RegClass == rcc::test::TestRegisterClass::GPR64, + "textual virtual registers must map deterministically to classed MachineIR vregs"); + + const rcc::MachineInstr *Add = Entry->firstInstruction()->next(); + Context.expect(Add->flags().has(rcc::MachineInstrFlag::FrameSetup) && + Add->operandAt(2)->targetFlags() == 0x5U, + "instruction and target operand flags must be preserved"); + + const rcc::MachineInstr *Constant = Add->next(); + const auto *Integer = Constant->operandAt(1)->getIf<rcc::IntegerImmediateOperand>(); + const auto *Floating = Constant->next()->operandAt(1)->getIf<rcc::FloatingImmediateOperand>(); + Context.expect(Integer != nullptr && Integer->Value.bitWidth() == 128 && + Integer->Value.words().size() == 2 && + Integer->Value.words()[0] == std::numeric_limits<std::uint64_t>::max() && + Integer->Value.words()[1] == std::numeric_limits<std::uint64_t>::max() && + Floating != nullptr && + Floating->Bits.semantics() == rcc::FloatSemantics::IEEEDouble && + Floating->Bits.words()[0] == 0x400921FB54442D18ULL, + "typed integer and exact floating-point operands must retain their bit patterns"); + + const rcc::MachineInstr *Metadata = Constant->next()->next(); + const auto *Reference = Metadata->operandAt(0)->getIf<rcc::MetadataOperand>(); + const auto *Expression = Metadata->operandAt(1)->getIf<rcc::MetadataOperand>(); + const auto *CFI = Metadata->operandAt(2)->getIf<rcc::CFIIndexOperand>(); + Context.expect( + Reference != nullptr && Reference->Value.reference() != nullptr && + Reference->Value.reference()->scope() == rcc::MachineMetadataScope::MachineFunction && + Reference->Value.reference()->slot() == 100 && Expression != nullptr && + Expression->Value.expression() != nullptr && + Expression->Value.expression()->elements().size() == 2 && + Expression->Value.expression()->elements()[0] == 0x1005U && CFI != nullptr && + Function->cfiInfo().instruction(CFI->Index) != nullptr && + Function->cfiInfo().instruction(CFI->Index)->operation() == + rcc::MachineCFIOperation::DefCFA && + Metadata->preInstrSymbol() == "before" && Metadata->postInstrSymbol() == "after" && + Metadata->heapAllocMarker().has_value() && Metadata->pcSections().has_value() && + Metadata->mmra().has_value() && Metadata->cfiType() == 17 && + Metadata->deactivationSymbol() == "guard" && Metadata->debugInstrNumber() == 9 && + Metadata->debugLocation().has_value() && + Metadata->debugLocation()->inlineLocation() != nullptr && + Metadata->debugLocation()->inlineLocation()->inlineChain().size() == 2 && + Metadata->debugLocation()->inlineLocation()->inlineChain()[0].Line == 42 && + Metadata->debugLocation()->inlineLocation()->inlineChain()[0].Column == 7 && + Metadata->debugLocation()->inlineLocation()->inlineChain()[0].IsImplicitCode && + Metadata->debugLocation()->inlineLocation()->inlineChain()[0].AtomGroup == 3 && + Metadata->debugLocation()->inlineLocation()->inlineChain()[0].AtomRank == 2 && + Metadata->debugLocation()->inlineLocation()->terminalInlinedAt() == + rcc::MachineMetadataReference::embeddedIR(0), + "metadata, debug expressions, and CFI operands must retain structured semantics"); + + const rcc::MachineInstr *Load = Metadata->next(); + Context.expect(Load->memOperands().size() == 1 && Load->memOperands().front()->isLoad() && + Load->memOperands().front()->description().Size == + *rcc::MemorySize::fixed(8) && + Load->memOperands().front()->description().Align.bytes() == 8 && + Load->memOperands().front()->description().TBAA.has_value(), + "machine memory operands must retain access, size, alignment, and metadata"); + + const rcc::MachineInstr *BundleHeader = Exit->firstInstruction()->next(); + const rcc::MachineInstr *BundleMember = BundleHeader->next(); + Context.expect(BundleHeader->flags().has(rcc::MachineInstrFlag::BundledWithSucc) && + BundleMember->flags().has(rcc::MachineInstrFlag::BundledWithPred), + "instruction bundle braces must produce reciprocal layout flags"); + + auto Printed = rcc::mir::printMachineFunctionBody(*Function, *Target, ParserContext); + Context.expect(Printed.has_value(), "complete MIR body printing must accept the parsed function"); + if (!Printed) { + std::cerr << "error: " << Printed.error().message() << '\n'; + return; + } + Context.expect(Printed->find("TEST_ADDI %0,") != std::string::npos, + "virtual-register uses must not repeat a definition constraint"); + + auto RoundTripFunction = createFunction(Context, 2); + if (!RoundTripFunction) + return; + auto RoundTripMetadata = + RoundTripFunction->metadata().addNode({.Slot = 100, .IsDistinct = false, .Elements = {}}); + Context.expect(RoundTripMetadata.has_value(), "round-trip metadata fixture must be valid"); + if (!RoundTripMetadata) + return; + rcc::mir::MIParser RoundTripParser(*RoundTripFunction, *Target, "round-trip.mir", *Printed, + ParserContext); + const auto RoundTrip = RoundTripParser.parse(); + Context.expect(RoundTrip.has_value() && !RoundTripParser.failed(), + "the complete body printer output must parse successfully"); + if (!RoundTrip) { + std::cerr << *Printed << '\n'; + for (const auto &Diagnostic : RoundTripParser.diagnostics()) + std::cerr << Diagnostic.Category << ": " << Diagnostic.Message << '\n'; + return; + } + auto PrintedAgain = + rcc::mir::printMachineFunctionBody(*RoundTripFunction, *Target, ParserContext); + Context.expect(PrintedAgain.has_value() && *PrintedAgain == *Printed, + "complete MIR body parse-print-parse must be deterministic"); +} + +void testAddressSpacePointerWidthResolver(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 3); + Context.expect(Target.has_value(), "pointer-width parser target must be valid"); + if (!Target || !Function) + return; + + rcc::mir::MIParserContext ParserContext; + ParserContext.PointerSizeInBits = [](std::uint32_t AddressSpace) -> rcc::Expected<std::uint32_t> { + if (AddressSpace == 1) + return 32; + return rcc::makeUnexpected(rcc::ErrorCode::InvalidArgument, + "test data layout has no requested address space"); + }; + rcc::mir::MIParserOptions ParserOptions; + ParserOptions.VerifyAfterParse = false; + rcc::mir::MIParser Parser(*Function, *Target, "pointer-width.mir", + "bb.0:\n %0:_(p1) = TEST_COPY $g0\n", ParserContext, ParserOptions); + auto Parsed = Parser.parse(); + const auto Registers = Function->registerInfo().virtualRegisters(); + auto ExpectedType = rcc::LowLevelType::makePointer(1, 32); + Context.expect(Parsed.has_value() && ExpectedType.has_value() && Registers.size() == 1 && + Registers.front().Type == *ExpectedType, + "MI parser must use the address-space-specific pointer-width resolver"); + if (!Parsed) { + for (const rcc::TextDiagnostic &Diagnostic : Parser.diagnostics()) + std::cerr << Diagnostic.Category << ": " << Diagnostic.Message << '\n'; + std::cerr << "error: " << Parsed.error().message() << '\n'; + } +} + +void expectParseError(TestContext &Context, const rcc::TargetDescription &Target, std::string Body, + std::string_view Message); + +void testCompatibleRegisterClassAndBankAnnotations(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 5); + Context.expect(Target.has_value(), "register-constraint target must be valid"); + if (!Target || !Function) + return; + + rcc::mir::MIParser Parser(*Function, *Target, "compatible-register-constraints.mir", + "bb.0:\n %0:gpr64 = TEST_COPY $g0\n $g1 = TEST_COPY %0:gpr\n"); + auto Parsed = Parser.parse(); + const auto Registers = Function->registerInfo().virtualRegisters(); + Context.expect(Parsed.has_value() && Registers.size() == 1 && + Registers.front().RegClass == rcc::test::TestRegisterClass::GPR64 && + Registers.front().Bank == rcc::test::TestRegisterBank::GPR, + "compatible class and bank annotations must coexist on a virtual register"); + if (!Parsed) { + for (const rcc::TextDiagnostic &Diagnostic : Parser.diagnostics()) + std::cerr << Diagnostic.Category << ": " << Diagnostic.Message << '\n'; + std::cerr << "error: " << Parsed.error().message() << '\n'; + } + + expectParseError(Context, *Target, + "bb.0:\n %0:gpr64 = TEST_COPY $g0\n $g1 = TEST_COPY %0:fpr\n", + "conflicting virtual register bank"); +} + +void testTypedIntegerTruncation(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 12); + Context.expect(Target.has_value(), "typed-integer target description must be valid"); + if (!Target || !Function) + return; + + const std::string Body = R"(bb.0: + %0:_(s64) = TEST_CONSTANT i64 -1 + %1:_(s16) = TEST_CONSTANT i16 -64769 + %2:_(s4) = TEST_CONSTANT i4 -8 + %3:_(s64) = TEST_CONSTANT i64 18446744073709551617 +)"; + rcc::mir::MIParser Parser(*Function, *Target, "typed-integer-truncation.mir", Body); + const auto Parsed = Parser.parse(); + const auto Instructions = Function->allInstructions(); + const auto *MinusOne = !Instructions.empty() + ? Instructions[0]->operandAt(1)->getIf<rcc::IntegerImmediateOperand>() + : nullptr; + const auto *NarrowNegative = + Instructions.size() > 1 ? Instructions[1]->operandAt(1)->getIf<rcc::IntegerImmediateOperand>() + : nullptr; + const auto *NarrowMinimum = + Instructions.size() > 2 ? Instructions[2]->operandAt(1)->getIf<rcc::IntegerImmediateOperand>() + : nullptr; + const auto *WidePositive = + Instructions.size() > 3 ? Instructions[3]->operandAt(1)->getIf<rcc::IntegerImmediateOperand>() + : nullptr; + Context.expect(Parsed.has_value() && MinusOne != nullptr && + MinusOne->Value.words()[0] == std::numeric_limits<std::uint64_t>::max() && + NarrowNegative != nullptr && NarrowNegative->Value.bitWidth() == 16 && + NarrowNegative->Value.words()[0] == 767 && NarrowMinimum != nullptr && + NarrowMinimum->Value.bitWidth() == 4 && NarrowMinimum->Value.words()[0] == 8 && + WidePositive != nullptr && WidePositive->Value.words()[0] == 1, + "typed integer literals must use LLVM width truncation semantics"); + if (!Parsed) { + for (const auto &Diagnostic : Parser.diagnostics()) + std::cerr << Diagnostic.Category << ": " << Diagnostic.Message << '\n'; + std::cerr << "error: " << Parsed.error().message() << '\n'; + } +} + +void testLLVMInlineAsmAndConstantMemorySyntax(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 11); + Context.expect(Target.has_value(), "LLVM MIR syntax target description must be valid"); + if (!Target || !Function) + return; + + const std::string Body = R"(bb.0: + INLINEASM &"", sideeffect mayload inteldialect + %0:gpr64 = TEST_LOAD $g0 :: (load (s64) from `ptr poison`) + %1:gpr64 = TEST_LOAD $g0 :: (load (s64) from unknown-address + 4) +)"; + rcc::mir::MIParserContext ParserContext; + ParserContext.PointerSizeInBits = [](std::uint32_t) -> rcc::Expected<std::uint32_t> { + return 64; + }; + rcc::mir::MIParser Parser(*Function, *Target, "llvm-compatible.mir", Body, ParserContext); + auto Parsed = Parser.parse(); + Context.expect(Parsed.has_value(), "LLVM inline-asm and constant-pointer MMO syntax must parse"); + if (!Parsed) { + for (const auto &Diagnostic : Parser.diagnostics()) + std::cerr << Diagnostic.Category << ": " << Diagnostic.Message << '\n'; + return; + } + + const rcc::MachineInstr *InlineAsm = Function->firstBlock()->firstInstruction(); + const auto *ExtraInfo = InlineAsm->operandAt(1)->getIf<std::int64_t>(); + const rcc::MachineInstr *Load = InlineAsm->next(); + const rcc::MachineMemOperandDesc &Memory = Load->memOperands().front()->description(); + const auto MemoryType = rcc::LowLevelType::makeCategoryNeutralScalar(64); + Context.expect(ExtraInfo != nullptr && *ExtraInfo == 13, + "inline-asm extra-info names must aggregate into the LLVM bit field"); + Context.expect(Memory.Base.kind() == rcc::MemoryBase::Kind::IRConstant && + Memory.Base.constantValue() != nullptr && + Memory.Base.constantValue()->Kind == rcc::GenericConstantKind::Poison && + MemoryType && Memory.MemoryType == *MemoryType, + "quoted LLVM pointer constants must remain structured MMO bases"); + + auto Printed = rcc::mir::printMachineFunctionBody(*Function, *Target, ParserContext); + Context.expect(Printed && + Printed->find("INLINEASM &\"\", sideeffect mayload inteldialect") != + std::string::npos && + Printed->find("from `ptr poison`") != std::string::npos && + Printed->find("from unknown-address + 4") != std::string::npos, + "canonical MIR must print symbolic inline-asm flags and explicit memory bases"); +} + +void testLLVMCompareExchangeMemorySyntax(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 12); + Context.expect(Target.has_value(), "compare-exchange target description must be valid"); + if (!Target || !Function) + return; + + const std::string Body = R"(bb.0: + %0:gpr64(p0) = TEST_COPY $g0 + %1:_(s8) = TEST_CONSTANT i8 0 + %2:_(s8) = TEST_CONSTANT i8 1 + %3:_(s8) = G_ATOMIC_CMPXCHG %0, %1, %2 :: (load store monotonic (s8)) +)"; + rcc::mir::MIParserContext ParserContext; + ParserContext.PointerSizeInBits = [](std::uint32_t) -> rcc::Expected<std::uint32_t> { + return 64; + }; + rcc::mir::MIParser Parser(*Function, *Target, "llvm-cmpxchg.mir", Body, ParserContext); + const auto Parsed = Parser.parse(); + Context.expect(Parsed.has_value(), "LLVM one-ordering compare-exchange MMO syntax must parse"); + if (!Parsed) { + for (const auto &Diagnostic : Parser.diagnostics()) + std::cerr << Diagnostic.Category << ": " << Diagnostic.Message << '\n'; + return; + } + + const rcc::MachineInstr *CompareExchange = Function->allInstructions().back(); + const rcc::MachineMemOperandDesc &Memory = CompareExchange->memOperands().front()->description(); + Context.expect(Memory.AtomicKind == rcc::AtomicOperation::CompareExchange && + Memory.SuccessOrdering == rcc::AtomicOrdering::Monotonic && + Memory.FailureOrdering == rcc::AtomicOrdering::NotAtomic, + "compare-exchange opcode must disambiguate a one-ordering LLVM MMO"); + + auto Printed = rcc::mir::printMachineFunctionBody(*Function, *Target, ParserContext); + Context.expect(Printed && Printed->find(":: (load store monotonic (s8))") != std::string::npos && + Printed->find("unknown-address") == std::string::npos, + "canonical LLVM MMOs must omit an absent zero-offset pointer base"); +} + +void testUnweightedSuccessorNormalization(TestContext &Context) { + auto Target = createTarget(); + auto Function = createFunction(Context, 9); + Context.expect(Target.has_value(), "successor probability target description must be valid"); + if (!Target || !Function) + return; + + const std::string Body = R"(bb.0: + successors: %bb.1, %bb.2 + TEST_NOP + +bb.1: + TEST_NOP + +bb.2: + TEST_NOP +)"; + rcc::mir::MIParser Parser(*Function, *Target, "successors.mir", Body); + const auto Parsed = Parser.parse(); + Context.expect(Parsed.has_value(), "unweighted MIR successors must parse"); + if (!Parsed) + return; + + const rcc::MachineBasicBlock *Entry = Function->firstBlock(); + Context.expect(Entry != nullptr && Entry->successors().size() == 2 && + Entry->successors()[0].Probability.rawValue() == 0x40000000U && + Entry->successors()[1].Probability.rawValue() == 0x40000000U, + "unweighted successors must receive equal normalized probabilities"); +} + +void expectParseError(TestContext &Context, const rcc::TargetDescription &Target, std::string Body, + std::string_view Message) { + auto Function = createFunction(Context, 10); + if (!Function) + return; + rcc::mir::MIParser Parser(*Function, Target, "invalid.mir", std::move(Body)); + const auto Parsed = Parser.parse(); + const bool IsExpectedDiagnostic = + !Parsed && Parser.failed() && Parser.diagnostics().size() == 1 && + Parser.diagnostics().front().Category == "mir-parser" && + Parser.diagnostics().front().Message.find(Message) != std::string::npos; + Context.expect(IsExpectedDiagnostic, + "invalid MIR body must produce one stable parser diagnostic"); + if (!IsExpectedDiagnostic) { + for (const auto &Diagnostic : Parser.diagnostics()) + std::cerr << Diagnostic.Category << ": " << Diagnostic.Message << '\n'; + if (!Parsed) + std::cerr << "error: " << Parsed.error().message() << '\n'; + } +} + +void testDiagnostics(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "diagnostic target description must be valid"); + if (!Target) + std::cerr << "error: " << Target.error().message() << '\n'; + if (!Target) + return; + + expectParseError(Context, *Target, "bb.0:\n TEST_BRANCH %bb.9\n", + "undefined machine basic block"); + expectParseError(Context, *Target, "bb.0:\n TEST_NOP\nbb.0:\n TEST_NOP\n", + "redefinition of machine basic block"); + expectParseError(Context, *Target, + "bb.0:\n %0:gpr64 = TEST_ADDI $g0, " + "target-flags(no-such-flag) 1\n", + "unknown target operand flag"); + expectParseError(Context, *Target, " bb.0:\n TEST_NOP\n", + "basic block definition must be at the start of a line"); +} + +} // namespace + +int main() { + TestContext Context; + testTwoPassBodyParsing(Context); + testAddressSpacePointerWidthResolver(Context); + testCompatibleRegisterClassAndBankAnnotations(Context); + testTypedIntegerTruncation(Context); + testLLVMInlineAsmAndConstantMemorySyntax(Context); + testLLVMCompareExchangeMemorySyntax(Context); + testUnweightedSuccessorNormalization(Context); + testDiagnostics(Context); + return Context.result(); +} diff --git a/backend/unittests/MIR/MIRDocumentTest.cpp b/backend/unittests/MIR/MIRDocumentTest.cpp new file mode 100644 index 0000000..6d84580 --- /dev/null +++ b/backend/unittests/MIR/MIRDocumentTest.cpp @@ -0,0 +1,1148 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/CompilationUnit.h" +#include "rcc/CodeGen/InstructionDescriptor.h" +#include "rcc/CodeGen/MIRParser/MIRParser.h" +#include "rcc/CodeGen/MIRPrinter.h" +#include "rcc/CodeGen/MachineBasicBlock.h" +#include "rcc/CodeGen/MachineCompilationStatus.h" +#include "rcc/CodeGen/MachineConstantPool.h" +#include "rcc/CodeGen/MachineFrameInfo.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/CodeGen/MachineIRBuilder.h" +#include "rcc/CodeGen/MachineJumpTableInfo.h" +#include "rcc/CodeGen/MachineMetadata.h" +#include "rcc/CodeGen/MachineRegisterInfo.h" +#include "rcc/CodeGen/TargetConstant.h" +#include "rcc/CodeGen/TargetDescription.h" +#include "rcc/CodeGen/TargetFunctionState.h" +#include "rcc/CodeGen/TargetMIRTextInfo.h" + +#include <charconv> +#include <cstddef> +#include <cstdint> +#include <iostream> +#include <span> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +constexpr std::uint32_t CopyOpcode = 1; +constexpr std::uint32_t BranchOpcode = 2; +constexpr std::uint32_t LoadOpcode = 3; +constexpr std::uint32_t GlobalOpcode = 4; + +class DocumentTargetState final { +public: + DocumentTargetState(std::uint32_t FrameBase, std::string Mode) + : FrameBase(FrameBase), Mode(std::move(Mode)) {} + + [[nodiscard]] static constexpr rcc::TargetFunctionStateTypeID stableTypeID() noexcept { + return rcc::TargetFunctionStateTypeID::fromValue(0x5243434d49525354ULL); + } + [[nodiscard]] static constexpr std::uint32_t schemaVersion() noexcept { + return 1; + } + [[nodiscard]] rcc::Expected<void> verify() const { + if (FrameBase == 0 || Mode.empty()) + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "document target state is incomplete"); + return {}; + } + [[nodiscard]] rcc::Expected<DocumentTargetState> + clone(const rcc::MachineFunctionCloneContext &) const { + return DocumentTargetState(FrameBase, Mode); + } + [[nodiscard]] std::string print() const { + return Mode + ":" + std::to_string(FrameBase); + } + [[nodiscard]] rcc::Expected<std::vector<std::byte>> encode() const { + std::vector<std::byte> Bytes(4); + for (std::size_t Index = 0; Index < Bytes.size(); ++Index) + Bytes[Index] = static_cast<std::byte>((FrameBase >> (Index * 8U)) & 0xffU); + return Bytes; + } + [[nodiscard]] static rcc::Expected<DocumentTargetState> decode(std::uint32_t Version, + std::span<const std::byte> Bytes) { + if (Version != schemaVersion() || Bytes.size() != 4) + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, + "invalid document target state encoding"); + std::uint32_t Value = 0; + for (std::size_t Index = 0; Index < Bytes.size(); ++Index) + Value |= std::to_integer<std::uint32_t>(Bytes[Index]) << (Index * 8U); + return DocumentTargetState(Value, "test"); + } + + [[nodiscard]] std::uint32_t frameBase() const noexcept { + return FrameBase; + } + [[nodiscard]] std::string_view mode() const noexcept { + return Mode; + } + + friend bool operator==(const DocumentTargetState &Left, + const DocumentTargetState &Right) = default; + +private: + std::uint32_t FrameBase = 0; + std::string Mode; +}; + +class DocumentTargetConstant final { +public: + explicit DocumentTargetConstant(std::uint64_t Value) : Value(Value) {} + + [[nodiscard]] static constexpr rcc::TargetConstantTypeID stableTypeID() noexcept { + return rcc::TargetConstantTypeID::fromValue(0x5243434d49524354ULL); + } + [[nodiscard]] static constexpr std::uint32_t schemaVersion() noexcept { + return 1; + } + [[nodiscard]] rcc::Expected<void> verify() const { + return {}; + } + [[nodiscard]] rcc::Expected<DocumentTargetConstant> clone() const { + return DocumentTargetConstant(Value); + } + [[nodiscard]] std::uint64_t stableHash() const noexcept { + return Value; + } + [[nodiscard]] std::string print() const { + return "test-constant(" + std::to_string(Value) + ")"; + } + [[nodiscard]] rcc::Expected<std::vector<std::byte>> encode() const { + std::vector<std::byte> Bytes(8); + for (std::size_t Index = 0; Index < Bytes.size(); ++Index) + Bytes[Index] = static_cast<std::byte>((Value >> (Index * 8U)) & 0xffU); + return Bytes; + } + [[nodiscard]] rcc::Expected<rcc::TargetConstantEmission> emit() const { + auto Bytes = encode(); + if (!Bytes) + return std::unexpected(Bytes.error()); + return rcc::TargetConstantEmission{.Bytes = std::move(*Bytes), .Fixups = {}}; + } + [[nodiscard]] rcc::Alignment requiredAlignment() const { + return *rcc::Alignment::create(8); + } + [[nodiscard]] std::optional<rcc::TargetRelocationKindID> relocationKind() const { + return std::nullopt; + } + [[nodiscard]] static rcc::Expected<DocumentTargetConstant> parse(std::string_view Text) { + constexpr std::string_view Prefix = "test-constant("; + if (!Text.starts_with(Prefix) || !Text.ends_with(')')) + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, "invalid document target constant"); + Text.remove_prefix(Prefix.size()); + Text.remove_suffix(1); + std::uint64_t Parsed = 0; + const auto [End, Error] = std::from_chars(Text.data(), Text.data() + Text.size(), Parsed, 10); + if (Error != std::errc{} || End != Text.data() + Text.size()) + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, + "invalid document target constant value"); + return DocumentTargetConstant(Parsed); + } + [[nodiscard]] static rcc::Expected<DocumentTargetConstant> + decode(std::uint32_t Version, std::span<const std::byte> Bytes) { + if (Version != schemaVersion() || Bytes.size() != 8) + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, + "invalid document target constant encoding"); + std::uint64_t Parsed = 0; + for (std::size_t Index = 0; Index < Bytes.size(); ++Index) + Parsed |= std::to_integer<std::uint64_t>(Bytes[Index]) << (Index * 8U); + return DocumentTargetConstant(Parsed); + } + + [[nodiscard]] std::uint64_t value() const noexcept { + return Value; + } + + friend bool operator==(const DocumentTargetConstant &Left, + const DocumentTargetConstant &Right) = default; + +private: + std::uint64_t Value = 0; +}; + +[[nodiscard]] rcc::Expected<rcc::TargetFunctionState> +parseTargetState(const rcc::MIRTargetNode &Node) { + const rcc::MIRTargetNode *FrameBase = Node.mappingValue("frame-base"); + const rcc::MIRTargetNode *Mode = Node.mappingValue("mode"); + if (FrameBase == nullptr || Mode == nullptr || + FrameBase->Kind != rcc::MIRTargetNodeKind::Scalar || + Mode->Kind != rcc::MIRTargetNodeKind::Scalar) { + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, + "document target state requires frame-base and mode"); + } + std::uint32_t Value = 0; + const auto [End, Error] = std::from_chars( + FrameBase->Scalar.data(), FrameBase->Scalar.data() + FrameBase->Scalar.size(), Value, 10); + if (Error != std::errc{} || End != FrameBase->Scalar.data() + FrameBase->Scalar.size()) + return rcc::makeUnexpected(rcc::ErrorCode::ParseError, "document target frame-base is invalid"); + return rcc::TargetFunctionState::create(DocumentTargetState(Value, Mode->Scalar)); +} + +[[nodiscard]] rcc::Expected<rcc::MIRTargetNode> +printTargetState(const rcc::TargetFunctionState &State) { + const auto *Value = State.getIf<DocumentTargetState>(); + if (Value == nullptr) + return rcc::makeUnexpected(rcc::ErrorCode::InvalidArgument, "wrong document target state type"); + std::vector<rcc::MIRTargetMappingEntry> Entries; + Entries.push_back({"frame-base", rcc::MIRTargetNode::scalar(std::to_string(Value->frameBase()))}); + Entries.push_back({"mode", rcc::MIRTargetNode::scalar(std::string(Value->mode()))}); + return rcc::MIRTargetNode::mapping(std::move(Entries)); +} + +[[nodiscard]] rcc::Expected<rcc::TargetConstant> parseTargetConstant(std::string_view Text) { + return rcc::TargetConstant::parse<DocumentTargetConstant>(Text); +} + +[[nodiscard]] rcc::Expected<std::string> printTargetConstant(const rcc::TargetConstant &Constant) { + if (Constant.getIf<DocumentTargetConstant>() == nullptr) + return rcc::makeUnexpected(rcc::ErrorCode::InvalidArgument, + "wrong document target constant type"); + return Constant.print(); +} + +class TestContext final { +public: + void expect(bool Condition, std::string_view Message) { + if (Condition) + return; + std::cerr << Message << '\n'; + ++FailureCount; + } + + [[nodiscard]] int result() const noexcept { + return FailureCount == 0 ? 0 : 1; + } + +private: + int FailureCount = 0; +}; + +[[nodiscard]] rcc::OperandDescriptor registerOperand(rcc::RegisterOperandRole Role) { + rcc::OperandDescriptor Operand; + Operand.Kinds = {rcc::MachineOperand::Kind::Register}; + Operand.RegisterRole = Role; + Operand.RegClass = rcc::test::TestRegisterClass::GPR64; + return Operand; +} + +[[nodiscard]] rcc::Expected<rcc::TargetDescription> createTarget() { + rcc::InstructionDescriptor Copy; + Copy.InstructionOpcode = rcc::Opcode::fromValue(CopyOpcode); + Copy.Name = "TEST_COPY"; + Copy.Operands = { + registerOperand(rcc::RegisterOperandRole::Def), + registerOperand(rcc::RegisterOperandRole::Use), + }; + Copy.NumExplicitDefs = 1; + Copy.Flags.set(rcc::InstructionFlag::ProducesCode); + + rcc::InstructionDescriptor Branch; + Branch.InstructionOpcode = rcc::Opcode::fromValue(BranchOpcode); + Branch.Name = "TEST_BRANCH"; + rcc::OperandDescriptor Block; + Block.Kinds = {rcc::MachineOperand::Kind::BasicBlock}; + Branch.Operands.push_back(Block); + Branch.Flags = { + rcc::InstructionFlag::ProducesCode, + rcc::InstructionFlag::Branch, + rcc::InstructionFlag::Terminator, + rcc::InstructionFlag::Barrier, + }; + + rcc::InstructionDescriptor Load; + Load.InstructionOpcode = rcc::Opcode::fromValue(LoadOpcode); + Load.Name = "TEST_LOAD"; + Load.Operands = { + registerOperand(rcc::RegisterOperandRole::Def), + registerOperand(rcc::RegisterOperandRole::Use), + }; + Load.NumExplicitDefs = 1; + Load.Flags = { + rcc::InstructionFlag::ProducesCode, + rcc::InstructionFlag::MayLoad, + }; + Load.RequiresMemoryOperands = true; + + rcc::InstructionDescriptor Global; + Global.InstructionOpcode = rcc::Opcode::fromValue(GlobalOpcode); + Global.Name = "TEST_GLOBAL"; + rcc::OperandDescriptor GlobalOperand; + GlobalOperand.Kinds = { + rcc::MachineOperand::Kind::GlobalSymbol, + rcc::MachineOperand::Kind::ExternalSymbol, + }; + Global.Operands.push_back(GlobalOperand); + Global.Flags.set(rcc::InstructionFlag::ProducesCode); + + std::vector<rcc::InstructionDescriptor> Descriptors; + Descriptors.push_back(std::move(Copy)); + Descriptors.push_back(std::move(Branch)); + Descriptors.push_back(std::move(Load)); + Descriptors.push_back(std::move(Global)); + auto Instructions = rcc::InstructionSet::create(std::move(Descriptors)); + if (!Instructions) + return std::unexpected(Instructions.error()); + auto Registers = rcc::test::createTestTargetRegisterInfo(); + if (!Registers) + return std::unexpected(Registers.error()); + rcc::TargetMIRTextInfoDesc MIRText = rcc::test::createTestTargetMIRTextInfoDescription(); + MIRText.FunctionStateType = DocumentTargetState::stableTypeID(); + MIRText.ParseFunctionState = parseTargetState; + MIRText.PrintFunctionState = printTargetState; + MIRText.ParseTargetConstant = parseTargetConstant; + MIRText.PrintTargetConstant = printTargetConstant; + return rcc::TargetDescription::create(std::move(*Instructions), std::move(*Registers), + std::move(MIRText)); +} + +constexpr std::string_view ValidDocument = R"(--- | + target datalayout = "e-p:64:64" + target triple = "test64-unknown-linux-gnu" + + %pair = type { i8, i64 } + @ordinary = external global i8 + + define void @documented() { + entry: + %slot = alloca i64, align 8 + ret void + } + + !0 = !DILocalVariable(name: "slot", scope: !1) + !1 = distinct !DISubprogram(name: "documented") + !2 = !DILocation(line: 4, column: 2, scope: !1) +--- +name: documented +alignment: 16 +exposesReturnsTwice: true +tracksRegLiveness: true +noPhis: true +isSSA: true +noVRegs: false +registers: + - { id: 7, class: gpr64, preferred-register: '$g1', flags: [ test-vreg-one ] } + - { id: 8, class: gpr64, preferred-register: '' } +liveins: + - { reg: '$g0', virtual-reg: '%7' } + - { reg: '$g1', virtual-reg: '' } +calleeSavedRegisters: [ '$g1' ] +frameInfo: + isFrameAddressTaken: true + stackSize: 32 + maxAlignment: 16 + adjustsStack: true + hasCalls: true + framePointerPolicy: all + stackProtector: '%stack.4' + maxCallFrameSize: 24 + localFrameSize: 8 + savePoint: + - { point: '%bb.0', registers: [ '$g1' ] } +fixedStack: + - { id: 3, type: spill-slot, offset: -16, size: 8, alignment: 8 } +stack: + - { id: 4, name: slot, size: 8, alignment: 8, callee-saved-register: '', local-offset: 0, + debug-info-variable: '!0', + debug-info-expression: '!DIExpression(DW_OP_LLVM_arg, 0, 18446744073709551615)', + debug-info-location: '!DILocation(line: 4, column: 2, scope: !1, isImplicitCode: true)' } + - { id: 5, name: '', size: 4, alignment: 4 } +entry_values: + - { entry-value-register: '$g0', debug-info-variable: '!0', + debug-info-expression: '!DIExpression(DW_OP_stack_value)', debug-info-location: '!2' } +constants: + - id: 5 + value: 'i64 18446744073709551615' + alignment: 8 + - id: 9 + value: 'test-constant(1234)' + alignment: 8 + isTargetSpecific: true + - id: 11 + value: '%pair { i8 1, i64 2 }' +machineFunctionInfo: { frame-base: 64, mode: test } +jumpTable: + kind: block-address + entries: + - { id: 6, blocks: [ '%bb.0' ] } +machineMetadataNodes: + - '!10 = distinct !{!0, !"machine", null}' +body: | + bb.0.entry (%ir-block.entry): + liveins: $g0, $g1 + %7:gpr64 = TEST_LOAD $g0 :: (load syncscope("agent") acquire 8 from %ir.slot, align 8) + TEST_GLOBAL @ordinary + TEST_BRANCH %bb.0.entry +)"; + +constexpr std::string_view HeterogeneousPointerDocument = R"(--- | + target datalayout = "e-p:64:64-p1:32:32" + target triple = "test64-unknown-linux-gnu" + + define void @pointer_layout() { + ret void + } +--- +name: pointer_layout +constants: + - id: 0 + value: 'ptr addrspace(1) null' +body: | + bb.0: + %0:gpr64(p1) = TEST_COPY $g0 +)"; + +void printDiagnostics(const rcc::mir::MIRDocumentParseResult &Parsed) { + for (const rcc::TextDiagnostic &Diagnostic : Parsed.diagnostics()) + std::cerr << Diagnostic.Category << ": " << Diagnostic.Message << '\n'; +} + +void testDocumentTransaction(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "MIR document target must be valid"); + if (!Target) + return; + + auto Parsed = rcc::mir::parseMIRDocument(*Target, "document.mir", std::string(ValidDocument)); + Context.expect(Parsed.succeeded(), "complete MIR document must parse successfully"); + if (!Parsed.succeeded()) { + printDiagnostics(Parsed); + return; + } + + const rcc::mir::MIRDocument *Document = Parsed.document(); + Context.expect( + Document != nullptr && Document->embeddedIR() != nullptr && + Document->unit().functionCount() == 1 && Document->functionDocumentInfo().size() == 1 && + Document->syncScopes().size() == 1 && Document->syncScopes().front().Name == "agent", + "successful parsing must publish the complete document atomically"); + if (Document == nullptr) + return; + const rcc::MachineFunction *Function = Document->unit().function("documented"); + Context.expect(Function != nullptr, "parsed function must be queryable by name"); + if (Function == nullptr) + return; + + Context.expect(Function->registerInfo().virtualRegisters().size() == 2 && + Function->registerInfo().liveIns().size() == 2 && + Function->records().hasCalleeSavedRegisters(), + "register tables must publish into the common MachineIR model"); + Context.expect(Function->frameInfo().liveObjectCount() == 3 && + Function->frameInfo().state().StackSize == 32 && + Function->frameInfo().state().SavePoints.size() == 1, + "frame records and stack objects must publish together"); + const rcc::StackObjectDesc *StackDescription = + Function->frameInfo().object(rcc::FrameIndex::fromValue(1))->description(); + Context.expect(StackDescription != nullptr && StackDescription->DebugInfo.has_value() && + StackDescription->DebugInfo->Expression.has_value() && + StackDescription->DebugInfo->Expression->expression() != nullptr && + StackDescription->DebugInfo->Expression->expression()->elements().size() == + 3 && + StackDescription->DebugInfo->Location.has_value() && + StackDescription->DebugInfo->Location->inlineLocation() != nullptr && + Function->records().entryValues().size() == 1, + "stack and entry-value debug metadata must retain structured values"); + const auto *TargetState = Function->getTargetInfo<DocumentTargetState>(); + const rcc::MachineConstantPoolEntryDesc *TargetConstantDescription = + Function->constantPool().entry(rcc::ConstantPoolIndex::fromValue(1))->description(); + const auto *TargetConstantValue = + TargetConstantDescription == nullptr + ? nullptr + : std::get_if<rcc::TargetConstant>(&TargetConstantDescription->Value); + Context.expect(Function->constantPool().liveEntryCount() == 3 && + Function->jumpTableInfo().liveTableCount() == 1 && + Function->metadata().nodes().size() == 1 && TargetState != nullptr && + TargetState->frameBase() == 64 && TargetConstantValue != nullptr && + TargetConstantValue->getIf<DocumentTargetConstant>() != nullptr && + TargetConstantValue->getIf<DocumentTargetConstant>()->value() == 1234, + "constant, target-state, jump-table, and metadata tables must publish together"); + Context.expect(Function->blockCount() == 1 && Function->allInstructions().size() == 3, + "the body must use the YAML predeclared text slots"); + + auto Printed = rcc::mir::printMIRDocument(*Document, *Target); + Context.expect(Printed.has_value(), "complete MIR document printing must succeed"); + if (!Printed) { + std::cerr << "error: " << Printed.error().message() << '\n'; + return; + } + Context.expect(Printed->find("name: documented\nalignment: 16\n") != std::string::npos && + Printed->find("functionContext: ''") != std::string::npos && + Printed->find("...\n---\nname: documented") != std::string::npos && + Printed->ends_with("...\n"), + "complete MIR must use LLVM-compatible scalars, defaults, and document markers"); + auto RoundTrip = rcc::mir::parseMIRDocument(*Target, "round-trip.mir", std::string(*Printed)); + Context.expect(RoundTrip.succeeded(), "printed complete MIR must parse successfully"); + if (!RoundTrip.succeeded()) { + std::cerr << *Printed; + printDiagnostics(RoundTrip); + return; + } + auto PrintedAgain = rcc::mir::printMIRDocument(*RoundTrip.document(), *Target); + Context.expect(PrintedAgain.has_value() && *PrintedAgain == *Printed, + "complete MIR document printing must be deterministic"); + + auto Readable = rcc::mir::printMIRDocument( + *Document, *Target, + rcc::mir::MIRDocumentPrinterOptions{ + .Mode = rcc::mir::MIPrintMode::Readable, .EmitEmbeddedIR = true, .DataLayout = {}}); + Context.expect(Readable.has_value(), "readable MIR document printing must succeed"); + if (!Readable) + return; + auto ReadableRoundTrip = + rcc::mir::parseMIRDocument(*Target, "readable.mir", std::string(*Readable)); + Context.expect(ReadableRoundTrip.succeeded(), "readable MIR output must parse successfully"); + if (!ReadableRoundTrip.succeeded()) + printDiagnostics(ReadableRoundTrip); + + auto WithoutEmbeddedIR = rcc::mir::printMIRDocument( + *Document, *Target, + rcc::mir::MIRDocumentPrinterOptions{ + .Mode = rcc::mir::MIPrintMode::Complete, .EmitEmbeddedIR = false, .DataLayout = {}}); + Context.expect(!WithoutEmbeddedIR && WithoutEmbeddedIR.error().message().find( + "requires embedded LLVM IR") != std::string_view::npos, + "printer must reject output that omits required embedded LLVM IR"); +} + +void testEmptyYAMLDocuments(TestContext &Context) { + auto Target = createTarget(); + if (!Target) + return; + + constexpr std::string_view Boundary = "\n---\nname: documented"; + std::string WithEmptyDocuments(ValidDocument); + const std::size_t BoundaryPosition = WithEmptyDocuments.find(Boundary); + Context.expect(BoundaryPosition != std::string::npos, + "MIR empty-document fixture must find its document boundary"); + if (BoundaryPosition == std::string::npos) + return; + WithEmptyDocuments.replace(BoundaryPosition, Boundary.size(), "\n---\n---\nname: documented"); + WithEmptyDocuments.insert(0, "---\n"); + WithEmptyDocuments.append("\n...\n---\n"); + auto Parsed = + rcc::mir::parseMIRDocument(*Target, "empty-documents.mir", std::move(WithEmptyDocuments)); + Context.expect(Parsed.succeeded() && Parsed.document() != nullptr && + Parsed.document()->unit().functionCount() == 1, + "MIR must ignore leading, intermediate, and trailing empty YAML documents"); + if (!Parsed.succeeded()) + printDiagnostics(Parsed); + + std::string ExplicitNull(ValidDocument); + ExplicitNull.replace(BoundaryPosition, Boundary.size(), "\n---\nnull\n---\nname: documented"); + auto Rejected = rcc::mir::parseMIRDocument(*Target, "explicit-null.mir", std::move(ExplicitNull)); + Context.expect(!Rejected.succeeded(), + "MIR must not treat an explicit null document as an empty separator"); +} + +void testHeterogeneousPointerLayout(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "heterogeneous-layout target must be valid"); + if (!Target) + return; + + rcc::mir::MIRDocumentParserOptions ParserOptions; + ParserOptions.MachineInstructions.VerifyAfterParse = false; + ParserOptions.VerifyMachineFunctions = false; + auto Parsed = + rcc::mir::parseMIRDocument(*Target, "heterogeneous-layout.mir", + std::string(HeterogeneousPointerDocument), ParserOptions); + Context.expect(Parsed.succeeded(), "heterogeneous pointer-layout MIR must parse successfully"); + if (!Parsed.succeeded()) { + printDiagnostics(Parsed); + return; + } + + const rcc::MachineFunction *Function = Parsed.document()->unit().function("pointer_layout"); + auto PointerType = rcc::LowLevelType::makePointer(1, 32); + const rcc::MachineConstantPoolEntry *Constant = + Function == nullptr ? nullptr + : Function->constantPool().entry(rcc::ConstantPoolIndex::fromValue(0)); + const rcc::MachineConstantPoolEntryDesc *ConstantDescription = + Constant == nullptr ? nullptr : Constant->description(); + Context.expect(Function != nullptr && PointerType.has_value() && + Function->registerInfo().virtualRegisters().size() == 1 && + Function->registerInfo().virtualRegisters().front().Type == *PointerType && + ConstantDescription != nullptr && ConstantDescription->Size == 4, + "embedded data layout must determine p1 register and generic-constant widths"); + + rcc::mir::MIRDocumentParserOptions OverrideOptions; + OverrideOptions.DataLayout = "e-p:64:64-p1:64:64"; + OverrideOptions.MachineInstructions.VerifyAfterParse = false; + OverrideOptions.VerifyMachineFunctions = false; + auto Overridden = rcc::mir::parseMIRDocument( + *Target, "overridden-layout.mir", std::string(HeterogeneousPointerDocument), OverrideOptions); + const rcc::MachineFunction *OverriddenFunction = + Overridden.succeeded() ? Overridden.document()->unit().function("pointer_layout") : nullptr; + const rcc::MachineConstantPoolEntry *OverriddenConstant = + OverriddenFunction == nullptr + ? nullptr + : OverriddenFunction->constantPool().entry(rcc::ConstantPoolIndex::fromValue(0)); + const rcc::MachineConstantPoolEntryDesc *OverriddenConstantDescription = + OverriddenConstant == nullptr ? nullptr : OverriddenConstant->description(); + const auto OverriddenPointerType = rcc::LowLevelType::makePointer(1, 64); + Context.expect(Overridden.succeeded() && OverriddenFunction != nullptr && + OverriddenPointerType.has_value() && + OverriddenFunction->registerInfo().virtualRegisters().size() == 1 && + OverriddenFunction->registerInfo().virtualRegisters().front().Type == + *OverriddenPointerType && + OverriddenConstantDescription != nullptr && + OverriddenConstantDescription->Size == 8, + "explicit target DataLayout must override embedded LLVM IR layout"); + if (!Overridden.succeeded()) + printDiagnostics(Overridden); + + if (Overridden.succeeded()) { + rcc::mir::MIRDocumentPrinterOptions OverridePrinterOptions; + OverridePrinterOptions.EmitEmbeddedIR = false; + OverridePrinterOptions.DataLayout = OverrideOptions.DataLayout; + auto OverriddenOutput = + rcc::mir::printMIRDocument(*Overridden.document(), *Target, OverridePrinterOptions); + Context.expect(OverriddenOutput.has_value(), + "matching target DataLayout must allow omitting overridden embedded IR"); + } + + rcc::mir::MIRDocumentPrinterOptions PrinterOptions; + PrinterOptions.EmitEmbeddedIR = false; + auto WithoutLayout = rcc::mir::printMIRDocument(*Parsed.document(), *Target, PrinterOptions); + Context.expect(!WithoutLayout && WithoutLayout.error().message().find("printer DataLayout") != + std::string_view::npos, + "omitting embedded IR must reject layout-dependent MIR without DataLayout"); + + PrinterOptions.DataLayout = "e-p:64:64-p1:32:32"; + auto Printed = rcc::mir::printMIRDocument(*Parsed.document(), *Target, PrinterOptions); + Context.expect(Printed.has_value() && Printed->find("p1:32") == std::string_view::npos && + Printed->find("p1)") != std::string_view::npos, + "matching explicit DataLayout must emit canonical LLVM p1 MIR syntax"); + if (!Printed) { + std::cerr << "error: " << Printed.error().message() << '\n'; + return; + } + + rcc::mir::MIRDocumentParserOptions ReparseOptions; + ReparseOptions.DataLayout = PrinterOptions.DataLayout; + ReparseOptions.MachineInstructions.VerifyAfterParse = false; + ReparseOptions.VerifyMachineFunctions = false; + auto RoundTrip = rcc::mir::parseMIRDocument(*Target, "heterogeneous-layout-round-trip.mir", + std::string(*Printed), ReparseOptions); + const rcc::MachineFunction *RoundTripFunction = + RoundTrip.succeeded() ? RoundTrip.document()->unit().function("pointer_layout") : nullptr; + Context.expect(RoundTrip.succeeded() && RoundTripFunction != nullptr && + RoundTripFunction->registerInfo().virtualRegisters().size() == 1 && + RoundTripFunction->registerInfo().virtualRegisters().front().Type == + *PointerType, + "explicit DataLayout must preserve p1 type when embedded IR is omitted"); + if (!RoundTrip.succeeded()) + printDiagnostics(RoundTrip); + + PrinterOptions.DataLayout = "e-p:64:64-p1:64:64"; + auto Mismatched = rcc::mir::printMIRDocument(*Parsed.document(), *Target, PrinterOptions); + Context.expect(!Mismatched && Mismatched.error().message().find( + "pointer virtual-register width") != std::string_view::npos, + "mismatched output DataLayout must be rejected when embedded IR is omitted"); +} + +void expectReferenceFailure(TestContext &Context, const rcc::TargetDescription &Target, + std::string_view From, std::string_view To, + std::string_view ExpectedDiagnostic) { + std::string Invalid(ValidDocument); + const std::size_t Position = Invalid.find(From); + Context.expect(Position != std::string::npos, + "reference-validation test mutation must match the fixture"); + if (Position == std::string::npos) + return; + Invalid.replace(Position, From.size(), To); + auto Parsed = rcc::mir::parseMIRDocument(Target, "reference.mir", std::move(Invalid)); + const bool HasExpectedDiagnostic = + !Parsed.succeeded() && Parsed.document() == nullptr && Parsed.diagnostics().size() == 1 && + Parsed.diagnostics().front().Message.find(ExpectedDiagnostic) != std::string::npos; + Context.expect(HasExpectedDiagnostic, + "invalid embedded IR references must fail with their semantic category"); + if (!HasExpectedDiagnostic) + printDiagnostics(Parsed); +} + +void testEmbeddedReferenceValidation(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "reference-validation target must be valid"); + if (!Target) + return; + + expectReferenceFailure(Context, *Target, "from %ir.slot", "from %ir.missing", + "undefined embedded LLVM IR value '%missing'"); + expectReferenceFailure(Context, *Target, "TEST_GLOBAL @ordinary", "TEST_GLOBAL @missing", + "undefined embedded LLVM IR global 'missing'"); + expectReferenceFailure(Context, *Target, "value: 'i64 18446744073709551615'", + "value: 'ptr @missing'", + "generic constant references an undefined embedded LLVM IR global"); + expectReferenceFailure(Context, *Target, "value: 'i64 18446744073709551615'", + "value: 'ptr blockaddress(@documented, %missing)'", + "undefined embedded LLVM IR block 'missing'"); + expectReferenceFailure(Context, *Target, "debug-info-location: '!2'", "debug-info-location: '!0'", + "debug location requires DILocation metadata"); +} + +void expectNoEmbeddedIRReferenceFailure(TestContext &Context, const rcc::TargetDescription &Target, + std::string_view DocumentText, + std::string_view ExpectedDiagnostic) { + auto Parsed = + rcc::mir::parseMIRDocument(Target, "no-embedded-reference.mir", std::string(DocumentText)); + const bool Rejected = + !Parsed.succeeded() && Parsed.document() == nullptr && Parsed.diagnostics().size() == 1 && + Parsed.diagnostics().front().Message.find(ExpectedDiagnostic) != std::string::npos; + Context.expect(Rejected, "IR-specific MIR references without embedded LLVM IR must be rejected"); + if (!Rejected) + printDiagnostics(Parsed); +} + +void testReferencesWithoutEmbeddedIR(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "no-embedded-reference target must be valid"); + if (!Target) + return; + + constexpr std::string_view ExternalDocument = R"(--- +name: external_symbol +body: | + bb.0: + TEST_GLOBAL &runtime_symbol +)"; + auto External = + rcc::mir::parseMIRDocument(*Target, "external-symbol.mir", std::string(ExternalDocument)); + Context.expect(External.succeeded(), + "external symbols must remain usable without embedded LLVM IR"); + if (!External.succeeded()) { + printDiagnostics(External); + return; + } + auto Printed = rcc::mir::printMIRDocument(*External.document(), *Target); + Context.expect(Printed.has_value(), + "external-symbol MIR without embedded IR must remain printable"); + if (!Printed) { + std::cerr << "error: " << Printed.error().message() << '\n'; + } else { + auto RoundTrip = + rcc::mir::parseMIRDocument(*Target, "external-round-trip.mir", std::move(*Printed)); + Context.expect(RoundTrip.succeeded(), + "external-symbol MIR without embedded IR must remain reparseable"); + if (!RoundTrip.succeeded()) + printDiagnostics(RoundTrip); + } + + expectNoEmbeddedIRReferenceFailure(Context, *Target, R"(--- +name: global_reference +body: | + bb.0: + TEST_GLOBAL @ordinary +)", + "requires embedded LLVM IR to resolve global"); + expectNoEmbeddedIRReferenceFailure(Context, *Target, R"(--- +name: ir_block_reference +body: | + bb.0 (%ir-block.entry): +)", + "machine basic block requires embedded LLVM IR"); + expectNoEmbeddedIRReferenceFailure(Context, *Target, R"(--- +name: alloca_reference +stack: + - { id: 0, name: slot, size: 8, alignment: 8 } +body: | + bb.0: +)", + "stack object requires embedded LLVM IR"); + expectNoEmbeddedIRReferenceFailure(Context, *Target, R"(--- +name: ir_value_reference +body: | + bb.0: + $g1 = TEST_LOAD $g0 :: (load 8 from %ir.slot, align 8) +)", + "requires embedded LLVM IR to resolve IR value"); + expectNoEmbeddedIRReferenceFailure( + Context, *Target, R"(--- +name: constant_reference +constants: + - { id: 0, value: 'i64 ptrtoint (ptr @ordinary to i64)', alignment: 8 } +body: | + bb.0: +)", + "generic constant requires embedded LLVM IR to resolve global"); + expectNoEmbeddedIRReferenceFailure(Context, *Target, R"(--- +name: block_address_reference +constants: + - { id: 0, value: 'i64 ptrtoint (ptr blockaddress(@function, %entry) to i64)', alignment: 8 } +body: | + bb.0: +)", + "generic constant blockaddress requires embedded LLVM IR"); +} + +void testPrinterRejectsUnresolvedProgrammaticReferences(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "programmatic-reference target must be valid"); + if (!Target) + return; + + rcc::CompilationUnit Unit; + auto Function = Unit.createMachineFunction("programmatic_reference"); + Context.expect(Function.has_value(), "programmatic document function creation must succeed"); + if (!Function) + return; + auto Block = (*Function)->createBasicBlock(); + auto Appended = Block ? (*Function)->appendBlock(**Block) : rcc::Expected<void>{}; + Context.expect(Block && Appended.has_value(), + "programmatic document block creation must succeed"); + if (!Block || !Appended) + return; + + rcc::MachineIRBuilder Builder(**Function, *Target); + auto Insertion = Builder.setInsertPoint(**Block); + auto Pending = Builder.buildInstr(rcc::Opcode::fromValue(GlobalOpcode)); + auto Added = Pending ? Pending->addGlobalSymbol("ordinary") : rcc::Expected<void>{}; + Context.expect(Insertion && Pending && Added.has_value(), + "programmatic global-reference instruction construction must succeed"); + if (!Insertion || !Pending || !Added) + return; + auto Built = Pending->build(); + Context.expect(Built.has_value(), "programmatic global-reference instruction must build"); + if (!Built) + return; + + rcc::mir::MIRDocument Document(std::move(Unit)); + auto Printed = rcc::mir::printMIRDocument(Document, *Target); + const bool Rejected = + !Printed && Printed.error().message().find("requires embedded LLVM IR to resolve global") != + std::string_view::npos; + Context.expect(Rejected, + "printer must reject programmatic IR-specific references without embedded IR"); + if (!Rejected && !Printed) + std::cerr << "error: " << Printed.error().message() << '\n'; +} + +void testMetadataSlotPriorityAndCanonicalization(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "metadata-priority target must be valid"); + if (!Target) + return; + + std::string CollisionDocument(ValidDocument); + constexpr std::string_view Original = " - '!10 = distinct !{!0, !\"machine\", null}'"; + constexpr std::string_view Replacement = " - '!0 = distinct !{!0, !\"machine\", null}'"; + const std::size_t Position = CollisionDocument.find(Original); + Context.expect(Position != std::string::npos, + "metadata-priority test mutation must match the fixture"); + if (Position == std::string::npos) + return; + CollisionDocument.replace(Position, Original.size(), Replacement); + + auto Parsed = + rcc::mir::parseMIRDocument(*Target, "metadata-priority.mir", std::move(CollisionDocument)); + Context.expect(Parsed.succeeded(), + "machine metadata may reuse an embedded LLVM IR metadata slot"); + if (!Parsed.succeeded()) { + printDiagnostics(Parsed); + return; + } + const rcc::MachineFunction *Function = Parsed.document()->unit().function("documented"); + const auto Nodes = Function == nullptr ? std::span<const rcc::MachineMetadataNode>{} + : Function->metadata().nodes(); + const rcc::MachineMetadataReference *Reference = + Nodes.size() == 1 && Nodes.front().Elements.size() == 3 + ? Nodes.front().Elements.front().reference() + : nullptr; + Context.expect(Function != nullptr && Nodes.size() == 1 && Nodes.front().Slot == 0 && + Reference != nullptr && + *Reference == rcc::MachineMetadataReference::embeddedIR(0), + "bare metadata references must prefer embedded LLVM IR slots"); + + auto Printed = rcc::mir::printMIRDocument(*Parsed.document(), *Target); + const bool Canonicalized = + Printed && Printed->find("'!3 = distinct !{!0, !\"machine\", null}'") != std::string::npos; + Context.expect(Canonicalized, + "printer must assign a noncolliding metadata slot for local metadata"); + if (!Canonicalized) + return; + auto RoundTrip = + rcc::mir::parseMIRDocument(*Target, "metadata-priority-round-trip.mir", std::move(*Printed)); + Context.expect(RoundTrip.succeeded(), + "canonicalized metadata slots must survive a document round-trip"); + if (!RoundTrip.succeeded()) + printDiagnostics(RoundTrip); +} + +void testFixedWidthDocumentFields(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "fixed-width field target must be valid"); + if (!Target) + return; + + constexpr std::string_view Original = "maxCallFrameSize: 24"; + std::string BoundaryDocument(ValidDocument); + const std::size_t BoundaryPosition = BoundaryDocument.find(Original); + Context.expect(BoundaryPosition != std::string::npos, + "fixed-width boundary test mutation must match the fixture"); + if (BoundaryPosition == std::string::npos) + return; + BoundaryDocument.replace(BoundaryPosition, Original.size(), "maxCallFrameSize: 4294967295"); + auto Boundary = + rcc::mir::parseMIRDocument(*Target, "frame-boundary.mir", std::move(BoundaryDocument)); + Context.expect(Boundary.succeeded(), + "maximum 32-bit maxCallFrameSize must remain representable in MIR"); + if (Boundary.succeeded()) { + auto Printed = rcc::mir::printMIRDocument(*Boundary.document(), *Target); + Context.expect(Printed && Printed->find("maxCallFrameSize: 4294967295") != std::string::npos, + "maximum 32-bit maxCallFrameSize must print without narrowing"); + } else { + printDiagnostics(Boundary); + } + + std::string OversizedDocument(ValidDocument); + const std::size_t OversizedPosition = OversizedDocument.find(Original); + if (OversizedPosition == std::string::npos) + return; + OversizedDocument.replace(OversizedPosition, Original.size(), "maxCallFrameSize: 4294967296"); + auto Oversized = + rcc::mir::parseMIRDocument(*Target, "frame-oversized.mir", std::move(OversizedDocument)); + const bool Rejected = + !Oversized.succeeded() && Oversized.document() == nullptr && + Oversized.diagnostics().size() == 1 && + Oversized.diagnostics().front().Message.find("maximum call-frame size") != std::string::npos; + Context.expect(Rejected, + "maxCallFrameSize values beyond the fixed 32-bit MIR range must be rejected"); + if (!Rejected) + printDiagnostics(Oversized); +} + +void testAtomicFailure(TestContext &Context) { + auto Target = createTarget(); + if (!Target) + return; + std::string Invalid(ValidDocument); + Invalid.append("---\nname: second\nunknownField: true\n"); + auto Parsed = rcc::mir::parseMIRDocument(*Target, "atomic.mir", std::move(Invalid)); + Context.expect(!Parsed.succeeded() && Parsed.document() == nullptr && + Parsed.diagnostics().size() == 1 && + Parsed.diagnostics().front().Message.find("unknown machine function key") != + std::string::npos, + "a later function failure must prevent publication of the entire document"); +} + +void testLLVMPropertySemantics(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "property-semantics target must be valid"); + if (!Target) + return; + + constexpr std::string_view PropertyDocument = R"(--- +name: property_state +failedISel: true +failsVerification: true +body: | + bb.0: +)"; + auto Parsed = + rcc::mir::parseMIRDocument(*Target, "properties.mir", std::string(PropertyDocument)); + Context.expect(Parsed.succeeded(), "LLVM MIR pipeline properties must parse"); + if (!Parsed.succeeded()) { + printDiagnostics(Parsed); + return; + } + const rcc::MachineFunction *Function = Parsed.document()->unit().function("property_state"); + Context.expect(Function != nullptr && + Function->properties().has(rcc::MachineFunctionProperty::FailedISel) && + Function->properties().has(rcc::MachineFunctionProperty::FailsVerification) && + Function->compilationStatus().state() == rcc::MachineCompilationState::Success, + "failedISel and failsVerification must be pipeline properties, not run failures"); + + auto Printed = rcc::mir::printMIRDocument(*Parsed.document(), *Target); + Context.expect(Printed.has_value() && Printed->find("failedISel: true") != std::string::npos && + Printed->find("failsVerification: true") != std::string::npos, + "LLVM MIR pipeline properties must print deterministically"); + + constexpr std::string_view InvalidDocument = R"(--- +name: still_checked +failsVerification: true +noVRegs: true +registers: + - { id: 1, class: gpr64 } +body: | + bb.0: +)"; + auto Invalid = + rcc::mir::parseMIRDocument(*Target, "properties-invalid.mir", std::string(InvalidDocument)); + const bool Rejected = !Invalid.succeeded() && Invalid.document() == nullptr && + Invalid.diagnostics().size() == 1 && + Invalid.diagnostics().front().Message.find("NoVRegs") != std::string::npos; + Context.expect(Rejected, "failsVerification must not bypass RCC's mandatory input validation"); + if (!Rejected) + printDiagnostics(Invalid); +} + +void testLiveInCreatesVirtualRegister(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "live-in virtual-register target must be valid"); + if (!Target) + return; + + constexpr std::string_view DocumentText = R"(--- +name: live_in_vreg +liveins: + - { reg: '$g0', virtual-reg: '%0' } +body: | + bb.0: + %0:gpr64 = TEST_COPY $g0 +)"; + auto Parsed = rcc::mir::parseMIRDocument(*Target, "live-in-vreg.mir", std::string(DocumentText)); + const rcc::MachineFunction *Function = + Parsed.succeeded() ? Parsed.document()->unit().function("live_in_vreg") : nullptr; + const auto Registers = Function != nullptr ? Function->registerInfo().virtualRegisters() + : std::span<const rcc::VirtualRegisterInfo>{}; + const auto LiveIns = Function != nullptr ? Function->registerInfo().liveIns() + : std::span<const rcc::PhysicalRegisterLiveIn>{}; + Context.expect(Parsed.succeeded() && Function != nullptr && Registers.size() == 1 && + Registers.front().RegClass.has_value() && LiveIns.size() == 1 && + LiveIns.front().InitialVirtualRegister.has_value(), + "a live-in must create a virtual register before the body constrains it"); + if (!Parsed.succeeded()) { + printDiagnostics(Parsed); + return; + } + + auto Printed = rcc::mir::printMIRDocument(*Parsed.document(), *Target); + Context.expect(Printed.has_value(), + "an implicitly created live-in virtual register must be printable"); + if (!Printed) + return; + auto RoundTrip = rcc::mir::parseMIRDocument(*Target, "live-in-vreg-round-trip.mir", *Printed); + Context.expect(RoundTrip.succeeded(), + "an implicitly created live-in virtual register must round-trip"); + if (!RoundTrip.succeeded()) + printDiagnostics(RoundTrip); +} + +void testBodyDiagnosticLocation(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "body-diagnostic target must be valid"); + if (!Target) + return; + + constexpr std::string_view InvalidDocument = R"(--- +name: body_diagnostic +body: | + bb.0: + UNKNOWN_OPCODE +)"; + const std::size_t ExpectedOffset = InvalidDocument.find("UNKNOWN_OPCODE"); + auto Parsed = + rcc::mir::parseMIRDocument(*Target, "body-diagnostic.mir", std::string(InvalidDocument)); + const bool PointsAtToken = !Parsed.succeeded() && Parsed.document() == nullptr && + Parsed.diagnostics().size() == 1 && + Parsed.diagnostics().front().Category == "mir-body" && + Parsed.diagnostics().front().Range.Begin.Offset == ExpectedOffset; + Context.expect(PointsAtToken, + "body parser errors must retain the source position of the failing token"); + if (!PointsAtToken) + printDiagnostics(Parsed); +} + +void testAnonymousFunctionsAndIRBlocks(TestContext &Context) { + auto Target = createTarget(); + Context.expect(Target.has_value(), "anonymous-function target must be valid"); + if (!Target) + return; + + constexpr std::string_view DocumentText = R"(--- | + define void @0() { + ret void + } + + define void @named() { + ret void + } + + define void @1() { + ret void + } +... +--- +name: '' +body: | + bb.0 (%ir-block.0): +... +--- +name: named +body: | + bb.0 (%ir-block.0): +... +--- +name: '' +body: | + bb.0 (%ir-block.0): +... +)"; + auto Parsed = rcc::mir::parseMIRDocument(*Target, "anonymous.mir", std::string(DocumentText)); + const rcc::MachineFunction *First = + Parsed.succeeded() ? Parsed.document()->unit().functionAt(0) : nullptr; + const rcc::MachineFunction *Named = + Parsed.succeeded() ? Parsed.document()->unit().functionAt(1) : nullptr; + const rcc::MachineFunction *Second = + Parsed.succeeded() ? Parsed.document()->unit().functionAt(2) : nullptr; + Context.expect(Parsed.succeeded() && First != nullptr && First->name().empty() && + Named != nullptr && Named->name() == "named" && Second != nullptr && + Second->name().empty(), + "MIR must match empty names to embedded anonymous functions in document order"); + if (!Parsed.succeeded()) { + printDiagnostics(Parsed); + return; + } + + auto Printed = rcc::mir::printMIRDocument(*Parsed.document(), *Target); + Context.expect(Printed.has_value(), "anonymous MIR functions must remain printable"); + if (!Printed) + return; + const std::size_t FirstAnonymous = Printed->find("name: ''"); + const std::size_t SecondAnonymous = + FirstAnonymous == std::string::npos + ? std::string::npos + : Printed->find("name: ''", FirstAnonymous + std::string_view("name: ''").size()); + Context.expect(FirstAnonymous != std::string::npos && SecondAnonymous != std::string::npos && + Printed->find("%ir-block.0") != std::string::npos, + "anonymous function names and unnamed IR block slots must print canonically"); + + constexpr std::string_view NoIRText = R"(... +--- +name: '' +body: | + bb.0: +... +)"; + auto NoIR = rcc::mir::parseMIRDocument(*Target, "anonymous-no-ir.mir", std::string(NoIRText)); + Context.expect(NoIR.succeeded() && NoIR.document()->unit().functionAt(0) != nullptr && + NoIR.document()->unit().functionAt(0)->name().empty(), + "MIR without embedded IR must accept an anonymous dummy function"); + if (!NoIR.succeeded()) + printDiagnostics(NoIR); +} + +} // namespace + +int main() { + TestContext Context; + testDocumentTransaction(Context); + testEmptyYAMLDocuments(Context); + testHeterogeneousPointerLayout(Context); + testEmbeddedReferenceValidation(Context); + testReferencesWithoutEmbeddedIR(Context); + testPrinterRejectsUnresolvedProgrammaticReferences(Context); + testMetadataSlotPriorityAndCanonicalization(Context); + testFixedWidthDocumentFields(Context); + testAtomicFailure(Context); + testLLVMPropertySemantics(Context); + testLiveInCreatesVirtualRegister(Context); + testBodyDiagnosticLocation(Context); + testAnonymousFunctionsAndIRBlocks(Context); + return Context.result(); +} diff --git a/backend/unittests/Support/CMakeLists.txt b/backend/unittests/Support/CMakeLists.txt new file mode 100644 index 0000000..8c79340 --- /dev/null +++ b/backend/unittests/Support/CMakeLists.txt @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +add_executable(RCCBackendSupportTests ErrorTest.cpp) +rcc_backend_configure_target(RCCBackendSupportTests) +target_link_libraries(RCCBackendSupportTests PRIVATE RCCBackend::Support) + +add_test(NAME RCCBackend.Support.Error COMMAND RCCBackendSupportTests) + +add_executable(RCCBackendTextDiagnosticTests TextDiagnosticTest.cpp) +rcc_backend_configure_target(RCCBackendTextDiagnosticTests) +target_link_libraries(RCCBackendTextDiagnosticTests PRIVATE RCCBackend::Support) + +add_test(NAME RCCBackend.Support.TextDiagnostic COMMAND RCCBackendTextDiagnosticTests) + +add_executable(RCCBackendYAMLTests YAMLTest.cpp) +rcc_backend_configure_target(RCCBackendYAMLTests) +target_link_libraries(RCCBackendYAMLTests PRIVATE RCCBackend::Support) + +add_test(NAME RCCBackend.Support.YAML COMMAND RCCBackendYAMLTests) + +add_executable(RCCBackendSHA256Tests SHA256Test.cpp) +rcc_backend_configure_target(RCCBackendSHA256Tests) +target_link_libraries(RCCBackendSHA256Tests PRIVATE RCCBackend::Support) + +add_test(NAME RCCBackend.Support.SHA256 COMMAND RCCBackendSHA256Tests) + +rcc_backend_register_format_files( + "${CMAKE_CURRENT_SOURCE_DIR}/ErrorTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/SHA256Test.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/TextDiagnosticTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/YAMLTest.cpp" +) diff --git a/backend/unittests/Support/ErrorTest.cpp b/backend/unittests/Support/ErrorTest.cpp new file mode 100644 index 0000000..803ccee --- /dev/null +++ b/backend/unittests/Support/ErrorTest.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/Support/Error.h" + +#include <iostream> +#include <string> + +namespace { + +int fail(const char *Message) { + std::cerr << Message << '\n'; + return 1; +} + +} // namespace + +int main() { + rcc::Expected<int> Value = 42; + if (!Value || *Value != 42) + return fail("Expected must preserve a successful value"); + + rcc::Expected<int> Failure = rcc::makeUnexpected(rcc::ErrorCode::ParseError, "bad MIR"); + if (Failure) + return fail("Expected must preserve an error"); + if (Failure.error().code() != rcc::ErrorCode::ParseError) + return fail("Error must preserve its code"); + if (Failure.error().message() != "bad MIR") + return fail("Error must preserve its message"); + if (rcc::getErrorCodeName(Failure.error().code()) != "parse error") + return fail("Error code must have a stable name"); + if (rcc::getErrorCodeName(rcc::ErrorCode::FrameLayoutFailed) != "frame layout failed") + return fail("frame-layout errors must have a stable name"); + if (rcc::getErrorCodeName(rcc::ErrorCode::SchedulingFailed) != "scheduling failed") + return fail("scheduling errors must have a stable name"); + if (rcc::getErrorCodeName(rcc::ErrorCode::BranchRelaxationFailed) != "branch relaxation failed") + return fail("branch-relaxation errors must have a stable name"); + if (rcc::getErrorCodeName(rcc::ErrorCode::PseudoExpansionFailed) != "pseudo expansion failed") + return fail("pseudo-expansion errors must have a stable name"); + if (rcc::getErrorCodeName(rcc::ErrorCode::FinalEncodabilityFailed) != "final encodability failed") + return fail("final-encodability errors must have a stable name"); + if (rcc::getErrorCodeName(rcc::ErrorCode::CFGOptimizationFailed) != "CFG optimization failed") + return fail("CFG-optimization errors must have a stable name"); + if (rcc::getErrorCodeName(rcc::ErrorCode::AssemblyLoweringFailed) != "assembly lowering failed") + return fail("assembly-lowering errors must have a stable name"); + if (rcc::getErrorCodeName(rcc::ErrorCode::InstructionEncodingFailed) != + "instruction encoding failed") + return fail("instruction-encoding errors must have a stable name"); + if (rcc::getErrorCodeName(rcc::ErrorCode::FixupApplicationFailed) != "fixup application failed") + return fail("fixup-application errors must have a stable name"); + if (rcc::getErrorCodeName(rcc::ErrorCode::RelocationFailed) != "relocation failed") + return fail("relocation errors must have a stable name"); + if (rcc::getErrorCodeName(rcc::ErrorCode::ObjectWritingFailed) != "object writing failed") + return fail("object-writing errors must have a stable name"); + + return 0; +} diff --git a/backend/unittests/Support/SHA256Test.cpp b/backend/unittests/Support/SHA256Test.cpp new file mode 100644 index 0000000..fe7b386 --- /dev/null +++ b/backend/unittests/Support/SHA256Test.cpp @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/Support/SHA256.h" + +#include <cstddef> +#include <iostream> +#include <span> +#include <string> +#include <string_view> + +namespace { + +int fail(const char *Message) { + std::cerr << Message << '\n'; + return 1; +} + +} // namespace + +int main() { + if (rcc::sha256Hex("") != "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") { + return fail("SHA-256 must match the empty-input standard vector"); + } + if (rcc::sha256Hex("abc") != "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad") { + return fail("SHA-256 must match the short standard vector"); + } + constexpr std::string_view MultiBlock = + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"; + if (rcc::sha256Hex(MultiBlock) != + "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1") { + return fail("SHA-256 must match the multi-block standard vector"); + } + + const std::string MillionA(1'000'000, 'a'); + if (rcc::sha256Hex(MillionA) != + "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0") { + return fail("SHA-256 must match the million-byte standard vector"); + } + + const std::string Binary("a\0b", 3); + const auto *Bytes = reinterpret_cast<const std::byte *>(Binary.data()); + if (rcc::sha256Hex(std::span<const std::byte>(Bytes, Binary.size())) != + "59b271ae1bbcb1d31d41929817f4b16fb439eb4f31520b5ad1d5ce98920a7138") { + return fail("SHA-256 must hash embedded null bytes without truncation"); + } + return 0; +} diff --git a/backend/unittests/Support/TextDiagnosticTest.cpp b/backend/unittests/Support/TextDiagnosticTest.cpp new file mode 100644 index 0000000..71d02b2 --- /dev/null +++ b/backend/unittests/Support/TextDiagnosticTest.cpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/Support/TextDiagnostic.h" + +#include <iostream> +#include <string> + +namespace { + +int fail(const char *Message) { + std::cerr << Message << '\n'; + return 1; +} + +} // namespace + +int main() { + rcc::TextSource Source("input.mir", "first\r\nsecond line\n"); + if (Source.lineCount() != 3) + return fail("TextSource must index every physical line"); + if (Source.line(1) != "first" || Source.line(2) != "second line" || !Source.line(3).empty()) + return fail("TextSource must expose lines without line endings"); + + const rcc::TextRange Range = Source.range(9, 15); + if (Range.Begin.Line != 2 || Range.Begin.Column != 3 || Range.End.Line != 2 || + Range.End.Column != 9) + return fail("TextSource must map byte offsets to one-based line and column values"); + + const rcc::TextDiagnostic Diagnostic{ + .Severity = rcc::DiagnosticSeverity::Error, + .Category = "yaml", + .Message = "invalid scalar", + .SourceName = "input.mir", + .Range = Range, + }; + const std::string Formatted = rcc::formatTextDiagnostic(Diagnostic, Source); + if (Formatted != "error: input.mir:2:3: invalid scalar\nsecond line\n ^~~~~~") + return fail("Text diagnostics must have deterministic source rendering"); + + if (rcc::getDiagnosticSeverityName(rcc::DiagnosticSeverity::Warning) != "warning") + return fail("Diagnostic severities must have stable names"); + return 0; +} diff --git a/backend/unittests/Support/YAMLTest.cpp b/backend/unittests/Support/YAMLTest.cpp new file mode 100644 index 0000000..74f7bf2 --- /dev/null +++ b/backend/unittests/Support/YAMLTest.cpp @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/Support/YAML.h" + +#include <iostream> +#include <string> +#include <string_view> + +namespace { + +int fail(const char *Message) { + std::cerr << Message << '\n'; + return 1; +} + +const rcc::yaml::Node *mappingValue(const rcc::yaml::Stream &Stream, const rcc::yaml::Node &Mapping, + std::string_view Key) { + const std::optional<rcc::yaml::NodeID> Value = Mapping.mappingValue(Key); + return Value ? Stream.node(*Value) : nullptr; +} + +int testMIRShape() { + const std::string Input = + "...\n" + "--- |\n" + " define void @f() {\n" + " entry:\n" + " ret void\n" + " }\n" + "---\n" + "name: f\n" + "alignment: 16\n" + "registers:\n" + " - { id: 0, class: gpr, flags: [foo, 'bar',], preferred-register:, }\n" + "frameInfo:\n" + " hasCalls: true\n" + "jumpTable:\n" + " blocks: [ '%bb.0',\n" + " '%bb.1', ]\n" + "callSites:\n" + " - { bb: 0, offset: 4, fwdArgRegs:\n" + " - { arg: 0, reg: '$x1' }\n" + " - { arg: 1, reg: '$x2' } }\n" + "body: |-\n" + " bb.0:\n" + " RET\n" + "...\n"; + + rcc::yaml::ParseResult Result = rcc::yaml::parse("input.mir", Input); + const rcc::yaml::Stream *ParsedStream = Result.stream(); + if (!Result.succeeded() || ParsedStream == nullptr) + return fail("YAML parser must accept the LLVM MIR document shape"); + + const rcc::yaml::Stream &Stream = *ParsedStream; + if (Stream.documents().size() != 2) + return fail("YAML parser must retain every document in input order"); + + const rcc::yaml::Node *IR = Stream.node(Stream.documents()[0].Root); + if (IR == nullptr || IR->kind() != rcc::yaml::NodeKind::Scalar || + IR->scalarStyle() != rcc::yaml::ScalarStyle::Literal || + IR->scalar() != "define void @f() {\nentry:\n ret void\n}\n") + return fail("YAML parser must decode the embedded LLVM IR block scalar"); + + const rcc::yaml::Node *Function = Stream.node(Stream.documents()[1].Root); + if (Function == nullptr || Function->kind() != rcc::yaml::NodeKind::Mapping) + return fail("YAML parser must represent a MachineFunction document as a mapping"); + + const rcc::yaml::Node *Name = mappingValue(Stream, *Function, "name"); + const rcc::yaml::Node *Registers = mappingValue(Stream, *Function, "registers"); + const rcc::yaml::Node *FrameInfo = mappingValue(Stream, *Function, "frameInfo"); + const rcc::yaml::Node *JumpTable = mappingValue(Stream, *Function, "jumpTable"); + const rcc::yaml::Node *CallSites = mappingValue(Stream, *Function, "callSites"); + const rcc::yaml::Node *Body = mappingValue(Stream, *Function, "body"); + if (Name == nullptr || Name->scalar() != "f") + return fail("YAML mapping lookup must preserve scalar values"); + if (Registers == nullptr || Registers->sequence().size() != 1) + return fail("YAML parser must decode block and flow collections together"); + if (FrameInfo == nullptr || FrameInfo->kind() != rcc::yaml::NodeKind::Mapping) + return fail("YAML parser must decode nested block mappings"); + const rcc::yaml::Node *Blocks = + JumpTable == nullptr ? nullptr : mappingValue(Stream, *JumpTable, "blocks"); + if (Blocks == nullptr || Blocks->sequence().size() != 2) + return fail("YAML parser must decode line-wrapped flow sequences"); + const rcc::yaml::Node *CallSite = + CallSites == nullptr ? nullptr : Stream.node(CallSites->sequence()[0]); + const rcc::yaml::Node *Forwarded = + CallSite == nullptr ? nullptr : mappingValue(Stream, *CallSite, "fwdArgRegs"); + if (Forwarded == nullptr || Forwarded->sequence().size() != 2) + return fail("YAML parser must decode block sequences nested in line-wrapped flow mappings"); + if (Body == nullptr || Body->scalar() != "bb.0:\n RET") + return fail("YAML block scalar strip chomping must remove trailing line breaks"); + + const rcc::yaml::Node *Register = Stream.node(Registers->sequence()[0]); + const rcc::yaml::Node *Flags = + Register == nullptr ? nullptr : mappingValue(Stream, *Register, "flags"); + if (Register == nullptr || Register->collectionStyle() != rcc::yaml::CollectionStyle::Flow || + Flags == nullptr || Flags->sequence().size() != 2) + return fail("YAML flow mapping and sequence structure must be retained"); + return 0; +} + +int testScalarRules() { + const std::string Input = "plain: value # comment\n" + "single: 'it''s YAML'\n" + "double: \"line\\n\\u03bb\"\n" + "nothing: null\n" + "literal: |-\n" + " \tcontent\n" + "hash: |-\n" + " # scalar content\n" + "folded: >-\n" + " one\n" + " two\n"; + rcc::yaml::ParseResult Result = rcc::yaml::parse("scalars.yaml", Input); + const rcc::yaml::Stream *ParsedStream = Result.stream(); + if (!Result.succeeded() || ParsedStream == nullptr) + return fail("YAML parser must accept supported scalar forms"); + + const rcc::yaml::Stream &Stream = *ParsedStream; + const rcc::yaml::Node *Root = Stream.node(Stream.documents()[0].Root); + if (Root == nullptr) + return fail("YAML scalar test must have a root"); + const rcc::yaml::Node *Single = mappingValue(Stream, *Root, "single"); + const rcc::yaml::Node *Double = mappingValue(Stream, *Root, "double"); + const rcc::yaml::Node *Nothing = mappingValue(Stream, *Root, "nothing"); + const rcc::yaml::Node *Literal = mappingValue(Stream, *Root, "literal"); + const rcc::yaml::Node *Hash = mappingValue(Stream, *Root, "hash"); + const rcc::yaml::Node *Folded = mappingValue(Stream, *Root, "folded"); + if (Single == nullptr || Single->scalar() != "it's YAML") + return fail("YAML single quotes must decode doubled quotes"); + if (Double == nullptr || Double->scalar() != "line\n\xCE\xBB") + return fail("YAML double quotes must decode escapes and Unicode"); + if (Nothing == nullptr || Nothing->kind() != rcc::yaml::NodeKind::Null) + return fail("YAML null spellings must produce null nodes"); + if (Literal == nullptr || Literal->scalar() != "\tcontent") + return fail("YAML block scalars must preserve tab characters in scalar content"); + if (Hash == nullptr || Hash->scalar() != "# scalar content") + return fail("YAML block scalar content must not interpret '#' as a comment"); + if (Folded == nullptr || Folded->scalar() != "one two") + return fail("YAML folded scalars must fold ordinary line breaks"); + return 0; +} + +int testNestedSequenceMapping() { + const std::string Input = "savePoint:\n" + " - point: '%bb.1'\n" + " registers:\n" + " - '$x1'\n" + " - '$x2'\n"; + rcc::yaml::ParseResult Result = rcc::yaml::parse("nested.yaml", Input); + const rcc::yaml::Stream *ParsedStream = Result.stream(); + if (!Result.succeeded() || ParsedStream == nullptr) + return fail("YAML parser must accept sequence entries that start block mappings"); + + const rcc::yaml::Stream &Stream = *ParsedStream; + const rcc::yaml::Node *Root = Stream.node(Stream.documents()[0].Root); + const rcc::yaml::Node *SavePoints = + Root == nullptr ? nullptr : mappingValue(Stream, *Root, "savePoint"); + const rcc::yaml::Node *SavePoint = + SavePoints == nullptr ? nullptr : Stream.node(SavePoints->sequence()[0]); + const rcc::yaml::Node *Registers = + SavePoint == nullptr ? nullptr : mappingValue(Stream, *SavePoint, "registers"); + if (Registers == nullptr || Registers->sequence().size() != 2) + return fail("YAML nested sequence mappings must retain continuation keys"); + return 0; +} + +int testDiagnostics() { + rcc::yaml::ParseResult Duplicate = rcc::yaml::parse("duplicate.yaml", "name: a\nname: b\n"); + if (Duplicate.succeeded() || Duplicate.diagnostics().size() != 1 || + Duplicate.diagnostics()[0].Message != "duplicate YAML mapping key 'name'" || + Duplicate.diagnostics()[0].Range.Begin.Line != 2) + return fail("YAML parser must reject duplicate keys at the second definition"); + + rcc::yaml::ParseResult BadIndent = rcc::yaml::parse("tab.yaml", "root:\n\tchild: value\n"); + if (BadIndent.succeeded() || BadIndent.diagnostics().empty() || + BadIndent.diagnostics()[0].Message != "tab characters are not allowed in YAML indentation") + return fail("YAML parser must reject tab indentation"); + + rcc::yaml::ParseResult BadFlow = rcc::yaml::parse("flow.yaml", "values: [one two\n"); + if (BadFlow.succeeded() || BadFlow.diagnostics().empty() || + BadFlow.diagnostics()[0].Message != "unterminated YAML flow sequence") + return fail("YAML parser must diagnose unterminated flow collections"); + + rcc::yaml::ParseResult BadEnd = rcc::yaml::parse("end.yaml", "---\nname: f\n... garbage\n"); + if (BadEnd.succeeded() || BadEnd.diagnostics().empty() || + BadEnd.diagnostics()[0].Message != "document end marker may only be followed by a comment") + return fail("YAML parser must reject content after a document end marker"); + + rcc::yaml::ParseResult Directive = rcc::yaml::parse("directive.yaml", "%YAML 1.2\n---\na\n"); + if (Directive.succeeded() || Directive.diagnostics().empty() || + Directive.diagnostics()[0].Message != "YAML directives are not supported") + return fail("YAML parser must explicitly reject unsupported directives"); + + rcc::yaml::ParseResult Alias = rcc::yaml::parse("alias.yaml", "value: *shared\n"); + if (Alias.succeeded() || Alias.diagnostics().empty() || + Alias.diagnostics()[0].Message != + "YAML anchors, aliases, tags, and directives are not supported") + return fail("YAML parser must explicitly reject unsupported aliases"); + + const std::string NulInput("name:\0f", 7); + rcc::yaml::ParseResult Nul = rcc::yaml::parse("nul.yaml", NulInput); + if (Nul.succeeded() || Nul.diagnostics().empty() || + Nul.diagnostics()[0].Message != "NUL bytes are not allowed in YAML input") + return fail("YAML parser must reject embedded NUL bytes"); + + rcc::yaml::ParseOptions Limits; + Limits.MaximumDepth = 2; + rcc::yaml::ParseResult Deep = rcc::yaml::parse("deep.yaml", "a:\n b:\n c: value\n", Limits); + if (Deep.succeeded() || Deep.diagnostics().empty() || + Deep.diagnostics()[0].Message != "maximum YAML nesting depth exceeded") + return fail("YAML parser must enforce its nesting limit"); + + Limits.MaximumDepth = 128; + Limits.MaximumNodes = 1; + rcc::yaml::ParseResult ManyNodes = rcc::yaml::parse("nodes.yaml", "name: f\n", Limits); + if (ManyNodes.succeeded() || ManyNodes.diagnostics().empty() || + ManyNodes.diagnostics()[0].Message != "maximum YAML node count exceeded") + return fail("YAML parser must enforce its node limit"); + return 0; +} + +} // namespace + +int main() { + if (const int Result = testMIRShape(); Result != 0) + return Result; + if (const int Result = testScalarRules(); Result != 0) + return Result; + if (const int Result = testNestedSequenceMapping(); Result != 0) + return Result; + return testDiagnostics(); +} 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 diff --git a/backend/unittests/Tooling/ArtifactStoreTest.cpp b/backend/unittests/Tooling/ArtifactStoreTest.cpp new file mode 100644 index 0000000..3f9febb --- /dev/null +++ b/backend/unittests/Tooling/ArtifactStoreTest.cpp @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/Tooling/ArtifactStore.h" + +#include "ToolingTestSupport.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +#include <cstdint> +#include <filesystem> +#include <span> +#include <string> +#include <utility> +#include <vector> + +namespace { + +[[nodiscard]] std::vector<std::uint8_t> bytes(std::string_view Text) { + const auto *Begin = reinterpret_cast<const std::uint8_t *>(Text.data()); + return std::vector<std::uint8_t>(Begin, Begin + Text.size()); +} + +[[nodiscard]] std::filesystem::path cacheEntry(const std::filesystem::path &Root, + std::string_view Key) { + return Root / std::string(Key.substr(0, 2)) / std::string(Key); +} + +[[nodiscard]] bool isCacheMiss(const rcc::Expected<rcc::tooling::CacheEntry> &Result) { + return !Result && Result.error().code() == rcc::ErrorCode::CacheMiss; +} + +} // namespace + +int main() { + auto Target = rcc::riscv::createTargetDescription(); + if (!Target) + return tooling_test::fail(Target.error().message()); + const rcc::tooling::BackendInvocation Invocation = rcc::tooling::defaultRV64GCInvocation(); + auto Input = tooling_test::loadCanonicalInput("RISCVGenericPipeline.mir", *Target, Invocation); + if (!Input) + return tooling_test::fail(Input.error().message()); + auto Temporary = tooling_test::TemporaryDirectory::create("rcc-artifact-store-test"); + if (!Temporary) + return tooling_test::fail(Temporary.error().message()); + + const std::vector<std::uint8_t> Assembly = bytes(".text\nret\n"); + auto CacheManifest = rcc::tooling::createBackendManifest( + rcc::tooling::ManifestKind::Cache, rcc::tooling::ArtifactKind::Assembly, Invocation, *Target, + Input->Text, std::span<const std::uint8_t>(Assembly)); + if (!CacheManifest) + return tooling_test::fail(CacheManifest.error().message()); + const std::filesystem::path CacheRoot = Temporary->path() / "cache"; + auto Stored = rcc::tooling::storeCacheEntry(CacheRoot, *CacheManifest, Input->Text, Assembly); + if (!Stored) + return tooling_test::fail(Stored.error().message()); + auto Loaded = rcc::tooling::loadCacheEntry(CacheRoot, rcc::tooling::ArtifactKind::Assembly, + Invocation, *Target, Input->Text); + if (!Loaded || Loaded->Manifest != *CacheManifest || Loaded->InputMIR != Input->Text || + Loaded->Artifact != Assembly || Loaded->ParsedArtifact) { + return tooling_test::fail("cache hit must reload and verify every stored artifact fact"); + } + if (rcc::tooling::storeCacheEntry(CacheRoot, *CacheManifest, Input->Text, Assembly)) + return tooling_test::fail("cache publication must not overwrite an existing entry"); + auto WrongKind = rcc::tooling::loadCacheEntry(CacheRoot, rcc::tooling::ArtifactKind::FinalMIR, + Invocation, *Target, Input->Text); + if (!isCacheMiss(WrongKind)) + return tooling_test::fail("cache lookup must include the requested artifact kind"); + auto NonBoundary = rcc::tooling::loadCacheEntry( + CacheRoot, rcc::tooling::ArtifactKind::LegalizedMIR, Invocation, *Target, Input->Text); + if (!isCacheMiss(NonBoundary)) + return tooling_test::fail("cache lookup must reject non-boundary MIR stages"); + rcc::tooling::BackendInvocation OtherInvocation = Invocation; + OtherInvocation.Determinism.ThreadCount = 2; + auto OtherInvocationResult = rcc::tooling::loadCacheEntry( + CacheRoot, rcc::tooling::ArtifactKind::Assembly, OtherInvocation, *Target, Input->Text); + if (!isCacheMiss(OtherInvocationResult)) + return tooling_test::fail("cache lookup must include deterministic invocation facts"); + + const std::vector<std::uint8_t> MIRBytes = bytes(Input->Text); + auto MIRManifest = rcc::tooling::createBackendManifest( + rcc::tooling::ManifestKind::Cache, rcc::tooling::ArtifactKind::PreLegalizerMIR, Invocation, + *Target, Input->Text, std::span<const std::uint8_t>(MIRBytes)); + if (!MIRManifest) + return tooling_test::fail(MIRManifest.error().message()); + const std::filesystem::path MIRCacheRoot = Temporary->path() / "mir-cache"; + Stored = rcc::tooling::storeCacheEntry(MIRCacheRoot, *MIRManifest, Input->Text, MIRBytes); + if (!Stored) + return tooling_test::fail(Stored.error().message()); + auto LoadedMIR = rcc::tooling::loadCacheEntry( + MIRCacheRoot, rcc::tooling::ArtifactKind::PreLegalizerMIR, Invocation, *Target, Input->Text); + if (!LoadedMIR || !LoadedMIR->ParsedArtifact || LoadedMIR->ParsedArtifact->Text != Input->Text) { + return tooling_test::fail("cached MIR artifacts must be parsed and canonicalized on reload"); + } + + const auto StoreCorruptible = [&](std::string_view Name) -> rcc::Expected<std::filesystem::path> { + const std::filesystem::path Root = Temporary->path() / Name; + auto Result = rcc::tooling::storeCacheEntry(Root, *CacheManifest, Input->Text, Assembly); + if (!Result) + return std::unexpected(Result.error()); + return cacheEntry(Root, CacheManifest->CacheKey); + }; + + auto CorruptArtifactPath = StoreCorruptible("corrupt-artifact-cache"); + if (!CorruptArtifactPath) + return tooling_test::fail(CorruptArtifactPath.error().message()); + auto Written = tooling_test::writeText(*CorruptArtifactPath / "artifact.s", "corrupt"); + if (!Written) + return tooling_test::fail(Written.error().message()); + auto CorruptArtifact = rcc::tooling::loadCacheEntry(Temporary->path() / "corrupt-artifact-cache", + rcc::tooling::ArtifactKind::Assembly, + Invocation, *Target, Input->Text); + if (!isCacheMiss(CorruptArtifact)) + return tooling_test::fail("cache reload must reject a corrupted artifact"); + + auto CorruptInputPath = StoreCorruptible("corrupt-input-cache"); + if (!CorruptInputPath) + return tooling_test::fail(CorruptInputPath.error().message()); + Written = tooling_test::writeText(*CorruptInputPath / "input.mir", "corrupt"); + if (!Written) + return tooling_test::fail(Written.error().message()); + auto CorruptInput = rcc::tooling::loadCacheEntry(Temporary->path() / "corrupt-input-cache", + rcc::tooling::ArtifactKind::Assembly, Invocation, + *Target, Input->Text); + if (!isCacheMiss(CorruptInput)) + return tooling_test::fail("cache reload must reject a corrupted canonical input"); + + auto CorruptManifestPath = StoreCorruptible("corrupt-manifest-cache"); + if (!CorruptManifestPath) + return tooling_test::fail(CorruptManifestPath.error().message()); + Written = tooling_test::writeText(*CorruptManifestPath / "manifest.yaml", "---\ninvalid: true\n"); + if (!Written) + return tooling_test::fail(Written.error().message()); + auto CorruptManifest = rcc::tooling::loadCacheEntry(Temporary->path() / "corrupt-manifest-cache", + rcc::tooling::ArtifactKind::Assembly, + Invocation, *Target, Input->Text); + if (!isCacheMiss(CorruptManifest)) + return tooling_test::fail("cache reload must reject a corrupted manifest"); + + std::vector<std::uint8_t> WrongArtifact = Assembly; + WrongArtifact.push_back(0); + if (rcc::tooling::storeCacheEntry(Temporary->path() / "wrong-artifact-cache", *CacheManifest, + Input->Text, WrongArtifact)) { + return tooling_test::fail("cache store must reject bytes that disagree with the manifest"); + } + + auto ReproducerManifest = rcc::tooling::createBackendManifest( + rcc::tooling::ManifestKind::Reproducer, rcc::tooling::ArtifactKind::Assembly, Invocation, + *Target, Input->Text); + if (!ReproducerManifest) + return tooling_test::fail(ReproducerManifest.error().message()); + const std::filesystem::path ReproducerPath = Temporary->path() / "reproducer"; + auto ReproducerWritten = + rcc::tooling::writeReproducer(ReproducerPath, *ReproducerManifest, Input->Text); + if (!ReproducerWritten) + return tooling_test::fail(ReproducerWritten.error().message()); + auto Reproducer = rcc::tooling::loadReproducer(ReproducerPath, *Target); + if (!Reproducer || Reproducer->Manifest != *ReproducerManifest || + Reproducer->Input.Text != Input->Text) { + return tooling_test::fail("reproducer reload must verify manifest and canonical MIR identity"); + } + if (rcc::tooling::writeReproducer(ReproducerPath, *ReproducerManifest, Input->Text)) + return tooling_test::fail("reproducer publication must not overwrite an existing package"); + + rcc::tooling::StructuredDiagnostic Diagnostic; + Diagnostic.Category = "verification error"; + Diagnostic.Stage = "regbank-mir"; + Diagnostic.Pass = "InstructionSelect"; + Diagnostic.Function = "generic_pipeline"; + Diagnostic.Rule = "machine-pass"; + Diagnostic.Message = "required selection Action was rejected"; + auto Updated = rcc::tooling::updateReproducerDiagnostics(ReproducerPath, {Diagnostic}); + if (!Updated) + return tooling_test::fail(Updated.error().message()); + Reproducer = rcc::tooling::loadReproducer(ReproducerPath, *Target); + if (!Reproducer || Reproducer->Manifest.Diagnostics != + std::vector<rcc::tooling::StructuredDiagnostic>({Diagnostic})) { + return tooling_test::fail("reproducer diagnostic update must round-trip atomically"); + } + for (const std::filesystem::directory_entry &Entry : + std::filesystem::directory_iterator(ReproducerPath)) { + if (Entry.path().filename().string().contains(".tmp.")) + return tooling_test::fail("reproducer diagnostic update must not leave temporary files"); + } + + Written = tooling_test::writeText(ReproducerPath / "input.mir", "corrupt"); + if (!Written) + return tooling_test::fail(Written.error().message()); + if (rcc::tooling::loadReproducer(ReproducerPath, *Target)) + return tooling_test::fail("reproducer reload must reject a modified input"); + if (rcc::tooling::updateReproducerDiagnostics(ReproducerPath, {Diagnostic})) + return tooling_test::fail("diagnostic update must reject a modified reproducer input"); + + return 0; +} diff --git a/backend/unittests/Tooling/BackendManifestTest.cpp b/backend/unittests/Tooling/BackendManifestTest.cpp new file mode 100644 index 0000000..2e2a04c --- /dev/null +++ b/backend/unittests/Tooling/BackendManifestTest.cpp @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/Tooling/BackendManifest.h" + +#include "ToolingTestSupport.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +#include <algorithm> +#include <cstdint> +#include <optional> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +using rcc::tooling::BackendInvocation; + +[[nodiscard]] bool keyDiffers(const BackendInvocation &Base, BackendInvocation Changed, + std::string_view Input, std::string_view Fingerprint) { + auto Canonicalized = Changed.canonicalizeAndVerify(); + if (!Canonicalized) + return false; + auto BaseKey = rcc::tooling::computeCacheKey(Input, Base, Fingerprint); + auto ChangedKey = rcc::tooling::computeCacheKey(Input, Changed, Fingerprint); + return BaseKey && ChangedKey && *BaseKey != *ChangedKey; +} + +} // namespace + +int main() { + auto Target = rcc::riscv::createTargetDescription(); + if (!Target) + return tooling_test::fail(Target.error().message()); + const BackendInvocation Base = rcc::tooling::defaultRV64GCInvocation(); + auto Fingerprint = rcc::tooling::computeTargetDescriptionFingerprint(*Target); + if (!Fingerprint || Fingerprint->size() != 64) + return tooling_test::fail("target description fingerprint must be a SHA-256 digest"); + auto RepeatedFingerprint = rcc::tooling::computeTargetDescriptionFingerprint(*Target); + if (!RepeatedFingerprint || *RepeatedFingerprint != *Fingerprint) + return tooling_test::fail("target description fingerprint must be deterministic"); + + BackendInvocation Unordered = Base; + std::ranges::reverse(Unordered.Features); + std::ranges::reverse(Unordered.TargetOptions); + Unordered.EnabledActions = {"spill", "coalesce"}; + auto Canonicalized = Unordered.canonicalizeAndVerify(); + if (!Canonicalized || !std::ranges::is_sorted(Unordered.Features) || + !std::ranges::is_sorted(Unordered.TargetOptions, {}, &rcc::tooling::BackendOption::Name) || + Unordered.EnabledActions != std::vector<std::string>({"coalesce", "spill"})) { + return tooling_test::fail("backend invocation canonicalization must sort explicit identities"); + } + BackendInvocation DuplicateFeature = Base; + DuplicateFeature.Features.push_back("+i"); + if (DuplicateFeature.canonicalizeAndVerify()) + return tooling_test::fail("backend invocation must reject duplicate target features"); + BackendInvocation ZeroThreads = Base; + ZeroThreads.Determinism.ThreadCount = 0; + if (ZeroThreads.canonicalizeAndVerify()) + return tooling_test::fail("backend invocation must reject a zero thread count"); + + constexpr std::string_view Input = "canonical MIR"; + auto BaseKey = rcc::tooling::computeCacheKey(Input, Base, *Fingerprint); + if (!BaseKey || BaseKey->size() != 64) + return tooling_test::fail("cache key must be a SHA-256 digest"); + auto DifferentInput = rcc::tooling::computeCacheKey("different MIR", Base, *Fingerprint); + if (!DifferentInput || *DifferentInput == *BaseKey) + return tooling_test::fail("cache key must include canonical MIR"); + std::string DifferentFingerprint(64, '0'); + auto DifferentTarget = rcc::tooling::computeCacheKey(Input, Base, DifferentFingerprint); + if (!DifferentTarget || *DifferentTarget == *BaseKey) + return tooling_test::fail("cache key must include the target description fingerprint"); + + BackendInvocation Changed = Base; + Changed.Triple += "-variant"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the target triple"); + Changed = Base; + Changed.CPU += "-variant"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the target CPU"); + Changed = Base; + Changed.TuneCPU += "-variant"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the tuning CPU"); + Changed = Base; + Changed.Features.front() = "+variant"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include target features"); + Changed = Base; + Changed.ABI += "-variant"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the ABI"); + Changed = Base; + Changed.DataLayout += "-variant"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the data layout"); + Changed = Base; + Changed.Relocation = rcc::RelocationModel::Static; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the relocation model"); + Changed = Base; + Changed.Code = rcc::CodeModel::Medium; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the code model"); + Changed = Base; + Changed.TargetOptions.front().Value = "variant"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include target options"); + Changed = Base; + Changed.Layout = rcc::tooling::LayoutOwnership::CompilerFinalLayout; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include layout ownership"); + Changed = Base; + Changed.AssemblerVersion += "-variant"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include assembler identity"); + Changed = Base; + Changed.AssemblerOptions.front().Value = "false"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include assembler options"); + Changed = Base; + Changed.Optimization = rcc::OptimizationLevel::O3; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the optimization level"); + Changed = Base; + Changed.PassPipeline = {"legalizer"}; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the pass pipeline"); + Changed = Base; + Changed.Determinism.RandomSeed = 1; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the random seed"); + Changed = Base; + Changed.Determinism.ThreadCount = 2; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the thread count"); + Changed = Base; + Changed.Determinism.StableScheduling = false; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include stable scheduling"); + Changed = Base; + Changed.ActionFuel = 7; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include Action fuel"); + Changed = Base; + Changed.EnabledActions = {"Legalizer"}; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include enabled Actions"); + Changed = Base; + Changed.BuildFingerprint += "-variant"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the backend build fingerprint"); + + rcc::tooling::StructuredDiagnostic Diagnostic; + Diagnostic.Category = "verification error"; + Diagnostic.Stage = "selected-mir"; + Diagnostic.Pass = "test-pass"; + Diagnostic.Function = "test-function"; + Diagnostic.Block = 3; + Diagnostic.Instruction = 7; + Diagnostic.Operand = 1; + Diagnostic.SourceLocation = "input.mir:4:5"; + Diagnostic.TargetConstraint = "GPR"; + Diagnostic.Rule = "test-rule"; + Diagnostic.Message = "quoted \"message\"\nline"; + auto Manifest = rcc::tooling::createBackendManifest(rcc::tooling::ManifestKind::Reproducer, + rcc::tooling::ArtifactKind::Assembly, Base, + *Target, Input, std::nullopt, {Diagnostic}); + if (!Manifest) + return tooling_test::fail(Manifest.error().message()); + auto Printed = rcc::tooling::printBackendManifest(*Manifest); + if (!Printed) + return tooling_test::fail(Printed.error().message()); + auto Parsed = rcc::tooling::parseBackendManifest("manifest.yaml", *Printed); + if (!Parsed || *Parsed != *Manifest) + return tooling_test::fail("manifest printer and strict parser must round-trip all fields"); + + BackendInvocation EmptyCollections = Base; + EmptyCollections.TargetOptions.clear(); + EmptyCollections.AssemblerOptions.clear(); + auto EmptyManifest = rcc::tooling::createBackendManifest(rcc::tooling::ManifestKind::Reproducer, + rcc::tooling::ArtifactKind::FinalMIR, + EmptyCollections, *Target, Input); + if (!EmptyManifest) + return tooling_test::fail(EmptyManifest.error().message()); + auto EmptyPrinted = rcc::tooling::printBackendManifest(*EmptyManifest); + if (!EmptyPrinted) + return tooling_test::fail(EmptyPrinted.error().message()); + auto EmptyParsed = rcc::tooling::parseBackendManifest("empty-manifest.yaml", *EmptyPrinted); + if (!EmptyParsed || *EmptyParsed != *EmptyManifest) + return tooling_test::fail("manifest must preserve explicitly empty collections"); + + std::string Unknown = *Printed; + Unknown.insert(Unknown.find("schema:"), "unknown: 1\n"); + if (rcc::tooling::parseBackendManifest("unknown.yaml", std::move(Unknown))) + return tooling_test::fail("strict manifest parsing must reject unknown keys"); + std::string Duplicate = *Printed; + Duplicate.insert(Duplicate.find("schema:"), "schema: 1\n"); + if (rcc::tooling::parseBackendManifest("duplicate.yaml", std::move(Duplicate))) + return tooling_test::fail("strict manifest parsing must reject duplicate keys"); + std::string Missing = *Printed; + const std::size_t ABI = Missing.find(" abi:"); + Missing.erase(ABI, Missing.find('\n', ABI) - ABI + 1U); + if (rcc::tooling::parseBackendManifest("missing.yaml", std::move(Missing))) + return tooling_test::fail("strict manifest parsing must reject missing invocation facts"); + + return 0; +} diff --git a/backend/unittests/Tooling/CMakeLists.txt b/backend/unittests/Tooling/CMakeLists.txt new file mode 100644 index 0000000..8c3ac78 --- /dev/null +++ b/backend/unittests/Tooling/CMakeLists.txt @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +function(rcc_backend_add_tooling_test target source test_name) + add_executable(${target} ${source}) + rcc_backend_configure_target(${target}) + target_link_libraries(${target} PRIVATE RCCBackend::Tooling RCCBackend::RISCV) + target_compile_definitions( + ${target} + PRIVATE + RCC_BACKEND_TOOLING_TEST_INPUT_DIR="${RCC_BACKEND_SOURCE_DIR}/unittests/Target/Inputs" + ) + add_test(NAME ${test_name} COMMAND ${target}) +endfunction() + +rcc_backend_add_tooling_test( + RCCBackendManifestTests + BackendManifestTest.cpp + RCCBackend.Tooling.BackendManifest +) +rcc_backend_add_tooling_test( + RCCBackendArtifactStoreTests + ArtifactStoreTest.cpp + RCCBackend.Tooling.ArtifactStore +) +rcc_backend_add_tooling_test( + RCCBackendPipelineDriverTests + PipelineDriverTest.cpp + RCCBackend.Tooling.PipelineDriver +) +rcc_backend_add_tooling_test( + RCCBackendMIRReducerFuzzerTests + MIRReducerFuzzerTest.cpp + RCCBackend.Tooling.MIRReducerFuzzer +) + +rcc_backend_register_format_files( + "${CMAKE_CURRENT_SOURCE_DIR}/BackendManifestTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/ArtifactStoreTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/PipelineDriverTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MIRReducerFuzzerTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/ToolingTestSupport.h" +) diff --git a/backend/unittests/Tooling/MIRReducerFuzzerTest.cpp b/backend/unittests/Tooling/MIRReducerFuzzerTest.cpp new file mode 100644 index 0000000..2ffbdfb --- /dev/null +++ b/backend/unittests/Tooling/MIRReducerFuzzerTest.cpp @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ToolingTestSupport.h" +#include "rcc/Target/RISCV/RISCVTarget.h" +#include "rcc/Tooling/BackendDriver.h" +#include "rcc/Tooling/MIRFuzzer.h" +#include "rcc/Tooling/MIRReducer.h" + +#include <array> +#include <cstddef> +#include <cstdint> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +[[nodiscard]] bool hasErrorCode(const auto &Result, rcc::ErrorCode Code) { + return !Result && Result.error().code() == Code; +} + +[[nodiscard]] bool hasReductionStep(const rcc::tooling::MIRReductionResult &Result, + rcc::tooling::ReductionGranularity Granularity) { + for (const rcc::tooling::MIRReductionStep &Step : Result.Steps) { + if (Step.Granularity == Granularity) + return true; + } + return false; +} + +[[nodiscard]] rcc::Expected<std::string> addDeadInstruction(std::string Source) { + constexpr std::string_view Anchor = " $x10 = COPY %2\n"; + const std::size_t Position = Source.find(Anchor); + if (Position == std::string::npos) { + return rcc::makeUnexpected(rcc::ErrorCode::InternalError, + "test MIR does not contain the expected COPY"); + } + Source.insert(Position, " %3:gprb(i64) = G_CONSTANT 99\n"); + return Source; +} + +} // namespace + +int main() { + auto Target = rcc::riscv::createTargetDescription(); + if (!Target) + return tooling_test::fail(Target.error().message()); + const rcc::tooling::BackendInvocation Invocation = rcc::tooling::defaultRV64GCInvocation(); + auto Source = tooling_test::readInput("RISCVGenericPipeline.mir"); + if (!Source) + return tooling_test::fail(Source.error().message()); + auto ReducibleSource = addDeadInstruction(*Source); + if (!ReducibleSource) + return tooling_test::fail(ReducibleSource.error().message()); + auto CanonicalReducible = rcc::tooling::parseAndCanonicalizeMIR( + "validity-seed.mir", *ReducibleSource, *Target, Invocation); + if (!CanonicalReducible) + return tooling_test::fail(CanonicalReducible.error().message()); + + rcc::tooling::MIRReducerOptions ValidityOptions; + ValidityOptions.Mode = rcc::tooling::ReductionMode::PreserveValidity; + ValidityOptions.MaximumAttempts = 128; + ValidityOptions.Granularities = {rcc::tooling::ReductionGranularity::Instruction}; + auto ValidityReduction = rcc::tooling::reduceMIR("validity-reducer.mir", *ReducibleSource, + Invocation, *Target, {}, ValidityOptions); + if (!ValidityReduction || ValidityReduction->MIR.size() >= CanonicalReducible->Text.size() || + !hasReductionStep(*ValidityReduction, rcc::tooling::ReductionGranularity::Instruction)) { + return tooling_test::fail( + "validity reduction must remove a dead instruction and record the accepted step"); + } + auto ReducedCanonical = rcc::tooling::parseAndCanonicalizeMIR( + "validity-reduced.mir", ValidityReduction->MIR, *Target, ValidityReduction->Invocation); + if (!ReducedCanonical || ReducedCanonical->Text != ValidityReduction->MIR) + return tooling_test::fail("validity reduction must return canonical verified MIR"); + + rcc::tooling::MIRReducerOptions FeatureOptions; + FeatureOptions.Mode = rcc::tooling::ReductionMode::PreserveFailure; + FeatureOptions.MaximumAttempts = 128; + FeatureOptions.Granularities = {rcc::tooling::ReductionGranularity::TargetFeature}; + const auto KeepTwoFeatures = [](std::string_view, + const rcc::tooling::BackendInvocation &Candidate) { + return rcc::Expected<bool>(Candidate.Features.size() >= 2); + }; + auto FeatureReduction = rcc::tooling::reduceMIR("feature-reducer.mir", *Source, Invocation, + *Target, KeepTwoFeatures, FeatureOptions); + if (!FeatureReduction || FeatureReduction->Invocation.Features.size() != 2 || + !hasReductionStep(*FeatureReduction, rcc::tooling::ReductionGranularity::TargetFeature)) { + return tooling_test::fail("target-feature reduction must preserve its interestingness limit"); + } + + rcc::tooling::BackendInvocation PassInvocation = Invocation; + PassInvocation.PassPipeline = {"legalizer", "regbankselect", "instruction-select"}; + rcc::tooling::MIRReducerOptions PassOptions; + PassOptions.Mode = rcc::tooling::ReductionMode::PreserveFailure; + PassOptions.MaximumAttempts = 32; + PassOptions.Granularities = {rcc::tooling::ReductionGranularity::PassPipeline}; + const auto KeepOnePass = [](std::string_view, const rcc::tooling::BackendInvocation &Candidate) { + return rcc::Expected<bool>(!Candidate.PassPipeline.empty()); + }; + auto PassReduction = rcc::tooling::reduceMIR("pass-reducer.mir", *Source, PassInvocation, *Target, + KeepOnePass, PassOptions); + if (!PassReduction || PassReduction->Invocation.PassPipeline.size() != 1 || + !hasReductionStep(*PassReduction, rcc::tooling::ReductionGranularity::PassPipeline)) { + return tooling_test::fail("pass-pipeline reduction must retain one interesting pass"); + } + + auto RegBankInput = rcc::tooling::parseAndCanonicalizeMIR("failure-seed.mir", *ReducibleSource, + *Target, Invocation); + if (!RegBankInput) + return tooling_test::fail(RegBankInput.error().message()); + auto RegBank = rcc::tooling::runBackend(std::move(*RegBankInput), *Target, Invocation, + rcc::tooling::ArtifactKind::RegisterBankMIR); + if (!RegBank) + return tooling_test::fail(RegBank.error().message()); + rcc::tooling::BackendInvocation FailureInvocation = Invocation; + FailureInvocation.PassPipeline = {"instruction-select"}; + FailureInvocation.ActionFuel = 0; + const auto PreservesActionFailure = [&](std::string_view MIR, + const rcc::tooling::BackendInvocation &Candidate) { + auto Input = rcc::tooling::parseAndCanonicalizeMIR("failure-candidate.mir", std::string(MIR), + *Target, Candidate); + if (!Input) + return rcc::Expected<bool>(std::unexpected(Input.error())); + auto Run = rcc::tooling::runBackend(std::move(*Input), *Target, Candidate, + rcc::tooling::ArtifactKind::SelectedMIR); + return rcc::Expected<bool>(!Run && Run.error().code() == rcc::ErrorCode::VerificationError); + }; + rcc::tooling::MIRReducerOptions FailureOptions; + FailureOptions.Mode = rcc::tooling::ReductionMode::PreserveFailure; + FailureOptions.MaximumAttempts = 128; + FailureOptions.Granularities = {rcc::tooling::ReductionGranularity::Instruction}; + auto FailureReduction = + rcc::tooling::reduceMIR("failure-reducer.mir", std::string(RegBank->text()), + FailureInvocation, *Target, PreservesActionFailure, FailureOptions); + if (!FailureReduction || + !hasReductionStep(*FailureReduction, rcc::tooling::ReductionGranularity::Instruction)) { + return tooling_test::fail("failure reduction must retain the Action/Fuel failure"); + } + auto StillInteresting = + PreservesActionFailure(FailureReduction->MIR, FailureReduction->Invocation); + if (!StillInteresting || !*StillInteresting) + return tooling_test::fail("reduced failure input must still satisfy its predicate"); + + rcc::tooling::MIRReducerOptions InvalidReducerOptions = ValidityOptions; + InvalidReducerOptions.MaximumAttempts = 0; + auto ZeroBudget = rcc::tooling::reduceMIR("zero-budget.mir", *Source, Invocation, *Target, {}, + InvalidReducerOptions); + if (!hasErrorCode(ZeroBudget, rcc::ErrorCode::InvalidArgument)) + return tooling_test::fail("reducer must reject a zero attempt budget"); + InvalidReducerOptions = FeatureOptions; + auto MissingPredicate = rcc::tooling::reduceMIR("missing-predicate.mir", *Source, Invocation, + *Target, {}, InvalidReducerOptions); + if (!hasErrorCode(MissingPredicate, rcc::ErrorCode::InvalidArgument)) + return tooling_test::fail("failure reduction must require an interestingness predicate"); + const auto NeverInteresting = [](std::string_view, const rcc::tooling::BackendInvocation &) { + return rcc::Expected<bool>(false); + }; + auto FalsePredicate = rcc::tooling::reduceMIR("false-predicate.mir", *Source, Invocation, *Target, + NeverInteresting, FeatureOptions); + if (!hasErrorCode(FalsePredicate, rcc::ErrorCode::ReductionFailed)) + return tooling_test::fail("reducer must reject an initially uninteresting input"); + InvalidReducerOptions = ValidityOptions; + InvalidReducerOptions.Granularities = { + rcc::tooling::ReductionGranularity::Instruction, + rcc::tooling::ReductionGranularity::Instruction, + }; + auto DuplicateGranularity = rcc::tooling::reduceMIR( + "duplicate-granularity.mir", *Source, Invocation, *Target, {}, InvalidReducerOptions); + if (!hasErrorCode(DuplicateGranularity, rcc::ErrorCode::InvalidArgument)) + return tooling_test::fail("reducer must reject duplicate granularities"); + volatile std::uint8_t InvalidGranularityValue = 255; + InvalidReducerOptions.Granularities = { + static_cast<rcc::tooling::ReductionGranularity>(InvalidGranularityValue)}; + auto InvalidGranularity = rcc::tooling::reduceMIR("invalid-granularity.mir", *Source, Invocation, + *Target, {}, InvalidReducerOptions); + if (!hasErrorCode(InvalidGranularity, rcc::ErrorCode::InvalidArgument)) + return tooling_test::fail("reducer must reject an unknown granularity"); + + rcc::tooling::MIRFuzzOptions FuzzOptions; + FuzzOptions.Seed = 0x123456789abcdef0ULL; + FuzzOptions.Iterations = 2; + auto FirstFuzz = + rcc::tooling::fuzzMIR("fuzz-seed.mir", *Source, Invocation, *Target, FuzzOptions); + auto SecondFuzz = + rcc::tooling::fuzzMIR("fuzz-seed.mir", *Source, Invocation, *Target, FuzzOptions); + if (!FirstFuzz || !SecondFuzz || FirstFuzz->Seed != FuzzOptions.Seed || + FirstFuzz->Layers != SecondFuzz->Layers || FirstFuzz->Layers.size() != 6 || + FirstFuzz->executed() != 11 || + FirstFuzz->accepted() + FirstFuzz->rejected() != FirstFuzz->executed()) { + return tooling_test::fail("all six fuzz layers must produce deterministic accounting"); + } + constexpr std::array LayerNames{ + std::string_view("parser"), std::string_view("builder"), + std::string_view("property-stage"), std::string_view("mutation"), + std::string_view("pipeline"), std::string_view("semantics"), + }; + for (std::size_t Index = 0; Index < LayerNames.size(); ++Index) { + auto Parsed = rcc::tooling::parseMIRFuzzLayer(LayerNames[Index]); + if (!Parsed || rcc::tooling::spelling(*Parsed) != LayerNames[Index] || + FirstFuzz->Layers[Index].Layer != *Parsed) { + return tooling_test::fail("fuzz layer spelling and parsing must round-trip"); + } + } + if (rcc::tooling::parseMIRFuzzLayer("unknown")) + return tooling_test::fail("fuzz layer parser must reject unknown names"); + + rcc::tooling::MIRFuzzOptions InvalidFuzzOptions = FuzzOptions; + InvalidFuzzOptions.Iterations = 0; + auto ZeroIterations = rcc::tooling::fuzzMIR("zero-iterations.mir", *Source, Invocation, *Target, + InvalidFuzzOptions); + if (!hasErrorCode(ZeroIterations, rcc::ErrorCode::InvalidArgument)) + return tooling_test::fail("fuzzer must reject zero iterations"); + InvalidFuzzOptions = FuzzOptions; + InvalidFuzzOptions.MaximumInputSize = 0; + auto ZeroInputLimit = rcc::tooling::fuzzMIR("zero-input-limit.mir", *Source, Invocation, *Target, + InvalidFuzzOptions); + if (!hasErrorCode(ZeroInputLimit, rcc::ErrorCode::InvalidArgument)) + return tooling_test::fail("fuzzer must reject a zero input-size limit"); + InvalidFuzzOptions = FuzzOptions; + InvalidFuzzOptions.MaximumInputSize = 1; + auto OversizedSeed = + rcc::tooling::fuzzMIR("oversized-seed.mir", *Source, Invocation, *Target, InvalidFuzzOptions); + if (!hasErrorCode(OversizedSeed, rcc::ErrorCode::InvalidArgument)) + return tooling_test::fail("fuzzer must reject a seed larger than its input limit"); + InvalidFuzzOptions = FuzzOptions; + InvalidFuzzOptions.Layers = {rcc::tooling::MIRFuzzLayer::Parser, + rcc::tooling::MIRFuzzLayer::Parser}; + auto DuplicateLayer = rcc::tooling::fuzzMIR("duplicate-layer.mir", *Source, Invocation, *Target, + InvalidFuzzOptions); + if (!hasErrorCode(DuplicateLayer, rcc::ErrorCode::InvalidArgument)) + return tooling_test::fail("fuzzer must reject duplicate layers"); + volatile std::uint8_t InvalidFuzzLayerValue = 255; + InvalidFuzzOptions.Layers = {static_cast<rcc::tooling::MIRFuzzLayer>(InvalidFuzzLayerValue)}; + auto InvalidLayer = + rcc::tooling::fuzzMIR("invalid-layer.mir", *Source, Invocation, *Target, InvalidFuzzOptions); + if (!hasErrorCode(InvalidLayer, rcc::ErrorCode::InvalidArgument)) + return tooling_test::fail("fuzzer must reject an unknown layer"); + + return 0; +} diff --git a/backend/unittests/Tooling/PipelineDriverTest.cpp b/backend/unittests/Tooling/PipelineDriverTest.cpp new file mode 100644 index 0000000..32a99c5 --- /dev/null +++ b/backend/unittests/Tooling/PipelineDriverTest.cpp @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/Tooling/PipelineDriver.h" + +#include "ToolingTestSupport.h" +#include "rcc/CodeGen/MIRPrinter.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/Target/RISCV/RISCVTarget.h" +#include "rcc/Tooling/BackendDriver.h" + +#include <array> +#include <cstddef> +#include <cstdint> +#include <optional> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +[[nodiscard]] rcc::Expected<std::string> +printDocument(const rcc::mir::MIRDocument &Document, const rcc::TargetDescription &Target, + const rcc::tooling::BackendInvocation &Invocation) { + rcc::mir::MIRDocumentPrinterOptions Options; + Options.Mode = rcc::mir::MIPrintMode::Complete; + Options.EmitEmbeddedIR = true; + Options.DataLayout = Invocation.DataLayout; + return rcc::mir::printMIRDocument(Document, Target, std::move(Options)); +} + +[[nodiscard]] rcc::Expected<rcc::tooling::BackendRunResult> +runInput(std::string_view SourceName, std::string SourceText, const rcc::TargetDescription &Target, + const rcc::tooling::BackendInvocation &Invocation, rcc::tooling::ArtifactKind Output, + rcc::tooling::PipelineControls Controls = {}) { + auto Input = rcc::tooling::parseAndCanonicalizeMIR(std::string(SourceName), std::move(SourceText), + Target, Invocation); + if (!Input) + return std::unexpected(Input.error()); + return rcc::tooling::runBackend(std::move(*Input), Target, Invocation, Output, + std::move(Controls)); +} + +[[nodiscard]] rcc::Expected<std::string> makeMultiFunctionInput() { + auto First = tooling_test::readInput("RISCVGenericPipeline.mir"); + if (!First) + return std::unexpected(First.error()); + std::string Second = *First; + constexpr std::string_view OriginalName = "name: generic_pipeline"; + const std::size_t Name = Second.find(OriginalName); + if (Name == std::string::npos) + return rcc::makeUnexpected(rcc::ErrorCode::InternalError, "test MIR function name is absent"); + Second.replace(Name, OriginalName.size(), "name: generic_pipeline_second"); + First->push_back('\n'); + First->append(Second); + return First; +} + +struct DeterministicOutputs final { + std::vector<std::uint8_t> FinalMIR; + std::vector<std::uint8_t> Assembly; + std::vector<std::uint8_t> Object; + std::vector<rcc::MachineActionEvent> Actions; + std::vector<rcc::tooling::ExecutedPass> ExecutedPasses; +}; + +[[nodiscard]] rcc::Expected<DeterministicOutputs> +runDeterminismCase(std::string_view Input, const rcc::TargetDescription &Target, + std::uint32_t ThreadCount) { + rcc::tooling::BackendInvocation Invocation = rcc::tooling::defaultRV64GCInvocation(); + Invocation.Determinism.ThreadCount = ThreadCount; + auto Final = runInput("determinism-final.mir", std::string(Input), Target, Invocation, + rcc::tooling::ArtifactKind::FinalMIR); + if (!Final) + return std::unexpected(Final.error()); + auto Assembly = runInput("determinism-assembly.mir", std::string(Input), Target, Invocation, + rcc::tooling::ArtifactKind::Assembly); + if (!Assembly) + return std::unexpected(Assembly.error()); + auto Object = runInput("determinism-object.mir", std::string(Input), Target, Invocation, + rcc::tooling::ArtifactKind::RelocatableObject); + if (!Object) + return std::unexpected(Object.error()); + return DeterministicOutputs{ + .FinalMIR = std::move(Final->Output), + .Assembly = std::move(Assembly->Output), + .Object = std::move(Object->Output), + .Actions = std::move(Final->Actions), + .ExecutedPasses = std::move(Final->Pipeline.ExecutedPasses), + }; +} + +} // namespace + +int main() { + auto Target = rcc::riscv::createTargetDescription(); + if (!Target) + return tooling_test::fail(Target.error().message()); + const rcc::tooling::BackendInvocation Invocation = rcc::tooling::defaultRV64GCInvocation(); + auto Source = tooling_test::readInput("RISCVGenericPipeline.mir"); + if (!Source) + return tooling_test::fail(Source.error().message()); + + rcc::tooling::BackendInvocation AtomicOnlyInvocation = Invocation; + AtomicOnlyInvocation.Features = {"+a", "+zacas", "+zabha"}; + AtomicOnlyInvocation.PassPipeline = {"legalizer"}; + auto CanonicalAtomicOnly = AtomicOnlyInvocation.canonicalizeAndVerify(); + if (!CanonicalAtomicOnly) + return tooling_test::fail(CanonicalAtomicOnly.error().message()); + auto AtomicOnlyRun = runInput("atomic-only.mir", *Source, *Target, AtomicOnlyInvocation, + rcc::tooling::ArtifactKind::LegalizedMIR); + if (!AtomicOnlyRun) + return tooling_test::fail(AtomicOnlyRun.error().message()); + + rcc::tooling::BackendInvocation UnsupportedFeatureInvocation = Invocation; + UnsupportedFeatureInvocation.Features = {"+v"}; + auto CanonicalUnsupported = UnsupportedFeatureInvocation.canonicalizeAndVerify(); + if (!CanonicalUnsupported) + return tooling_test::fail(CanonicalUnsupported.error().message()); + if (rcc::tooling::verifyRV64GCInvocation(UnsupportedFeatureInvocation)) + return tooling_test::fail("RV64 backend invocation must reject unsupported target features"); + + constexpr std::array StageCases{ + std::pair{rcc::tooling::ArtifactKind::PreLegalizerMIR, + rcc::tooling::BackendStage::PreLegalizerMIR}, + std::pair{rcc::tooling::ArtifactKind::LegalizedMIR, rcc::tooling::BackendStage::LegalizedMIR}, + std::pair{rcc::tooling::ArtifactKind::RegisterBankMIR, + rcc::tooling::BackendStage::RegisterBankMIR}, + std::pair{rcc::tooling::ArtifactKind::SelectedMIR, rcc::tooling::BackendStage::SelectedMIR}, + std::pair{rcc::tooling::ArtifactKind::PostSSAMIR, rcc::tooling::BackendStage::PostSSAMIR}, + std::pair{rcc::tooling::ArtifactKind::PostRegisterAllocationMIR, + rcc::tooling::BackendStage::PostRegisterAllocationMIR}, + std::pair{rcc::tooling::ArtifactKind::FinalMIR, rcc::tooling::BackendStage::FinalMIR}, + }; + std::optional<std::string> SelectedMIR; + for (const auto &[Output, ExpectedStage] : StageCases) { + auto Run = runInput("stage.mir", *Source, *Target, Invocation, Output); + if (!Run) + return tooling_test::fail(Run.error().message()); + auto Parsed = rcc::tooling::parseAndCanonicalizeMIR( + "stage-output.mir", std::string(Run->text()), *Target, Invocation); + if (!Parsed || Parsed->Document.unit().functionCount() != 1) + return tooling_test::fail("every MIR stage output must strictly parse and verify"); + const rcc::MachineFunction *Function = Parsed->Document.unit().functionAt(0); + if (Function == nullptr) + return tooling_test::fail("MIR stage output must contain a machine function"); + const rcc::tooling::BackendStage ActualStage = rcc::tooling::stageForFunction(*Function); + if (ActualStage != ExpectedStage) { + return tooling_test::fail( + "MIR output requested " + std::string(rcc::tooling::spelling(ExpectedStage)) + + " but reparsed as " + std::string(rcc::tooling::spelling(ActualStage))); + } + if (Output == rcc::tooling::ArtifactKind::SelectedMIR) + SelectedMIR = std::string(Run->text()); + } + + auto Assembly = + runInput("assembly.mir", *Source, *Target, Invocation, rcc::tooling::ArtifactKind::Assembly); + if (!Assembly || !Assembly->text().contains("generic_pipeline") || + !Assembly->text().contains("jalr x0, x1, 0")) { + return tooling_test::fail("assembly output must use the complete RV64GC production pipeline"); + } + auto Object = runInput("object.mir", *Source, *Target, Invocation, + rcc::tooling::ArtifactKind::RelocatableObject); + constexpr std::array<std::uint8_t, 4> ELFMagic{0x7fU, 'E', 'L', 'F'}; + if (!Object || Object->Output.size() < ELFMagic.size() || + !std::ranges::equal(ELFMagic, std::span<const std::uint8_t>(Object->Output).first(4))) { + return tooling_test::fail("object output must be a native ELF relocatable object"); + } + + auto SnapshotInput = + rcc::tooling::parseAndCanonicalizeMIR("snapshots.mir", *Source, *Target, Invocation); + if (!SnapshotInput) + return tooling_test::fail(SnapshotInput.error().message()); + rcc::tooling::PipelineControls SnapshotControls; + SnapshotControls.CaptureStageSnapshots = true; + auto Snapshots = rcc::tooling::runRV64GCPipeline(SnapshotInput->Document, *Target, Invocation, + SnapshotControls); + if (!Snapshots || Snapshots->Snapshots.size() != 7 || + Snapshots->Snapshots.front().Stage != rcc::tooling::BackendStage::PreLegalizerMIR || + Snapshots->Snapshots.back().Stage != rcc::tooling::BackendStage::FinalMIR) { + return tooling_test::fail("pipeline must capture every stable MIR stage boundary"); + } + for (const rcc::tooling::PipelineSnapshot &Snapshot : Snapshots->Snapshots) { + if (Snapshot.Phase != rcc::tooling::SnapshotPhase::Stage || Snapshot.MIR.empty()) + return tooling_test::fail("stage snapshots must contain complete canonical MIR"); + } + + auto PrintInput = + rcc::tooling::parseAndCanonicalizeMIR("print-points.mir", *Source, *Target, Invocation); + if (!PrintInput) + return tooling_test::fail(PrintInput.error().message()); + rcc::tooling::PipelineControls PrintControls; + PrintControls.PrintBefore = {"legalizer"}; + PrintControls.PrintAfter = {"legalizer"}; + auto PrintPoints = + rcc::tooling::runRV64GCPipeline(PrintInput->Document, *Target, Invocation, PrintControls); + if (!PrintPoints || PrintPoints->Snapshots.size() != 2 || + PrintPoints->Snapshots[0].Phase != rcc::tooling::SnapshotPhase::BeforePass || + PrintPoints->Snapshots[1].Phase != rcc::tooling::SnapshotPhase::AfterPass) { + return tooling_test::fail("pass selectors must capture deterministic before/after snapshots"); + } + + auto StopInput = + rcc::tooling::parseAndCanonicalizeMIR("stop-after.mir", *Source, *Target, Invocation); + if (!StopInput) + return tooling_test::fail(StopInput.error().message()); + rcc::tooling::PipelineControls StopControls; + StopControls.StopAfter = "legalizer,0"; + auto Stopped = + rcc::tooling::runRV64GCPipeline(StopInput->Document, *Target, Invocation, StopControls); + const rcc::MachineFunction *StoppedFunction = StopInput->Document.unit().functionAt(0); + if (!Stopped || StoppedFunction == nullptr || + rcc::tooling::stageForFunction(*StoppedFunction) != + rcc::tooling::BackendStage::LegalizedMIR || + Stopped->ExecutedPasses.size() != 2 || + Stopped->ExecutedPasses[0].Pass != "riscv-prelegalizer-combiner" || + Stopped->ExecutedPasses[1].Pass != "legalizer") { + return tooling_test::fail("stop-after must stop at the selected pass occurrence"); + } + StopControls.StopAfter = "legalizer,-1"; + if (rcc::tooling::runRV64GCPipeline(StopInput->Document, *Target, Invocation, StopControls)) + return tooling_test::fail("pipeline controls must reject a negative pass instance"); + + rcc::tooling::BackendInvocation MixedPipeline = Invocation; + MixedPipeline.PassPipeline = {"default", "legalizer"}; + auto MixedInput = + rcc::tooling::parseAndCanonicalizeMIR("mixed-pipeline.mir", *Source, *Target, MixedPipeline); + if (!MixedInput) + return tooling_test::fail(MixedInput.error().message()); + if (rcc::tooling::runRV64GCPipeline(MixedInput->Document, *Target, MixedPipeline)) + return tooling_test::fail("named passes must not be mixed with the default pipeline marker"); + + if (!SelectedMIR) + return tooling_test::fail("selected MIR stage was not captured"); + auto LaterInput = + rcc::tooling::parseAndCanonicalizeMIR("later-stage.mir", *SelectedMIR, *Target, Invocation); + if (!LaterInput) + return tooling_test::fail(LaterInput.error().message()); + if (rcc::tooling::runBackend(std::move(*LaterInput), *Target, Invocation, + rcc::tooling::ArtifactKind::PreLegalizerMIR)) { + return tooling_test::fail( + "backend must not reconstruct an earlier stage from a later snapshot"); + } + + auto RegBankRun = runInput("action-regbank.mir", *Source, *Target, Invocation, + rcc::tooling::ArtifactKind::RegisterBankMIR); + if (!RegBankRun) + return tooling_test::fail(RegBankRun.error().message()); + rcc::tooling::BackendInvocation InstructionSelectInvocation = Invocation; + InstructionSelectInvocation.PassPipeline = {"instruction-select"}; + InstructionSelectInvocation.ActionFuel = 0; + auto RejectedInput = rcc::tooling::parseAndCanonicalizeMIR( + "action-rejected.mir", std::string(RegBankRun->text()), *Target, InstructionSelectInvocation); + if (!RejectedInput) + return tooling_test::fail(RejectedInput.error().message()); + const std::string BeforeRejected = RejectedInput->Text; + if (rcc::tooling::runRV64GCPipeline(RejectedInput->Document, *Target, + InstructionSelectInvocation)) { + return tooling_test::fail("zero Action fuel must reject required instruction selection"); + } + auto AfterRejected = printDocument(RejectedInput->Document, *Target, InstructionSelectInvocation); + if (!AfterRejected || *AfterRejected != BeforeRejected) + return tooling_test::fail("Action rejection must not leave a partially modified MachineIR"); + + InstructionSelectInvocation.ActionFuel.reset(); + auto AcceptedFirst = + runInput("action-accepted-1.mir", std::string(RegBankRun->text()), *Target, + InstructionSelectInvocation, rcc::tooling::ArtifactKind::SelectedMIR); + auto AcceptedSecond = + runInput("action-accepted-2.mir", std::string(RegBankRun->text()), *Target, + InstructionSelectInvocation, rcc::tooling::ArtifactKind::SelectedMIR); + if (!AcceptedFirst || !AcceptedSecond || AcceptedFirst->Actions.empty() || + AcceptedFirst->Actions != AcceptedSecond->Actions || + AcceptedFirst->Output != AcceptedSecond->Output) { + return tooling_test::fail("Action replay and stable IDs must be deterministic"); + } + for (const rcc::MachineActionEvent &Action : AcceptedFirst->Actions) { + if (!Action.Accepted || Action.ID.Function.value() == 0 || Action.ID.Instruction.value() == 0) + return tooling_test::fail("instruction selection Actions must use stable nonzero IDs"); + } + + auto MultiFunction = makeMultiFunctionInput(); + if (!MultiFunction) + return tooling_test::fail(MultiFunction.error().message()); + auto OneThread = runDeterminismCase(*MultiFunction, *Target, 1); + auto TwoThreads = runDeterminismCase(*MultiFunction, *Target, 2); + auto FourThreads = runDeterminismCase(*MultiFunction, *Target, 4); + if (!OneThread || !TwoThreads || !FourThreads) + return tooling_test::fail("parallel determinism pipeline failed"); + if (OneThread->FinalMIR != TwoThreads->FinalMIR || OneThread->FinalMIR != FourThreads->FinalMIR || + OneThread->Assembly != TwoThreads->Assembly || OneThread->Assembly != FourThreads->Assembly || + OneThread->Object != TwoThreads->Object || OneThread->Object != FourThreads->Object || + OneThread->Actions != TwoThreads->Actions || OneThread->Actions != FourThreads->Actions || + OneThread->ExecutedPasses != TwoThreads->ExecutedPasses || + OneThread->ExecutedPasses != FourThreads->ExecutedPasses) { + return tooling_test::fail("1/2/4-thread execution must produce stable output and event order"); + } + + return 0; +} diff --git a/backend/unittests/Tooling/ToolingTestSupport.h b/backend/unittests/Tooling/ToolingTestSupport.h new file mode 100644 index 0000000..bff0ebb --- /dev/null +++ b/backend/unittests/Tooling/ToolingTestSupport.h @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +// The private header has no stable include-root-relative guard name. +// NOLINTNEXTLINE(llvm-header-guard) +#ifndef RCC_UNITTESTS_TOOLING_TOOLINGTESTSUPPORT_H +#define RCC_UNITTESTS_TOOLING_TOOLINGTESTSUPPORT_H + +#include "rcc/Support/Error.h" +#include "rcc/Tooling/ArtifactStore.h" +#include "rcc/Tooling/BackendInvocation.h" + +#include <chrono> +#include <cstddef> +#include <cstdint> +#include <filesystem> +#include <fstream> +#include <iostream> +#include <iterator> +#include <span> +#include <string> +#include <string_view> +#include <system_error> +#include <utility> + +#ifndef RCC_BACKEND_TOOLING_TEST_INPUT_DIR +#error "RCC_BACKEND_TOOLING_TEST_INPUT_DIR must be defined" +#endif + +namespace tooling_test { + +inline int fail(std::string_view Message) { + std::cerr << Message << '\n'; + return 1; +} + +[[nodiscard]] inline rcc::Expected<std::string> readText(const std::filesystem::path &Path) { + std::ifstream Input(Path, std::ios::binary); + if (!Input) { + return rcc::makeUnexpected(rcc::ErrorCode::IoError, + "cannot open test input '" + Path.string() + "'"); + } + std::string Text((std::istreambuf_iterator<char>(Input)), std::istreambuf_iterator<char>()); + if (Input.bad()) { + return rcc::makeUnexpected(rcc::ErrorCode::IoError, + "cannot read test input '" + Path.string() + "'"); + } + return Text; +} + +[[nodiscard]] inline rcc::Expected<void> writeText(const std::filesystem::path &Path, + std::string_view Text) { + std::ofstream Output(Path, std::ios::binary | std::ios::trunc); + if (!Output) + return rcc::makeUnexpected(rcc::ErrorCode::IoError, "cannot create test file"); + Output.write(Text.data(), static_cast<std::streamsize>(Text.size())); + Output.close(); + if (!Output) + return rcc::makeUnexpected(rcc::ErrorCode::IoError, "cannot write test file"); + return {}; +} + +[[nodiscard]] inline rcc::Expected<void> writeBytes(const std::filesystem::path &Path, + std::span<const std::uint8_t> Bytes) { + if (Bytes.empty()) + return writeText(Path, {}); + const auto *Data = reinterpret_cast<const char *>(Bytes.data()); + return writeText(Path, std::string_view(Data, Bytes.size())); +} + +[[nodiscard]] inline rcc::Expected<std::string> readInput(std::string_view Name) { + return readText(std::filesystem::path(RCC_BACKEND_TOOLING_TEST_INPUT_DIR) / Name); +} + +[[nodiscard]] inline rcc::Expected<rcc::tooling::CanonicalMIR> +loadCanonicalInput(std::string_view Name, const rcc::TargetDescription &Target, + const rcc::tooling::BackendInvocation &Invocation) { + auto Source = readInput(Name); + if (!Source) + return std::unexpected(Source.error()); + return rcc::tooling::parseAndCanonicalizeMIR(std::string(Name), std::move(*Source), Target, + Invocation); +} + +class TemporaryDirectory final { +public: + TemporaryDirectory(const TemporaryDirectory &) = delete; + TemporaryDirectory &operator=(const TemporaryDirectory &) = delete; + + TemporaryDirectory(TemporaryDirectory &&Other) noexcept : Path(std::move(Other.Path)) { + Other.Path.clear(); + } + TemporaryDirectory &operator=(TemporaryDirectory &&) = delete; + + ~TemporaryDirectory() { + if (Path.empty()) + return; + std::error_code Error; + std::filesystem::remove_all(Path, Error); + } + + [[nodiscard]] static rcc::Expected<TemporaryDirectory> create(std::string_view Prefix) { + std::error_code Error; + const std::filesystem::path Root = std::filesystem::temp_directory_path(Error); + if (Error) + return rcc::makeUnexpected(rcc::ErrorCode::IoError, "cannot locate test temporary directory"); + const auto Stamp = std::chrono::steady_clock::now().time_since_epoch().count(); + for (std::size_t Attempt = 0; Attempt < 1024; ++Attempt) { + const std::filesystem::path Candidate = + Root / + (std::string(Prefix) + "-" + std::to_string(Stamp) + "-" + std::to_string(Attempt)); + Error.clear(); + if (std::filesystem::create_directory(Candidate, Error)) + return TemporaryDirectory(Candidate); + } + return rcc::makeUnexpected(rcc::ErrorCode::IoError, + "cannot create a unique test temporary directory"); + } + + [[nodiscard]] const std::filesystem::path &path() const noexcept { + return Path; + } + +private: + explicit TemporaryDirectory(std::filesystem::path Path) : Path(std::move(Path)) {} + + std::filesystem::path Path; +}; + +} // namespace tooling_test + +#endif // RCC_UNITTESTS_TOOLING_TOOLINGTESTSUPPORT_H |
