cppfront icon indicating copy to clipboard operation
cppfront copied to clipboard

[BUG] Add support for defining template specializations.

Open leejy12 opened this issue 1 year ago • 11 comments

AFAIK, there's no way to define template specializations in cppfront. I found this out when trying to define a custom std::formatter.

Here's my expected syntax.

Foo: @struct type = {
    x: i32 = 0;
}

std::formatter<Foo>: <> @struct type = {
    parse: (this, inout ctx: std::format_parse_context) -> _ = {
        return ctx.end();
    }

    format: <FormatContext> (this, foo: Foo, inout ctx: FormatContext) -> _ = {
        return std::format_to(ctx.out(), "Foo({})", foo.x);
    }
}

As of now, cppfront doesn't attempt to compile the std::formatter<Foo> part.

https://godbolt.org/z/eKd8Y9aWz

leejy12 avatar May 23 '23 06:05 leejy12

std::formatter<Foo>: <> @struct type = { }

The diamonds come after the metafunctions.

Some more examples:

::std::formatter<Foo>: @struct <> type = { }

same1: <T, U> type == std::false_type;
same1<T, T>: <T> type == std::true_type;

same2: <T, U> type == std::false_type;
same2<T, U>: <T, U> type requires std::same_as<T, U> == std::true_type;

same_v1: <T, U> const bool = false;
same_v1<T, T>: <T> const bool = true;

same_v2: <T, U> const bool = false;
same_v2<T, U>: <T, U> const bool requires std::same_as<T, U> = true;

Another thing to consider is how a nested namespace definition will be supported (see also https://github.com/hsutter/cppfront/issues/438#issuecomment-1538121203):

std::inline literals: namespace = { }

The current grammar has identifiers before a declaration's colon only:

name: (
  name: i32,
  in name: i32
) = { }
name: type = {
  public data: i32;
  operator=: (implicit in this) = { } // `this` doesn't permit `:` yet.
}

export name: () = { } pending #269.

Supporting the specialization of templates introduces :: and arbitrary expressions before the : (and tentatively the inline qualifier for namespaces). Maybe not function expressions, see #385.

The expressions, unfortunately, complicate parsing. https://en.cppreference.com/w/cpp/experimental/is_detected#Possible_implementation:

has_value_: <T, Void> type == std::false_type;
has_value_<T, std::void_t<decltype(T().value)>>: // Would need to parse arbitrary expressions before `:`.
  <T> type == std::true_type;

has_value: <T> type == has_value_<T, void>;

JohelEGP avatar May 23 '23 17:05 JohelEGP

@JohelEGP Unrelated, but how will this code be compiled?

same1: <T, U> type == std::false_type;
same1<T, T>: <T> type == std::true_type;

Because

template <typename T, typename U>
using same1 = std::false_type;

template <typename T>
using same1<T, T> = std::true_type;

is invalid Cpp1.

leejy12 avatar May 25 '23 05:05 leejy12

That was my mistake. It should have been this:

same1: <T, U> type = {
  this: std::false_type = ();
}
same1<T, T>: <T> type = {
  this: std::true_type = ();
}

same2: <T, U> type = {
  this: std::false_type = ();
}
same2<T, U>: <T, U> type requires std::same_as<T, U> = {
  this: std::true_type = ();
}

JohelEGP avatar May 25 '23 12:05 JohelEGP

Just an idea: Directly specifying the specialized template arguments in the declaration name comes from the current cpp1 syntax. In cpp2 syntax this could be moved nearly anywhere else. So a few examples:

same1: <T, U> type = {
  this: std::false_type = ();
}
same1: <T> type<T, T> = { // 1: Have the specialized arguments defined in the type.
  this: std::true_type = ();
}

same1: <T> specialize <T, T> type = { // 2: Have a new keyword 'specialize' which defines the specialization 
  this: std::true_type = ();
}
same1: <T>  type = { 
  specialize: same1 = <T, T> // 3: Have a new keyword 'specialize' which defines the specialization in the body.
  this: std::true_type = ();
}
same1: <T>  type = { 
  same1: type == <T, T> // 4: Use the class name for the specialization.
  this: std::true_type = ();
}

I think 1. would have problems with the specialization of function templates. 2 keeps the specialization near the current location. 3 would be more in line with the new definition of inheritance. 4 might be the weakest one.

My preference would be 2.

MaxSagebaum avatar May 31 '23 07:05 MaxSagebaum

I think the original expected syntax is consistent with Cpp2's way of "declaration follows use". same<T, T>: <T> (specialization declaration) matches same<int, int> (use). same: <T, U> (primary declaration) matches same<i32, i64> (use).

same1: <T> type<T, T> = { // 1: Have the specialized arguments defined in the type.
  this: std::true_type = ();
}

Originally, this is what I was going to suggest. Eventually, I noticed it'd be inconsistent with variable templates, which don't have something like type to put the template-parameter-list.

same1: <T> specialize <T, T> type = { // 2: Have a new keyword 'specialize' which defines the specialization
  this: std::true_type = ();
}

~~To keep the template-parameter-list upfront, it could be:~~ Actually, first comes the template-argument-list (as lowered to Cpp1) before the template-parameter-list. Which matches "declaration follows use".

same1: <T, T> for <T> type = {
  this: std::true_type = ();
}

JohelEGP avatar May 31 '23 13:05 JohelEGP

Actually, first comes the template-argument-list (as lowered to Cpp1) before the template-parameter-list. Which matches "declaration follows use".

same1: <T, T> for <T> type = {
  this: std::true_type = ();
}

I think in this example the declaration follows use would be broken. First you declare the type, then the template-argument-list and afterwards the template-parameter-list. But the templates in the template-parameter-list would be used in the template-argument-list before they are declared.

In

same1: <T> specialize <T, T> type = { // 2: Have a new keyword 'specialize' which defines the specialization
  this: std::true_type = ();
}

we would follow the left to right principle: We declare a something with the name same1 it has the template-parameter-list <T> and is a specialization of the original template-parameters with the arguments <T, T> which is a type and has the definition ....

For a function it would look like:

f: <T> (a: T) -> T = { return 2 * a ; }

f: specialize <std::string> (a: std::string) -> std::string = {return a + a;}

MaxSagebaum avatar May 31 '23 13:05 MaxSagebaum

IIUC, it seems like it comes down to choosing tradeoffs.

Another thing to consider when choosing the syntax is that an specialization only need to match the primary's template-parameter-list.

t: @value <T> = { }
t<i32>: <> = { } // No metafunction.

JohelEGP avatar May 31 '23 14:05 JohelEGP

I like this, and will try to implement it. We can add any prefix keyword to introduce the template arguments of the specialization later.

//G template-specialization-argument-list:
//G     '<' template-argument-list? '>'
//G
//G unnamed-declaration:
//G     ':' meta-functions-list? template-parameter-declaration-list? function-type requires-clause? '=' statement
//G     ':' meta-functions-list? template-parameter-declaration-list? template-specialization-argument-list? type-id? requires-clause? '=' statement
//G     ':' meta-functions-list? template-parameter-declaration-list? type-id
//G     ':' meta-functions-list? template-parameter-declaration-list? template-specialization-argument-list? 'final'? 'type' requires-clause? '=' statement
//G     ':' 'namespace' '=' statement

JohelEGP avatar Aug 17 '23 15:08 JohelEGP

I'm going with the type prefix, which is much easier to parse.

Solved in the PR

There's an unsolvable ambiguity at the grammar level. You can't make a full specialization that uses a non-templated metafunction, since the <> is part of the metafunction's identifier.

t: @struct <> type<i32> type = { } // error: `struct<>` is not a known metafunction.

JohelEGP avatar Aug 17 '23 19:08 JohelEGP

If the obvious syntax is problematic, how about this specialization-declaration instead?

var_name<t_args>: type = value;   // Obvious syntax (challenging to lex/parse).
var_name: <t_args>: type = value; // Proposed syntax.
+//G specialization-declaration:
+//G     template-parameter-declaration-list ':' unnamed-declaration
+//G     template-parameter-declaration-list ':' alias
 //G
 //G unnamed-declaration:
 //G     ':' meta-functions-list? template-parameter-declaration-list? function-type requires-clause? '=' statement
 //G     ':' meta-functions-list? template-parameter-declaration-list? function-type statement
 //G     ':' meta-functions-list? template-parameter-declaration-list? type-id? requires-clause? '=' statement
 //G     ':' meta-functions-list? template-parameter-declaration-list? type-id
 //G     ':' meta-functions-list? template-parameter-declaration-list? 'final'? 'type' requires-clause? '=' statement
+//G     ':' specialization-declaration
 //G     ':' 'namespace' '=' statement
 //G
 //G alias:
 //G     ':' template-parameter-declaration-list? 'type' requires-clause? '==' type-id ';'
 //G     ':' 'namespace' '==' id-expression ';'
 //G     ':' template-parameter-declaration-list? type-id? requires-clause? '==' expression ';'
+//G     ':' specialization-declaration

Or:

 //G declaration:
 //G     access-specifier? identifier '...'? unnamed-declaration
 //G     access-specifier? identifier alias
+//G     access-specifier? identifier '...'? specialization-declaration

JohelEGP avatar Feb 01 '24 15:02 JohelEGP

As with past issues, the only problem with the "obvious" syntax is that it doesn't work in the global namespace. I can live with this:

std::common_type<my_type, my_type>: @struct type { type: type == my_type; }                 // Illegal
std: namespace = { common_type<my_type, my_type>: @struct type { type: type == my_type; } } // Legal

This will mostly come up in tests or slide code, where it's usual to not have namespaces.

JohelEGP avatar Feb 11 '24 13:02 JohelEGP