cppfront icon indicating copy to clipboard operation
cppfront copied to clipboard

feat: recognize `requires` expressions

Open JohelEGP opened this issue 2 years ago • 11 comments
trafficstars

This PR adds support for requires-expressions (first based on https://github.com/hsutter/cppfront/discussions/588#discussioncomment-6715505).

This is the syntax, based on Cpp1's:

Cpp2 Cpp1
number: <T> concept = requires(c: T) { template<typename T> concept number = requires(T const& c) {
number_difference_t<T>; typename number_difference_t<T>;
_ = T(); T();
{ c - c } !throws; { c - c } noexcept;
{ c + c } is std::common_with<T>; { c + c } -> std::common_with<T>;
requires std::common_with<number_difference_t<T>, T>; requires std::common_with<number_difference_t<T>,T>;
!requires std::iter_reference_t<T>; requires !requires { typename std::iter_reference_t<T>; };
}; };

The feature, as added by this PR, prevents or completely avoids known Cpp1 pitfalls. By requiring that expression requirements are discarded, it should help to prevent the list of pitfalls at https://github.com/hsutter/cppfront/discussions/588#discussioncomment-6715051. It also adds negative requirements as a first-class feature to create a pit of success for https://quuxplusone.github.io/blog/2021/06/09/another-concepts-chest-mimic/.

Additionally, I think we could:

JohelEGP avatar Aug 17 '23 02:08 JohelEGP

( _ = T() );
{ _ = c - c } throws;

I find it weird that these use different kind of brackets. Since, compound expressions are just simple expressions with throws or is-type-requirement, they should look similar

AbhinavK00 avatar Aug 17 '23 05:08 AbhinavK00

Well, since most expression requirements will need disambiguating parentheses to distinguish them from type requirements anyways, we could indeed remove expression requirements. Then, expression requirements have to be expressed as the compound requirement { 𝘌 };.

JohelEGP avatar Aug 17 '23 14:08 JohelEGP

My choice makes it so that we end up requiring more braces:

requires requires(const T& c) { c - c; } // Cpp1
requires requires(c: T) { { _ = c - c } throws; } // Cpp2

JohelEGP avatar Aug 17 '23 16:08 JohelEGP

To simplify that, in addition to

Additionally, I think we could:

  • Make negative-requirement a primary expression to simplify if constexpr (!requires { (t.begin()); }) to if constexpr (!requires t.begin();).

we could make a requires-expression's body also be a single requirement.

    //G requires-expression:
    //G     'requires' parameter-declaration-list? requirement-body
    //G     '!'? 'requires' parameter-declaration-list? requirement
requires requires(c: T) { _ = c - c; } // Long-winded spelling.
requires requires(c: T) _ = c - c;     // Simpler spelling.
requires !requires std::iter_reference_t<T>;      // OK.

To compare:

requires requires(const T& c) { c - c; } // Cpp1
requires requires(c: T) _ = c - c;     // Cpp2
requires requires(c: T) { _ = c - c; } // Cpp2
requires !requires { typename std::iter_reference_t<T>; } // Cpp1
requires !requires std::iter_reference_t<T>; // Cpp2

JohelEGP avatar Aug 17 '23 16:08 JohelEGP

Make negative-requirement a primary expression to simplify if constexpr (!requires { (t.begin()); }) to if constexpr (!requires t.begin();)

This makes distinguishing between requires clauses and expressions even harder. In cpp1, I know that if there's no braces after requires, it will be a requires clause but if you allow this, then it can be requires clause or a requires expression in cpp2.

I would not sacrifice readability for one less pair of braces.

AbhinavK00 avatar Aug 17 '23 16:08 AbhinavK00

A requires clause can only happen before the = of a declaration.

JohelEGP avatar Aug 17 '23 16:08 JohelEGP

Yes, but it still can be confusing to the reader.

AbhinavK00 avatar Aug 17 '23 16:08 AbhinavK00

I will doubt that until I find out code that makes me think so. To me, the context dependence makes this pretty unambiguous.

JohelEGP avatar Aug 17 '23 16:08 JohelEGP

Fair enough 👌

AbhinavK00 avatar Aug 17 '23 16:08 AbhinavK00

I'm changing this #526's and #596's use of throws. The default will be the same as Cpp1's, which is the same as most authored functions. Instead, I'll extend throws-specifier to accept !throws, which is a shorthand for https://wg21.link/P0709's throws(false).

With that change, you won't need to add throws when rewriting Cpp1 non-noexcept function types or expression requirements for Cpp2. For #596, I'll be able to re-add expression-requirement as _ = 𝘌;. Explicitly discarding the result should more naturally handle the talk's pitfalls.

JohelEGP avatar Aug 20 '23 14:08 JohelEGP