[clang] [lld] [llvm] [Support] Support 5-component VersionTuples (PR #181275)
Adrian Prantl via cfe-commits
cfe-commits at lists.llvm.org
Fri Feb 13 10:40:41 PST 2026
https://github.com/adrian-prantl updated https://github.com/llvm/llvm-project/pull/181275
>From cede5ffb4bce14c50488ff569ca2a8186bbc26da Mon Sep 17 00:00:00 2001
From: Adrian Prantl <aprantl at apple.com>
Date: Thu, 12 Feb 2026 15:47:52 -0800
Subject: [PATCH 1/2] [Support] Support 5-component VersionTuples
LLDB parses compiler versions out of DW_AT_producer DWARF attributes
into a VersionTuple. The Swift compiler recently switched to
5-component version numbers. In order to support this version scheme
without growing the size of VersionTuple, this patch dedicates the
last 10 bits of the build version to a 5th "sub-build" component. The
Swift compiler currently uses 1 digit for this and promises to never
use more than 3 digits for the last 3 components.
This patch still leaves 6 decimal digits for the build component for
other version schemes.
rdar://170181060
---
llvm/include/llvm/Support/VersionTuple.h | 63 +++++++++++++++------
llvm/lib/Support/VersionTuple.cpp | 31 ++++++++--
llvm/unittests/Support/VersionTupleTest.cpp | 24 +++++++-
3 files changed, 93 insertions(+), 25 deletions(-)
diff --git a/llvm/include/llvm/Support/VersionTuple.h b/llvm/include/llvm/Support/VersionTuple.h
index 867f81d74fb4d..009f301e5b7b2 100644
--- a/llvm/include/llvm/Support/VersionTuple.h
+++ b/llvm/include/llvm/Support/VersionTuple.h
@@ -36,36 +36,50 @@ class VersionTuple {
unsigned Subminor : 31;
unsigned HasSubminor : 1;
- unsigned Build : 31;
+ unsigned Build : 20;
+ unsigned Subbuild : 10;
unsigned HasBuild : 1;
+ unsigned HasSubbuild : 1;
public:
constexpr VersionTuple()
: Major(0), Minor(0), HasMinor(false), Subminor(0), HasSubminor(false),
- Build(0), HasBuild(false) {}
+ Build(0), Subbuild(0), HasBuild(false), HasSubbuild(false) {}
explicit constexpr VersionTuple(unsigned Major)
: Major(Major), Minor(0), HasMinor(false), Subminor(0),
- HasSubminor(false), Build(0), HasBuild(false) {}
+ HasSubminor(false), Build(0), Subbuild(0), HasBuild(false),
+ HasSubbuild(false) {}
explicit constexpr VersionTuple(unsigned Major, unsigned Minor)
: Major(Major), Minor(Minor), HasMinor(true), Subminor(0),
- HasSubminor(false), Build(0), HasBuild(false) {}
+ HasSubminor(false), Build(0), Subbuild(0), HasBuild(false),
+ HasSubbuild(false) {}
explicit constexpr VersionTuple(unsigned Major, unsigned Minor,
unsigned Subminor)
: Major(Major), Minor(Minor), HasMinor(true), Subminor(Subminor),
- HasSubminor(true), Build(0), HasBuild(false) {}
+ HasSubminor(true), Build(0), Subbuild(0), HasBuild(false),
+ HasSubbuild(false) {}
explicit constexpr VersionTuple(unsigned Major, unsigned Minor,
unsigned Subminor, unsigned Build)
: Major(Major), Minor(Minor), HasMinor(true), Subminor(Subminor),
- HasSubminor(true), Build(Build), HasBuild(true) {}
+ HasSubminor(true), Build(Build), Subbuild(0), HasBuild(true),
+ HasSubbuild(false) {}
+
+ explicit constexpr VersionTuple(unsigned Major, unsigned Minor,
+ unsigned Subminor, unsigned Build,
+ unsigned Subbuild)
+ : Major(Major), Minor(Minor), HasMinor(true), Subminor(Subminor),
+ HasSubminor(true), Build(Build), Subbuild(Subbuild), HasBuild(true),
+ HasSubbuild(true) {}
/// Determine whether this version information is empty
/// (e.g., all version components are zero).
bool empty() const {
- return Major == 0 && Minor == 0 && Subminor == 0 && Build == 0;
+ return Major == 0 && Minor == 0 && Subminor == 0 && Build == 0 &&
+ Subbuild == 0;
}
/// Retrieve the major version number.
@@ -92,6 +106,13 @@ class VersionTuple {
return Build;
}
+ /// Retrieve the subbuild version number, if provided.
+ std::optional<unsigned> getSubbuild() const {
+ if (!HasSubbuild)
+ return std::nullopt;
+ return Subbuild;
+ }
+
/// Return a version tuple that contains only the first 3 version components.
VersionTuple withoutBuild() const {
if (HasBuild)
@@ -106,12 +127,15 @@ class VersionTuple {
/// Return a version tuple that contains only components that are non-zero.
VersionTuple normalize() const {
VersionTuple Result = *this;
- if (Result.Build == 0) {
- Result.HasBuild = false;
- if (Result.Subminor == 0) {
- Result.HasSubminor = false;
- if (Result.Minor == 0)
- Result.HasMinor = false;
+ if (Result.Subbuild == 0) {
+ Result.HasSubbuild = false;
+ if (Result.Build == 0) {
+ Result.HasBuild = false;
+ if (Result.Subminor == 0) {
+ Result.HasSubminor = false;
+ if (Result.Minor == 0)
+ Result.HasMinor = false;
+ }
}
}
return Result;
@@ -121,7 +145,8 @@ class VersionTuple {
/// provided, minor and subminor version numbers are considered to be zero.
friend bool operator==(const VersionTuple &X, const VersionTuple &Y) {
return X.Major == Y.Major && X.Minor == Y.Minor &&
- X.Subminor == Y.Subminor && X.Build == Y.Build;
+ X.Subminor == Y.Subminor && X.Build == Y.Build &&
+ X.Subbuild == Y.Subbuild;
}
/// Determine if two version numbers are not equivalent.
@@ -137,8 +162,8 @@ class VersionTuple {
/// If not provided, minor and subminor version numbers are considered to be
/// zero.
friend bool operator<(const VersionTuple &X, const VersionTuple &Y) {
- return std::tie(X.Major, X.Minor, X.Subminor, X.Build) <
- std::tie(Y.Major, Y.Minor, Y.Subminor, Y.Build);
+ return std::tie(X.Major, X.Minor, X.Subminor, X.Build, X.Subbuild) <
+ std::tie(Y.Major, Y.Minor, Y.Subminor, Y.Build, Y.Subbuild);
}
/// Determine whether one version number follows another.
@@ -168,13 +193,13 @@ class VersionTuple {
}
friend hash_code hash_value(const VersionTuple &VT) {
- return hash_combine(VT.Major, VT.Minor, VT.Subminor, VT.Build);
+ return hash_combine(VT.Major, VT.Minor, VT.Subminor, VT.Build, VT.Subbuild);
}
template <typename HasherT, llvm::endianness Endianness>
friend void addHash(HashBuilder<HasherT, Endianness> &HBuilder,
const VersionTuple &VT) {
- HBuilder.add(VT.Major, VT.Minor, VT.Subminor, VT.Build);
+ HBuilder.add(VT.Major, VT.Minor, VT.Subminor, VT.Build, VT.Subbuild);
}
/// Retrieve a string representation of the version number.
@@ -203,6 +228,8 @@ template <> struct DenseMapInfo<VersionTuple> {
Result = detail::combineHashValue(Result, *Subminor);
if (auto Build = Value.getBuild())
Result = detail::combineHashValue(Result, *Build);
+ if (auto Subbuild = Value.getSubbuild())
+ Result = detail::combineHashValue(Result, *Subbuild);
return Result;
}
diff --git a/llvm/lib/Support/VersionTuple.cpp b/llvm/lib/Support/VersionTuple.cpp
index c6e20f1bd3ef4..0a023c08d3039 100644
--- a/llvm/lib/Support/VersionTuple.cpp
+++ b/llvm/lib/Support/VersionTuple.cpp
@@ -35,6 +35,8 @@ raw_ostream &llvm::operator<<(raw_ostream &Out, const VersionTuple &V) {
Out << '.' << *Subminor;
if (std::optional<unsigned> Build = V.getBuild())
Out << '.' << *Build;
+ if (std::optional<unsigned> Subbuild = V.getSubbuild())
+ Out << '.' << *Subbuild;
return Out;
}
@@ -61,7 +63,7 @@ static bool parseInt(StringRef &input, unsigned &value) {
}
bool VersionTuple::tryParse(StringRef input) {
- unsigned major = 0, minor = 0, micro = 0, build = 0;
+ unsigned major = 0, minor = 0, subminor = 0, build = 0, subbuild = 0;
// Parse the major version, [0-9]+
if (parseInt(input, major))
@@ -84,32 +86,49 @@ bool VersionTuple::tryParse(StringRef input) {
return false;
}
- // If we're not done, parse the micro version, \.[0-9]+
+ // If we're not done, parse the subminor version, \.[0-9]+
if (!input.consume_front("."))
return true;
- if (parseInt(input, micro))
+ if (parseInt(input, subminor))
return true;
if (input.empty()) {
- *this = VersionTuple(major, minor, micro);
+ *this = VersionTuple(major, minor, subminor);
return false;
}
- // If we're not done, parse the micro version, \.[0-9]+
+ // If we're not done, parse the build version, \.[0-9]+
if (!input.consume_front("."))
return true;
if (parseInt(input, build))
return true;
+ if (build >= 1024 * 1024)
+ return true;
+
+ if (input.empty()) {
+ *this = VersionTuple(major, minor, subminor, build);
+ return false;
+ }
+
+ // And the subbuild version, \.[0-9]+
+ if (!input.consume_front("."))
+ return true;
+ if (parseInt(input, subbuild))
+ return true;
+ if (subbuild >= 1024)
+ return true;
// If we have characters left over, it's an error.
if (!input.empty())
return true;
- *this = VersionTuple(major, minor, micro, build);
+ *this = VersionTuple(major, minor, subminor, build, subbuild);
return false;
}
VersionTuple VersionTuple::withMajorReplaced(unsigned NewMajor) const {
+ if (HasSubbuild)
+ return VersionTuple(NewMajor, Minor, Subminor, Build, Subbuild);
if (HasBuild)
return VersionTuple(NewMajor, Minor, Subminor, Build);
if (HasSubminor)
diff --git a/llvm/unittests/Support/VersionTupleTest.cpp b/llvm/unittests/Support/VersionTupleTest.cpp
index d498d670fb710..724e365f82360 100644
--- a/llvm/unittests/Support/VersionTupleTest.cpp
+++ b/llvm/unittests/Support/VersionTupleTest.cpp
@@ -17,6 +17,14 @@ TEST(VersionTuple, getAsString) {
EXPECT_EQ("1.2", VersionTuple(1, 2).getAsString());
EXPECT_EQ("1.2.3", VersionTuple(1, 2, 3).getAsString());
EXPECT_EQ("1.2.3.4", VersionTuple(1, 2, 3, 4).getAsString());
+ EXPECT_EQ("1.2.3.4.5", VersionTuple(1, 2, 3, 4, 5).getAsString());
+
+ VersionTuple v(1, 2, 3, 4, 5);
+ EXPECT_EQ(v.getMajor(), 1u);
+ EXPECT_EQ(v.getMinor(), 2u);
+ EXPECT_EQ(v.getSubminor(), 3u);
+ EXPECT_EQ(v.getBuild(), 4u);
+ EXPECT_EQ(v.getSubbuild(), 5u);
}
TEST(VersionTuple, tryParse) {
@@ -34,18 +42,24 @@ TEST(VersionTuple, tryParse) {
EXPECT_FALSE(VT.tryParse("1.2.3.4"));
EXPECT_EQ("1.2.3.4", VT.getAsString());
+ EXPECT_FALSE(VT.tryParse("1.2.3.4.5"));
+ EXPECT_EQ("1.2.3.4.5", VT.getAsString());
+
EXPECT_TRUE(VT.tryParse(""));
EXPECT_TRUE(VT.tryParse("1."));
EXPECT_TRUE(VT.tryParse("1.2."));
EXPECT_TRUE(VT.tryParse("1.2.3."));
EXPECT_TRUE(VT.tryParse("1.2.3.4."));
- EXPECT_TRUE(VT.tryParse("1.2.3.4.5"));
+ EXPECT_TRUE(VT.tryParse("1.2.3.4.5."));
+ EXPECT_TRUE(VT.tryParse("1.2.3.4.5.6"));
EXPECT_TRUE(VT.tryParse("1-2"));
EXPECT_TRUE(VT.tryParse("1+2"));
EXPECT_TRUE(VT.tryParse(".1"));
EXPECT_TRUE(VT.tryParse(" 1"));
EXPECT_TRUE(VT.tryParse("1 "));
EXPECT_TRUE(VT.tryParse("."));
+ EXPECT_TRUE(VT.tryParse("1.2.3.1048576"));
+ EXPECT_TRUE(VT.tryParse("1.2.3.4.1024"));
}
TEST(VersionTuple, withMajorReplaced) {
@@ -76,4 +90,12 @@ TEST(VersionTuple, withMajorReplaced) {
EXPECT_TRUE(ReplacedVersion.getSubminor().has_value());
EXPECT_TRUE(ReplacedVersion.getBuild().has_value());
EXPECT_EQ(VersionTuple(7, 11, 12, 2), ReplacedVersion);
+
+ VT = VersionTuple(101, 11, 12, 2, 8);
+ ReplacedVersion = VT.withMajorReplaced(7);
+ EXPECT_TRUE(ReplacedVersion.getMinor().has_value());
+ EXPECT_TRUE(ReplacedVersion.getSubminor().has_value());
+ EXPECT_TRUE(ReplacedVersion.getBuild().has_value());
+ EXPECT_TRUE(ReplacedVersion.getSubbuild().has_value());
+ EXPECT_EQ(VersionTuple(7, 11, 12, 2, 8), ReplacedVersion);
}
>From 60b1eec1e3507731ec62ba66465b21c78607612f Mon Sep 17 00:00:00 2001
From: Adrian Prantl <aprantl at apple.com>
Date: Fri, 13 Feb 2026 10:40:09 -0800
Subject: [PATCH 2/2] [LLD] Reject platform versions with 4+ components
---
clang/lib/Driver/ToolChains/Darwin.cpp | 10 +++++++---
lld/MachO/Driver.cpp | 11 +++++------
lld/test/MachO/platform-version.s | 6 +++---
llvm/include/llvm/Support/VersionTuple.h | 16 +++++++---------
4 files changed, 22 insertions(+), 21 deletions(-)
diff --git a/clang/lib/Driver/ToolChains/Darwin.cpp b/clang/lib/Driver/ToolChains/Darwin.cpp
index 073f23950160c..1c95a79a52a9c 100644
--- a/clang/lib/Driver/ToolChains/Darwin.cpp
+++ b/clang/lib/Driver/ToolChains/Darwin.cpp
@@ -1119,10 +1119,14 @@ VersionTuple MachO::getLinkerVersion(const llvm::opt::ArgList &Args) const {
}
VersionTuple NewLinkerVersion;
- if (Arg *A = Args.getLastArg(options::OPT_mlinker_version_EQ))
- if (NewLinkerVersion.tryParse(A->getValue()))
+ if (Arg *A = Args.getLastArg(options::OPT_mlinker_version_EQ)) {
+ // Rejecting subbuild version is probably not necessary, but some
+ // existing tests depend on this.
+ if (NewLinkerVersion.tryParse(A->getValue()) ||
+ NewLinkerVersion.getSubbuild())
getDriver().Diag(diag::err_drv_invalid_version_number)
- << A->getAsString(Args);
+ << A->getAsString(Args);
+ }
LinkerVersion = NewLinkerVersion;
return *LinkerVersion;
diff --git a/lld/MachO/Driver.cpp b/lld/MachO/Driver.cpp
index 973b3f5535cb4..58fbe64c2d1f9 100644
--- a/lld/MachO/Driver.cpp
+++ b/lld/MachO/Driver.cpp
@@ -874,13 +874,12 @@ static PlatformVersion parsePlatformVersion(const Arg *arg) {
.Default(PLATFORM_UNKNOWN);
if (platformVersion.platform == PLATFORM_UNKNOWN)
error(Twine("malformed platform: ") + platformStr);
- // TODO: check validity of version strings, which varies by platform
- // NOTE: ld64 accepts version strings with 5 components
- // llvm::VersionTuple accepts no more than 4 components
- // Has Apple ever published version strings with 5 components?
- if (platformVersion.minimum.tryParse(minVersionStr))
+ // The underlying load command only supports 3 components.
+ if (platformVersion.minimum.tryParse(minVersionStr) ||
+ platformVersion.minimum.getBuild())
error(Twine("malformed minimum version: ") + minVersionStr);
- if (platformVersion.sdk.tryParse(sdkVersionStr))
+ if (platformVersion.sdk.tryParse(sdkVersionStr) ||
+ platformVersion.sdk.getBuild())
error(Twine("malformed sdk version: ") + sdkVersionStr);
return platformVersion;
}
diff --git a/lld/test/MachO/platform-version.s b/lld/test/MachO/platform-version.s
index 57fbae62b2ffc..46957ccd44492 100644
--- a/lld/test/MachO/platform-version.s
+++ b/lld/test/MachO/platform-version.s
@@ -25,10 +25,10 @@
# RUN: -platform_version iOS 1 2.a \
# RUN: | FileCheck --check-prefix=FAIL-MALFORM %s
# RUN: not %no-arg-lld -arch x86_64 -o %t %t.o 2>&1 \
-# RUN: -platform_version tvOS 1.2.3.4.5 10 \
+# RUN: -platform_version tvOS 1.2.3.4 10 \
# RUN: | FileCheck --check-prefix=FAIL-MALFORM %s
# RUN: not %no-arg-lld -arch x86_64 -o %t %t.o 2>&1 \
-# RUN: -platform_version watchOS 10 1.2.3.4.5 \
+# RUN: -platform_version watchOS 10 1.2.3.4 \
# RUN: | FileCheck --check-prefix=FAIL-MALFORM %s
# FAIL-MALFORM-NOT: malformed platform: {{.*}}
# FAIL-MALFORM: malformed {{minimum|sdk}} version: {{.*}}
@@ -40,7 +40,7 @@
# RUN: %no-arg-lld -arch x86_64 -o %t %t.o 2>&1 \
# RUN: -platform_version "iOS Simulator" 1.2.3 5.6.7
# RUN: %no-arg-lld -arch x86_64 -o %t %t.o 2>&1 \
-# RUN: -platform_version tvOS-Simulator 1.2.3.4 5.6.7.8
+# RUN: -platform_version tvOS-Simulator 1.2.3 5.6.7
# RUN: %no-arg-lld -arch x86_64 -o %t %t.o 2>&1 \
# RUN: -platform_version watchOS-Simulator 1 5
# RUN: %no-arg-lld -arch x86_64 -o %t %t.o 2>&1 \
diff --git a/llvm/include/llvm/Support/VersionTuple.h b/llvm/include/llvm/Support/VersionTuple.h
index 009f301e5b7b2..e4500a714d12b 100644
--- a/llvm/include/llvm/Support/VersionTuple.h
+++ b/llvm/include/llvm/Support/VersionTuple.h
@@ -75,12 +75,13 @@ class VersionTuple {
HasSubminor(true), Build(Build), Subbuild(Subbuild), HasBuild(true),
HasSubbuild(true) {}
+ std::tuple<unsigned, unsigned, unsigned, unsigned, unsigned> asTuple() const {
+ return {Major, Minor, Subminor, Build, Subbuild};
+ }
+
/// Determine whether this version information is empty
/// (e.g., all version components are zero).
- bool empty() const {
- return Major == 0 && Minor == 0 && Subminor == 0 && Build == 0 &&
- Subbuild == 0;
- }
+ bool empty() const { return *this == VersionTuple(); }
/// Retrieve the major version number.
unsigned getMajor() const { return Major; }
@@ -144,9 +145,7 @@ class VersionTuple {
/// Determine if two version numbers are equivalent. If not
/// provided, minor and subminor version numbers are considered to be zero.
friend bool operator==(const VersionTuple &X, const VersionTuple &Y) {
- return X.Major == Y.Major && X.Minor == Y.Minor &&
- X.Subminor == Y.Subminor && X.Build == Y.Build &&
- X.Subbuild == Y.Subbuild;
+ return X.asTuple() == Y.asTuple();
}
/// Determine if two version numbers are not equivalent.
@@ -162,8 +161,7 @@ class VersionTuple {
/// If not provided, minor and subminor version numbers are considered to be
/// zero.
friend bool operator<(const VersionTuple &X, const VersionTuple &Y) {
- return std::tie(X.Major, X.Minor, X.Subminor, X.Build, X.Subbuild) <
- std::tie(Y.Major, Y.Minor, Y.Subminor, Y.Build, Y.Subbuild);
+ return X.asTuple() < Y.asTuple();
}
/// Determine whether one version number follows another.
More information about the cfe-commits
mailing list