-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Left-to-right evaluation order is unlikely to be enforced for instance method invocations #36744
Comments
Are our tools currently consistent with one another? If so, it might be desirable to use a feature flag to implement this change or to time the work so that all tools make the change on the same release. FWIW - We have concerns on the dart2js side that this will be a non-trivial effort to prevent a performance regression. In particular, the current implementation matches the calling convention in JS and allows us to generate compact code. We can add the new evaluation order, but we'll need to implement an analysis to determine when the evaluation order doesn't matter* and in those cases continue to use the old code-pattern so we can prevent a big code-size and performance regression on the generated code. *e.g. some scenarios we can optimize away: statically known invocation sites, dynamic invocations on selectors that are never declared as getters, dynamic invocations on selectors that may be getters but that are always pure. |
@eernstg Your test should also test dynamic invocations, since at least one implementation (DDC) I believe uses a difference evaluation order for the two. |
@eernstg This is a breaking change, we will need to file a breaking change request. |
If all of the tools are already consistent on this (or mostly consistent), and if we have performance concerns about making the change, I'm not sure it's clear to me that we should change the tools and not the spec. cc @lrhn @munificent |
The current behavior is:
I believe the reason for the change was that DDC uses JavaScript's evaluation order, which is The (abbreviated) test program used to test this was: List<String> trace = [];
T log<T>(String entry, [T value]) {
trace.add(entry);
return value;
}
class C {
void Function(Object) get g => log("get g", (arg) { log("call g"); });
}
C get r => log("get r", C());
dynamic get d => log("get d", C());
main() {
log("3");
r.g(log("arg"));
log("4");
log("r.g", r.g)(log("arg"));
log("7");
d.g(log("arg"));
log("8");
log("d.g", d.g)(log("arg"));
print(trace.join("\n"));
} The result is:
for VM and dart2js, and the only difference for DDC is swapping If we keep the current majority behavior, and change the spec back, then it means that DDC can no longer compile If we change to the DDC/JS behavior, then even DDC needs to change because its behavior for a |
@sigmundch wrote:
@lrhn already mentioned a test. Here's another one that includes more forms: // FILE 'scratch_test.dart'.
import "package:expect/expect.dart";
import "scratch_lib.dart" as lib;
int i;
void f() => i *= 2;
class A {
void Function(void) get m {
++i;
return (_) {};
}
void absentInclass(int j) {
i = j;
m(f()); // No syntactic receiver, calling instance getter.
Expect.equals((j + 1) * 2, i); //# absent_inclass: ok
}
static void Function(void) get n {
++i;
return (_) {};
}
}
void Function(void) get m {
++i;
return (_) {};
}
main() {
i = 0;
A().m(f()); // Type of receiver is not `dynamic`.
Expect.equals(2, i); //# checked: ok
dynamic d = A();
i = 2;
d.m(f()); // Receiver has type `dynamic`.
Expect.equals(6, i); //# dynamic: ok
i = 6;
A.n(f()); // Syntactic receiver is type literal.
Expect.equals(14, i); //# static: ok
i = 14;
m(f()); // No syntactic receiver.
Expect.equals(30, i); //# absent: ok
A().absentInclass(30);
lib.j = 0;
lib.m(f()); // Syntactic receiver is a prefix.
Expect.equals(2, lib.j); //# prefix: ok
}
// FILE 'scratch_lib.dart'.
int j;
void f() => j *= 2;
void Function(void) get m {
++j;
return (_) {};
} In the current SDK this produces the following failures:
This means that DDC and DDK evaluate left-to-right except for the invocation on a receiver of type Presumably, this means that at least many developers have been debugging their programs under the specified left-to-right semantics in the typical case of statically checked instance method invocations, and then they've deployed programs where the old args-first evaluation order was used for those same call sites. So the situation is still not crystal clear, but the difference does not seem to break many programs. One inconsistency that stands out is that the invocation of a static getter ('static') and the plain top-level getter invocation ('absent') are treated differently from the top-level getter invocation that involves a library prefix ('prefix'), in all tools. |
This topic was discussed recently in the language team. It is a gnarly corner of the language semantics, but we still want to reconcile the language specification and the actual implementations (of course — no language debt should be left behind ;-). So here's the current status. The first column shows a specific kind of invocation, and the second column characterizes the evaluation order ("GA" means that the getter is evaluated before the actual arguments, and "AG" means that the arguments are evaluated before the getter):
The CL https://dart-review.googlesource.com/238903 confirms that the behavior of all the standard try-bots is consistent with the above table (which wasn't true the previous time we discussed this). So we could make the decision that the current behavior is what we want, and then we would adjust the language specification accordingly; or we could decide that we really want consistent left-to-right evaluation as specified, and then we'd need to assess and handle the breakage. There is also the question about the implementation efforts, if we decide that we want consistent left-to-right evaluation. @sigmundch, @srujzs, I get the impression that JavaScript interop is a source of particular difficulties in this context. Another topic of interest is dynamic invocations. WDYT? @mkustermann, @a-siva, I believe that the evaluation order for statically checked invocations could be changed in the common front end, but it might require a substantial amount of backend work to change the evaluation order for dynamic invocations. WDYT? |
Thanks for following up @eernstg - you are correct, there are many difficulties on the web side for both jsinterop and dynamic invocations (the concerns mentioned in the earlier comment #36744 (comment) still apply today). Those make me hesitate here. I think we should however reevaluate this in the context of the new static interop. It may be possible for us going forward to distinguish statically all jsinterop calls, to the point that instance getters would all be non-jsinterop. That would greatly simplify things on our end implementation wise. |
@eernstg and I discussed this earlier today. Given that the implementations are consistent, and that the current behavior has a certain kind of consistency (instance is R->L, others are L->R) - and given that there is probably minimal user value to be gained by spending time changing this (and dealing with breakage) - I'm fairly inclined to just make the current behavior canonical. |
Sounds great - I'm in favor of not rocking the boat too 😄 -- Even if we ignore JSInterop, the work to get the performance at parity is non trivial too. |
Here's a proposed specification change: dart-lang/language#2182. A small set of language tests are here: https://dart-review.googlesource.com/c/sdk/+/238903. Adjusted the title of this issue to reflect the current situation: We are more likely than not to drop the change, and turn the current semantics into the specified one. |
[New status Apr 2022, eernstg: This issue has been lingering for years; it is now more likely than not that we will change the specification to make the existing behavior the specified behavior. I closed the subissues; we can easily create new ones if we change our minds.]
Cf. the discussion in #34497, the October 2017 language team decision described here which was integrated into the language specification as part of CL 51323 has not been implemented everywhere.
Said decision has the following contents: When an ordinary method invocation i of the form
e.m(arguments)
is such thatm
is a getter, the evaluation of i proceeds by evaluatinge
to an objecto
, then invoking the gettero.m
to an objectf
, and then evaluatingf(arguments)
.In other words, the evaluation order is strictly left-to-right, including the getter invocation. The old rules specified that the argument list should be evaluated first, and then the getter should be invoked.
Surely, this change request could have been communicated better. However, at this point I believe the way ahead is to adjust the implementations such that they support the left-to-right evaluation order which was specified and intended.
Here is an example:
This currently fails on the following trybots (I just used the standard selection of trybots), illustrating that dart2js as well as the vm needs adjustments: dart2js-minified-strong-linux-x64-d8-try, dart2js-strong-hostasserts-linux-ia32-d8-try, dart2js-strong-linux-x64-chrome-try, vm-kernel-linux-product-x64-try, vm-kernel-linux-release-simdbc64-try, vm-kernel-linux-release-x64-try.
I've chosen 'type-enhancement' for this issue, because it is concerned with the implementation of a specification change.
Subtasks
The text was updated successfully, but these errors were encountered: