cppfront
cppfront copied to clipboard
[BUG] Add support for defining template specializations.
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
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 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.
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 = ();
}
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.
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 = ();
}
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;}
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.
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
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.
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
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.