https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127572

            Bug ID: 127572
           Summary: [c++26][contracts] a function-contract-specifier binds
                    to the preceding parameter list rather than to the
                    declarator
           Product: gcc
           Version: 16.2.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: c++
          Assignee: unassigned at gcc dot gnu.org
          Reporter: berne at notadragon dot com
  Target Milestone: ---

A function-contract-specifier-seq follows the complete declarator.  g++
instead allows the specifiers as part of a function type and attaches them to
whatever parameters-and-qualifiers immediately precede them.
In the common case this is fine, but there are cases where invalid
positions are allowed (and the specifiers end up silently ignored) and
cases where the specifiers bind to the wrong parameter list (and assorted
bad things happen).

Concretely: g++ looks for the contract immediately after a parameter list, so
it finds whichever parameter list was written last.  For a function returning
a pointer to function, that is the return type's, not the function's:

  int (*f (int i)) (int) pre (i > 0);
          ^^^^^^^          the function's own parameter list
                   ^^^^^   the one the contract is taken to follow

When those two coincide, which is the ordinary case, the feature works.  The
consequences when they do not fall into two groups, and the difference in
impact between them is large, so I have separated them.  `g` is a
namespace-scope bool.

GROUP 1 -- the declaration is a well-formed function declaration and the
contract belongs to it.  These are correct programs, and the contract on them
is wrongly rejected, silently discarded, or checked against the wrong object:

  int f (int i) pre (i > 0);                    correct
  int (*f (int)) (int) pre (g);                 correct
  auto f (int i) -> char (*) [3] pre (i > 0);   correct
  int (*f (int i)) (int) pre (i > 0);           rejected: 'i' not declared
  char (*f (int i)) [3] pre (i > 0);            rejected: syntax error
  auto f (int) -> int (*) (int) pre (g);        discarded, no diagnostic
  long (*f (int i)) (long i) pre (i > 0);       checks the return type's 'i'

GROUP 2 -- the declarator declares no function, so [dcl.decl.general]/6 says
the contract may not be there at all.  The program is ill-formed and g++
accepts it:

  typedef int F (int) pre (true);               discarded, no diagnostic
  using F = int (int) pre (true);               discarded, no diagnostic
  int (*p) (int) pre (true);                    discarded, no diagnostic
  void h (int bar () pre (true));               discarded, no diagnostic


1.  REJECTS-VALID, group 1 -- valid-declarator-rejected.cpp

All four of these are well-formed and all four fail to compile.

  char (*returns_array     (int i)) [3]   pre (i > 0);  // expected initializer
  char (&returns_array_ref (int i)) [3]   pre (i > 0);  // expected initializer
  int  (*returns_fn        (int i)) (int) pre (i > 0);  // 'i' not declared
  int  (S::*returns_memfn  (int i)) (int) pre (i > 0);  // 'i' not declared

The same four declarators with no contract on them are in the file as
controls, and all four are accepted, so the shapes themselves are spellable
and the contract is what breaks them.

The first two fail because the declarator's outermost part is an array, so
the parser is not looking for a contract there at all.  The second two fail
for a different reason: the contract does reach the function -- the same
spelling with a parameter-free predicate is checked, see reproducer 2 -- but
it is parsed after the function's own parameter scope has been left, so i
does not resolve.  The return type's parameter list is unnamed in both rows;
naming it replaces this error with the silent wrong answer in reproducer 3.

2.  WRONG-CODE, group 1, dropped -- contract-silently-dropped.cpp

This half of group 1 compiles and the contract simply never runs.  All three
declarations below are accepted without a diagnostic; only the first attaches
the contract assertion to the function and evaluates it.

  static bool ok = false;

  int  (*returns_fn_classic   (int))  (int)            pre (ok);
  auto   returns_fn_trailing  (int) -> int (*) (int)   pre (ok);
  auto   returns_ref_trailing (int) -> int (&) (int)   post (r : ok);

Each predicate is false at its call, so three violations should be detected.

  $ g++ -std=c++26 -fcontracts -fcontract-evaluation-semantic=observe \
        -o a.out contract-silently-dropped.cpp && ./a.out
  ... one violation, from returns_fn_classic, then "done"

The two trailing-return spellings are dropped: the parameter list sits
inside the type-id, so the contract binds to an abstract declarator that
never becomes a function.  The classic spelling is the control -- it is the
same function without a trailing return type, and it is checked, which is
what shows the other two should be.  The predicates deliberately name no
parameter, so the defect in the next section cannot show and this file
measures the drop alone.


3.  WRONG-CODE, group 1, wrong scope -- contract-predicate-wrong-scope.cpp

The classic spelling is reported, but it is not correct either -- it is
attached to the right function with its predicate parsed in the wrong scope.
The contract is parsed while the return type's parameter scope is still
open, so a name in the predicate resolves to the return type's parameter
rather than to the function's.

In each declaration the function's own parameter is `int i`, and the return
type long (*) (long) declares its own parameter, also spelled i, of type
long.  Only the int should be in scope.

  long (*which_type (int i)) (long i) pre (same_v<decltype (i), int>);
  long (*which_size (int i)) (long i) pre (sizeof (i) == sizeof (int));

Both report a violation: decltype (i) is long, and sizeof (i) is
sizeof (long).  The name found the return type's parameter declaration.
(same_v is a two-line hand-rolled is_same, so the file needs no
<type_traits>.)

The contract is attached and evaluated, not dropped.  A contradiction, false
for every possible value of i, reports -- which a dropped contract cannot do:

  long (*contradiction (int i)) (long i) pre (i > 0 && i <= 0);

So a predicate that merely reads the value is a live check against an object
that was never created.  At -O0 on x86_64:

  int (*f (int a)) (long i) pre (i > 0);

  _Z1fi:
          movl    %edi, -20(%rbp)     # the real parameter, a
          cmpq    $0, -8(%rbp)        # the predicate's i: never written
          jg      .L4

The orphaned parameter is given its own slot in the enclosing function's
frame and the predicate reads it uninitialised.  That row is not measured in
the reproducer, because the value it reads can differ from run to run; the
three above are deterministic and establish the same thing.

[basic.scope.param]/1.1 gives the function's own parameters a scope reaching
the end of the init-declarator, and the return type's parameter-declaration-
clause is a separate parameter scope that does not extend here at all.


4.  ACCEPTS-INVALID, group 2 -- contract-on-non-function-declarator.cpp

[dcl.decl.general]/6: the function-contract-specifier-seq of an
init-declarator "shall not be present unless the declarator declares a
function", and the note in [dcl.contract.func]/8 says a pointer to function,
a pointer to member function and a function type alias cannot carry one at
all.  Four kinds of declarator take one anyway.  None of them declares a
function, so there is nothing for the contract to belong to and no call is
ever checked.

  typedef int FTypedef (int) pre (true);
  using FAlias = int (int) pre (true);
  void takes_fn (int bar () pre (true));
  void defines_fn (int bar () pre (true)) { (void) bar; }
  int (*gp) (int) pre (true);

  $ g++ -std=c++26 -fcontracts -fsyntax-only \
        contract-on-non-function-declarator.cpp; echo $?
  0

No diagnostic of any kind, for any of them.  In defines_fn the contract sits
on a parameter of a function that is being defined, and is still dropped.

How much of the dropped predicate is looked at depends on the scope, and
inside a class the answer is none of it.  At namespace scope the predicate is
parsed before being discarded, so a meaningless one is still diagnosed:

  typedef int FBadSyntax (int) pre (1 + * / 2);   // expected primary-expr
  typedef int FBadName   (int) pre (nosuchname);  // not declared in scope

Inside a class being defined, cp_parser_function_contract_specifier takes its
deferred branch and stores a token cache, cp_parser_late_contracts is only
ever driven for a FUNCTION_DECL, and the cache is discarded unparsed.  Only
the balance of the parentheses is checked, by the caching scan itself.  Every
one of these is accepted with no diagnostic:

  struct S {
    typedef int FSyntax (int) pre (1 + * / 2);
    typedef int FName   (int) pre (nosuchname);
    typedef int FWords  (int) pre (a b c d);
    typedef int FString (int) pre ("not a bool");
    typedef int FStmt   (int) pre (return 7);
    using   UWords = int (int) pre (a b c d);
    int    (*mpWords) (int) pre (a b c d);
  };

Only an unbalanced predicate such as pre (%%% ][ &&) is refused there, and
that is the paren-matcher failing to find the closing token, not the
predicate being checked.


ANALYSIS

cp_parser_direct_declarator (gcc/cp/parser.cc) parses a
function-contract-specifier-seq in the CPP_OPEN_PAREN arm of its
direct-declarator loop -- after any parameter list -- guarded only by
flag_contracts, and stores it on the cdk_function declarator node via
make_call_declarator.  That arm runs once per parameter list in the
declarator, and it cannot tell which of them, if any, belongs to the
function being declared: at that point it has not seen the decl-specifiers,
so `typedef` is invisible; it does not know whether it is inside a
parameter-declaration-clause; and it builds declarators inside-out, so an
enclosing pointer declarator has not been reached yet.  Parsing there also
means the predicate is parsed with the wrong parameter scope open, which is
the misresolution in reproducer 3.

The grammar puts the seq somewhere the parser does know the answer: after
the complete declarator and its optional requires-clause, in init-declarator,
function-definition, member-declarator and lambda-declarator.  Those are the
same four positions PR123909 names for the requires-clause.

grokdeclarator (gcc/cp/decl.cc) then accumulates the node's specifiers into
a local in its `case cdk_function:` arm, and reads that local only at the
two grokfndecl calls.  Every other exit lets it go out of scope unread --
which is why the drops in reproducers 2 and 4 are silent.

The requires-clause travels in the same declarator slot and fails in the
same positions, in the complementary direction: it does have the "declares
no function" guards, so most of reproducer 4 is caught, but it applies them
to the wrong node, so all of reproducers 1, 2 and 3 are wrongly rejected for it
too.  Its parameter case is additionally missed, because the guard it hangs
on,

  if (!FUNC_OR_METHOD_TYPE_P (type))

is evaluated before the function-to-pointer adjustment that would make it
true.  That adjustment ([dcl.fct]/5) happens further down, in the
decl_context == PARM handling --

  else if (TREE_CODE (type) == FUNCTION_TYPE)
    type = build_pointer_type (type);

-- after the guard that depends on it.  So a function-typed parameter still
holds a bare FUNCTION_TYPE at the guard, and the question "is this a
function type?" answers yes about a declarator that is about to stop being
one.

Two further details.  For a member typedef or a member alias the predicate
is never even parsed: cp_parser_function_contract_specifier takes its
deferred branch inside a class being defined and stores a DEFERRED_PARSE
token cache, and cp_parser_late_contracts is only ever driven for a
FUNCTION_DECL, so the tokens are discarded.  And set_fn_contract_specifiers
(gcc/cp/contracts.cc) never asserts that its key is a FUNCTION_DECL, so
nothing downstream notices that no specifiers were ever recorded.


VERSIONS -- all on x86_64-linux-gnu, and identical on each

  16.1.0
  16.2.0
  17.0.0 20260922 (experimental), f008f03eff80

  reproducer 1  4 diagnostics on well-formed code    should be 0
  reproducer 2  1 violation of the 3 owed            should be 3
  reproducer 3  which_type, which_size,              should be contradiction
                contradiction and control report     and control only
  reproducer 4  0 diagnostics on ill-formed code     should be nonzero

13.4.0, 14.4.0 and 15.3.0 do not accept the pre/post syntax at all.

Reply via email to