[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