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

Wildcards and primary constructor syntax #3720

Closed
kallentu opened this issue Apr 23, 2024 · 11 comments
Closed

Wildcards and primary constructor syntax #3720

kallentu opened this issue Apr 23, 2024 · 11 comments
Labels
feature-completeness A special or edge case of another feature which isn't supported

Comments

@kallentu
Copy link
Member

Trying to nail down the last few cases of wildcards. I'll regurgitate the issues and options that @munificent wrote to me and bring it up for discussion:

In the proposal, it states wildcards apply to local variables and parameters, but not to top-level variables and class members.

In an extension type, the primary constructor syntax looks like a parameter but defines a field. So if you have a parameter there named _, does it behave like a wildcard or like a field?

Likewise, when/if we add general primary constructor syntax, what does a parameter named _ mean?

Options:

  1. Yes, the wildcard proposal applies and it gets no name that you can use. Probably not very useful, but consistent with the syntax which does look like a parameter declaration.

  2. No, the wildcard proposal does not apply since you're defining a field. You get a field named _. Probably useful.

  3. Avoid the situation entirely by making it an error to have a parameter named _ in a primary constructor or extension type.

@dart-lang/language-team It would be good to make a decision on this and pick an outcome. Your input please?

@kallentu kallentu added the feature-completeness A special or edge case of another feature which isn't supported label Apr 23, 2024
@lrhn
Copy link
Member

lrhn commented Apr 24, 2024

Disallowing using _ in an extension type is breaking. But the entire feature is potentially breaking, and it will be language versioned, so that's not a blocker. (I'll be sorry, but I'll cope.)

Allowing _ is is consistent with allowing _ as a field or getter name. I'd prefer to do that, and have it declare the field.

That leaves the question of whether it introduces a local variable in the constructor.
That's moot for an extension type "primary constructor", because the constructor has no body, but might apply to other primary constructors in the future, and to initializing formals of any constructor.

That is:

class C {
  final int _;
  C(this._) : assert(_ > 0);
}

I would say that this._ works, because that's not a wildcard variable and we do need to initialize the field, but it will not introduce a local variable that the assert can reference.

About naming of the parameter, if we introduce "public name of private initializer", that will, and can, only apply when removing the leading underscore leaves a valid identifier. So it wouldn't apply here, the parameter name is still _, and it cannot be a named parameter.

What about function types?
If I write

typedrf F = void Function(int _);

is that allowed?
(Sure, why not, the name has no effect, and nothing prevents you from writing

  void foo(int _) {}

either, so it might even be accurate.)

All in all, I'd treat a primary constructor parameter named _ as a field and an initializing formal of that name, and both are valid. The initializing formal will not introduce a local variable, though.

And you can do

  C(super._, super._);

to forward two positional parameters to a superclass constructor, and get no local variables in the initializer list.

@eernstg
Copy link
Member

eernstg commented Apr 24, 2024

I think it's relevant to mention the proposal to allow an initializing formal to have a private name syntactically, and implicitly derive a public name which is the parameter name that is actually used at call sites:

class A {
  final int _i, _j;
  A({this._i}) : _j = _i; // `_i` can be used in initializers.
}

void main() => A(i: 10); // The name is `i` at invocations.

We discussed this in #2509, and #3058 is related as well.

If we adopt this proposal then it would be an error to have an initializing formal parameter whose name is _ (because it doesn't have a corresponding public name). This would be true for both positional and named parameters. For formal parameters that aren't initializing, any name starting with _ would be a compile-time error.

If we apply these rules to primary constructors (where the regular formal parameter declaration is desugared to an initializing formal and an instance variable declaration) then it would be an error to use _ as a primary-constructor-parameter name.

This certainly makes sense to me for named parameters.

With positional parameters it would actually make sense to say that (1) the syntactic parameter name _ is used to declare an instance variable whose name is _, but (2) it is used to create a wildcard-named formal parameter. So we'd just use the syntax as written, and interpret it in two different ways for the instance variable and for the formal parameter. A similar relaxation could be made for positional parameters in general (that is, in anything other than a primary constructor).

So I'd recommend that we allow positional primary constructor parameters named _ and interpret them to specify a wildcard named parameter and an instance variable named _, and we make it an error to use the name _ for a named primary constructor parameter.

@eernstg
Copy link
Member

eernstg commented Apr 24, 2024

Update: The adjustments I mentioned above have already been made in this proposal by @lrhn.

The short version would then be: _ can be used as the syntactic name of a positional formal parameter in a primary constructor. It introduces an instance variable named _ and it serves as a wildcarded name for the formal parameter. It is a compile-time error if _ is used as the syntactic name of a named parameter (in primary constructors and elsewhere).

At this time it doesn't matter that a positional primary constructor parameter with syntactic name _ is wildcarded, if that primary constructor is part of the header. But it matters for a primary constructor in the body, because it can have an initializer list and a superconstructor invocation, and it matters if we add support for superconstructor invocations in the header, like class const Point3D(int x, int y, int z) extends Point(x, y);.

@eernstg
Copy link
Member

eernstg commented Apr 30, 2024

The spec side of this issue is handled in #3729.

@DanTup
Copy link

DanTup commented Sep 10, 2024

I was looking at a change to add semantic token modifiers to wildcards so that they could be styled differently by users (dart-lang/sdk#56567).

While doing so, I found that the analyzer considered field formals to not be wildcards, but the spec seemed to imply that they could be:

It includes all parameter kinds, excluding named parameters: simple, field formals, and function-typed formals, etc.:

While trying to find details about the rules here, I found @lrhn's comment above which says (emphasis mine):

I would say that this._ works, because that's not a wildcard variable and we do need to initialize the field, but it will not introduce a local variable that the assert can reference.

So I'm a little confused about the rules. In particular, it's not clear to me what a "wildcard variable" exactly is, if it's not the same as something that doesn't introduce a name that can be referenced. If I wanted my IDE to color wildcard variables differently to non-wildcard variables, would the _ in this._ be coloured as a wildcard or not?

class C {
  final int _;
  C(this._);
}

Thanks!

@eernstg
Copy link
Member

eernstg commented Sep 10, 2024

The initializing formal is an ambiguous case: Usually, the name of an initializing formal can be used to denote the formal parameter in the initializer list:

class A {
  final int x, y;
  A(this.x): y = x + 1;
}

However, we can use an initializing formal of the form this._ to initialize an instance variable whose name is _, and the parameter still doesn't introduce a name into the initializer list scope:

class A {
  final int _, y;
  A(this._): y = _ + 1; // Error, cannot access `this._` here.
}

It can be argued that the initializing formal declaration should be highlighted as a wildcard (because the parameter doesn't introduce a name into the scope where this normally goes), and also that it should be highlighted as an identifier, because it's clearly associated with the instance variable with that name. I don't know what's more convincing. Semi-transparent? ;-)

@DanTup
Copy link

DanTup commented Sep 10, 2024

I guess I was more interested in the definition than how this applies specifically to colouring. The analyzer has an existing extension getter isWildcardVariable that I was using for the highlighting but I noticed it specifically excluded field formals (I had written a test that expected them to be wildcards based on how I read the spec).

The current behaviour works fine for me (they would not be coloured as wildcards, and I don't think this would be confusing to anyone), but it made me wonder if the definition of isWildcardVariable was correct or if there might be other code using that value that might not behave as expected. I had a scan through the other places in the analyzer using isWildcardVariable though I don't understand all of that code well enough to know whether the exclusion of field formals is right or wrong.

@bwilkerson given the info above, do you think there may be any issue here, or does excluding field formals in isWildcardVariable seem like it will be correct for the other code using it? (it's not an issue for semantic tokens, I can just write the test based on the implementation that satisfied the other use of this value).

@bwilkerson
Copy link
Member

I took a brief look at the list linked above, and all of those uses of the extension getter seem reasonable to me.

I think we shouldn't highlight an occurrence of this._ as being a wildcard because it isn't, it's a reference to the field.

@DanTup
Copy link

DanTup commented Sep 10, 2024

@bwilkerson sgtm, thanks!

@lrhn
Copy link
Member

lrhn commented Sep 10, 2024

There are no wildcard variables. The entire point of the "unnamed variables" feature is to not actually introduce a variable.

A "wildcard variable declaration", or more precisely an unnamed variable declaration is a declaration which looks like a variable declaration with a declared name of _, but it doesn't actually introduce a variable.
There is a declaration, but not a variable.
So the isWildcardVariable predicate is probably answering whether something is a "wildcard variable declaration".

Obviously we're being imprecise about this and omitting the "declaration" all the time. We probably don't even agree on the underlying model, just the visible behavior, so this is, like, my opinion.
It's not the canonical Truth

One could argue that it does introduce a variable, that variable just doesn't introduce a name into the surrounding scope, so there is no way to refer to the variable.
Potato, potatoh. That's indistinguishable from not actually being a variable. (And if we make unnamed non-nullable optional parameters not need a default value, it's really like there is no variable.)

An initializing formal like this.foo has two effects during "binding actuals to formals": store that argument value in the instance variable named foo, and introduce a final local variable named foo bound to the same value into the initializer list scope.

The syntax is two things at the same time.

An initializing formal for an instance variable named _, this._, does initialize the instance variable, but it introduces no local variable. It's only syntax for one thing.

If the isWildcardVariable predicate answers the question "is this a declaration with a declared name _, which doesn't introduce a name into the surrounding scope?", then this._ should say yes.
If it answers "does this declaration do nothing?", the answer is no.
If it just answers "should this be colored as an unnamed variable?", you'll have to decide on which behavior we want.

I'd probably want to colorize this.. differently from _, and then I'm not sure how to colorize _.

@DanTup
Copy link

DanTup commented Sep 11, 2024

If the isWildcardVariable predicate answers the question "is this a declaration with a declared name _, which doesn't introduce a name into the surrounding scope?", then this._ should say yes.

I think this is the intention, and I think for colouring we should use this same definition.

I'd probably want to colorize this.. differently from _, and then I'm not sure how to colorize _.

Yep, this is the case today, terms like this and super are coloured like keywords (dark blue in my theme). Currently the _ is coloured as a variable (light blue in my theme), but the intention here is to mark them in a way that users could configure their theme to colour them differently (for ex. faded).

image

Thanks for the insights, they've been very helpful! :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature-completeness A special or edge case of another feature which isn't supported
Projects
None yet
Development

No branches or pull requests

5 participants