[PATCH v2 0/5] libstdc++: chrono tzdb correctness fixes

Tomasz Kaminski tkaminsk@redhat.com
Tue May 19 08:05:52 GMT 2026


On Tue, May 12, 2026 at 2:40 PM Tomasz Kaminski <tkaminsk@redhat.com> wrote:

>
>
> On Tue, May 12, 2026 at 12:43 PM Álvaro Begué <alvaro.begue@gmail.com>
> wrote:
>
>> Dear Tomasz,
>>
>> I'm on vacation and won't be able to work on this until next week.
>>
> Ah, I didn't mean to bring time pressure, my question was in you could
> still working on the update in general. Happy to hear that you are able to
> follow up,
> we are very grateful for your contributions.
>
Patches 1/2 from the patch series were merged. I have also posted my review
for patches 3/4/5. I suggest updating and posting patch 4 first (it has the
fewest
comments).

For current patch 3, please not hesitate to ask further clarifications if
the current
direction is not clear.


>
>> Apologies for the formatting issues. I will be more careful in the future.
>>
>> --Álvaro.
>>
>>
>>
>> On Tue, May 12, 2026, 11:14 Tomasz Kaminski <tkaminsk@redhat.com> wrote:
>>
>>> Hi Álvaro,
>>>
>>> I will handle the requested changes and then merge patches 1/2 in the
>>> series,
>>> so you do not need to update them.
>>>
>>> I have already provided feedback for patch 3 requesting major changes,
>>> could
>>> you please take a look at that, and let me know if you have any
>>> questions and will
>>> be able to update it.
>>>
>>> For the patch 4, I think it is independent, and we could merge it before
>>> update patch 3,
>>> so I would suggest reverting the order, in the new series.
>>>
>>> I will let you know when first patches will be merged, and then you can
>>> create a new
>>> series with changes. Please take extra care that tabs are preserved.
>>>
>>> Regards,
>>> Tomasz
>>>
>>> On Mon, Apr 27, 2026 at 1:43 AM Álvaro Begué <alvaro.begue@gmail.com>
>>> wrote:
>>>
>>>> This is v2 of the chrono tzdb correctness fix series.  The actual fixes
>>>> are unchanged; v2 addresses Tomasz Kamiński's review comments, which
>>>> were all stylistic.
>>>>
>>>> Changes since v1 (per Tomasz's feedback):
>>>>
>>>>   * Trim verbose "Regression test:" / past-bug-history prose from the
>>>>     test files.  Test comments now describe the behavior under test
>>>>     rather than the historical bug.
>>>>   * Trim redundant cross-reference comments and shorten the surviving
>>>>     ones.  "// PR 116110" breadcrumbs in code comments are gone -- the
>>>>     PR number is captured in the commit message and the ChangeLog entry.
>>>>   * Patch 2: rename parse_on_day_body -> parse_day_spec; use C++20
>>>>     designated initializers and if-with-initializer for `on_day` and
>>>>     `abbrev_month`; restructure the optional DAY/TIME parse so TIME is
>>>>     only attempted when DAY parsing succeeds.
>>>>
>>>> Each commit now carries a Signed-off-by trailer.
>>>>
>>>> A range-diff of v1 vs v2 is included at the end of this cover letter.
>>>>
>>>> Test plan (rerun against current master HEAD):
>>>>
>>>>   * libstdc++ stage1 build of GCC 16.0.1 trunk (b99e67e8a) succeeds.
>>>>   * All 7 chrono tests pass (Wakely's existing 116110 + 124513 plus the
>>>>     5 new ones added by this series).
>>>>   * Brute-force comparison harness (628310 samples × 447 zones) reports
>>>>     zero mismatches against libc localtime_r.
>>>>   * Abbreviation-only sweep (188493 samples) reports zero offset and
>>>>     zero abbrev diffs.
>>>>
>>>> The series builds on Jonathan Wakely's recent PR116110 / PR124513
>>>> work (commits 663e5ade1, cddf4111c, fbc5d2b1a).  Patch 3 in particular
>>>> resolves the "FIXME: PR116110" left in operator>>(istream&, ZoneInfo&)
>>>> for the named-rule wall-UNTIL case.
>>>>
>>>>   1. Fix numeric save offset on Zone lines [PR124851].
>>>>
>>>>      ZoneInfo::m_offset had inconsistent semantics: the parser path
>>>>      stored stdoff alone, but the two sys_info-taking constructors
>>>>      stored the total (stdoff + save).  Normalize m_offset to stdoff
>>>>      alone everywhere; to() adds save back when reconstructing.
>>>>
>>>>   2. Support ON-format DAY in Zone UNTIL field [PR124852].
>>>>
>>>>      The UNTIL parser only accepted a plain integer as the DAY,
>>>>      silently misparsing tzdata.zi entries like Europe/Simferopol's
>>>>      "1997 Mar lastSu 1u".  Reuse the on_day machinery and the
>>>>      parse_day_spec helper.
>>>>
>>>>   3. Resolve named-rule UNTIL save adjustment [PR116110].
>>>>
>>>>      The remaining FIXME in operator>>(istream&, ZoneInfo&) for
>>>>      wall-time UNTILs on named-rule zone lines.  At parse time the
>>>>      active rule cannot be evaluated, so the parser leaves the SAVE
>>>>      adjustment pending and a fixup pass in reload_tzdb walks every
>>>>      pending ZoneInfo and applies the adjustment using a new
>>>>      find_pre_until_rule helper with iterative-boundary cascade
>>>>      semantics.  Removes the +11h workaround from test_apia in
>>>>      116110.cc.
>>>>
>>>>   4. Cascade wall-time saves in lazy expansion seeding [PR124853].
>>>>
>>>>      Replace the per-rule isolated active-rule lookup in
>>>>      _M_get_sys_info with a chronological cascade walker that
>>>>      maintains a running save and interprets each Wall-time rule's
>>>>      at_time relative to the cascaded state (matching zic.c's
>>>>      outzone()).
>>>>
>>>>   5. Implement zic writezone merge optimization [PR124854].
>>>>
>>>>      Two related fixes: (a) always seed info.offset/save from
>>>>      find_active_rule (not just when letters is empty), so partial-
>>>>      expansion re-entry sees the right state; (b) add the writezone
>>>>      merge for backward jumps at zone-line boundaries.
>>>>
>>>> Álvaro Begué (5):
>>>>   libstdc++: Fix numeric save offset on Zone lines [PR124851]
>>>>   libstdc++: Support ON-format DAY in Zone UNTIL field [PR124852]
>>>>   libstdc++: Resolve named-rule UNTIL save adjustment [PR116110]
>>>>   libstdc++: Cascade wall-time saves in lazy expansion seeding
>>>>     [PR124853]
>>>>   libstdc++: Implement zic writezone merge optimization [PR124854]
>>>>
>>>>  libstdc++-v3/src/c++20/tzdb.cc                | 378 ++++++++++++++----
>>>>  .../testsuite/std/time/time_zone/116110.cc    |   5 +-
>>>>  .../std/time/time_zone/numeric_save.cc        |  58 +++
>>>>  .../std/time/time_zone/pr116110_named.cc      |  74 ++++
>>>>  .../std/time/time_zone/until_day_on.cc        | 168 ++++++++
>>>>  .../std/time/time_zone/wall_cascade.cc        |  70 ++++
>>>>  .../std/time/time_zone/zone_merge.cc          |  84 ++++
>>>>  7 files changed, 748 insertions(+), 89 deletions(-)
>>>>  create mode 100644
>>>> libstdc++-v3/testsuite/std/time/time_zone/numeric_save.cc
>>>>  create mode 100644
>>>> libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc
>>>>  create mode 100644
>>>> libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc
>>>>  create mode 100644
>>>> libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc
>>>>  create mode 100644
>>>> libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc
>>>>
>>>> Range-diff against v1:
>>>> 1:  7e77ac729 ! 1:  2136a9e20 libstdc++: Fix numeric save offset on
>>>> Zone lines [PR 124851]
>>>>     @@
>>>>       ## Metadata ##
>>>>     -Author: Alvaro Begue <alvaro.begue@gmail.com>
>>>>     +Author: Álvaro Begué <alvaro.begue@gmail.com>
>>>>
>>>>       ## Commit message ##
>>>>     -    libstdc++: Fix numeric save offset on Zone lines [PR 124851]
>>>>     +    libstdc++: Fix numeric save offset on Zone lines [PR124851]
>>>>
>>>>          When a Zone line specifies a numeric value as its RULES field
>>>> (the
>>>>          constant DST save value for that zone line, e.g.
>>>> Africa/Gaborone's
>>>>     @@ Commit message
>>>>                  populating sys_info::offset.
>>>>                  * testsuite/std/time/time_zone/numeric_save.cc: New
>>>> test.
>>>>
>>>>     +    Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>
>>>>     +
>>>>       ## libstdc++-v3/src/c++20/tzdb.cc ##
>>>>      @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>
>>>>     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>             }
>>>>
>>>>      -      // STDOFF: Seconds from UTC during standard time.
>>>>     -+      // STDOFF: Seconds from UTC during standard time.  Always
>>>> the
>>>>     -+      // standard offset only; the saved value (if any) is in
>>>> m_save and
>>>>     -+      // is added back when reconstructing a sys_info via to().
>>>>     ++      // STDOFF: Seconds from UTC during standard time (without
>>>> any save).
>>>>             seconds
>>>>             offset() const noexcept { return m_offset; }
>>>>
>>>>     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>
>>>>       info.end = until();
>>>>      - info.offset = offset();
>>>>     -+ // m_offset is the standard offset only; add the saved value to
>>>>     -+ // reconstruct the total offset.  See ZoneInfo's m_offset
>>>> comment.
>>>>      + info.offset = offset() + seconds(m_save);
>>>>       info.save = minutes(m_save);
>>>>       info.abbrev = format();
>>>>     @@ libstdc++-v3/testsuite/std/time/time_zone/numeric_save.cc (new)
>>>>      +// { dg-require-effective-target cxx11_abi }
>>>>      +// { dg-xfail-run-if "no weak override on AIX" { powerpc-ibm-aix*
>>>> } }
>>>>      +
>>>>     -+// Regression test: when a Zone line specifies a numeric value as
>>>> its
>>>>     -+// RULES field, that value is the constant DST save value for
>>>> that zone
>>>>     -+// line.  ZoneInfo::to() previously set sys_info::offset to the
>>>> zone
>>>>     -+// line's STDOFF only, ignoring the parsed save.  Per
>>>> [time.zone.info.sys]
>>>>     -+// sys_info::offset is the *total* UTC offset (stdoff + save), so
>>>> any
>>>>     -+// zone line with a non-zero numeric save reported the wrong
>>>> offset.
>>>>     -+//
>>>>     -+// Mirrors Africa/Gaborone's tzdata, which uses
>>>>     -+//   2 - CAT 1943 S 19 2
>>>>     -+//   2 1 CAST 1944 Mar 19 2     <-- numeric "1" RULES, save = +1h
>>>>     -+//   2 - CAT
>>>>     -+// The middle line is what triggers the bug.
>>>>     ++// When a Zone line specifies a numeric value as its RULES field,
>>>> that
>>>>     ++// value is the constant DST save value for that zone line.  Per
>>>>     ++// [time.zone.info.sys] sys_info::offset is the total UTC offset
>>>>     ++// (stdoff + save).
>>>>      +
>>>>      +#include <chrono>
>>>>      +#include <fstream>
>>>> 2:  57b11cf6e ! 2:  60c6f5eef libstdc++: Support ON-format DAY in Zone
>>>> UNTIL field [PR 124852]
>>>>     @@
>>>>       ## Metadata ##
>>>>     -Author: Alvaro Begue <alvaro.begue@gmail.com>
>>>>     +Author: Álvaro Begué <alvaro.begue@gmail.com>
>>>>
>>>>       ## Commit message ##
>>>>     -    libstdc++: Support ON-format DAY in Zone UNTIL field [PR
>>>> 124852]
>>>>     +    libstdc++: Support ON-format DAY in Zone UNTIL field [PR124852]
>>>>
>>>>          The Zone-line UNTIL parser only accepted a plain day-of-month
>>>> integer
>>>>          for the DAY field, while the tzdata.zi grammar accepts the
>>>> same ON-style
>>>>     @@ Commit message
>>>>          left d == 1 when the day token wasn't a digit, then went on to
>>>> parse the
>>>>          remainder as the TIME field.
>>>>
>>>>     -    Fix by reusing the existing parse_on_day_body() helper that
>>>> already
>>>>     +    Factor out the day-component parser from operator>>(istream&,
>>>> on_day&)
>>>>     +    as parse_day_spec(), and reuse it for the UNTIL DAY field.
>>>>  parse_day_spec
>>>>          handles all three on_day forms (DayOfMonth, LastWeekday,
>>>> LessEq /
>>>>     -    GreaterEq) for Rule lines. The MONTH-only and YEAR-only short
>>>> forms are
>>>>     -    still accepted because the DAY/TIME fields are optional and
>>>> default to
>>>>     -    day 1, time 00:00. The on_day struct's pin() method handles the
>>>>     -    year/month-relative resolution.
>>>>     +    GreaterEq).  The MONTH-only and YEAR-only short forms are
>>>> still accepted
>>>>     +    because the DAY/TIME fields are optional and default to day 1,
>>>> time 00:00.
>>>>     +    The on_day struct's pin() method handles the
>>>> year/month-relative
>>>>     +    resolution.
>>>>
>>>>          The DAY field is unambiguously distinguishable from a TIME
>>>> field that
>>>>          could otherwise follow the MONTH directly: per zic's grammar,
>>>> MONTH
>>>>     -    must be followed by DAY before any TIME is allowed. So we
>>>> always
>>>>     +    must be followed by DAY before any TIME is allowed.  So we
>>>> always
>>>>          attempt to parse a DAY if any non-whitespace remains after the
>>>> MONTH.
>>>>
>>>>          libstdc++-v3/ChangeLog:
>>>>
>>>>                  PR libstdc++/124852
>>>>     -            * src/c++20/tzdb.cc (parse_on_day_body): Factor out
>>>> the day-
>>>>     -            component parser from operator>>(istream&, on_day&) so
>>>> it can
>>>>     -            be reused.
>>>>     -            (operator>>(istream&, on_day&)): Use the new helper.
>>>>     +            * src/c++20/tzdb.cc (parse_day_spec): New function,
>>>> factored
>>>>     +            out of operator>>(istream&, on_day&).
>>>>     +            (operator>>(istream&, on_day&)): Use parse_day_spec.
>>>>                  (operator>>(istream&, ZoneInfo&)): Replace the integer
>>>> DAY
>>>>     -            parser with parse_on_day_body for the UNTIL field.
>>>>     +            parser with parse_day_spec for the UNTIL field.
>>>>                  * testsuite/std/time/time_zone/until_day_on.cc: New
>>>> test.
>>>>
>>>>     +    Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>
>>>>     +
>>>>       ## libstdc++-v3/src/c++20/tzdb.cc ##
>>>>      @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>             }
>>>>     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>      +    // Read the day-component of an on_day expression (everything
>>>> after the
>>>>      +    // month).  Three forms are accepted: a plain day-of-month
>>>> number,
>>>>      +    // "lastXxx" where Xxx is a weekday name (LastWeekday), or
>>>> "Xxx<=N" or
>>>>     -+    // "Xxx>=N" (LessEq / GreaterEq).  The caller is responsible
>>>> for setting
>>>>     -+    // `on.month` before calling.  On failure the function sets
>>>> failbit and
>>>>     -+    // leaves `on` unchanged.
>>>>     ++    // "Xxx>=N" (LessEq / GreaterEq).  On failure the function
>>>> sets failbit
>>>>     ++    // and leaves `on` unchanged.
>>>>      +    istream&
>>>>     -+    parse_on_day_body(istream& in, on_day& on)
>>>>     ++    parse_day_spec(istream& in, on_day& on)
>>>>           {
>>>>      -      on_day on{};
>>>>      -      abbrev_month m{};
>>>>     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>      +      abbrev_month m{};
>>>>      +      in >> m;
>>>>      +      on.month = static_cast<unsigned>(m.m);
>>>>     -+      if (parse_on_day_body(in, on))
>>>>     ++      if (parse_day_spec(in, on))
>>>>      + to = on;
>>>>      +      return in;
>>>>      +    }
>>>>     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>           {
>>>>             int sign = 1;
>>>>      @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>     +       in.exceptions(ios::goodbit); // Don't throw ios::failure if
>>>> YEAR absent.
>>>>             if (int y = int(year::max()); in >> y)
>>>>       {
>>>>     -  abbrev_month m{January};
>>>>     +-  abbrev_month m{January};
>>>>      -  int d = 1;
>>>>     -+  on_day on{};
>>>>     -+  on.kind = on_day::DayOfMonth;
>>>>     -+  on.month = 1;          // default January
>>>>     -+  on.day_of_month = 1;   // default day-of-month 1
>>>>     ++  on_day on{.kind = on_day::DayOfMonth, .month = 1, .day_of_month
>>>> = 1};
>>>>        at_time t{};
>>>>      -  // XXX DAY should support ON format, e.g. lastSun or Sun>=8
>>>>      -  in >> m >> d >> t;
>>>>      -  inf.m_until = sys_days(year(y)/m.m/day(d)) + seconds(t.time);
>>>>     -+  if (in >> m)
>>>>     ++  if (abbrev_month m{January}; in >> m)
>>>>      +    {
>>>>      +      on.month = static_cast<unsigned>(m.m);
>>>>     -+      // The DAY field is optional.  Per the tzdata.zi grammar,
>>>>     -+      // a MONTH followed by anything more is always followed by
>>>>     -+      // a DAY (possibly followed by a TIME); MONTH directly
>>>>     -+      // followed by TIME is not a valid form.  So if there's
>>>>     -+      // any non-whitespace before end of line, parse a DAY.
>>>>      +      if (!ws(in).eof())
>>>>     -+ parse_on_day_body(in, on);
>>>>     ++ if (parse_day_spec(in, on))
>>>>     ++  in >> t;
>>>>      +    }
>>>>     -+  in >> t;
>>>>      +  year_month_day ymd = on.pin(year(y));
>>>>      +  inf.m_until = sys_days(ymd) + seconds(t.time);
>>>>        if (t.indicator != at_time::Universal)
>>>>     @@ libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc (new)
>>>>      +// { dg-require-effective-target cxx11_abi }
>>>>      +// { dg-xfail-run-if "no weak override on AIX" { powerpc-ibm-aix*
>>>> } }
>>>>      +
>>>>     -+// Regression test: the DAY portion of a Zone line's UNTIL field
>>>> accepts
>>>>     -+// not only a numeric day-of-month but also "lastXxx" (last
>>>> weekday in
>>>>     -+// the month) and "Xxx<=N" / "Xxx>=N" forms, just like the ON
>>>> field of
>>>>     -+// a Rule line.  Previously the UNTIL parser used `int d; in >>
>>>> d;` which
>>>>     -+// silently failed on the non-numeric forms and defaulted d to 1,
>>>> placing
>>>>     -+// any zone-line transition with such an UNTIL on the wrong
>>>> calendar day.
>>>>     ++// The DAY portion of a Zone line's UNTIL field accepts not only a
>>>>     ++// numeric day-of-month but also "lastXxx" (last weekday in the
>>>> month)
>>>>     ++// and "Xxx<=N" / "Xxx>=N" forms, just like the ON field of a
>>>> Rule line.
>>>>      +//
>>>>      +// Real-world example: Europe/Simferopol has
>>>>      +//   3 - MSK 1997 Mar lastSu 1u
>>>>     -+// which must place the boundary on 1997-03-30 (the last Sunday
>>>> of March),
>>>>     -+// not on 1997-03-01.
>>>>     ++// which places the boundary on 1997-03-30 (the last Sunday of
>>>> March).
>>>>      +
>>>>      +#include <chrono>
>>>>      +#include <fstream>
>>>>     @@ libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc (new)
>>>>      +  auto at = tz->get_info(boundary);
>>>>      +  VERIFY( at.abbrev == "X" );
>>>>      +
>>>>     -+  // Critical regression check: a sample 15 days BEFORE the
>>>> boundary must
>>>>     -+  // still be in the MSK line.  The unfixed parser placed the
>>>> boundary on
>>>>     -+  // March 1 because "lastSu" defaulted to day 1, and a March-15
>>>> query
>>>>     -+  // landed in the X line instead.
>>>>     ++  // Check that the lastSu day is parsed correctly, and not
>>>> defaulted
>>>>     ++  // to the 1st: a March 15 query must still be in the MSK line.
>>>>      +  auto mid_march = tz->get_info(sys_days{1997y/March/15});
>>>>      +  VERIFY( mid_march.abbrev == "MSK" );
>>>>      +  VERIFY( mid_march.offset == 3h );
>>>>     @@ libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc (new)
>>>>      +  auto at = tz->get_info(boundary);
>>>>      +  VERIFY( at.abbrev == "B" );
>>>>      +
>>>>     -+  // A June-1 query must still be in the A line (the unfixed
>>>> parser
>>>>     -+  // placed the boundary on June 1).
>>>>     ++  // Check that Sun>=8 is parsed correctly, and not defaulted to
>>>> the 1st.
>>>>      +  auto early = tz->get_info(sys_days{1990y/June/1});
>>>>      +  VERIFY( early.abbrev == "A" );
>>>>      +}
>>>>     @@ libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc (new)
>>>>      +{
>>>>      +  using namespace std::chrono;
>>>>      +
>>>>     -+  // Sanity check: a UNTIL with only a year (no MONTH, no DAY, no
>>>> TIME)
>>>>     -+  // must continue to default to January 1 00:00.
>>>>     ++  // MONTH, DAY and TIME default to January 1st 00:00 if not
>>>> specified.
>>>>      +  std::ofstream("tzdata.zi") << R"(# version test_year_only
>>>>      +Z Test/YearOnly 0 - A 1990
>>>>      +                0 - B
>>>>     @@ libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc (new)
>>>>      +{
>>>>      +  using namespace std::chrono;
>>>>      +
>>>>     -+  // Sanity check: UNTIL with only YEAR and MONTH (no DAY, no
>>>> TIME)
>>>>     -+  // must default DAY to 1 and TIME to 00:00.
>>>>     ++  // DAY and TIME default to the 1st 00:00 if not specified.
>>>>      +  std::ofstream("tzdata.zi") << R"(# version test_year_month_only
>>>>      +Z Test/YearMonth 0 - A 1990 Jul
>>>>      +                 0 - B
>>>> 3:  38e5ea518 ! 3:  5b20eac08 libstdc++: Resolve named-rule UNTIL save
>>>> adjustment [PR116110]
>>>>     @@
>>>>       ## Metadata ##
>>>>     -Author: Alvaro Begue <alvaro.begue@gmail.com>
>>>>     +Author: Álvaro Begué <alvaro.begue@gmail.com>
>>>>
>>>>       ## Commit message ##
>>>>          libstdc++: Resolve named-rule UNTIL save adjustment [PR116110]
>>>>     @@ Commit message
>>>>                  (operator>>(istream&, ZoneInfo&)): Set
>>>> m_until_save_pending
>>>>                  when the wall UNTIL on a named-rule line cannot have
>>>> its save
>>>>                  subtracted at parse time.  Replaces the FIXME.
>>>>     -            (time_zone::_Impl::_M_get_sys_info): Change the
>>>> seeding active-
>>>>     +            (time_zone::_M_get_sys_info): Change the seeding
>>>> active-
>>>>                  rule lookup to use t = info.begin + 1s, so a rule
>>>> firing at
>>>>                  exactly info.begin is included.
>>>>                  (reload_tzdb): After sorting node->rules, run a fixup
>>>> pass over
>>>>     @@ Commit message
>>>>                  canonical +10h boundary is now produced.
>>>>                  * testsuite/std/time/time_zone/pr116110_named.cc: New
>>>> test.
>>>>
>>>>     +    Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>
>>>>     +
>>>>       ## libstdc++-v3/src/c++20/tzdb.cc ##
>>>>      @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>             sys_seconds
>>>>             until() const noexcept { return m_until; }
>>>>
>>>>     -+      // PR 116110: When the parser sees a wall-time UNTIL on a
>>>> named-Rule
>>>>     -+      // line, the SAVE component of the conversion can't be
>>>> applied yet
>>>>     -+      // because the active Rule depends on the cumulative state
>>>> of the
>>>>     -+      // (not-yet-loaded) rule set.  The parser leaves m_until in
>>>> the
>>>>     -+      // "STDOFF subtracted but SAVE not yet subtracted"
>>>> intermediate
>>>>     -+      // state and sets this bit; reload_tzdb runs a fixup pass
>>>> after all
>>>>     -+      // Rule records are loaded to subtract the SAVE.
>>>>     ++      // True if this is a named-rule zone line whose wall-time
>>>> UNTIL still
>>>>     ++      // needs its SAVE adjustment applied.  See reload_tzdb for
>>>> the fixup.
>>>>      +      bool
>>>>      +      until_save_pending() const noexcept { return
>>>> m_until_save_pending; }
>>>>      +
>>>>     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>      +      void
>>>>      +      clear_until_save_pending() noexcept { m_until_save_pending
>>>> = 0; }
>>>>      +
>>>>     -+      // Subtract `s` from m_until.  Used by the PR 116110 fixup
>>>> pass.
>>>>      +      void
>>>>      +      adjust_until(seconds s) noexcept { m_until -= s; }
>>>>      +
>>>>     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>      -      uint_least16_t m_pos : 15 = 0; // offset of format() in
>>>> m_buf
>>>>      +      uint_least16_t m_pos : 14 = 0; // offset of format() in
>>>> m_buf
>>>>             uint_least16_t m_expanded : 1 = 0;
>>>>     -+      uint_least16_t m_until_save_pending : 1 = 0; // PR 116110,
>>>> see above
>>>>     ++      uint_least16_t m_until_save_pending : 1 = 0;
>>>>             duration<int_least16_t, ratio<60>> m_save{};
>>>>             sec32_t m_offset{};
>>>>             sys_seconds m_until{};
>>>>     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>      +
>>>>      +    // Find the Rule whose save value is in force at the
>>>> wall-time UNTIL
>>>>      +    // of a Zone line, given that `wall_minus_stdoff` is the
>>>> line's UNTIL
>>>>     -+    // expressed in the "save=0" frame (i.e. the parsed wall
>>>> UNTIL with
>>>>     -+    // the line's STDOFF subtracted) and `stdoff` is the line's
>>>> standard
>>>>     -+    // offset.
>>>>     -+    //
>>>>     -+    // The function walks all (rule, year) pairs in chronological
>>>> order,
>>>>     -+    // maintaining a running save value.  Wall-time rules have
>>>> their TIME
>>>>     -+    // field interpreted relative to the running save (since
>>>> "wall" means
>>>>     -+    // local civil time = stdoff + save), so a rule's effective
>>>> UT firing
>>>>     -+    // time depends on which prior rule was last in force.  This
>>>> matches
>>>>     -+    // zic.c's outzone() logic and is required for zone lines
>>>> whose
>>>>     -+    // rule set has rules whose at_time depends on cascading
>>>> saves.
>>>>     ++    // with STDOFF subtracted and `stdoff` is the line's standard
>>>> offset.
>>>>      +    //
>>>>     -+    // The comparison `fire < boundary` shrinks `boundary` as the
>>>> running
>>>>     -+    // save cascades up: a rule that fires AT the boundary (with
>>>> the
>>>>     -+    // cascaded save applied) is treated as belonging to the next
>>>> zone
>>>>     -+    // line, and its save is excluded from the running total.
>>>>     ++    // Walks (rule, year) pairs chronologically, maintaining a
>>>> running
>>>>     ++    // save value used to interpret subsequent Wall-indicator
>>>> rules.
>>>>     ++    // The boundary `wall_minus_stdoff - running_save` shrinks as
>>>> save
>>>>     ++    // accumulates, so a rule firing AT the boundary is treated as
>>>>     ++    // belonging to the next zone line.
>>>>      +    //
>>>>     -+    // Canonical case: Africa/Algiers 1977-10-21.  The "Algeria
>>>> 1977
>>>>     -+    // Oct 21" rule (save=0) fires at the same instant as the
>>>> wall UNTIL
>>>>     -+    // of line 6 ("0 d WE%sT 1977 O 21").  The pre-rule save (1h,
>>>> from
>>>>     -+    // the May 6 rule) is what determines the boundary's UT
>>>> placement,
>>>>     -+    // not the Oct 21 rule's save=0.
>>>>     -+    //
>>>>     -+    // The calendar window is extended by one year on each side,
>>>> to
>>>>     -+    // catch rules whose wall-time at_time falls in early January
>>>> or
>>>>     -+    // late December but whose UT firing crosses a year boundary
>>>> due
>>>>     -+    // to a large stdoff or save.
>>>>     ++    // The calendar window extends by one year on each side to
>>>> catch
>>>>     ++    // rules whose wall at_time crosses a year boundary in UT due
>>>> to a
>>>>     ++    // large stdoff or save.
>>>>      +    template<typename _RuleRange>
>>>>      +      const Rule*
>>>>      +      find_pre_until_rule(const _RuleRange& rules,
>>>>     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>           if (letters.empty())
>>>>             {
>>>>      - sys_seconds t = info.begin - seconds(1);
>>>>     -+ // We want the rule whose effect is in force at info.begin --
>>>>     -+ // including a rule that fires at exactly info.begin (its effect
>>>>     -+ // has just begun and is active for the first sys_info we are
>>>>     -+ // about to generate).  The search below uses a strict
>>>>     -+ // `rule_start < t` comparison, so pass info.begin + 1s to make
>>>>     -+ // the half-open lookup (..., info.begin] inclusive of the
>>>>     -+ // boundary instant.  This is what makes named-rule zone lines
>>>>     -+ // like Africa/Algiers (PR 116110) seed with the correct save:
>>>>     -+ // the Oct-21 rule fires at Oct 20 23:00 UTC in the new line's
>>>>     -+ // frame, which is exactly the new line's begin.
>>>>     ++ // info.begin + 1s makes the strict `rule_start < t` search
>>>>     ++ // inclusive of a rule that fires at exactly info.begin.
>>>>      + sys_seconds t = info.begin + seconds(1);
>>>>       const year_month_day date(chrono::floor<days>(t));
>>>>
>>>>     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>           ranges::sort(node->db.links, {}, &time_zone_link::name);
>>>>           ranges::stable_sort(node->rules, {}, &Rule::name);
>>>>
>>>>     -+    // PR 116110 fixup pass.  For every Zone line whose UNTIL was
>>>> a wall-
>>>>     -+    // time expression on a named-rule line, the parser deferred
>>>> the SAVE
>>>>     -+    // adjustment because the active rule wasn't yet
>>>> identifiable.  Now
>>>>     -+    // that all Rule records are loaded and indexed, walk every
>>>> pending
>>>>     -+    // ZoneInfo, find the rule whose effect was in force just
>>>> before the
>>>>     -+    // wall UNTIL, and subtract that rule's save from m_until.
>>>>     -+    //
>>>>     -+    // "Just before the wall UNTIL" matches zic.c's
>>>> interpretation: the
>>>>     -+    // wall time of UNTIL is read in the frame in effect
>>>> immediately
>>>>     -+    // prior to the boundary, so the SAVE used is the value that
>>>> the
>>>>     -+    // most recent rule strictly before the UNTIL set.  A rule
>>>> firing at
>>>>     -+    // exactly the UNTIL is not yet in force at the moment the
>>>> wall
>>>>     -+    // time is being interpreted -- its effect belongs to the
>>>> next zone
>>>>     -+    // line, not this one.  find_pre_until_rule's
>>>> iterative-boundary
>>>>     -+    // walker implements this semantics.
>>>>     ++    // For every Zone line whose UNTIL was a wall-time expression
>>>> on a
>>>>     ++    // named-rule line, the parser deferred the SAVE adjustment
>>>> because
>>>>     ++    // the active rule was not yet identifiable.  Now that all
>>>> Rule
>>>>     ++    // records are loaded and indexed, find the rule active just
>>>> before
>>>>     ++    // the wall UNTIL and subtract its save from m_until.
>>>>      +    for (const auto& tz : node->db.zones)
>>>>      +      {
>>>>      + auto& infos = tz._M_impl->infos;
>>>>     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>          inf.m_until -= inf.m_save;
>>>>      -  // else Named Rule, SAVE is unknown. FIXME: PR 116110
>>>>      +  else
>>>>     -+    // Named Rule: SAVE depends on which rule of the set
>>>>     -+    // was active at this instant, which can only be
>>>>     -+    // determined once all Rule records are loaded.  Mark
>>>>     -+    // the ZoneInfo so that the fixup pass in reload_tzdb
>>>>     -+    // applies the deferred adjustment.  PR 116110.
>>>>     ++    // Named Rule: defer SAVE adjustment until reload_tzdb
>>>>     ++    // has loaded all Rule records.
>>>>      +    inf.set_until_save_pending();
>>>>       }
>>>>          }
>>>>     @@ libstdc++-v3/testsuite/std/time/time_zone/116110.cc: test_apia()
>>>>
>>>>      -  // FIXME: this should be + 10h but we do not account for DST
>>>> yet, so + 11h.
>>>>      -  sys_seconds ut(t.time_since_epoch() + 11h );
>>>>     -+  // The wall UNTIL "2011 Dec 29 24" is interpreted in the prior
>>>> offset
>>>>     -+  // (-11h + save 1h = -10h), so the boundary is at local_days
>>>> +24h +10h
>>>>     -+  // (the FIXME for the +11h compensation has been resolved by the
>>>>     -+  // fix for the named-rule UNTIL case in PR 116110).
>>>>     ++  // The wall UNTIL is interpreted in the prior offset (-11h +
>>>> save 1h
>>>>     ++  // = -10h), so the boundary is at local_days + 24h + 10h.
>>>>      +  sys_seconds ut(t.time_since_epoch() + 10h );
>>>>         sys_info info;
>>>>         info = tz->get_info(ut - 1s);
>>>>     @@ libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc (new)
>>>>      +// { dg-require-effective-target cxx11_abi }
>>>>      +// { dg-xfail-run-if "no weak override on AIX" { powerpc-ibm-aix*
>>>> } }
>>>>      +
>>>>     -+// Regression test for PR 116110, named-rule case.
>>>>     ++// Africa/Algiers 1977-10-21: a Zone line whose RULES references a
>>>>     ++// named Rule and whose UNTIL is a wall-time expression.  The wall
>>>>     ++// UNTIL is interpreted using the SAVE value in force just before
>>>> the
>>>>     ++// boundary (the May-6 rule's save=1, not the Oct-21 rule's save=0
>>>>     ++// even though the Oct-21 rule fires at the same wall instant).
>>>>      +//
>>>>     -+// A Zone line whose RULES references a named Rule and whose
>>>> UNTIL is a
>>>>     -+// wall-time expression cannot have its UNTIL converted to a true
>>>> UTC
>>>>     -+// instant at parse time, because the SAVE value at the UNTIL
>>>> depends on
>>>>     -+// which rule of the named set was last in force just before that
>>>> wall
>>>>     -+// time -- and that's not known until all Rule records have been
>>>> loaded
>>>>     -+// and indexed.  The "Partial fix for interpretation of non-UTC
>>>> UNTIL
>>>>     -+// times" commit handled the simpler cases (UNTIL with `s`
>>>> indicator,
>>>>     -+// `u` indicator, or wall + non-named RULES) but explicitly left a
>>>>     -+// FIXME for the named-rule case.  This test exercises that case
>>>> via
>>>>     -+// the canonical Africa/Algiers boundary at 1977-10-21.
>>>>     -+//
>>>>     -+// In this synthetic data:
>>>>      +//   Rule d 1977 May  6 0:00 wall  save=1
>>>>      +//   Rule d 1977 Oct 21 0:00 wall  save=0
>>>>      +//   Z A    0 d WE%sT 1977 O 21
>>>>      +//          1 d CE%sT
>>>>     -+//
>>>>     -+// The first Zone line has STDOFF=0 and uses rule set d.  The May
>>>> rule
>>>>     -+// sets save=1 (WEST, total +1).  zic.c interprets the wall UNTIL
>>>>     -+// "1977 O 21" using the SAVE value in force just before the
>>>> boundary
>>>>     -+// (i.e. May's save=1, since the Oct-21 rule in this line's frame
>>>> fires
>>>>     -+// at exactly the boundary, after the wall time has been read).
>>>> So:
>>>>     -+//   wall(0:00) - stdoff(0) - save_just_before(1)  =  Oct 20
>>>> 23:00 UTC
>>>>     -+// is the correct UTC instant of the line's end.
>>>>     -+//
>>>>     -+// The second Zone line has STDOFF=1 and uses the same rule set
>>>> d.  In
>>>>     -+// its own frame, the Oct-21 rule fires at exactly its starting
>>>> instant
>>>>     -+// (Oct 20 23:00 UTC = wall(0:00) - stdoff(1)), setting save=0.
>>>> So at
>>>>     -+// Oct 20 23:00 UTC the new line begins with stdoff=1, save=0,
>>>> abbrev
>>>>     -+// "CET", total offset +1.  Both sides of the boundary have total
>>>> +1;
>>>>     -+// only the (stdoff, save) split changes.  This is the merge that
>>>>     -+// zdump shows for real Africa/Algiers in October 1977.
>>>>      +
>>>>      +#include <chrono>
>>>>      +#include <fstream>
>>>>     @@ libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc (new)
>>>>      +  VERIFY( pre.save == 1h );
>>>>      +  VERIFY( pre.abbrev == "WEST" );
>>>>      +
>>>>     -+  // The "active rule just before the wall UNTIL" is May-6
>>>> (save=1),
>>>>     -+  // so the wall UNTIL "1977 O 21" gets adjusted by
>>>> stdoff(0)+save(1).
>>>>     -+  // Without the fix, master leaves the line's m_until 1 hour too
>>>> late
>>>>     -+  // and the query just before Oct 21 00:00 UTC is in the wrong
>>>> frame.
>>>>     -+  // With the fix, queries strictly before the boundary stay in
>>>> the
>>>>     -+  // first line (WEST) and queries at/after the boundary are in
>>>> the
>>>>     -+  // second line (CET).
>>>>     ++  // The boundary is Oct 20 23:00 UTC (= wall 00:00 - stdoff(0) -
>>>> save(1)).
>>>>     ++  // At and after the boundary we are in the second line (CET).
>>>>      +  auto at = tz->get_info(sys_days{1977y/October/20} + 23h);
>>>>      +  VERIFY( at.offset == 1h );    // stdoff 1 + save 0 (CET, second
>>>> line)
>>>>      +  VERIFY( at.save == 0min );
>>>>     @@ libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc (new)
>>>>      +  VERIFY( after.save == 0min );
>>>>      +  VERIFY( after.abbrev == "CET" );
>>>>      +
>>>>     -+  // And a regression check that the boundary really moved: a
>>>> query at
>>>>     -+  // 1977-10-20 23:30 UTC must be in the SECOND line.  Without the
>>>>     -+  // fix, master's m_until for the first line is 1977-10-21 00:00
>>>> UTC,
>>>>     -+  // and this query lands in the WET stretch produced by the first
>>>>     -+  // line's expansion of the Oct-21 rule.
>>>>     ++  // A query inside the [Oct 20 23:00, Oct 21 00:00] UTC window
>>>> must be
>>>>     ++  // in the second line, not in a leftover stretch from the first
>>>> line.
>>>>      +  auto window = tz->get_info(sys_days{1977y/October/20} + 23h +
>>>> 30min);
>>>>      +  VERIFY( window.offset == 1h );
>>>>      +  VERIFY( window.abbrev == "CET" );
>>>> 4:  d94019806 ! 4:  9a01b2555 libstdc++: Cascade wall-time saves in
>>>> lazy expansion seeding [PR 124853]
>>>>     @@
>>>>       ## Metadata ##
>>>>     -Author: Alvaro Begue <alvaro.begue@gmail.com>
>>>>     +Author: Álvaro Begué <alvaro.begue@gmail.com>
>>>>
>>>>       ## Commit message ##
>>>>     -    libstdc++: Cascade wall-time saves in lazy expansion seeding
>>>> [PR 124853]
>>>>     +    libstdc++: Cascade wall-time saves in lazy expansion seeding
>>>> [PR124853]
>>>>
>>>>          When _M_get_sys_info seeds a Zone line by looking up the
>>>> active rule
>>>>          just before info.begin, the previous code interpreted each
>>>> rule in
>>>>     @@ Commit message
>>>>          libstdc++-v3/ChangeLog:
>>>>
>>>>                  PR libstdc++/124853
>>>>     -            * src/c++20/tzdb.cc
>>>> (time_zone::_Impl::_M_get_sys_info):
>>>>     +            * src/c++20/tzdb.cc (time_zone::_M_get_sys_info):
>>>>                  Replace the per-rule isolated active-rule search with a
>>>>                  chronological cascade walker that maintains a running
>>>> save
>>>>                  and interprets Wall-time rules' at_time relative to it.
>>>>     @@ Commit message
>>>>                  "earliest STD rule" fallback to its own branch.
>>>>                  * testsuite/std/time/time_zone/wall_cascade.cc: New
>>>> test.
>>>>
>>>>     +    Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>
>>>>     +
>>>>       ## libstdc++-v3/src/c++20/tzdb.cc ##
>>>>      @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>       #endif
>>>>     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>
>>>>      -    // Find the Rule whose save value is in force at the
>>>> wall-time UNTIL
>>>>      -    // of a Zone line, given that `wall_minus_stdoff` is the
>>>> line's UNTIL
>>>>     --    // expressed in the "save=0" frame (i.e. the parsed wall
>>>> UNTIL with
>>>>     --    // the line's STDOFF subtracted) and `stdoff` is the line's
>>>> standard
>>>>     --    // offset.
>>>>     -+    // Find the Rule in `rules` whose effect was last in force at
>>>> time
>>>>     -+    // `t`, given that `stdoff` is the standard offset of the
>>>> enclosing
>>>>     -+    // zone line.  Returns nullptr if no rule fired strictly
>>>> before t.
>>>>     +-    // with STDOFF subtracted and `stdoff` is the line's standard
>>>> offset.
>>>>     ++    // Find the Rule whose effect was last in force at time `t`,
>>>> given
>>>>     ++    // that `stdoff` is the standard offset of the enclosing zone
>>>> line.
>>>>     ++    // Returns nullptr if no rule fired strictly before t.
>>>>           //
>>>>     -     // The function walks all (rule, year) pairs in chronological
>>>> order,
>>>>     -     // maintaining a running save value.  Wall-time rules have
>>>> their TIME
>>>>     -@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>     -     // zic.c's outzone() logic and is required for zone lines
>>>> whose
>>>>     -     // rule set has rules whose at_time depends on cascading
>>>> saves.
>>>>     +     // Walks (rule, year) pairs chronologically, maintaining a
>>>> running
>>>>     +     // save value used to interpret subsequent Wall-indicator
>>>> rules.
>>>>     +-    // The boundary `wall_minus_stdoff - running_save` shrinks as
>>>> save
>>>>     +-    // accumulates, so a rule firing AT the boundary is treated as
>>>>     +-    // belonging to the next zone line.
>>>>           //
>>>>     -+    // Canonical case: Europe/Paris around 1945, where the France
>>>> rules
>>>>     -+    //   1945 Apr 2  02:00 wall  save=2  M
>>>>     -+    //   1945 Sep 16 03:00 wall  save=0  -
>>>>     -+    // chain together: in the (stdoff=1, save=2) frame the
>>>> September
>>>>     -+    // rule fires at Sep 16 00:00 UT, not Sep 16 02:00 UT.
>>>>     -+    //
>>>>     -+    // The calendar window is extended by one year on each side,
>>>> to
>>>>     -+    // catch rules whose wall-time at_time falls in early January
>>>> or
>>>>     -+    // late December but whose UT firing crosses a year boundary
>>>> due
>>>>     -+    // to a large stdoff or save (Pacific/Auckland's "1946 Ja 1"
>>>> rule
>>>>     -+    // in stdoff=12h fires at 1945-12-31 11:30 UT).
>>>>     +     // The calendar window extends by one year on each side to
>>>> catch
>>>>     +     // rules whose wall at_time crosses a year boundary in UT due
>>>> to a
>>>>     +     // large stdoff or save.
>>>>      +    template<typename _RuleRange>
>>>>      +      const Rule*
>>>>      +      find_active_rule(const _RuleRange& rules, sys_seconds t,
>>>> seconds stdoff)
>>>>     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>      +
>>>>      +    // Find the Rule whose save value is in force at the
>>>> wall-time UNTIL
>>>>      +    // of a Zone line, given that `wall_minus_stdoff` is the
>>>> line's UNTIL
>>>>     -+    // expressed in the "save=0" frame (i.e. the parsed wall
>>>> UNTIL with
>>>>     -+    // the line's STDOFF subtracted) and `stdoff` is the line's
>>>> standard
>>>>     -+    // offset.
>>>>     ++    // with STDOFF subtracted and `stdoff` is the line's standard
>>>> offset.
>>>>      +    //
>>>>     -+    // Like find_active_rule, walks (rule, year) pairs
>>>> chronologically
>>>>     -+    // with a running save value, but interprets `t` differently.
>>>>     -+    //
>>>>     -     // The comparison `fire < boundary` shrinks `boundary` as the
>>>> running
>>>>     -     // save cascades up: a rule that fires AT the boundary (with
>>>> the
>>>>     -     // cascaded save applied) is treated as belonging to the next
>>>> zone
>>>>     -@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>     -     // of line 6 ("0 d WE%sT 1977 O 21").  The pre-rule save (1h,
>>>> from
>>>>     -     // the May 6 rule) is what determines the boundary's UT
>>>> placement,
>>>>     -     // not the Oct 21 rule's save=0.
>>>>     --    //
>>>>     --    // The calendar window is extended by one year on each side,
>>>> to
>>>>     --    // catch rules whose wall-time at_time falls in early January
>>>> or
>>>>     --    // late December but whose UT firing crosses a year boundary
>>>> due
>>>>     --    // to a large stdoff or save.
>>>>     ++    // Like find_active_rule but `wall_minus_stdoff -
>>>> running_save`
>>>>     ++    // shrinks as save accumulates, so a rule firing AT the
>>>> boundary is
>>>>     ++    // treated as belonging to the next zone line.
>>>>           template<typename _RuleRange>
>>>>             const Rule*
>>>>             find_pre_until_rule(const _RuleRange& rules,
>>>>      @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>     - // the Oct-21 rule fires at Oct 20 23:00 UTC in the new line's
>>>>     - // frame, which is exactly the new line's begin.
>>>>     + // info.begin + 1s makes the strict `rule_start < t` search
>>>>     + // inclusive of a rule that fires at exactly info.begin.
>>>>       sys_seconds t = info.begin + seconds(1);
>>>>      - const year_month_day date(chrono::floor<days>(t));
>>>>
>>>>     @@ libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc (new)
>>>>      +// { dg-require-effective-target cxx11_abi }
>>>>      +// { dg-xfail-run-if "no weak override on AIX" { powerpc-ibm-aix*
>>>> } }
>>>>      +
>>>>     -+// Regression test: when lazy expansion seeds a Zone line whose
>>>> rule set
>>>>     -+// has wall-time rules whose effective firing time depends on a
>>>> prior
>>>>     -+// rule's save (cascading wall-time interpretation), the seeding
>>>> code
>>>>     -+// must walk the rules chronologically with a running save value,
>>>> not
>>>>     -+// look at each rule in isolation against a fixed `save=0` frame.
>>>>     -+//
>>>>     -+// Mirrors the Europe/Paris 1945 case.  France's rules
>>>>     ++// Wall-time rules in the same rule set whose effective firing
>>>> time
>>>>     ++// depends on a prior rule's save (Europe/Paris 1945):
>>>>      +//   1945 Apr 2  02:00 wall  save=2  M
>>>>      +//   1945 Sep 16 03:00 wall  save=0  -
>>>>     -+// chain together: in the (stdoff=1, save=2) frame the September
>>>> rule
>>>>     -+// fires at Sep 16 00:00 UT, not Sep 16 02:00 UT.
>>>>     -+//
>>>>     -+// Construct a synthetic two-line zone whose second line begins
>>>> between
>>>>     -+// those two interpretations of the September rule, so the
>>>> seeding has
>>>>     -+// to choose: with the cascade, the September rule has already
>>>> fired at
>>>>     -+// info.begin and the new line seeds with save=0; without the
>>>> cascade,
>>>>     -+// the September rule appears not to have fired yet and the new
>>>> line
>>>>     -+// would (incorrectly) seed with save=2 from the April rule.
>>>>     ++// In the (stdoff=1, save=2) frame the September rule fires at
>>>>     ++// Sep 16 00:00 UT, not Sep 16 02:00 UT.
>>>>      +
>>>>      +#include <chrono>
>>>>      +#include <fstream>
>>>>     @@ libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc (new)
>>>>      +  // Line 1 ends at "1945 Sep 16 1u" (Universal time, no save
>>>> shenanigans),
>>>>      +  // so info.begin for line 2 is exactly 1945-09-16 01:00 UT.
>>>>      +  //
>>>>     -+  // Cascade seeding: in line 2's frame (stdoff=1):
>>>>     -+  //   * Apr 2 fires at Apr 2 01:00 UT (running save 0 → 2)
>>>>     -+  //   * Sep 16 fires at Sep 16 00:00 UT (running save 2 → 0)
>>>>     -+  // Both fire before info.begin (Sep 16 01:00 UT), so the active
>>>> rule
>>>>     -+  // at info.begin is the Sep 16 one, save=0 → CET, total offset
>>>> 1h.
>>>>     -+  //
>>>>     -+  // Without cascade: Sep 16 is computed as 03:00 - stdoff(1) =
>>>> 02:00 UT,
>>>>     -+  // which is *after* info.begin, so the seeding falls back to
>>>> the April
>>>>     -+  // rule's save=2 → CEMT, total offset 3h.  That is the bug.
>>>>     ++  // Two-line zone whose second line begins at 1945 Sep 16 01:00
>>>> UT,
>>>>     ++  // between the cascaded firing time (Sep 16 00:00 UT) and the
>>>>     ++  // non-cascaded firing time (Sep 16 02:00 UT) of the September
>>>> rule.
>>>>     ++  // The seeding must pick the September rule (save=0, CET) at
>>>> info.begin.
>>>>      +  std::ofstream("tzdata.zi") << R"(# version test_wall_cascade
>>>>      +R Fr 1945 o - Apr 2  2 2 M
>>>>      +R Fr 1945 o - Sep 16 3 0 -
>>>> 5:  ab7938201 ! 5:  1c717d45a libstdc++: Implement zic writezone merge
>>>> optimization [PR 124854]
>>>>     @@
>>>>       ## Metadata ##
>>>>     -Author: Alvaro Begue <alvaro.begue@gmail.com>
>>>>     +Author: Álvaro Begué <alvaro.begue@gmail.com>
>>>>
>>>>       ## Commit message ##
>>>>     -    libstdc++: Implement zic writezone merge optimization [PR
>>>> 124854]
>>>>     +    libstdc++: Implement zic writezone merge optimization
>>>> [PR124854]
>>>>
>>>>          Two distinct correctness fixes that together let lazy
>>>> expansion match
>>>>          zic.c's writezone output for zones with rule firings near
>>>> zone-line
>>>>     @@ Commit message
>>>>          libstdc++-v3/ChangeLog:
>>>>
>>>>                  PR libstdc++/124854
>>>>     -            * src/c++20/tzdb.cc
>>>> (time_zone::_Impl::_M_get_sys_info):
>>>>     +            * src/c++20/tzdb.cc (time_zone::_M_get_sys_info):
>>>>                  Always run find_active_rule to seed info.offset and
>>>> info.save,
>>>>                  regardless of whether letters was already populated
>>>> from
>>>>                  i[-1].next_letters().  Add a writezone merge
>>>> optimization
>>>>     @@ Commit message
>>>>                  re-entry.
>>>>                  * testsuite/std/time/time_zone/zone_merge.cc: New test.
>>>>
>>>>     +    Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>
>>>>     +
>>>>       ## libstdc++-v3/src/c++20/tzdb.cc ##
>>>>      @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>           if (i != infos.begin() && i[-1].expanded())
>>>>     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>
>>>>      -    if (letters.empty())
>>>>      -      {
>>>>     -- // We want the rule whose effect is in force at info.begin --
>>>>     -- // including a rule that fires at exactly info.begin (its effect
>>>>     -- // has just begun and is active for the first sys_info we are
>>>>     -- // about to generate).  The search below uses a strict
>>>>     -- // `rule_start < t` comparison, so pass info.begin + 1s to make
>>>>     -- // the half-open lookup (..., info.begin] inclusive of the
>>>>     -- // boundary instant.  This is what makes named-rule zone lines
>>>>     -- // like Africa/Algiers (PR 116110) seed with the correct save:
>>>>     -- // the Oct-21 rule fires at Oct 20 23:00 UTC in the new line's
>>>>     -- // frame, which is exactly the new line's begin.
>>>>     +- // info.begin + 1s makes the strict `rule_start < t` search
>>>>     +- // inclusive of a rule that fires at exactly info.begin.
>>>>      - sys_seconds t = info.begin + seconds(1);
>>>>      -
>>>>      - // Try to find a Rule active before this time, to get initial
>>>>     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>      -  {
>>>>      -    info.offset = ri.offset() + active_rule->save;
>>>>      -    info.save = chrono::duration_cast<minutes>(active_rule->save);
>>>>     -+    // Seed info.offset and info.save from the rule whose effect
>>>> is in
>>>>     -+    // force at info.begin.  This must run even when `letters` was
>>>>     -+    // already populated from i[-1].next_letters() (which happens
>>>>     -+    // during a re-entry of partial lazy expansion: the previous
>>>> batch
>>>>     -+    // left an expanded ZoneInfo whose next_letters() field is the
>>>>     -+    // letters for the first sys_info of this batch), because
>>>>     -+    // info.offset/save are still at their stdoff/0 init values
>>>> and
>>>>     -+    // would otherwise carry through into the first emitted
>>>> sys_info
>>>>     -+    // with the wrong total offset.
>>>>     ++    // Seed info.offset and info.save from the rule active at
>>>>     ++    // info.begin.  Always run this (even when `letters` was
>>>> populated
>>>>     ++    // from i[-1].next_letters() during partial-expansion
>>>> re-entry),
>>>>     ++    // because info.offset/save are still at their stdoff/0 init
>>>> values
>>>>     ++    // and would otherwise produce the wrong total offset.
>>>>      +    //
>>>>     -+    // The search uses a strict `rule_start < t` comparison, so
>>>> pass
>>>>     -+    // info.begin + 1s to make the half-open lookup (...,
>>>> info.begin]
>>>>     -+    // inclusive of the boundary instant.  This is what makes
>>>> named-
>>>>     -+    // rule zone lines like Africa/Algiers (PR 116110) seed with
>>>> the
>>>>     -+    // correct save: the Oct-21 rule fires at Oct 20 23:00 UTC in
>>>> the
>>>>     -+    // new line's frame, which is exactly the new line's begin.
>>>>     ++    // info.begin + 1s makes the strict `rule_start < t` search
>>>>     ++    // inclusive of a rule that fires at exactly info.begin.
>>>>      +    {
>>>>      +      sys_seconds t = info.begin + seconds(1);
>>>>      +      const Rule* active_rule = find_active_rule(rules, t,
>>>> ri.offset());
>>>>     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
>>>>      + }
>>>>      +    }
>>>>      +
>>>>     -+    // zic.c writezone merge optimization.  When the previous zone
>>>>     -+    // line's end total offset differs from this line's seeded
>>>> total
>>>>     -+    // and the local time would jump backward at the boundary,
>>>> look
>>>>     -+    // for a rule in this line's set that fires within the
>>>> resulting
>>>>     -+    // gap window and would compensate the jump.  zic folds such a
>>>>     -+    // rule into the boundary transition; we mirror that here by
>>>>     -+    // pulling the rule's save back to info.begin.
>>>>     -+    //
>>>>     -+    // Only runs at the first sys_info of a zone line (not on
>>>> re-entry
>>>>     -+    // mid-line during partial lazy expansion), which is detected
>>>> by
>>>>     -+    // an empty next_letters() on the prior expanded ZoneInfo: a
>>>> mid-
>>>>     -+    // line re-entry's prior ZoneInfo always has a non-empty
>>>>     -+    // next_letters() (the letters of the rule that fires at the
>>>> new
>>>>     -+    // batch's start), whereas a zone-line transition's prior
>>>> ZoneInfo
>>>>     -+    // ends with empty next_letters() because the line's last
>>>>     -+    // iteration emits with letters cleared.
>>>>     ++    // zic.c writezone merge optimization.  When the local time
>>>> jumps
>>>>     ++    // backward at a zone-line boundary and a rule in the new
>>>> line's set
>>>>     ++    // fires within that gap window, fold the rule's save into the
>>>>     ++    // boundary so the new line begins with the post-rule save.
>>>>      +    //
>>>>     -+    // Canonical examples:
>>>>     -+    //   * America/Argentina/Buenos_Aires 1999-10-03: lines
>>>>     -+    //       -3 A -03/-02 1999 O 3
>>>>     -+    //       -4 A -04/-03 2000 Mar 3
>>>>     -+    //     have new_total = -4, prev_total = -3 (jump = -1h).  The
>>>>     -+    //     1999 Oct Argentina rule fires at Oct 3 04:00 UTC in the
>>>>     -+    //     new -4 frame, which is exactly info.begin + 1h, inside
>>>>     -+    //     the 1h window.
>>>>     -+    //   * Europe/Berlin 1945-05-24: lines
>>>>     -+    //       1 c CE%sT 1945 May 24 2
>>>>     -+    //       1 So CE%sT 1946
>>>>     -+    //     have new_total = 1, prev_total = 2 (jump = -1h).  The
>>>> So
>>>>     -+    //     1945-May-24 rule (save = 2h, "CEMT") fires at 01:00 UTC
>>>>     -+    //     in the new frame, inside the 1h window.
>>>>     ++    // Only runs at the first sys_info of a zone line (not on
>>>> partial-
>>>>     ++    // expansion re-entry mid-line): a mid-line re-entry's prior
>>>>     ++    // ZoneInfo has a non-empty next_letters(), while a zone-line
>>>>     ++    // transition's prior ZoneInfo ends with empty next_letters().
>>>>      +    if (i != infos.begin() && i[-1].expanded()
>>>>      +  && i[-1].next_letters().empty())
>>>>      +      {
>>>>     @@ libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc (new)
>>>>      +// { dg-require-effective-target cxx11_abi }
>>>>      +// { dg-xfail-run-if "no weak override on AIX" { powerpc-ibm-aix*
>>>> } }
>>>>      +
>>>>     -+// Regression test: zic.c's writezone merges a zone-line
>>>> transition with
>>>>     -+// a rule firing that would otherwise create a brief
>>>> observably-wrong
>>>>     -+// stretch of local time.  When two adjacent Zone lines have
>>>> different
>>>>     -+// total offsets and the new line's rule set has a rule firing
>>>> within
>>>>     -+// |jump| of the boundary (where jump = new_total - old_total <
>>>> 0, i.e.
>>>>     -+// local time goes backward at the boundary), zic folds that rule
>>>> into
>>>>     -+// the boundary itself: the single transition emitted has the
>>>> rule's
>>>>     -+// save value already applied, so the new line begins with the
>>>> post-rule
>>>>     -+// save rather than briefly using the pre-rule save and then
>>>> transitioning
>>>>     -+// again moments later.
>>>>     -+//
>>>>     -+// Two canonical real-world cases:
>>>>     -+//   * America/Argentina/Buenos_Aires 1999-10-03 (lines change
>>>> stdoff
>>>>     -+//     -3 → -4 with an Argentina DST rule firing on the same day).
>>>>     -+//   * Europe/Berlin 1945-05-24 (lines split a rule set, with the
>>>> So
>>>>     -+//     1945-May-24 rule firing inside the boundary's window).
>>>>     ++// When two adjacent Zone lines differ in total offset and the
>>>> new line's
>>>>     ++// rule set has a rule firing within |jump| of the boundary
>>>> (where jump
>>>>     ++// is a backward local-time jump), zic.c's writezone folds that
>>>> rule
>>>>     ++// into the boundary, so the new line begins with the post-rule
>>>> save.
>>>>      +//
>>>>     -+// Mirror the Buenos Aires shape with a synthetic zone.
>>>>     ++// Mirrors America/Argentina/Buenos_Aires around 1999-10-03.
>>>>      +
>>>>      +#include <chrono>
>>>>      +#include <fstream>
>>>>     @@ libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc (new)
>>>>      +{
>>>>      +  using namespace std::chrono;
>>>>      +
>>>>     -+  // Argentina-style: stdoff jumps from -3 to -4 at the same
>>>> instant
>>>>     -+  // a save=1 ("S") rule fires.  In the new (-4) frame, the rule's
>>>>     -+  // wall at_time of 00:00 is at UT 04:00, which is 1 hour after
>>>> the
>>>>     -+  // boundary at UT 03:00.  Without the merge optimization the new
>>>>     -+  // line would seed with save=0 (offset -4, abbrev -04) for that
>>>> 1
>>>>     -+  // hour and then transition to save=1 (offset -3, abbrev -03);
>>>>     -+  // with the merge, the boundary itself is at offset=-3, save=1.
>>>>     ++  // stdoff jumps from -3 to -4 at the same instant a save=1 rule
>>>> fires.
>>>>     ++  // In the new (-4) frame the rule fires 1 hour after the
>>>> boundary at
>>>>     ++  // UT 03:00, so the merge folds the rule into the boundary and
>>>> the
>>>>     ++  // new line begins at offset=-3, save=1 (abbrev "-03").
>>>>      +  std::ofstream("tzdata.zi") << R"(# version test_zone_merge
>>>>      +R T 1999 o - O 3 0 1 -
>>>>      +R T 2000 o - Mar 3 0 0 -
>>>>     @@ libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc (new)
>>>>      +  VERIFY( before.save == 0min );
>>>>      +  VERIFY( before.abbrev == "-03" );
>>>>      +
>>>>     -+  // At the boundary the merge optimization kicks in: the second
>>>> zone
>>>>     -+  // line's first sys_info should already have save=1 from the
>>>> Oct 3
>>>>     -+  // rule, total offset -3h, abbrev "-03".  Without the fix,
>>>> chrono
>>>>     -+  // would emit a 1-hour stretch of save=0 ("-04") here.
>>>>     ++  // The new line's first sys_info already has save=1 from the
>>>> merge,
>>>>     ++  // total offset -3h, abbrev "-03".
>>>>      +  auto at_boundary = tz->get_info(boundary);
>>>>      +  VERIFY( at_boundary.offset == -3h );
>>>>      +  VERIFY( at_boundary.save == 60min );
>>>>
>>>> --
>>>> 2.34.1
>>>>
>>>>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://gcc.gnu.org/pipermail/libstdc++/attachments/20260519/99547fc5/attachment-0001.htm>


More information about the Libstdc++ mailing list