Skip to content

Is f<int>.toString() a method invocation? #1802

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

Closed
eernstg opened this issue Aug 12, 2021 · 9 comments
Closed

Is f<int>.toString() a method invocation? #1802

eernstg opened this issue Aug 12, 2021 · 9 comments
Labels
constructor-tearoffs question Further information is requested

Comments

@eernstg
Copy link
Member

eernstg commented Aug 12, 2021

This section indicates that when C<T> is a type literal, C<T>.toString and C<T>.toString() is a constructor tearoff respectively a constructor invocation, which causes a compile-time error when C does not have a constructor named C.toString.

This is consistent with a very old rule: C.toString() is an error when C does not have a static member named toString, and if we want to call the Object instance method we need to use (C).toString().

However, when f denotes a function declaration we have a similar constructs f<T>.toString and f<T>.toString(). In this case there is no interpretation of the term which involves a constructor (tearoff or invocation), and hence it seems odd that we would insist that it is an operation that involves a constructor, and then report an error when there is no constructor named f.toString.

Hence, it seems reasonable that we should

  • Accept terms like f<T>.toString and f<T>.toString(), and consider them to be a tearoff / an invocation of the instance member toString. Note that this may be useful, because we might also have some applicable extension methods.
  • Declare that such terms are a compile-time error (we can write (f<T>).toString()), but report an error which is more meaningful than "there is no such constructor".

WDYT, @munificent, @JakeMac, @lrhn, @leafpetersen, @natebosch, @stereotype441?

PS: Unblock dart-lang/sdk#46887 when this issue has been resolved.

@lrhn
Copy link
Member

lrhn commented Aug 12, 2021

For a class, C<T>.toString() should only be a constructor invocation. I don't want C<T>.named() to be a constructor invocation and C<T>.toString() to be an instance member invocation on the Type object for C<T>.
So, anything starting with C.name or C<T>.name must be referring to a non-instance member of C. The former can be a constructor or static, the latter only a constructor.

When the receiver does not start with a class name, then it's not a static lookup, and f<T>.toString() can be valid.
We could also disallow it and require you to write (f<T>).toString(), but that feels wrong, because f<T> is already guaranteed to denote an object, and (f<T>) is the same object. (As opposed to C which denotes a class and (C) which denotes an object).

So, all in all, I'm fine with accepting the syntax in general, allowing f<T>.toString() to denote an instance member invocation on the value f<T>, while still being strict that C<T>.anything is a static reference.

What worries me is type aliases.

typedef D = C<int>;
typedef F = void Function()
...
   D.toString(); // static or constructor invocation on `C<int>`.
   F.toString(); // instance member invocation on `Type`.

Here you have to look up what the alias refers to in order to see whether .toString will be a static lookup or an instance lookup.
I guess that's unavoidable the way we've defined type aliases and allowed static access through those that are "aliased for classes or mixins".

@eernstg
Copy link
Member Author

eernstg commented Aug 12, 2021

@lrhn wrote:

What worries me is type aliases.

typedef D = C<int>;
typedef F = void Function();
...
  D.toString(); // (1).
  F.toString(); // (2).

I think there are several reasons why we might want to maintain that "<type> '.' <identifier> <arguments>?" should never invoke (or tear off) an instance member on Type:

We have already specified that D.m() denotes an invocation of a static method C.m if it exists, and it is the instance creation C<int>.m() if C declares a constructor named C.m. Moreover, there's the old rule which says that int.toString() must be written (int).toString() in order to invoke the instance member (because we don't want "<type> '.' <identifier> <arguments>?" to invoke an instance method on Type once in a blue moon, and a non-instance member in all other cases).

I tend to think that F.toString() should be included, that is: F is a type, so F.toString() is an error (because function types do not have static methods, and they do not have user-accessible constructors). This means that we're "wasting" the syntax F.toString(), but the semantics of (F).toString() isn't that amazing, either, so who cares that it isn't optimally easy to get it.

The point is that we have a simple rule: "<type> '.' <identifier> <arguments>?" invokes a function which is not an instance method.

I'm happy to support the notion that we make a distinction and say that "<function> '.' <identifier> <arguments>?" is an instance member invocation (or an extension member invocation) on the given function. This is again consistent with current semantics: print.toString() is allowed.

[Edit: Added backquotes to the pseudo sentential forms such that <...> doesn't disappear.]

@lrhn
Copy link
Member

lrhn commented Aug 13, 2021

It's not on Type, but on a type, that we may want to not call Type instance members.
We definitely want Type t = int; t.toString(); to work, because an object of type Type is still just an object.

That's probably a good idea.

We can say that if an expression e denotes a type, which basically means it's an identifier T (potentially qualified) denoting a type declaration (class/mixin/type alias) or denoting a type variable, and possibly instantiated with type arguments, then T.name/T<A>.name does not convert T/T<A> to a Type object before trying to look up name.

That means that if T is a type variable, then all lookups fail (type variables are types themselves in the static type system, but cannot have static members because they don't represent a single type declaration).
If T denotes a class or mixin, or a type alias for a class or mixin, T.name does static lookup on the declaration denoted by T.

If e does not denote a type, like it's f<int> in f<int>.toString() and f denotes a function, then the value of e is always and only an object, so the call is an instance member invocation. (So, we need to define "denotes a type" precisely, and say that if e denotes a type, then e.something is a static member access).

(The big questions are then whether int?.toString() and int..toString() should agree or differ. They currently evaluate the receiver to an object, which means that they do instance member invocations. I've occasionally wanted to be able to cascade static invocations on the same class. I've never needed int?.toString(), but it's once character shorter than (int).toString(), and doesn't need backtracking while writing to place the (, so you just know someone will use it if it's there!)

@eernstg
Copy link
Member Author

eernstg commented Aug 13, 2021

It's not on Type, but on a type

Yes, that's a good way to say it! And we certainly want to allow e.toString() when e is an expression of type Type which isn't a type literal.

For int?.toString() we already specify that it is treated as int.toString() because int is a type literal, and then the rule discussed above kicks in and makes it an error because we don't invoke instance members on a type. I think that rule is reasonable, int?.toString() is an anomaly in any case, because int isn't null when evaluated to a Type object.

With int..toString(), we specify cascades in terms of their desugaring into constructs using . or ?., so the consistent rule would also be to make it an error for the same reason as the others.

@lrhn
Copy link
Member

lrhn commented Aug 16, 2021

I've always thought String?.foo should be an error because String as a Type is not nullable (we only made that a warning in general, though, but I still think this one should be an error. It's useless and confusing.)

For cascades we desugar into evaluating the expression to a value, then calling multiple methods on that value (and finally evaluating to the value). This avoids evaluating the receiver more than once, but also ensures that it is evaluated to a value, and that the invocations are instance member invocations.

We haven't, so far, disallows String..toString()..runtimeType from working. I don't think anyone would be hurt by it not working any more, and in fact, I'd probably love to be able to do multiple static calls on the same type as a cascade. It's a change, though. (See also https://github.com/dart-lang/language/blob/master/working/specification/compositional%20semantics.md).

@eernstg
Copy link
Member Author

eernstg commented Aug 16, 2021

I've always thought String?.foo should be an error

This makes sense today. But it was probably not equally obvious when we introduced ?. (we had it in March 2015, at least). So it would be a breaking change at any time during the work on null safety, though probably a low-impact one.

I'd like to make it an error, too!

For cascades we desugar into evaluating the expression to a value, then calling multiple methods on that value

Right, but we specify the semantics of an ordinary method invocation in the same way, evaluating the receiver expression to an object first.

I think the consistent approach would be to special-case cascades where the receiver is a type literal: It can be used to call one or more static methods, and it is an error to attempt to use it to call one or more instance methods on Type.

We haven't, so far, disallows String..toString()..runtimeType from working

Right, but I believe we're in a good position to fix the spec, because the analyzer already flags both invocations as an error in the following example (and the first one is already worded as if we had the same rule as we do for String.toString()):

void main() {
  String..toString()..runtimeType;  
}

@lrhn
Copy link
Member

lrhn commented Aug 16, 2021

Right, but we specify the semantics of an ordinary method invocation in the same way, evaluating the receiver expression to an object first.

True. Then after we've evaluated the value, we check whether the original expression was a type literal, and then ignore that value (which is safe since evaluating it has no side effects, so the compiler can just not do it anyway).

We could do that for cascades too, but I don't think we do. (At least, it doesn't work to call static members on a type).

the analyzer already flags both invocations as an error

Wohoo. Let's do it!

@eernstg
Copy link
Member Author

eernstg commented Aug 16, 2021

after we've evaluated the value, we check whether the original expression was a type literal

That doesn't match the spec language: It special cases up front how to deal with (including: how to execute) C.m... where C is a type literal.

Anyway, let's do it! ;-)

@eernstg
Copy link
Member Author

eernstg commented Aug 26, 2021

Conclusions from language meeting:

  • When f is an expression whose static type is a generic function type, and f<int> is an explicit generic function instantiation that is not a compile-time error, expressions like f<int>.m() and f<int>.m can be used to invoke or tear off members of the given function object (that is, members of Object, plus the given call method, plus any applicable extension methods).
  • When C is a type literal, C<int> is regular-bounded parameterized type, and C.m is the name of a constructor, an expression of the form C<int>.m(...) denotes an instance creation and C<int>.m denotes a constructor tearoff, and they are not errors. When C.m is not a constructor name, those forms are a compile-time error, even in the case where Type has a member named m (for instance, C<int>.toString() is an error unless C.toString is a constructor).

This is in line with the current specification wording, so we will not make any changes.

The treatment of cascades (.., ?..) and conditional member accesses (?.) on type literals has not been determined, but this will be discussed elsewhere (#1819).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
constructor-tearoffs question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants