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

Tomasz Kaminski tkaminsk@redhat.com
Tue May 12 12:40:32 GMT 2026


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.

>
> 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/20260512/37dfcf0f/attachment-0001.htm>


More information about the Libstdc++ mailing list