![Florian Loitsch](/assets/img/avatar_default.png)
Address comments from https://dart-review.googlesource.com/c/sdk/+/12161 Change-Id: I05294112753891732784a38fc5ef14fb7fed291b Reviewed-on: https://dart-review.googlesource.com/12298 Reviewed-by: Florian Loitsch <floitsch@google.com>
12 KiB
Dart Language and Library Newsletter
2017-10-06 @floitschG
Welcome to the Dart Language and Library Newsletter.
Did You Know?
Static Initializers
This section discusses initializers of static variables. For example, the following program has 3 static variables (seenIds
, someDouble
and A._hashIdCounter
) that are all initialized with a value:
import 'dart:math';
final seenIds = new Set<int>();
var someDouble = sin(0.5);
class A {
static int _hashIdCounter = 0;
A();
final hashCode = _hashIdCounter++;
}
main() {
print(new A().hashCode); // => 0.
print(new A().hashCode); // => 1.
print(someDouble); // => 0.479425538604203.
print(seenIds); // => {}.
}
This program is pretty straightforward and its output should not surprise anyone.
Things get more interesting when the initializers have side effects:
int _counter = 0;
int someInt() => _counter++;
var foo = someInt();
final bar = someInt();
class A {
static int gee = someInt();
}
main() {
print("A.gee: ${A.gee}");
print("foo: $foo");
print("bar: $bar");
}
The initializers of foo
, bar
and A.gee
all call someInt
which has a side-effect of updating the _counter
variable (which, itself, is initialized with a side-effect-free value: 0
).
The output of this program is:
A.gee: 0
foo: 1
bar: 2
Dart simply evaluates the initial value at first access. This is a consequence of one of Dart's fundamental properties: no code is executed before entering main
. All code is run as the consequence of the program's actions.
This choice has many nice properties. For example, loading additional classes doesn't slow down a program. Instantiating an instance of a class will not initialize the static members of that class, and the instantiation is thus very fast.
Conceptually, lazy initialization is done with a getter that checks if the field has already been initialized. If not, it evaluates the initializer expression and sets the value of the field first. That's, at least, the simple version. There is much more that the getter needs to handle.
Some Interesting Questions
What happens, if the field is assigned to before the first reading access to it?
int foo() { throw "bad?"; }
var x = foo();
main() {
x = 0;
print(x);
}
This is allowed and prints 0
. The initializer expression is simply ignored.
What happens when the initializer tries to read the field that is currently being initialized?
var x = foo();
int foo() => x + 1;
main() { print(x); }
Dart requires implementations to throw an exception in this case:
Unhandled exception:
Reading static variable 'x' during its initialization
However, initializers are allowed to read the field if it has been assigned to first:
int foo() {
x = 0;
return x + 1;
}
var x = foo();
main() { print(x); } // => 1.
What happens when the initializer throws and the variable is accessed again?
int foo() { throw "bad"; }
var x = foo();
main() {
try {
print(x);
} catch (e) {
print("caught");
}
print(x);
}
The result of this program is:
caught
null
When an initializer throws, the field is simply initialized with null
and there is no further attempt to run the initializer again.
As a language team we would need to have another look at this behavior, when non-nullable types enter the equation...
What happens when the initializer throws, but the field is already initialized?
int foo() {
x = 0;
throw "bad";
}
var x = foo();
main() {
try {
print(x);
} catch (e) {
print("caught");
}
print(x);
}
This program outputs:
caught
null
An exception during initialization resets the value to null
.
Implementation
As shown, there are many edge cases that need to be covered when a variable is lazily initialized. Fortunately, the cost for these cases are only paid at first access. Here is, for example, dart2js' lazy initializer routine (with some comments):
prototype[getterName] = function() {
var result = this[fieldName];
// If we are already initializing the variable, throw.
if (result == sentinelInProgress)
H.throwCyclicInit(staticName || fieldName);
try {
// If the field hasn't been set yet, it needs to be initialized.
if (result === sentinelUndefined) {
// Make sure that we can detect cycles.
this[fieldName] = sentinelInProgress;
try {
// Run the expression that gives the initial value.
result = this[fieldName] = lazyValue();
} finally {
// If we didn't succeed, set the value to `null`.
if (result === sentinelUndefined) {
this[fieldName] = null;
}
}
}
return result;
} finally {
// Replace this getter with a much more efficient version that
// just looks at the field instead.
this[getterName] = function() {
return this[fieldName];
};
}
};
There are two important things to notice:
- This routine is shared among all lazily initialized variables. There is thus little cost in code size for lazily initialized variables.
- The
finally
block replaces the lazy getter with a function that just returns the contents of the field. After the first access, JavaScript engines thus see a simple small function that can be inlined. This means that lazily-initialized fields behave very efficiently after the first access.
The finally
optimization is not the only thing that our compilers do to make static variables more efficient: before even generating the lazy getters, an analysis inspects the initialization-value and determines whether it's just cheaper to initialize the field with that value instead. For example, var x = 0
, does not need a lazy initialization. It's much cheaper to just initialize the field with that value directly. Obviously, this replacement can only be made if the evaluation of the expression is cheap, and is guaranteed not to have any side-effect.
Evaluation Order
The language team is planning to change the evaluation order of method calls. This section covers the change and explains why it is not as breaking as it might sound.
In general, Dart evaluates all its expressions from left to right. However, there is an interesting exception: the arguments to a method invocation are evaluated before the receiver function is evaluated. Concretely, given o.foo(e1, ..., eN)
, Dart requires o
, e1
, ..., eN
to be evaluated before evaluating o.foo
. Most of the time o.foo
is a method and the evaluation doesn't matter (since evaluating to a method doesn't have any visible side-effect). However, it makes a difference when o.foo
is a getter.
class A {
get getter {
print("evaluating getter");
return (x) {};
}
}
int bar() {
print("in bar");
return 499;
}
main() {
var a = new A();
a.getter(bar());
}
According to the specification, this program should print:
in bar
evaluating getter
Even though the a.getter
is syntactically before the call to bar()
Dart requires the argument to the call to be evaluated first.
Reasoning
This exception was put into the specification on purpose and was added for performance reasons. Previous experience with other virtual machines (like V8) showed that this approach yielded a simpler calling convention which leads to a slightly faster method call.
It's instrumental to compare the two conventions:
With the current Dart convention the VM can start by evaluating the arguments first, and then, when all the arguments are nicely pushed on the stack, it can look up the target function and do the call.
If the target function needs to be evaluated first, then there is an additional value that the VM needs to keep alive while it evaluates the arguments. That is, while it evaluates the arguments, it might run out registers (and thus spill to the stack), because there is now one more value (the target function address) that needs to be kept alive.
With few exceptions, evaluating the receiver last, doesn't really affect any user and the Dart team thus opted for the unexpected evaluation order in return for less register pressure. In fact, most of the time this inverted evaluation order was actually beneficial to our users: it provided more information to our users when they had bugs in their programs: noSuchMethod
errors (including on null
) had the arguments that were passed to the non-existing method.
noSuchMethod and null
Whenever a member doesn't exist (or the shape/signature doesn't match), the noSuchMethod
function is invoked. This method receives a filled Invocation
object which contains the arguments to the non-existing member. This means that for non-existing members, arguments also need to be executed first.
This has important properties for null
errors. Since null
errors are mapped to noSuchMethod
executions on null
, the arguments to null
calls are evaluated before the error is thrown:
int bar() {
print("in bar");
return 499;
}
main() {
null.foo(bar());
}
A valid output for this program is:
in bar
Unhandled exception:
NoSuchMethodError: The method 'foo' was called on null.
Receiver: null
Tried calling: foo(499)
#0 Object._noSuchMethod (dart:core-patch/object_patch.dart:44)
#1 Object.noSuchMethod (dart:core-patch/object_patch.dart:47)
#2 main (null.dart:6:8)
#3 _startIsolate.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:261)
#4 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:148)
Note how the error message contains the argument 499
that was passed to the function. This is only possible, because the argument was evaluated before the target function was evaluated. Note: this information is only available on the VM, since the null
-errors in the browser are triggered by the JavaScript engine which doesn't capture any argument values.
Changing the Evaluation Order
Despite the benefits of the current semantics, we decided to change the behavior so that the evaluation is strictly from left to right.
There are 4 main reasons for the change:
- The behavior is unexpected.
- Seemingly similar invocations don't behave the same.
- Because of static types, it's easier to know whether the evaluation of the target function can be evaluated last (as an optimization).
- Implementing the specification-behavior is hard when compiling to JavaScript and is actually detrimental to code size and performance. For this reason, and because of minor bugs, our implementations don't correctly implement the specification.
Expected Behavior
We found that our users don't expect that the target of member invocation is evaluated last. In practice, this was not a problem, but we would prefer to match our user's expectations.
Related Equivalences
Because of the evaluation order and noSuchMethod
we currently end up with different behavior for seemingly similar constructs:
// The following invocations are *usually* the same, but aren't when `o` is null, or when
// `foo` is not a member of `o`.
o.foo(e1);
o.foo.call(e1);
(o.foo)(e1);
Implementations
Our implementations (VM, dart2js and DDC) all behave differently, and dart2js and DDC aren't even internally consistent. Dart2js' behavior depends on optimizations, and DDC treats dynamic and non-dynamic calls (on the same member) differently.
Static Typing
Since Dart is getting more static, in most cases a method call is known not to go through getters or through noSuchMethod
. For the majority of calls that are known to hit a "normal" method, the compilers can keep evaluating the function target last (since it doesn't have any side-effects). In those simple cases the only change consists of making sure that the receive isn't null
before evaluating any arguments.
Conclusion
The original reason for the inverted evaluation order doesn't apply anymore, and it's time to bring Dart's evaluation order in line with what our users expect. A simple o.foo(bar())
should not be a "Dart Puzzler".