From 2d388c94090c670d6c95b35e2ff4d615803b2428 Mon Sep 17 00:00:00 2001 From: Wyatt Childers Date: Tue, 6 Oct 2026 20:48:54 +0000 Subject: [PATCH 1/2] Implement fix for "if constexpr" incorrectly handling non-brace statements [GH #229] --- src/Changes | 18 ++ src/lexical.c | 266 +++++++++++++++--- .../regressions/.gh-229.rto/default.1.1.txt | 13 + .../regressions/.gh-229.rto/default.2.1.txt | 13 + tests/tests/regressions/gh-229.sft.cpp | 62 ++++ 5 files changed, 333 insertions(+), 39 deletions(-) create mode 100644 tests/tests/regressions/.gh-229.rto/default.1.1.txt create mode 100644 tests/tests/regressions/.gh-229.rto/default.2.1.txt create mode 100644 tests/tests/regressions/gh-229.sft.cpp diff --git a/src/Changes b/src/Changes index ef7a906913..ccc20c62fb 100644 --- a/src/Changes +++ b/src/Changes @@ -1,5 +1,23 @@ Version 7.1 +10/8/26 [GH #229] +Improper handling of "if constexpr" substatements + +Previously, when a substatement of an "if constexpr" was discarded during +instantiation and was not enclosed in braces, statements following the +discarded statement could additionally be discarded, e.g.: + + template constexpr int f() { + return [] { + if constexpr (B) while (false) {} + return 1; // Previously erroneously discarded when B was false. + return 2; + }(); + } + +This is now fixed. + + 10/7/26 [GH #176] C-generating back end: always_inline attribute and inlined setjmp calls diff --git a/src/lexical.c b/src/lexical.c index 837b907458..1deb343f15 100644 --- a/src/lexical.c +++ b/src/lexical.c @@ -19599,56 +19599,244 @@ when suppress_warning is TRUE. } /* flush_to_end_of_source */ -void flush_if_or_else_statement(void) +static INLINE void flush_statement_header() /* -Flush tokens of a dependent statement of an "if" or "else" statement. -The current token is the token after the "if" or "else" keyword. +Flush the parenthesized header of a statement such as "while", "for", +"switch" or "catch". The current token is the token after the keyword (and +after any "co_await"), which should be the opening parenthesis. On return, +the current token is the token following the closing parenthesis (e.g., the +start of the controlled substatement). + +Note: The caller is responsible for having set flushing_tokens to TRUE. */ { - /* Special handling is needed for nested "if" statements so that - substatements that are "else if" statements are handled properly. */ - if (curr_token == tok_if) { - (void)get_token(); - if (curr_token == tok_not && next_token() == tok_consteval) { - /* Skip the "not" token only if it is followed by "consteval". */ + if (curr_token == tok_lparen) { + flush_until_matching_token_full(/*limit_flush=*/FALSE); + if (curr_token != tok_end_of_source) { + /* Skip the closing parenthesis. */ (void)get_token(); } /* if */ - if (curr_token == tok_consteval) { + } else { + pos_error(ec_exp_lparen, &error_position); + } /* if */ +} /* flush_statement_header */ + + +static void flush_one_statement() +/* +Flush the tokens of exactly one statement. The current token is the first +token of the statement. + +On return, the current token is the last token of the statement. It is left +unconsumed so that the caller can fetch the next token after leaving the +flushing lexical state, which preserves pragma state (see +flush_if_or_else_statement). + +Note: The caller is responsible for having set flushing_tokens to TRUE. +*/ +{ + /* Note this routine is recursive because many statements have substatements, + e.g., "while (a) while (b) { }" is a single statement. */ + /* Skip a leading attribute-specifier-seq. */ + while (curr_token == tok_lbracket && next_token() == tok_lbracket) { + flush_until_matching_token_full(/*limit_flush=*/FALSE); + (void)get_token(); + } /* while */ + /* Skip the "template" of a C++26 "template for" expansion statement. The + rest of the statement is scanned like an ordinary "for" statement. */ + if (curr_token == tok_template && next_token() == tok_for) { + (void)get_token(); + } /* if */ + switch (curr_token) { + case tok_lbrace: + /* Compound statement. */ + flush_until_matching_token_full(/*limit_flush=*/FALSE); + break; + case tok_if: + /* Special handling is needed for nested "if" statements so that + substatements that are "else if" statements are handled properly. */ (void)get_token(); - } else { - if (curr_token == tok_constexpr) (void)get_token(); - if (curr_token == tok_lparen) { - flush_until_matching_token_full(/*limit_flush=*/FALSE); + if (curr_token == tok_not && next_token() == tok_consteval) { + /* Skip the "not" token only if it is followed by "consteval". */ + (void)get_token(); + } /* if */ + if (curr_token == tok_consteval) { (void)get_token(); } else { - pos_error(ec_exp_lparen, &error_position); + if (curr_token == tok_constexpr) (void)get_token(); + if (curr_token == tok_lparen) { + flush_until_matching_token_full(/*limit_flush=*/FALSE); + (void)get_token(); + } else { + pos_error(ec_exp_lparen, &error_position); + } /* if */ } /* if */ - } /* if */ - flush_if_or_else_statement(); - if (curr_token == tok_else) { + flush_one_statement(); + if (next_token() == tok_else) { + /* Consume the final token of the statement skipped by + flush_one_statement. */ + (void)get_token(); + /* Consume the "else". */ + (void)get_token(); + flush_one_statement(); + } /* if */ + break; + case tok_while: + case tok_switch: +#if MICROSOFT_EXTENSIONS_ALLOWED + case tok_for_each: +#endif /* MICROSOFT_EXTENSIONS_ALLOWED */ + /* The controlled substatement is a statement in its own right. */ (void)get_token(); - flush_if_or_else_statement(); - } /* if */ - } else { - push_lexical_state_stack(); - curr_lexical_state_stack_entry->flushing_tokens = TRUE; - /* If we found a left brace, skip the compound statement. */ - if (curr_token == tok_lbrace) { - flush_until_matching_token_full(/*limit_flush=*/FALSE); - } else { - /* Initialize a local stop token set. Also stop on right brace and end - of source for error cases. */ - a_token_set_array stop_tokens; - clear_token_set_array(stop_tokens); - incr_token_set_array_element(stop_tokens, tok_end_of_source); - incr_token_set_array_element(stop_tokens, tok_rbrace); - incr_token_set_array_element(stop_tokens, tok_semicolon); - flush_tokens_with_stop_tokens_and_warning_flag(stop_tokens, + flush_statement_header(); + flush_one_statement(); + break; +#if UPC_EXTENSIONS_ALLOWED + case tok_upc_forall: + /* The upc_forall statement is similar to the standard for statement. */ + FALLTHROUGH +#endif /* UPC_EXTENSIONS_ALLOWED */ + case tok_for: + /* For statement and range-based for statement, which may have a + "co_await" between the "for" and the opening parenthesis. */ + (void)get_token(); + if (curr_token == tok_coroutine_await) (void)get_token(); + flush_statement_header(); + flush_one_statement(); + break; + case tok_do: + /* "do statement while ( expression ) ;" */ + (void)get_token(); + flush_one_statement(); + if (next_token() == tok_while) { + /* Consume the final token of the statement skipped by + flush_one_statement. */ + (void)get_token(); + /* Consume the "while". */ + (void)get_token(); + /* Leaves the current token on the ";" following the ")". */ + flush_statement_header(); + } /* if */ + break; + case tok_try: + /* "try compound-statement handler-seq" */ + (void)get_token(); + if (curr_token == tok_lbrace) { + flush_until_matching_token_full(/*limit_flush=*/FALSE); + } /* if */ + while (next_token() == tok_catch) { + /* Consume the final token of the preceding compound statement (the + "try" block or the previous handler). */ + (void)get_token(); + /* Consume the "catch". */ + (void)get_token(); + flush_statement_header(); + if (curr_token == tok_lbrace) { + flush_until_matching_token_full(/*limit_flush=*/FALSE); + } /* if */ + } /* while */ + break; +#if MICROSOFT_EXTENSIONS_ALLOWED + case tok_microsoft_try: + /* "__try compound-statement __except ( expression ) compound-statement" + or "__try compound-statement __finally compound-statement". */ + (void)get_token(); + if (curr_token == tok_lbrace) { + flush_until_matching_token_full(/*limit_flush=*/FALSE); + } /* if */ + if (next_token() == tok_except) { + /* Consume the final token of the "__try" compound statement. */ + (void)get_token(); + /* Consume the "__except". */ + (void)get_token(); + flush_statement_header(); + } else if (next_token() == tok_identifier) { + /* Consume the final token of the "__try" compound statement. */ + (void)get_token(); + /* Consume the "__finally". */ + (void)get_token(); + } /* if */ + if (curr_token == tok_lbrace) { + flush_until_matching_token_full(/*limit_flush=*/FALSE); + } /* if */ + break; +#endif /* MICROSOFT_EXTENSIONS_ALLOWED */ + case tok_case: { + /* A case label labels the statement that follows it. Skip to the + ":", allowing for conditional expressions in the case value. */ + unsigned long quest_mark_count = 0; + (void)get_token(); + while (curr_token != tok_end_of_source && curr_token != tok_semicolon && + curr_token != tok_rbrace && + (curr_token != tok_colon || quest_mark_count != 0)) { + if (curr_token == tok_quest_mark) { + quest_mark_count++; + } else if (curr_token == tok_colon) { + quest_mark_count--; + } else if (curr_token == tok_lparen || curr_token == tok_lbracket || + curr_token == tok_lbrace) { + flush_until_matching_token_full(/*limit_flush=*/FALSE); + } /* if */ + (void)get_token(); + } /* while */ + if (curr_token == tok_colon) { + (void)get_token(); + flush_one_statement(); + } /* if */ + break; + } + case tok_default: + /* "default :" labels the statement that follows it. */ + if (next_token() == tok_colon) { + (void)get_token(); + (void)get_token(); + flush_one_statement(); + } /* if */ + break; + case tok_identifier: + if (next_token() == tok_colon) { + /* A label definition labels the statement that follows it. */ + (void)get_token(); + (void)get_token(); + flush_one_statement(); + break; + } /* if */ + /* Otherwise this is an expression or declaration statement. */ + FALLTHROUGH + default: + { /* An expression statement, declaration, jump statement, empty + statement, etc. Such statements end at the first ";" that isn't + nested within parentheses, brackets or braces. Initialize a local + stop token set. Also stop on right brace and end of source for + error cases. */ + a_token_set_array stop_tokens; + clear_token_set_array(stop_tokens); + incr_token_set_array_element(stop_tokens, tok_end_of_source); + incr_token_set_array_element(stop_tokens, tok_rbrace); + incr_token_set_array_element(stop_tokens, tok_semicolon); + flush_tokens_with_stop_tokens_and_warning_flag( + stop_tokens, /*suppress_warning=*/TRUE); - } /* if */ - pop_lexical_state_stack(); - if (curr_token != tok_end_of_source) (void)get_token(); - } /* if */ + } + break; + } /* switch */ +} /* flush_one_statement */ + + +void flush_if_or_else_statement(void) +/* +Flush tokens of a dependent statement of an "if" or "else" statement. +The current token is the token after the "if" or "else" keyword. Exactly one +statement is flushed, so for an unbraced dependent statement such as +"if constexpr (c) while (a) while (b) { } return;", the "return" is not +flushed. On return the current token is the first token after the statement. +*/ +{ + push_lexical_state_stack(); + curr_lexical_state_stack_entry->flushing_tokens = TRUE; + flush_one_statement(); + pop_lexical_state_stack(); + if (curr_token != tok_end_of_source) (void)get_token(); } /* flush_if_or_else_statement */ diff --git a/tests/tests/regressions/.gh-229.rto/default.1.1.txt b/tests/tests/regressions/.gh-229.rto/default.1.1.txt new file mode 100644 index 0000000000..24633ea5ec --- /dev/null +++ b/tests/tests/regressions/.gh-229.rto/default.1.1.txt @@ -0,0 +1,13 @@ +cpfe -DTEST_NUMBER=1 --c++17 Test_name.c +"Test_name.c", line 5: warning: statement is unreachable + return [] { if constexpr (B) while (false) {} return 1; return 2; }(); + ^ + +"Test_name.c", line 38: warning: label "L" was declared but never referenced + if constexpr (B) L: while (B) { } + ^ + +"Test_name.c", line 40: warning: label "x" was declared but never referenced + if constexpr (B) x: if (B) while (B) {} else for (;;) {} + ^ + diff --git a/tests/tests/regressions/.gh-229.rto/default.2.1.txt b/tests/tests/regressions/.gh-229.rto/default.2.1.txt new file mode 100644 index 0000000000..a53b8e526b --- /dev/null +++ b/tests/tests/regressions/.gh-229.rto/default.2.1.txt @@ -0,0 +1,13 @@ +cpfe -DTEST_NUMBER=2 --c++17 --gnu_version 130300 Test_name.c +"Test_name.c", line 5: warning: statement is unreachable + return [] { if constexpr (B) while (false) {} return 1; return 2; }(); + ^ + +"Test_name.c", line 38: warning: label "L" was declared but never referenced + if constexpr (B) L: while (B) { } + ^ + +"Test_name.c", line 40: warning: label "x" was declared but never referenced + if constexpr (B) x: if (B) while (B) {} else for (;;) {} + ^ + diff --git a/tests/tests/regressions/gh-229.sft.cpp b/tests/tests/regressions/gh-229.sft.cpp new file mode 100644 index 0000000000..840a1bc0bb --- /dev/null +++ b/tests/tests/regressions/gh-229.sft.cpp @@ -0,0 +1,62 @@ +//options::--gnu_version 130300 +//options_all:--c++17 + +template constexpr int h() { + return [] { if constexpr (B) while (false) {} return 1; return 2; }(); +} + +static_assert(h() == 1, "h() must be 1"); + +// Nested unbraced substatements: only one statement may be discarded. +template constexpr int nested() { + return [] { + if constexpr (B) while (B) while (!B) { }; + return 1; + }(); +} +static_assert(nested() == 1, "nested"); + +int arr[2]; + +template constexpr int others() { + return [] { + int n = 0; + if constexpr (B) for (;;) {} + n += 1; + if constexpr (B) for (int i : arr) for (int j : arr) { } + n += 1; +#ifdef __cpp_expansion_statements + if constexpr (B) template for (auto x : arr) { } +#endif + n += 1; + if constexpr (B) do {} while (0); + n += 1; + if constexpr (B) switch (n) { case 1: break; } + n += 1; + if constexpr (B) try {} catch (int) {} catch (...) {} + n += 1; + if constexpr (B) L: while (B) { } + n += 1; + if constexpr (B) x: if (B) while (B) {} else for (;;) {} + n += 1; + if constexpr (B) [] {}(); + n += 1; + if constexpr (B) ; else while (B) {} + n += 1; + // Expression statements containing lambdas: everything after the + // lambda's closing brace must also be discarded. + if constexpr (B) n = [] { return 100; }() + 1000, n = 999999; + n += 1; + if constexpr (B) n = [&] { if (n) { n = 5; } return n; }() * 7 + + [] { return 1; }() + arr[0], n = 999999; + n += 1; + if constexpr (B) auto l = [] { return 1; }, m = [] { return 2; }, + o = (n = 999999); + n += 1; + if constexpr (!B) n += 1; else n = [] { return 100; }() + 1000, + n = 999999; + if constexpr (B) n = 100; else while (B) {} + return n; + }(); +} +static_assert(others() == 14, "others"); From 5f15e516c70001509adf110a930985308b6a34fc Mon Sep 17 00:00:00 2001 From: Wyatt Childers Date: Thu, 8 Oct 2026 12:13:59 -0400 Subject: [PATCH 2/2] Fixes for Changes entry formatting issues [] --- src/Changes | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Changes b/src/Changes index ccc20c62fb..1801c97b15 100644 --- a/src/Changes +++ b/src/Changes @@ -44,6 +44,8 @@ For example, with --g++: This is now fixed: When GCC is the back end compiler, such calls are now not inlined into a routine that has the always_inline attribute. + + 10/7/26 [GH #175] C-generating back end: always_inline routines defined in several objects @@ -133,6 +135,7 @@ behavior standard-mandated. It is now the default behavior, except in Microsoft mode with microsoft_version < 1951. +------------------------------------------------------------------------------- Version 7.0, September 28, 2026 9/15/26 [EDGcpfe/28958] @@ -1686,6 +1689,7 @@ For example, with --c++11: C<{}> c0; // Previously a spurious error, now okay. +------------------------------------------------------------------------------- Version 6.9, July 20, 2026 7/16/26 [EDGcpfe/28936]