'Florian Loitsch' via Dart Misc
2017-10-06 19:47:19 UTC
Github link for this newsletter:
https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md
Earlier newsletters:
https://github.com/dart-lang/sdk/tree/master/docs/newsletter
Dart Language and Library Newsletter
Welcome to the Dart Language and Library Newsletter.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#did-you-know>Did
You Know?
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#static-initializers>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. Every 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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#some-interesting-questions>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 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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#implementation>
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:
1. This routine is shared among all lazily initialized variables. There
is thus little cost in code size for lazily initialized variables.
2. 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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#evaluation-order>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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#reasoning>
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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#nosuchmethod-and-null>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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#changing-the-evaluation-order>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:
1. The behavior is unexpected.
2. Seemingly similar invocations don't behave the same.
3. Because of static types, it's easier to know whether the evaluation
of the target function can be evaluated last (as an optimization).
4. 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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#expected-behavior>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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#related-equivalences>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);
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#implementations>
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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#static-typing>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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#conclusion>
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".
https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md
Earlier newsletters:
https://github.com/dart-lang/sdk/tree/master/docs/newsletter
Dart Language and Library Newsletter
Welcome to the Dart Language and Library Newsletter.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#did-you-know>Did
You Know?
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#static-initializers>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. Every 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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#some-interesting-questions>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 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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#implementation>
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:
1. This routine is shared among all lazily initialized variables. There
is thus little cost in code size for lazily initialized variables.
2. 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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#evaluation-order>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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#reasoning>
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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#nosuchmethod-and-null>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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#changing-the-evaluation-order>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:
1. The behavior is unexpected.
2. Seemingly similar invocations don't behave the same.
3. Because of static types, it's easier to know whether the evaluation
of the target function can be evaluated last (as an optimization).
4. 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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#expected-behavior>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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#related-equivalences>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);
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#implementations>
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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#static-typing>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.
<https://github.com/dart-lang/sdk/blob/master/docs/newsletter/20171006.md#conclusion>
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".
--
For other discussions, see https://groups.google.com/a/dartlang.org/
For HOWTO questions, visit http://stackoverflow.com/tags/dart
To file a bug report or feature request, go to http://www.dartbug.com/new
---
You received this message because you are subscribed to the Google Groups "Dart Misc" group.
To unsubscribe from this group and stop receiving emails from it, send an email to misc+***@dartlang.org.
For other discussions, see https://groups.google.com/a/dartlang.org/
For HOWTO questions, visit http://stackoverflow.com/tags/dart
To file a bug report or feature request, go to http://www.dartbug.com/new
---
You received this message because you are subscribed to the Google Groups "Dart Misc" group.
To unsubscribe from this group and stop receiving emails from it, send an email to misc+***@dartlang.org.