[llvm-branch-commits] [libcxx] [libc++][chrono] Adds the sys_info class. (PR #85619)

via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Mon Mar 18 02:44:52 PDT 2024


llvmbot wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-libcxx

Author: Mark de Wever (mordante)

<details>
<summary>Changes</summary>

Adds the sys_info class and time_zone::get_info(). The code still has a few quirks and has not been optimized for performance yet.

The returned sys_info is compared against the output of the zdump tool in the test giving confidence the implementation is correct.

Implements parts of:
- P0355 Extending <chrono> to Calendars and Time Zones

Implements:
- LWGXXXX The sys_info range should be affected by save

---

Patch is 119.62 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/85619.diff


17 Files Affected:

- (modified) libcxx/docs/Status/Cxx2cIssues.csv (+1) 
- (modified) libcxx/include/CMakeLists.txt (+1) 
- (added) libcxx/include/__chrono/sys_info.h (+53) 
- (modified) libcxx/include/__chrono/time_zone.h (+9) 
- (modified) libcxx/include/chrono (+13) 
- (modified) libcxx/include/libcxx.imp (+1) 
- (modified) libcxx/modules/std/chrono.inc (+2) 
- (modified) libcxx/src/include/tzdb/types_private.h (+13-2) 
- (modified) libcxx/src/include/tzdb/tzdb_list_private.h (+7) 
- (modified) libcxx/src/time_zone.cpp (+855) 
- (modified) libcxx/test/libcxx/diagnostics/chrono.nodiscard_extensions.compile.pass.cpp (+1) 
- (modified) libcxx/test/libcxx/diagnostics/chrono.nodiscard_extensions.verify.cpp (+2) 
- (added) libcxx/test/libcxx/time/time.zone/time.zone.timezone/time.zone.members/get_info.sys_time.pass.cpp (+140) 
- (added) libcxx/test/std/time/time.zone/time.zone.info/time.zone.info.sys/sys_info.members.compile.pass.cpp (+33) 
- (added) libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/get_info.sys_time.pass.cpp (+1374) 
- (added) libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/sys_info.zdump.pass.cpp (+124) 
- (modified) libcxx/utils/libcxx/test/features.py (+6) 


``````````diff
diff --git a/libcxx/docs/Status/Cxx2cIssues.csv b/libcxx/docs/Status/Cxx2cIssues.csv
index 58e995809777c1..a6aa70ecc866e0 100644
--- a/libcxx/docs/Status/Cxx2cIssues.csv
+++ b/libcxx/docs/Status/Cxx2cIssues.csv
@@ -41,4 +41,5 @@
 "`4001 <https://wg21.link/LWG4001>`__","``iota_view`` should provide ``empty``","Kona November 2023","","","|ranges|"
 "","","","","",""
 "`3343 <https://wg21.link/LWG3343>`__","Ordering of calls to ``unlock()`` and ``notify_all()`` in Effects element of ``notify_all_at_thread_exit()`` should be reversed","Not Yet Adopted","|Complete|","16.0",""
+"XXXX","","The sys_info range should be affected by save","Not Yet Adopted","|Complete|","19.0"
 "","","","","",""
diff --git a/libcxx/include/CMakeLists.txt b/libcxx/include/CMakeLists.txt
index 5d9a693d152e4a..24cef9cd877c62 100644
--- a/libcxx/include/CMakeLists.txt
+++ b/libcxx/include/CMakeLists.txt
@@ -289,6 +289,7 @@ set(files
   __chrono/ostream.h
   __chrono/parser_std_format_spec.h
   __chrono/statically_widen.h
+  __chrono/sys_info.h
   __chrono/steady_clock.h
   __chrono/system_clock.h
   __chrono/time_point.h
diff --git a/libcxx/include/__chrono/sys_info.h b/libcxx/include/__chrono/sys_info.h
new file mode 100644
index 00000000000000..16ec5dd254d59a
--- /dev/null
+++ b/libcxx/include/__chrono/sys_info.h
@@ -0,0 +1,53 @@
+// -*- 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
+//
+//===----------------------------------------------------------------------===//
+
+// For information see https://libcxx.llvm.org/DesignDocs/TimeZone.html
+
+#ifndef _LIBCPP___CHRONO_SYS_INFO_H
+#define _LIBCPP___CHRONO_SYS_INFO_H
+
+#include <version>
+// Enable the contents of the header only when libc++ was built with experimental features enabled.
+#if !defined(_LIBCPP_HAS_NO_INCOMPLETE_TZDB)
+
+#  include <__chrono/duration.h>
+#  include <__chrono/system_clock.h>
+#  include <__chrono/time_point.h>
+#  include <__config>
+#  include <string>
+
+#  if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
+#    pragma GCC system_header
+#  endif
+
+_LIBCPP_BEGIN_NAMESPACE_STD
+
+#  if _LIBCPP_STD_VER >= 20 && !defined(_LIBCPP_HAS_NO_TIME_ZONE_DATABASE) && !defined(_LIBCPP_HAS_NO_FILESYSTEM) &&   \
+      !defined(_LIBCPP_HAS_NO_LOCALIZATION)
+
+namespace chrono {
+
+struct sys_info {
+  sys_seconds begin;
+  sys_seconds end;
+  seconds offset;
+  minutes save;
+  string abbrev;
+};
+
+} // namespace chrono
+
+#  endif // _LIBCPP_STD_VER >= 20 && !defined(_LIBCPP_HAS_NO_TIME_ZONE_DATABASE) && !defined(_LIBCPP_HAS_NO_FILESYSTEM)
+         // && !defined(_LIBCPP_HAS_NO_LOCALIZATION)
+
+_LIBCPP_END_NAMESPACE_STD
+
+#endif // !defined(_LIBCPP_HAS_NO_INCOMPLETE_TZDB)
+
+#endif // _LIBCPP___CHRONO_SYS_INFO_H
diff --git a/libcxx/include/__chrono/time_zone.h b/libcxx/include/__chrono/time_zone.h
index 7d97327a6c8e99..a5af6297a4cf92 100644
--- a/libcxx/include/__chrono/time_zone.h
+++ b/libcxx/include/__chrono/time_zone.h
@@ -16,6 +16,7 @@
 // Enable the contents of the header only when libc++ was built with experimental features enabled.
 #if !defined(_LIBCPP_HAS_NO_INCOMPLETE_TZDB)
 
+#  include <__chrono/sys_info.h>
 #  include <__compare/strong_order.h>
 #  include <__config>
 #  include <__memory/unique_ptr.h>
@@ -55,10 +56,18 @@ class _LIBCPP_AVAILABILITY_TZDB time_zone {
 
   _LIBCPP_NODISCARD_EXT _LIBCPP_HIDE_FROM_ABI string_view name() const noexcept { return __name(); }
 
+  template <class _Duration>
+  _LIBCPP_NODISCARD_EXT _LIBCPP_HIDE_FROM_ABI sys_info get_info(const sys_time<_Duration>& __time) const {
+    return __get_info(chrono::time_point_cast<seconds>(__time));
+  }
+
   [[nodiscard]] _LIBCPP_HIDE_FROM_ABI const __impl& __implementation() const noexcept { return *__impl_; }
 
 private:
   [[nodiscard]] _LIBCPP_EXPORTED_FROM_ABI string_view __name() const noexcept;
+
+  [[nodiscard]] _LIBCPP_AVAILABILITY_TZDB _LIBCPP_EXPORTED_FROM_ABI sys_info __get_info(sys_seconds __time) const;
+
   unique_ptr<__impl> __impl_;
 };
 
diff --git a/libcxx/include/chrono b/libcxx/include/chrono
index 8fdc30a3624dfc..00b940a6610a3a 100644
--- a/libcxx/include/chrono
+++ b/libcxx/include/chrono
@@ -724,6 +724,15 @@ const time_zone* current_zone()
 const tzdb& reload_tzdb();                                                       // C++20
 string remote_version();                                                         // C++20
 
+// [time.zone.info], information classes
+struct sys_info {                                                                // C++20
+  sys_seconds   begin;
+  sys_seconds   end;
+  seconds       offset;
+  minutes       save;
+  string        abbrev;
+};
+
 // 25.10.5, class time_zone                                                      // C++20
 enum class choose {earliest, latest};
 class time_zone {
@@ -733,6 +742,9 @@ class time_zone {
   // unspecified additional constructors
 
   string_view name() const noexcept;
+
+  template<class Duration>
+  sys_info get_info(const sys_time<Duration>& st) const;
 };
 bool operator==(const time_zone& x, const time_zone& y) noexcept;                // C++20
 strong_ordering operator<=>(const time_zone& x, const time_zone& y) noexcept;    // C++20
@@ -906,6 +918,7 @@ constexpr chrono::year                                  operator ""y(unsigned lo
 #if !defined(_LIBCPP_HAS_NO_TIME_ZONE_DATABASE) && !defined(_LIBCPP_HAS_NO_FILESYSTEM) &&                              \
     !defined(_LIBCPP_HAS_NO_LOCALIZATION)
 #  include <__chrono/leap_second.h>
+#  include <__chrono/sys_info.h>
 #  include <__chrono/time_zone.h>
 #  include <__chrono/time_zone_link.h>
 #  include <__chrono/tzdb.h>
diff --git a/libcxx/include/libcxx.imp b/libcxx/include/libcxx.imp
index 2d6ac5f9e982aa..e3a08860c0b059 100644
--- a/libcxx/include/libcxx.imp
+++ b/libcxx/include/libcxx.imp
@@ -287,6 +287,7 @@
   { include: [ "<__chrono/parser_std_format_spec.h>", "private", "<chrono>", "public" ] },
   { include: [ "<__chrono/statically_widen.h>", "private", "<chrono>", "public" ] },
   { include: [ "<__chrono/steady_clock.h>", "private", "<chrono>", "public" ] },
+  { include: [ "<__chrono/sys_info.h>", "private", "<chrono>", "public" ] },
   { include: [ "<__chrono/system_clock.h>", "private", "<chrono>", "public" ] },
   { include: [ "<__chrono/time_point.h>", "private", "<chrono>", "public" ] },
   { include: [ "<__chrono/time_zone.h>", "private", "<chrono>", "public" ] },
diff --git a/libcxx/modules/std/chrono.inc b/libcxx/modules/std/chrono.inc
index e14228043d3b84..575e6347aecce1 100644
--- a/libcxx/modules/std/chrono.inc
+++ b/libcxx/modules/std/chrono.inc
@@ -212,10 +212,12 @@ export namespace std {
     // [time.zone.exception], exception classes
     using std::chrono::ambiguous_local_time;
     using std::chrono::nonexistent_local_time;
+#    endif // if 0
 
     // [time.zone.info], information classes
     using std::chrono::sys_info;
 
+#    if 0
     // [time.zone.timezone], class time_zone
     using std::chrono::choose;
 #    endif // if 0
diff --git a/libcxx/src/include/tzdb/types_private.h b/libcxx/src/include/tzdb/types_private.h
index 4604b9fc88114d..bdc9418a8866be 100644
--- a/libcxx/src/include/tzdb/types_private.h
+++ b/libcxx/src/include/tzdb/types_private.h
@@ -33,7 +33,17 @@ namespace chrono::__tz {
 // Sun>=8   first Sunday on or after the eighth
 // Sun<=25  last Sunday on or before the 25th
 struct __constrained_weekday {
-  /*  year_month_day operator()(year __year, month __month);*/ // needed but not implemented
+  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI year_month_day operator()(year __year, month __month) const {
+    auto __result = static_cast<sys_days>(year_month_day{__year, __month, __day});
+    weekday __wd{static_cast<sys_days>(__result)};
+
+    if (__comparison == __le)
+      __result -= __wd - __weekday;
+    else
+      __result += __weekday - __wd;
+
+    return __result;
+  }
 
   weekday __weekday;
   enum __comparison_t { __le, __ge } __comparison;
@@ -85,7 +95,8 @@ struct __continuation {
   // used.
   // If this field contains - then standard time always
   // applies. This is indicated by the monostate.
-  using __rules_t = variant<monostate, __tz::__save, string, size_t>;
+  // TODO TZDB Investigate implemention the size_t based caching.
+  using __rules_t = variant<monostate, __tz::__save, string /*, size_t*/>;
 
   __rules_t __rules;
 
diff --git a/libcxx/src/include/tzdb/tzdb_list_private.h b/libcxx/src/include/tzdb/tzdb_list_private.h
index f43d7d8ea772be..a3f56e79a51791 100644
--- a/libcxx/src/include/tzdb/tzdb_list_private.h
+++ b/libcxx/src/include/tzdb/tzdb_list_private.h
@@ -84,6 +84,13 @@ class tzdb_list::__impl {
   const_iterator cbegin() const noexcept { return begin(); }
   const_iterator cend() const noexcept { return end(); }
 
+  const __tz::__rules_storage_type& __rules() const noexcept {
+#ifndef _LIBCPP_HAS_NO_THREADS
+    shared_lock __lock{__mutex_};
+#endif
+    return __rules_.front();
+  }
+
 private:
   // Loads the tzdbs
   // pre: The caller ensures the locking, if needed, is done.
diff --git a/libcxx/src/time_zone.cpp b/libcxx/src/time_zone.cpp
index b6bf06a116f68b..84ff0e1ae49268 100644
--- a/libcxx/src/time_zone.cpp
+++ b/libcxx/src/time_zone.cpp
@@ -8,14 +8,706 @@
 
 // For information see https://libcxx.llvm.org/DesignDocs/TimeZone.html
 
+// TODO TZDB look at optimizations
+//
+// The current algorithm is correct but not efficient. For example, in a named
+// rule based continuation finding the next rule does quite a bit of work,
+// returns the next rule and "forgets" its state. This could be better.
+//
+// It would be possible to cache lookups. If a time for a zone is calculated its
+// sys_info could be kept and the next lookup could test whether the time is in
+// a "known" sys_info. The wording in the Standard hints at this slowness by
+// "suggesting" this could be implemented at the user's side.
+
+// TODO TZDB look at removing quirks
+//
+// The code has some special rules to adjust the timing at the continuation
+// switches. This works correctly, but some of the places feel odd. It would be
+// good to investigate this further and see whether all quirks are needed or
+// that there are better fixes.
+//
+// These quirks often use a 12h interval; this is the scan interval of zdump,
+// which implies there are no sys_info objects with a duration of less than 12h.
+
+#include <algorithm>
 #include <chrono>
+#include <expected>
+#include <map>
+#include <ranges>
 
 #include "include/tzdb/time_zone_private.h"
+#include "include/tzdb/tzdb_list_private.h"
+
+// TODO TZDB remove debug printing
+#ifdef PRINT
+#  include <print>
+#endif
 
 _LIBCPP_BEGIN_NAMESPACE_STD
 
+#ifdef PRINT
+template <>
+struct formatter<chrono::sys_info, char> {
+  template <class ParseContext>
+  constexpr typename ParseContext::iterator parse(ParseContext& ctx) {
+    return ctx.begin();
+  }
+
+  template <class FormatContext>
+  typename FormatContext::iterator format(const chrono::sys_info& info, FormatContext& ctx) const {
+    return std::format_to(
+        ctx.out(), "[{}, {}) {:%Q%q} {:%Q%q} {}", info.begin, info.end, info.offset, info.save, info.abbrev);
+  }
+};
+#endif
+
 namespace chrono {
 
+//===----------------------------------------------------------------------===//
+//                           Details
+//===----------------------------------------------------------------------===//
+
+struct __sys_info {
+  sys_info __info;
+  bool __can_merge; // Can the returned sys_info object be merged with
+};
+
+// Return type for helper function to get a sys_info.
+// - The expected result returns the "best" sys_info object. This object can be
+//   before the requested time. Sometimes sys_info objects from different
+//   continuations share their offset, save, and abbrev and these objects are
+//   merged to one sys_info object. The __can_merge flag determines whether the
+//   current result can be merged with the next result.
+// - The unexpected result means no sys_info object was found and the time is
+//   the time to be used for the next search iteration.
+using __sys_info_result = expected<__sys_info, sys_seconds>;
+
+template <ranges::forward_range _Range,
+          class _Type,
+          class _Proj                                                                                  = identity,
+          indirect_strict_weak_order<const _Type*, projected<ranges::iterator_t<_Range>, _Proj>> _Comp = ranges::less>
+[[nodiscard]] static ranges::borrowed_iterator_t<_Range>
+__binary_find(_Range&& __r, const _Type& __value, _Comp __comp = {}, _Proj __proj = {}) {
+  auto __end = ranges::end(__r);
+  auto __ret = ranges::lower_bound(ranges::begin(__r), __end, __value, __comp, __proj);
+  if (__ret == __end)
+    return __end;
+
+  // When the value does not match the predicate it's equal and a valid result
+  // was found.
+  return !std::invoke(__comp, __value, std::invoke(__proj, *__ret)) ? __ret : __end;
+}
+
+// Format based on https://data.iana.org/time-zones/tz-how-to.html
+//
+// 1  a time zone abbreviation that is a string of three or more characters that
+//    are either ASCII alphanumerics, “+”, or “-”
+// 2  the string “%z”, in which case the “%z” will be replaced by a numeric time
+//    zone abbreviation
+// 3  a pair of time zone abbreviations separated by a slash (‘/’), in which
+//    case the first string is the abbreviation for the standard time name and
+//    the second string is the abbreviation for the daylight saving time name
+// 4  a string containing “%s”, in which case the “%s” will be replaced by the
+//    text in the appropriate Rule’s LETTER column, and the resulting string should
+//    be a time zone abbreviation
+//
+// Accepting invalid formats that can be processed in a sensible way would better
+// serve the user than throwing an exception. So some of these rules are not
+// strictly validated.
+// 1  This is not validated. Some examples that will be accepted are, "+04:30",
+//    "Q", "42".
+// 2  How this format is formatted is not specified. In the current tzdata.zi
+//    this value is not used. This value is accepted in a part of the format. So
+//    "a%s%zb" will be considered valid.
+// 3  This is not validated, the output might be incorrect.
+//    Proper validation would make the algorithm more complex. Then the first
+//    element of the pair is used the parsing of FORMAT can stop. To do proper
+//    validation the tail should be validated.
+// 4  This value is accepted in a part of the format. So "a%s%zb" will be
+//    considered valid.
+[[nodiscard]] static string
+__format(const __tz::__continuation& __continuation, const string& __letters, seconds __save) {
+  bool __shift = false;
+  string __result;
+  for (char __c : __continuation.__format) {
+    if (__shift) {
+      switch (__c) {
+      case 's':
+        std::ranges::copy(__letters, std::back_inserter(__result));
+        break;
+
+      case 'z': {
+        chrono::hh_mm_ss __offset{__continuation.__stdoff + __save};
+        if (__offset.is_negative()) {
+          __result += '-';
+          __offset = chrono::hh_mm_ss{-(__continuation.__stdoff + __save)};
+        } else
+          __result += '+';
+
+        if (__offset.minutes() != 0min)
+          std::format_to(std::back_inserter(__result), "{:%H%M}", __offset);
+        else
+          std::format_to(std::back_inserter(__result), "{:%H}", __offset);
+      } break;
+
+      default:
+        std::__throw_runtime_error(
+            std::format("corrupt tzdb FORMAT field: invalid sequence '%{}' found, expected %s or %z", __c).c_str());
+      }
+      __shift = false;
+
+    } else if (__c == '/') {
+      if (__save != 0s)
+        __result.clear();
+      else
+        break;
+
+    } else if (__c == '%') {
+      __shift = true;
+    } else {
+      __result.push_back(__c);
+    }
+  }
+
+  if (__shift)
+    std::__throw_runtime_error("corrupt tzdb FORMAT field: input ended with the start of the escape sequence '%'");
+
+  return __result;
+}
+
+[[nodiscard]] static sys_seconds __to_sys_seconds(year_month_day __ymd, seconds __seconds) {
+  seconds __result = static_cast<sys_days>(__ymd).time_since_epoch() + __seconds;
+  return sys_seconds{__result};
+}
+
+[[nodiscard]] static seconds __at_to_sys_seconds(const __tz::__continuation& __continuation) {
+  switch (__continuation.__at.__clock) {
+  case __tz::__clock::__local:
+    return __continuation.__at.__time - __continuation.__stdoff -
+           std::visit(
+               [](const auto& __value) {
+                 using _Tp = decay_t<decltype(__value)>;
+                 if constexpr (same_as<_Tp, monostate>)
+                   return chrono::seconds{0};
+                 else if constexpr (same_as<_Tp, __tz::__save>)
+                   return chrono::duration_cast<seconds>(__value.__time);
+                 else if constexpr (same_as<_Tp, std::string>)
+                   // For a named rule based continuation the SAVE depends on the RULE
+                   // active at the end. This should be determined separately.
+                   return chrono::seconds{0};
+                 else
+                   static_assert(false);
+
+                 std::__libcpp_unreachable();
+               },
+               __continuation.__rules);
+
+  case __tz::__clock::__universal:
+    return __continuation.__at.__time;
+
+  case __tz::__clock::__standard:
+    return __continuation.__at.__time - __continuation.__stdoff;
+  }
+}
+
+[[nodiscard]] static year_month_day __to_year_month_day(year __year, month __month, __tz::__on __on) {
+  return std::visit(
+      [&](const auto& __value) {
+        using T = decay_t<decltype(__value)>;
+        if constexpr (same_as<T, chrono::day>)
+          return year_month_day{__year, __month, __value};
+        else if constexpr (same_as<T, weekday_last>)
+          return year_month_day{static_cast<sys_days>(year_month_weekday_last{__year, __month, __value})};
+        else if constexpr (same_as<T, __tz::__constrained_weekday>)
+          return __value(__year, __month);
+        else
+          static_assert(false);
+
+        std::__libcpp_unreachable();
+      },
+      __on);
+}
+
+[[nodiscard]] static sys_seconds __until_to_sys_seconds(const __tz::__continuation& __continuation) {
+  // Does UNTIL contain the magic value for the last continuation?
+  if (__continuation.__year == chrono::year::min())
+    return sys_seconds::max();
+
+  year_month_day __ymd = chrono::__to_year_month_day(__continuation.__year, __continuation.__in, __continuation.__on);
+  return chrono::__to_sys_seconds(__ymd, chrono::__at_to_sys_seconds(__continuation));
+}
+
+// Holds the UNTIL time for a continuation with a named rule.
+//
+// Unlike continuations with an fixed SAVE named rules have a variable SAVE.
+// This means when the UNTIL uses the local wall time the actual UNTIL value can
+// only be determined when the SAVE is known. This class holds that abstraction.
+class __named_rule_until {
+public:
+  explicit __named_rule_until(const __tz::__continuation& __continuation)
+      : __until_{chrono::__until_to_sys_seconds(__continuation)},
+        __needs_adjustment_{
+            // The last continuation of a ZONE has no UNTIL which basically is
+            // until the end of _local_ time. This value is expressed by
+            // sys_seconds::max(). Subtracting the SAVE leaves large value.
+            // However SAVE can be negative, which would add a value to maximum
+            // leading to undefined behaviour. In practice this often results in
+            // an overflow to a very small value.
+            __until_ != sys_seconds::max() && __continuation.__at.__clock == __tz::__clock::__local} {}
+
+  // Gives the unadjusted until value, this is useful when the SAVE is not known
+  // at all.
+  sys_seconds __until() const noexcept { return __until_; }
+
+  bool __needs_adjustment() const noexcept { return __needs_adjustment_; }
+
+  // Returns the UNTIL adjusted for SAVE.
+  sys_seconds operator()(seconds __save) const noexcept { return __until_ - __needs_adjustment_ * __save; }
+
+private:
+  sys_seconds __until_;
+  bool __needs_adjustment_;
+};
+
+[[nodiscard]] static seconds __at_to_seconds(seconds __stdoff, const __tz::__rule...
[truncated]

``````````

</details>


https://github.com/llvm/llvm-project/pull/85619


More information about the llvm-branch-commits mailing list