[PATCH v2 0/5] libstdc++: chrono tzdb correctness fixes
Tomasz Kaminski
tkaminsk@redhat.com
Mon Jul 20 09:45:30 GMT 2026
On Tue, May 19, 2026 at 10:05 AM Tomasz Kaminski <tkaminsk@redhat.com>
wrote:
>
>
> 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).
>
The modified versions of all the patches are now merged to the trunk (GCC
17),
and I will consider backporting (at least some of them) to GCC 16. While I
have heavily
changed the implementation, your initial work identifying and categorizing
the remaining
issues was immensely helpful. Thank you very much for your contributions.
Regards,
Tomasz
>
> 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/20260720/3102a537/attachment-0001.htm>
More information about the Libstdc++
mailing list