[PATCH] c++: Diagnose unstable satisfaction results
Patrick Palka
ppalka@redhat.com
Fri Dec 11 15:31:06 GMT 2020
On Thu, 10 Dec 2020, Patrick Palka wrote:
> On Thu, 10 Dec 2020, Jason Merrill wrote:
>
> > On 12/10/20 11:21 AM, Patrick Palka wrote:
> > > This implements lightweight heuristical detection and diagnosing of
> > > satisfaction results that change at different points in the program,
> > > which renders the program as ill-formed NDR as of P2014. We've recently
> > > started to more aggressively cache satisfaction results, and so the goal
> > > here is to make this caching behavior more transparent to users.
> > >
> > > A satisfaction result is flagged as "potentially unstable" (at the atom
> > > granularity) if during its computation, some type completion failure
> > > occurs. This is detected by making complete_type_or_maybe_complain
> > > increment a counter upon failure and comparing the value of the counter
> > > before and after satisfaction. (We don't instrument complete_type
> > > directly because it's used "opportunistically" in many spots where type
> > > completion failure doesn't necessary lead to substitution failure.)
> > >
> > > Flagged satisfaction results are always recomputed from scratch, even
> > > when performing satisfaction quietly. We then compare the recomputed
> > > result with the cached result, and if they differ, proceed with
> > > diagnosing the instability. (We may also unflag a result if it turned
> > > out to be independent of the previously detected type completion
> > > failure.) When performing satisfaction noisily, we always check
> > > instability.
> > >
> > > Most of the implementation is confined to the satisfaction_cache class,
> > > which has been completely rewritten.
> > >
> > > Bootstrapped and regtested on x86_64-pc-linux-gnu, and also tested on
> > > cmcstl2 and range-v3. The static_assert failures in the view.join test
> > > from cmcstl2 are now elaborated on after this patch, and additionally
> > > the alg.equal_range test now fails for the same reason as the view.join
> > > test.
> > >
> > > gcc/cp/ChangeLog:
> > >
> > > * constraint.cc (failed_type_completion_count): New.
> > > (note_failed_type_completion_for_satisfaction): New.
> > > (sat_entry::constr): Rename to ...
> > > (sat_entry::atom): ... this.
> > > (sat_entry::location): New member.
> > > (sat_entry::maybe_unstable): New member.
> > > (sat_entry::diagnose_instability): New member.
> > > (struct sat_hasher): Adjust after the above renaming.
> > > (get_satisfaction, save_satisfaction): Remove.
> > > (satisfaction_cache): Rewrite completely.
> > > (satisfy_atom): When instantiation of the parameter mapping
> > > fails, set diagnose_instability. Propagate location from
> > > inst_cache.entry to cache.entry if the secondary lookup
> > > succeeded.
> > > (satisfy_declaration_constraints): When
> > > failed_type_completion_count differs before and after
> > > satisfaction, then don't cache the satisfaction result.
> > > * cp-tree.h (note_failed_type_completion_for_satisfaction):
> > > Declare.
> > > * pt.c (tsubst) <case TYPENAME_TYPE>: Use
> > > complete_type_or_maybe_complain instead of open-coding it.
> > > * typeck.c (complete_type_or_maybe_complain): Call
> > > note_failed_type_completion_for_satisfaction when type
> > > completion fails.
> > >
> > > gcc/testsuite/ChangeLog:
> > >
> > > * g++.dg/cpp2a/concepts-complete1.C: New test.
> > > * g++.dg/cpp2a/concepts-complete2.C: New test.
> > > * g++.dg/cpp2a/concepts-complete3.C: New test.
> > > ---
> > > gcc/cp/constraint.cc | 283 ++++++++++++++----
> > > gcc/cp/cp-tree.h | 2 +
> > > gcc/cp/pt.c | 9 +-
> > > gcc/cp/typeck.c | 1 +
> > > .../g++.dg/cpp2a/concepts-complete1.C | 18 ++
> > > .../g++.dg/cpp2a/concepts-complete2.C | 23 ++
> > > .../g++.dg/cpp2a/concepts-complete3.C | 16 +
> > > 7 files changed, 282 insertions(+), 70 deletions(-)
> > > create mode 100644 gcc/testsuite/g++.dg/cpp2a/concepts-complete1.C
> > > create mode 100644 gcc/testsuite/g++.dg/cpp2a/concepts-complete2.C
> > > create mode 100644 gcc/testsuite/g++.dg/cpp2a/concepts-complete3.C
> > >
> > > diff --git a/gcc/cp/constraint.cc b/gcc/cp/constraint.cc
> > > index 73c038e3afe..ee702b34d01 100644
> > > --- a/gcc/cp/constraint.cc
> > > +++ b/gcc/cp/constraint.cc
> > > @@ -2374,35 +2374,82 @@ tsubst_parameter_mapping (tree map, tree args,
> > > tsubst_flags_t complain, tree in_
> > > Constraint satisfaction
> > > ---------------------------------------------------------------------------*/
> > > -/* Hash functions for satisfaction entries. */
> > > +/* A counter incremented by note_failed_type_completion_for_satisfaction().
> > > + It's used by the satisfaction caches in order to flag "potentially
> > > unstable"
> > > + satisfaction results. */
> > > +
> > > +static unsigned failed_type_completion_count;
> > > +
> > > +/* Called whenever a type completion failure occurs that definitely affects
> > > + the semantics of the program, by e.g. inducing substitution failure. */
> > > +
> > > +void
> > > +note_failed_type_completion_for_satisfaction (tree type)
> > > +{
> > > + gcc_checking_assert (!COMPLETE_TYPE_P (type));
> > > + if (CLASS_TYPE_P (type)
> > > + && CLASSTYPE_TEMPLATE_INSTANTIATION (type))
> > > + /* After instantiation, an incomplete class template specialization
> > > + will always be incomplete, so we don't increment the counter in this
> > > + case. */;
> >
> > Hmm? If a class template is declared early in the TU and defined later, a
> > later attempt will succeed in instantiating it.
>
> But shouldn't the result of the first instantiation, the one that yields
> an incomplete class type, prevail for the entire TU? At least, that's
> how I understand [temp.inst]/1 and [temp.point]/7.
Oops, I meant to refer to [temp.inst]/2 here instead. In particular,
the sentences
from [temp.inst]/2:
If a class template has been declared, but not defined, at the point
of instantiation, the instantiation yields an incomplete class type.
from [temp.point]/7:
A specialization for a class template has at most one point of
instantiation within a translation unit.
together seem to imply it's impossible to complete a class template
specialization after an earlier instantiation yielded an incomplete
type, for then there would be two different points of instantiation
for the same specialization.
>
> FWIW, this "optimization" was added in order to avoid a significant
> performance regression in cmcstl's view.take testcase, which ends up
> frequently checking an unsatisfiable atom that wants completeness of
> std::tuple_size<T>.
>
> >
> > > + else
> > > + ++failed_type_completion_count;
> > > +}
> > > +
> > > +/* Hash functions and data types for satisfaction cache entries. */
> > > struct GTY((for_user)) sat_entry
> > > {
> > > - tree constr;
> > > + /* The relevant ATOMIC_CONSTR. */
> > > + tree atom;
> > > +
> > > + /* The relevant template arguments. */
> > > tree args;
> > > +
> > > + /* The result of satisfaction of ATOM+ARGS.
> > > + This is either boolean_true_node, boolean_false_node or
> > > error_mark_node,
> > > + where error_mark_node indicates ill-formed satisfaction.
> > > + It's set to NULL_TREE while computing satisfaction of ATOM+ARGS for
> > > + the first time. */
> > > tree result;
> > > +
> > > + /* The value of input_location when satisfaction of ATOM+ARGS was first
> > > + performed. */
> > > + location_t location;
> > > +
> > > + /* True if this satisfaction result is flagged as "potentially unstable",
> > > + i.e. the result might change at different points in the program if
> > > + recomputed from scratch (which would be UB). This flag is used to
> > > + heuristically diagnose such UB when it occurs, by recomputing this
> > > + satisfaction from scratch even when evaluating quietly. */
> > > + bool maybe_unstable;
> > > +
> > > + /* True if we want to diagnose the above instability when it's detected.
> > > + We don't always want to do so, in order to avoid emitting duplicate
> > > + diagnostics in some cases. */
> > > + bool diagnose_instability;
> > > };
> > > struct sat_hasher : ggc_ptr_hash<sat_entry>
> > > {
> > > static hashval_t hash (sat_entry *e)
> > > {
> > > - if (ATOMIC_CONSTR_MAP_INSTANTIATED_P (e->constr))
> > > + if (ATOMIC_CONSTR_MAP_INSTANTIATED_P (e->atom))
> > > {
> > > /* Atoms with instantiated mappings are built during satisfaction.
> > > They live only inside the sat_cache, and we build one to query
> > > the cache with each time we instantiate a mapping. */
> > > gcc_assert (!e->args);
> > > - return hash_atomic_constraint (e->constr);
> > > + return hash_atomic_constraint (e->atom);
> > > }
> > > /* Atoms with uninstantiated mappings are built during
> > > normalization.
> > > Since normalize_atom caches the atoms it returns, we can assume
> > > pointer-based identity for fast hashing and comparison. Even if
> > > this
> > > assumption is violated, that's okay, we'll just get a cache miss.
> > > */
> > > - hashval_t value = htab_hash_pointer (e->constr);
> > > + hashval_t value = htab_hash_pointer (e->atom);
> > > - if (tree map = ATOMIC_CONSTR_MAP (e->constr))
> > > + if (tree map = ATOMIC_CONSTR_MAP (e->atom))
> > > /* Only the parameters that are used in the targets of the mapping
> > > affect the satisfaction value of the atom. So we consider only
> > > the arguments for these parameters, and ignore the rest. */
> > > @@ -2425,21 +2472,21 @@ struct sat_hasher : ggc_ptr_hash<sat_entry>
> > > static bool equal (sat_entry *e1, sat_entry *e2)
> > > {
> > > - if (ATOMIC_CONSTR_MAP_INSTANTIATED_P (e1->constr)
> > > - != ATOMIC_CONSTR_MAP_INSTANTIATED_P (e2->constr))
> > > + if (ATOMIC_CONSTR_MAP_INSTANTIATED_P (e1->atom)
> > > + != ATOMIC_CONSTR_MAP_INSTANTIATED_P (e2->atom))
> > > return false;
> > > /* See sat_hasher::hash. */
> > > - if (ATOMIC_CONSTR_MAP_INSTANTIATED_P (e1->constr))
> > > + if (ATOMIC_CONSTR_MAP_INSTANTIATED_P (e1->atom))
> > > {
> > > gcc_assert (!e1->args && !e2->args);
> > > - return atomic_constraints_identical_p (e1->constr, e2->constr);
> > > + return atomic_constraints_identical_p (e1->atom, e2->atom);
> > > }
> > > - if (e1->constr != e2->constr)
> > > + if (e1->atom != e2->atom)
> > > return false;
> > > - if (tree map = ATOMIC_CONSTR_MAP (e1->constr))
> > > + if (tree map = ATOMIC_CONSTR_MAP (e1->atom))
> > > for (tree target_parms = TREE_TYPE (map);
> > > target_parms;
> > > target_parms = TREE_CHAIN (target_parms))
> > > @@ -2467,59 +2514,146 @@ static GTY((deletable)) hash_table<sat_hasher>
> > > *sat_cache;
> > > /* Cache the result of constraint_satisfaction_value. */
> > > static GTY((deletable)) hash_map<tree, tree> *decl_satisfied_cache;
> > > -static tree
> > > -get_satisfaction (tree constr, tree args)
> > > -{
> > > - if (!sat_cache)
> > > - return NULL_TREE;
> > > - sat_entry elt = { constr, args, NULL_TREE };
> > > - sat_entry* found = sat_cache->find (&elt);
> > > - if (found)
> > > - return found->result;
> > > - else
> > > - return NULL_TREE;
> > > -}
> > > -
> > > -static void
> > > -save_satisfaction (tree constr, tree args, tree result)
> > > -{
> > > - if (!sat_cache)
> > > - sat_cache = hash_table<sat_hasher>::create_ggc (31);
> > > - sat_entry elt = {constr, args, result};
> > > - sat_entry** slot = sat_cache->find_slot (&elt, INSERT);
> > > - sat_entry* entry = ggc_alloc<sat_entry> ();
> > > - *entry = elt;
> > > - *slot = entry;
> > > -}
> > > -
> > > -/* A tool to help manage satisfaction caching in satisfy_constraint_r.
> > > - Note the cache is only used when not diagnosing errors. */
> > > +/* A tool used by satisfy_atom to help manage satisfaction caching and to
> > > + diagnose "unstable" satisfaction values. We insert into the cache only
> > > + when performing satisfaction quietly. */
> > > struct satisfaction_cache
> > > {
> > > - satisfaction_cache (tree constr, tree args, tsubst_flags_t complain)
> > > - : constr(constr), args(args), complain(complain)
> > > - { }
> > > + satisfaction_cache (tree, tree, sat_info);
> > > + tree get ();
> > > + tree save (tree);
> > > - tree get ()
> > > - {
> > > - if (complain == tf_none)
> > > - return get_satisfaction (constr, args);
> > > - return NULL_TREE;
> > > - }
> > > -
> > > - tree save (tree result)
> > > - {
> > > - if (complain == tf_none)
> > > - save_satisfaction (constr, args, result);
> > > - return result;
> > > - }
> > > -
> > > - tree constr;
> > > - tree args;
> > > - tsubst_flags_t complain;
> > > + sat_entry *entry;
> > > + sat_info info;
> > > + unsigned ftc_count;
> > > };
> > > +/* Constructor for the satisfaction_cache class. We're performing
> > > satisfaction
> > > + of ATOM+ARGS according to INFO. */
> > > +
> > > +satisfaction_cache
> > > +::satisfaction_cache (tree atom, tree args, sat_info info)
> > > + : entry(nullptr), info(info), ftc_count(failed_type_completion_count)
> > > +{
> > > + if (!sat_cache)
> > > + sat_cache = hash_table<sat_hasher>::create_ggc (31);
> > > +
> > > + /* When noisy, we query the satisfaction cache in order to diagnose
> > > + "unstable" satisfaction values. */
> > > + if (info.noisy ())
> > > + {
> > > + /* When noisy, constraints have been re-normalized, and that breaks
> > > the
> > > + pointer-based identity assumption of sat_cache (for atoms with
> > > + uninstantiated mappings). So undo this re-normalization by looking
> > > in
> > > + the atom_cache for the corresponding atom that was used during quiet
> > > + satisfaction. */
> > > + if (!ATOMIC_CONSTR_MAP_INSTANTIATED_P (atom))
> > > + {
> > > + if (tree found = atom_cache->find (atom))
> > > + atom = found;
> > > + else
> > > + /* The lookup should always succeed, but if it fails then let's
> > > + just leave 'entry' empty, effectively disabling the cache. */
> > > + return;
> > > + }
> > > + }
> > > +
> > > + /* Look up or create the corresponding satisfaction entry. */
> > > + sat_entry elt;
> > > + elt.atom = atom;
> > > + elt.args = args;
> > > + sat_entry **slot = sat_cache->find_slot (&elt, INSERT);
> > > + if (*slot)
> > > + entry = *slot;
> > > + else if (info.quiet ())
> > > + {
> > > + entry = ggc_alloc<sat_entry> ();
> > > + entry->atom = atom;
> > > + entry->args = args;
> > > + entry->result = NULL_TREE;
> > > + entry->location = input_location;
> > > + entry->maybe_unstable = false;
> > > + entry->diagnose_instability = false;
> > > + if (ATOMIC_CONSTR_MAP_INSTANTIATED_P (atom))
> > > + /* We always want to diagnose instability of an atom with an
> > > + instantiated parameter mapping. For atoms with an uninstiantiated
> >
> > Extra 'i' in "uninstantiated"
>
> Fixed, as well as applied David's suggestion to use
> auto_diagnostic_group. Here's an updated patch which improves some
> comments too, no functional changes:
>
> -- >8 --
>
> Subject: [PATCH] c++: Diagnose unstable satisfaction
>
> This implements lightweight heuristical detection and diagnosing of
> satisfaction whose result changes at different points in the program,
> which renders the program ill-formed NDR as of P2104. We've recently
> started to more aggressively cache satisfaction results, and so the goal
> with this patch is to make this caching behavior more transparent to
> the user.
>
> A satisfaction result is flagged as "potentially unstable" (at the atom
> granularity) if during its computation, some type completion failure
> occurs. This is detected by making complete_type_or_maybe_complain
> increment a counter upon failure and comparing the value of the counter
> before and after satisfaction. (We don't instrument complete_type
> directly because it's used "opportunistically" in many spots where type
> completion failure doesn't necessary lead to substitution failure.)
>
> Such flagged satisfaction results are always recomputed from scratch,
> even when performing satisfaction quietly. When saving a satisfaction
> result, we now compare the computed result with the cached result, and
> if they differ, proceed with diagnosing the instability.
>
> Most of the implementation is confined to the satisfaction_cache class,
> which has been completely rewritten.
>
> Bootstrapped and regtested on x86_64-pc-linux-gnu, and also tested on
> cmcstl2 and range-v3. The static_assert failures in the view.join test
> from cmcstl2 are now elaborated on after this patch, and additionally
> the alg.equal_range test now fails for the same reason as the view.join
> test.
>
> gcc/cp/ChangeLog:
>
> * constraint.cc (failed_type_completion_count): New.
> (note_failed_type_completion_for_satisfaction): New.
> (sat_entry::constr): Rename to ...
> (sat_entry::atom): ... this.
> (sat_entry::location): New member.
> (sat_entry::maybe_unstable): New member.
> (sat_entry::diagnose_instability): New member.
> (struct sat_hasher): Adjust after the above renaming.
> (get_satisfaction, save_satisfaction): Remove.
> (satisfaction_cache): Rewrite completely.
> (satisfy_atom): When instantiation of the parameter mapping
> fails, set diagnose_instability. Propagate location from
> inst_cache.entry to cache.entry if the secondary lookup
> succeeded.
> (satisfy_declaration_constraints): When
> failed_type_completion_count differs before and after
> satisfaction, then don't cache the satisfaction result.
> * cp-tree.h (note_failed_type_completion_for_satisfaction):
> Declare.
> * pt.c (tsubst) <case TYPENAME_TYPE>: Use
> complete_type_or_maybe_complain instead of open-coding it.
> * typeck.c (complete_type_or_maybe_complain): Call
> note_failed_type_completion_for_satisfaction when type
> completion fails.
>
> gcc/testsuite/ChangeLog:
>
> * g++.dg/cpp2a/concepts-complete1.C: New test.
> * g++.dg/cpp2a/concepts-complete2.C: New test.
> * g++.dg/cpp2a/concepts-complete3.C: New test.
> ---
> gcc/cp/constraint.cc | 283 ++++++++++++++----
> gcc/cp/cp-tree.h | 2 +
> gcc/cp/pt.c | 9 +-
> gcc/cp/typeck.c | 1 +
> .../g++.dg/cpp2a/concepts-complete1.C | 18 ++
> .../g++.dg/cpp2a/concepts-complete2.C | 23 ++
> .../g++.dg/cpp2a/concepts-complete3.C | 16 +
> 7 files changed, 282 insertions(+), 70 deletions(-)
> create mode 100644 gcc/testsuite/g++.dg/cpp2a/concepts-complete1.C
> create mode 100644 gcc/testsuite/g++.dg/cpp2a/concepts-complete2.C
> create mode 100644 gcc/testsuite/g++.dg/cpp2a/concepts-complete3.C
>
> diff --git a/gcc/cp/constraint.cc b/gcc/cp/constraint.cc
> index 73c038e3afe..b98befaf71b 100644
> --- a/gcc/cp/constraint.cc
> +++ b/gcc/cp/constraint.cc
> @@ -2374,35 +2374,82 @@ tsubst_parameter_mapping (tree map, tree args, tsubst_flags_t complain, tree in_
> Constraint satisfaction
> ---------------------------------------------------------------------------*/
>
> -/* Hash functions for satisfaction entries. */
> +/* A counter incremented by note_failed_type_completion_for_satisfaction().
> + It's used by the satisfaction caches in order to flag "potentially unstable"
> + satisfaction results. */
> +
> +static unsigned failed_type_completion_count;
> +
> +/* Called whenever a type completion failure occurs that definitely affects
> + the semantics of the program, by e.g. inducing substitution failure. */
> +
> +void
> +note_failed_type_completion_for_satisfaction (tree type)
> +{
> + gcc_checking_assert (!COMPLETE_TYPE_P (type));
> + if (CLASS_TYPE_P (type)
> + && CLASSTYPE_TEMPLATE_INSTANTIATION (type))
> + /* After instantiation, a class template specialization that's
> + incomplete will remain incomplete, so for our purposes we can
> + ignore this completion failure event. */;
> + else
> + ++failed_type_completion_count;
> +}
> +
> +/* Hash functions and data types for satisfaction cache entries. */
>
> struct GTY((for_user)) sat_entry
> {
> - tree constr;
> + /* The relevant ATOMIC_CONSTR. */
> + tree atom;
> +
> + /* The relevant template arguments. */
> tree args;
> +
> + /* The result of satisfaction of ATOM+ARGS.
> + This is either boolean_true_node, boolean_false_node or error_mark_node,
> + where error_mark_node indicates ill-formed satisfaction.
> + It's set to NULL_TREE while computing satisfaction of ATOM+ARGS for
> + the first time. */
> tree result;
> +
> + /* The value of input_location when satisfaction of ATOM+ARGS was first
> + performed. */
> + location_t location;
> +
> + /* True if this satisfaction result is flagged as "potentially unstable",
> + i.e. the result might change at different points in the program if
> + recomputed from scratch (which would be ill-formed). This flag controls
> + whether to recompute a cached satisfaction result from scratch even when
> + evaluating quietly. */
> + bool maybe_unstable;
> +
> + /* True if we want to diagnose the above instability when it's detected.
> + We don't always want to do so, in order to avoid emitting duplicate
> + diagnostics in some cases. */
> + bool diagnose_instability;
> };
>
> struct sat_hasher : ggc_ptr_hash<sat_entry>
> {
> static hashval_t hash (sat_entry *e)
> {
> - if (ATOMIC_CONSTR_MAP_INSTANTIATED_P (e->constr))
> + if (ATOMIC_CONSTR_MAP_INSTANTIATED_P (e->atom))
> {
> /* Atoms with instantiated mappings are built during satisfaction.
> They live only inside the sat_cache, and we build one to query
> the cache with each time we instantiate a mapping. */
> gcc_assert (!e->args);
> - return hash_atomic_constraint (e->constr);
> + return hash_atomic_constraint (e->atom);
> }
>
> /* Atoms with uninstantiated mappings are built during normalization.
> Since normalize_atom caches the atoms it returns, we can assume
> pointer-based identity for fast hashing and comparison. Even if this
> assumption is violated, that's okay, we'll just get a cache miss. */
> - hashval_t value = htab_hash_pointer (e->constr);
> + hashval_t value = htab_hash_pointer (e->atom);
>
> - if (tree map = ATOMIC_CONSTR_MAP (e->constr))
> + if (tree map = ATOMIC_CONSTR_MAP (e->atom))
> /* Only the parameters that are used in the targets of the mapping
> affect the satisfaction value of the atom. So we consider only
> the arguments for these parameters, and ignore the rest. */
> @@ -2425,21 +2472,21 @@ struct sat_hasher : ggc_ptr_hash<sat_entry>
>
> static bool equal (sat_entry *e1, sat_entry *e2)
> {
> - if (ATOMIC_CONSTR_MAP_INSTANTIATED_P (e1->constr)
> - != ATOMIC_CONSTR_MAP_INSTANTIATED_P (e2->constr))
> + if (ATOMIC_CONSTR_MAP_INSTANTIATED_P (e1->atom)
> + != ATOMIC_CONSTR_MAP_INSTANTIATED_P (e2->atom))
> return false;
>
> /* See sat_hasher::hash. */
> - if (ATOMIC_CONSTR_MAP_INSTANTIATED_P (e1->constr))
> + if (ATOMIC_CONSTR_MAP_INSTANTIATED_P (e1->atom))
> {
> gcc_assert (!e1->args && !e2->args);
> - return atomic_constraints_identical_p (e1->constr, e2->constr);
> + return atomic_constraints_identical_p (e1->atom, e2->atom);
> }
>
> - if (e1->constr != e2->constr)
> + if (e1->atom != e2->atom)
> return false;
>
> - if (tree map = ATOMIC_CONSTR_MAP (e1->constr))
> + if (tree map = ATOMIC_CONSTR_MAP (e1->atom))
> for (tree target_parms = TREE_TYPE (map);
> target_parms;
> target_parms = TREE_CHAIN (target_parms))
> @@ -2467,59 +2514,146 @@ static GTY((deletable)) hash_table<sat_hasher> *sat_cache;
> /* Cache the result of constraint_satisfaction_value. */
> static GTY((deletable)) hash_map<tree, tree> *decl_satisfied_cache;
>
> -static tree
> -get_satisfaction (tree constr, tree args)
> -{
> - if (!sat_cache)
> - return NULL_TREE;
> - sat_entry elt = { constr, args, NULL_TREE };
> - sat_entry* found = sat_cache->find (&elt);
> - if (found)
> - return found->result;
> - else
> - return NULL_TREE;
> -}
> -
> -static void
> -save_satisfaction (tree constr, tree args, tree result)
> -{
> - if (!sat_cache)
> - sat_cache = hash_table<sat_hasher>::create_ggc (31);
> - sat_entry elt = {constr, args, result};
> - sat_entry** slot = sat_cache->find_slot (&elt, INSERT);
> - sat_entry* entry = ggc_alloc<sat_entry> ();
> - *entry = elt;
> - *slot = entry;
> -}
> -
> -/* A tool to help manage satisfaction caching in satisfy_constraint_r.
> - Note the cache is only used when not diagnosing errors. */
> +/* A tool used by satisfy_atom to help manage satisfaction caching and to
> + diagnose "unstable" satisfaction values. We insert into the cache only
> + when performing satisfaction quietly. */
>
> struct satisfaction_cache
> {
> - satisfaction_cache (tree constr, tree args, tsubst_flags_t complain)
> - : constr(constr), args(args), complain(complain)
> - { }
> + satisfaction_cache (tree, tree, sat_info);
> + tree get ();
> + tree save (tree);
>
> - tree get ()
> - {
> - if (complain == tf_none)
> - return get_satisfaction (constr, args);
> - return NULL_TREE;
> - }
> -
> - tree save (tree result)
> - {
> - if (complain == tf_none)
> - save_satisfaction (constr, args, result);
> - return result;
> - }
> -
> - tree constr;
> - tree args;
> - tsubst_flags_t complain;
> + sat_entry *entry;
> + sat_info info;
> + unsigned ftc_count;
> };
>
> +/* Constructor for the satisfaction_cache class. We're performing satisfaction
> + of ATOM+ARGS according to INFO. */
> +
> +satisfaction_cache
> +::satisfaction_cache (tree atom, tree args, sat_info info)
> + : entry(nullptr), info(info), ftc_count(failed_type_completion_count)
> +{
> + if (!sat_cache)
> + sat_cache = hash_table<sat_hasher>::create_ggc (31);
> +
> + /* When noisy, we query the satisfaction cache in order to diagnose
> + "unstable" satisfaction values. */
> + if (info.noisy ())
> + {
> + /* When noisy, constraints have been re-normalized, and that breaks the
> + pointer-based identity assumption of sat_cache (for atoms with
> + uninstantiated mappings). So undo this re-normalization by looking in
> + the atom_cache for the corresponding atom that was used during quiet
> + satisfaction. */
> + if (!ATOMIC_CONSTR_MAP_INSTANTIATED_P (atom))
> + {
> + if (tree found = atom_cache->find (atom))
> + atom = found;
> + else
> + /* The lookup should always succeed, but if it fails then let's
> + just leave 'entry' empty, effectively disabling the cache. */
> + return;
> + }
> + }
> +
> + /* Look up or create the corresponding satisfaction entry. */
> + sat_entry elt;
> + elt.atom = atom;
> + elt.args = args;
> + sat_entry **slot = sat_cache->find_slot (&elt, INSERT);
> + if (*slot)
> + entry = *slot;
> + else if (info.quiet ())
> + {
> + entry = ggc_alloc<sat_entry> ();
> + entry->atom = atom;
> + entry->args = args;
> + entry->result = NULL_TREE;
> + entry->location = input_location;
> + entry->maybe_unstable = false;
> + entry->diagnose_instability = false;
> + if (ATOMIC_CONSTR_MAP_INSTANTIATED_P (atom))
> + /* We always want to diagnose instability of an atom with an
> + instantiated parameter mapping. For atoms with an uninstantiated
> + mapping, we set this flag (in satisfy_atom) only if substitution
> + into its mapping previously failed. */
> + entry->diagnose_instability = true;
> + *slot = entry;
> + }
> + else
> + /* We shouldn't get here, but if we do, let's just leave 'entry'
> + empty, effectively disabling the cache. */
> + return;
> +}
> +
> +/* Returns the cached satisfaction result if we have one and we're not
> + recomputing the satisfaction result from scratch. Otherwise returns
> + NULL_TREE. */
> +
> +tree
> +satisfaction_cache::get ()
> +{
> + if (!entry)
> + return NULL_TREE;
> +
> + if (info.noisy () || entry->maybe_unstable)
> + /* We're recomputing the satisfaction result from scratch. */
> + return NULL_TREE;
> + else
> + return entry->result;
> +}
> +
> +/* RESULT is the computed satisfaction result. If RESULT differs from the
> + previously cached result, this routine issues an appropriate error.
> + Otherwise, when evaluating quietly, updates the cache appropriately. */
> +
> +tree
> +satisfaction_cache::save (tree result)
> +{
> + if (!entry)
> + return result;
> +
> + if (entry->result && result != entry->result)
> + {
> + if (info.quiet ())
> + /* Return error_mark_node to force satisfaction to get replayed
> + noisily. */
> + return error_mark_node;
> + else
> + {
> + if (entry->diagnose_instability)
> + {
> + auto_diagnostic_group d;
> + error_at (EXPR_LOCATION (ATOMIC_CONSTR_EXPR (entry->atom)),
> + "satisfaction value of atomic constraint %qE changed "
> + "from %qE to %qE", entry->atom, entry->result, result);
> + inform (entry->location,
> + "satisfaction value first evaluated to %qE from here",
> + entry->result);
> + }
> + /* For sake of error recovery, allow this latest satisfaction result
> + to prevail. */
> + entry->result = result;
> + return result;
> + }
> + }
> +
> + if (info.quiet ())
> + {
> + entry->result = result;
> + /* We heuristically flag this satisfaction result as potentially unstable
> + iff during its computation, completion of a type failed. Note that
> + this may also clear the flag if the result turned out to be
> + independent of the previously detected type completion failure. */
> + entry->maybe_unstable = (ftc_count != failed_type_completion_count);
> + }
> +
> + return result;
> +}
> +
> static int satisfying_constraint = 0;
>
> /* Returns true if we are currently satisfying a constraint.
> @@ -2731,7 +2865,7 @@ static void diagnose_atomic_constraint (tree, tree, tree, subst_info);
> static tree
> satisfy_atom (tree t, tree args, sat_info info)
> {
> - satisfaction_cache cache (t, args, info.complain);
> + satisfaction_cache cache (t, args, info);
> if (tree r = cache.get ())
> return r;
>
> @@ -2751,6 +2885,11 @@ satisfy_atom (tree t, tree args, sat_info info)
> not satisfied. Replay the substitution. */
> if (info.diagnose_unsatisfaction_p ())
> tsubst_parameter_mapping (ATOMIC_CONSTR_MAP (t), args, info);
> + if (info.quiet ())
> + /* Since instantiation of the parameter mapping failed, we
> + want to diagnose potential instability of this satisfaction
> + result. */
> + cache.entry->diagnose_instability = true;
> return cache.save (boolean_false_node);
> }
>
> @@ -2762,9 +2901,12 @@ satisfy_atom (tree t, tree args, sat_info info)
> ATOMIC_CONSTR_MAP (t) = map;
> gcc_assert (!ATOMIC_CONSTR_MAP_INSTANTIATED_P (t));
> ATOMIC_CONSTR_MAP_INSTANTIATED_P (t) = true;
> - satisfaction_cache inst_cache (t, /*args=*/NULL_TREE, info.complain);
> + satisfaction_cache inst_cache (t, /*args=*/NULL_TREE, info);
> if (tree r = inst_cache.get ())
> - return cache.save (r);
> + {
> + cache.entry->location = inst_cache.entry->location;
> + return cache.save (r);
> + }
>
> /* Rebuild the argument vector from the parameter mapping. */
> args = get_mapped_args (map);
> @@ -2955,6 +3097,8 @@ satisfy_declaration_constraints (tree t, sat_info info)
> norm = normalize_nontemplate_requirements (t, info.noisy ());
> }
>
> + unsigned ftc_count = failed_type_completion_count;
> +
> tree result = boolean_true_node;
> if (norm)
> {
> @@ -2966,7 +3110,20 @@ satisfy_declaration_constraints (tree t, sat_info info)
> pop_tinst_level ();
> }
>
> - if (info.quiet ())
> + /* True if this satisfaction is (heuristically) potentially unstable, i.e.
> + if its result may depend on where in the program it was performed. */
> + bool maybe_unstable_satisfaction = false;
> +
> + if (ftc_count != failed_type_completion_count)
> + /* Type completion failure occurred during satisfaction. The satisfaction
> + result may (or may not) materially depend on the completeness of a type,
> + so we consider it potentially unstable. */
> + maybe_unstable_satisfaction = true;
> +
> + if (maybe_unstable_satisfaction)
> + /* Don't cache potentially unstable satisfaction, to allow satisfy_atom
> + to check the stability the next time around. */;
> + else if (info.quiet ())
> hash_map_safe_put<hm_ggc> (decl_satisfied_cache, saved_t, result);
>
> return result;
> diff --git a/gcc/cp/cp-tree.h b/gcc/cp/cp-tree.h
> index f59591aa865..209efd4b251 100644
> --- a/gcc/cp/cp-tree.h
> +++ b/gcc/cp/cp-tree.h
> @@ -8075,6 +8075,8 @@ extern hashval_t iterative_hash_constraint (tree, hashval_t);
> extern hashval_t hash_atomic_constraint (tree);
> extern void diagnose_constraints (location_t, tree, tree);
>
> +extern void note_failed_type_completion_for_satisfaction (tree);
> +
> /* A structural hasher for ATOMIC_CONSTRs. */
>
> struct atom_hasher : default_hash_traits<tree>
> diff --git a/gcc/cp/pt.c b/gcc/cp/pt.c
> index 2d3ab92dfd1..a2f1fdeb4f5 100644
> --- a/gcc/cp/pt.c
> +++ b/gcc/cp/pt.c
> @@ -15948,13 +15948,8 @@ tsubst (tree t, tree args, tsubst_flags_t complain, tree in_decl)
> But, such constructs have already been resolved by this
> point, so here CTX really should have complete type, unless
> it's a partial instantiation. */
> - ctx = complete_type (ctx);
> - if (!COMPLETE_TYPE_P (ctx))
> - {
> - if (complain & tf_error)
> - cxx_incomplete_type_error (NULL_TREE, ctx);
> - return error_mark_node;
> - }
> + if (!complete_type_or_maybe_complain (ctx, NULL_TREE, complain))
> + return error_mark_node;
> }
>
> f = make_typename_type (ctx, f, typename_type,
> diff --git a/gcc/cp/typeck.c b/gcc/cp/typeck.c
> index 267b284ea40..d961f719110 100644
> --- a/gcc/cp/typeck.c
> +++ b/gcc/cp/typeck.c
> @@ -154,6 +154,7 @@ complete_type_or_maybe_complain (tree type, tree value, tsubst_flags_t complain)
> {
> if (complain & tf_error)
> cxx_incomplete_type_diagnostic (value, type, DK_ERROR);
> + note_failed_type_completion_for_satisfaction (type);
> return NULL_TREE;
> }
> else
> diff --git a/gcc/testsuite/g++.dg/cpp2a/concepts-complete1.C b/gcc/testsuite/g++.dg/cpp2a/concepts-complete1.C
> new file mode 100644
> index 00000000000..e8487bf9c09
> --- /dev/null
> +++ b/gcc/testsuite/g++.dg/cpp2a/concepts-complete1.C
> @@ -0,0 +1,18 @@
> +// Verify we diagnose an unstable satisfaction result that depends on
> +// completeness of the type A below.
> +//
> +// { dg-do compile { target c++20 } }
> +
> +template <class T> concept has_mem_type = requires { typename T::type; };
> +// { dg-message "satisfaction of 'has_mem_type<T>' .with T = A." "" { target *-*-* } .-1 }
> +// { dg-error "satisfaction value of atomic constraint 'requires.typename T::type;. .with T = A.' changed from 'false' to 'true'" "" { target *-*-* } .-2 }
> +
> +template <has_mem_type T> int f () { return 0; }
> +template <class T> char f() { return 0; }
> +
> +struct A;
> +static_assert (sizeof (f<A>()) == 1); // { dg-message "first evaluated to 'false' from here" }
> +struct A { typedef int type; };
> +static_assert (sizeof (f<A>()) > 1); // { dg-error "assert" }
> +// { dg-message "required from here" "" { target *-*-* } .-1 }
> +static_assert (sizeof (f<A>()) > 1);
> diff --git a/gcc/testsuite/g++.dg/cpp2a/concepts-complete2.C b/gcc/testsuite/g++.dg/cpp2a/concepts-complete2.C
> new file mode 100644
> index 00000000000..b2c11606737
> --- /dev/null
> +++ b/gcc/testsuite/g++.dg/cpp2a/concepts-complete2.C
> @@ -0,0 +1,23 @@
> +// Verify we diagnose an unstable satisfaction result that depends on
> +// completeness of the type A below.
> +//
> +// Like in the previous test, here satisfaction also initially fails,
> +// but this time due to failed substitution into the atom's parameter mapping.
> +//
> +// { dg-do compile { target c++20 } }
> +
> +template <class T> concept valid_type = requires { typename T; };
> +// { dg-message "satisfaction of 'valid_type<typename T::type>' .with T = A." "" { target *-*-* } .-1 }
> +// { dg-error "satisfaction value of atomic constraint 'requires.T;. .with T = typename T::type.' changed from 'false' to 'true'" "" { target *-*-* } .-2 }
> +
> +template <class T> concept has_mem_type = valid_type<typename T::type>;
> +// { dg-message "satisfaction of 'has_mem_type<T>' .with T = A." "" { target *-*-* } .-1 }
> +
> +template <has_mem_type T> int f () { return 0; }
> +template <class T> char f() { return 0; }
> +
> +struct A;
> +static_assert (sizeof (f<A>()) == 1); // { dg-message "first evaluated to 'false' from here" }
> +struct A { typedef int type; };
> +static_assert (sizeof (f<A>()) > 1); // { dg-error "assert" }
> +static_assert (sizeof (f<A>()) > 1);
> diff --git a/gcc/testsuite/g++.dg/cpp2a/concepts-complete3.C b/gcc/testsuite/g++.dg/cpp2a/concepts-complete3.C
> new file mode 100644
> index 00000000000..5b07371a6be
> --- /dev/null
> +++ b/gcc/testsuite/g++.dg/cpp2a/concepts-complete3.C
> @@ -0,0 +1,16 @@
> +// Verify we diagnose an unstable satisfaction result that depends on
> +// return type deduction of the member function A::foo() below.
> +//
> +// { dg-do compile { target c++20 } }
> +
> +template <class T> concept fooable = requires (T t) { t.foo(); };
> +// { dg-error "'false' to 'true'" "" { target *-*-* } .-1 }
> +
> +template <fooable T> int f () { return 0; }
> +template <class T> char f() { return 0; }
> +
> +struct A { auto foo(); };
> +static_assert (sizeof (f<A>()) == 1); // { dg-message "first evaluated to 'false' from here" }
> +auto A::foo() { }
> +static_assert (sizeof (f<A>()) > 1); // { dg-error "assert" }
> +static_assert (sizeof (f<A>()) > 1);
> --
> 2.29.2.404.ge67fbf927d
>
>
More information about the Gcc-patches
mailing list