diff options
author | Iain Sandoe <iain@sandoe.co.uk> | 2024-06-15 17:47:33 +0100 |
---|---|---|
committer | Iain Sandoe <iains@gcc.gnu.org> | 2024-07-16 16:58:52 +0100 |
commit | d1706235ed2b274a2d1fa3c3039b5874b4ae7a0e (patch) | |
tree | 375aef82318f1248418042d9c6b6eae346128d22 /gcc/cp/contracts.cc | |
parent | f8b302c98378b54e09c5f20cd6f6197871311da2 (diff) | |
download | gcc-d1706235ed2b274a2d1fa3c3039b5874b4ae7a0e.zip gcc-d1706235ed2b274a2d1fa3c3039b5874b4ae7a0e.tar.gz gcc-d1706235ed2b274a2d1fa3c3039b5874b4ae7a0e.tar.bz2 |
c++, coroutines, contracts: Handle coroutine and void functions [PR110871,PR110872,PR115434].
The current implementation of contracts emits the checks into function
bodies in three places; for pre-conditions at the start of the body,
for asserts in-line in the function body and for post-conditions as an
addition to return statements.
In general (at least with existing "2a" contract semantics) the in-line
contract asserts behave as expected.
However, the mechanism is not applicable to:
* Handling pre conditions in coroutines since, for those, the standard
specifies a wrapping of the original function body by functionality
implementing initial and final suspends (along with some housekeeping
to route exceptions). Thus for such transformed function bodies, the
preconditions then get actioned after the initial suspend, which does
not behave as intended.
* Handling post conditions in functions that do not have return
statements (which applies to coroutines and void functions).
In the following, we identify a potentially transformed function body
(in the case of coroutines, this is usually called the "ramp()" function).
The patch here re-implements the code insertion in one of the two
following ways (code for exposition only):
* For functions with no post-conditions we wrap the potentially
transformed function as follows:
{
handle_pre_condition_checking ();
potentially_transformed_function_body ();
}
This implements the intent that the preconditions are processed after
the function parameters are initialised but before any other actions.
* For functions with post-conditions:
if (preconditions_exist)
handle_pre_condition_checking ();
try
{
potentially_transformed_function_body ();
}
finally
{
handle_post_condition_checking ();
}
else [only if the function is not marked noexcept(true) ]
{
;
}
In this, post-conditions [that might apply to the return value etc.]
are evaluated on every non-exceptional edge out of the function.
At present, the model here is that exceptions thrown by the function
propagate upwards as if there were no contracts present. If the desired
semantic becomes that an exception is counted as equivalent to a contract
violation - then we can add a second handler in place of the empty
statement.
This patch specifically does not address changes to code-gen and constexpr
handling that are contained in P2900.
PR c++/115434
PR c++/110871
PR c++/110872
gcc/cp/ChangeLog:
* constexpr.cc (cxx_eval_constant_expression): Handle EH_ELSE_EXPR.
* contracts.cc (finish_contract_attribute): Remove excess line.
(build_contract_condition_function): Post condition handlers are
void now.
(emit_postconditions_cleanup): Remove.
(emit_postconditions): New.
(add_pre_condition_fn_call): New.
(add_post_condition_fn_call): New.
(apply_preconditions): New.
(apply_postconditions): New.
(maybe_apply_function_contracts): New.
(apply_postcondition_to_return): Remove.
* contracts.h (apply_postcondition_to_return): Remove.
(maybe_apply_function_contracts): Add.
* coroutines.cc (coro_build_actor_or_destroy_function): Do not
copy contracts to coroutine helpers.
* decl.cc (finish_function): Handle wrapping a possibly
transformed function body in contract checks.
* typeck.cc (check_return_expr): Remove handling of post
conditions on return expressions.
gcc/ChangeLog:
* gimplify.cc (struct gimplify_ctx): Add a flag to show we are
expending a handler.
(gimplify_expr): When we are expanding a handler, and the body
transforms might have re-written DECL_RESULT into a gimple var,
ensure that hander references to DECL_RESULT are also re-written
to refer to the gimple var. When we are processing an EH_ELSE
expression, then add it if either of the cleanup slots is in
use.
gcc/testsuite/ChangeLog:
* g++.dg/contracts/pr115434.C: New test.
* g++.dg/coroutines/pr110871.C: New test.
* g++.dg/coroutines/pr110872.C: New test.
Signed-off-by: Iain Sandoe <iain@sandoe.co.uk>
Diffstat (limited to 'gcc/cp/contracts.cc')
-rw-r--r-- | gcc/cp/contracts.cc | 257 |
1 files changed, 159 insertions, 98 deletions
diff --git a/gcc/cp/contracts.cc b/gcc/cp/contracts.cc index a7d0fda..39f0487 100644 --- a/gcc/cp/contracts.cc +++ b/gcc/cp/contracts.cc @@ -88,64 +88,47 @@ along with GCC; see the file COPYING3. If not see return -v; } - The original decl is left alone and instead calls are generated to pre/post - functions within the body: + We implement the checking as follows: - void fun.pre(int v) - { - [[ assert: v > 0 ]]; - } - int fun.post(int v, int __r) + For functions with no post-conditions we wrap the original function body as + follows: + + { + handle_pre_condition_checking (); + original_function_body (); + } + + This implements the intent that the preconditions are processed after the + function parameters are initialised but before any other actions. + + For functions with post-conditions: + + if (preconditions_exist) + handle_pre_condition_checking (); + try { - [[ assert: __r < 0 ]]; - return __r; + original_function_body (); } - int fun(int v) + finally { - fun.pre(v); - return fun.post(v, -v); + handle_post_condition_checking (); } - - If fun returns in memory, the return value is not passed through the post - function; instead, the return object is initialized directly and then passed - to the post function by invisible reference. - - This sides steps a number of issues with having to rewrite the bodies or - rewrite the parsed conditions as the parameters to the original function - changes (as happens during redeclaration). The ultimate goal is to get - something that optimizes well along the lines of - - int fun(int v) + else [only if the function is not marked noexcept(true) ] { - [[ assert: v > 0 ]]; - auto &&__r = -v; - goto out; - out: - [[ assert: __r < 0 ]]; - return __r; + ; } - With the idea being that multiple return statements could collapse the - function epilogue after inlining the pre/post functions. clang is able - to collapse common function epilogues, while gcc needs -O3 -Os combined. - - Directly laying the pre contracts down in the function body doesn't have - many issues. The post contracts may need to be repeated multiple times, once - for each return, or a goto epilogue would need to be generated. - For this initial implementation, generating function calls and letting - later optimizations decide whether to inline and duplicate the actual - checks or whether to collapse the shared epilogue was chosen. - - For cdtors a post contract is implemented using a CLEANUP_STMT. + In this, post-conditions [that might apply to the return value etc.] are + evaluated on every non-exceptional edge out of the function. - FIXME the compiler already shores cleanup code on multiple exit paths, so - this outlining seems unnecessary if we represent the postcondition as a - cleanup for all functions. + FIXME outlining contract checks into separate functions was motivated + partly by wanting to call the postcondition function at each return + statement, which we no longer do; at this point outlining doesn't seem to + have any advantage over emitting the contracts directly in the function + body. More helpful for optimization might be to make the contracts a wrapper - function (for non-variadic functions), that could be inlined into a - caller while preserving the call to the actual function? Either that or - mirror a never-continue post contract with an assume in the caller. */ + function that could be inlined into the caller, the callee, or both. */ #include "config.h" #include "system.h" @@ -808,7 +791,6 @@ finish_contract_attribute (tree identifier, tree contract) tree attribute = build_tree_list (build_tree_list (NULL_TREE, identifier), build_tree_list (NULL_TREE, contract)); - /* Mark the attribute as dependent if the condition is dependent. TODO: I'm not sure this is strictly necessary. It's going to be marked as @@ -1451,10 +1433,8 @@ build_contract_condition_function (tree fndecl, bool pre) *last = build_tree_list (NULL_TREE, value_type); TREE_CHAIN (*last) = void_list_node; - if (aggregate_value_p (value_type, fndecl)) - /* If FNDECL returns in memory, don't return the value from the - postcondition. */ - value_type = void_type_node; + /* The handler is a void return. */ + value_type = void_type_node; } TREE_TYPE (fn) = build_function_type (value_type, arg_types); @@ -1892,12 +1872,9 @@ emit_preconditions (tree attr) /* Emit statements for postcondition attributes. */ static void -emit_postconditions_cleanup (tree contracts) +emit_postconditions (tree attr) { - tree stmts = push_stmt_list (); - emit_contract_conditions (contracts, POSTCONDITION_STMT); - stmts = pop_stmt_list (stmts); - push_cleanup (NULL_TREE, stmts, /*eh_only*/false); + return emit_contract_conditions (attr, POSTCONDITION_STMT); } /* We're compiling the pre/postcondition function CONDFN; remap any FN @@ -2000,30 +1977,142 @@ start_function_contracts (tree decl1) if (!handle_contracts_p (decl1)) return; + /* For cdtors, we evaluate the contracts check inline. */ if (!outline_contracts_p (decl1)) - { - emit_preconditions (DECL_CONTRACTS (current_function_decl)); - emit_postconditions_cleanup (DECL_CONTRACTS (current_function_decl)); - return; - } + return; /* Contracts may have just been added without a chance to parse them, though we still need the PRE_FN available to generate a call to it. */ if (!DECL_PRE_FN (decl1)) build_contract_function_decls (decl1); +} + +/* If we have a precondition function and it's valid, call it. */ + +static void +add_pre_condition_fn_call (tree fndecl) +{ /* If we're starting a guarded function with valid contracts, we need to insert a call to the pre function. */ - if (DECL_PRE_FN (decl1) - && DECL_PRE_FN (decl1) != error_mark_node) + gcc_checking_assert (DECL_PRE_FN (fndecl) + && DECL_PRE_FN (fndecl) != error_mark_node); + + releasing_vec args = build_arg_list (fndecl); + tree call = build_call_a (DECL_PRE_FN (fndecl), args->length (), + args->address ()); + CALL_FROM_THUNK_P (call) = true; + finish_expr_stmt (call); +} + +/* Build and add a call to the post-condition checking function, when that + is in use. */ + +static void +add_post_condition_fn_call (tree fndecl) +{ + gcc_checking_assert (DECL_POST_FN (fndecl) + && DECL_POST_FN (fndecl) != error_mark_node); + + releasing_vec args = build_arg_list (fndecl); + if (get_postcondition_result_parameter (fndecl)) + vec_safe_push (args, DECL_RESULT (fndecl)); + tree call = build_call_a (DECL_POST_FN (fndecl), args->length (), + args->address ()); + CALL_FROM_THUNK_P (call) = true; + finish_expr_stmt (call); +} + +/* Add a call or a direct evaluation of the pre checks. */ + +static void +apply_preconditions (tree fndecl) +{ + if (outline_contracts_p (fndecl)) + add_pre_condition_fn_call (fndecl); + else + emit_preconditions (DECL_CONTRACTS (fndecl)); +} + +/* Add a call or a direct evaluation of the post checks. */ + +static void +apply_postconditions (tree fndecl) +{ + if (outline_contracts_p (fndecl)) + add_post_condition_fn_call (fndecl); + else + emit_postconditions (DECL_CONTRACTS (fndecl)); +} + +/* Add contract handling to the function in FNDECL. + + When we have only pre-conditions, this simply prepends a call (or a direct + evaluation, for cdtors) to the existing function body. + + When we have post conditions we build a try-finally block. + If the function might throw then the handler in the try-finally is an + EH_ELSE expression, where the post condition check is applied to the + non-exceptional path, and an empty statement is added to the EH path. If + the function has a non-throwing eh spec, then the handler is simply the + post-condition checker. */ + +void +maybe_apply_function_contracts (tree fndecl) +{ + if (!handle_contracts_p (fndecl)) + /* We did nothing and the original function body statement list will be + popped by our caller. */ + return; + + bool do_pre = has_active_preconditions (fndecl); + bool do_post = has_active_postconditions (fndecl); + /* We should not have reached here with nothing to do... */ + gcc_checking_assert (do_pre || do_post); + + /* This copies the approach used for function try blocks. */ + tree fnbody = pop_stmt_list (DECL_SAVED_TREE (fndecl)); + DECL_SAVED_TREE (fndecl) = push_stmt_list (); + tree compound_stmt = begin_compound_stmt (0); + current_binding_level->artificial = 1; + + /* Do not add locations for the synthesised code. */ + location_t loc = UNKNOWN_LOCATION; + + /* For other cases, we call a function to process the check. */ + + /* If we have a pre, but not a post, then just emit that and we are done. */ + if (!do_post) + { + apply_preconditions (fndecl); + add_stmt (fnbody); + finish_compound_stmt (compound_stmt); + return; + } + + if (do_pre) + /* Add a precondition call, if we have one. */ + apply_preconditions (fndecl); + tree try_fin = build_stmt (loc, TRY_FINALLY_EXPR, fnbody, NULL_TREE); + add_stmt (try_fin); + TREE_OPERAND (try_fin, 1) = push_stmt_list (); + /* If we have exceptions, and a function that might throw, then add + an EH_ELSE clause that allows the exception to propagate upwards + without encountering the post-condition checks. */ + if (flag_exceptions && !type_noexcept_p (TREE_TYPE (fndecl))) { - releasing_vec args = build_arg_list (decl1); - tree call = build_call_a (DECL_PRE_FN (decl1), - args->length (), - args->address ()); - CALL_FROM_THUNK_P (call) = true; - finish_expr_stmt (call); + tree eh_else = build_stmt (loc, EH_ELSE_EXPR, NULL_TREE, NULL_TREE); + add_stmt (eh_else); + TREE_OPERAND (eh_else, 0) = push_stmt_list (); + apply_postconditions (fndecl); + TREE_OPERAND (eh_else, 0) = pop_stmt_list (TREE_OPERAND (eh_else, 0)); + TREE_OPERAND (eh_else, 1) = build_empty_stmt (loc); } + else + apply_postconditions (fndecl); + TREE_OPERAND (try_fin, 1) = pop_stmt_list (TREE_OPERAND (try_fin, 1)); + finish_compound_stmt (compound_stmt); + /* The DECL_SAVED_TREE stmt list will be popped by our caller. */ } /* Finish up the pre & post function definitions for a guarded FNDECL, @@ -2078,34 +2167,6 @@ finish_function_contracts (tree fndecl) } } -/* Rewrite the expression of a returned expression so that it invokes the - postcondition function as needed. */ - -tree -apply_postcondition_to_return (tree expr) -{ - tree fn = current_function_decl; - tree post = DECL_POST_FN (fn); - if (!post) - return NULL_TREE; - - /* If FN returns in memory, POST has a void return type and we call it when - EXPR is DECL_RESULT (fn). If FN returns a scalar, POST has the same - return type and we call it when EXPR is the value being returned. */ - if (VOID_TYPE_P (TREE_TYPE (TREE_TYPE (post))) - != (expr == DECL_RESULT (fn))) - return NULL_TREE; - - releasing_vec args = build_arg_list (fn); - if (get_postcondition_result_parameter (fn)) - vec_safe_push (args, expr); - tree call = build_call_a (post, - args->length (), - args->address ()); - CALL_FROM_THUNK_P (call) = true; - - return call; -} /* A subroutine of duplicate_decls. Diagnose issues in the redeclaration of guarded functions. */ |