Re: [PATCH] c++: Fix up constexpr handling of break in expansion statements [PR125601]
Jason Merrill <[email protected]> Wed, 5 Aug 2026 18:05:30 -0400
| Newsgroups | gmane.comp.gcc.patches |
|---|---|
| Message-ID | <[email protected]> |
On 6/5/26 2:35 AM, Jakub Jelinek wrote:
> Hi!
>
> As the following testcase shows, we mishandle break statements in expansion
> statements during constant evaluation.
> finish_expansion_stmt changes the BREAK_STMT/CONTINUE_STMTs to GOTO_EXPRs
> to corresponding labels and marks those labels with
> LABEL_DECL_BREAK/LABEL_DECL_CONTINUE so that constexpr.cc is happy about
> those. The continue label (if any is needed) is right after each
> iteration's instantiated body, the break label (if any is needed) is after
> the last body.
> Now, continue seems to work properly, when we encounter it, we set
> *jump_target to it and continues predicate is true on it, but
> cxx_eval_statement_list has
> /* We've found a continue, so skip everything until we reach
> the label its jumping to. */
> if (continues (jump_target))
> {
> if (label_matches (ctx, jump_target, stmt))
> /* Found it. */
> *jump_target = NULL_TREE;
> else
> continue;
> }
> ...
> if (returns (jump_target)
> || breaks (jump_target)
> || throws (jump_target))
> break;
> and so it properly iterates through statement lists until it finds
> the label decl.
> But unfortunately it doesn't work for break, we set *jump_target on
> the GOTO_EXPR, breaks predicate is true, but then break out of any
> STATEMENT_LISTs and the only way to resume processing of statements
> in that case is when cxx_eval_loop_expr does
> if (breaks (jump_target))
> {
> *jump_target = NULL_TREE;
> break;
> }
> or similarly switch handling. But for expansion stmt there is
> nothing like that in the IL, so either we break some outer loop
> (foo in the testcase) instead, or fail because we think there was no return
> in the function.
>
> The following patch fixes this by wrapping the series of instantiated
> expansion stmt bodies (for all iterations) in an artificial
> do ... while (0); statement, but does that only if break; was actually
> needed (i.e. when we are emitting a break_label).
I suppose we could change break handling to iterate like continue, but
this is a clever solution. OK.
> Bootstrapped/regtested on x86_64-linux and i686-linux, ok for trunk?
>
> 2026-06-05 Jakub Jelinek <[email protected]>
>
> PR c++/125601
> * pt.cc (finish_expansion_stmt): If break; was seen in any of the
> expansion stmt bodies, wrap all the bodies in an artificial
> do ... while (0); stmt.
>
> * g++.dg/cpp26/expansion-stmt43.C: New test.
>
> --- gcc/cp/pt.cc.jj 2026-06-03 11:49:04.897785724 +0200
> +++ gcc/cp/pt.cc 2026-06-04 18:12:39.870465887 +0200
> @@ -33631,6 +33631,7 @@ finish_expansion_stmt (tree expansion_st
> DECL_NAME (decl) = NULL_TREE;
> }
>
> + tree stmt_list = push_stmt_list ();
> expansion_stmt_bc bc_data = { NULL_TREE, NULL_TREE, NULL, loc, false };
>
> for (unsigned HOST_WIDE_INT i = 0; i < n; ++i)
> @@ -33830,7 +33831,19 @@ finish_expansion_stmt (tree expansion_st
> }
> }
> if (bc_data.break_label)
> - add_stmt (build1 (LABEL_EXPR, void_type_node, bc_data.break_label));
> + {
> + /* If break; is seen, wrap all the expansion stmt bodies in
> + a single artificial do ... while (0); statement, so that
> + constant evaluation handles break; correctly. */
> + tree do_stmt
> + = build_stmt (loc, DO_STMT, NULL_TREE, NULL_TREE, NULL_TREE);
> + DO_COND (do_stmt) = boolean_false_node;
> + DO_BODY (do_stmt) = pop_stmt_list (stmt_list);
> + add_stmt (do_stmt);
> + add_stmt (build1 (LABEL_EXPR, void_type_node, bc_data.break_label));
> + }
> + else
> + add_stmt (pop_stmt_list (stmt_list));
> if (args == NULL_TREE)
> {
> TREE_TYPE (range_decl) = error_mark_node;
> --- gcc/testsuite/g++.dg/cpp26/expansion-stmt43.C.jj 2026-06-04 18:17:40.123509169 +0200
> +++ gcc/testsuite/g++.dg/cpp26/expansion-stmt43.C 2026-06-04 18:17:03.575503867 +0200
> @@ -0,0 +1,64 @@
> +// PR c++/125601
> +// { dg-do run { target c++14 } }
> +// { dg-options "-O2" }
> +
> +constexpr int
> +foo (int x)
> +{
> + int a = 0, b = 3;
> + while (b > 0)
> + {
> + ++a;
> + --b;
> + template for (constexpr int value : { 10, 20, 30 }) // { dg-warning "'template for' only available with" "" { target c++23_down } }
> + {
> + a += value;
> + if (x == 0)
> + break;
> + else if (x == 1)
> + continue;
> + a += 42;
> + }
> + }
> + return a;
> +}
> +
> +constexpr int
> +bar (int x)
> +{
> + int a = 0;
> + template for (constexpr int value : { 10, 20, 30 }) // { dg-warning "'template for' only available with" "" { target c++23_down } }
> + {
> + a += value;
> + if (x == 0)
> + break;
> + else if (x == 1)
> + continue;
> + a += 42;
> + }
> + return a;
> +}
> +
> +static_assert (foo (0) == 3 * (1 + 10), "");
> +static_assert (foo (1) == 3 * (1 + 10 + 20 + 30), "");
> +static_assert (foo (2) == 3 * (1 + 10 + 20 + 30 + 3 * 42), "");
> +static_assert (bar (0) == 10, "");
> +static_assert (bar (1) == 10 + 20 + 30, "");
> +static_assert (bar (2) == 10 + 20 + 30 + 3 * 42, "");
> +
> +int
> +main ()
> +{
> + if (foo (0) != 3 * (1 + 10))
> + __builtin_abort ();
> + if (foo (1) != 3 * (1 + 10 + 20 + 30))
> + __builtin_abort ();
> + if (foo (2) != 3 * (1 + 10 + 20 + 30 + 3 * 42))
> + __builtin_abort ();
> + if (bar (0) != 10)
> + __builtin_abort ();
> + if (bar (1) != 10 + 20 + 30)
> + __builtin_abort ();
> + if (bar (2) != 10 + 20 + 30 + 3 * 42)
> + __builtin_abort ();
> +}
>
> Jakub
>