aboutsummaryrefslogtreecommitdiff
path: root/backend/unittests
diff options
context:
space:
mode:
Diffstat (limited to 'backend/unittests')
-rw-r--r--backend/unittests/AsmParser/CMakeLists.txt14
-rw-r--r--backend/unittests/AsmParser/LLParserTest.cpp533
-rw-r--r--backend/unittests/CMakeLists.txt9
-rw-r--r--backend/unittests/CodeGen/CMakeLists.txt545
-rw-r--r--backend/unittests/CodeGen/CompilationUnitTest.cpp104
-rw-r--r--backend/unittests/CodeGen/CoreTypesTest.cpp224
-rw-r--r--backend/unittests/CodeGen/GenericOpcodesTest.cpp748
-rw-r--r--backend/unittests/CodeGen/GlobalISel/CMakeLists.txt40
-rw-r--r--backend/unittests/CodeGen/GlobalISel/GISelMITest.h115
-rw-r--r--backend/unittests/CodeGen/GlobalISel/InstructionSelectTest.cpp364
-rw-r--r--backend/unittests/CodeGen/GlobalISel/LegalizerTest.cpp174
-rw-r--r--backend/unittests/CodeGen/GlobalISel/RegBankSelectTest.cpp89
-rw-r--r--backend/unittests/CodeGen/InstructionDescriptorTest.cpp368
-rw-r--r--backend/unittests/CodeGen/LiveIntervalTest.cpp116
-rw-r--r--backend/unittests/CodeGen/LiveIntervalsTest.cpp265
-rw-r--r--backend/unittests/CodeGen/LivePhysRegsTest.cpp56
-rw-r--r--backend/unittests/CodeGen/LiveRegMatrixTest.cpp92
-rw-r--r--backend/unittests/CodeGen/LiveRegUnitsTest.cpp37
-rw-r--r--backend/unittests/CodeGen/LiveTestSupport.h264
-rw-r--r--backend/unittests/CodeGen/LiveVariablesTest.cpp320
-rw-r--r--backend/unittests/CodeGen/MachineAnalysesTest.cpp582
-rw-r--r--backend/unittests/CodeGen/MachineCompilationStatusTest.cpp164
-rw-r--r--backend/unittests/CodeGen/MachineConstantPoolTest.cpp260
-rw-r--r--backend/unittests/CodeGen/MachineDebugInfoTest.cpp518
-rw-r--r--backend/unittests/CodeGen/MachineFrameInfoTest.cpp191
-rw-r--r--backend/unittests/CodeGen/MachineFunctionClonerTest.cpp846
-rw-r--r--backend/unittests/CodeGen/MachineFunctionRecordsTest.cpp362
-rw-r--r--backend/unittests/CodeGen/MachineIRAttributesTest.cpp206
-rw-r--r--backend/unittests/CodeGen/MachineIRBuilderTest.cpp556
-rw-r--r--backend/unittests/CodeGen/MachineIRCoreTest.cpp484
-rw-r--r--backend/unittests/CodeGen/MachineJumpTableInfoTest.cpp173
-rw-r--r--backend/unittests/CodeGen/MachineMetadataTest.cpp211
-rw-r--r--backend/unittests/CodeGen/MachineOperandTest.cpp524
-rw-r--r--backend/unittests/CodeGen/MachineOptimizationTest.cpp798
-rw-r--r--backend/unittests/CodeGen/MachinePassManagerTest.cpp426
-rw-r--r--backend/unittests/CodeGen/MachineSchedulingTest.cpp518
-rw-r--r--backend/unittests/CodeGen/MachineValueTypesTest.cpp227
-rw-r--r--backend/unittests/CodeGen/MachineVerifierTargetTest.cpp485
-rw-r--r--backend/unittests/CodeGen/MachineVerifierTest.cpp738
-rw-r--r--backend/unittests/CodeGen/PBQPTest.cpp177
-rw-r--r--backend/unittests/CodeGen/PHIEliminationTest.cpp479
-rw-r--r--backend/unittests/CodeGen/PassInstrumentationTest.cpp173
-rw-r--r--backend/unittests/CodeGen/PassManagerTest.cpp260
-rw-r--r--backend/unittests/CodeGen/RegAllocBasicTest.cpp108
-rw-r--r--backend/unittests/CodeGen/RegAllocFastTest.cpp83
-rw-r--r--backend/unittests/CodeGen/RegAllocPBQPTest.cpp117
-rw-r--r--backend/unittests/CodeGen/RegisterAllocationTest.cpp1044
-rw-r--r--backend/unittests/CodeGen/RegisterAllocationTestSupport.h474
-rw-r--r--backend/unittests/CodeGen/SSAExitTestSupport.h444
-rw-r--r--backend/unittests/CodeGen/SlotIndexesTest.cpp93
-rw-r--r--backend/unittests/CodeGen/SpillerTest.cpp94
-rw-r--r--backend/unittests/CodeGen/TargetCallLoweringTest.cpp328
-rw-r--r--backend/unittests/CodeGen/TargetConstantTest.cpp278
-rw-r--r--backend/unittests/CodeGen/TargetFunctionStateTest.cpp255
-rw-r--r--backend/unittests/CodeGen/TargetMIRTextInfoTest.cpp374
-rw-r--r--backend/unittests/CodeGen/TargetRegisterInfoTest.cpp378
-rw-r--r--backend/unittests/CodeGen/TwoAddressInstructionPassTest.cpp268
-rw-r--r--backend/unittests/CodeGen/VirtRegMapTest.cpp91
-rw-r--r--backend/unittests/MC/CMakeLists.txt16
-rw-r--r--backend/unittests/MC/MCObjectTest.cpp500
-rw-r--r--backend/unittests/MC/MCTest.cpp147
-rw-r--r--backend/unittests/MIR/CMakeLists.txt46
-rw-r--r--backend/unittests/MIR/EmbeddedIRTest.cpp143
-rw-r--r--backend/unittests/MIR/MILexerTest.cpp417
-rw-r--r--backend/unittests/MIR/MIParserTest.cpp640
-rw-r--r--backend/unittests/MIR/MIRDocumentTest.cpp1148
-rw-r--r--backend/unittests/Support/CMakeLists.txt32
-rw-r--r--backend/unittests/Support/ErrorTest.cpp56
-rw-r--r--backend/unittests/Support/SHA256Test.cpp47
-rw-r--r--backend/unittests/Support/TextDiagnosticTest.cpp43
-rw-r--r--backend/unittests/Support/YAMLTest.cpp233
-rw-r--r--backend/unittests/Target/CMakeLists.txt117
-rw-r--r--backend/unittests/Target/Inputs/RISCVDataPipeline.mir18
-rw-r--r--backend/unittests/Target/Inputs/RISCVGenericPipeline.mir12
-rw-r--r--backend/unittests/Target/Inputs/RISCVStackFramePipeline.mir12
-rw-r--r--backend/unittests/Target/Inputs/RISCVWideConstantPipeline.mir10
-rw-r--r--backend/unittests/Target/RISCVAssemblyPrinterTest.cpp182
-rw-r--r--backend/unittests/Target/RISCVCallLoweringTest.cpp910
-rw-r--r--backend/unittests/Target/RISCVFrameLoweringTest.cpp1365
-rw-r--r--backend/unittests/Target/RISCVLateCodeGenTest.cpp1310
-rw-r--r--backend/unittests/Target/RISCVMCEncodingCoverageTest.cpp413
-rw-r--r--backend/unittests/Target/RISCVMCTest.cpp322
-rw-r--r--backend/unittests/Target/RISCVObjectFileTest.cpp791
-rw-r--r--backend/unittests/Target/RISCVRegisterAllocationTest.cpp539
-rw-r--r--backend/unittests/Target/RISCVSSAExitTest.cpp414
-rw-r--r--backend/unittests/Target/RISCVTargetTest.cpp330
-rw-r--r--backend/unittests/Target/TableGenTest.cpp202
-rw-r--r--backend/unittests/Target/TestTarget.cpp217
-rw-r--r--backend/unittests/Target/TestTarget.h66
-rw-r--r--backend/unittests/Tooling/ArtifactStoreTest.cpp193
-rw-r--r--backend/unittests/Tooling/BackendManifestTest.cpp210
-rw-r--r--backend/unittests/Tooling/CMakeLists.txt42
-rw-r--r--backend/unittests/Tooling/MIRReducerFuzzerTest.cpp241
-rw-r--r--backend/unittests/Tooling/PipelineDriverTest.cpp301
-rw-r--r--backend/unittests/Tooling/ToolingTestSupport.h131
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> &sections() const noexcept {
+ return Sections;
+ }
+
+private:
+ explicit ELFView(std::span<const std::uint8_t> Bytes) : Bytes(Bytes) {}
+
+ std::span<const std::uint8_t> Bytes;
+ std::vector<ELFSection> Sections;
+};
+
+[[nodiscard]] const ELFSymbol *findSymbol(const std::vector<ELFSymbol> &Symbols,
+ std::string_view Name) {
+ const auto Position =
+ std::ranges::find(Symbols, Name, [](const ELFSymbol &Symbol) { return Symbol.Name; });
+ return Position == Symbols.end() ? nullptr : &*Position;
+}
+
+[[nodiscard]] rcc::Expected<rcc::Alignment> alignment(std::uint64_t Value) {
+ return rcc::Alignment::create(Value);
+}
+
+[[nodiscard]] rcc::Expected<rcc::GlobalInitializer> integer(std::uint32_t Bits,
+ std::uint64_t Value) {
+ auto Immediate = rcc::APInt::fromUnsigned(Bits, Value);
+ if (!Immediate)
+ return std::unexpected(Immediate.error());
+ return rcc::GlobalInitializer::integer(std::move(*Immediate));
+}
+
+[[nodiscard]] bool addObject(rcc::CompilationUnit &Unit, std::string Name, std::uint64_t Size,
+ std::uint64_t Alignment, rcc::GlobalLinkage Linkage,
+ std::optional<rcc::GlobalInitializer> Initializer,
+ const auto &Configure) {
+ auto Align = alignment(Alignment);
+ if (!Align)
+ return false;
+ rcc::GlobalObjectDesc Object;
+ Object.Symbol = std::move(Name);
+ Object.ObjectType = rcc::GenericConstantType::byte();
+ Object.Linkage = Linkage;
+ Object.Align = *Align;
+ Object.Size = Size;
+ Object.Initializer = std::move(Initializer);
+ Configure(Object);
+ return Unit.addGlobalObject(std::move(Object)).has_value();
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCExprRef> symbolDifference(rcc::mc::MCContext &Context,
+ std::string_view Left,
+ std::string_view Right,
+ std::int64_t Addend) {
+ auto LeftSymbol = Context.getOrCreateSymbol(std::string(Left), rcc::mc::MCSymbolKind::Generic);
+ auto RightSymbol = Context.getOrCreateSymbol(std::string(Right), rcc::mc::MCSymbolKind::Generic);
+ if (!LeftSymbol || !RightSymbol)
+ return std::unexpected(!LeftSymbol ? LeftSymbol.error() : RightSymbol.error());
+ auto LeftExpression = rcc::mc::MCExpr::symbol(*LeftSymbol);
+ auto RightExpression = rcc::mc::MCExpr::symbol(*RightSymbol);
+ if (!LeftExpression || !RightExpression)
+ return std::unexpected(!LeftExpression ? LeftExpression.error() : RightExpression.error());
+ auto Difference = rcc::mc::MCExpr::binary(
+ rcc::mc::MCBinaryOperator::Subtract, std::move(*LeftExpression), std::move(*RightExpression));
+ if (!Difference)
+ return std::unexpected(Difference.error());
+ return rcc::mc::MCExpr::binary(rcc::mc::MCBinaryOperator::Add, std::move(*Difference),
+ rcc::mc::MCExpr::constant(Addend));
+}
+
+[[nodiscard]] rcc::Expected<rcc::mc::MCExprRef>
+symbolExpression(rcc::mc::MCContext &Context, std::string_view Name,
+ rcc::mc::MCSymbolKind Kind = rcc::mc::MCSymbolKind::Generic) {
+ auto Symbol = Context.getOrCreateSymbol(std::string(Name), Kind);
+ if (!Symbol)
+ return std::unexpected(Symbol.error());
+ return rcc::mc::MCExpr::symbol(std::move(*Symbol));
+}
+
+[[nodiscard]] rcc::mc::MCInst instruction(rcc::riscv::InstructionOpcode Opcode) {
+ return rcc::mc::MCInst(rcc::mc::MCOpcode::fromValue(static_cast<std::uint32_t>(Opcode)));
+}
+
+void testMCRelaxation(TestContext &Context, const rcc::TargetDescription &Target) {
+ rcc::riscv::RISCVMCCodeEmitter Emitter(Target);
+ rcc::riscv::RISCVAsmBackend Backend;
+ rcc::mc::MCContext MCContext;
+ auto Text = MCContext.getOrCreateSection(rcc::mc::MCSectionDesc{
+ .Name = ".text.relax",
+ .Kind = rcc::mc::MCSectionKind::Text,
+ .Flags = {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Executable},
+ .Alignment = 2,
+ .EntrySize = 0,
+ .Group = {},
+ .UniqueID = std::nullopt,
+ });
+ auto JumpTarget = MCContext.createTemporarySymbol(".Ljump_target");
+ auto BranchTarget = MCContext.createTemporarySymbol(".Lbranch_target");
+ if (!Text || !JumpTarget || !BranchTarget) {
+ Context.expect(false, "RISC-V MC relaxation fixture construction must succeed");
+ return;
+ }
+ auto JumpExpression = rcc::mc::MCExpr::symbol(*JumpTarget);
+ auto BranchExpression = rcc::mc::MCExpr::symbol(*BranchTarget);
+ if (!JumpExpression || !BranchExpression) {
+ Context.expect(false, "RISC-V MC relaxation expressions must be constructible");
+ return;
+ }
+
+ rcc::mc::MCInst Jump = instruction(rcc::riscv::InstructionOpcode::C_J);
+ Jump.addOperand(rcc::mc::MCOperand::expression(*JumpExpression));
+ rcc::mc::MCInst Branch = instruction(rcc::riscv::InstructionOpcode::C_BEQZ);
+ Branch.addOperand(rcc::mc::MCOperand::registerOperand(rcc::mc::MCRegister::fromValue(
+ static_cast<std::uint32_t>(rcc::riscv::PhysicalRegisterID::X8))));
+ Branch.addOperand(rcc::mc::MCOperand::expression(*BranchExpression));
+
+ rcc::mc::MCAssembler Assembler(Emitter, Backend);
+ if (!Assembler.switchSection(*Text) || !Assembler.emitInstruction(Jump) ||
+ !Assembler.emitZeroFill(3000) || !Assembler.emitLabel(*JumpTarget) ||
+ !Assembler.emitInstruction(Branch) || !Assembler.emitZeroFill(600) ||
+ !Assembler.emitLabel(*BranchTarget)) {
+ Context.expect(false, "RISC-V MC relaxation events must succeed");
+ return;
+ }
+ auto Object = Assembler.assemble();
+ if (!Object)
+ std::cerr << "RISC-V MC relaxation failure: " << Object.error().message() << '\n';
+ Context.expect(Object.has_value(), "RISC-V MC relaxation must reach a fixed point");
+ if (!Object || Object->Sections.size() != 1)
+ return;
+ const auto JumpWord = readU32(Object->Sections[0].Contents, 0);
+ const auto BranchWord = readU32(Object->Sections[0].Contents, 3004);
+ Context.expect(JumpWord && (*JumpWord & 0xfffU) == 0x6fU,
+ "out-of-range C.J must monotonically relax to JAL x0");
+ Context.expect(BranchWord && (*BranchWord & 0x01fff07fU) == 0x00040063U,
+ "out-of-range C.BEQZ must monotonically relax to BEQ x8, x0");
+ Context.expect(Object->Sections[0].AddressSize == 3608 && Object->Sections[0].Relocations.empty(),
+ "resolved MC relaxation must update layout without linker relocations");
+
+ auto ExternalExpression = symbolExpression(MCContext, "external_jump");
+ if (!ExternalExpression) {
+ Context.expect(false, "external MC relaxation expression must be constructible");
+ return;
+ }
+ rcc::mc::MCInst ExternalJump = instruction(rcc::riscv::InstructionOpcode::C_J);
+ ExternalJump.addOperand(rcc::mc::MCOperand::expression(*ExternalExpression));
+ rcc::mc::MCAssembler Relocatable(Emitter, Backend);
+ auto Switched = Relocatable.switchSection(*Text);
+ auto Emitted = Relocatable.emitInstruction(ExternalJump);
+ auto ExternalObject = Relocatable.assemble();
+ Context.expect(
+ Switched && Emitted && ExternalObject &&
+ ExternalObject->Sections[0].Relocations.size() == 1 &&
+ ExternalObject->Sections[0].Relocations[0].Type == 45,
+ "unresolved C.J must retain one mandatory relocation without implicit linker relax");
+}
+
+void testAssemblerLayout(TestContext &Context, const rcc::TargetDescription &Target) {
+ rcc::riscv::RISCVMCCodeEmitter Emitter(Target);
+ rcc::riscv::RISCVAsmBackend Backend;
+ rcc::mc::MCContext MCContext;
+ auto Data = MCContext.getOrCreateSection(rcc::mc::MCSectionDesc{
+ .Name = ".data",
+ .Kind = rcc::mc::MCSectionKind::Data,
+ .Flags = {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Writable},
+ .Alignment = 1,
+ .EntrySize = 0,
+ .Group = {},
+ .UniqueID = std::nullopt,
+ });
+ auto BSS = MCContext.getOrCreateSection(rcc::mc::MCSectionDesc{
+ .Name = ".bss",
+ .Kind = rcc::mc::MCSectionKind::BSS,
+ .Flags = {rcc::mc::MCSectionFlag::Alloc, rcc::mc::MCSectionFlag::Writable},
+ .Alignment = 1,
+ .EntrySize = 0,
+ .Group = {},
+ .UniqueID = std::nullopt,
+ });
+ auto Difference = symbolDifference(MCContext, "left", "right", 5);
+ if (!Data || !BSS || !Difference) {
+ Context.expect(false, "MC assembler layout fixture construction must succeed");
+ return;
+ }
+
+ rcc::mc::MCAssembler Assembler(Emitter, Backend);
+ constexpr std::uint64_t LargeBSS = (std::uint64_t{1} << 32U) + 32U;
+ if (!Assembler.switchSection(*Data) || !Assembler.emitAlignment(128, 0, 0) ||
+ !Assembler.emitValue(*Difference, 8) || !Assembler.switchSection(*BSS) ||
+ !Assembler.emitAlignment(4096, 0, 0) || !Assembler.emitZeroFill(LargeBSS)) {
+ Context.expect(false, "MC assembler layout events must succeed");
+ return;
+ }
+ auto Object = Assembler.assemble();
+ if (!Object)
+ std::cerr << "MC assembler layout failure: " << Object.error().message() << '\n';
+ Context.expect(Object.has_value(), "MC assembler layout must complete");
+ if (!Object || Object->Sections.size() != 2)
+ return;
+ Context.expect(Object->Sections[0].Alignment == 128 &&
+ Object->Sections[0].Relocations.size() == 2 &&
+ Object->Sections[0].Relocations[0].Type == 36 &&
+ Object->Sections[0].Relocations[0].Addend == 5 &&
+ Object->Sections[0].Relocations[1].Type == 40 &&
+ Object->Sections[0].Relocations[1].Addend == 0,
+ "MC assembler must preserve effective alignment and paired symbol relocations");
+ Context.expect(Object->Sections[1].Alignment == 4096 &&
+ Object->Sections[1].AddressSize == LargeBSS &&
+ Object->Sections[1].Contents.empty(),
+ "MC assembler must keep NOBITS address size without materializing zero bytes");
+
+ rcc::mc::MCAssembler BadAssembler(Emitter, Backend);
+ const std::array<std::uint8_t, 1> Nonzero{1};
+ auto Switched = BadAssembler.switchSection(*BSS);
+ auto Emitted = BadAssembler.emitBytes(Nonzero);
+ auto Failure = BadAssembler.assemble();
+ Context.expect(Switched && Emitted && !Failure &&
+ Failure.error().code() == rcc::ErrorCode::ObjectWritingFailed,
+ "MC assembler must reject initialized bytes in a NOBITS section");
+}
+
+void testNativeObjectBoundaries(TestContext &Context, const rcc::TargetDescription &Target) {
+ rcc::riscv::RISCVMCCodeEmitter Emitter(Target);
+ rcc::riscv::RISCVAsmBackend Backend;
+ rcc::mc::ELF64LEObjectWriter Writer(rcc::mc::ELFObjectTargetInfo{
+ .Machine = 243, .OSABI = 0, .ABIVersion = 0, .Flags = 0, .ExtraSections = {}});
+ rcc::mc::ELFObjectStreamer Streamer(Emitter, Backend, Writer);
+ auto EmptyAssembly = Streamer.emitInlineAssembly({});
+ auto RawAssembly = Streamer.emitInlineAssembly("addi a0, a0, 1");
+ Context.expect(EmptyAssembly && !RawAssembly &&
+ RawAssembly.error().code() == rcc::ErrorCode::Unsupported,
+ "native object emission must reject unparsed inline assembly without dropping it");
+
+ rcc::mc::MCContext MCContext;
+ auto EHFrame = MCContext.getOrCreateSection(rcc::mc::MCSectionDesc{
+ .Name = ".eh_frame.boundary",
+ .Kind = rcc::mc::MCSectionKind::EHFrame,
+ .Flags = {rcc::mc::MCSectionFlag::Alloc},
+ .Alignment = 8,
+ .EntrySize = 0,
+ .Group = {},
+ .UniqueID = std::nullopt,
+ });
+ auto Function = MCContext.getOrCreateSymbol("cfi_boundary", rcc::mc::MCSymbolKind::Function);
+ auto Personality =
+ MCContext.getOrCreateSymbol("cfi_personality", rcc::mc::MCSymbolKind::Function);
+ if (!EHFrame || !Function || !Personality) {
+ Context.expect(false, "native CFI boundary fixture must be constructible");
+ return;
+ }
+ const rcc::mc::MCDwarfFrameInfo Info{.CodeAlignment = 1,
+ .DataAlignment = -8,
+ .ReturnAddressRegister = 1,
+ .InitialCFARegister = 2,
+ .InitialCFAOffset = 0};
+ const std::array UnsupportedEvents{
+ rcc::mc::MCCFIInstruction{.Operation = rcc::mc::MCCFIOperation::Personality,
+ .Operands = {0},
+ .Bytes = {},
+ .Symbol = *Personality},
+ rcc::mc::MCCFIInstruction{.Operation = rcc::mc::MCCFIOperation::LSDA,
+ .Operands = {0},
+ .Bytes = {},
+ .Symbol = *Personality},
+ rcc::mc::MCCFIInstruction{.Operation = rcc::mc::MCCFIOperation::SignalFrame,
+ .Operands = {},
+ .Bytes = {},
+ .Symbol = {}},
+ rcc::mc::MCCFIInstruction{.Operation = rcc::mc::MCCFIOperation::ReturnColumn,
+ .Operands = {1},
+ .Bytes = {},
+ .Symbol = {}},
+ rcc::mc::MCCFIInstruction{.Operation = rcc::mc::MCCFIOperation::NegateReturnAddressState,
+ .Operands = {},
+ .Bytes = {},
+ .Symbol = {}},
+ rcc::mc::MCCFIInstruction{.Operation =
+ rcc::mc::MCCFIOperation::NegateReturnAddressStateWithPC,
+ .Operands = {},
+ .Bytes = {},
+ .Symbol = {}},
+ };
+ for (const rcc::mc::MCCFIInstruction &Event : UnsupportedEvents) {
+ rcc::mc::MCDwarfFrameProcedure Procedure{
+ .Function = *Function,
+ .AddressRange = 4,
+ .Instructions = {{.Instruction = Event, .Offset = 0}},
+ };
+ auto Frame = rcc::mc::emitDwarfEHFrame(*EHFrame, std::span(&Procedure, 1), Info, Backend);
+ Context.expect(!Frame && Frame.error().code() == rcc::ErrorCode::Unsupported,
+ "unsupported RV64GC CFI extensions must fail structurally");
+ }
+}
+
+[[nodiscard]] bool buildGlobalFixture(rcc::CompilationUnit &Unit) {
+ constexpr auto NoConfiguration = [](rcc::GlobalObjectDesc &) {};
+ if (!addObject(Unit, "external_target", 0, 1, rcc::GlobalLinkage::External, std::nullopt,
+ NoConfiguration) ||
+ !addObject(Unit, "weak_target", 0, 1, rcc::GlobalLinkage::ExternalWeak, std::nullopt,
+ NoConfiguration)) {
+ return false;
+ }
+
+ constexpr std::uint64_t LargeBSS = (std::uint64_t{1} << 32U) + 32U;
+ auto LargeZero = rcc::GlobalInitializer::zeroFill(LargeBSS);
+ auto TLSValue = integer(64, 0x1122334455667788ULL);
+ auto TLSZero = rcc::GlobalInitializer::zeroFill(32);
+ auto Relocation = rcc::GlobalInitializer::symbolAddress(
+ {.Symbol = "external_target", .Addend = 7, .Size = 8, .TargetFlags = 0});
+ auto COMDATRelocation = rcc::GlobalInitializer::symbolAddress(
+ {.Symbol = "external_target", .Addend = 3, .Size = 8, .TargetFlags = 0});
+ auto HiddenValue = integer(32, 0x12345678U);
+ auto ProtectedValue = integer(64, 0x8877665544332211ULL);
+ auto ReadOnlyValue = integer(32, 0xaabbccddU);
+ auto TargetConstant = rcc::TargetConstant::create(rcc::riscv::RISCVSymbolConstant(
+ rcc::riscv::RISCVSymbolConstantKind::ExternalSymbol, "external_target", 13));
+ auto TargetInitializer =
+ TargetConstant
+ ? rcc::GlobalInitializer::target(std::move(*TargetConstant))
+ : rcc::Expected<rcc::GlobalInitializer>(std::unexpected(TargetConstant.error()));
+ if (!LargeZero || !TLSValue || !TLSZero || !Relocation || !COMDATRelocation || !HiddenValue ||
+ !ProtectedValue || !ReadOnlyValue || !TargetInitializer) {
+ return false;
+ }
+
+ std::vector<rcc::GlobalInitializer> Differences;
+ for (const auto [Size, Addend] :
+ std::array<std::pair<std::uint8_t, std::int64_t>, 4>{{{1, 11}, {2, 9}, {4, 7}, {8, 5}}}) {
+ auto Difference = rcc::GlobalInitializer::symbolDifference({.Minuend = "external_target",
+ .Subtrahend = "weak_target",
+ .Addend = Addend,
+ .Size = Size});
+ if (!Difference)
+ return false;
+ Differences.push_back(std::move(*Difference));
+ }
+ auto DifferenceAggregate = rcc::GlobalInitializer::aggregate(std::move(Differences));
+ if (!DifferenceAggregate)
+ return false;
+
+ return addObject(Unit, "large_bss", LargeBSS, 4096, rcc::GlobalLinkage::Internal,
+ std::move(*LargeZero), NoConfiguration) &&
+ addObject(Unit, "tls_data", 8, 8, rcc::GlobalLinkage::External, std::move(*TLSValue),
+ [](rcc::GlobalObjectDesc &Object) {
+ Object.TLSModel = rcc::GlobalTLSModel::LocalExec;
+ }) &&
+ addObject(Unit, "tls_bss", 32, 32, rcc::GlobalLinkage::External, std::move(*TLSZero),
+ [](rcc::GlobalObjectDesc &Object) {
+ Object.TLSModel = rcc::GlobalTLSModel::GeneralDynamic;
+ }) &&
+ addObject(Unit, "common_value", 128, 64, rcc::GlobalLinkage::Common, std::nullopt,
+ NoConfiguration) &&
+ addObject(Unit, "relro_pointer", 8, 8, rcc::GlobalLinkage::External,
+ std::move(*Relocation),
+ [](rcc::GlobalObjectDesc &Object) { Object.IsConstant = true; }) &&
+ addObject(Unit, "difference_value", 15, 1, rcc::GlobalLinkage::External,
+ std::move(*DifferenceAggregate), NoConfiguration) &&
+ addObject(Unit, "comdat_value", 8, 8, rcc::GlobalLinkage::LinkOnceODR,
+ std::move(*COMDATRelocation),
+ [](rcc::GlobalObjectDesc &Object) { Object.COMDAT = Object.Symbol; }) &&
+ addObject(Unit, "hidden_value", 4, 16, rcc::GlobalLinkage::External,
+ std::move(*HiddenValue),
+ [](rcc::GlobalObjectDesc &Object) {
+ Object.Visibility = rcc::GlobalVisibility::Hidden;
+ Object.Section = ".custom.data";
+ }) &&
+ addObject(Unit, "protected_value", 8, 128, rcc::GlobalLinkage::External,
+ std::move(*ProtectedValue),
+ [](rcc::GlobalObjectDesc &Object) {
+ Object.Visibility = rcc::GlobalVisibility::Protected;
+ }) &&
+ addObject(Unit, "readonly_value", 4, 4, rcc::GlobalLinkage::External,
+ std::move(*ReadOnlyValue),
+ [](rcc::GlobalObjectDesc &Object) { Object.IsConstant = true; }) &&
+ addObject(Unit, "target_constant_value", 8, 8, rcc::GlobalLinkage::Internal,
+ std::move(*TargetInitializer), [](rcc::GlobalObjectDesc &Object) {
+ Object.IsConstant = true;
+ Object.Section = ".data.rel.ro.target-constant";
+ });
+}
+
+void testGlobalObjectEmission(TestContext &Context, const rcc::TargetDescription &Target) {
+ rcc::CompilationUnit Unit;
+ Context.expect(buildGlobalFixture(Unit), "global object fixture construction must succeed");
+ auto First = rcc::riscv::emitObject(Unit, Target);
+ auto Second = rcc::riscv::emitObject(Unit, Target);
+ if (!First)
+ std::cerr << "first global object emission failure: " << First.error().message() << '\n';
+ if (!Second)
+ std::cerr << "second global object emission failure: " << Second.error().message() << '\n';
+ Context.expect(First && Second && *First == *Second,
+ "global object emission must be successful and deterministic");
+ if (!First)
+ return;
+ Context.expect(First->size() < std::size_t{64} * 1024U,
+ "large NOBITS globals must not inflate the ELF file");
+ auto View = ELFView::create(*First);
+ Context.expect(View.has_value(), "emitted global object must be a valid ELF64LE container");
+ if (!View)
+ return;
+
+ const ELFSection *BSS = View->section(".bss");
+ const ELFSection *Data = View->section(".data");
+ const ELFSection *TLSData = View->section(".tdata");
+ const ELFSection *TLSBSS = View->section(".tbss");
+ const ELFSection *RelRO = View->section(".data.rel.ro");
+ const ELFSection *COMDAT = View->section(".data.comdat_value");
+ const ELFSection *COMDATRelocations = View->section(".rela.data.comdat_value");
+ const ELFSection *Group = View->section(".group");
+ constexpr std::uint64_t LargeBSS = (std::uint64_t{1} << 32U) + 32U;
+ Context.expect(BSS != nullptr && BSS->Type == SHTNoBits && BSS->Size == LargeBSS &&
+ BSS->Alignment == 4096 &&
+ (BSS->Flags & (SHFWrite | SHFAlloc)) == (SHFWrite | SHFAlloc),
+ "ELF BSS section must preserve its address size, flags, and alignment");
+ Context.expect(Data != nullptr && Data->Alignment == 128,
+ "ELF data section alignment must include later fragment requirements");
+ Context.expect(TLSData != nullptr && TLSData->Type != SHTNoBits &&
+ (TLSData->Flags & SHFTLS) != 0 && TLSBSS != nullptr &&
+ TLSBSS->Type == SHTNoBits && (TLSBSS->Flags & SHFTLS) != 0,
+ "ELF TLS data and BSS sections must carry their TLS semantics");
+ Context.expect(RelRO != nullptr && COMDAT != nullptr && COMDATRelocations != nullptr &&
+ Group != nullptr && Group->Type == SHTGroup &&
+ (COMDAT->Flags & SHFGroup) != 0 && (COMDATRelocations->Flags & SHFGroup) != 0,
+ "ELF COMDAT data and relocation sections must share a group");
+ Context.expect(View->section(".custom.data") != nullptr && View->section(".rodata") != nullptr &&
+ View->section(".riscv.attributes") != nullptr,
+ "ELF custom, read-only, and RISC-V attribute sections must be present");
+ const ELFSection *TargetConstantSection = View->section(".data.rel.ro.target-constant");
+ Context.expect(TargetConstantSection != nullptr && TargetConstantSection->Size == 8 &&
+ std::ranges::all_of(View->contents(*TargetConstantSection),
+ [](std::uint8_t Byte) { return Byte == 0; }),
+ "RV64GC target constants must reserve their relocation storage");
+
+ auto Symbols = View->symbols();
+ Context.expect(Symbols.has_value(), "ELF symbol table must be parseable");
+ if (!Symbols)
+ return;
+ const ELFSymbol *External = findSymbol(*Symbols, "external_target");
+ const ELFSymbol *Weak = findSymbol(*Symbols, "weak_target");
+ const ELFSymbol *Large = findSymbol(*Symbols, "large_bss");
+ const ELFSymbol *TLS = findSymbol(*Symbols, "tls_data");
+ const ELFSymbol *Common = findSymbol(*Symbols, "common_value");
+ const ELFSymbol *Hidden = findSymbol(*Symbols, "hidden_value");
+ const ELFSymbol *Protected = findSymbol(*Symbols, "protected_value");
+ const ELFSymbol *COMDATSymbol = findSymbol(*Symbols, "comdat_value");
+ Context.expect(External != nullptr && External->Binding == 1 &&
+ External->Section == SHNUndefined && External->Type == 1 && Weak != nullptr &&
+ Weak->Binding == 2 && Weak->Section == SHNUndefined,
+ "undefined object declarations must retain global and weak bindings");
+ Context.expect(Large != nullptr && Large->Binding == 0 && Large->Size == LargeBSS &&
+ TLS != nullptr && TLS->Binding == 1 && TLS->Type == 6,
+ "local BSS and global TLS symbols must have correct ELF types and sizes");
+ Context.expect(Common != nullptr && Common->Binding == 1 && Common->Section == SHNCommon &&
+ Common->Value == 64 && Common->Size == 128,
+ "ELF common symbol alignment and size must be preserved");
+ Context.expect(Hidden != nullptr && Hidden->Visibility == 2 && Protected != nullptr &&
+ Protected->Visibility == 3 && COMDATSymbol != nullptr &&
+ COMDATSymbol->Binding == 2,
+ "ELF visibility and COMDAT weak binding must be preserved");
+
+ auto DifferenceRelocations = View->relocations(".rela.data");
+ Context.expect(DifferenceRelocations && DifferenceRelocations->size() == 8,
+ "symbol differences must emit eight paired ELF relocations");
+ if (DifferenceRelocations) {
+ const std::array<std::uint32_t, 8> ExpectedTypes{33, 37, 34, 38, 35, 39, 36, 40};
+ for (std::size_t Index = 0; Index < ExpectedTypes.size(); ++Index) {
+ Context.expect((*DifferenceRelocations)[Index].Type == ExpectedTypes[Index],
+ "symbol difference relocation type order is incorrect");
+ }
+ const std::array<std::int64_t, 8> ExpectedAddends{11, 0, 9, 0, 7, 0, 5, 0};
+ for (std::size_t Index = 0; Index < ExpectedAddends.size(); ++Index) {
+ Context.expect((*DifferenceRelocations)[Index].Addend == ExpectedAddends[Index],
+ "symbol difference relocation addend is incorrect");
+ }
+ }
+ auto Relocations = View->relocations(".rela.data.rel.ro");
+ Context.expect(Relocations && Relocations->size() == 1 && (*Relocations)[0].Type == 2 &&
+ (*Relocations)[0].Addend == 7,
+ "read-only pointer data must emit an R_RISCV_64 relocation");
+ auto COMDATFixups = View->relocations(".rela.data.comdat_value");
+ Context.expect(COMDATFixups && COMDATFixups->size() == 1 && (*COMDATFixups)[0].Type == 2 &&
+ (*COMDATFixups)[0].Addend == 3,
+ "COMDAT pointer data must retain its relocation");
+ auto TargetConstantFixups = View->relocations(".rela.data.rel.ro.target-constant");
+ Context.expect(TargetConstantFixups && TargetConstantFixups->size() == 1 &&
+ (*TargetConstantFixups)[0].Type == 2 &&
+ (*TargetConstantFixups)[0].Addend == 13,
+ "RV64GC target constants must emit their declared R_RISCV_64 relocation");
+
+ if (Group != nullptr && COMDATSymbol != nullptr) {
+ const std::span<const std::uint8_t> GroupBytes = View->contents(*Group);
+ const auto GroupFlags = readU32(GroupBytes, 0);
+ const auto COMDATIndex = View->sectionIndex(".data.comdat_value");
+ const auto RelocationIndex = View->sectionIndex(".rela.data.comdat_value");
+ bool ContainsCOMDAT = false;
+ bool ContainsRelocations = false;
+ for (std::size_t Offset = 4; Offset + 4 <= GroupBytes.size(); Offset += 4) {
+ const auto Member = readU32(GroupBytes, Offset);
+ ContainsCOMDAT |= Member && COMDATIndex && *Member == *COMDATIndex;
+ ContainsRelocations |= Member && RelocationIndex && *Member == *RelocationIndex;
+ }
+ Context.expect(GroupFlags == 1 && Group->Info < Symbols->size() &&
+ (*Symbols)[Group->Info].Name == "comdat_value" && ContainsCOMDAT &&
+ ContainsRelocations,
+ "ELF COMDAT group must name its signature and both member sections");
+ }
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ auto Target = rcc::riscv::createTargetDescription();
+ Context.expect(Target.has_value(), "RISC-V object test target creation must succeed");
+ if (!Target)
+ return Context.result();
+ testAssemblerLayout(Context, *Target);
+ testMCRelaxation(Context, *Target);
+ testNativeObjectBoundaries(Context, *Target);
+ testGlobalObjectEmission(Context, *Target);
+ return Context.result();
+}
diff --git a/backend/unittests/Target/RISCVRegisterAllocationTest.cpp b/backend/unittests/Target/RISCVRegisterAllocationTest.cpp
new file mode 100644
index 0000000..29bc756
--- /dev/null
+++ b/backend/unittests/Target/RISCVRegisterAllocationTest.cpp
@@ -0,0 +1,539 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "RISCV.h"
+#include "rcc/CodeGen/LiveIntervals.h"
+#include "rcc/CodeGen/LiveVariables.h"
+#include "rcc/CodeGen/LowLevelType.h"
+#include "rcc/CodeGen/MachineBasicBlock.h"
+#include "rcc/CodeGen/MachineFrameInfo.h"
+#include "rcc/CodeGen/MachineFunction.h"
+#include "rcc/CodeGen/MachineIRBuilder.h"
+#include "rcc/CodeGen/MachineInstr.h"
+#include "rcc/CodeGen/MachineMemOperand.h"
+#include "rcc/CodeGen/MachineOperand.h"
+#include "rcc/CodeGen/MachinePassManager.h"
+#include "rcc/CodeGen/MachineRegisterInfo.h"
+#include "rcc/CodeGen/RegAllocCommon.h"
+#include "rcc/CodeGen/RegAllocFast.h"
+#include "rcc/CodeGen/RegAllocPBQP.h"
+#include "rcc/CodeGen/TargetDescription.h"
+#include "rcc/CodeGen/TargetRegisterAllocationInfo.h"
+#include "rcc/Target/RISCV/RISCVInstructions.h"
+#include "rcc/Target/RISCV/RISCVRegisters.h"
+#include "rcc/Target/RISCV/RISCVTarget.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <iostream>
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace {
+
+class TestContext final {
+public:
+ void expect(bool Condition, std::string_view Message) {
+ if (Condition)
+ return;
+ std::cerr << Message << '\n';
+ ++Failures;
+ }
+
+ [[nodiscard]] int result() const noexcept {
+ return Failures == 0 ? 0 : 1;
+ }
+
+private:
+ int Failures = 0;
+};
+
+[[nodiscard]] constexpr rcc::RegisterClassID
+registerClass(rcc::riscv::RegisterClass Value) noexcept {
+ return rcc::RegisterClassID::fromValue(static_cast<std::uint32_t>(Value));
+}
+
+[[nodiscard]] constexpr rcc::RegisterBankID registerBank(rcc::riscv::RegisterBank Value) noexcept {
+ return rcc::RegisterBankID::fromValue(static_cast<std::uint32_t>(Value));
+}
+
+[[nodiscard]] rcc::Register physical(rcc::riscv::PhysicalRegisterID ID) {
+ return rcc::Register::fromPhysical(*rcc::riscv::physicalRegister(ID));
+}
+
+struct FunctionFixture final {
+ std::unique_ptr<rcc::MachineFunction> Function;
+ rcc::MachineBasicBlock *Block = nullptr;
+};
+
+[[nodiscard]] rcc::Expected<FunctionFixture> createFunction(std::string Name, std::uint64_t ID) {
+ auto Function =
+ rcc::MachineFunction::create(std::move(Name), rcc::MachineFunctionStableID::fromValue(ID));
+ if (!Function)
+ return std::unexpected(Function.error());
+ auto Block = (*Function)->createBasicBlock();
+ if (!Block)
+ return std::unexpected(Block.error());
+ auto Appended = (*Function)->appendBlock(**Block);
+ if (!Appended)
+ return std::unexpected(Appended.error());
+ return FunctionFixture{std::move(*Function), *Block};
+}
+
+[[nodiscard]] rcc::Expected<rcc::Register> createVReg(rcc::MachineFunction &Function,
+ rcc::LowLevelType Type,
+ rcc::riscv::RegisterBank Bank,
+ rcc::riscv::RegisterClass Class) {
+ auto Reg = Function.registerInfo().createVirtualRegister(Type);
+ if (!Reg)
+ return std::unexpected(Reg.error());
+ if (auto Result = Function.registerInfo().setRegisterBank(*Reg, registerBank(Bank)); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Function.registerInfo().setRegisterClass(*Reg, registerClass(Class)); !Result)
+ return std::unexpected(Result.error());
+ return *Reg;
+}
+
+[[nodiscard]] rcc::Expected<rcc::MachineInstr *>
+buildAddImmediate(rcc::MachineFunction &Function, const rcc::TargetDescription &Target,
+ rcc::MachineBasicBlock &Block, rcc::Register Destination, rcc::Register Source,
+ std::int64_t Immediate) {
+ rcc::MachineIRBuilder Builder(Function, Target);
+ if (auto Result = Builder.setInsertPoint(Block); !Result)
+ return std::unexpected(Result.error());
+ auto Pending =
+ Builder.buildInstr(rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::ADDI));
+ if (!Pending)
+ return std::unexpected(Pending.error());
+ if (auto Result = Pending->addDef(Destination); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Pending->addUse(Source); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Pending->addImmediate(Immediate); !Result)
+ return std::unexpected(Result.error());
+ return Pending->build();
+}
+
+[[nodiscard]] rcc::Expected<rcc::MachineInstr *> buildAdd(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target,
+ rcc::MachineBasicBlock &Block,
+ rcc::Register Destination,
+ rcc::Register Left, rcc::Register Right) {
+ rcc::MachineIRBuilder Builder(Function, Target);
+ if (auto Result = Builder.setInsertPoint(Block); !Result)
+ return std::unexpected(Result.error());
+ auto Pending =
+ Builder.buildInstr(rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::ADD));
+ if (!Pending)
+ return std::unexpected(Pending.error());
+ if (auto Result = Pending->addDef(Destination); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Pending->addUse(Left); !Result)
+ return std::unexpected(Result.error());
+ if (auto Result = Pending->addUse(Right); !Result)
+ return std::unexpected(Result.error());
+ return Pending->build();
+}
+
+[[nodiscard]] rcc::Expected<void> prepareForAllocation(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target) {
+ auto Rebuilt = rcc::rebuildMachineLiveness(Function, Target);
+ if (!Rebuilt)
+ return std::unexpected(Rebuilt.error());
+ for (const rcc::MachineFunctionProperty Property : {
+ rcc::MachineFunctionProperty::TracksLiveness,
+ rcc::MachineFunctionProperty::Legalized,
+ rcc::MachineFunctionProperty::RegBankSelected,
+ rcc::MachineFunctionProperty::Selected,
+ rcc::MachineFunctionProperty::NoPHIs,
+ rcc::MachineFunctionProperty::TiedOpsRewritten,
+ }) {
+ Function.properties().set(Property);
+ }
+ return {};
+}
+
+enum class AllocatorKind : std::uint8_t {
+ Basic,
+ Fast,
+ PBQP,
+};
+
+[[nodiscard]] rcc::Expected<void> runAllocator(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target,
+ AllocatorKind Kind,
+ rcc::RegisterAllocationOptions Options = {}) {
+ rcc::MachineFunctionAnalysisManager Analyses;
+ auto Registered = rcc::registerStage11MachineAnalyses(Analyses);
+ if (!Registered)
+ return std::unexpected(Registered.error());
+ std::unique_ptr<rcc::MachineFunctionPass> Pass;
+ switch (Kind) {
+ case AllocatorKind::Basic:
+ Pass = rcc::createBasicAllocatorPass(rcc::riscv::registerAllocationInfo(), Options);
+ break;
+ case AllocatorKind::Fast:
+ Pass = std::make_unique<rcc::FastAllocatorPass>(rcc::riscv::registerAllocationInfo());
+ break;
+ case AllocatorKind::PBQP:
+ Pass = std::make_unique<rcc::PBQPAllocatorPass>(rcc::riscv::registerAllocationInfo(), Options);
+ break;
+ }
+ rcc::MachineFunctionPassManager Pipeline;
+ auto Added = Pipeline.addPass(std::move(Pass));
+ if (!Added)
+ return std::unexpected(Added.error());
+ return Pipeline.run(Function, Target, Analyses);
+}
+
+[[nodiscard]] std::size_t countOpcode(const rcc::MachineFunction &Function,
+ rcc::riscv::InstructionOpcode Value) {
+ std::size_t Count = 0;
+ for (const rcc::MachineBasicBlock *Block = Function.firstBlock(); Block != nullptr;
+ Block = Block->next()) {
+ for (const rcc::MachineInstr *Instruction = Block->firstInstruction(); Instruction != nullptr;
+ Instruction = Instruction->next()) {
+ Count += Instruction->opcode() == rcc::riscv::instructionOpcode(Value) ? 1U : 0U;
+ }
+ }
+ return Count;
+}
+
+[[nodiscard]] std::vector<std::uint64_t> allocationSignature(const rcc::MachineFunction &Function) {
+ std::vector<std::uint64_t> Result;
+ Result.push_back(Function.frameInfo().liveObjectCount());
+ for (const rcc::MachineBasicBlock *Block = Function.firstBlock(); Block != nullptr;
+ Block = Block->next()) {
+ for (const rcc::MachineInstr *Instruction = Block->firstInstruction(); Instruction != nullptr;
+ Instruction = Instruction->next()) {
+ Result.push_back(Instruction->opcode().value());
+ for (const rcc::MachineOperand &Operand : Instruction->operands()) {
+ Result.push_back(static_cast<std::uint64_t>(Operand.kind()));
+ const rcc::Register *Reg = Operand.getIf<rcc::Register>();
+ Result.push_back(Reg == nullptr ? 0 : Reg->opaqueValue());
+ const auto *Frame = Operand.getIf<rcc::FrameIndexOperand>();
+ Result.push_back(Frame == nullptr ? 0
+ : static_cast<std::uint64_t>(Frame->Index.value()) + 1U);
+ }
+ }
+ }
+ return Result;
+}
+
+void testTargetHooks(TestContext &Context, const rcc::TargetDescription &Target) {
+ const rcc::TargetRegisterAllocationInfo &Info = rcc::riscv::registerAllocationInfo();
+ for (const rcc::riscv::RegisterClass Class : {
+ rcc::riscv::RegisterClass::GPR,
+ rcc::riscv::RegisterClass::GPRC,
+ rcc::riscv::RegisterClass::FPR64,
+ rcc::riscv::RegisterClass::FPR64C,
+ rcc::riscv::RegisterClass::FPR32,
+ rcc::riscv::RegisterClass::FPR32C,
+ rcc::riscv::RegisterClass::GPR_NX0,
+ rcc::riscv::RegisterClass::GPR_NX0_NX2,
+ }) {
+ Context.expect(Info.validateSpillReload(registerClass(Class), Target).has_value(),
+ "every allocatable RV64GC scalar class must have spill/reload hooks");
+ }
+
+ auto Fixture = createFunction("riscv-spill-hooks", 1);
+ if (!Fixture)
+ return;
+ rcc::StackObjectDesc StackDescription;
+ StackDescription.Kind = rcc::StackObjectKind::SpillSlot;
+ StackDescription.Size = 8;
+ auto Align = rcc::Alignment::create(8);
+ if (!Align)
+ return;
+ StackDescription.Align = *Align;
+ auto Slot = Fixture->Function->frameInfo().createObject(std::move(StackDescription));
+ if (!Slot)
+ return;
+ rcc::MachineIRBuilder Builder(*Fixture->Function, Target);
+ if (!Builder.setInsertPoint(*Fixture->Block))
+ return;
+
+ auto GPRReload =
+ Info.emitReload(Builder, physical(rcc::riscv::PhysicalRegisterID::X5),
+ registerClass(rcc::riscv::RegisterClass::GPR), *Slot, rcc::LaneMask::all());
+ auto GPRSpill =
+ Info.emitSpill(Builder, physical(rcc::riscv::PhysicalRegisterID::X5),
+ registerClass(rcc::riscv::RegisterClass::GPR), *Slot, rcc::LaneMask::all());
+ auto F32Reload =
+ Info.emitReload(Builder, physical(rcc::riscv::PhysicalRegisterID::F10_F),
+ registerClass(rcc::riscv::RegisterClass::FPR32), *Slot, rcc::LaneMask::all());
+ auto F32Spill =
+ Info.emitSpill(Builder, physical(rcc::riscv::PhysicalRegisterID::F10_F),
+ registerClass(rcc::riscv::RegisterClass::FPR32), *Slot, rcc::LaneMask::all());
+ auto F64Reload =
+ Info.emitReload(Builder, physical(rcc::riscv::PhysicalRegisterID::F11_D),
+ registerClass(rcc::riscv::RegisterClass::FPR64), *Slot, rcc::LaneMask::all());
+ auto F64Spill =
+ Info.emitSpill(Builder, physical(rcc::riscv::PhysicalRegisterID::F11_D),
+ registerClass(rcc::riscv::RegisterClass::FPR64), *Slot, rcc::LaneMask::all());
+ Context.expect(GPRReload && GPRSpill && F32Reload && F32Spill && F64Reload && F64Spill &&
+ (*GPRReload)->opcode() ==
+ rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::LD) &&
+ (*GPRSpill)->opcode() ==
+ rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::SD) &&
+ (*F32Reload)->opcode() ==
+ rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::FLW) &&
+ (*F32Spill)->opcode() ==
+ rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::FSW) &&
+ (*F64Reload)->opcode() ==
+ rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::FLD) &&
+ (*F64Spill)->opcode() ==
+ rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::FSD),
+ "RV64GC hooks must select width- and bank-correct stack instructions");
+ if (GPRReload && F32Reload && F64Reload) {
+ Context.expect(
+ (*GPRReload)->operandAt(1)->getIf<rcc::FrameIndexOperand>() != nullptr &&
+ (*GPRReload)->memOperands().front()->description().Size.minimumBytes() == 8 &&
+ (*F32Reload)->memOperands().front()->description().Size.minimumBytes() == 4 &&
+ (*F64Reload)->memOperands().front()->description().Size.minimumBytes() == 8,
+ "RV64GC stack instructions must retain precise FrameIndex MMO size metadata");
+ }
+ auto EmptyLane = Info.emitReload(Builder, physical(rcc::riscv::PhysicalRegisterID::X5),
+ registerClass(rcc::riscv::RegisterClass::GPR), *Slot, {});
+ Context.expect(!EmptyLane && EmptyLane.error().code() == rcc::ErrorCode::InvalidArgument,
+ "RV64GC spill hooks must reject an empty lane request");
+}
+
+void testRematerialization(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Fixture = createFunction("riscv-rematerialization", 2);
+ auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64);
+ if (!Fixture || !I64)
+ return;
+ auto Original = createVReg(*Fixture->Function, *I64, rcc::riscv::RegisterBank::GPR,
+ rcc::riscv::RegisterClass::GPR);
+ auto Child = createVReg(*Fixture->Function, *I64, rcc::riscv::RegisterBank::GPR,
+ rcc::riscv::RegisterClass::GPR);
+ if (!Original || !Child)
+ return;
+ auto Definition = buildAddImmediate(*Fixture->Function, Target, *Fixture->Block, *Original,
+ physical(rcc::riscv::PhysicalRegisterID::X0), 17);
+ if (!Definition)
+ return;
+ const rcc::TargetRegisterAllocationInfo &Info = rcc::riscv::registerAllocationInfo();
+ auto Accepted = Info.isRematerializable(**Definition, *Original, *Fixture->Function, Target);
+ rcc::MachineIRBuilder Builder(*Fixture->Function, Target);
+ if (!Builder.setInsertPointAfter(**Definition))
+ return;
+ auto Materialized = Info.rematerialize(Builder, **Definition, *Child, *Original, Target);
+ Context.expect(Accepted && *Accepted && Materialized &&
+ (*Materialized)->opcode() ==
+ rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::ADDI) &&
+ (*Materialized)->operandAt(0)->registerValue() == *Child &&
+ (*Materialized)->operandAt(1)->registerValue() ==
+ physical(rcc::riscv::PhysicalRegisterID::X0),
+ "RV64GC must rematerialize constant ADDI forms with a replacement definition");
+}
+
+[[nodiscard]] rcc::Expected<FunctionFixture>
+createSimpleAllocationFunction(const rcc::TargetDescription &Target, std::uint64_t ID) {
+ auto Fixture = createFunction("riscv-ra-" + std::to_string(ID), ID);
+ auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64);
+ if (!Fixture)
+ return std::unexpected(Fixture.error());
+ if (!I64)
+ return std::unexpected(I64.error());
+ std::vector<rcc::Register> Values;
+ for (std::int64_t Immediate : {1, 2, 3}) {
+ auto Reg = createVReg(*Fixture->Function, *I64, rcc::riscv::RegisterBank::GPR,
+ rcc::riscv::RegisterClass::GPR);
+ if (!Reg)
+ return std::unexpected(Reg.error());
+ if (auto Built = buildAddImmediate(*Fixture->Function, Target, *Fixture->Block, *Reg,
+ physical(rcc::riscv::PhysicalRegisterID::X0), Immediate);
+ !Built) {
+ return std::unexpected(Built.error());
+ }
+ Values.push_back(*Reg);
+ }
+ auto Sum = createVReg(*Fixture->Function, *I64, rcc::riscv::RegisterBank::GPR,
+ rcc::riscv::RegisterClass::GPR);
+ if (!Sum)
+ return std::unexpected(Sum.error());
+ if (auto Built =
+ buildAdd(*Fixture->Function, Target, *Fixture->Block, *Sum, Values[0], Values[1]);
+ !Built) {
+ return std::unexpected(Built.error());
+ }
+ if (auto Built = buildAdd(*Fixture->Function, Target, *Fixture->Block,
+ physical(rcc::riscv::PhysicalRegisterID::X0), *Sum, Values[2]);
+ !Built) {
+ return std::unexpected(Built.error());
+ }
+ auto Prepared = prepareForAllocation(*Fixture->Function, Target);
+ if (!Prepared)
+ return std::unexpected(Prepared.error());
+ return Fixture;
+}
+
+void testRealAllocators(TestContext &Context, const rcc::TargetDescription &Target) {
+ for (const auto [Kind, ID, Name] : {
+ std::tuple{AllocatorKind::Basic, std::uint64_t{10}, std::string_view{"Basic"}},
+ std::tuple{AllocatorKind::Fast, std::uint64_t{11}, std::string_view{"Fast"}},
+ std::tuple{AllocatorKind::PBQP, std::uint64_t{12}, std::string_view{"PBQP"}},
+ }) {
+ auto Fixture = createSimpleAllocationFunction(Target, ID);
+ if (!Fixture)
+ continue;
+ auto Allocated = runAllocator(*Fixture->Function, Target, Kind);
+ if (!Allocated)
+ std::cerr << "RV64GC " << Name << " error: " << Allocated.error().message() << '\n';
+ Context.expect(
+ Allocated && Fixture->Function->properties().has(rcc::MachineFunctionProperty::NoVRegs) &&
+ Fixture->Function->registerInfo().virtualRegisters().size() == 4,
+ "every allocator must rewrite real RV64GC selected instructions to physical registers");
+ if (!Allocated)
+ continue;
+ bool OnlyPhysical = true;
+ for (const rcc::MachineInstr *Instruction : Fixture->Function->allInstructions()) {
+ if (Instruction == nullptr || Instruction->parent() == nullptr)
+ continue;
+ for (const rcc::MachineOperand &Operand : Instruction->operands()) {
+ const rcc::Register *Reg = Operand.getIf<rcc::Register>();
+ OnlyPhysical &= Reg == nullptr || !Reg->isVirtual();
+ }
+ }
+ Context.expect(OnlyPhysical,
+ "RV64GC allocator output must not retain a linked virtual-register operand");
+ }
+}
+
+void testFunctionReservedRegisters(TestContext &Context, const rcc::TargetDescription &Target) {
+ for (const auto [Kind, ID, Name] : {
+ std::tuple{AllocatorKind::Basic, std::uint64_t{13}, std::string_view{"Basic"}},
+ std::tuple{AllocatorKind::Fast, std::uint64_t{14}, std::string_view{"Fast"}},
+ std::tuple{AllocatorKind::PBQP, std::uint64_t{15}, std::string_view{"PBQP"}},
+ }) {
+ auto Fixture = createSimpleAllocationFunction(Target, ID);
+ auto Align64 = rcc::Alignment::create(64);
+ if (!Fixture || !Align64)
+ continue;
+ rcc::StackObjectDesc Description;
+ Description.Size = 8;
+ Description.Align = *Align64;
+ if (!Fixture->Function->frameInfo().createObject(std::move(Description)))
+ continue;
+ auto Allocated = runAllocator(*Fixture->Function, Target, Kind);
+ const rcc::PhysicalRegister FP =
+ *rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X8);
+ const rcc::PhysicalRegister BP =
+ *rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X9);
+ bool AllocatedFrameRegister = false;
+ for (const rcc::MachineInstr *Instruction : Fixture->Function->allInstructions()) {
+ if (Instruction == nullptr || Instruction->parent() == nullptr)
+ continue;
+ for (const rcc::MachineOperand &Operand : Instruction->operands()) {
+ const rcc::Register *Reg = Operand.getIf<rcc::Register>();
+ const auto Physical =
+ Reg != nullptr && Reg->isPhysical() ? Reg->physicalRegister() : std::nullopt;
+ AllocatedFrameRegister |= Physical && (*Physical == FP || *Physical == BP);
+ }
+ }
+ if (!Allocated)
+ std::cerr << "RV64GC " << Name << " reservation error: " << Allocated.error().message()
+ << '\n';
+ Context.expect(Allocated && Fixture->Function->registerInfo().isReserved(FP) &&
+ Fixture->Function->registerInfo().isReserved(BP) && !AllocatedFrameRegister,
+ "every allocator must persist and honor function-level FP/BP reservations");
+ }
+}
+
+[[nodiscard]] rcc::Expected<FunctionFixture>
+createPressureFunction(const rcc::TargetDescription &Target, std::uint64_t ID) {
+ auto Fixture = createFunction("riscv-pressure-" + std::to_string(ID), ID);
+ auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64);
+ if (!Fixture)
+ return std::unexpected(Fixture.error());
+ if (!I64)
+ return std::unexpected(I64.error());
+
+ std::vector<rcc::Register> Values;
+ Values.reserve(30);
+ for (std::int64_t Immediate = 0; Immediate < 30; ++Immediate) {
+ auto Reg = createVReg(*Fixture->Function, *I64, rcc::riscv::RegisterBank::GPR,
+ rcc::riscv::RegisterClass::GPR);
+ if (!Reg)
+ return std::unexpected(Reg.error());
+ auto Built = buildAddImmediate(*Fixture->Function, Target, *Fixture->Block, *Reg,
+ physical(rcc::riscv::PhysicalRegisterID::X0), Immediate);
+ if (!Built)
+ return std::unexpected(Built.error());
+ Values.push_back(*Reg);
+ }
+ const rcc::Register Zero = physical(rcc::riscv::PhysicalRegisterID::X0);
+ for (const rcc::Register Value : Values) {
+ auto Built = buildAdd(*Fixture->Function, Target, *Fixture->Block, Zero, Value, Zero);
+ if (!Built)
+ return std::unexpected(Built.error());
+ }
+ auto Prepared = prepareForAllocation(*Fixture->Function, Target);
+ if (!Prepared)
+ return std::unexpected(Prepared.error());
+ return Fixture;
+}
+
+void testRealPressureAndDeterminism(TestContext &Context, const rcc::TargetDescription &Target) {
+ std::vector<std::uint64_t> FirstPBQPSignature;
+ for (const auto [Kind, ID, Name] : {
+ std::tuple{AllocatorKind::Basic, std::uint64_t{20}, std::string_view{"Basic"}},
+ std::tuple{AllocatorKind::PBQP, std::uint64_t{21}, std::string_view{"PBQP"}},
+ std::tuple{AllocatorKind::PBQP, std::uint64_t{22}, std::string_view{"PBQP-repeat"}},
+ }) {
+ auto Fixture = createPressureFunction(Target, ID);
+ if (!Fixture)
+ continue;
+ auto Allocated =
+ runAllocator(*Fixture->Function, Target, Kind, {.EnableRematerialization = false});
+ if (!Allocated)
+ std::cerr << "RV64GC pressure " << Name << " error: " << Allocated.error().message() << '\n';
+ Context.expect(Allocated && Fixture->Function->frameInfo().liveObjectCount() != 0 &&
+ countOpcode(*Fixture->Function, rcc::riscv::InstructionOpcode::LD) != 0 &&
+ countOpcode(*Fixture->Function, rcc::riscv::InstructionOpcode::SD) != 0,
+ "RV64GC global allocators must spill and reload real GPR pressure");
+ if (!Allocated || Kind != AllocatorKind::PBQP)
+ continue;
+ const std::vector<std::uint64_t> Signature = allocationSignature(*Fixture->Function);
+ if (FirstPBQPSignature.empty())
+ FirstPBQPSignature = Signature;
+ else
+ Context.expect(Signature == FirstPBQPSignature,
+ "RV64GC PBQP allocation must be stable across identical functions");
+ }
+
+ auto RematerializedPressure = createPressureFunction(Target, 23);
+ if (RematerializedPressure) {
+ auto Allocated = runAllocator(*RematerializedPressure->Function, Target, AllocatorKind::PBQP);
+ if (!Allocated)
+ std::cerr << "RV64GC rematerialization pressure error: " << Allocated.error().message()
+ << '\n';
+ Context.expect(Allocated && RematerializedPressure->Function->properties().has(
+ rcc::MachineFunctionProperty::NoVRegs),
+ "RV64GC PBQP rematerialization must finish with a valid register map");
+ }
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ auto Target = rcc::riscv::createTargetDescription();
+ Context.expect(Target.has_value(), "RV64GC register-allocation target must be constructible");
+ if (!Target) {
+ std::cerr << "target construction error: " << Target.error().message() << '\n';
+ return Context.result();
+ }
+ testTargetHooks(Context, *Target);
+ testRematerialization(Context, *Target);
+ testRealAllocators(Context, *Target);
+ testFunctionReservedRegisters(Context, *Target);
+ testRealPressureAndDeterminism(Context, *Target);
+ return Context.result();
+}
diff --git a/backend/unittests/Target/RISCVSSAExitTest.cpp b/backend/unittests/Target/RISCVSSAExitTest.cpp
new file mode 100644
index 0000000..1912666
--- /dev/null
+++ b/backend/unittests/Target/RISCVSSAExitTest.cpp
@@ -0,0 +1,414 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "RISCV.h"
+#include "RISCVInstrInfo.h"
+#include "rcc/CodeGen/GenericOpcodes.h"
+#include "rcc/CodeGen/LiveVariables.h"
+#include "rcc/CodeGen/MachineBasicBlock.h"
+#include "rcc/CodeGen/MachineCFGUpdater.h"
+#include "rcc/CodeGen/MachineFunction.h"
+#include "rcc/CodeGen/MachineIRBuilder.h"
+#include "rcc/CodeGen/MachineInstr.h"
+#include "rcc/CodeGen/MachineOperand.h"
+#include "rcc/CodeGen/MachinePassManager.h"
+#include "rcc/CodeGen/MachineVerifier.h"
+#include "rcc/CodeGen/PHIElimination.h"
+#include "rcc/Target/RISCV/RISCVTarget.h"
+
+#include <algorithm>
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <iostream>
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace {
+
+class TestContext final {
+public:
+ void expect(bool Condition, std::string_view Message) {
+ if (Condition)
+ return;
+ std::cerr << Message << '\n';
+ ++Failures;
+ }
+
+ [[nodiscard]] int result() const noexcept {
+ return Failures == 0 ? 0 : 1;
+ }
+
+private:
+ int Failures = 0;
+};
+
+[[nodiscard]] rcc::RegisterBankID bank(rcc::riscv::RegisterBank Bank) {
+ return rcc::RegisterBankID::fromValue(static_cast<std::uint32_t>(Bank));
+}
+
+[[nodiscard]] rcc::RegisterClassID regClass(rcc::riscv::RegisterClass Class) {
+ return rcc::RegisterClassID::fromValue(static_cast<std::uint32_t>(Class));
+}
+
+[[nodiscard]] rcc::Expected<rcc::Register> createVReg(rcc::MachineFunction &Function,
+ rcc::LowLevelType Type,
+ rcc::riscv::RegisterBank Bank,
+ rcc::riscv::RegisterClass Class) {
+ auto Reg = Function.registerInfo().createVirtualRegister(Type);
+ if (!Reg)
+ return std::unexpected(Reg.error());
+ auto BankResult = Function.registerInfo().setRegisterBank(*Reg, bank(Bank));
+ if (!BankResult)
+ return std::unexpected(BankResult.error());
+ auto ClassResult = Function.registerInfo().setRegisterClass(*Reg, regClass(Class));
+ if (!ClassResult)
+ return std::unexpected(ClassResult.error());
+ return *Reg;
+}
+
+[[nodiscard]] rcc::Expected<rcc::MachineBasicBlock *> appendBlock(rcc::MachineFunction &Function) {
+ auto Block = Function.createBasicBlock();
+ if (!Block)
+ return std::unexpected(Block.error());
+ auto Appended = Function.appendBlock(**Block);
+ if (!Appended)
+ return std::unexpected(Appended.error());
+ return *Block;
+}
+
+[[nodiscard]] rcc::Expected<rcc::MachineInstr *>
+buildAddImmediate(rcc::MachineFunction &Function, const rcc::TargetDescription &Target,
+ rcc::MachineBasicBlock &Block, rcc::Register Destination, rcc::Register Source,
+ std::int64_t Immediate) {
+ rcc::MachineIRBuilder Builder(Function, Target);
+ auto Point = Builder.setInsertPoint(Block);
+ if (!Point)
+ return std::unexpected(Point.error());
+ auto Pending =
+ Builder.buildInstr(rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::ADDI));
+ if (!Pending)
+ return std::unexpected(Pending.error());
+ auto Def = Pending->addDef(Destination);
+ if (!Def)
+ return std::unexpected(Def.error());
+ auto Use = Pending->addUse(Source);
+ if (!Use)
+ return std::unexpected(Use.error());
+ auto Value = Pending->addImmediate(Immediate);
+ if (!Value)
+ return std::unexpected(Value.error());
+ return Pending->build();
+}
+
+[[nodiscard]] rcc::Expected<rcc::MachineInstr *>
+buildBranchEqual(rcc::MachineFunction &Function, const rcc::TargetDescription &Target,
+ rcc::MachineBasicBlock &Block, rcc::Register Left, rcc::Register Right,
+ rcc::MachineBasicBlock &Destination) {
+ rcc::MachineIRBuilder Builder(Function, Target);
+ auto Point = Builder.setInsertPoint(Block);
+ if (!Point)
+ return std::unexpected(Point.error());
+ auto Pending =
+ Builder.buildInstr(rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::BEQ));
+ if (!Pending)
+ return std::unexpected(Pending.error());
+ auto LeftUse = Pending->addUse(Left);
+ if (!LeftUse)
+ return std::unexpected(LeftUse.error());
+ auto RightUse = Pending->addUse(Right);
+ if (!RightUse)
+ return std::unexpected(RightUse.error());
+ auto TargetOperand = Pending->addBasicBlock(Destination);
+ if (!TargetOperand)
+ return std::unexpected(TargetOperand.error());
+ return Pending->build();
+}
+
+[[nodiscard]] rcc::Expected<rcc::MachineInstr *>
+buildPhi(rcc::MachineFunction &Function, const rcc::TargetDescription &Target,
+ rcc::MachineBasicBlock &Block, rcc::Register Destination,
+ const std::array<std::pair<rcc::Register, rcc::MachineBasicBlock *>, 2> &Incoming) {
+ rcc::MachineIRBuilder Builder(Function, Target);
+ auto Point = Builder.setInsertPoint(Block);
+ if (!Point)
+ return std::unexpected(Point.error());
+ auto Pending = Builder.buildInstr(rcc::opcode(rcc::GenericOpcode::PHI));
+ if (!Pending)
+ return std::unexpected(Pending.error());
+ auto Def = Pending->addDef(Destination);
+ if (!Def)
+ return std::unexpected(Def.error());
+ for (const auto &[Value, Predecessor] : Incoming) {
+ auto Use = Pending->addUse(Value);
+ if (!Use)
+ return std::unexpected(Use.error());
+ auto IncomingBlock = Pending->addBasicBlock(*Predecessor);
+ if (!IncomingBlock)
+ return std::unexpected(IncomingBlock.error());
+ }
+ return Pending->build();
+}
+
+[[nodiscard]] const rcc::BasicBlockOperand *blockOperand(const rcc::MachineInstr &Instruction,
+ std::size_t Index) {
+ const rcc::MachineOperand *Operand = Instruction.operandAt(Index);
+ return Operand == nullptr ? nullptr : Operand->getIf<rcc::BasicBlockOperand>();
+}
+
+[[nodiscard]] std::vector<rcc::MachineInstr *> linkedInstructions(rcc::MachineBasicBlock &Block) {
+ std::vector<rcc::MachineInstr *> Result;
+ for (rcc::MachineInstr *Instruction = Block.firstInstruction(); Instruction != nullptr;
+ Instruction = Instruction->next()) {
+ Result.push_back(Instruction);
+ }
+ return Result;
+}
+
+[[nodiscard]] rcc::Expected<void> setSelectedSSAProperties(rcc::MachineFunction &Function,
+ const rcc::TargetDescription &Target) {
+ auto Rebuilt = rcc::rebuildMachineLiveness(Function, Target);
+ if (!Rebuilt)
+ return std::unexpected(Rebuilt.error());
+ for (rcc::MachineFunctionProperty Property :
+ {rcc::MachineFunctionProperty::IsSSA, rcc::MachineFunctionProperty::TracksLiveness,
+ rcc::MachineFunctionProperty::Legalized, rcc::MachineFunctionProperty::RegBankSelected,
+ rcc::MachineFunctionProperty::Selected}) {
+ Function.properties().set(Property);
+ }
+ return {};
+}
+
+void testBehaviorAndCopyLegality(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Function = rcc::MachineFunction::create("riscv_ssa_behavior",
+ rcc::MachineFunctionStableID::fromValue(1));
+ if (!Function)
+ return;
+ auto First = appendBlock(**Function);
+ auto Second = appendBlock(**Function);
+ auto Third = appendBlock(**Function);
+ if (!First || !Second || !Third)
+ return;
+ auto CFG = (*Function)->cfgUpdater();
+ if (!CFG.addSuccessor(**First, **Second, rcc::BranchProbability::one()))
+ return;
+
+ auto Fallthrough =
+ rcc::riscv::instrInfo().isFallthroughEdge(**First, **Second, **Function, Target);
+ Context.expect(Fallthrough && *Fallthrough,
+ "RV64GC SSA behavior must recognize an empty layout fallthrough edge");
+ auto Inserted = rcc::riscv::instrInfo().insertBranch(**First, **Second, **Function, Target);
+ Context.expect(Inserted.has_value(), "RV64GC SSA behavior must insert an unconditional branch");
+ if (!Inserted)
+ return;
+ rcc::MachineInstr *Jump = (*First)->lastInstruction();
+ const auto X0 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X0);
+ Context.expect(Jump != nullptr &&
+ Jump->opcode() ==
+ rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::JAL) &&
+ Jump->operandAt(0)->registerValue() == rcc::Register::fromPhysical(*X0) &&
+ blockOperand(*Jump, 1) != nullptr && blockOperand(*Jump, 1)->Block == *Second,
+ "RV64GC unconditional branches must use JAL with x0 as the link destination");
+ auto NoFallthrough =
+ rcc::riscv::instrInfo().isFallthroughEdge(**First, **Second, **Function, Target);
+ Context.expect(NoFallthrough && !*NoFallthrough, "JAL must terminate RV64GC layout fallthrough");
+ auto Rewritten =
+ rcc::riscv::instrInfo().rewriteBranchTarget(**First, **Second, **Third, **Function, Target);
+ Context.expect(Rewritten && *Rewritten && blockOperand(*Jump, 1)->Block == *Third,
+ "RV64GC branch target rewriting must update direct JAL operands");
+
+ auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64);
+ auto F64 = rcc::LowLevelType::makeCategoryNeutralScalar(64);
+ auto F32 = rcc::LowLevelType::makeCategoryNeutralScalar(32);
+ if (!I64 || !F64 || !F32)
+ return;
+ auto GPR =
+ createVReg(**Function, *I64, rcc::riscv::RegisterBank::GPR, rcc::riscv::RegisterClass::GPR);
+ auto OtherGPR =
+ createVReg(**Function, *I64, rcc::riscv::RegisterBank::GPR, rcc::riscv::RegisterClass::GPR);
+ auto FPR64 =
+ createVReg(**Function, *F64, rcc::riscv::RegisterBank::FPR, rcc::riscv::RegisterClass::FPR64);
+ auto FPR32 =
+ createVReg(**Function, *F32, rcc::riscv::RegisterBank::FPR, rcc::riscv::RegisterClass::FPR32);
+ auto Unclassified = (*Function)->registerInfo().createVirtualRegister(*I64);
+ if (!GPR || !OtherGPR || !FPR64 || !FPR32 || !Unclassified)
+ return;
+
+ Context.expect(
+ rcc::riscv::instrInfo().validateCopy(*GPR, {}, *OtherGPR, {}, **Function, Target).has_value(),
+ "RV64GC must allow a compatible GPR COPY");
+ Context.expect(
+ rcc::riscv::instrInfo().validateCopy(*GPR, {}, *FPR64, {}, **Function, Target).has_value(),
+ "RV64GC must allow supported 64-bit GPR/FPR bit moves");
+ Context.expect(!rcc::riscv::instrInfo().validateCopy(*FPR64, {}, *FPR32, {}, **Function, Target),
+ "RV64GC must reject incompatible same-bank COPY widths");
+ Context.expect(!rcc::riscv::instrInfo().validateCopy(*GPR, rcc::SubRegIndex::fromValue(1),
+ *OtherGPR, {}, **Function, Target),
+ "RV64GC must reject unmodeled subregister COPYs");
+ Context.expect(
+ !rcc::riscv::instrInfo().validateCopy(*GPR, {}, *Unclassified, {}, **Function, Target),
+ "RV64GC must reject COPYs involving an unconstrained virtual register");
+}
+
+void testCompressedConditionalBranchBehavior(TestContext &Context,
+ const rcc::TargetDescription &Target) {
+ auto Function = rcc::MachineFunction::create("riscv_compressed_branch",
+ rcc::MachineFunctionStableID::fromValue(3));
+ if (!Function)
+ return;
+ auto Source = appendBlock(**Function);
+ auto LayoutSuccessor = appendBlock(**Function);
+ auto BranchTarget = appendBlock(**Function);
+ auto ReplacementTarget = appendBlock(**Function);
+ if (!Source || !LayoutSuccessor || !BranchTarget || !ReplacementTarget)
+ return;
+ auto Half = rcc::BranchProbability::fromFraction(1, 2);
+ auto CFG = (*Function)->cfgUpdater();
+ if (!Half || !CFG.addSuccessor(**Source, **BranchTarget, *Half) ||
+ !CFG.addSuccessor(**Source, **LayoutSuccessor, *Half)) {
+ Context.expect(false, "compressed-branch CFG construction must succeed");
+ return;
+ }
+
+ auto X8 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X8);
+ rcc::MachineIRBuilder Builder(**Function, Target);
+ auto Point = Builder.setInsertPoint(**Source);
+ if (!Point) {
+ Context.expect(false, "compressed conditional branch insertion point must be established");
+ return;
+ }
+ auto Pending =
+ Builder.buildInstr(rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::C_BEQZ));
+ if (!X8 || !Pending || !Pending->addUse(rcc::Register::fromPhysical(*X8)) ||
+ !Pending->addBasicBlock(**BranchTarget)) {
+ Context.expect(false, "compressed conditional branch construction must succeed");
+ return;
+ }
+ auto Branch = Pending->build();
+ if (!Branch) {
+ Context.expect(false, "compressed conditional branch insertion must succeed");
+ return;
+ }
+
+ auto Fallthrough =
+ rcc::riscv::instrInfo().isFallthroughEdge(**Source, **LayoutSuccessor, **Function, Target);
+ auto Analysis = rcc::riscv::instrInfo().analyzeBranch(**Source, **Function, Target);
+ Context.expect(Fallthrough && *Fallthrough && Analysis && Analysis->Conditional == *Branch &&
+ Analysis->TrueTarget == *BranchTarget &&
+ Analysis->FalseTarget == *LayoutSuccessor,
+ "RV64GC instruction information must recognize compressed conditional branches");
+
+ auto Rewritten = rcc::riscv::instrInfo().rewriteBranchTarget(
+ **Source, **BranchTarget, **ReplacementTarget, **Function, Target);
+ Context.expect(Rewritten && *Rewritten && blockOperand(**Branch, 1) != nullptr &&
+ blockOperand(**Branch, 1)->Block == *ReplacementTarget,
+ "RV64GC instruction information must rewrite compressed branch targets");
+}
+
+void testRV64GCCriticalEdge(TestContext &Context, const rcc::TargetDescription &Target) {
+ auto Function = rcc::MachineFunction::create("riscv_ssa_critical_edge",
+ rcc::MachineFunctionStableID::fromValue(2));
+ if (!Function)
+ return;
+ auto From = appendBlock(**Function);
+ auto Side = appendBlock(**Function);
+ auto Other = appendBlock(**Function);
+ auto Merge = appendBlock(**Function);
+ if (!From || !Side || !Other || !Merge)
+ return;
+ auto I64 = rcc::LowLevelType::makeCategoryNeutralScalar(64);
+ auto X0 = rcc::riscv::physicalRegister(rcc::riscv::PhysicalRegisterID::X0);
+ if (!I64 || !X0)
+ return;
+ auto FromValue =
+ createVReg(**Function, *I64, rcc::riscv::RegisterBank::GPR, rcc::riscv::RegisterClass::GPR);
+ auto OtherValue =
+ createVReg(**Function, *I64, rcc::riscv::RegisterBank::GPR, rcc::riscv::RegisterClass::GPR);
+ auto Destination =
+ createVReg(**Function, *I64, rcc::riscv::RegisterBank::GPR, rcc::riscv::RegisterClass::GPR);
+ if (!FromValue || !OtherValue || !Destination)
+ return;
+
+ auto Half = rcc::BranchProbability::fromFraction(1, 2);
+ auto CFG = (*Function)->cfgUpdater();
+ Context.expect(Half && CFG.addSuccessor(**From, **Side, *Half) &&
+ CFG.addSuccessor(**From, **Merge, *Half) &&
+ CFG.addSuccessor(**Other, **Merge, rcc::BranchProbability::one()),
+ "RV64GC critical-edge CFG must be connected");
+ const rcc::Register Zero = rcc::Register::fromPhysical(*X0);
+ Context.expect(buildAddImmediate(**Function, Target, **From, *FromValue, Zero, 1) &&
+ buildBranchEqual(**Function, Target, **From, Zero, Zero, **Merge) &&
+ buildAddImmediate(**Function, Target, **Other, *OtherValue, Zero, 2),
+ "RV64GC critical-edge instructions must be built");
+ const std::array Incoming{
+ std::pair<rcc::Register, rcc::MachineBasicBlock *>{*FromValue, *From},
+ std::pair<rcc::Register, rcc::MachineBasicBlock *>{*OtherValue, *Other}};
+ Context.expect(buildPhi(**Function, Target, **Merge, *Destination, Incoming).has_value(),
+ "RV64GC critical-edge PHI must be built");
+ auto InitialLiveness = setSelectedSSAProperties(**Function, Target);
+ Context.expect(InitialLiveness.has_value(), "RV64GC PHI liveness rebuild must succeed");
+ if (!InitialLiveness)
+ return;
+
+ rcc::MachineFunctionPassManager Pipeline;
+ Context.expect(
+ Pipeline.addPass(std::make_unique<rcc::PHIEliminationPass>(rcc::riscv::instrInfo()))
+ .has_value(),
+ "RV64GC PHI-elimination pass must be registered");
+ rcc::MachineFunctionAnalysisManager Analyses;
+ auto Run = Pipeline.run(**Function, Target, Analyses);
+ Context.expect(Run.has_value(), "RV64GC critical-edge PHI elimination must succeed");
+ if (!Run)
+ return;
+
+ rcc::MachineBasicBlock *Edge = (*Merge)->previous();
+ Context.expect(Edge != nullptr && Edge != *Other && Edge->previous() == *Other,
+ "RV64GC critical edge must receive a dedicated layout block");
+ if (Edge == nullptr || Edge == *Other)
+ return;
+ rcc::MachineInstr *Conditional = (*From)->lastInstruction();
+ Context.expect(Conditional != nullptr &&
+ Conditional->opcode() ==
+ rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::BEQ) &&
+ blockOperand(*Conditional, 2) != nullptr &&
+ blockOperand(*Conditional, 2)->Block == Edge,
+ "RV64GC BEQ must be redirected to the critical-edge block");
+
+ const auto OtherInstructions = linkedInstructions(**Other);
+ const auto EdgeInstructions = linkedInstructions(*Edge);
+ Context.expect(OtherInstructions.size() == 3 &&
+ OtherInstructions[1]->opcode() == rcc::opcode(rcc::GenericOpcode::COPY) &&
+ OtherInstructions[2]->opcode() ==
+ rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::JAL) &&
+ blockOperand(*OtherInstructions[2], 1)->Block == *Merge,
+ "RV64GC must materialize an unrelated fallthrough before edge-block insertion");
+ Context.expect(EdgeInstructions.size() == 2 &&
+ EdgeInstructions[0]->opcode() == rcc::opcode(rcc::GenericOpcode::COPY) &&
+ EdgeInstructions[1]->opcode() ==
+ rcc::riscv::instructionOpcode(rcc::riscv::InstructionOpcode::JAL) &&
+ EdgeInstructions[1]->operandAt(0)->registerValue() == Zero &&
+ blockOperand(*EdgeInstructions[1], 1)->Block == *Merge,
+ "RV64GC edge block must transfer the PHI value and jump to the merge");
+ Context.expect((*Function)->properties().has(rcc::MachineFunctionProperty::NoPHIs) &&
+ !(*Function)->properties().has(rcc::MachineFunctionProperty::IsSSA),
+ "RV64GC PHI elimination must publish SSA-exit properties");
+ Context.expect(rcc::MachineVerifier::verify(**Function, Target).has_value(),
+ "RV64GC SSA-exit result must pass MachineVerifier");
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ auto Target = rcc::riscv::createTargetDescription();
+ Context.expect(Target.has_value(), "RV64GC target must be created for SSA-exit tests");
+ if (!Target)
+ return Context.result();
+ testBehaviorAndCopyLegality(Context, *Target);
+ testCompressedConditionalBranchBehavior(Context, *Target);
+ testRV64GCCriticalEdge(Context, *Target);
+ return Context.result();
+}
diff --git a/backend/unittests/Target/RISCVTargetTest.cpp b/backend/unittests/Target/RISCVTargetTest.cpp
new file mode 100644
index 0000000..c067168
--- /dev/null
+++ b/backend/unittests/Target/RISCVTargetTest.cpp
@@ -0,0 +1,330 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/Target/RISCV/RISCVTarget.h"
+
+#include "rcc/CodeGen/GenericOpcodes.h"
+
+#include <algorithm>
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <iostream>
+#include <ranges>
+#include <string_view>
+
+namespace {
+
+class TestContext final {
+public:
+ void expect(bool Condition, std::string_view Message) {
+ if (Condition)
+ return;
+ std::cerr << Message << '\n';
+ ++FailureCount;
+ }
+
+ [[nodiscard]] int result() const noexcept {
+ return FailureCount == 0 ? 0 : 1;
+ }
+
+private:
+ int FailureCount = 0;
+};
+
+[[nodiscard]] rcc::PhysicalRegister registerID(rcc::riscv::PhysicalRegisterID ID) {
+ const auto Register = rcc::riscv::physicalRegister(ID);
+ if (!Register)
+ std::terminate();
+ return *Register;
+}
+
+void testRegisterFacts(TestContext &Context) {
+ auto Registers = rcc::riscv::createRegisterInfo();
+ Context.expect(Registers.has_value(), "RV64GC register description must be valid");
+ if (!Registers)
+ return;
+
+ const rcc::PhysicalRegister X0 = registerID(rcc::riscv::PhysicalRegisterID::X0);
+ const rcc::PhysicalRegister X2 = registerID(rcc::riscv::PhysicalRegisterID::X2);
+ const rcc::PhysicalRegister X5 = registerID(rcc::riscv::PhysicalRegisterID::X5);
+ const rcc::PhysicalRegister X8 = registerID(rcc::riscv::PhysicalRegisterID::X8);
+ const rcc::PhysicalRegister F31D = registerID(rcc::riscv::PhysicalRegisterID::F31_D);
+ const rcc::PhysicalRegister F31F = registerID(rcc::riscv::PhysicalRegisterID::F31_F);
+ const rcc::PhysicalRegister FFLAGS = registerID(rcc::riscv::PhysicalRegisterID::FFLAGS);
+ const rcc::PhysicalRegister FRM = registerID(rcc::riscv::PhysicalRegisterID::FRM);
+ const rcc::PhysicalRegister V8 = registerID(rcc::riscv::PhysicalRegisterID::V8);
+
+ Context.expect(
+ Registers->physicalRegisters().size() == 326 && Registers->registerUnits().size() == 98 &&
+ Registers->registerClasses().size() == 18 && Registers->registerBanks().size() == 4,
+ "RISC-V must expose scalar, vector, restricted, and status physical registers");
+ Context.expect(Registers->physicalRegister("x0") != nullptr &&
+ Registers->physicalRegister("zero") == nullptr &&
+ Registers->physicalRegisterByAlternativeName("ZERO") != nullptr &&
+ Registers->physicalRegisterByAlternativeName("zero")->Reg == X0 &&
+ Registers->physicalRegisterByAlternativeName("Sp") != nullptr &&
+ Registers->physicalRegisterByAlternativeName("sp")->Reg == X2 &&
+ Registers->physicalRegisterMIRName(X8) == "x8",
+ "canonical MIR names and case-insensitive ABI aliases must use separate lookups");
+ Context.expect(
+ Registers->isReservedRegister(X0) && Registers->isReservedRegister(X2) &&
+ !Registers->isReservedRegister(X5),
+ "zero, stack, global, and thread register constraints must be recorded statically");
+ Context.expect(Registers->physicalRegister(F31D) != nullptr &&
+ Registers->physicalRegister(F31D)->Encoding == 31 &&
+ Registers->physicalRegister("f31_d") != nullptr &&
+ Registers->physicalRegister("f31_d")->Reg == F31D &&
+ Registers->physicalRegister("f31_f") != nullptr &&
+ Registers->physicalRegister("f31_f")->Reg == F31F &&
+ Registers->physicalRegisterMIRName(F31D) == "f31_d" &&
+ Registers->physicalRegisterMIRName(F31F) == "f31_f" &&
+ Registers->regsOverlap(F31D, F31F) &&
+ Registers->subRegister(F31D, rcc::SubRegIndex::fromValue(1)) == F31F,
+ "FPR32 and FPR64 MIR views must retain encoding, overlap, and subregister facts");
+ Context.expect(Registers->physicalRegister("v8") != nullptr &&
+ Registers->physicalRegister("v8")->Reg == V8 &&
+ Registers->physicalRegister("v8")->Encoding == 8 &&
+ Registers->physicalRegisterMIRName(V8) == "v8" &&
+ Registers->registerClass("vr") != nullptr &&
+ !Registers->registerClass("vr")->Allocatable &&
+ Registers->registerClassContains(Registers->registerClass("vr")->ID, V8),
+ "vector physical registers must retain LLVM MIR names and encodings");
+ const auto *V8V9 = Registers->physicalRegister("v8_v9");
+ const auto *V28V29V30 = Registers->physicalRegister("v28_v29_v30");
+ const auto *V0ThroughV7 = Registers->physicalRegister("v0_v1_v2_v3_v4_v5_v6_v7");
+ Context.expect(V8V9 != nullptr && V28V29V30 != nullptr && V0ThroughV7 != nullptr &&
+ Registers->regsOverlap(V8V9->Reg, V8) &&
+ !Registers->regsOverlap(V28V29V30->Reg, V8) &&
+ Registers->regsOverlap(V0ThroughV7->Reg,
+ registerID(rcc::riscv::PhysicalRegisterID::V7)) &&
+ Registers->registerClass("vrtuple2") != nullptr &&
+ !Registers->registerClass("vrtuple2")->Allocatable,
+ "vector tuple registers must expose canonical names and physical overlap");
+ Context.expect(Registers->physicalRegister("fflags") != nullptr &&
+ Registers->physicalRegister("fflags")->Reg == FFLAGS &&
+ Registers->isReservedRegister(FFLAGS) &&
+ Registers->registerClass("fflags") != nullptr &&
+ !Registers->registerClass("fflags")->Allocatable,
+ "FFLAGS must be a reserved nonallocatable status register");
+ Context.expect(Registers->physicalRegister("frm") != nullptr &&
+ Registers->physicalRegister("frm")->Reg == FRM &&
+ Registers->isReservedRegister(FRM) &&
+ Registers->registerClass("frm") != nullptr &&
+ !Registers->registerClass("frm")->Allocatable,
+ "FRM must be a reserved nonallocatable status register");
+
+ const auto *GPR = Registers->registerClass("gpr");
+ Context.expect(
+ GPR != nullptr && std::ranges::find(GPR->AllocationOrder, X0) == GPR->AllocationOrder.end() &&
+ std::ranges::find(GPR->AllocationOrder, X2) == GPR->AllocationOrder.end() &&
+ std::ranges::find(GPR->AllocationOrder, X5) != GPR->AllocationOrder.end(),
+ "the general-register allocation order must exclude statically reserved registers");
+ Context.expect(Registers->registerClass("gpr_nx0") != nullptr &&
+ Registers->registerClass("gpr_nx0_nx2") != nullptr,
+ "compressed instructions must have explicit nonzero register classes");
+}
+
+void testABIAndDebugFacts(TestContext &Context) {
+ const rcc::PhysicalRegister X1 = registerID(rcc::riscv::PhysicalRegisterID::X1);
+ const rcc::PhysicalRegister X10 = registerID(rcc::riscv::PhysicalRegisterID::X10);
+ const rcc::PhysicalRegister F8D = registerID(rcc::riscv::PhysicalRegisterID::F8_D);
+ const rcc::PhysicalRegister F8F = registerID(rcc::riscv::PhysicalRegisterID::F8_F);
+ const rcc::PhysicalRegister F10D = registerID(rcc::riscv::PhysicalRegisterID::F10_D);
+ const rcc::PhysicalRegister F10F = registerID(rcc::riscv::PhysicalRegisterID::F10_F);
+ const rcc::PhysicalRegister F31D = registerID(rcc::riscv::PhysicalRegisterID::F31_D);
+ const rcc::PhysicalRegister F31F = registerID(rcc::riscv::PhysicalRegisterID::F31_F);
+
+ const auto LP64Mask = rcc::riscv::createCallPreservedMask(rcc::riscv::CallingConvention::LP64);
+ const auto LP64DMask = rcc::riscv::createCallPreservedMask(rcc::riscv::CallingConvention::LP64D);
+ Context.expect(
+ LP64DMask != nullptr && LP64DMask->preserves(X1) && LP64DMask->preserves(F8D) &&
+ LP64DMask->preserves(F8F) && LP64DMask->clobbers(X10) && LP64DMask->clobbers(F10D) &&
+ LP64DMask->clobbers(F10F) && LP64Mask != nullptr && LP64Mask->preserves(X1) &&
+ LP64Mask->clobbers(F8D) && LP64Mask->clobbers(F8F) && LP64Mask->clobbers(X10),
+ "LP64 and LP64D masks must distinguish integer and floating callee-saved registers");
+
+ auto MIRText = rcc::riscv::createMIRTextInfo();
+ Context.expect(MIRText.has_value(), "RV64GC MIR text facts must be valid");
+ if (!MIRText)
+ return;
+ const auto TextLP64Mask = MIRText->registerMask("csr_ilp32_lp64");
+ const auto TextLP64DMask = MIRText->registerMask("csr_ilp32d_lp64d");
+ Context.expect(MIRText->registerMasks().size() == 2 && TextLP64Mask != nullptr &&
+ LP64Mask != nullptr && *TextLP64Mask == *LP64Mask &&
+ TextLP64DMask != nullptr && LP64DMask != nullptr &&
+ *TextLP64DMask == *LP64DMask,
+ "RV64GC must expose named LP64 and LP64D call-preserved masks");
+ Context.expect(MIRText->directOperandFlagMask() == 127 &&
+ MIRText->directOperandFlag("riscv-call") == 1 &&
+ MIRText->directOperandFlag("riscv-hi") == 32 &&
+ MIRText->directOperandFlag("riscv-lo") == 64,
+ "RV64GC MIR target flags must expose canonical call and address spellings");
+ Context.expect(MIRText->dwarfRegister(X1) == 1,
+ "the return-address register must retain its DWARF register number");
+ Context.expect(MIRText->dwarfRegister(F31D) == 63 && MIRText->dwarfRegister(F31F) == 63 &&
+ MIRText->physicalRegister(63) == F31D,
+ "typed floating views must share one canonical DWARF register mapping");
+
+ const auto Conventions = rcc::riscv::callingConventions();
+ const auto *LP64 = rcc::riscv::callingConvention(rcc::riscv::CallingConvention::LP64);
+ const auto *LP64D = rcc::riscv::callingConvention(rcc::riscv::CallingConvention::LP64D);
+ Context.expect(
+ Conventions.size() == 2 && LP64 != nullptr && LP64->Name == "lp64" &&
+ LP64->IntegerArgumentRegisters.size() == 8 && LP64->FloatingArgumentRegisters.empty() &&
+ LP64->IntegerReturnRegisters.size() == 2 && LP64->FloatingReturnRegisters.empty() &&
+ LP64->PreservedMaskName == "CSR_ILP32_LP64" && LP64D != nullptr &&
+ LP64D->Name == "lp64d" && LP64D->IntegerArgumentRegisters.size() == 8 &&
+ LP64D->FloatingArgumentRegisters.size() == 8 &&
+ LP64D->IntegerReturnRegisters.size() == 2 && LP64D->FloatingReturnRegisters.size() == 2 &&
+ LP64D->StackAlignmentBytes == 16 && LP64D->StackArgumentAlignmentBytes == 16 &&
+ LP64D->SupportsVarArgs && LP64D->SupportsTailCalls &&
+ LP64D->PreservedMaskName == "CSR_ILP32D_LP64D",
+ "LP64 and LP64D calling-convention tables must expose complete ABI facts");
+}
+
+void testInstructionFacts(TestContext &Context) {
+ constexpr std::size_t TargetInstructionCount =
+ static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::MAXU) -
+ static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::LUI) + 1U;
+ auto Target = rcc::riscv::createTargetDescription();
+ Context.expect(Target.has_value(), "RV64GC target description must be structurally valid");
+ if (!Target) {
+ std::cerr << "target construction error: " << Target.error().message() << '\n';
+ return;
+ }
+
+ const auto *Addi = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::ADDI)));
+ const auto *Load = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::LD)));
+ const auto *Branch = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::BEQ)));
+ const auto *Floating = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::FADD_S)));
+ const auto *Atomic = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::LR_W)));
+ const auto *ShiftAdd = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::SH1ADD)));
+ const auto *ShiftLeftUnsignedWord = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::SLLI_UW)));
+ const auto *SignExtendByte = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::SEXT_B)));
+ const auto *SignedMinimum = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::MIN)));
+ const auto *Compressed = Target->instructionDescriptor(rcc::Opcode::fromValue(
+ static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::C_ADDI16SP)));
+ const auto *GenericAdd = Target->instructionDescriptor(rcc::opcode(rcc::GenericOpcode::G_ADD));
+ const auto *PseudoCall = Target->instructionDescriptor(rcc::Opcode::fromValue(
+ static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::PseudoCALL)));
+ const auto *PseudoTail = Target->instructionDescriptor(rcc::Opcode::fromValue(
+ static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::PseudoTAIL)));
+ const auto *Assembly = rcc::riscv::assemblyInstruction(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::ADDI)));
+ const auto *FenceAssembly = rcc::riscv::assemblyInstruction(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(rcc::riscv::InstructionOpcode::FENCE_I)));
+
+ constexpr std::array SymbolicLowOpcodes = {
+ rcc::riscv::InstructionOpcode::LB, rcc::riscv::InstructionOpcode::LH,
+ rcc::riscv::InstructionOpcode::LW, rcc::riscv::InstructionOpcode::LBU,
+ rcc::riscv::InstructionOpcode::LHU, rcc::riscv::InstructionOpcode::LWU,
+ rcc::riscv::InstructionOpcode::SB, rcc::riscv::InstructionOpcode::SH,
+ rcc::riscv::InstructionOpcode::SW, rcc::riscv::InstructionOpcode::LD,
+ rcc::riscv::InstructionOpcode::SD, rcc::riscv::InstructionOpcode::FLW,
+ rcc::riscv::InstructionOpcode::FSW, rcc::riscv::InstructionOpcode::FLD,
+ rcc::riscv::InstructionOpcode::FSD,
+ };
+ const rcc::OperandKindSet SymbolicLowKinds = {
+ rcc::MachineOperand::Kind::Immediate, rcc::MachineOperand::Kind::ConstantPoolIndex,
+ rcc::MachineOperand::Kind::JumpTableIndex, rcc::MachineOperand::Kind::GlobalSymbol,
+ rcc::MachineOperand::Kind::ExternalSymbol, rcc::MachineOperand::Kind::BlockAddress,
+ rcc::MachineOperand::Kind::MCSymbol,
+ };
+ Context.expect(
+ Load != nullptr && Load->Operands.size() == 3 &&
+ std::ranges::all_of(SymbolicLowOpcodes,
+ [&](rcc::riscv::InstructionOpcode Opcode) {
+ const auto *Descriptor = Target->instructionDescriptor(
+ rcc::Opcode::fromValue(static_cast<std::uint32_t>(Opcode)));
+ return Descriptor != nullptr && Descriptor->Operands.size() == 3 &&
+ Descriptor->Operands[2].Kinds == SymbolicLowKinds &&
+ Descriptor->Operands[2].ImmediatePredicate ==
+ Load->Operands[2].ImmediatePredicate &&
+ Descriptor->Operands[2].AllowedTargetFlags == 68;
+ }),
+ "RV64GC loads and stores must accept SIMM12 low-part symbolic operands");
+
+ Context.expect(
+ Target->instructions().descriptors().size() ==
+ TargetInstructionCount + rcc::genericInstructionDescriptors().size() &&
+ Addi != nullptr && Addi->Name == "ADDI" && Addi->NumExplicitDefs == 1 &&
+ Addi->Operands.size() == 3 &&
+ Addi->Operands[1].Kinds.contains(rcc::MachineOperand::Kind::FrameIndex) &&
+ Addi->Operands.back().ImmediatePredicate && Load != nullptr &&
+ Load->Flags.has(rcc::InstructionFlag::MayLoad) && !Load->RequiresMemoryOperands &&
+ Branch != nullptr && Branch->Flags.has(rcc::InstructionFlag::Terminator) &&
+ Branch->Flags.has(rcc::InstructionFlag::Branch) && Floating != nullptr &&
+ Floating->Operands.size() == 4 && Floating->Operands.back().ImmediatePredicate &&
+ Floating->AllowExtraImplicitOperands && Atomic != nullptr &&
+ Atomic->Flags.has(rcc::InstructionFlag::MayLoad) && !Atomic->RequiresMemoryOperands &&
+ Compressed != nullptr && Compressed->ImplicitDefs.size() == 1 &&
+ Compressed->ImplicitUses.size() == 1 && GenericAdd != nullptr &&
+ GenericAdd->Flags.has(rcc::InstructionFlag::Generic) && PseudoCall != nullptr &&
+ PseudoCall->Operands.size() == 2 && PseudoCall->Operands[1].Optional &&
+ PseudoTail != nullptr && PseudoTail->Operands.size() == 2 &&
+ PseudoTail->Operands[1].Optional && PseudoCall->Flags.has(rcc::InstructionFlag::Pseudo) &&
+ PseudoCall->Flags.has(rcc::InstructionFlag::Call) &&
+ rcc::riscv::assemblyInstruction(PseudoCall->InstructionOpcode) == nullptr &&
+ Assembly != nullptr && Assembly->Mnemonic == "addi" &&
+ Assembly->Format == rcc::AssemblyFormatID::fromValue(static_cast<std::uint32_t>(
+ rcc::riscv::AssemblyFormat::RD_RS1_IMM)),
+ "generated instruction and assembly descriptors must drive the formal RV64GC target");
+ Context.expect(
+ Load != nullptr &&
+ Target->instructions()
+ .validateMemOperands(Load->InstructionOpcode,
+ std::span<const rcc::MachineMemOperand *const>{})
+ .has_value() &&
+ !Target->instructions().validateMemOperands(
+ rcc::opcode(rcc::GenericOpcode::G_LOAD),
+ std::span<const rcc::MachineMemOperand *const>{}),
+ "target memory operands must be optional while generic memory operands remain required");
+ Context.expect(ShiftAdd != nullptr && ShiftAdd->Operands.size() == 3 &&
+ ShiftLeftUnsignedWord != nullptr &&
+ ShiftLeftUnsignedWord->Operands.size() == 3 &&
+ ShiftLeftUnsignedWord->Operands.back().ImmediatePredicate &&
+ SignExtendByte != nullptr && SignExtendByte->Operands.size() == 2 &&
+ SignedMinimum != nullptr && SignedMinimum->Operands.size() == 3 &&
+ SignedMinimum->Flags.has(rcc::InstructionFlag::Commutable),
+ "RISC-V Zba and Zbb instructions must expose complete generated constraints");
+
+ const auto Schedules = rcc::riscv::schedulingClasses();
+ const auto Encodings = rcc::riscv::encodingCategories();
+ const auto Formats = rcc::riscv::assemblyFormats();
+ Context.expect(Schedules.size() == 15 && Encodings.size() == 10 && Formats.size() == 35 &&
+ rcc::riscv::assemblyInstructions().size() == 211 && FenceAssembly != nullptr &&
+ FenceAssembly->Mnemonic == "fence.i" &&
+ std::ranges::any_of(Schedules,
+ [](const rcc::riscv::SchedulingClassInfo &Class) {
+ return Class.Name == "load" && Class.MicroOps == 1 &&
+ Class.Latency == 3;
+ }) &&
+ std::ranges::any_of(Encodings,
+ [](const rcc::riscv::EncodingCategoryInfo &Category) {
+ return Category.Name == "i" && Category.SizeInBytes == 4;
+ }) &&
+ std::ranges::any_of(Formats,
+ [](const rcc::riscv::AssemblyFormatInfo &Format) {
+ return Format.Name == "rd-rs1-imm";
+ }),
+ "generated scheduling, encoding, and assembly tables must preserve target facts");
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ testRegisterFacts(Context);
+ testABIAndDebugFacts(Context);
+ testInstructionFacts(Context);
+ return Context.result();
+}
diff --git a/backend/unittests/Target/TableGenTest.cpp b/backend/unittests/Target/TableGenTest.cpp
new file mode 100644
index 0000000..af4d718
--- /dev/null
+++ b/backend/unittests/Target/TableGenTest.cpp
@@ -0,0 +1,202 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "TableGenBackends.h"
+
+#include <cstddef>
+#include <iostream>
+#include <string>
+#include <string_view>
+
+namespace {
+
+class TestContext final {
+public:
+ void expect(bool Condition, std::string_view Message) {
+ if (Condition)
+ return;
+ std::cerr << Message << '\n';
+ ++FailureCount;
+ }
+
+ [[nodiscard]] int result() const noexcept {
+ return FailureCount == 0 ? 0 : 1;
+ }
+
+private:
+ int FailureCount = 0;
+};
+
+[[nodiscard]] std::string validDescription(std::string_view Aliases = "[acc]") {
+ return std::string(R"(schema: rcc-target-description-v2
+target:
+ namespace: tiny
+ name: Tiny
+ triple: tiny-unknown-none
+ cpu: tiny-cpu
+ abi: tiny-abi
+ data_layout: e-p:64:64
+ features: [base]
+register_units:
+ - { id: 1, symbol: U_R1, name: r1.unit }
+registers:
+ - { id: 1, symbol: R1, name: r1, encoding: 0, dwarf_register: 0, units: [{ unit: U_R1, lanes: 1 }], aliases: )") +
+ std::string(Aliases) + R"( }
+register_banks:
+ - { id: 1, symbol: GPR, name: GPR, maximum_size_bits: 64 }
+register_classes:
+ - { id: 1, symbol: GPR64, name: GPR64, bank: GPR, size_bits: 64, spill_size_bits: 64, spill_alignment_bits: 64, lanes: 1, registers: [R1], allocation_order: [R1] }
+register_masks:
+ - { name: CSR_TINY, preserved: [R1] }
+mir_operand_flags:
+ direct_mask: 31
+ direct: [{ name: tiny-call, value: 1 }]
+ bitmask: [{ name: tiny-relax, value: 32 }]
+calling_conventions:
+ - { id: 1, symbol: TINY, name: tiny, integer_register_class: GPR64, floating_register_class: GPR64, integer_argument_registers: [R1], floating_argument_registers: [R1], integer_return_registers: [R1], floating_return_registers: [R1], preserved_mask: CSR_TINY, stack_alignment_bytes: 8, stack_argument_alignment_bytes: 8, shadow_space_bytes: 0, supports_varargs: true, supports_tail_calls: true }
+named_operands:
+ - { id: 1, symbol: RD, name: rd }
+ - { id: 2, symbol: IMM, name: imm }
+immediate_predicates:
+ - { id: 1, symbol: SIMM8, name: simm8, kind: signed, bits: 8 }
+scheduling_classes:
+ - { id: 1, symbol: ALU, name: alu, micro_ops: 1, latency: 1 }
+encoding_categories:
+ - { id: 1, symbol: I, name: i, size_bytes: 4 }
+assembly_formats:
+ - { id: 1, symbol: RD_IMM, name: rd-imm }
+instructions:
+ - { id: 1024, symbol: LOAD_IMM, name: LOAD_IMM, assembly_mnemonic: load.imm, encoding: 19, operands: [{ kinds: [register], role: def, register_class: GPR64, register_bank: GPR, name: RD }, { kinds: [immediate, global_symbol], immediate_predicate: SIMM8, name: IMM, allowed_target_flags: 33 }], num_explicit_defs: 1, flags: [produces_code], scheduling_class: ALU, encoding_category: I, assembly_format: RD_IMM }
+)";
+}
+
+void testGeneration(TestContext &Context) {
+ const auto First = rcc::targetgen::generateDescription("tiny.rccdesc", validDescription());
+ const auto Second = rcc::targetgen::generateDescription("tiny.rccdesc", validDescription());
+ if (!First.succeeded()) {
+ for (const rcc::TextDiagnostic &Diagnostic : First.Diagnostics)
+ std::cerr << Diagnostic.Message << '\n';
+ }
+ Context.expect(First.succeeded() && Second.succeeded(),
+ "a complete restricted target description must generate successfully");
+ if (!First.succeeded() || !Second.succeeded() || !First.Output || !Second.Output)
+ return;
+ const auto &FirstOutput = First.Output.value();
+ const auto &SecondOutput = Second.Output.value();
+
+ Context.expect(FirstOutput.RegisterHeader == SecondOutput.RegisterHeader &&
+ FirstOutput.InstructionHeader == SecondOutput.InstructionHeader &&
+ FirstOutput.TargetTables == SecondOutput.TargetTables,
+ "target description generation must be deterministic");
+ Context.expect(
+ FirstOutput.RegisterHeader.find("enum class PhysicalRegisterID") != std::string::npos &&
+ FirstOutput.RegisterHeader.find("R1 = 1") != std::string::npos &&
+ FirstOutput.InstructionHeader.find("enum class InstructionOpcode") != std::string::npos &&
+ FirstOutput.InstructionHeader.find("LOAD_IMM = 1024") != std::string::npos &&
+ FirstOutput.TargetTables.find("appendGeneratedRegisterInfo") != std::string::npos &&
+ FirstOutput.TargetTables.find("appendGeneratedInstructionInfo") != std::string::npos &&
+ FirstOutput.TargetTables.find("GeneratedAssemblyInstructions") != std::string::npos &&
+ FirstOutput.TargetTables.find("Description.DirectOperandFlagMask = 31") !=
+ std::string::npos &&
+ FirstOutput.TargetTables.find("\"tiny-call\", 1") != std::string::npos &&
+ FirstOutput.TargetTables.find("\"load.imm\"") != std::string::npos &&
+ FirstOutput.TargetTables.find("makePreservedMask") != std::string::npos,
+ "generated output must contain typed target IDs, assembly mnemonics, and static target "
+ "tables");
+}
+
+void testSemanticRejection(TestContext &Context) {
+ const auto CollidingAlias =
+ rcc::targetgen::generateDescription("bad-alias.rccdesc", validDescription("[r1]"));
+ if (CollidingAlias.succeeded())
+ std::cerr << "colliding alias description unexpectedly generated\n";
+ Context.expect(!CollidingAlias.succeeded() && !CollidingAlias.Diagnostics.empty() &&
+ CollidingAlias.Diagnostics.front().Message.find("aliases") !=
+ std::string::npos,
+ "an ABI alias that collides with a canonical name must be rejected");
+
+ std::string UnknownUnit = validDescription();
+ constexpr std::string_view UnitReference = "unit: U_R1";
+ const std::size_t UnitPosition = UnknownUnit.find(UnitReference);
+ if (UnitPosition != std::string::npos)
+ UnknownUnit.replace(UnitPosition, UnitReference.size(), "unit: U_MISSING");
+ const auto BadUnit = rcc::targetgen::generateDescription("bad-unit.rccdesc", UnknownUnit);
+ Context.expect(!BadUnit.succeeded() && !BadUnit.Diagnostics.empty() &&
+ BadUnit.Diagnostics.front().Message.find("declared units") !=
+ std::string::npos,
+ "a register unit reference must resolve during semantic validation");
+
+ std::string UnknownClass = validDescription();
+ constexpr std::string_view ClassReference = "register_class: GPR64";
+ const std::size_t ClassPosition = UnknownClass.find(ClassReference);
+ if (ClassPosition != std::string::npos)
+ UnknownClass.replace(ClassPosition, ClassReference.size(), "register_class: MISSING");
+ const auto BadClass =
+ rcc::targetgen::generateDescription("bad-instruction-class.rccdesc", UnknownClass);
+ Context.expect(!BadClass.succeeded() && !BadClass.Diagnostics.empty() &&
+ BadClass.Diagnostics.front().Message.find("register class") !=
+ std::string::npos,
+ "instruction register classes must resolve during semantic validation");
+
+ std::string BadAllowedValue = validDescription();
+ constexpr std::string_view Predicate = "kind: signed, bits: 8";
+ const std::size_t PredicatePosition = BadAllowedValue.find(Predicate);
+ if (PredicatePosition != std::string::npos) {
+ BadAllowedValue.replace(PredicatePosition, Predicate.size(),
+ "kind: signed, bits: 8, allowed_values: [128]");
+ }
+ const auto InvalidAllowedValue =
+ rcc::targetgen::generateDescription("bad-allowed-value.rccdesc", BadAllowedValue);
+ Context.expect(!InvalidAllowedValue.succeeded() && !InvalidAllowedValue.Diagnostics.empty() &&
+ InvalidAllowedValue.Diagnostics.front().Message.find("allowed_values") !=
+ std::string::npos,
+ "immediate predicates must reject allowed values outside their signed range");
+
+ std::string ContradictoryNonZero = validDescription();
+ if (PredicatePosition != std::string::npos) {
+ const std::size_t Position = ContradictoryNonZero.find(Predicate);
+ ContradictoryNonZero.replace(Position, Predicate.size(),
+ "kind: signed, bits: 8, nonzero: true, allowed_values: [0]");
+ }
+ const auto InvalidNonZero =
+ rcc::targetgen::generateDescription("bad-nonzero.rccdesc", ContradictoryNonZero);
+ Context.expect(!InvalidNonZero.succeeded() && !InvalidNonZero.Diagnostics.empty() &&
+ InvalidNonZero.Diagnostics.front().Message.find("allowed_values") !=
+ std::string::npos,
+ "nonzero immediate predicates must reject an explicit zero value");
+
+ std::string BadStackAlignment = validDescription();
+ constexpr std::string_view StackAlignment =
+ "stack_alignment_bytes: 8, stack_argument_alignment_bytes: 8";
+ const std::size_t StackPosition = BadStackAlignment.find(StackAlignment);
+ if (StackPosition != std::string::npos) {
+ BadStackAlignment.replace(StackPosition, StackAlignment.size(),
+ "stack_alignment_bytes: 8, stack_argument_alignment_bytes: 16");
+ }
+ const auto InvalidStackAlignment =
+ rcc::targetgen::generateDescription("bad-stack-alignment.rccdesc", BadStackAlignment);
+ Context.expect(!InvalidStackAlignment.succeeded() && !InvalidStackAlignment.Diagnostics.empty() &&
+ InvalidStackAlignment.Diagnostics.front().Message.find("stack alignments") !=
+ std::string::npos,
+ "calling conventions must reject incompatible stack alignments");
+
+ std::string BadMnemonic = validDescription();
+ constexpr std::string_view Mnemonic = "assembly_mnemonic: load.imm";
+ const std::size_t MnemonicPosition = BadMnemonic.find(Mnemonic);
+ if (MnemonicPosition != std::string::npos)
+ BadMnemonic.replace(MnemonicPosition, Mnemonic.size(), "assembly_mnemonic: .load");
+ const auto InvalidMnemonic =
+ rcc::targetgen::generateDescription("bad-mnemonic.rccdesc", BadMnemonic);
+ Context.expect(!InvalidMnemonic.succeeded() && !InvalidMnemonic.Diagnostics.empty() &&
+ InvalidMnemonic.Diagnostics.front().Message.find("assembly mnemonics") !=
+ std::string::npos,
+ "assembly mnemonics must use a canonical lowercase spelling");
+}
+
+} // namespace
+
+int main() {
+ TestContext Context;
+ testGeneration(Context);
+ testSemanticRejection(Context);
+ return Context.result();
+}
diff --git a/backend/unittests/Target/TestTarget.cpp b/backend/unittests/Target/TestTarget.cpp
new file mode 100644
index 0000000..a367454
--- /dev/null
+++ b/backend/unittests/Target/TestTarget.cpp
@@ -0,0 +1,217 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "Target/TestTarget.h"
+
+#include <cstdint>
+#include <memory>
+#include <utility>
+#include <vector>
+
+namespace rcc::test {
+namespace {
+
+[[nodiscard]] PhysicalRegister phys(std::uint32_t Number) {
+ return *PhysicalRegister::create(Number);
+}
+
+[[nodiscard]] RegisterUnitDesc unit(std::uint32_t ID, const char *Name) {
+ return RegisterUnitDesc{RegisterUnitID::fromValue(ID), Name};
+}
+
+[[nodiscard]] RegisterUnitLane unitLane(std::uint32_t ID, std::uint64_t Lanes) {
+ return RegisterUnitLane{RegisterUnitID::fromValue(ID), LaneMask::fromRaw(Lanes)};
+}
+
+[[nodiscard]] PhysicalRegisterDesc physicalRegister(std::uint32_t Number, const char *Name,
+ std::uint32_t Encoding,
+ std::vector<RegisterUnitLane> Units) {
+ return PhysicalRegisterDesc{phys(Number), Name, Encoding, std::move(Units),
+ false, false, false, {}};
+}
+
+[[nodiscard]] RegisterClassDesc
+registerClass(RegisterClassID ID, const char *Name, std::vector<PhysicalRegister> Registers,
+ std::vector<PhysicalRegister> AllocationOrder, std::uint32_t SizeInBits,
+ LaneMask Lanes, RegisterBankID DefaultBank,
+ std::vector<SubRegIndex> SupportedSubRegs = {},
+ std::vector<RegisterClassID> SuperClasses = {}, bool Allocatable = true,
+ bool CoveredBySubRegs = false) {
+ return RegisterClassDesc{
+ ID,
+ Name,
+ std::move(Registers),
+ std::move(AllocationOrder),
+ SizeInBits,
+ SizeInBits,
+ SizeInBits,
+ Lanes,
+ std::move(SupportedSubRegs),
+ std::move(SuperClasses),
+ DefaultBank,
+ Allocatable,
+ 1,
+ CoveredBySubRegs,
+ };
+}
+
+} // namespace
+
+TargetRegisterInfoDesc createTestTargetRegisterInfoDescription() {
+ TargetRegisterInfoDesc Description;
+ Description.RegisterUnits = {
+ unit(1, "g0.lo16"), unit(2, "g0.bits16_31"), unit(3, "g0.bits32_47"), unit(4, "g0.hi16"),
+ unit(5, "g1.lo16"), unit(6, "g1.bits16_31"), unit(7, "g1.bits32_47"), unit(8, "g1.hi16"),
+ unit(9, "sp"), unit(10, "f0"), unit(11, "f1"),
+ };
+
+ Description.PhysicalRegisters = {
+ physicalRegister(TestRegister::G0, "g0", 0,
+ {unitLane(1, 0x1), unitLane(2, 0x2), unitLane(3, 0x4), unitLane(4, 0x8)}),
+ physicalRegister(TestRegister::G0Low32, "g0l", 0, {unitLane(1, 0x1), unitLane(2, 0x2)}),
+ physicalRegister(TestRegister::G0High32, "g0h", 0, {unitLane(3, 0x1), unitLane(4, 0x2)}),
+ physicalRegister(TestRegister::G0Low16, "g0w", 0, {unitLane(1, 0x1)}),
+ physicalRegister(TestRegister::G1, "g1", 1,
+ {unitLane(5, 0x1), unitLane(6, 0x2), unitLane(7, 0x4), unitLane(8, 0x8)}),
+ physicalRegister(TestRegister::G1Low32, "g1l", 1, {unitLane(5, 0x1), unitLane(6, 0x2)}),
+ physicalRegister(TestRegister::G1High32, "g1h", 1, {unitLane(7, 0x1), unitLane(8, 0x2)}),
+ physicalRegister(TestRegister::G1Low16, "g1w", 1, {unitLane(5, 0x1)}),
+ physicalRegister(TestRegister::StackPointer, "sp", 2, {unitLane(9, 0xF)}),
+ physicalRegister(TestRegister::F0, "f0", 0, {unitLane(10, 0x1)}),
+ physicalRegister(TestRegister::F1, "f1", 1, {unitLane(11, 0x1)}),
+ };
+
+ Description.SubRegIndices = {
+ SubRegIndexDesc{TestSubReg::Low32, "low32", LaneMask::fromRaw(0x3), 32, 0},
+ SubRegIndexDesc{TestSubReg::High32, "high32", LaneMask::fromRaw(0xC), 32, 32},
+ SubRegIndexDesc{TestSubReg::Low16, "low16", LaneMask::fromRaw(0x1), 16, 0},
+ };
+ Description.PhysicalSubRegisters = {
+ {phys(TestRegister::G0), TestSubReg::Low32, phys(TestRegister::G0Low32)},
+ {phys(TestRegister::G0), TestSubReg::High32, phys(TestRegister::G0High32)},
+ {phys(TestRegister::G0), TestSubReg::Low16, phys(TestRegister::G0Low16)},
+ {phys(TestRegister::G0Low32), TestSubReg::Low16, phys(TestRegister::G0Low16)},
+ {phys(TestRegister::G1), TestSubReg::Low32, phys(TestRegister::G1Low32)},
+ {phys(TestRegister::G1), TestSubReg::High32, phys(TestRegister::G1High32)},
+ {phys(TestRegister::G1), TestSubReg::Low16, phys(TestRegister::G1Low16)},
+ {phys(TestRegister::G1Low32), TestSubReg::Low16, phys(TestRegister::G1Low16)},
+ };
+ Description.SubRegCompositions = {
+ {TestSubReg::Low32, TestSubReg::Low16, TestSubReg::Low16},
+ };
+
+ const auto G0 = phys(TestRegister::G0);
+ const auto G0Low32 = phys(TestRegister::G0Low32);
+ const auto G0High32 = phys(TestRegister::G0High32);
+ const auto G0Low16 = phys(TestRegister::G0Low16);
+ const auto G1 = phys(TestRegister::G1);
+ const auto G1Low32 = phys(TestRegister::G1Low32);
+ const auto G1High32 = phys(TestRegister::G1High32);
+ const auto G1Low16 = phys(TestRegister::G1Low16);
+ const auto StackPointer = phys(TestRegister::StackPointer);
+ const auto F0 = phys(TestRegister::F0);
+ const auto F1 = phys(TestRegister::F1);
+
+ Description.RegisterClasses = {
+ registerClass(TestRegisterClass::GPR64, "GPR64", {G0, G1}, {G0, G1}, 64,
+ LaneMask::fromRaw(0xF), TestRegisterBank::GPR,
+ {TestSubReg::Low32, TestSubReg::High32, TestSubReg::Low16}, {}, true, true),
+ registerClass(TestRegisterClass::GPR64Alloc, "GPR64Alloc", {G0, G1}, {G1, G0}, 64,
+ LaneMask::fromRaw(0xF), TestRegisterBank::GPR, {}, {TestRegisterClass::GPR64}),
+ registerClass(TestRegisterClass::GPR32, "GPR32", {G0Low32, G0High32, G1Low32, G1High32},
+ {G0Low32, G1Low32, G0High32, G1High32}, 32, LaneMask::fromRaw(0x3),
+ TestRegisterBank::GPR),
+ registerClass(TestRegisterClass::GPR32Low, "GPR32Low", {G0Low32, G1Low32}, {G0Low32, G1Low32},
+ 32, LaneMask::fromRaw(0x3), TestRegisterBank::GPR, {TestSubReg::Low16},
+ {TestRegisterClass::GPR32}),
+ registerClass(TestRegisterClass::GPR32High, "GPR32High", {G0High32, G1High32},
+ {G0High32, G1High32}, 32, LaneMask::fromRaw(0x3), TestRegisterBank::GPR, {},
+ {TestRegisterClass::GPR32}),
+ registerClass(TestRegisterClass::GPR16, "GPR16", {G0Low16, G1Low16}, {G0Low16, G1Low16}, 16,
+ LaneMask::fromRaw(0x1), TestRegisterBank::GPR),
+ registerClass(TestRegisterClass::FPR64, "FPR64", {F0, F1}, {F0, F1}, 64,
+ LaneMask::fromRaw(0x1), TestRegisterBank::FPR),
+ registerClass(TestRegisterClass::SpecialGPR64, "SpecialGPR64", {StackPointer}, {}, 64,
+ LaneMask::fromRaw(0xF), TestRegisterBank::GPR, {}, {}, false),
+ };
+
+ Description.SubRegClassMappings = {
+ {TestRegisterClass::GPR64, TestSubReg::Low32, TestRegisterClass::GPR32Low},
+ {TestRegisterClass::GPR64, TestSubReg::High32, TestRegisterClass::GPR32High},
+ {TestRegisterClass::GPR64, TestSubReg::Low16, TestRegisterClass::GPR16},
+ {TestRegisterClass::GPR32Low, TestSubReg::Low16, TestRegisterClass::GPR16},
+ };
+ Description.RegisterBanks = {
+ {TestRegisterBank::GPR,
+ "GPR",
+ 64,
+ {TestRegisterClass::GPR64, TestRegisterClass::GPR64Alloc, TestRegisterClass::GPR32,
+ TestRegisterClass::GPR32Low, TestRegisterClass::GPR32High, TestRegisterClass::GPR16,
+ TestRegisterClass::SpecialGPR64}},
+ {TestRegisterBank::FPR, "FPR", 64, {TestRegisterClass::FPR64}},
+ };
+
+ return Description;
+}
+
+Expected<TargetRegisterInfo> createTestTargetRegisterInfo() {
+ return TargetRegisterInfo::create(createTestTargetRegisterInfoDescription());
+}
+
+TargetMIRTextInfoDesc createTestTargetMIRTextInfoDescription() {
+ TargetMIRTextInfoDesc Description;
+ Description.RegisterMasks = {
+ {"Test-Preserved", createTestCallPreservedMask()},
+ };
+ Description.TargetIndices = {
+ {"test-index", TargetIndex::fromValue(0)},
+ };
+ Description.DirectOperandFlagMask = 0x3;
+ Description.DirectOperandFlags = {
+ {"test-direct-one", 0x1},
+ {"test-direct-two", 0x2},
+ {"test-direct-three", 0x3},
+ };
+ Description.BitmaskOperandFlags = {
+ {"test-bit-four", 0x4},
+ {"test-bit-eight", 0x8},
+ };
+ Description.MachineMemOperandFlags = {
+ {"test-mmo-one", 0x1},
+ {"test-mmo-two", 0x2},
+ };
+ Description.VirtualRegisterFlags = {
+ {"test-vreg-one", 0x1},
+ {"test-vreg-two", 0x2},
+ };
+ Description.Intrinsics = {
+ {"llvm.test.seven", IntrinsicID::fromValue(7)},
+ {"llvm.test.seventeen", IntrinsicID::fromValue(17)},
+ };
+
+ for (const PhysicalRegisterDesc &Register :
+ createTestTargetRegisterInfoDescription().PhysicalRegisters) {
+ Description.CFIRegisters.push_back({Register.Reg, Register.Reg.number(), true});
+ }
+ return Description;
+}
+
+Expected<TargetDescription> createTestTargetDescription(InstructionSet Instructions) {
+ auto Registers = createTestTargetRegisterInfo();
+ if (!Registers)
+ return std::unexpected(Registers.error());
+ return TargetDescription::create(std::move(Instructions), std::move(*Registers),
+ createTestTargetMIRTextInfoDescription());
+}
+
+std::shared_ptr<const RegisterMask> createTestCallPreservedMask() {
+ std::uint64_t Preserved = 0;
+ Preserved |= std::uint64_t{1} << TestRegister::G1;
+ Preserved |= std::uint64_t{1} << TestRegister::G1Low32;
+ Preserved |= std::uint64_t{1} << TestRegister::G1High32;
+ Preserved |= std::uint64_t{1} << TestRegister::G1Low16;
+ Preserved |= std::uint64_t{1} << TestRegister::StackPointer;
+ Preserved |= std::uint64_t{1} << TestRegister::F1;
+ return std::make_shared<const RegisterMask>(std::vector<std::uint64_t>{Preserved});
+}
+
+} // namespace rcc::test
diff --git a/backend/unittests/Target/TestTarget.h b/backend/unittests/Target/TestTarget.h
new file mode 100644
index 0000000..fe434a2
--- /dev/null
+++ b/backend/unittests/Target/TestTarget.h
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef HOME_RICHARD_CODE_RCC_PROJECT_BACKEND_UNITTESTS_TARGET_TESTTARGET_H
+#define HOME_RICHARD_CODE_RCC_PROJECT_BACKEND_UNITTESTS_TARGET_TESTTARGET_H
+
+#include "rcc/CodeGen/InstructionDescriptor.h"
+#include "rcc/CodeGen/RegisterMask.h"
+#include "rcc/CodeGen/TargetDescription.h"
+#include "rcc/CodeGen/TargetMIRTextInfo.h"
+#include "rcc/CodeGen/TargetRegisterInfo.h"
+#include "rcc/Support/Error.h"
+
+#include <cstdint>
+#include <memory>
+
+namespace rcc::test {
+
+struct TestRegister final {
+
+ static constexpr std::uint32_t G0 = 1;
+ static constexpr std::uint32_t G0Low32 = 2;
+ static constexpr std::uint32_t G0High32 = 3;
+ static constexpr std::uint32_t G0Low16 = 4;
+ static constexpr std::uint32_t G1 = 5;
+ static constexpr std::uint32_t G1Low32 = 6;
+ static constexpr std::uint32_t G1High32 = 7;
+ static constexpr std::uint32_t G1Low16 = 8;
+ static constexpr std::uint32_t StackPointer = 9;
+ static constexpr std::uint32_t F0 = 10;
+ static constexpr std::uint32_t F1 = 11;
+};
+
+struct TestSubReg final {
+
+ static constexpr SubRegIndex Low32 = SubRegIndex::fromValue(1);
+ static constexpr SubRegIndex High32 = SubRegIndex::fromValue(2);
+ static constexpr SubRegIndex Low16 = SubRegIndex::fromValue(3);
+};
+
+struct TestRegisterBank final {
+
+ static constexpr RegisterBankID GPR = RegisterBankID::fromValue(1);
+ static constexpr RegisterBankID FPR = RegisterBankID::fromValue(2);
+};
+
+struct TestRegisterClass final {
+
+ static constexpr RegisterClassID GPR64 = RegisterClassID::fromValue(1);
+ static constexpr RegisterClassID GPR64Alloc = RegisterClassID::fromValue(2);
+ static constexpr RegisterClassID GPR32 = RegisterClassID::fromValue(3);
+ static constexpr RegisterClassID GPR32Low = RegisterClassID::fromValue(4);
+ static constexpr RegisterClassID GPR32High = RegisterClassID::fromValue(5);
+ static constexpr RegisterClassID GPR16 = RegisterClassID::fromValue(6);
+ static constexpr RegisterClassID FPR64 = RegisterClassID::fromValue(7);
+ static constexpr RegisterClassID SpecialGPR64 = RegisterClassID::fromValue(8);
+};
+
+[[nodiscard]] TargetRegisterInfoDesc createTestTargetRegisterInfoDescription();
+[[nodiscard]] Expected<TargetRegisterInfo> createTestTargetRegisterInfo();
+[[nodiscard]] TargetMIRTextInfoDesc createTestTargetMIRTextInfoDescription();
+[[nodiscard]] Expected<TargetDescription> createTestTargetDescription(InstructionSet Instructions);
+[[nodiscard]] std::shared_ptr<const RegisterMask> createTestCallPreservedMask();
+
+} // namespace rcc::test
+
+#endif // HOME_RICHARD_CODE_RCC_PROJECT_BACKEND_UNITTESTS_TARGET_TESTTARGET_H
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