aboutsummaryrefslogtreecommitdiff
path: root/backend/unittests/Tooling
diff options
context:
space:
mode:
authorRichard-Qin-X <richard.qin.001@gmail.com>2026-08-10 20:36:50 +0800
committerRichard-Qin-X <richard.qin.001@gmail.com>2026-08-10 20:36:50 +0800
commitadcf8c9b1bd1b47e00bf859693112cea1f7e8c9f (patch)
tree59067aa71befab31678946dbf5e19f57131dd6f2 /backend/unittests/Tooling
parent3a57627e59ba18b6f9f9aae7bb60c390c0d3575a (diff)
downloadrcc-project-adcf8c9b1bd1b47e00bf859693112cea1f7e8c9f.tar.gz
rcc-project-adcf8c9b1bd1b47e00bf859693112cea1f7e8c9f.tar.xz
[Tooling] Complete backend tooling and validation
Diffstat (limited to 'backend/unittests/Tooling')
-rw-r--r--backend/unittests/Tooling/ArtifactStoreTest.cpp193
-rw-r--r--backend/unittests/Tooling/BackendManifestTest.cpp210
-rw-r--r--backend/unittests/Tooling/CMakeLists.txt48
-rw-r--r--backend/unittests/Tooling/MIRReducerFuzzerTest.cpp241
-rw-r--r--backend/unittests/Tooling/PipelineDriverTest.cpp284
-rw-r--r--backend/unittests/Tooling/ToolingTestSupport.h131
6 files changed, 1107 insertions, 0 deletions
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..5a6a0e9
--- /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::tooling::RelocationModel::Static;
+ if (!keyDiffers(Base, Changed, Input, *Fingerprint))
+ return tooling_test::fail("cache key must include the relocation model");
+ Changed = Base;
+ Changed.Code = rcc::tooling::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::tooling::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..3d637ec
--- /dev/null
+++ b/backend/unittests/Tooling/CMakeLists.txt
@@ -0,0 +1,48 @@
+# 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"
+)
+rcc_backend_register_tidy_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"
+)
diff --git a/backend/unittests/Tooling/MIRReducerFuzzerTest.cpp b/backend/unittests/Tooling/MIRReducerFuzzerTest.cpp
new file mode 100644
index 0000000..236f4e9
--- /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:gpr(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", "RegisterBankSelect", "InstructionSelect"};
+ 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::BackendOutputKind::RegisterBankMIR);
+ if (!RegBank)
+ return tooling_test::fail(RegBank.error().message());
+ rcc::tooling::BackendInvocation FailureInvocation = Invocation;
+ FailureInvocation.PassPipeline = {"InstructionSelect"};
+ 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::BackendOutputKind::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..4a223b6
--- /dev/null
+++ b/backend/unittests/Tooling/PipelineDriverTest.cpp
@@ -0,0 +1,284 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/Tooling/PipelineDriver.h"
+
+#include "ToolingTestSupport.h"
+#include "rcc/CodeGen/MachineFunction.h"
+#include "rcc/MIR/MIRDocument.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;
+ Options.DefaultPointerSizeInBits = 64;
+ 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::BackendOutputKind 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::BackendOutputKind::FinalMIR);
+ if (!Final)
+ return std::unexpected(Final.error());
+ auto Assembly = runInput("determinism-assembly.mir", std::string(Input), Target, Invocation,
+ rcc::tooling::BackendOutputKind::Assembly);
+ if (!Assembly)
+ return std::unexpected(Assembly.error());
+ auto Object = runInput("determinism-object.mir", std::string(Input), Target, Invocation,
+ rcc::tooling::BackendOutputKind::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());
+
+ constexpr std::array StageCases{
+ std::pair{rcc::tooling::BackendOutputKind::PreLegalizerMIR,
+ rcc::tooling::BackendStage::PreLegalizerMIR},
+ std::pair{rcc::tooling::BackendOutputKind::LegalizedMIR,
+ rcc::tooling::BackendStage::LegalizedMIR},
+ std::pair{rcc::tooling::BackendOutputKind::RegisterBankMIR,
+ rcc::tooling::BackendStage::RegisterBankMIR},
+ std::pair{rcc::tooling::BackendOutputKind::SelectedMIR,
+ rcc::tooling::BackendStage::SelectedMIR},
+ std::pair{rcc::tooling::BackendOutputKind::PostSSAMIR,
+ rcc::tooling::BackendStage::PostSSAMIR},
+ std::pair{rcc::tooling::BackendOutputKind::PostRegisterAllocationMIR,
+ rcc::tooling::BackendStage::PostRegisterAllocationMIR},
+ std::pair{rcc::tooling::BackendOutputKind::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::BackendOutputKind::SelectedMIR)
+ SelectedMIR = std::string(Run->text());
+ }
+
+ auto Assembly = runInput("assembly.mir", *Source, *Target, Invocation,
+ rcc::tooling::BackendOutputKind::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::BackendOutputKind::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#1"};
+ 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#1";
+ 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() != 1) {
+ return tooling_test::fail("stop-after must stop at the selected pass occurrence");
+ }
+ StopControls.StopAfter = "Legalizer#0";
+ if (rcc::tooling::runRV64GCPipeline(StopInput->Document, *Target, Invocation, StopControls))
+ return tooling_test::fail("pipeline controls must reject occurrence zero");
+
+ 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::BackendOutputKind::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::BackendOutputKind::RegisterBankMIR);
+ if (!RegBankRun)
+ return tooling_test::fail(RegBankRun.error().message());
+ rcc::tooling::BackendInvocation InstructionSelectInvocation = Invocation;
+ InstructionSelectInvocation.PassPipeline = {"InstructionSelect"};
+ 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::BackendOutputKind::SelectedMIR);
+ auto AcceptedSecond =
+ runInput("action-accepted-2.mir", std::string(RegBankRun->text()), *Target,
+ InstructionSelectInvocation, rcc::tooling::BackendOutputKind::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