From f99ef4322a7a2c09380fff61f88ab9440f05a92c Mon Sep 17 00:00:00 2001 From: Richard-Qin-X Date: Fri, 7 Aug 2026 22:06:28 +0800 Subject: [CodeGen] Complete machine pass and ABI lowering infrastructure --- backend/unittests/CodeGen/MachinePassTest.cpp | 918 ++++++++++++++++++++++++++ 1 file changed, 918 insertions(+) create mode 100644 backend/unittests/CodeGen/MachinePassTest.cpp (limited to 'backend/unittests/CodeGen/MachinePassTest.cpp') diff --git a/backend/unittests/CodeGen/MachinePassTest.cpp b/backend/unittests/CodeGen/MachinePassTest.cpp new file mode 100644 index 0000000..fb125a4 --- /dev/null +++ b/backend/unittests/CodeGen/MachinePassTest.cpp @@ -0,0 +1,918 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/CodeGen/MachinePass.h" + +#include "Target/TestTarget.h" +#include "rcc/CodeGen/CompilationUnit.h" +#include "rcc/CodeGen/GenericOpcodes.h" +#include "rcc/CodeGen/MachineFunction.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 createTarget() { + std::vector Descriptors; + std::vector 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 run(rcc::MachineFunction &Function, + const rcc::TargetDescription &, + rcc::MachineAnalysisManager &) { + ++Runs; + return Result{Function.blockCount()}; + } + + static inline std::atomic 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 run(rcc::MachineFunction &Function, + const rcc::TargetDescription &Target, + rcc::MachineAnalysisManager &Analyses) { + auto Blocks = Analyses.getResult(Function, Target); + if (!Blocks) + return std::unexpected(Blocks.error()); + return Result{(*Blocks)->Count}; + } +}; + +class NonThreadSafeAnalysis final { +public: + struct Result final {}; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "NonThreadSafe"; + } + + [[nodiscard]] static rcc::Expected + run(rcc::MachineFunction &, const rcc::TargetDescription &, rcc::MachineAnalysisManager &) { + return Result{}; + } +}; + +class FailingAnalysis final { +public: + struct Result final {}; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "Failing"; + } + + [[nodiscard]] static rcc::Expected + run(rcc::MachineFunction &, const rcc::TargetDescription &, rcc::MachineAnalysisManager &) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "intentional machine analysis failure"); + } +}; + +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 + run(rcc::MachineFunction &, const rcc::TargetDescription &, rcc::MachineAnalysisManager &) { + 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 + run(rcc::MachineFunction &, const rcc::TargetDescription &, rcc::MachineAnalysisManager &) { + return Result{}; + } +}; + +class CycleA final { +public: + struct Result final {}; + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "CycleA"; + } + [[nodiscard]] static rcc::Expected + run(rcc::MachineFunction &, const rcc::TargetDescription &, rcc::MachineAnalysisManager &) { + return Result{}; + } +}; + +class CycleB final { +public: + struct Result final {}; + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "CycleB"; + } + [[nodiscard]] static rcc::Expected + run(rcc::MachineFunction &, const rcc::TargetDescription &, rcc::MachineAnalysisManager &) { + 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::MachineAnalysisManager::id()}, + .VerificationStage = std::nullopt, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected + run(rcc::MachineFunction &Function, const rcc::TargetDescription &Target, + rcc::MachineAnalysisManager &Analyses) override { + auto Dependent = Analyses.getResult(Function, Target); + if (!Dependent) + return std::unexpected(Dependent.error()); + auto FirstAction = Analyses.requestAction("first", Function); + auto SecondAction = Analyses.requestAction("second", Function); + if (!FirstAction || !SecondAction || !*FirstAction || *SecondAction) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "per-function action fuel was not enforced"); + } + 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 run(rcc::MachineFunction &Function, + const rcc::TargetDescription &, + rcc::MachineAnalysisManager &) 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 RequiresFailingAnalysisPass final : public rcc::MachineFunctionPass { +public: + [[nodiscard]] const rcc::MachinePassContract &contract() const noexcept override { + static const rcc::MachinePassContract Contract{ + .Name = "RequiresFailingAnalysis", + .Requires = {}, + .Sets = {}, + .Clears = {}, + .RequiredAnalyses = {rcc::MachineAnalysisManager::id()}, + .VerificationStage = std::nullopt, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected run(rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::MachineAnalysisManager &) override { + 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::MachineAnalysisManager::id()}, + .VerificationStage = std::nullopt, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected run(rcc::MachineFunction &Function, + const rcc::TargetDescription &, + rcc::MachineAnalysisManager &) 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 &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 run(rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::MachineAnalysisManager &) override { + Count->fetch_add(1, std::memory_order_relaxed); + return rcc::MachinePassResult{}; + } + +private: + std::atomic *Count; +}; + +class InstrumentedParallelPass final : public rcc::MachineFunctionPass { +public: + InstrumentedParallelPass(std::atomic &Runs, std::atomic &Active, + std::atomic &MaximumActive) + : Runs(&Runs), Active(&Active), MaximumActive(&MaximumActive) {} + + [[nodiscard]] const rcc::MachinePassContract &contract() const noexcept override { + static const rcc::MachinePassContract Contract = [] { + rcc::MachinePassContract Value{ + .Name = "InstrumentedParallel", + .Requires = {}, + .Sets = {}, + .Clears = {}, + .RequiredAnalyses = {}, + .VerificationStage = std::nullopt, + }; + Value.ThreadSafety = rcc::PassThreadSafety::ThreadSafe; + Value.DeclaredChanges = {rcc::MachinePassChange::BlockLayout}; + return Value; + }(); + return Contract; + } + + [[nodiscard]] rcc::Expected run(rcc::MachineFunction &Function, + const rcc::TargetDescription &, + rcc::MachineAnalysisManager &) override { + const std::uint32_t Current = Active->fetch_add(1, std::memory_order_acq_rel) + 1U; + std::uint32_t Maximum = MaximumActive->load(std::memory_order_relaxed); + while (Maximum < Current && + !MaximumActive->compare_exchange_weak(Maximum, Current, std::memory_order_relaxed)) { + } + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + auto Block = Function.createBasicBlock(); + if (!Block) { + Active->fetch_sub(1, std::memory_order_acq_rel); + return std::unexpected(Block.error()); + } + auto Appended = Function.appendBlock(**Block); + Active->fetch_sub(1, std::memory_order_acq_rel); + if (!Appended) + return std::unexpected(Appended.error()); + Runs->fetch_add(1, std::memory_order_relaxed); + rcc::MachinePassResult Result; + Result.Changed = true; + Result.Changes.add(rcc::MachinePassChange::BlockLayout); + return Result; + } + +private: + std::atomic *Runs; + std::atomic *Active; + std::atomic *MaximumActive; +}; + +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::MachineAnalysisManager::id(), + }, + .VerificationStage = std::nullopt, + }; + Value.ThreadSafety = rcc::PassThreadSafety::ThreadSafe; + return Value; + }(); + return Contract; + } + + [[nodiscard]] rcc::Expected run(rcc::MachineFunction &, + const rcc::TargetDescription &, + rcc::MachineAnalysisManager &) 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 run(rcc::CompilationUnit &Unit, + const rcc::TargetDescription &, + rcc::CompilationUnitAnalysisManager &) { + return Result{Unit.functionCount()}; + } +}; + +class FailingUnitAnalysis final { +public: + struct Result final {}; + + [[nodiscard]] static constexpr std::string_view name() noexcept { + return "FailingUnit"; + } + + [[nodiscard]] static rcc::Expected run(rcc::CompilationUnit &, + const rcc::TargetDescription &, + rcc::CompilationUnitAnalysisManager &) { + return rcc::makeUnexpected(rcc::ErrorCode::VerificationError, + "intentional compilation-unit analysis failure"); + } +}; + +class RequiresFailingUnitAnalysisPass final : public rcc::CompilationUnitPass { +public: + [[nodiscard]] const rcc::CompilationUnitPassContract &contract() const noexcept override { + static const rcc::CompilationUnitPassContract Contract{ + .Name = "RequiresFailingUnitAnalysis", + .RequiredAnalyses = + { + rcc::CompilationUnitAnalysisManager::id(), + }, + .Parameters = {}, + .DeclaredChanges = {}, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected + run(rcc::CompilationUnit &, const rcc::TargetDescription &, rcc::CompilationUnitAnalysisManager &, + rcc::MachineAnalysisManager &) override { + return rcc::CompilationUnitPassResult{}; + } +}; + +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()}, + .Parameters = {}, + .DeclaredChanges = {}, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected + run(rcc::CompilationUnit &Unit, const rcc::TargetDescription &Target, + rcc::CompilationUnitAnalysisManager &Analyses, rcc::MachineAnalysisManager &) override { + auto Count = Analyses.getResult(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()}, + .Parameters = {}, + .DeclaredChanges = {rcc::MachinePassChange::BlockLayout}, + }; + return Contract; + } + + [[nodiscard]] rcc::Expected + run(rcc::CompilationUnit &Unit, const rcc::TargetDescription &, + rcc::CompilationUnitAnalysisManager &, rcc::MachineAnalysisManager &) 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 testAnalysisInfrastructure(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::MachineAnalysisManager Analyses; + Context.expect(Analyses + .registerAnalysis({ + .Name = "BlockCount", + .Dependencies = {}, + .InvalidatedBy = {rcc::MachinePassChange::CFG}, + }) + .has_value(), + "base analysis registration must succeed"); + Context.expect(Analyses + .registerAnalysis({ + .Name = "Dependent", + .Dependencies = {rcc::MachineAnalysisManager::id()}, + .InvalidatedBy = {rcc::MachinePassChange::Operands}, + }) + .has_value(), + "dependent analysis registration must succeed"); + + BlockCountAnalysis::Runs = 0; + auto First = Analyses.getResult(**Function, Target); + auto Second = Analyses.getResult(**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::MachineAnalysisManager Cyclic; + Context.expect(Cyclic + .registerAnalysis({ + .Name = "CycleA", + .Dependencies = {rcc::MachineAnalysisManager::id()}, + }) + .has_value() && + Cyclic + .registerAnalysis({ + .Name = "CycleB", + .Dependencies = {rcc::MachineAnalysisManager::id()}, + }) + .has_value(), + "cyclic analysis registrations must be accepted until requested"); + Context.expect(!Cyclic.getResult(**Function, Target), + "analysis dependency cycles must produce a structured error"); + + rcc::MachineAnalysisManager CustomInvalidation; + Context.expect(CustomInvalidation + .registerAnalysis({ + .Name = "Sticky", + .Dependencies = {}, + .InvalidatedBy = {rcc::MachinePassChange::CFG}, + }) + .has_value() && + CustomInvalidation + .registerAnalysis({ + .Name = "Expiring", + .Dependencies = {}, + .InvalidatedBy = {rcc::MachinePassChange::CFG}, + }) + .has_value() && + CustomInvalidation.getResult(**Function, Target) && + CustomInvalidation.getResult(**Function, Target), + "custom invalidation analyses must materialize"); + CustomInvalidation.invalidate(**Function, rcc::PreservedAnalyses::none(), + {rcc::MachinePassChange::CFG}); + Context.expect( + CustomInvalidation.hasCached(**Function, rcc::MachineAnalysisManager::id()) && + !CustomInvalidation.hasCached(**Function, + rcc::MachineAnalysisManager::id()), + "analysis invalidate() must return false to preserve and true to invalidate"); +} + +void testPassAndInstrumentation(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::MachineAnalysisManager Analyses; + Context.expect( + Analyses + .registerAnalysis({ + .Name = "BlockCount", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + .ThreadSafety = rcc::PassThreadSafety::ThreadSafe, + }) + .has_value() && + Analyses + .registerAnalysis({ + .Name = "Dependent", + .Dependencies = {rcc::MachineAnalysisManager::id()}, + }) + .has_value(), + "pass analysis registrations 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 AnalysisCacheHits = 0; + std::uint32_t Printed = 0; + std::uint32_t ChangeEvents = 0; + std::uint32_t FailureEvents = 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, &AnalysisCacheHits](std::string_view, const rcc::MachineFunction &, + bool CacheHit, std::uint64_t) { + ++AfterAnalysis; + AnalysisCacheHits += CacheHit ? 1U : 0U; + }); + Instrumentation.addIRPrintCallback([&Printed](rcc::InstrumentationPhase, std::string_view, + const rcc::MachineFunction &) { ++Printed; }); + Instrumentation.addChangeCallback([&ChangeEvents](std::string_view, const rcc::MachineFunction &, + rcc::MachinePassChanges Changes, + bool FingerprintChanged) { + if (Changes.empty() && !FingerprintChanged) + ++ChangeEvents; + }); + Instrumentation.addFailureCallback([&FailureEvents](std::string_view, + const rcc::MachineFunction &, + const rcc::Error &) { ++FailureEvents; }); + + rcc::MachinePassManager Pipeline; + Context.expect(Pipeline.addPass(std::make_unique()).has_value(), + "a valid pass must be accepted"); + auto Run = Pipeline.run(**Function, Target, Analyses, &Instrumentation); + Context.expect(Run.has_value(), "a pass with a materialized required analysis must run"); + Context.expect(BeforePass == 1 && AfterPass == 1 && BeforeAnalysis == 2 && AfterAnalysis == 4 && + AnalysisCacheHits == 2 && Printed == 2 && ChangeEvents == 1, + "pass, analysis, cache-hit, IR-print, and change callbacks must all run"); + const auto Statistics = Instrumentation.statistics(); + Context.expect(Statistics.size() == 3, + "instrumentation must collect one pass and two analysis statistics"); + + auto ChangedFunction = + rcc::MachineFunction::create("unreported", rcc::MachineFunctionStableID::fromValue(3)); + rcc::MachineAnalysisManager EmptyAnalyses; + rcc::MachinePassManager Detecting( + {.VerifyBefore = true, .VerifyEach = true, .DetectUnreportedChanges = true}); + Context.expect(ChangedFunction && + Detecting.addPass(std::make_unique()).has_value() && + !Detecting.run(**ChangedFunction, Target, EmptyAnalyses, &Instrumentation) && + FailureEvents == 1, + "change detection must reject an unreported MachineIR mutation"); + + auto FailedFunction = + rcc::MachineFunction::create("failed", rcc::MachineFunctionStableID::fromValue(4)); + rcc::MachineAnalysisManager FailureAnalyses; + Context.expect(FailedFunction && FailureAnalyses + .registerAnalysis({ + .Name = "BlockCount", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + }) + .has_value(), + "mutating failure analysis fixtures must be valid"); + rcc::MachinePassManager FailingPipeline; + Context.expect(FailedFunction && + FailingPipeline.addPass(std::make_unique()).has_value() && + !FailingPipeline.run(**FailedFunction, Target, FailureAnalyses) && + !FailureAnalyses.hasCached( + **FailedFunction, rcc::MachineAnalysisManager::id()), + "a pass that mutates and fails must not leave stale function analyses cached"); + + rcc::MachineAnalysisManager FailingAnalyses; + Context.expect(FailingAnalyses + .registerAnalysis({ + .Name = "Failing", + .Dependencies = {}, + }) + .has_value(), + "failing analysis registration must succeed"); + rcc::MachinePassInstrumentation FailureInstrumentation; + std::uint32_t BeforeFailingAnalysis = 0; + std::uint32_t FailingAnalysisEvents = 0; + std::uint32_t RequiredAnalysisPassFailures = 0; + FailureInstrumentation.addBeforeAnalysisCallback( + [&BeforeFailingAnalysis](std::string_view, const rcc::MachineFunction &) { + ++BeforeFailingAnalysis; + }); + FailureInstrumentation.addAnalysisFailureCallback( + [&FailingAnalysisEvents](std::string_view, const rcc::MachineFunction &, const rcc::Error &) { + ++FailingAnalysisEvents; + }); + FailureInstrumentation.addFailureCallback( + [&RequiredAnalysisPassFailures](std::string_view, const rcc::MachineFunction &, + const rcc::Error &) { ++RequiredAnalysisPassFailures; }); + rcc::MachinePassManager RequiredAnalysisFailurePipeline; + Context.expect( + RequiredAnalysisFailurePipeline.addPass(std::make_unique()) + .has_value() && + !RequiredAnalysisFailurePipeline.run(**Function, Target, FailingAnalyses, + &FailureInstrumentation) && + BeforeFailingAnalysis == 1 && FailingAnalysisEvents == 1 && + RequiredAnalysisPassFailures == 1, + "analysis and pass failure instrumentation must report required-analysis failures"); +} + +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 Runs = 0; + rcc::MachinePassManager Parallel; + Context.expect(Parallel.addPass(std::make_unique(Runs)).has_value(), + "thread-safe pass registration must succeed"); + rcc::MachineAnalysisManager Prototype; + auto ParallelRun = Parallel.runFunctions(Unit, Target, Prototype, 4); + Context.expect(ParallelRun.has_value() && Runs == Unit.functionCount(), + "parallel function execution must visit every function exactly once"); + + std::atomic DuplicateRuns = 0; + rcc::MachinePassManager Repeated; + Context.expect( + Repeated.addPass(std::make_unique(DuplicateRuns)).has_value() && + Repeated.addPass(std::make_unique(DuplicateRuns)).has_value() && + Repeated.runFunctions(Unit, Target, Prototype, 4).has_value() && + DuplicateRuns == Unit.functionCount() * 2, + "a pipeline must allow the same named pass to run more than once"); + + rcc::MachinePassInstrumentation Instrumentation; + std::vector ObservedFunctions; + std::vector BeforeBlockCounts; + std::vector AfterBlockCounts; + Instrumentation.addBeforeCallback([&ObservedFunctions, &BeforeBlockCounts]( + std::string_view, const rcc::MachineFunction &Function) { + ObservedFunctions.emplace_back(Function.name()); + BeforeBlockCounts.push_back(Function.blockCount()); + }); + Instrumentation.addAfterCallback([&AfterBlockCounts](std::string_view, + const rcc::MachineFunction &Function, + const rcc::MachinePassResult &) { + AfterBlockCounts.push_back(Function.blockCount()); + }); + std::atomic InstrumentedRuns = 0; + std::atomic ActiveRuns = 0; + std::atomic MaximumActiveRuns = 0; + rcc::MachinePassManager Instrumented; + Context.expect( + Instrumented + .addPass(std::make_unique(InstrumentedRuns, ActiveRuns, + MaximumActiveRuns)) + .has_value() && + Instrumented.runFunctions(Unit, Target, Prototype, 4, &Instrumentation).has_value() && + InstrumentedRuns == Unit.functionCount() && MaximumActiveRuns > 1, + "instrumented function pipelines must execute concurrently with a parallel request"); + bool StableCallbackOrder = ObservedFunctions.size() == Unit.functionCount(); + for (std::size_t Index = 0; StableCallbackOrder && Index < ObservedFunctions.size(); ++Index) + StableCallbackOrder = ObservedFunctions[Index] == "f" + std::to_string(Index); + Context.expect(StableCallbackOrder, + "instrumentation callbacks must commit in stable compilation-unit order"); + Context.expect( + BeforeBlockCounts.size() == Unit.functionCount() && + AfterBlockCounts.size() == Unit.functionCount() && + std::ranges::all_of(BeforeBlockCounts, [](std::size_t Count) { return Count == 0; }) && + std::ranges::all_of(AfterBlockCounts, [](std::size_t Count) { return Count == 1; }), + "buffered instrumentation must retain distinct before/after IR snapshots"); + + rcc::MachineAnalysisManager UnsafePrototype; + Context.expect(UnsafePrototype + .registerAnalysis({ + .Name = "NonThreadSafe", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + .ThreadSafety = rcc::PassThreadSafety::NotThreadSafe, + }) + .has_value(), + "non-thread-safe analysis registration must be explicit"); + rcc::MachinePassManager UnsafeParallel; + Context.expect( + UnsafeParallel.addPass(std::make_unique()).has_value() && + !UnsafeParallel.runFunctions(Unit, Target, UnsafePrototype, 4), + "parallel pipelines must reject a required analysis not marked thread-safe"); + + rcc::CompilationUnitAnalysisManager UnitAnalyses; + Context.expect(UnitAnalyses + .registerAnalysis({ + .Name = "UnitFunctionCount", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + }) + .has_value(), + "compilation-unit analysis registration must succeed"); + rcc::MachineAnalysisManager FunctionAnalyses; + rcc::CompilationUnitPassManager UnitPipeline; + Context.expect(UnitPipeline.addPass(std::make_unique()).has_value(), + "compilation-unit pass registration must succeed"); + rcc::MachinePassInstrumentation UnitInstrumentation; + std::uint32_t BeforeUnit = 0; + std::uint32_t AfterUnit = 0; + std::uint32_t BeforeUnitAnalysis = 0; + std::uint32_t AfterUnitAnalysis = 0; + std::uint32_t UnitAnalysisCacheHits = 0; + UnitInstrumentation.addBeforeUnitCallback( + [&BeforeUnit](std::string_view, const rcc::CompilationUnit &) { ++BeforeUnit; }); + UnitInstrumentation.addAfterUnitCallback( + [&AfterUnit](std::string_view, const rcc::CompilationUnit &, + const rcc::CompilationUnitPassResult &) { ++AfterUnit; }); + UnitInstrumentation.addBeforeUnitAnalysisCallback( + [&BeforeUnitAnalysis](std::string_view, const rcc::CompilationUnit &) { + ++BeforeUnitAnalysis; + }); + UnitInstrumentation.addAfterUnitAnalysisCallback( + [&AfterUnitAnalysis, &UnitAnalysisCacheHits](std::string_view, const rcc::CompilationUnit &, + bool CacheHit, std::uint64_t) { + ++AfterUnitAnalysis; + UnitAnalysisCacheHits += CacheHit ? 1U : 0U; + }); + Context.expect( + UnitPipeline.run(Unit, Target, UnitAnalyses, FunctionAnalyses, &UnitInstrumentation) + .has_value(), + "compilation-unit pass and analysis must run through their own manager"); + Context.expect(BeforeUnit == 1 && AfterUnit == 1 && BeforeUnitAnalysis == 1 && + AfterUnitAnalysis == 2 && UnitAnalysisCacheHits == 1, + "compilation-unit instrumentation must report pass and analysis cache events"); + + rcc::CompilationUnitAnalysisManager FailingUnitAnalyses; + Context.expect(FailingUnitAnalyses + .registerAnalysis({ + .Name = "FailingUnit", + .Dependencies = {}, + }) + .has_value(), + "failing compilation-unit analysis registration must succeed"); + rcc::CompilationUnitPassManager FailingUnitAnalysisPipeline; + Context.expect( + FailingUnitAnalysisPipeline.addPass(std::make_unique()) + .has_value(), + "a pass requiring a failing unit analysis must register"); + rcc::MachinePassInstrumentation FailingUnitInstrumentation; + std::uint32_t BeforeFailingUnitAnalysis = 0; + std::uint32_t FailingUnitAnalysisEvents = 0; + std::uint32_t RequiredUnitAnalysisPassFailures = 0; + FailingUnitInstrumentation.addBeforeUnitAnalysisCallback( + [&BeforeFailingUnitAnalysis](std::string_view, const rcc::CompilationUnit &) { + ++BeforeFailingUnitAnalysis; + }); + FailingUnitInstrumentation.addUnitAnalysisFailureCallback( + [&FailingUnitAnalysisEvents](std::string_view, const rcc::CompilationUnit &, + const rcc::Error &) { ++FailingUnitAnalysisEvents; }); + FailingUnitInstrumentation.addUnitFailureCallback( + [&RequiredUnitAnalysisPassFailures](std::string_view, const rcc::CompilationUnit &, + const rcc::Error &) { + ++RequiredUnitAnalysisPassFailures; + }); + Context.expect( + !FailingUnitAnalysisPipeline.run(Unit, Target, FailingUnitAnalyses, FunctionAnalyses, + &FailingUnitInstrumentation) && + BeforeFailingUnitAnalysis == 1 && FailingUnitAnalysisEvents == 1 && + RequiredUnitAnalysisPassFailures == 1, + "unit analysis and pass failure instrumentation must report required-analysis failures"); + + rcc::MachineFunction *FirstFunction = Unit.functionAt(0); + Context.expect( + FirstFunction != nullptr && + FunctionAnalyses + .registerAnalysis({ + .Name = "BlockCount", + .Dependencies = {}, + .InvalidatedBy = rcc::MachinePassChanges::all(), + }) + .has_value() && + FunctionAnalyses.getResult(*FirstFunction, Target).has_value(), + "compilation-unit failure cache fixtures must materialize"); + rcc::CompilationUnitPassManager FailingUnitPipeline; + Context.expect( + FirstFunction != nullptr && + FailingUnitPipeline.addPass(std::make_unique()).has_value() && + !FailingUnitPipeline.run(Unit, Target, UnitAnalyses, FunctionAnalyses) && + !UnitAnalyses.hasCached( + Unit, rcc::CompilationUnitAnalysisManager::id()) && + !FunctionAnalyses.hasCached(*FirstFunction, + rcc::MachineAnalysisManager::id()), + "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(); + + testAnalysisInfrastructure(Context, *Target); + testPassAndInstrumentation(Context, *Target); + testCompilationUnitAndParallelism(Context, *Target); + return Context.result(); +} -- cgit v1.2.3