On 1/19/26 10:57 PM, Iain Sandoe wrote:
Hi Jason,

Thanks for the review.

The attached patch will be part of the V3 series (which needs work to
patch 8 to complete).

The current series (including all review changes, completed to date) is
here:
  
https://forge.sourceware.org/iains/gcc-TEST/commits/branch/C%2B%2B26-contracts-base

The individual commits for fixes to this specific review are here:

https://forge.sourceware.org/iains/gcc-TEST/commits/branch/C%2B%2B26-contracts-base

(the branches are also on GH if that is easier to access)
https://github.com/iains/gcc-git/tree/C%2B%2B26-contracts-base
https://github.com/iains/gcc-git/tree/contracts_v2_patch_3_review

I believe that I’ve addressed the merge blockers here and also many of the 
other points
including the typographical and comment ones.

This work has been rebased onto r16-6881-gddda478641ffcc, which includes the
reflection commits and the annual changes to (c).
how does it look now?
thanks
Iain

PS. We expect to post the complete V3 series in the next few days, after 
addressing
recent review comments.

On 16 Jan 2026, at 16:16, Jason Merrill <[email protected]> wrote:

On 12/1/25 8:50 PM, Iain Sandoe wrote:

   above.  A future improvement planned here is to store the specifiers using a
   tree vec instead of the attribute list.

So you removed the attributes from the decls, but kept the TREE_LIST format?  
Interesting.

Until the discussions in Porto, we were expecting to continue to have cxx2a 
contracts
still active; however having contract specifiers in the attributes was very 
unhelpful since
the semantics of merging the attributes and the contracts duplication checks 
are quite
different.   So we had split the specifiers out, but since all the existing 
code was expecting
an attribute list, we kept that.

Similarly, as we also discussed, we have re-used code from the cxx2a 
implementation
that handled the various re-mappings of contract conditions (some of which we 
all agree
is probably overly-complex and possibly obtuse for what it is doing) relevant 
to a couple
of replies to later parts of this review.

Between Porto, things to prepare for WG21 and the end of stage #1 there was not
time to update all the interfaces or to replace the inherited code.

However, these elements are self-contained and local to the contracts 
implementation
so it seems to me they could be improved once the code is in trunk (obviously 
the
inherited code is already in trunk).

Agreed.

+  { "contract_assert", RID_CONTASSERT, D_CXXONLY | D_CXXWARN }, // removed 
D_CXX20 in order for contracts to work out of the box

This breaks valid C++98-23 code that uses contract_assert as an identifier.

wa.C:3:8: error: expected unqualified-id before ‘contract_assert’
    3 |   auto contract_assert = 42;

This must be fixed before merge.

Done, also added logic for the Wc++26-compat and fixed up with 
warn_keyword_macro.

Perhaps (at some point) we should add a ‘__contract_assert’ that is generally 
available?
I would see no reason for contracts not to work back to at least C++17 (we might
want to consider if there are any implications to postconditions in the absence 
of
guaranteed copy elision if we wanted to go earlier).

Makes sense.

   /* These builtins shall be ignored during constant expression
     evaluation.  */
   return void_node;
+      case BUILT_IN_OBSERVABLE_CHKPT:
+ if (ctx->manifestly_const_eval == mce_true)

Why only when manifestly constant-evaluated?  Shouldn't we be able to 
speculatively constant-evaluate a call to function that contains a checkpoint?

Fixed (i.e. observable checkpoints are now also ignored).

@@ -10175,7 +10180,18 @@ cxx_eval_constant_expression (const constexpr_ctx 
*ctx, tree t,
     case ASSERTION_STMT:
     case PRECONDITION_STMT:
     case POSTCONDITION_STMT:
-      gcc_checking_assert (false && "hoo...");
+      {
+       if (contract_ignored_p (t))
+         break;
+
+       if (!cxx_eval_assert (ctx, CONTRACT_CONDITION (t),
+                             G_("contract predicate is false in "
+                                "constant expression"),
+                             EXPR_LOCATION (t), contract_evaluated_p (t),
+                             non_constant_p, overflow_p))
+         *non_constant_p = true;
+       r = void_node;

I thought the agreement was that contracts don't affect whether something is a 
constant-expression, that if evaluating a contract ends up being non-constant, 
that's a hard error if the evaluation is otherwise constant.

yes… I see - that would be:

https://eel.is/c++draft/basic.contract#eval-7.3

  So we need to track non-constancy of this evaluation separately from the 
normal non_constant_p (perhaps in constexpr_global_ctx) and check it in 
_outermost_ to give an error before returning a constant value.

I see what you mean …

We also have
https://eel.is/c++draft/basic.contract#eval-8
https://eel.is/c++draft/intro.compliance#general-2.3.4

Which means I think we need to stash
  - if it failed 7.3
  - if it passed 7.3 but evaluated false
  - the semantic in force
  - the location.
  (or maybe some higher-level thing that allows us to retrieve these).

Done.

Great.

+      }
       break;
      case TEMPLATE_ID_EXPR:
@@ -12519,7 +12524,9 @@ potential_constant_expression_1 (tree t, bool 
want_rval, bool strict, bool now,
      case ASSERTION_STMT:
      case PRECONDITION_STMT:
      case POSTCONDITION_STMT:
-      gcc_checking_assert (false && "hmm...");
+      if (!contract_evaluated_p (t))
+ return true;
+      return RECUR (CONTRACT_CONDITION (t), rval);

Similarly, I would expect this to always return true.

Yes, we must assume that it would not affect the outcome at this point.

+      insert_decl_map (&id, sp, dp);
+      do_remap = true;
+
+      /* First artificial arg is *this. We want to remap that. However, we
+ want to skip _in_charge param and __vtt_parm. Do so now.  */

Why do we want to skip them?

This is cxx2a inherited code, as noted in Porto some of it seems to be overly
sophisticated for what we need to do.

+/* Returns an invented parameter declaration of the form 'TYPE ID' for the
+   purpose of parsing the postcondition.
+
+   We use a PARM_DECL instead of a VAR_DECL so that tsubst forces a lookup
+   in local specializations when we instantiate these things later.  */

??? tsubst_expr does retrieve_local_specialization for VAR_DECL, too. The 
difference is that if that returns null, we then build a new PARM_DECL, but 
only in unevaluated context, which a contract specifier shouldn't be...except 
that you also add processing_postcondition, which seems like a big kludge.  And 
seems like it shouldn't be necessary since you register_local_specialization in 
rebuild_postconditions.  If we need to register_local_specialization elsewhere 
(say, tsubst_contract), let's do that and lose processing_postcondition.

Umm .. inherited code ...

This comment is inherited, but processing_postcondition is not.

+      /* Always update the context of the result variable so that it can
+ be remapped by remap_contracts.  */
+      DECL_CONTEXT (oldvar) = fndecl;

When would it not have the right context?

Much of the time it will be empty, since we do not have a function decl
when we are parsing the contract specifiers.  We set the context here in
‘maybe_rebuild_postconditions` which is called after deferred parsing
or when we come to use the contracts (for free functions).  It is also set
in tsubst for instantiations.

I added:
       gcc_checking_assert (!DECL_CONTEXT (oldvar)
                           || DECL_CONTEXT (oldvar) == fndecl);

But by the time we get to rebuild_postconditions in grokfndecl we've already set DECL_CONTEXT. Deferred parsing is even later. And tsubst_function_decl sets DECL_CONTEXT immediately after tsubsting the parms.

+  bool viol_is_var = false;
+  if (TREE_CONSTANT (ctor))

I wonder about adding && !check_might_throw to avoid the need for 
__tu_has_violation_exception?  How much of a win is avoiding the variable?

I see you retracted this - but … for the record:

As mentioned earlier, the violation object ABI is still somewhat in flux which 
makes
me reluctant to invest in changes just yet.

In tests, I found that the code size increase from the instructions to generate 
the
object on the stack (or parameters for a call to some helper that creates it) 
was
greater than the ro-data size and so it was worth doing this.

Code size growth from contracts was/maybe still is considered an important
issue (more so that performance - since this is the ‘failed’ path).

Agreed.

+    violation = build_contract_violation_constant (ctor, contract, /*is_const*/
+   true);
+  else
+    {
+      violation = build_contract_violation_var (ctor, contract);
+      add_decl_expr (violation);
+      BIND_EXPR_VARS (cc_bind) = violation;
+      viol_is_var = true;
+    }
+  /* So now do we need a try-catch?  */
+  if (check_might_throw)
+    {
+      /* This will hold the computed condition.  */
+      tree check_failed = build_decl (loc, VAR_DECL, NULL, boolean_type_node);
+      DECL_ARTIFICIAL (check_failed) = true;
+      DECL_IGNORED_P (check_failed) = true;
+      DECL_CONTEXT (check_failed) = current_function_decl;
+      layout_decl (check_failed, 0);
+      add_decl_expr (check_failed);
+      DECL_CHAIN (check_failed) = BIND_EXPR_VARS (cc_bind);
+      BIND_EXPR_VARS (cc_bind) = check_failed;
+      /* This will let us check if we had an exception.  */
+      tree no_excp_ = build_decl (loc, VAR_DECL, NULL, boolean_type_node);
+      DECL_ARTIFICIAL (no_excp_) = true;
+      DECL_IGNORED_P (no_excp_) = true;
+      DECL_CONTEXT (no_excp_) = current_function_decl;
+      DECL_INITIAL (no_excp_) = boolean_true_node;
+      layout_decl (no_excp_, 0);
+      add_decl_expr (no_excp_);
+      DECL_CHAIN (no_excp_) = BIND_EXPR_VARS (cc_bind);
+      BIND_EXPR_VARS (cc_bind) = no_excp_;

I don't see why you need two variables:

+      tree check_try = begin_try_block ();
+      finish_expr_stmt (cp_build_init_expr (check_failed, cond));
+      finish_try_block (check_try);
+      tree handler = begin_handler ();
+      finish_handler_parms (NULL_TREE, handler); /* catch (...) */
+      tree e = cp_build_modify_expr (loc, no_excp_, NOP_EXPR, 
boolean_false_node,
+     tf_warning_or_error);
+      finish_expr_stmt (e);
+      tree s_const = build_int_cst (uint16_type_node, semantic);
+      if (viol_is_var)
+ {
+  /* We can update the detection mode here.  */
+  tree memb
+    = lookup_member (builtin_contract_violation_type,
+     get_identifier ("_M_detection_mode"),
+     1, 0, tf_warning_or_error);
+  tree r
+    = build_class_member_access_expr (violation, memb, NULL_TREE, false,
+      tf_warning_or_error);
+  r = cp_build_modify_expr (loc, r, NOP_EXPR,
+ build_int_cst (uint16_type_node, (uint16_t)CDM_EVAL_EXCEPTION),
+ tf_warning_or_error);
+  finish_expr_stmt (r);
+  finish_expr_stmt (build_call_n (__tu_has_violation, 2,
+  build_address (violation),
+  s_const));

...here instead of calling __tu_has_violation you could set check_failed.

+ }
+      else
+ /* We need to make a copy of the violation object to update.  */
+ finish_expr_stmt (build_call_n (__tu_has_violation_exception, 2,
+ build_address (violation),
+ s_const));

...and here after calling __tu_has_violation_exception you could clear 
check_failed.

Oops, no, we need to make the call inside the handler because the standard says 
there's an active handler.


+      finish_handler (handler);
+      finish_handler_sequence (check_try);
+      cond = build2 (TRUTH_ANDIF_EXPR, boolean_type_node, check_failed, 
no_excp_);

...and then just set cond to check_failed.

TODO: on hold until the library ABI is finalised.

Dropping no_excp_ seems like a simple non-ABI change, but I guess it can wait.

+  else if (fn && processing_contract_condition && DECL_DESTRUCTOR_P (fn))
+    error ("invalid use of %<this%> after it is valid");
    else if (fn)
      error ("invalid use of %<this%> in non-member function");
    else
@@ -4674,6 +4688,9 @@ process_outer_var_ref (tree decl, tsubst_flags_t 
complain, bool odr_use)
   }
        return error_mark_node;
      }
+  else if (processing_contract_condition && (TREE_CODE (decl) == PARM_DECL))
+    /* Use of a parameter in a contract condition is fine.  */
+    return decl;

Only if it's a parameter of the current function.
Why does outer_var_p return true in this case?

At this point the PARM does not have DECL_CONTEXT since we don’t yet have the 
function decl.
If we have a PARM_DECL and outer_var_p is true (and we are processing a 
contract condition)
it’s not clear how the parm could belong to anything else, I guess I’m missing 
something?
void f (int i) {
  struct A {
    void g() pre(i);
  };
}

I guess you want to add a check that DECL_CONTEXT is null.

...though maybe outer_var_p should return false for a PARM_DECL with null DECL_CONTEXT.

+/* If we have a successful constant evaluation, now check whether there is
+   a failed or non-constant contract that would invalidate this.  */
+
+static bool
+check_for_failed_contracts (constexpr_global_ctx *global_ctx)
+{
+  if (!flag_contracts || !global_ctx->contract_statement)
+    return false;
+
+  /* [basic.contract.eval]/7.3 */
+  location_t loc = EXPR_LOCATION (global_ctx->contract_statement);
+  if (global_ctx->contract_condition_non_const)
+    {
+      error_at (loc, "contract condition is not constant");
+      return true;
+    }

From your [basic.contract.eval] citation, this also counts as a contract violation, so it also should be an error only for terminating semantic.

+  /* [basic.contract.eval]/8. */
+  if (contract_terminating_p (global_ctx->contract_statement))
+    {
+      error_at (loc, "contract predicate is false in constant expression");
+      return true;
+    }
+  /* [intro.compliance.general]/2.3.4. */
+  warning_at (loc, 0, "contract predicate is false in constant expression");

Instead of separate error/warning calls you could call emit_diagnostic with diagnostics::kind::error/warning.

@@ -3656,6 +3666,10 @@ finish_this_expr (void)
     }
   else if (fn && DECL_STATIC_FUNCTION_P (fn))
     error ("%<this%> is unavailable for static member functions");
+  else if (fn && processing_contract_condition && DECL_CONSTRUCTOR_P (fn))
+    error ("invalid use of %<this%> in a constructor %<pre%> condition");
+  else if (fn && processing_contract_condition && DECL_DESTRUCTOR_P (fn))
+    error ("invalid use of %<this%> a destructor %<post%> condition");

Missing "in"

Jason

Reply via email to