{"id":2222417,"url":"http://patchwork.ozlabs.org/api/1.2/patches/2222417/?format=json","web_url":"http://patchwork.ozlabs.org/project/gcc/patch/d134c51fccf31bf2e57f5525848977a531186cb2.1775912642.git.alvaro.begue@gmail.com/","project":{"id":17,"url":"http://patchwork.ozlabs.org/api/1.2/projects/17/?format=json","name":"GNU Compiler Collection","link_name":"gcc","list_id":"gcc-patches.gcc.gnu.org","list_email":"gcc-patches@gcc.gnu.org","web_url":null,"scm_url":null,"webscm_url":null,"list_archive_url":"","list_archive_url_format":"","commit_url_format":""},"msgid":"<d134c51fccf31bf2e57f5525848977a531186cb2.1775912642.git.alvaro.begue@gmail.com>","list_archive_url":null,"date":"2026-04-11T13:33:08","name":"[4/5] libstdc++: Cascade wall-time saves in lazy expansion seeding [PR 124853]","commit_ref":null,"pull_url":null,"state":"new","archived":false,"hash":"ab98de1d5dd39b40e236ebb7e01bc7937b088031","submitter":{"id":93119,"url":"http://patchwork.ozlabs.org/api/1.2/people/93119/?format=json","name":"Alvaro Begue","email":"alvaro.begue@gmail.com"},"delegate":null,"mbox":"http://patchwork.ozlabs.org/project/gcc/patch/d134c51fccf31bf2e57f5525848977a531186cb2.1775912642.git.alvaro.begue@gmail.com/mbox/","series":[{"id":499557,"url":"http://patchwork.ozlabs.org/api/1.2/series/499557/?format=json","web_url":"http://patchwork.ozlabs.org/project/gcc/list/?series=499557","date":"2026-04-11T13:33:04","name":"libstdc++: chrono tzdb correctness fixes","version":1,"mbox":"http://patchwork.ozlabs.org/series/499557/mbox/"}],"comments":"http://patchwork.ozlabs.org/api/patches/2222417/comments/","check":"pending","checks":"http://patchwork.ozlabs.org/api/patches/2222417/checks/","tags":{},"related":[],"headers":{"Return-Path":"<gcc-patches-bounces~incoming=patchwork.ozlabs.org@gcc.gnu.org>","X-Original-To":["incoming@patchwork.ozlabs.org","gcc-patches@gcc.gnu.org"],"Delivered-To":["patchwork-incoming@legolas.ozlabs.org","gcc-patches@gcc.gnu.org"],"Authentication-Results":["legolas.ozlabs.org;\n\tdkim=pass (2048-bit key;\n unprotected) header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256\n header.s=20251104 header.b=GlRCGr0t;\n\tdkim-atps=neutral","legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=gcc.gnu.org\n (client-ip=2620:52:6:3111::32; helo=vm01.sourceware.org;\n envelope-from=gcc-patches-bounces~incoming=patchwork.ozlabs.org@gcc.gnu.org;\n receiver=patchwork.ozlabs.org)","sourceware.org;\n\tdkim=pass (2048-bit key,\n unprotected) header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256\n header.s=20251104 header.b=GlRCGr0t","sourceware.org;\n dmarc=pass (p=none dis=none) header.from=gmail.com","sourceware.org; spf=pass smtp.mailfrom=gmail.com","server2.sourceware.org;\n arc=none smtp.remote-ip=209.85.219.46"],"Received":["from vm01.sourceware.org (vm01.sourceware.org\n [IPv6:2620:52:6:3111::32])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange x25519 server-signature ECDSA (secp384r1) server-digest SHA384)\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4ftF9P6xnhz1yGb\n\tfor <incoming@patchwork.ozlabs.org>; Sat, 11 Apr 2026 23:38:01 +1000 (AEST)","from vm01.sourceware.org (localhost [127.0.0.1])\n\tby sourceware.org (Postfix) with ESMTP id F01D24BA23DF\n\tfor <incoming@patchwork.ozlabs.org>; Sat, 11 Apr 2026 13:37:59 +0000 (GMT)","from mail-qv1-f46.google.com (mail-qv1-f46.google.com\n [209.85.219.46])\n by sourceware.org (Postfix) with ESMTPS id 5C8834BA23D2\n for <gcc-patches@gcc.gnu.org>; Sat, 11 Apr 2026 13:33:31 +0000 (GMT)","by mail-qv1-f46.google.com with SMTP id\n 6a1803df08f44-8ac9ef74131so1443986d6.1\n for <gcc-patches@gcc.gnu.org>; Sat, 11 Apr 2026 06:33:31 -0700 (PDT)","from alvaro-MS-7D37.verizon.net\n ([2600:4041:592d:7300:b8f7:c631:1ab:88b0])\n by smtp.gmail.com with ESMTPSA id\n 6a1803df08f44-8ac84caace4sm47629306d6.39.2026.04.11.06.33.30\n (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n Sat, 11 Apr 2026 06:33:30 -0700 (PDT)"],"DKIM-Filter":["OpenDKIM Filter v2.11.0 sourceware.org F01D24BA23DF","OpenDKIM Filter v2.11.0 sourceware.org 5C8834BA23D2"],"DMARC-Filter":"OpenDMARC Filter v1.4.2 sourceware.org 5C8834BA23D2","ARC-Filter":"OpenARC Filter v1.0.0 sourceware.org 5C8834BA23D2","ARC-Seal":"i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1775914411; cv=none;\n b=SjzFXCbSqSYfaCW2vAirsVW7itOsd3H7JITAW3JfaXNlcLFoQUxhJyglG4pUgX0vhx1FZwTiE9BshqP5OCDgKeOHsuMVjzghsZHbUri4q6Bfbg71jXA0MV9PHj/+nvV6e9GMySRS+KZW9wbeFdD2zCU8uUPwAYkVv38b4aIvZHs=","ARC-Message-Signature":"i=1; a=rsa-sha256; d=sourceware.org; s=key;\n t=1775914411; c=relaxed/simple;\n bh=Cvc+Wd1DTEtT3gOQ8GMsySwnscJBb4VW5fsyVcWjGBI=;\n h=DKIM-Signature:From:To:Subject:Date:Message-Id:MIME-Version;\n b=Nu+yo071s8l0g0OCP5dBB2NEu3cVgHd9kJo380AHriMbpZIAzyh2xlRR/y9hoG3HwCZ53UT44vTWC5y9OIyvG096BL6PYUbCELhdsRGYBxqxBCecwJzERqn/juAvI4JzxA5DMbxk7shZXJkUosqv9jPJyBFPau/FW+QwGe163q8=","ARC-Authentication-Results":"i=1; server2.sourceware.org","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=gmail.com; s=20251104; t=1775914411; x=1776519211; darn=gcc.gnu.org;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:from:to:cc:subject:date\n :message-id:reply-to;\n bh=7z4RCRTw/J83D36QBNqnfxwq6Iolk2LTUfpBbBsTKVQ=;\n b=GlRCGr0tOPLJ31x923fDQiJJpWWFNZvkQnr5dNTtjRNkKAeUklug0OgPIXjj+OAhDI\n ZAQf2/I+64sB02OSxskoBpmUJjWnrwqLMIK4v/QL7lQrC3XOmHk6KEPce/aX4GEaVYx4\n 5GbnSKTUKqzFbboNC0ysJPwvXTtznCsuI7e3Z8xpbnGvanaOo8Y0Su/J6u5bwW1G8RsO\n bU2es2gIK12BJZSmF6bV/TsaABO9dUGXH6AOS0ZSIxMW4Q0drXrB+ddOg498ZwvDnnFB\n N/0PH3fXyphrXSCnK8SPPTf+KCz6r12bKpf5xMxQnBd7c2jSJalZeei7n4odh4D3A6Fa\n jM7g==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=1e100.net; s=20251104; t=1775914411; x=1776519211;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from\n :to:cc:subject:date:message-id:reply-to;\n bh=7z4RCRTw/J83D36QBNqnfxwq6Iolk2LTUfpBbBsTKVQ=;\n b=q+Ax1a8XKbtglJSvuDrlYqko8kjdgL1lkeUN0yOQi9hSBa/C4OuWZDJsx1amBotufY\n rLLTAfTZ25e0VdtymK5opi4fdMaPsuS2aKcQpzggBVoLh6BniLR3mAFEDz8bPFKlxFZc\n EOu11dKW/FmG7s9zorT46uOLIvH77SdCLlZuck8c3f8Q5zXkX2duCDVfD06f0FBXOSOe\n /7mmAib3vtkzXcTgOHtjUvVKF27r3bGjTimc0xqwqDmMo71oPQITvDkptX0BA7F9orlE\n ElcK64RsHeEgtHUWICG7r6PzoY92wU+qMdsRl8sF/1qYUVaE1OWJhQp2FoMLXe3JZ6RZ\n 5INQ==","X-Gm-Message-State":"AOJu0YzOB/ACk5zH/lR1tkNt1qTthEka4UTdp46K9dMuX50g5JpaSbYa\n 5+a8Pvp/1zxseHdB1+D+y9Psgupj3YK9y/GIVNwOXVxFuXEyd31yfGUDZCo+Bw==","X-Gm-Gg":"AeBDieskZj0NdVDoMtdJCf5odSVhgiCty12wkT1ggPeSF8RAQM296GwAxAu5dR+q+ZE\n slZ0ZDSRdfU4S2OtmkTSUB6DZt5I60/DenGjaKrzOJoAf2+LzY0LIgVSzkua0XqBPEZd3ecwpuj\n fBy72dE1CT/x4Eob1cKHWj8Z3LtXlrQDoXKqJtgaEbtdiErKML09kvLg6a0RFTUzXXFeMTEssbO\n H814hSh6cTade3M+ALCae1MwF89EPj3BhTCukQOT5+BEjrbi9fYYXPEETAU+G83WvQbAzY7mwoU\n BEDWzRvoFYKNwGzDlFW2yVhULtUGsFE5+mTs70DVbJou61HOeL0/8dRToeFd/2BhqsLBs1tToBG\n iM0IPo/0wWYCQ0N4klbViPqA58R+HrGCit1Vt30saHta0m3SA5HhjS5EgdDAhFc0fFBpRBGCiwh\n Ef7Ik/gTzEJ20zBih6WMo8xvXryojjmYSWTvm8qiNxSeEQxLE1VGHHjpNa7ZJaVpWwSkGSkDvz7\n pnV71xNV5Z7zP49GiuS35MfVo8yGB8SO/CXikHST1jTxAYCW1ZUoXOfndVNdom3TuJPfDgoqVu3\n xjq0aUUMntJC3VE=","X-Received":"by 2002:a05:6214:4981:b0:89c:ed12:41b with SMTP id\n 6a1803df08f44-8ac8628a17cmr109877646d6.31.1775914410568;\n Sat, 11 Apr 2026 06:33:30 -0700 (PDT)","From":"Alvaro Begue <alvaro.begue@gmail.com>","To":"gcc-patches@gcc.gnu.org","Cc":"jwakely@redhat.com, libstdc++@gcc.gnu.org,\n Alvaro Begue <alvaro.begue@gmail.com>","Subject":"[PATCH 4/5] libstdc++: Cascade wall-time saves in lazy expansion\n seeding [PR 124853]","Date":"Sat, 11 Apr 2026 09:33:08 -0400","Message-Id":"\n <d134c51fccf31bf2e57f5525848977a531186cb2.1775912642.git.alvaro.begue@gmail.com>","X-Mailer":"git-send-email 2.34.1","In-Reply-To":"<cover.1775912642.git.alvaro.begue@gmail.com>","References":"<cover.1775912642.git.alvaro.begue@gmail.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","X-BeenThere":"gcc-patches@gcc.gnu.org","X-Mailman-Version":"2.1.30","Precedence":"list","List-Id":"Gcc-patches mailing list <gcc-patches.gcc.gnu.org>","List-Unsubscribe":"<https://gcc.gnu.org/mailman/options/gcc-patches>,\n <mailto:gcc-patches-request@gcc.gnu.org?subject=unsubscribe>","List-Archive":"<https://gcc.gnu.org/pipermail/gcc-patches/>","List-Post":"<mailto:gcc-patches@gcc.gnu.org>","List-Help":"<mailto:gcc-patches-request@gcc.gnu.org?subject=help>","List-Subscribe":"<https://gcc.gnu.org/mailman/listinfo/gcc-patches>,\n <mailto:gcc-patches-request@gcc.gnu.org?subject=subscribe>","Errors-To":"gcc-patches-bounces~incoming=patchwork.ozlabs.org@gcc.gnu.org"},"content":"When _M_get_sys_info seeds a Zone line by looking up the active rule\njust before info.begin, the previous code interpreted each rule in\nisolation against ri.offset() (the line's standard offset alone),\nignoring the running save accumulated by earlier rules in the same\nyear.  For most zones this gives the right answer because the search\nonly matters when no rule has fired yet, but for zones whose rule\nset has wall-time rules whose effective firing time depends on a\nprior rule's save it produces wrong answers.\n\nCanonical case: Europe/Paris around 1945.  France's rules\n\n  R Fr 1945 o - Apr 2  2 2 M\n  R Fr 1945 o - Sep 16 3 0 -\n\nboth use plain wall time.  In Paris's stdoff=1 frame, the September\nrule's at_time of 03:00 wall translates to UT Sep 16 02:00 if no\nprior save is applied, but to UT Sep 16 00:00 once the running save\nof 2h from the April rule is taken into account.  When seeding a\nsys_info whose info.begin falls between those two values, the simple\nsearch picks the April rule (save=2 → CEMT, total offset 3h) when\nthe correct answer is the September rule (save=0 → CET, total offset\n1h).  The harness reports this as a sustained CEMT stretch where\nzic and libc agree on CET.\n\nReplace the per-rule isolated lookup with a chronological cascade\nwalker (the same shape as find_pre_until_rule from the previous\nPR 116110 commit, but with a fixed boundary `t = info.begin + 1s`\ninstead of iterative-shrink semantics).  All (rule, year) pairs are\ncollected over [min(rule.from), year(t)+1], sorted by an approximate\nfire time, and walked in order while a running save value is\nmaintained.  Each Wall-time rule's actual fire time is computed\nrelative to the cascaded save state, and rules whose actual fire\ntime is < t apply their save and become candidates for the active\nrule.  The +1 year extension catches rules whose wall at_time falls\nin early January or late December but whose UT firing crosses a\nyear boundary due to a large stdoff or save (Pacific/Auckland's\n1946 Jan 1 rule, in stdoff=12h, fires at 1945-12-31 11:30 UT).\n\nThe fallback \"earliest STD rule\" logic is preserved for the case\nwhere no rule has fired yet, but is moved to its own branch for\nclarity.\n\nlibstdc++-v3/ChangeLog:\n\n\tPR libstdc++/124853\n\t* src/c++20/tzdb.cc (time_zone::_Impl::_M_get_sys_info):\n\tReplace the per-rule isolated active-rule search with a\n\tchronological cascade walker that maintains a running save\n\tand interprets Wall-time rules' at_time relative to it.\n\tExtend the calendar window by one year on each side to catch\n\trules whose UT firing crosses a year boundary.  Move the\n\t\"earliest STD rule\" fallback to its own branch.\n\t* testsuite/std/time/time_zone/wall_cascade.cc: New test.\n---\n libstdc++-v3/src/c++20/tzdb.cc                | 159 +++++++++++-------\n .../std/time/time_zone/wall_cascade.cc        |  87 ++++++++++\n 2 files changed, 188 insertions(+), 58 deletions(-)\n create mode 100644 libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc","diff":"diff --git a/libstdc++-v3/src/c++20/tzdb.cc b/libstdc++-v3/src/c++20/tzdb.cc\nindex 2e910c99d..76ec956b5 100644\n--- a/libstdc++-v3/src/c++20/tzdb.cc\n+++ b/libstdc++-v3/src/c++20/tzdb.cc\n@@ -705,11 +705,9 @@ namespace std::chrono\n #endif\n     };\n \n-    // Find the Rule whose save value is in force at the wall-time UNTIL\n-    // of a Zone line, given that `wall_minus_stdoff` is the line's UNTIL\n-    // expressed in the \"save=0\" frame (i.e. the parsed wall UNTIL with\n-    // the line's STDOFF subtracted) and `stdoff` is the line's standard\n-    // offset.\n+    // Find the Rule in `rules` whose effect was last in force at time\n+    // `t`, given that `stdoff` is the standard offset of the enclosing\n+    // zone line.  Returns nullptr if no rule fired strictly before t.\n     //\n     // The function walks all (rule, year) pairs in chronological order,\n     // maintaining a running save value.  Wall-time rules have their TIME\n@@ -719,6 +717,84 @@ namespace std::chrono\n     // zic.c's outzone() logic and is required for zone lines whose\n     // rule set has rules whose at_time depends on cascading saves.\n     //\n+    // Canonical case: Europe/Paris around 1945, where the France rules\n+    //   1945 Apr 2  02:00 wall  save=2  M\n+    //   1945 Sep 16 03:00 wall  save=0  -\n+    // chain together: in the (stdoff=1, save=2) frame the September\n+    // rule fires at Sep 16 00:00 UT, not Sep 16 02:00 UT.\n+    //\n+    // The calendar window is extended by one year on each side, to\n+    // catch rules whose wall-time at_time falls in early January or\n+    // late December but whose UT firing crosses a year boundary due\n+    // to a large stdoff or save (Pacific/Auckland's \"1946 Ja 1\" rule\n+    // in stdoff=12h fires at 1945-12-31 11:30 UT).\n+    template<typename _RuleRange>\n+      const Rule*\n+      find_active_rule(const _RuleRange& rules, sys_seconds t, seconds stdoff)\n+      {\n+\tif (rules.empty())\n+\t  return nullptr;\n+\n+\tconst year last_year\n+\t  = year_month_day{chrono::floor<days>(t)}.year() + years(1);\n+\tyear first_year = year::max();\n+\tfor (const auto& r : rules)\n+\t  if (r.from < first_year)\n+\t    first_year = r.from;\n+\tif (first_year > last_year)\n+\t  return nullptr;\n+\n+\tstruct Pending\n+\t{\n+\t  const Rule* rule;\n+\t  year y;\n+\t  sys_seconds approx_when;\n+\t};\n+\tvector<Pending> pending;\n+\tpending.reserve(64);\n+\tfor (year y = first_year; y <= last_year; ++y)\n+\t  for (const auto& r : rules)\n+\t    {\n+\t      if (y < r.from || y > r.to)\n+\t\tcontinue;\n+\t      seconds approx_off{};\n+\t      if (r.when.indicator == at_time::Wall\n+\t\t    || r.when.indicator == at_time::Standard)\n+\t\tapprox_off = stdoff;\n+\t      pending.push_back({&r, y, r.start_time(y, approx_off)});\n+\t    }\n+\tstd::sort(pending.begin(), pending.end(),\n+\t\t  [](const Pending& a, const Pending& b) {\n+\t\t    return a.approx_when < b.approx_when;\n+\t\t  });\n+\n+\tseconds running_save{};\n+\tconst Rule* last_fired = nullptr;\n+\tfor (const auto& p : pending)\n+\t  {\n+\t    seconds offset{};\n+\t    if (p.rule->when.indicator == at_time::Wall)\n+\t      offset = stdoff + running_save;\n+\t    else if (p.rule->when.indicator == at_time::Standard)\n+\t      offset = stdoff;\n+\t    sys_seconds fire = p.rule->start_time(p.y, offset);\n+\t    if (fire >= t)\n+\t      continue;\n+\t    last_fired = p.rule;\n+\t    running_save = p.rule->save;\n+\t  }\n+\treturn last_fired;\n+      }\n+\n+    // Find the Rule whose save value is in force at the wall-time UNTIL\n+    // of a Zone line, given that `wall_minus_stdoff` is the line's UNTIL\n+    // expressed in the \"save=0\" frame (i.e. the parsed wall UNTIL with\n+    // the line's STDOFF subtracted) and `stdoff` is the line's standard\n+    // offset.\n+    //\n+    // Like find_active_rule, walks (rule, year) pairs chronologically\n+    // with a running save value, but interprets `t` differently.\n+    //\n     // The comparison `fire < boundary` shrinks `boundary` as the running\n     // save cascades up: a rule that fires AT the boundary (with the\n     // cascaded save applied) is treated as belonging to the next zone\n@@ -729,11 +805,6 @@ namespace std::chrono\n     // of line 6 (\"0 d WE%sT 1977 O 21\").  The pre-rule save (1h, from\n     // the May 6 rule) is what determines the boundary's UT placement,\n     // not the Oct 21 rule's save=0.\n-    //\n-    // The calendar window is extended by one year on each side, to\n-    // catch rules whose wall-time at_time falls in early January or\n-    // late December but whose UT firing crosses a year boundary due\n-    // to a large stdoff or save.\n     template<typename _RuleRange>\n       const Rule*\n       find_pre_until_rule(const _RuleRange& rules,\n@@ -994,19 +1065,28 @@ namespace std::chrono\n \t// the Oct-21 rule fires at Oct 20 23:00 UTC in the new line's\n \t// frame, which is exactly the new line's begin.\n \tsys_seconds t = info.begin + seconds(1);\n-\tconst year_month_day date(chrono::floor<days>(t));\n \n \t// Try to find a Rule active before this time, to get initial\n-\t// SAVE and LETTERS values. There may not be a Rule for the period\n-\t// before the first DST transition, so find the earliest DST->STD\n-\t// transition and use the LETTERS from that.\n-\tconst Rule* active_rule = nullptr;\n-\tsys_seconds active_rule_start = sys_seconds::min();\n-\tconst Rule* first_std = nullptr;\n-\tfor (const auto& rule : rules)\n+\t// SAVE and LETTERS values.  See find_active_rule for the search\n+\t// semantics.\n+\tconst Rule* active_rule = find_active_rule(rules, t, ri.offset());\n+\n+\tif (active_rule)\n+\t  {\n+\t    info.offset = ri.offset() + active_rule->save;\n+\t    info.save = chrono::duration_cast<minutes>(active_rule->save);\n+\t    letters = active_rule->letters;\n+\t  }\n+\telse\n \t  {\n-\t    if (rule.save == minutes(0))\n+\t    // No rule applies before info.begin; fall back to the LETTERS\n+\t    // of the earliest STD rule, since the period before the first\n+\t    // DST transition is conventionally standard time.\n+\t    const Rule* first_std = nullptr;\n+\t    for (const auto& rule : rules)\n \t      {\n+\t\tif (rule.save != minutes(0))\n+\t\t  continue;\n \t\tif (!first_std)\n \t\t  first_std = &rule;\n \t\telse if (rule.from < first_std->from)\n@@ -1018,46 +1098,9 @@ namespace std::chrono\n \t\t      first_std = &rule;\n \t\t  }\n \t      }\n-\n-\t    year y = date.year();\n-\n-\t    if (y > rule.to) // rule no longer applies at time t\n-\t      continue;\n-\t    if (y < rule.from) // rule doesn't apply yet at time t\n-\t      continue;\n-\n-\t    sys_seconds rule_start;\n-\n-\t    seconds offset{}; // appropriate for at_time::Universal\n-\t    if (rule.when.indicator == at_time::Wall)\n-\t      offset = info.offset;\n-\t    else if (rule.when.indicator == at_time::Standard)\n-\t      offset = ri.offset();\n-\n-\t    // Time the rule takes effect this year:\n-\t    rule_start = rule.start_time(y, offset);\n-\n-\t    if (rule_start >= t && rule.from < y)\n-\t      {\n-\t\t// Try this rule in the previous year.\n-\t\trule_start = rule.start_time(--y, offset);\n-\t      }\n-\n-\t    if (active_rule_start < rule_start && rule_start < t)\n-\t      {\n-\t\tactive_rule_start = rule_start;\n-\t\tactive_rule = &rule;\n-\t      }\n-\t  }\n-\n-\tif (active_rule)\n-\t  {\n-\t    info.offset = ri.offset() + active_rule->save;\n-\t    info.save = chrono::duration_cast<minutes>(active_rule->save);\n-\t    letters = active_rule->letters;\n+\t    if (first_std)\n+\t      letters = first_std->letters;\n \t  }\n-\telse if (first_std)\n-\t  letters = first_std->letters;\n       }\n \n     const Rule* curr_rule = nullptr;\ndiff --git a/libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc b/libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc\nnew file mode 100644\nindex 000000000..97f38c3bc\n--- /dev/null\n+++ b/libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc\n@@ -0,0 +1,87 @@\n+// { dg-do run { target c++20 } }\n+// { dg-require-effective-target tzdb }\n+// { dg-require-effective-target cxx11_abi }\n+// { dg-xfail-run-if \"no weak override on AIX\" { powerpc-ibm-aix* } }\n+\n+// Regression test: when lazy expansion seeds a Zone line whose rule set\n+// has wall-time rules whose effective firing time depends on a prior\n+// rule's save (cascading wall-time interpretation), the seeding code\n+// must walk the rules chronologically with a running save value, not\n+// look at each rule in isolation against a fixed `save=0` frame.\n+//\n+// Mirrors the Europe/Paris 1945 case.  France's rules\n+//   1945 Apr 2  02:00 wall  save=2  M\n+//   1945 Sep 16 03:00 wall  save=0  -\n+// chain together: in the (stdoff=1, save=2) frame the September rule\n+// fires at Sep 16 00:00 UT, not Sep 16 02:00 UT.\n+//\n+// Construct a synthetic two-line zone whose second line begins between\n+// those two interpretations of the September rule, so the seeding has\n+// to choose: with the cascade, the September rule has already fired at\n+// info.begin and the new line seeds with save=0; without the cascade,\n+// the September rule appears not to have fired yet and the new line\n+// would (incorrectly) seed with save=2 from the April rule.\n+\n+#include <chrono>\n+#include <fstream>\n+#include <testsuite_hooks.h>\n+\n+static bool override_used = false;\n+\n+namespace __gnu_cxx\n+{\n+  const char* zoneinfo_dir_override() {\n+    override_used = true;\n+    return \"./\";\n+  }\n+}\n+\n+int\n+main()\n+{\n+  using namespace std::chrono;\n+\n+  // Line 1 ends at \"1945 Sep 16 1u\" (Universal time, no save shenanigans),\n+  // so info.begin for line 2 is exactly 1945-09-16 01:00 UT.\n+  //\n+  // Cascade seeding: in line 2's frame (stdoff=1):\n+  //   * Apr 2 fires at Apr 2 01:00 UT (running save 0 → 2)\n+  //   * Sep 16 fires at Sep 16 00:00 UT (running save 2 → 0)\n+  // Both fire before info.begin (Sep 16 01:00 UT), so the active rule\n+  // at info.begin is the Sep 16 one, save=0 → CET, total offset 1h.\n+  //\n+  // Without cascade: Sep 16 is computed as 03:00 - stdoff(1) = 02:00 UT,\n+  // which is *after* info.begin, so the seeding falls back to the April\n+  // rule's save=2 → CEMT, total offset 3h.  That is the bug.\n+  std::ofstream(\"tzdata.zi\") << R\"(# version test_wall_cascade\n+R Fr 1945 o - Apr 2  2 2 M\n+R Fr 1945 o - Sep 16 3 0 -\n+Z Test/Paris 0  -  X     1945 Sep 16 1u\n+             1  Fr CE%sT\n+)\";\n+\n+  const auto& db = reload_tzdb();\n+  VERIFY( override_used );\n+  VERIFY( db.version == \"test_wall_cascade\" );\n+\n+  auto* tz = locate_zone(\"Test/Paris\");\n+\n+  // Line 2 begins at exactly 1945-09-16 01:00 UT.  Sample one second\n+  // after the boundary, well inside line 2's first sys_info.\n+  auto info = tz->get_info(sys_seconds{\n+      sys_days(1945y/September/16) + 1h + 1s});\n+  VERIFY( info.offset == 1h );\n+  VERIFY( info.save == 0min );\n+  VERIFY( info.abbrev == \"CET\" );\n+\n+  // The boundary instant itself is in the new line.\n+  auto at_boundary\n+    = tz->get_info(sys_seconds{sys_days(1945y/September/16) + 1h});\n+  VERIFY( at_boundary.offset == 1h );\n+  VERIFY( at_boundary.save == 0min );\n+\n+  // Sample later still in line 2 (winter): unchanged.\n+  auto winter = tz->get_info(sys_days(1945y/December/1));\n+  VERIFY( winter.offset == 1h );\n+  VERIFY( winter.save == 0min );\n+}\n","prefixes":["4/5"]}