diff --git a/llvm/include/llvm/IR/OptBisect.h b/llvm/include/llvm/IR/OptBisect.h index d813ae933d65e..de008338209a5 100644 --- a/llvm/include/llvm/IR/OptBisect.h +++ b/llvm/include/llvm/IR/OptBisect.h @@ -14,10 +14,12 @@ #ifndef LLVM_IR_OPTBISECT_H #define LLVM_IR_OPTBISECT_H +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/StringSet.h" #include "llvm/Support/Compiler.h" -#include +#include "llvm/Support/Range.h" namespace llvm { @@ -40,7 +42,7 @@ class OptPassGate { /// This class implements a mechanism to disable passes and individual /// optimizations at compile time based on a command line option -/// (-opt-bisect-limit) in order to perform a bisecting search for +/// (-opt-bisect) in order to perform a bisecting search for /// optimization-related problems. class LLVM_ABI OptBisect : public OptPassGate { public: @@ -53,12 +55,12 @@ class LLVM_ABI OptBisect : public OptPassGate { virtual ~OptBisect() = default; - /// Checks the bisect limit to determine if the specified pass should run. + /// Checks the bisect ranges to determine if the specified pass should run. /// /// The method prints the name of the pass, its assigned bisect number, and /// whether or not the pass will be executed. It returns true if the pass - /// should run, i.e. if the bisect limit is set to -1 or has not yet been - /// exceeded. + /// should run, i.e. if no ranges are specified or the current pass number + /// falls within one of the specified ranges. /// /// Most passes should not call this routine directly. Instead, it is called /// through helper routines provided by the base classes of the pass. For @@ -67,20 +69,22 @@ class LLVM_ABI OptBisect : public OptPassGate { StringRef IRDescription) const override; /// isEnabled() should return true before calling shouldRunPass(). - bool isEnabled() const override { return BisectLimit != Disabled; } + bool isEnabled() const override { return !BisectRanges.empty(); } - /// Set the new optimization limit and reset the counter. Passing - /// OptBisect::Disabled disables the limiting. - void setLimit(int Limit) { - BisectLimit = Limit; - LastBisectNum = 0; + /// Set ranges directly from a RangeList. + void setRanges(RangeUtils::RangeList Ranges) { + BisectRanges = std::move(Ranges); } - static constexpr int Disabled = std::numeric_limits::max(); + /// Clear all ranges, effectively disabling bisection. + void clearRanges() { + BisectRanges.clear(); + LastBisectNum = 0; + } private: - int BisectLimit = Disabled; mutable int LastBisectNum = 0; + RangeUtils::RangeList BisectRanges; }; /// This class implements a mechanism to disable passes and individual diff --git a/llvm/include/llvm/Support/DebugCounter.h b/llvm/include/llvm/Support/DebugCounter.h index 89349d1ebffee..f51a881ac4e24 100644 --- a/llvm/include/llvm/Support/DebugCounter.h +++ b/llvm/include/llvm/Support/DebugCounter.h @@ -48,6 +48,7 @@ #include "llvm/ADT/UniqueVector.h" #include "llvm/Support/Compiler.h" #include "llvm/Support/Debug.h" +#include "llvm/Support/Range.h" #include namespace llvm { @@ -56,18 +57,7 @@ class raw_ostream; class DebugCounter { public: - struct Chunk { - int64_t Begin; - int64_t End; - LLVM_ABI void print(llvm::raw_ostream &OS); - bool contains(int64_t Idx) const { return Idx >= Begin && Idx <= End; } - }; - - LLVM_ABI static void printChunks(raw_ostream &OS, ArrayRef); - - /// Return true on parsing error and print the error message on the - /// llvm::errs() - LLVM_ABI static bool parseChunks(StringRef Str, SmallVector &Res); + LLVM_ABI static void printChunks(raw_ostream &OS, ArrayRef Ranges); /// Returns a reference to the singleton instance. LLVM_ABI static DebugCounter &instance(); @@ -176,7 +166,7 @@ class DebugCounter { uint64_t CurrChunkIdx = 0; bool IsSet = false; std::string Desc; - SmallVector Chunks; + RangeUtils::RangeList Chunks; }; DenseMap Counters; diff --git a/llvm/include/llvm/Support/Range.h b/llvm/include/llvm/Support/Range.h new file mode 100644 index 0000000000000..8e3f174447dd4 --- /dev/null +++ b/llvm/include/llvm/Support/Range.h @@ -0,0 +1,126 @@ +//===- llvm/Support/Range.h - Range parsing utility -----------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This file provides utilities for parsing range specifications like +// "1-10,20-30,45" which are commonly used in debugging and bisection tools. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_SUPPORT_RANGE_H +#define LLVM_SUPPORT_RANGE_H + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" +#include +#include +#include + +namespace llvm { +class raw_ostream; +} // end namespace llvm + +namespace llvm { + +/// Represents a range of integers [Begin, End], inclusive on both ends, where +/// Begin <= End. +class Range { + int64_t Begin; + int64_t End; + +public: + /// Create a range [Begin, End]. + Range(int64_t Begin, int64_t End) : Begin(Begin), End(End) { + assert(Begin <= End && "Range Begin must be <= End"); + } + /// Create a range [Single, Single]. + Range(int64_t Single) : Begin(Single), End(Single) {} + + int64_t getBegin() const { return Begin; } + int64_t getEnd() const { return End; } + + void setBegin(int64_t NewBegin) { + assert(NewBegin <= End && "Range Begin must be <= End"); + Begin = NewBegin; + } + void setEnd(int64_t NewEnd) { + assert(Begin <= NewEnd && "Range Begin must be <= End"); + End = NewEnd; + } + + /// Check if the given value is within this range (inclusive). + bool contains(int64_t Value) const { return Value >= Begin && Value <= End; } + + /// Check if this range overlaps with another range. + bool overlaps(const Range &Other) const { + return Begin <= Other.End && End >= Other.Begin; + } + + /// Get the size of this range. + uint64_t size() const { + if (Begin >= 0 || End < 0) { + // Safe: (End - Begin) fits in int64_t in both same-sign cases. + return static_cast(End - Begin) + 1; + } + // Mixed signs: Begin < 0 <= End. + // Handle potential extreme overflow case explicitly. + assert(!(Begin == std::numeric_limits::min() && + End == std::numeric_limits::max()) && + "Range size would overflow uint64_t"); + // Compute |Begin| without overflowing when Begin == INT64_MIN. + const uint64_t AbsBegin = static_cast(-(Begin + 1)) + 1; + return static_cast(End) + AbsBegin + 1; + } + + /// Print the range to the output stream. + void print(raw_ostream &OS) const { + if (Begin == End) + OS << Begin; + else + OS << Begin << "-" << End; + } + + bool operator==(const Range &Other) const { + return Begin == Other.Begin && End == Other.End; + } +}; + +/// Utility class for parsing and managing range specifications. +class RangeUtils { +public: + using RangeList = SmallVector; + + /// Parse a range specification string like "1-10,20-30,45" or + /// "1-10:20-30:45". Ranges must be in increasing order and non-overlapping. + /// \param RangeStr The string to parse. + /// \param Separator The separator character to use (',' or ':'). + /// \returns Expected containing the parsed ranges on success, + /// or an Error on failure. + static Expected parseRanges(StringRef RangeStr, + char Separator = ','); + + /// Check if a value is contained in any of the ranges. + static bool contains(ArrayRef Ranges, int64_t Value); + + /// Print ranges to output stream. + /// \param OS The output stream to print to. + /// \param Ranges The ranges to print. + /// \param Separator The separator character to use between ranges (i.e. ',' + /// or ':'). + static void printRanges(raw_ostream &OS, ArrayRef Ranges, + char Separator = ','); + + /// Merge adjacent/consecutive ranges into single ranges. + /// Example: [1-3, 4-6, 8-10] -> [1-6, 8-10]. + static RangeList mergeAdjacentRanges(ArrayRef Ranges); +}; + +} // end namespace llvm + +#endif // LLVM_SUPPORT_RANGE_H diff --git a/llvm/lib/IR/OptBisect.cpp b/llvm/lib/IR/OptBisect.cpp index 29ca268408265..9313b7879ef09 100644 --- a/llvm/lib/IR/OptBisect.cpp +++ b/llvm/lib/IR/OptBisect.cpp @@ -13,10 +13,13 @@ //===----------------------------------------------------------------------===// #include "llvm/IR/OptBisect.h" +#include "llvm/ADT/StringExtras.h" #include "llvm/Pass.h" #include "llvm/Support/CommandLine.h" +#include "llvm/Support/Range.h" #include "llvm/Support/raw_ostream.h" #include +#include using namespace llvm; @@ -30,12 +33,49 @@ static OptDisable &getOptDisabler() { return OptDisabler; } -static cl::opt OptBisectLimit("opt-bisect-limit", cl::Hidden, - cl::init(OptBisect::Disabled), cl::Optional, - cl::cb([](int Limit) { - getOptBisector().setLimit(Limit); - }), - cl::desc("Maximum optimization to perform")); +static cl::opt OptBisectLimit( + "opt-bisect-limit", cl::Hidden, cl::init(-1), cl::Optional, + cl::cb([](int Limit) { + if (Limit == -1) + // -1 means run all passes. + getOptBisector().setRanges({{1, std::numeric_limits::max()}}); + else if (Limit == 0) + // 0 means run no passes. + getOptBisector().setRanges({{0, 0}}); + else if (Limit > 0) + // Convert limit to range 1-Limit. + getOptBisector().setRanges({{1, Limit}}); + else + llvm_unreachable( + ("Invalid limit for -opt-bisect-limit: " + llvm::utostr(Limit)) + .c_str()); + }), + cl::desc( + "Maximum optimization to perform (equivalent to -opt-bisect=1-N)")); + +static cl::opt OptBisectRanges( + "opt-bisect", cl::Hidden, cl::Optional, + cl::cb([](const std::string &RangeStr) { + if (RangeStr == "-1") { + // -1 means run all passes. + getOptBisector().setRanges({{1, std::numeric_limits::max()}}); + return; + } + + auto Ranges = RangeUtils::parseRanges(RangeStr); + if (!Ranges) { + handleAllErrors(Ranges.takeError(), [&](const StringError &E) { + errs() << "Error: Invalid range specification for -opt-bisect: " + << RangeStr << " (" << E.getMessage() << ")\n"; + }); + exit(1); + } + getOptBisector().setRanges(std::move(*Ranges)); + }), + cl::desc("Run optimization passes only for the specified ranges. " + "Format: '1-10,20-30,45' runs passes 1-10, 20-30, and 45, where " + "index 1 is the first pass. Supply '0' to run no passes and -1 to " + "run all passes.")); static cl::opt OptBisectVerbose( "opt-bisect-verbose", @@ -66,7 +106,10 @@ bool OptBisect::shouldRunPass(StringRef PassName, assert(isEnabled()); int CurBisectNum = ++LastBisectNum; - bool ShouldRun = (BisectLimit == -1 || CurBisectNum <= BisectLimit); + + // Check if current pass number falls within any of the specified ranges. + bool ShouldRun = RangeUtils::contains(BisectRanges, CurBisectNum); + if (OptBisectVerbose) printPassMessage(PassName, CurBisectNum, IRDescription, ShouldRun); return ShouldRun; diff --git a/llvm/lib/Support/CMakeLists.txt b/llvm/lib/Support/CMakeLists.txt index 10b6101d73277..f6c64579cec5c 100644 --- a/llvm/lib/Support/CMakeLists.txt +++ b/llvm/lib/Support/CMakeLists.txt @@ -231,6 +231,7 @@ add_llvm_component_library(LLVMSupport PluginLoader.cpp PrettyStackTrace.cpp RandomNumberGenerator.cpp + Range.cpp Regex.cpp RewriteBuffer.cpp RewriteRope.cpp diff --git a/llvm/lib/Support/DebugCounter.cpp b/llvm/lib/Support/DebugCounter.cpp index 6b65720440f30..e75c82c4497cc 100644 --- a/llvm/lib/Support/DebugCounter.cpp +++ b/llvm/lib/Support/DebugCounter.cpp @@ -9,77 +9,8 @@ using namespace llvm; namespace llvm { -void DebugCounter::Chunk::print(llvm::raw_ostream &OS) { - if (Begin == End) - OS << Begin; - else - OS << Begin << "-" << End; -} - -void DebugCounter::printChunks(raw_ostream &OS, ArrayRef Chunks) { - if (Chunks.empty()) { - OS << "empty"; - } else { - bool IsFirst = true; - for (auto E : Chunks) { - if (!IsFirst) - OS << ':'; - else - IsFirst = false; - E.print(OS); - } - } -} - -bool DebugCounter::parseChunks(StringRef Str, SmallVector &Chunks) { - StringRef Remaining = Str; - - auto ConsumeInt = [&]() -> int64_t { - StringRef Number = - Remaining.take_until([](char c) { return c < '0' || c > '9'; }); - int64_t Res; - if (Number.getAsInteger(10, Res)) { - errs() << "Failed to parse int at : " << Remaining << "\n"; - return -1; - } - Remaining = Remaining.drop_front(Number.size()); - return Res; - }; - - while (1) { - int64_t Num = ConsumeInt(); - if (Num == -1) - return true; - if (!Chunks.empty() && Num <= Chunks[Chunks.size() - 1].End) { - errs() << "Expected Chunks to be in increasing order " << Num - << " <= " << Chunks[Chunks.size() - 1].End << "\n"; - return true; - } - if (Remaining.starts_with("-")) { - Remaining = Remaining.drop_front(); - int64_t Num2 = ConsumeInt(); - if (Num2 == -1) - return true; - if (Num >= Num2) { - errs() << "Expected " << Num << " < " << Num2 << " in " << Num << "-" - << Num2 << "\n"; - return true; - } - - Chunks.push_back({Num, Num2}); - } else { - Chunks.push_back({Num, Num}); - } - if (Remaining.starts_with(":")) { - Remaining = Remaining.drop_front(); - continue; - } - if (Remaining.empty()) - break; - errs() << "Failed to parse at : " << Remaining; - return true; - } - return false; +void DebugCounter::printChunks(raw_ostream &OS, ArrayRef Ranges) { + RangeUtils::printRanges(OS, Ranges, ':'); } } // namespace llvm @@ -182,20 +113,24 @@ void DebugCounter::push_back(const std::string &Val) { auto CounterPair = StringRef(Val).split('='); if (CounterPair.second.empty()) { errs() << "DebugCounter Error: " << Val << " does not have an = in it\n"; - return; + exit(1); } StringRef CounterName = CounterPair.first; - SmallVector Chunks; - if (parseChunks(CounterPair.second, Chunks)) { - return; + auto ExpectedChunks = RangeUtils::parseRanges(CounterPair.second, ':'); + if (!ExpectedChunks) { + handleAllErrors(ExpectedChunks.takeError(), [&](const StringError &E) { + errs() << "DebugCounter Error: " << E.getMessage() << "\n"; + }); + exit(1); } + RangeUtils::RangeList Chunks = std::move(*ExpectedChunks); unsigned CounterID = getCounterId(std::string(CounterName)); if (!CounterID) { errs() << "DebugCounter Error: " << CounterName << " is not a registered counter\n"; - return; + exit(1); } enableAllCounters(); @@ -236,15 +171,15 @@ bool DebugCounter::shouldExecuteImpl(unsigned CounterName) { bool Res = CounterInfo.Chunks[CurrIdx].contains(CurrCount); if (Us.BreakOnLast && CurrIdx == (CounterInfo.Chunks.size() - 1) && - CurrCount == CounterInfo.Chunks[CurrIdx].End) { + CurrCount == CounterInfo.Chunks[CurrIdx].getEnd()) { LLVM_BUILTIN_DEBUGTRAP; } - if (CurrCount > CounterInfo.Chunks[CurrIdx].End) { + if (CurrCount > CounterInfo.Chunks[CurrIdx].getEnd()) { CounterInfo.CurrChunkIdx++; /// Handle consecutive blocks. if (CounterInfo.CurrChunkIdx < CounterInfo.Chunks.size() && - CurrCount == CounterInfo.Chunks[CounterInfo.CurrChunkIdx].Begin) + CurrCount == CounterInfo.Chunks[CounterInfo.CurrChunkIdx].getBegin()) return true; } return Res; diff --git a/llvm/lib/Support/Range.cpp b/llvm/lib/Support/Range.cpp new file mode 100644 index 0000000000000..2fe00aa0234ab --- /dev/null +++ b/llvm/lib/Support/Range.cpp @@ -0,0 +1,117 @@ +//===- llvm/Support/Range.cpp - Range parsing utility ---------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "llvm/Support/Range.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/Regex.h" +#include "llvm/Support/raw_ostream.h" +#include + +using namespace llvm; + +Expected RangeUtils::parseRanges(StringRef Str, + char Separator) { + RangeList Ranges; + + if (Str.empty()) + return std::move(Ranges); + + // Regex to match either single number or range "num1-num2". + const Regex RangeRegex("^([0-9]+)(-([0-9]+))?$"); + + for (StringRef Part : llvm::split(Str, Separator)) { + Part = Part.trim(); + if (Part.empty()) + continue; + + SmallVector Matches; + if (!RangeRegex.match(Part, &Matches)) + return createStringError(std::errc::invalid_argument, + "Invalid range format: '%s'", + Part.str().c_str()); + + int64_t Begin, End; + if (Matches[1].getAsInteger(10, Begin)) + return createStringError(std::errc::invalid_argument, + "Failed to parse number: '%s'", + Matches[1].str().c_str()); + + if (!Matches[3].empty()) { + // Range format "begin-end". + if (Matches[3].getAsInteger(10, End)) + return createStringError(std::errc::invalid_argument, + "Failed to parse number: '%s'", + Matches[3].str().c_str()); + if (Begin >= End) + return createStringError(std::errc::invalid_argument, + "Invalid range: %lld >= %lld", Begin, End); + } else + // Single number. + End = Begin; + + // Check ordering constraint (ranges must be in increasing order). + if (!Ranges.empty() && Begin <= Ranges.back().getEnd()) + return createStringError( + std::errc::invalid_argument, + "Expected ranges to be in increasing order: %lld <= %lld", Begin, + Ranges.back().getEnd()); + + Ranges.push_back(Range(Begin, End)); + } + + return Ranges; +} + +bool RangeUtils::contains(ArrayRef Ranges, int64_t Value) { + for (const Range &R : Ranges) { + if (R.contains(Value)) + return true; + } + return false; +} + +void RangeUtils::printRanges(raw_ostream &OS, ArrayRef Ranges, + char Separator) { + if (Ranges.empty()) + OS << "empty"; + else { + bool IsFirst = true; + for (const Range &R : Ranges) { + if (!IsFirst) + OS << Separator; + else + IsFirst = false; + R.print(OS); + } + } +} + +RangeUtils::RangeList RangeUtils::mergeAdjacentRanges(ArrayRef Ranges) { + if (Ranges.empty()) + return {}; + + RangeList Result; + Result.push_back(Ranges[0]); + + for (size_t I = 1; I < Ranges.size(); ++I) { + const Range &Current = Ranges[I]; + Range &Last = Result.back(); + + // Check if current range is adjacent to the last merged range. + if (Current.getBegin() == Last.getEnd() + 1) { + // Merge by extending the end of the last range. + Last.setEnd(Current.getEnd()); + } else { + // Not adjacent, add as separate range. + Result.push_back(Current); + } + } + + return Result; +} diff --git a/llvm/test/Other/debugcounter-multi-ranges.ll b/llvm/test/Other/debugcounter-multi-ranges.ll new file mode 100644 index 0000000000000..7d9b1949a5499 --- /dev/null +++ b/llvm/test/Other/debugcounter-multi-ranges.ll @@ -0,0 +1,116 @@ +; REQUIRES: asserts + +; Test debug counter with multiple ranges +; RUN: opt -passes=dce -S -debug-counter=dce-transform=0 < %s | FileCheck %s --check-prefix=CHECK-ZERO +; RUN: opt -passes=dce -S -debug-counter=dce-transform=1:3:5 < %s | FileCheck %s --check-prefix=CHECK-SINGLE +; RUN: opt -passes=dce -S -debug-counter=dce-transform=1-2:4:6-7 < %s | FileCheck %s --check-prefix=CHECK-MIXED +; RUN: opt -passes=dce -S -debug-counter=dce-transform=1-7 < %s | FileCheck %s --check-prefix=CHECK-ALL +; RUN: opt -passes=dce -S -debug-counter=dce-transform=100 < %s | FileCheck %s --check-prefix=CHECK-NONE +; RUN: opt -passes=dce -S -debug-counter=dce-transform=7 < %s | FileCheck %s --check-prefix=CHECK-LAST +; RUN: opt -passes=dce -S -debug-counter=dce-transform=1 < %s | FileCheck %s --check-prefix=CHECK-FIRST + +; Test error cases - these should produce error messages but not crash +; RUN: not opt -passes=dce -S -debug-counter=dce-transform=invalid 2>&1 | FileCheck %s --check-prefix=CHECK-ERROR-INVALID +; RUN: not opt -passes=dce -S -debug-counter=dce-transform=5-2 2>&1 | FileCheck %s --check-prefix=CHECK-ERROR-BACKWARDS +; RUN: not opt -passes=dce -S -debug-counter=dce-transform=1:3:2 2>&1 | FileCheck %s --check-prefix=CHECK-ERROR-UNORDERED +; RUN: not opt -passes=dce -S -debug-counter=dce-transform=abc-def 2>&1 | FileCheck %s --check-prefix=CHECK-ERROR-NON-NUMERIC +; RUN: not opt -passes=dce -S -debug-counter=dce-transform=1-abc 2>&1 | FileCheck %s --check-prefix=CHECK-ERROR-MIXED +; RUN: not opt -passes=dce -S -debug-counter=dce-transform=1:2:3:2:4 2>&1 | FileCheck %s --check-prefix=CHECK-ERROR-COMPLEX-UNORDERED +; RUN: not opt -passes=dce -S -debug-counter=dce-transform=1--5 2>&1 | FileCheck %s --check-prefix=CHECK-ERROR-DOUBLE-DASH +; RUN: not opt -passes=dce -S -debug-counter=dce-transform=-5 2>&1 | FileCheck %s --check-prefix=CHECK-ERROR-NEGATIVE +; RUN: not opt -passes=dce -S -debug-counter=dce-transform= 2>&1 | FileCheck %s --check-prefix=CHECK-ERROR-EMPTY +; RUN: not opt -passes=dce -S -debug-counter=dce-transform=1:1:1 2>&1 | FileCheck %s --check-prefix=CHECK-ERROR-DUPLICATE + +; Test that with debug counters on, we can selectively apply transformations +; using different range specifications. Also check that we catch errors during parsing. + +; Original function has 8 dead instructions that DCE can eliminate +define void @test() { + %dead1 = add i32 1, 2 + %dead2 = add i32 3, 4 + %dead3 = add i32 5, 6 + %dead4 = add i32 7, 8 + %dead5 = add i32 9, 10 + %dead6 = add i32 11, 12 + %dead7 = add i32 13, 14 + %dead8 = add i32 15, 16 + ret void +} + +; Test zero: eliminate transformation 0 +; CHECK-ZERO-LABEL: @test +; CHECK-ZERO-NEXT: %dead2 = add i32 3, 4 +; CHECK-ZERO-NEXT: %dead3 = add i32 5, 6 +; CHECK-ZERO-NEXT: %dead4 = add i32 7, 8 +; CHECK-ZERO-NEXT: %dead5 = add i32 9, 10 +; CHECK-ZERO-NEXT: %dead6 = add i32 11, 12 +; CHECK-ZERO-NEXT: %dead7 = add i32 13, 14 +; CHECK-ZERO-NEXT: %dead8 = add i32 15, 16 +; CHECK-ZERO-NEXT: ret void + +; Test single values: apply transformations 1, 3, 5 (eliminate dead2, dead4, dead6) +; CHECK-SINGLE-LABEL: @test +; CHECK-SINGLE-NEXT: %dead1 = add i32 1, 2 +; CHECK-SINGLE-NEXT: %dead3 = add i32 5, 6 +; CHECK-SINGLE-NEXT: %dead5 = add i32 9, 10 +; CHECK-SINGLE-NEXT: %dead7 = add i32 13, 14 +; CHECK-SINGLE-NEXT: %dead8 = add i32 15, 16 +; CHECK-SINGLE-NEXT: ret void + +; Test mixed ranges: apply transformations 1-2, 4, 6-7 (eliminate dead2, dead3, dead5, dead7, dead8) +; CHECK-MIXED-LABEL: @test +; CHECK-MIXED-NEXT: %dead1 = add i32 1, 2 +; CHECK-MIXED-NEXT: %dead4 = add i32 7, 8 +; CHECK-MIXED-NEXT: %dead6 = add i32 11, 12 +; CHECK-MIXED-NEXT: ret void + +; Test all range: apply transformations 1-7 (eliminate all dead instructions except dead1) +; CHECK-ALL-LABEL: @test +; CHECK-ALL-NEXT: %dead1 = add i32 1, 2 +; CHECK-ALL-NEXT: ret void + +; Test out of range: apply transformation 100 (eliminate nothing, counter too high) +; CHECK-NONE-LABEL: @test +; CHECK-NONE-NEXT: %dead1 = add i32 1, 2 +; CHECK-NONE-NEXT: %dead2 = add i32 3, 4 +; CHECK-NONE-NEXT: %dead3 = add i32 5, 6 +; CHECK-NONE-NEXT: %dead4 = add i32 7, 8 +; CHECK-NONE-NEXT: %dead5 = add i32 9, 10 +; CHECK-NONE-NEXT: %dead6 = add i32 11, 12 +; CHECK-NONE-NEXT: %dead7 = add i32 13, 14 +; CHECK-NONE-NEXT: %dead8 = add i32 15, 16 +; CHECK-NONE-NEXT: ret void + +; Test last transformation: apply transformation 7 (eliminate dead8) +; CHECK-LAST-LABEL: @test +; CHECK-LAST-NEXT: %dead1 = add i32 1, 2 +; CHECK-LAST-NEXT: %dead2 = add i32 3, 4 +; CHECK-LAST-NEXT: %dead3 = add i32 5, 6 +; CHECK-LAST-NEXT: %dead4 = add i32 7, 8 +; CHECK-LAST-NEXT: %dead5 = add i32 9, 10 +; CHECK-LAST-NEXT: %dead6 = add i32 11, 12 +; CHECK-LAST-NEXT: %dead7 = add i32 13, 14 +; CHECK-LAST-NEXT: ret void + +; Test first transformation: apply transformation 1 (eliminate dead2) +; CHECK-FIRST-LABEL: @test +; CHECK-FIRST-NEXT: %dead1 = add i32 1, 2 +; CHECK-FIRST-NEXT: %dead3 = add i32 5, 6 +; CHECK-FIRST-NEXT: %dead4 = add i32 7, 8 +; CHECK-FIRST-NEXT: %dead5 = add i32 9, 10 +; CHECK-FIRST-NEXT: %dead6 = add i32 11, 12 +; CHECK-FIRST-NEXT: %dead7 = add i32 13, 14 +; CHECK-FIRST-NEXT: %dead8 = add i32 15, 16 +; CHECK-FIRST-NEXT: ret void + +; Error case checks - test comprehensive error handling +; CHECK-ERROR-INVALID: DebugCounter Error: Invalid range format: 'invalid' +; CHECK-ERROR-BACKWARDS: DebugCounter Error: Invalid range: 5 >= 2 +; CHECK-ERROR-UNORDERED: DebugCounter Error: Expected ranges to be in increasing order: 2 <= 3 +; CHECK-ERROR-NON-NUMERIC: DebugCounter Error: Invalid range format: 'abc-def' +; CHECK-ERROR-MIXED: DebugCounter Error: Invalid range format: '1-abc' +; CHECK-ERROR-COMPLEX-UNORDERED: DebugCounter Error: Expected ranges to be in increasing order: 2 <= 3 +; CHECK-ERROR-DOUBLE-DASH: DebugCounter Error: Invalid range format: '1--5' +; CHECK-ERROR-NEGATIVE: DebugCounter Error: Invalid range format: '-5' +; CHECK-ERROR-EMPTY: DebugCounter Error: dce-transform= does not have an = in it +; CHECK-ERROR-DUPLICATE: DebugCounter Error: Expected ranges to be in increasing order: 1 <= 1 diff --git a/llvm/test/Other/opt-bisect-ranges.ll b/llvm/test/Other/opt-bisect-ranges.ll new file mode 100644 index 0000000000000..84b2d336f6f6e --- /dev/null +++ b/llvm/test/Other/opt-bisect-ranges.ll @@ -0,0 +1,58 @@ +; Test that verifies functionality for -opt-bisect with range specifications + +; Test basic range functionality: run passes 1-3 and 7-8 +; RUN: opt -passes='annotation2metadata,forceattrs,inferattrs,function(lower-expect),function(simplifycfg),function(sroa),function(early-cse),openmp-opt' \ +; RUN: -opt-bisect=1-3,7-8 %s 2>&1 | FileCheck %s --check-prefix=CHECK-RANGES +; CHECK-RANGES: BISECT: running pass (1) annotation2metadata on [module] +; CHECK-RANGES: BISECT: running pass (2) forceattrs on [module] +; CHECK-RANGES: BISECT: running pass (3) inferattrs on [module] +; CHECK-RANGES: BISECT: NOT running pass (4) lower-expect on foo +; CHECK-RANGES: BISECT: NOT running pass (5) simplifycfg on foo +; CHECK-RANGES: BISECT: NOT running pass (6) sroa on foo +; CHECK-RANGES: BISECT: running pass (7) early-cse on foo +; CHECK-RANGES: BISECT: running pass (8) openmp-opt on [module] + +; Test single pass selection: run only pass 5 +; RUN: opt -passes='annotation2metadata,forceattrs,inferattrs,function(lower-expect),function(simplifycfg),function(sroa),function(early-cse),openmp-opt' \ +; RUN: -opt-bisect=5 %s 2>&1 | FileCheck %s --check-prefix=CHECK-SINGLE +; CHECK-SINGLE: BISECT: NOT running pass (1) annotation2metadata on [module] +; CHECK-SINGLE: BISECT: NOT running pass (2) forceattrs on [module] +; CHECK-SINGLE: BISECT: NOT running pass (3) inferattrs on [module] +; CHECK-SINGLE: BISECT: NOT running pass (4) lower-expect on foo +; CHECK-SINGLE: BISECT: running pass (5) simplifycfg on foo +; CHECK-SINGLE: BISECT: NOT running pass (6) sroa on foo + +; Test running no passes +; RUN: opt -passes='annotation2metadata,forceattrs,inferattrs,function(lower-expect),function(simplifycfg),function(sroa),function(early-cse),openmp-opt' \ +; RUN: -opt-bisect=0 %s 2>&1 | FileCheck %s --check-prefix=CHECK-NONE +; CHECK-NONE: BISECT: NOT running pass (1) annotation2metadata on [module] +; CHECK-NONE: BISECT: NOT running pass (2) forceattrs on [module] +; CHECK-NONE: BISECT: NOT running pass (3) inferattrs on [module] +; CHECK-NONE: BISECT: NOT running pass (4) lower-expect on foo +; CHECK-NONE: BISECT: NOT running pass (5) simplifycfg on foo +; CHECK-NONE: BISECT: NOT running pass (6) sroa on foo + +; Test running all passes +; RUN: opt -passes='annotation2metadata,forceattrs,inferattrs,function(lower-expect),function(simplifycfg),function(sroa),function(early-cse),openmp-opt' \ +; RUN: -opt-bisect=-1 %s 2>&1 | FileCheck %s --check-prefix=CHECK-ALL +; CHECK-ALL: BISECT: running pass (1) annotation2metadata on [module] +; CHECK-ALL: BISECT: running pass (2) forceattrs on [module] +; CHECK-ALL: BISECT: running pass (3) inferattrs on [module] +; CHECK-ALL: BISECT: running pass (4) lower-expect on foo +; CHECK-ALL: BISECT: running pass (5) simplifycfg on foo +; CHECK-ALL: BISECT: running pass (6) sroa on foo + +; Test backward compatibility: -opt-bisect-limit=3 should be equivalent to -opt-bisect=1-3 +; RUN: opt -passes='annotation2metadata,forceattrs,inferattrs,function(lower-expect),function(simplifycfg),function(sroa),function(early-cse),openmp-opt' \ +; RUN: -opt-bisect-limit=3 %s 2>&1 | FileCheck %s --check-prefix=CHECK-LIMIT +; CHECK-LIMIT: BISECT: running pass (1) annotation2metadata on [module] +; CHECK-LIMIT: BISECT: running pass (2) forceattrs on [module] +; CHECK-LIMIT: BISECT: running pass (3) inferattrs on [module] +; CHECK-LIMIT: BISECT: NOT running pass (4) lower-expect on foo +; CHECK-LIMIT: BISECT: NOT running pass (5) simplifycfg on foo + +define void @foo() { + ret void +} + + diff --git a/llvm/tools/reduce-chunk-list/reduce-chunk-list.cpp b/llvm/tools/reduce-chunk-list/reduce-chunk-list.cpp index dc99859b516db..de41ca520c408 100644 --- a/llvm/tools/reduce-chunk-list/reduce-chunk-list.cpp +++ b/llvm/tools/reduce-chunk-list/reduce-chunk-list.cpp @@ -11,10 +11,9 @@ // //===----------------------------------------------------------------------===// -#include "llvm/ADT/DenseSet.h" #include "llvm/Support/CommandLine.h" -#include "llvm/Support/DebugCounter.h" #include "llvm/Support/Program.h" +#include "llvm/Support/Range.h" using namespace llvm; @@ -24,29 +23,15 @@ static cl::opt StartChunks(cl::Positional, cl::Required); static cl::opt Pessimist("pessimist", cl::init(false)); -using Chunk = DebugCounter::Chunk; - namespace { -SmallVector simplifyChunksList(ArrayRef Chunks) { - SmallVector Res; - Res.push_back(Chunks.front()); - for (unsigned Idx = 1; Idx < Chunks.size(); Idx++) { - if (Chunks[Idx].Begin == Res.back().End + 1) - Res.back().End = Chunks[Idx].End; - else - Res.push_back(Chunks[Idx]); - } - return Res; -} - -bool isStillInteresting(ArrayRef Chunks) { - SmallVector SimpleChunks = simplifyChunksList(Chunks); +bool isStillInteresting(ArrayRef Chunks) { + RangeUtils::RangeList SimpleChunks = RangeUtils::mergeAdjacentRanges(Chunks); std::string ChunkStr; { raw_string_ostream OS(ChunkStr); - DebugCounter::printChunks(OS, SimpleChunks); + RangeUtils::printRanges(OS, SimpleChunks); } errs() << "Checking with: " << ChunkStr << "\n"; @@ -73,18 +58,18 @@ bool isStillInteresting(ArrayRef Chunks) { return Res; } -bool increaseGranularity(SmallVector &Chunks) { +bool increaseGranularity(RangeUtils::RangeList &Chunks) { errs() << "Increasing granularity\n"; - SmallVector NewChunks; + RangeUtils::RangeList NewChunks; bool SplitOne = false; for (auto &C : Chunks) { - if (C.Begin == C.End) { + if (C.getBegin() == C.getEnd()) { NewChunks.push_back(C); } else { - int Half = (C.Begin + C.End) / 2; - NewChunks.push_back({C.Begin, Half}); - NewChunks.push_back({Half + 1, C.End}); + int64_t Half = (C.getBegin() + C.getEnd()) / 2; + NewChunks.push_back(Range(C.getBegin(), Half)); + NewChunks.push_back(Range(Half + 1, C.getEnd())); SplitOne = true; } } @@ -99,10 +84,14 @@ bool increaseGranularity(SmallVector &Chunks) { int main(int argc, char **argv) { cl::ParseCommandLineOptions(argc, argv); - SmallVector CurrChunks; - if (DebugCounter::parseChunks(StartChunks, CurrChunks)) { + auto ExpectedChunks = RangeUtils::parseRanges(StartChunks, ','); + if (!ExpectedChunks) { + handleAllErrors(ExpectedChunks.takeError(), [](const StringError &E) { + errs() << "Error parsing chunks: " << E.getMessage() << "\n"; + }); return 1; } + RangeUtils::RangeList CurrChunks = std::move(*ExpectedChunks); auto Program = sys::findProgramByName(ReproductionCmd); if (!Program) { @@ -126,7 +115,7 @@ int main(int argc, char **argv) { if (CurrChunks.size() == 1) break; - Chunk Testing = CurrChunks[Idx]; + Range Testing = CurrChunks[Idx]; errs() << "Trying to remove : "; Testing.print(errs()); errs() << "\n"; @@ -142,6 +131,7 @@ int main(int argc, char **argv) { } errs() << "Minimal Chunks = "; - DebugCounter::printChunks(llvm::errs(), simplifyChunksList(CurrChunks)); + RangeUtils::printRanges(llvm::errs(), + RangeUtils::mergeAdjacentRanges(CurrChunks)); errs() << "\n"; } diff --git a/llvm/unittests/Support/CMakeLists.txt b/llvm/unittests/Support/CMakeLists.txt index 868c40b13b9b2..e1821b0172960 100644 --- a/llvm/unittests/Support/CMakeLists.txt +++ b/llvm/unittests/Support/CMakeLists.txt @@ -74,6 +74,7 @@ add_llvm_unittest(SupportTests ProcessTest.cpp ProgramTest.cpp ProgramStackTest.cpp + RangeTest.cpp RecyclerTest.cpp RegexTest.cpp ReverseIterationTest.cpp diff --git a/llvm/unittests/Support/RangeTest.cpp b/llvm/unittests/Support/RangeTest.cpp new file mode 100644 index 0000000000000..172937c23f033 --- /dev/null +++ b/llvm/unittests/Support/RangeTest.cpp @@ -0,0 +1,284 @@ +//===- llvm/unittests/Support/RangeTest.cpp - Range tests ----------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "llvm/Support/Range.h" +#include "llvm/Testing/Support/Error.h" +#include "gtest/gtest.h" +#include + +using namespace llvm; + +TEST(RangeTest, BasicRange) { + Range R(5, 10); + EXPECT_EQ(R.getBegin(), 5); + EXPECT_EQ(R.getEnd(), 10); + EXPECT_TRUE(R.contains(5)); + EXPECT_TRUE(R.contains(7)); + EXPECT_TRUE(R.contains(10)); + EXPECT_FALSE(R.contains(4)); + EXPECT_FALSE(R.contains(11)); +} + +TEST(RangeTest, SingleValueRange) { + Range R(42); + EXPECT_EQ(R.getBegin(), 42); + EXPECT_EQ(R.getEnd(), 42); + EXPECT_TRUE(R.contains(42)); + EXPECT_FALSE(R.contains(41)); + EXPECT_FALSE(R.contains(43)); +} + +TEST(RangeTest, SizeBasic) { + Range R1(5, 10); + EXPECT_EQ(R1.size(), 6u); + + Range R2(0, 0); + EXPECT_EQ(R2.size(), 1u); +} + +TEST(RangeTest, SizeMixedSigns) { + Range R1(-2, 2); + EXPECT_EQ(R1.size(), 5u); + + Range R2(-1, 0); + EXPECT_EQ(R2.size(), 2u); +} + +TEST(RangeTest, SizeExtremesNonOverflow) { + // [INT64_MIN, -1] has size 2^63. + Range R1(std::numeric_limits::min(), -1); + EXPECT_EQ(R1.size(), (1ULL << 63)); + + // [0, INT64_MAX] has size 2^63. + Range R2(0, std::numeric_limits::max()); + EXPECT_EQ(R2.size(), (1ULL << 63)); + + // [INT64_MIN, 0] has size 2^63 + 1. + Range R3(std::numeric_limits::min(), 0); + EXPECT_EQ(R3.size(), (1ULL << 63) + 1); + + // Small extreme windows. + Range R4(std::numeric_limits::min(), + std::numeric_limits::min() + 10); + EXPECT_EQ(R4.size(), 11u); + + Range R5(std::numeric_limits::max() - 10, + std::numeric_limits::max()); + EXPECT_EQ(R5.size(), 11u); +} + +TEST(RangeTest, RangeOverlaps) { + Range R1(1, 5); + Range R2(3, 8); + Range R3(6, 10); + Range R4(11, 15); + + EXPECT_TRUE(R1.overlaps(R2)); + EXPECT_TRUE(R2.overlaps(R1)); + EXPECT_TRUE(R2.overlaps(R3)); + EXPECT_FALSE(R1.overlaps(R3)); + EXPECT_FALSE(R1.overlaps(R4)); + EXPECT_FALSE(R3.overlaps(R4)); +} + +TEST(RangeUtilsTest, ParseSingleNumber) { + auto ER = RangeUtils::parseRanges("42"); + ASSERT_THAT_EXPECTED(ER, Succeeded()); + auto Ranges = std::move(*ER); + EXPECT_EQ(Ranges.size(), 1U); + EXPECT_EQ(Ranges[0].getBegin(), 42); + EXPECT_EQ(Ranges[0].getEnd(), 42); +} + +TEST(RangeUtilsTest, ParseSingleRange) { + auto ER = RangeUtils::parseRanges("10-20"); + ASSERT_THAT_EXPECTED(ER, Succeeded()); + auto Ranges = std::move(*ER); + EXPECT_EQ(Ranges.size(), 1U); + EXPECT_EQ(Ranges[0].getBegin(), 10); + EXPECT_EQ(Ranges[0].getEnd(), 20); +} + +TEST(RangeUtilsTest, ParseMultipleRanges) { + auto ER = RangeUtils::parseRanges("1-5,10,15-20"); + ASSERT_THAT_EXPECTED(ER, Succeeded()); + auto Ranges = std::move(*ER); + EXPECT_EQ(Ranges.size(), 3U); + + // Ranges are in input order (DebugCounter style). + EXPECT_EQ(Ranges[0].getBegin(), 1); + EXPECT_EQ(Ranges[0].getEnd(), 5); + EXPECT_EQ(Ranges[1].getBegin(), 10); + EXPECT_EQ(Ranges[1].getEnd(), 10); + EXPECT_EQ(Ranges[2].getBegin(), 15); + EXPECT_EQ(Ranges[2].getEnd(), 20); +} + +TEST(RangeUtilsTest, ParseColonSeparated) { + auto ER = RangeUtils::parseRanges("1-5:10:15-20", ':'); + ASSERT_THAT_EXPECTED(ER, Succeeded()); + auto Ranges = std::move(*ER); + EXPECT_EQ(Ranges.size(), 3U); + EXPECT_EQ(Ranges[0].getBegin(), 1); + EXPECT_EQ(Ranges[0].getEnd(), 5); + EXPECT_EQ(Ranges[1].getBegin(), 10); + EXPECT_EQ(Ranges[1].getEnd(), 10); + EXPECT_EQ(Ranges[2].getBegin(), 15); + EXPECT_EQ(Ranges[2].getEnd(), 20); +} + +TEST(RangeUtilsTest, ParseEmptyString) { + auto ER = RangeUtils::parseRanges(""); + ASSERT_THAT_EXPECTED(ER, Succeeded()); + auto Ranges = std::move(*ER); + EXPECT_TRUE(Ranges.empty()); +} + +TEST(RangeUtilsTest, ParseInvalidRanges) { + // Invalid number. + auto ER1 = RangeUtils::parseRanges("abc"); + EXPECT_THAT_EXPECTED(ER1, Failed()); + consumeError(ER1.takeError()); + + // Invalid range (begin > end). + auto ER2 = RangeUtils::parseRanges("10-5"); + EXPECT_THAT_EXPECTED(ER2, Failed()); + consumeError(ER2.takeError()); + + // Out of order ranges (DebugCounter constraint and overlap). + auto ER3 = RangeUtils::parseRanges("10,5"); + EXPECT_THAT_EXPECTED(ER3, Failed()); + consumeError(ER3.takeError()); + + auto ER4 = RangeUtils::parseRanges("1-5,3-7"); + EXPECT_THAT_EXPECTED(ER4, Failed()); + consumeError(ER4.takeError()); +} + +TEST(RangeUtilsTest, Contains) { + auto ER = RangeUtils::parseRanges("1-5,10,15-20"); + ASSERT_THAT_EXPECTED(ER, Succeeded()); + auto Ranges = std::move(*ER); + + EXPECT_TRUE(RangeUtils::contains(Ranges, 1)); + EXPECT_TRUE(RangeUtils::contains(Ranges, 3)); + EXPECT_TRUE(RangeUtils::contains(Ranges, 5)); + EXPECT_TRUE(RangeUtils::contains(Ranges, 10)); + EXPECT_TRUE(RangeUtils::contains(Ranges, 15)); + EXPECT_TRUE(RangeUtils::contains(Ranges, 18)); + EXPECT_TRUE(RangeUtils::contains(Ranges, 20)); + + EXPECT_FALSE(RangeUtils::contains(Ranges, 6)); + EXPECT_FALSE(RangeUtils::contains(Ranges, 9)); + EXPECT_FALSE(RangeUtils::contains(Ranges, 11)); + EXPECT_FALSE(RangeUtils::contains(Ranges, 14)); + EXPECT_FALSE(RangeUtils::contains(Ranges, 21)); +} + +TEST(RangeUtilsTest, SeparatorParameter) { + RangeUtils::RangeList ColonRanges, CommaRanges; + + // Test explicit separator parameters. + auto ERC = RangeUtils::parseRanges("1-5:10:15-20", ':'); + ASSERT_THAT_EXPECTED(ERC, Succeeded()); + ColonRanges = std::move(*ERC); + + auto ERM = RangeUtils::parseRanges("1-5,10,15-20", ','); + ASSERT_THAT_EXPECTED(ERM, Succeeded()); + CommaRanges = std::move(*ERM); + + EXPECT_EQ(ColonRanges.size(), CommaRanges.size()); + for (size_t I = 0; I < ColonRanges.size(); ++I) { + EXPECT_EQ(ColonRanges[I].getBegin(), CommaRanges[I].getBegin()); + EXPECT_EQ(ColonRanges[I].getEnd(), CommaRanges[I].getEnd()); + } + + // Test that both work with contains(). + EXPECT_TRUE(RangeUtils::contains(ColonRanges, 3)); + EXPECT_TRUE(RangeUtils::contains(CommaRanges, 3)); + EXPECT_TRUE(RangeUtils::contains(ColonRanges, 10)); + EXPECT_TRUE(RangeUtils::contains(CommaRanges, 10)); + EXPECT_TRUE(RangeUtils::contains(ColonRanges, 18)); + EXPECT_TRUE(RangeUtils::contains(CommaRanges, 18)); + + EXPECT_FALSE(RangeUtils::contains(ColonRanges, 8)); + EXPECT_FALSE(RangeUtils::contains(CommaRanges, 8)); +} + +TEST(RangeUtilsTest, DefaultCommaSeparator) { + // Test that comma is the default separator. + auto ER = RangeUtils::parseRanges("1-5,10,15-20"); + ASSERT_THAT_EXPECTED(ER, Succeeded()); + auto Ranges = std::move(*ER); + EXPECT_EQ(Ranges.size(), 3U); + EXPECT_EQ(Ranges[0].getBegin(), 1); + EXPECT_EQ(Ranges[0].getEnd(), 5); + EXPECT_EQ(Ranges[1].getBegin(), 10); + EXPECT_EQ(Ranges[1].getEnd(), 10); + EXPECT_EQ(Ranges[2].getBegin(), 15); + EXPECT_EQ(Ranges[2].getEnd(), 20); +} + +TEST(RangeTest, MergeAdjacentRanges) { + RangeUtils::RangeList Input, Expected, Result; + + // Empty input + Result = RangeUtils::mergeAdjacentRanges(Input); + EXPECT_TRUE(Result.empty()); + + // Single range - no change. + Input.push_back(Range(5, 10)); + Expected.push_back(Range(5, 10)); + Result = RangeUtils::mergeAdjacentRanges(Input); + EXPECT_EQ(Expected, Result); + + // Adjacent ranges should merge. + Input.clear(); + Expected.clear(); + Input.push_back(Range(1, 3)); + Input.push_back(Range(4, 6)); + Input.push_back(Range(7, 9)); + Expected.push_back(Range(1, 9)); + Result = RangeUtils::mergeAdjacentRanges(Input); + EXPECT_EQ(Expected, Result); + + // Non-adjacent ranges should not merge. + Input.clear(); + Expected.clear(); + Input.push_back(Range(1, 3)); + Input.push_back(Range(5, 7)); // Gap between 3 and 5. + Input.push_back(Range(10, 12)); // Gap between 7 and 10. + Expected.push_back(Range(1, 3)); + Expected.push_back(Range(5, 7)); + Expected.push_back(Range(10, 12)); + Result = RangeUtils::mergeAdjacentRanges(Input); + EXPECT_EQ(Expected, Result); + + // Mixed adjacent and non-adjacent. + Input.clear(); + Expected.clear(); + Input.push_back(Range(1, 3)); + Input.push_back(Range(4, 6)); // Adjacent to first. + Input.push_back(Range(8, 10)); // Gap. + Input.push_back(Range(11, 13)); // Adjacent to third. + Input.push_back(Range(14, 16)); // Adjacent to fourth. + Expected.push_back(Range(1, 6)); // Merged 1-3 and 4-6. + Expected.push_back(Range(8, 16)); // Merged 8-10, 11-13, 14-16. + Result = RangeUtils::mergeAdjacentRanges(Input); + EXPECT_EQ(Expected, Result); + + // Single numbers that are adjacent. + Input.clear(); + Expected.clear(); + Input.push_back(Range(5)); + Input.push_back(Range(6)); + Input.push_back(Range(7)); + Expected.push_back(Range(5, 7)); + Result = RangeUtils::mergeAdjacentRanges(Input); + EXPECT_EQ(Expected, Result); +}