Closed kallentu closed 6 months ago
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.
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 https://github.com/dart-lang/language/issues/2509, and https://github.com/dart-lang/language/issues/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.
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);
.
The spec side of this issue is handled in https://github.com/dart-lang/language/pull/3729.
I was looking at a change to add semantic token modifiers to wildcards so that they could be styled differently by users (https://github.com/dart-lang/sdk/issues/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!
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? ;-)
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).
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.
@bwilkerson sgtm, thanks!
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 _
.
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?", thenthis._
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).
Thanks for the insights, they've been very helpful! :-)
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:
Options:
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.
No, the wildcard proposal does not apply since you're defining a field. You get a field named
_
. Probably useful.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?