Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Cannot specity <const> or <in> in function template arguments #456

Closed
realgdman opened this issue May 13, 2023 · 17 comments · Fixed by #521
Closed

[BUG] Cannot specity <const> or <in> in function template arguments #456

realgdman opened this issue May 13, 2023 · 17 comments · Fixed by #521
Labels
bug Something isn't working

Comments

@realgdman
Copy link

I'm using library (flecs) with API like

ecs.system<Position, const Velocity>()
.each([] some lambda

And haven't found way to specify that const in template call to system()
Which, I believe, cannot be deduced from parameters of lambda

foo<int>();
foo<const int>(); //error
foo<int const>(); //error
foo<in int>(); //error

https://cpp2.godbolt.org/z/sjMWTq3b7

But according to this comment #425 (comment) , it works on pointers
https://cpp2.godbolt.org/z/Tx8e9KfKc
f<* const int>();

@realgdman realgdman added the bug Something isn't working label May 13, 2023
@realgdman realgdman changed the title [BUG] Cannot specity <const> or <in> [BUG] Cannot specity <const> or <in> in function template arguments May 13, 2023
@JohelEGP
Copy link
Contributor

I think it may be a bug in the parser.
For f<const int>(),
it's supposed to parse an expression or type-id before a , or >:

//G template-argument-list:
//G     template-argument-list ',' template-argument
//G
//G template-argument:
//G     # note: < > << >> are not allowed in expressions until new ( is opened
//G     expression
//G     type-id

Or maybe it parses const as an identifier and calls it a day, expression completed.
Just to find that it's not followed by , or >, and fails.

@AbhinavK00
Copy link

Some Questions:
How do you expect the behavior of <vec> to be different from <in vec>?
Also, how do you expect this to extend upto <inout vec> and related?

One more thing, I think it works with pointers because cpp2 thinks it's a pointer that doesn't change an object instead of a pointer that can't be reseated. Does that make sense?

@JohelEGP
Copy link
Contributor

How do you expect the behavior of <vec> to be different from <in vec>?
Also, how do you expect this to extend upto <inout vec> and related?

See #425 (comment).

One more thing, I think it works with pointers because cpp2 thinks it's a pointer that doesn't change an object instead of a pointer that can't be reseated. Does that make sense?

That's the other side of #456 (comment).
* can't start an expression,
so it's unambiguously the other production of a template-argument,
namely, a type-id.

@realgdman
Copy link
Author

How do you expect the behavior of to be different from ?

Ouch.
Thinking about it, when I call function, I expect f(a) work as f(in a) aka f(const a)
But when I call f<A>() what I should expect? It seems that I expect usual f<A>() and need a way to distinguish from f<const A>().
If that's not the case, what would it be? If in aka const is default, All common templates need to be written f<inout A, inout B>()?

@AbhinavK00
Copy link

All common templates need to be written f<inout A, inout B>()

I don't think so, as #425(comment) points out, a parameter direction does NOT operate on a type. And when you think about it,

F<out T>
F<move T>

do not make sense. So we need not to change the types in templates to atleast.
There are two ways after this, either dont allow parameter passing directives on non-type template parameters and call it a day OR think about how all of this should work and allow parameter passing directives on non-type template parameters.

@realgdman
Copy link
Author

OK I got where my misunderstanding about <in> coming from.
What I learned about project, it was about
foo(in x); and (inout i:=0) { ... } not allowing const.
But apparently you can write

i: const int = 0;
T: type = { j: const int = 0; }

That means some places use new syntax, some old, and thus <const T> should be accepted.

Hence I found workaround for my issue

i: int = 0;
ci: const int = 0;
foo<decltype(i)>();
foo<decltype(ci)>();
foo<decltype(ci)>(); //careful with last use - std::move(decltype(ci)) remove const

print nonconst, const, nonconst.
https://cpp2.godbolt.org/z/8E8MxjnKd

Regarding <in T>, this can be chalked from issue, and thought as discussion of further unifying syntax.

@JohelEGP
Copy link
Contributor

What I learned about project, it was about foo(in x); and (inout i:=0) { ... } not allowing const. But apparently you can write

i: const int = 0;
T: type = { j: const int = 0; }

That means some places use new syntax, some old, and thus <const T> should be accepted.

That's because they're orthogonal features,
and you can use parameter-directions in various contexts.

You can declare a
variable and by-copy parameter
as (const [pointer to]) const T.

You can declare a parameter as being in, inout, out, copy, move, and forward.

You can declare a return type to be by (some of) those.

You can
pass an argument and return an expression
by (some of) those.

@AbhinavK00
Copy link

I also learnt the same thing from this issue, I had fully forgotten about const being in cpp2 lol.

@JohelEGP
Copy link
Contributor

JohelEGP commented May 21, 2023

const is definitely being parsed as an expression.
See how const * i32 this isn't transformed like in a type-only context: https://cpp2.godbolt.org/z/46KezxeY6.

main: () = {
  x: const * int = g;
  v<const * int>;
}
auto main() -> int{
  int* const x {g};
  v<const * int>;
}

One way to fix this is to introduce alternative grammars that are definitely not something else.
After applying #387 (comment)
(note that this clashes with #463's identifier:type expression, so further disambiguation is required):

+//G unqualified-type-id:
+//G     id-expression
+//G     function-type

+//G definite-type-id:
+//G     type-qualifier-seq unqualified-type-id
+//G     definite-function-type

 //G type-id:
-//G     type-qualifier-seq? id-expression
-//G     type-qualifier-seq? function-type
+//G     type-qualifier-seq? unqualified-type-id

 //G template-argument:
 //G     # note: < > << >> are not allowed in expressions until new ( is opened
+//G     definite-type-id
 //G     expression
 //G     type-id

+//G definite-parameter-declaration:
+//G     parameter-direction unnamed-declaration
+//G     this-specifier? parameter-direction? declaration
+//G     this-specifier parameter-direction? identifier

 //G parameter-declaration:
 //G     parameter-direction? unnamed-declaration
 //G     this-specifier? parameter-direction? declaration
 //G     this-specifier? parameter-direction? identifier

+//G definite-parameter-declaration-list
+//G     '(' definite-parameter-declaration-seq ')'

+//G definite-parameter-declaration-seq:
+//G     definite-parameter-declaration
+//G     definite-parameter-declaration-seq ',' definite-parameter-declaration

+//G definite-function-type:
+//G     definite-parameter-declaration-list throws-specifier? return-list?

 //G function-type:
 //G     parameter-declaration-list throws-specifier? return-list? contract-seq?

 //G return-list:
 //G     '->' type-id

And similarly for #387 (comment):

+//G inconclusive-parameter-declaration:
+//G     unnamed-declaration
+//G     parameter-direction identifier
+//G     identifier

+//G inconclusive-parameter-declaration-list:
+//G     inconclusive-parameter-declaration
+//G     inconclusive-parameter-declaration-list ',' inconclusive-parameter-declaration

+//G inconclusive-type-id:
+//G     '(' inconclusive-parameter-declaration-list ')'

 //G is-as-expression:
 //G     prefix-expression
+//G     is-as-expression is-definite-value-constraint
 //G     is-as-expression is-type-constraint
 //G     is-as-expression is-value-constraint
 //G     is-as-expression as-type-cast
 //GTODO     type-id is-type-constraint

+//G is-definite-value-constraint
+//G     'is' inconclusive-type-id

@JohelEGP
Copy link
Contributor

The suggested complication above
is to make f<const i32>() work
while keeping the grammar context-free.

The alternatives I've considered are (assuming no function type support):

  • i32 as const to work and cast i32 to const (inconsistent).
  • const type-id to be an expression
    that is semantically required to occur in a context
    where type-id is an alternative production.

@JohelEGP
Copy link
Contributor

Here's a simpler alternative, with support for function types.

Proposal parameter-declaration-list achieves feature parity with Cpp1.
It makes the name of a parameter optional in a declaration or type.

The alternative maintains the status-quo: Parameters of a declaration can't be unnamed.
Introduce the converse for types: A function-type-id whose parameters must be unnamed.

First, define function-type-id:

 //G type-id:
 //G     type-qualifier-seq? id-expression
+//G     type-qualifier-seq? function-type-id

 //G parameter-declaration:
 //G     this-specifier? parameter-direction? declaration
 //G     this-specifier? parameter-direction? identifier
+//G parameter-declaration-type:
+//G     parameter-direction? unnamed-declaration

 //G parameter-declaration-list
 //G     '(' parameter-declaration-seq? ')'
+//G parameter-declaration-type-list
+//G     '(' parameter-declaration-type-seq? ')'

 //G parameter-declaration-seq:
 //G     parameter-declaration
 //G     parameter-declaration-seq ',' parameter-declaration
+//G parameter-declaration-type-seq:
+//G     parameter-declaration-type
+//G     parameter-declaration-type-seq ',' parameter-declaration-type

 //G function-type:
 //G     parameter-declaration-list throws-specifier? return-list? contract-seq?
+//G function-type-id:
+//G     parameter-declaration-type-list throws-specifier? return-list?

Then, to fix this issue (#456), update template-argument:

 //G template-argument:
 //G     # note: < > << >> are not allowed in expressions until new ( is opened
+//G     'const' type-id
+//G     function-type-id
 //G     expression
 //G     type-id

With that:

v<const int, // Type.
  (:int), // Function type.
  ((:int=0))>; // Value `0`.

And to workaround #387 (comment):

x is (x); // Same as today.
x is (:int); // Asks if "`x` has type `(:int)`".
x is ((:int=0)); // Asks if "`x` is `0`".

@JohelEGP
Copy link
Contributor

Or maybe just add function-type-id as a synonym for function-type,
and require during semantic analysis that its parameters match parameter-direction? unnamed-declaration.

 //G type-id:
 //G     type-qualifier-seq? id-expression
+//G     type-qualifier-seq? function-type-id

 //G function-type:
 //G     parameter-declaration-list throws-specifier? return-list? contract-seq?
+//G function-type-id:
+//G     # Note: Parameters must match `parameter-direction? unnamed-declaration`.
+//G     function-type

 //G template-argument:
 //G     # note: < > << >> are not allowed in expressions until new ( is opened
+//G     'const' type-id
+//G     function-type-id
 //G     expression
 //G     type-id

@JohelEGP
Copy link
Contributor

JohelEGP commented May 31, 2023

Let's assume that in should be a valid parameter name (see #397).
The simplification above means that the meaning of in in (in) and (in: _)
changes depending on whether it's a function-type or function-type-id.
For the former, it's a parameter name, and for the latter, a parameter-direction.

@JohelEGP
Copy link
Contributor

After some thinking and recalling some of Herb's comments,
my impression is that Cpp2 f<int>() should continue working just like in Cpp1.
That is, the result of unambiguous parsing doesn't necessarily guide semantic analysis.

So if Cpp2 f<const int>() and f<* int>() (#502) is to work
as in Cpp1 and the Cpp1 equivalent, respectively,
the grammar for template-argument needs to be extended to account for these uses.

@JohelEGP
Copy link
Contributor

For #492 (comment), I wrote

operator_is: <T, F> (v: T, f: F) -> bool requires std::predicate<decltype((f)), decltype((v))> = { return std::invoke(f, v); }

I take advantage of decltype((x)) to form a type that is an lvalue reference to the type of x.

We've already mentioned
that in T can't work, because the parameter directions work on expressions.

But wanting to specify reference types for template arguments
made me think it'd be convenient to specify them just like a parameter declaration.
Based on the first illustration at https://github.com/hsutter/cppfront/wiki/Design-note%3A-Postfix-operators,
a function type will probably look like (:i32) -> * (:i32) -> std::string
(See #387 (comment) for some proposals).
So the idea is to be able to specify type template arguments using the same parameter declaration syntax.
Re-specifying the operator_is above:

operator_is: <T, F> (v: T, f: F) -> bool requires std::predicate<in: T, in: F> = { return std::invoke(f, v); }

@JohelEGP
Copy link
Contributor

Although : is supposed to introduce names, so that doesn't work quite well.

@MaxSagebaum
Copy link
Contributor

I think in one talk Herb metioned also, that he would like to introduce specifications on the calling side like

f: (out o: int, in i: int) -> void;
a: int = 1;
b: int = 2;
f(out a, in b);

If this is introduced, then it would only be logical that template parameters could also be specified in with these modifiers without using :.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants