This patch was written with the assistance of an LLM (Claude Fable 5.1, Anthropic), hence the Assisted-by: tag per https://gcc.gnu.org/ai-policy.html. I directed the analysis, reviewed every line, ran the testing below and take responsibility for it.
Bootstrapped and regression-tested on x86_64-pc-linux-gnu against trunk r17-3939-g08794c636095 (default languages, --disable-multilib), with the g++ testsuite run in all standard levels (98, 11, 14, 17, 20, 23, 26 and 26 -fimplicit-constexpr, as check-c++-all does). compare_tests against an unpatched run of the same revision shows no regressions; the new test passes in every level from C++11 up. OK for trunk? The bug is present on all open release branches (13 through 16), so backports after a soak on trunk would be welcome. Since r10-5927 (PR c++/80265) cxx_eval_builtin_function_call replaces the string arguments of the str/mem builtins with the address of a STRING_CST before folding, and for the pointer-returning ones (memchr, strchr, strrchr, strstr) it afterwards puts the original argument back as the base of the folded POINTER_PLUS_EXPR. That re-basing is unconditional, but the replacement only happens when the evaluated argument is a bare ADDR_EXPR (offset 0). When the argument already carries an offset, for instance memchr (a + 1, c, n), the argument is passed to fold as-is, fold_builtin_call_array merges the offset into the result (a + 10), and the re-basing then rewrites that result to (a + 1) + 10, counting the offset twice. constexpr const char a[] = "abcdefghijabxdefghijaaa"; static_assert (__builtin_memchr (a + 1, 'a', 22) == a + 10, ""); fails since GCC 10 in every dialect; &a[1] is unaffected because the evaluated argument is an ADDR_EXPR. C++26 exposes this to ordinary code: P2738 makes void* -> T* casts constant expressions, so the front end now folds char_traits<char>::find on constant haystacks and std::string_view::find (c, pos) returns pos too much. Fix by remembering which argument was replaced and re-basing only in that case; otherwise the folded result is already relative to the evaluated argument. Assisted-by: Claude Fable 5.1 (Anthropic) PR c++/127218 gcc/cp/ChangeLog: * constexpr.cc (cxx_eval_builtin_function_call): Remember the argument replaced by the address of a STRING_CST in STRRET_OP and re-base the folded result on it only when that replacement happened. gcc/testsuite/ChangeLog: * g++.dg/cpp0x/constexpr-memchr2.C: New test. Signed-off-by: Steven Sudit <[email protected]> Patch attached (also applies with git am).
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Steven Sudit <[email protected]> Date: Sat, 29 Aug 2026 12:00:00 -0400 Subject: [PATCH] c++: Don't re-base a folded string builtin on an offset argument [PR127218] Since r10-5927 (PR c++/80265) cxx_eval_builtin_function_call replaces the string arguments of the str/mem builtins with the address of a STRING_CST before folding, and for the pointer-returning ones (memchr, strchr, strrchr, strstr) it afterwards puts the original argument back as the base of the folded POINTER_PLUS_EXPR. That re-basing is unconditional, but the replacement only happens when the evaluated argument is a bare ADDR_EXPR (offset 0). When the argument already carries an offset, for instance memchr (a + 1, c, n), the argument is passed to fold as-is, fold_builtin_call_array merges the offset into the result (a + 10), and the re-basing then rewrites that result to (a + 1) + 10, counting the offset twice. constexpr const char a[] = "abcdefghijabxdefghijaaa"; static_assert (__builtin_memchr (a + 1, 'a', 22) == a + 10, ""); fails since GCC 10 in every dialect; &a[1] is unaffected because the evaluated argument is an ADDR_EXPR. C++26 exposes this to ordinary code: P2738 makes void* -> T* casts constant expressions, so the front end now folds char_traits<char>::find on constant haystacks and std::string_view::find (c, pos) returns pos too much. Fix by remembering which argument was replaced and re-basing only in that case; otherwise the folded result is already relative to the evaluated argument. Assisted-by: Claude Fable 5.1 (Anthropic) PR c++/127218 gcc/cp/ChangeLog: * constexpr.cc (cxx_eval_builtin_function_call): Remember the argument replaced by the address of a STRING_CST in STRRET_OP and re-base the folded result on it only when that replacement happened. gcc/testsuite/ChangeLog: * g++.dg/cpp0x/constexpr-memchr2.C: New test. Signed-off-by: Steven Sudit <[email protected]> --- This patch was written with the assistance of an LLM (Claude Fable 5.1, Anthropic), hence the Assisted-by: tag per https://gcc.gnu.org/ai-policy.html. I directed the analysis, reviewed every line, ran the testing below and take responsibility for it. Bootstrapped and regression-tested on x86_64-pc-linux-gnu against trunk r17-3939-g08794c636095 (default languages, --disable-multilib), with the g++ testsuite run in all standard levels (98, 11, 14, 17, 20, 23, 26 and 26 -fimplicit-constexpr, as check-c++-all does). compare_tests against an unpatched run of the same revision shows no regressions; the new test passes in every level from C++11 up. OK for trunk? The bug is present on all open release branches (13 through 16), so backports after a soak on trunk would be welcome. gcc/cp/constexpr.cc | 19 +++++++++++--- gcc/testsuite/g++.dg/cpp0x/constexpr-memchr2.C | 30 ++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 gcc/testsuite/g++.dg/cpp0x/constexpr-memchr2.C diff --git a/gcc/cp/constexpr.cc b/gcc/cp/constexpr.cc --- a/gcc/cp/constexpr.cc +++ b/gcc/cp/constexpr.cc @@ -2612,6 +2612,9 @@ cxx_eval_builtin_function_call (const co int strops = 0; int strret = 0; + /* Set when argument STRRET-1 is replaced by the address of a STRING_CST + below, so the folded result has to be re-based on the original. */ + tree strret_op = NULL_TREE; bool bos = false; if (fndecl_built_in_p (fun, BUILT_IN_NORMAL)) switch (DECL_FUNCTION_CODE (fun)) @@ -2709,7 +2712,11 @@ cxx_eval_builtin_function_call (const co if (TREE_CODE (arg) == CONSTRUCTOR) arg = braced_lists_to_strings (TREE_TYPE (arg), arg); if (TREE_CODE (arg) == STRING_CST) - arg = build_address (arg); + { + arg = build_address (arg); + if (strret && i == strret - 1) + strret_op = oarg; + } else arg = oarg; } @@ -2796,11 +2803,15 @@ cxx_eval_builtin_function_call (const co return t; } - if (strret) + if (strret_op) { /* memchr returns a pointer into the first argument, but we replaced the - argument above with a STRING_CST; put it back it now. */ - tree op = CALL_EXPR_ARG (t, strret-1); + argument above with a STRING_CST; put it back now. Only when the + replacement happened: otherwise the folded result is already relative + to the evaluated argument, and fold may have merged an offset that + argument carried into the result's offset, so re-basing it on the + original argument would count that offset twice. */ + tree op = strret_op; STRIP_NOPS (new_call); if (TREE_CODE (new_call) == POINTER_PLUS_EXPR) TREE_OPERAND (new_call, 0) = op; diff --git a/gcc/testsuite/g++.dg/cpp0x/constexpr-memchr2.C b/gcc/testsuite/g++.dg/cpp0x/constexpr-memchr2.C new file mode 100644 --- /dev/null +++ b/gcc/testsuite/g++.dg/cpp0x/constexpr-memchr2.C @@ -0,0 +1,30 @@ +// PR c++/127218 +// { dg-do compile { target c++11 } } +// The pointer-returning string builtins must not count a nonzero offset in +// their first argument twice when the front end folds them. + +constexpr const char a[] = "abcdefghijabxdefghijaaa"; +static_assert (__builtin_memchr (a, 'x', sizeof (a) - 1) == a + 12, ""); +static_assert (__builtin_memchr (a + 1, 'a', sizeof (a) - 2) == a + 10, ""); +static_assert (__builtin_memchr (a + 2, 'a', sizeof (a) - 3) == a + 10, ""); +static_assert (__builtin_memchr (a + 1, 'x', sizeof (a) - 2) == a + 12, ""); +static_assert (__builtin_memchr (a + 1, 'q', sizeof (a) - 2) == nullptr, ""); +static_assert (__builtin_memchr (&a[1], 'a', sizeof (a) - 2) == a + 10, ""); +static_assert (__builtin_strchr (a + 1, 'a') == a + 10, ""); +static_assert (__builtin_strchr (a + 1, '\0') == a + 23, ""); +static_assert (__builtin_strrchr (a + 1, 'b') == a + 11, ""); +static_assert (__builtin_strrchr (a + 3, 'a') == a + 22, ""); +static_assert (__builtin_strstr (a + 1, "ab") == a + 10, ""); +static_assert (__builtin_strstr (a + 1, "") == a + 1, ""); +static_assert (__builtin_strstr (a + 1, "zz") == nullptr, ""); +static_assert (__builtin_strlen (a + 1) == 22, ""); + +constexpr const void *f1 (const char *p, int q, unsigned long n) +{ return __builtin_memchr (p, q, n); } +constexpr const char *f2 (const char *p, int q) { return __builtin_strchr (p, q); } +constexpr const char *f3 (const char *p, int q) { return __builtin_strrchr (p, q); } +constexpr const char *f4 (const char *p, const char *q) { return __builtin_strstr (p, q); } +static_assert (f1 (a + 1, 'a', sizeof (a) - 2) == a + 10, ""); +static_assert (f2 (a + 1, 'a') == a + 10, ""); +static_assert (f3 (a + 1, 'b') == a + 11, ""); +static_assert (f4 (a + 1, "ab") == a + 10, ""); -- 2.43.0
