-
Notifications
You must be signed in to change notification settings - Fork 213
After all these years, should we support d()
-> d.call()
even in the case where call
is a getter?
#3482
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
Comments
d()
-> d.call()
even in the case where call
is a getter?d()
-> d.call()
even in the case where call
is a getter?
TL;DR: No. The dynamic invocation case of special in that we don't know whether In all other cases, the type signature of the receiver makes it one or the other. The underlying design principle of dynamic member invocations is that they should work the same way (within reason) as the same member invocation on the same receiver, if it had had its runtime type as static type. That otherwise does not involve things happening during type inference, or any coercions.
will not infer a type argument, it creates a The So we have a specified behavior of calling callable non-function objects, which we apply to dynamic invocations too. Which is explainable. And then implementations go one step further, and allow a dynamic invocation of an object which has no I still prefer to keep the current specification, allow the implementations to implement it, categorize the extra behavior of existing implementations as legacy bugs, and let them remove it if they want to. |
Just want to check if this is also on the list of things to discuss for the language team. If language team prefers to keep the currently specified behavior we need to slate necessary work in CFE and backends to pay off this technical debt. |
The language team did discuss this issue today, but we did not reach a conclusion. Hopefully we will soon. |
Per @nshahan the current implemented semantics for dart2js, VM and DDC are as follows:
So either way we have some work to do in the implementations. I believe that the first example using The second example using |
The current CFE encoding of both |
Why is it important to prevent the |
+1 to this question. (There is one case of this in VM's core libraries - |
The stack overflow isn't so much the problem, as it is a symptom that something more complicated than necessary is going on in what looks like a simple function call. class C {
static int ctr = 0;
C get call { if (ctr & ++ctr == 0) print(ctr); return this; }
}
void main() {
foo(C());
}
foo(dynamic f) {
f(); // Looks innocuous. Stack-overflows, or does an infinity of work with only a single call.
} And does stack-overflow in dart2js and the JIT VM:
I agree that the AoT VM doesn't seem to stack overflow. But it's not that problem that makes me want to change implementation. It's more important to me to follow the guiding principle of dynamic invocations, which is that the invocation works like the similar invocation would, had the receiver had its runtime type as static type. But that'll be without any compile-time coercions, since it's happening at runtime. Allowing a Doing things dynamically should not be more powerful than casting to the actual type and doing the same thing. I don't want Taking the code above, C c = C();
c(); // Compile-time error, `C` is not a function type and not a callable type. No `.call` insertion.
c.call(); // Compile-time error, c.call has type `C`, still not callable.
dynamic d = c;
d() // calls `call` method forever.
d.call() // calls `call` method forever. To match the intended behavior, the last two should be runtime errors.
That is, the first item should match the actual Eagerly expanding |
@lrhn I see. Thanks - this makes sense to me. I was looking at it from the perspective of To implement this behavior we need to retain the difference in Kernel AST and then retain this information in the generated code i.e. use different lookup stubs for It is not clear to me if the same applies to JS backends: you need to carry over the difference between But again this looks like a lot of hassle - and it is unclear if it worth it. So while I get the argument that we might want |
But where does it end? Does I want to say no, but if Do we insert the Restricting |
|
The language team decided on Jan 3 that it is preferable to get the specified semantics implemented (that is, an implicitly invoked @mkustermann, @mraleph, @sigmundch, @nshahan, @osa1, considering the comment from @johnniwinther here, how much of an effort do you think it would be to use this new |
cc @rakudrama |
I don't expect we will need any major changes at compile time to support the new representation from the CFE. Supporting the new representation could also be the right time to revisit the runtime library to implement support for the dynamic invocations of |
This should be a straightforward change in dart2wasm. I think it will require a special forwarder for the "call" method members, to be used in dynamic invocations of other members. Currently in dart2wasm, the code generated for a dynamic invocation of a method |
My initial take is that this could be complicated change for dart2js. For dart2js, this is effectively a new feature - splitting dynamic calls from instance calls. Today, dart2js maintains the equivalence between dynamic an instance calls ( The upgrade might be worth it, the cost might be less than I initially think (e.g. by special-casing the construct - adding technical debt rather than paying it down), but please plan of this taking multiple releases and having quite a high cost. I'm busy with some more urgent issue right now but I can come back to this after those have been addressed. |
Thanks for the info, @rakudrama, that's very important to have in mind! It would be great, however, if you could comment on a couple of questions about this situation. My understanding is that the relevant terms ( I do understand that it may be a substantial effort to handle the new kind of Kernel AST nodes and propagate the new information to all parts of dart2js, but it does seem likely to me that the required information is available when it has been propagated from the Kernel AST and into all parts of dart2js. I don't quite understand how this change (we now have 2 kinds of dynamic invocations in Kernel rather than 1) can disrupt the relationship between dynamic invocations and statically checked instance invocations. I understand that those two actions may be modeled identically in dart2js (I'm not quite sure I understand how that's possible, but bear with me ;-). In the end, the fact that is most surprising to me is that this difficulty is created by a change which was seen by the language team as a step in the direction of making the semantics of statically checked and dynamic invocations more consistent, not less. Anyway, sorry about ranting about dart2js internals that I know so little about. Comments are very welcome! ;-) |
After chatting w/ @rakudrama and @leafpetersen I feel OK moving forward with the language decision #3482 (comment) with the understanding that fixing this bug is not an immediate priority for backend teams (e.g. P3). This would mean to keep the spec and tests as they are. This aligns static & dynamic semantics, but implies that backends have an implementation bug going forward. Circling back to the cost question - I believe the cost for fixing this in dart2js can be contained by special casing what we do for |
I've landed https://dart-review.googlesource.com/c/sdk/+/346240 which adds an |
According to the spec, a call in the form e(a0,...,aN) where static type of 'e' is 'dynamic' should succeed only if (1) 'e' evaluates to a function, or (2) runtime type of 'e' has a 'call' *method*. If runtime type of 'e' has a 'call' getter this invocation should fail with NSM. This behavior is different from 'e.call(a0,...,aN)' which accepts 'call' getters. --- In order to implement this behavior in the VM, a special 'dyn:implicit:call' selector is added. It behaves similarly to 'dyn:call' except when looking for a getter target. This selector is used when CFE sets FlagImplicitCall on a DynamicInvocation node. TEST=co19/Language/Expressions/Function_Invocation/Function_Expression_Invocation/call_A04_t01 TEST=co19/Language/Expressions/Function_Invocation/Function_Expression_Invocation/call_A04_t02 Fixes #59965 Issue #59952 Issue #51517 Issue dart-lang/language#3482 Change-Id: Ic45f7743ad75571476642dcec9c91e6a77e8e321 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/407161 Reviewed-by: Slava Egorov <[email protected]> Commit-Queue: Alexander Markov <[email protected]> Reviewed-by: Ryan Macnak <[email protected]>
VM is fixed to conform to the spec in dart-lang/sdk@4305541. |
[Edit: The language team decided on Jan 3 2024 that the answer is "No" (see this comment).]
See dart-lang/sdk#51517 for some background information.
The language specification has specified for at least 6 years that an invocation of the form
e<typeArgs>(args)
wheree
has static typedynamic
must handle a calleeo
(the value ofe
) which is not a function object in the following way:o
is an instance of an interface type that has a method namedcall
.o.call<typeArgs>(args)
.(and this includes a similar rule where there are no actual type arguments and/or no actual value arguments because "
typeArgs
andargs
can be empty".)However, the implementations (at least the VM, the JavaScript output from
dart compile js
, and the executable provided bydart compile exe
, on x64) supports the following scenario as well:o
is an instance of an interface type that has a getter namedcall
.o.call
, to obtain an objectfo
.fo<typeArgs>(args)
(which may run the same algorithm recursively).The language team has had discussions about this behavior previously (a long time ago), and did not support the generalization. That is, the language team does not want the second scenario to be supported.
However, many backends (perhaps even all of them?) support the second scenario, and @mraleph suggested in dart-lang/sdk#51517 that we should change the specification such that the second scenario is allowed: (1) This is the most convenient decision because this behavior is implemented today, and (2) it would be a breaking change to stop supporting scenario 2.
I believe we had some arguments about the potential performance implications of supporting scenario 2, but at this time I can't see any reason why this would be important. In particular, I can't see why supporting scenario 2 would cause function invocation to be slower in all cases, or function objects would occupy more space in all cases; it seems more likely that it just makes dynamic function invocations more expensive, and they are already allowed to be expensive because we assume that they are rare (and should be even rarer ;-).
@dart-lang/language-team, WDYT? Should we change the specification such that scenario 2 is supported? Alternatively, should we initiate a breaking change process about scenario 2 being unsupported in the future?
@mkustermann, @mraleph, @sigmundch, @nshahan, @askeksa, @osa1, WDYT? Are you aware of any optimization opportunities that are made impossible in the case where the language supports scenario 2? And assuming that it is correct that we do this already, it's more like: Are you aware of any optimization opportunities that we could benefit from in the future if we stop supporting scenario 2?
@johnniwinther, WDYT? As far as I understand @osa1's comment, the CFE generates code where it is impossible to make the distinction between the invocation of a method and the invocation of a getter that yields a function object which is subsequently invoked. I thought that this had now been separated out into two distinct kinds of Kernel code, and also that it had been beneficial for function invocation performance in general to make this distinction explicit in Kernel code. Is this situation special because it is an invocation of an object of type
dynamic
?The text was updated successfully, but these errors were encountered: