Dart Language and Library Newsletter (2017-10-06)

110 views
Skip to first unread message

Florian Loitsch

unread,
Oct 6, 2017, 3:47:36 PM10/6/17
to General Dart Discussion

Dart Language and Library Newsletter

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 (seenIdssomeDouble 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 foobar 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.

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.

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.

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 oe1, ..., 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:

  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.

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".

Danny Tuppeny

unread,
Oct 8, 2017, 5:28:27 AM10/8/17
to mi...@dartlang.org
On Fri, 6 Oct 2017 at 20:47 'Florian Loitsch' via Dart Misc <mi...@dartlang.org> wrote:

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.

I'm slightly confused by this - the example given behaves how I would expect, and (if I'm understanding correctly) if I do the same thing in C# it seems to behave the same.. given:

Object a = null;
a.Equals(bar());

bar() is invoked before this throws a NullReferenceException. To me, the change to make this throw before evaluating bar() seems unusual (though admittedly, I don't know how other languages behave, C# might be the odd one!).

I don't object to this change (it actually seems quite logical and I don't think it's likely to affect anything I do), it just surprised me that this wasn't deemed to be expected behaviour. I tried, but failed, at Googling to see what other languages do!

Mark Nordine

unread,
Oct 8, 2017, 10:22:52 AM10/8/17
to mi...@dartlang.org
Agreed, I'm not against it either, but saying the behavior is unexpected is a pretty contentious claim. I expected bar() to evaluate first.

We found that our users don't expect that the target of member invocation is evaluated last

What users? How was this survey done?

Florian Loitsch

unread,
Oct 9, 2017, 8:34:37 AM10/9/17
to mi...@dartlang.org
Thanks for the feedback.
I have to admit, that I missed C# and Java evaluate their arguments first. From what I can see, C++ and JavaScript evaluate left-to-right, though.

I will write a follow-up in the next newsletter.

On Sun, Oct 8, 2017 at 4:22 PM Mark Nordine <mark.n...@gmail.com> wrote:
Agreed, I'm not against it either, but saying the behavior is unexpected is a pretty contentious claim. I expected bar() to evaluate first.

On Sunday, October 8, 2017 at 6:28:27 AM UTC-3, Danny Tuppeny wrote:
On Fri, 6 Oct 2017 at 20:47 'Florian Loitsch' via Dart Misc <mi...@dartlang.org> wrote:

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.

I'm slightly confused by this - the example given behaves how I would expect, and (if I'm understanding correctly) if I do the same thing in C# it seems to behave the same.. given:

Object a = null;
a.Equals(bar());

bar() is invoked before this throws a NullReferenceException. To me, the change to make this throw before evaluating bar() seems unusual (though admittedly, I don't know how other languages behave, C# might be the odd one!).

I don't object to this change (it actually seems quite logical and I don't think it's likely to affect anything I do), it just surprised me that this wasn't deemed to be expected behaviour. I tried, but failed, at Googling to see what other languages do!

--
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+uns...@dartlang.org.

Benjamin Strauß

unread,
Oct 9, 2017, 10:21:54 AM10/9/17
to Dart Misc
Having left to right would be completely counterintuitive. I also would like to know where this data comes from.

I expect (in dynamic lanugages) something that is inside braces to be evaluated first, this is also how it works in Javascript.

Sean Eagan

unread,
Oct 9, 2017, 1:34:17 PM10/9/17
to mi...@dartlang.org
--
Thanks,
Sean Eagan

Lasse R.H. Nielsen

unread,
Oct 9, 2017, 3:57:27 PM10/9/17
to mi...@dartlang.org
On Mon, Oct 9, 2017 at 4:21 PM, Benjamin Strauß <benm...@gmail.com> wrote:
Having left to right would be completely counterintuitive. I also would like to know where this data comes from.

I expect (in dynamic lanugages) something that is inside braces to be evaluated first, this is also how it works in Javascript.



Just for the record, JavaScript does evaluate the function expression before the arguments. This was changed in ECMAScript 5.0. See http://es5.github.io/#x11.2.3
The change was that GetValue(ref) in step 2 is now evaluated before evaluating the arguments, not after like in ECMAScript 3 (section "11.2.3 Function Calls" of https://www-archive.mozilla.org/js/language/E262-3.pdf).

In your example, the function expression (x.foo) evaluates to undefined, then the arguments are evaluated, and then it tries to call "undefined", which fails.
If your example started with:
  var x = null;
then you would see an error without a console.log output, because evaluating `x.foo` fails to evaluate entirely (and doesn't just fail to evaluate to a callable value);

/L
-- 
Lasse R.H. Nielsen - l...@google.com  
'Faith without judgement merely degrades the spirit divine'
Google Denmark ApS - Frederiksborggade 20B, 1 sal - 1360 København K - Denmark - CVR nr. 28 86 69 84

Benjamin Strauß

unread,
Oct 9, 2017, 4:14:12 PM10/9/17
to Dart Misc
Thanks for the detailed spec info! :)

tatumizer-v0.2

unread,
Oct 10, 2017, 12:33:13 AM10/10/17
to Dart Misc
Getter is just a syntactic sugar for a regular method. Suppose I'm not aware about this sugar, and I implement 'getter' as a regular method.
The invocation looks like a.getter()(bar()) - so a.getter() is obviously invoked before the bar().
Then I find out about the existence of said sugar, and add 'get' keyword accordingly. I fix my program by just erasing parens: a.getter(bar())
The question is: in this scenario, what evaluation strategy would seem more intuitive? The one that preserves the order, or the one that changes it?


Lasse R.H. Nielsen

unread,
Oct 10, 2017, 3:51:38 AM10/10/17
to mi...@dartlang.org
Or another way to frame the same question:

Is o.foo(bar()) the same as (o.foo)(bar())?
- when o.foo is a getter
- when o.foo is a method.
- when o.foo doesn't exist (dynamic calls only in Dart 2).

We want it to be the case for getters, and for all successful method invocations (there are some issues around which noSuchMethod gets invoked when you dynamically call a method or method tear-off with the wrong number of arguments, but bad code is bad).

/L
Reply all
Reply to author
Forward
0 new messages