Test262 is a JavaScript Sideshow

An illustration of a carnival barker holding back the JavaScript logoIllustration by Sue Lockwood

TC-39, the standards body that defines JavaScript, maintains a gigantic suite of tests for the language. The name of that test suite is Test262. When we started extending Test262 to cover brand new language features, we knew we were in for some surprises.

Even so, we never could have anticipated the horrors we would uncover.

JavaScript is a powerful language. The abstractions it provides allows developers to express complex algorithms with a small amount of (usually readable) code. However, these language features can sometimes interact in strange ways. We saw this time and again in the 18 months since we started collaborating with the V8 team.

In this blog post, we have assembled some of the more bizarre tests we contributed–our own menagerie of delinquent code. All of it is valid JavaScript, being fully defined by the language specification, ECMA262. None of it would be accepted in a code review.

So today, I will be your tour guide through the grotesque corners of the language. Leave your scruples at the door, and if you have a heart condition, please consider sitting this one out.

The Sideshow

Exhibit #1: Destructuring & Generators

To begin, consider destructuring assignment (spec,MDN). Code like [x, y] = [1, 2] assigns the value 1 to x and 2 to y. Behind the scenes, the runtime retrieves an iterator for the [1, 2] array, and advances it for each “binding” (x and y), assigning the iterated value as it goes along.

It can get more complicated than that. Just like in non-destructuring assignment, the targets could themselves be complex expressions. Think[foo.bar] = [3] or even [baz["q" + "ux"]] = [4]. Depending on the result of those expressions, the iteration mentioned above could be interrupted. Our first exhibit demonstrates how messy this can be:

var returnCount = 0;
var unreachable = 0;
var iterable = {};
var iterator = {
   return: function() {
     returnCount += 1;
     return {};
   }
};
var iter, result;
iterable[Symbol.iterator] = function() {
  return iterator;
};

function* g() {
  [ {}[yield] ] = iterable;
  unreachable += 1;
}
iter = g();
iter.next();
result = iter.return(777);

assert.sameValue(returnCount, 1);
assert.sameValue(unreachable, 0, 'Unreachable statement was not executed');
assert.sameValue(result.value, 777);
assert(result.done, 'Iterator correctly closed');

(source:language/expressions/assignment/dstr-array-elem-iter-rtrn-close.js)

Here, we have placed the destructuring assignment within a generator function (spec,MDN) and included a yield expression in a position that is evaluated during iteration. This allows us to interrupt the iteration right in the middle of things—we call the little-knowniter.return()(spec,MDN) instead of iter.next().

All of this allows us to assert the intended behavior when iteration is interrupted at this step. The call to return triggers the creation of a “return completion,” which is similar to a “throw completion” created by athrow statement (we have a test for that, too). The custom iterable allows us to observe the expected behavior.

They say, “beauty is in the eye of the beholder,” but you’d be hard-pressed to find anyone willing to stare at this thing for very long.

Exhibit #2: Tail Call Optimization & Tagged Templates

Sometimes called “TCO” for short, tail call optimization (spec,2ality) allows for recursive function calls to clean up after themselves before they are technically done executing. This occurs automatically for functions authored in the correct way, and it can be useful for computationally-expensive algorithms that use a “divide and conquer” strategy.

Here, we’re making sure that the optimization occurs for functions invoked using ES2015’s new “tagged template” feature (spec,MDN):

(function() {
  var finished = false;
  function getF() {
    return f;
  }
  function f(_, n) {
    if (n === 0) {
      finished = true;
      return;
    }
    return getF()`${n-1}`;
  }
  f(null, 100000);
  return finished;
}());

(source:language/expressions/tagged-template/tco-call.js)

Just as there is no special syntax to enable TCO, there is no cut-and-dry way to ensure it is taking place. So in this test, we are ensuring that TCO is occurring by recursing a huge number of times. If the optimization is notenabled, this test will “fail” because the program will exhaust available memory resources and crash. This makes the test a little rude, but you can’t expect good manners from a creature that has been forgotten by nature.

(By the way, the same implicitness that makes this test hard to read and write has prompted a larger discussionabout re-working this feature to require explicit syntax.)

Exhibit #3: Typed Arrays & Reflect

When you create a typed array (spec,MDN) from another typed array, generally you would expect the new one to be derived from the same class. For instance, new Int8Array(anotherInt8Array) produces a second Int8Array. The subtle truth is that the constructor to be used isactually defined by the NewTarget value (spec,MDN).

This distinction normally doesn’t matter because most applications invoke constructors with the new keyword. Doing so sets the NewTarget value to the constructor, resulting in the behavior described by the Int8Array example above.

However! ES2015 introduces the Reflect API (spec,MDN), and Reflect.construct allows you to invoke a constructor with any arbitrary value for NewTarget. We’re doing just that in the following test:

function newTarget() {}
newTarget.prototype = null;

var sample = new Int8Array(8);

var ta = Reflect.construct(Int8Array, [sample], newTarget);

assert.sameValue(ta.constructor, Int8Array);
assert.sameValue(Object.getPrototypeOf(ta), Int8Array.prototype);

(source:built-ins/TypedArrays/typedarray-arg-use-default-proto-if-custom-proto-is-not-object.js)

“But wait!” you’re saying, “The NewTarget value isn’t being honored at all!ta‘s prototype is still the Int8Array prototype.” Well, that’s exactly the detail that landed this test in the sideshow. If, for whatever reason, the NewTarget value has a non-object prototype (as this one does), then the specification dictates the “active function” (Int8Array here) should be used instead. Chilling.

We’re about to enter the show’s “Promise” tent. I hope you’re ready, because this one has two separate exhibits.

Exhibit #4: Promises & Arrays

Promises (spec,MDN) are a powerful way to orchestrate asynchronous operations. One important method for managing these operations is Promise.all: it allows for defining what should happen once many operations complete successfully.

This is essentially a convenience method; it’s something that JavaScript developers can write (and have written) on their own. The specification for its behavior reflects this because it uses a similar approach as a so-called “user land” version would.

Specifically: the method is expected to create an array of “resolution values,” and pass that array on to the internal Promise mechanism. Later, this array is itself interpreted as a resolution value. Because such values may represent further asynchronous operations (they’re called “thenables” in that case), bizarre extensions to Array can trigger surprising results…

var value = {};
var nonThenable = [];
var promise;
nonThenable.then = null;

promise = Promise.all(nonThenable);

Object.defineProperty(Array.prototype, 'then', {
  get: function() {
    throw value;
  }
});

promise.then(function() {
    $DONE('The promise should not be fulfilled.');
  }, function(val) {
    if (val !== value) {
      $DONE('The promise should be rejected with the expected value.');
      return;
    }

    $DONE();
  });

(source:built-ins/Promise/all/resolve-poisoned-then.js)

We’re passing an empty array to Promise.resolve because we aren’t actually interested in control flow for this test. This is valid because the method follows the same basic steps regardless of the number of elements provided. It is still expected to create a completely new array and store that as a resolution value. (We’ve “nulled out” the then property of the nonThenablearray to drive home the fact that the trouble starts with yet another unseenarray.)

When the time comes to resolve the promise, the runtime interprets this hidden array like any other resolution value. The runtime makes no special consideration for the fact that it created this array itself, so it begins by checking to see if the value is “thenable.” In this test, we’ve damaged theArray prototype with a “poisoned” then property—one that throws an error when accessed. That’s why we expect the Promise to be rejected.

While it might not be easy to see why the runtime rejects this Promise, it’s obvious why society has rejected this test.

Exhibit #5: Promises & …well, more Promises

One of the most powerful aspects of Promises is their ability to be “chained.” By resolving one Promise with another, we’re able to schedule events to happen in series in a very natural way. (We saw some of the mechanics for this laid bare in the previous exhibit.)

Naively designed, this feature could enable disastrous bugs: if a Promise is resolved with a reference to itself, then the runtime could enter into an infinite loop. This sounds kind of abstract, but it’s not really difficult to demonstrate:

// The arrow function is invoked asynchronously, *after*
// the promise has been created and assigned to `p`.
var p = Promise.resolve().then(() => p);

Luckily, ES2015 was not naively designed. There are specific guards in place for exactly this condition, so in the example above, the runtime calls the arrow function, recognizes that the return value is the same as the promise itself, and proceeds to reject p with a TypeError.

Promise.prototype.then has to be protected from this case because the provided callback is invoked asynchronously, after the promise has been created.

Compare this with Promise.resolve. This method synchronously creates a Promise that has been resolved with some value. At first glance, the “self-resolution” problem doesn’t seem relevant here:

// The promise is created *before* the value is assigned to
// `p1`, so this promise is resolved with the value `undefined`.
var p1 = Promise.resolve(p1);

// ...and unlike `Promise.prototype.then`, `Promise.resolve`
// doesn't invoke function arguments, so this promise is
// resolved with the arrow function (not its return value).
var p2 = Promise.resolve(() => p2);

Believe it or not, Promise.resolve can return a Promise that has been resolved with itself. It just takes a little more effort:

var resolve, reject;
var only = new Promise(function(_resolve, _reject) {
  resolve = _resolve;
  reject = _reject;
});
var P = function(executor) {
  executor(resolve, reject);
  return only;
};

Promise.resolve.call(P, only)
  .then(function() {
    $DONE('The promise should not be fulfilled.');
  }, function(value) {
    if (!value) {
      $DONE('The promise should be rejected with a value.');
      return;
    }
    if (value.constructor !== TypeError) {
      $DONE('The promise should be rejected with a TypeError instance.');
      return;
    }

    $DONE();
  });

(source:built-ins/Promise/resolve/resolve-self.js)

In order to demonstrate this behavior, we have to create a custom Promise constructor. This “constructor” returns a valid Promise instance, but it always returns the same Promise instance named only. So when we use that constructor as the “this” value for Promise.resolve (viaFunction.prototype.callspec,MDN),Promise.resolve attempts to create a new Promise but winds up using theonly Promise instance. Since we’re also specifying the only promise as the resolution value, this triggers that same guard we saw above, resulting in the expected TypeError.

Okay, the Promises are getting anxious. We should move along.

Exhibit 6: Default Parameters & eval

One of the more radical additions in ES2015 is the default parameter (spec,MDN. I’m sure many would disagree, but I consider it so severe because it allows for arbitrary expressions in a completely novel position. When it comes to defining this feature, it’s not enough to simply say, “You can write expressions here now, have fun.” The specification authors had to carefully consider all the implications, and they identified some surprising edge cases. Which, of course, we need to test.

Among the many things you might write in the position of a default parameter value is a direct eval (spec,MDN). This much-maligned feature has a unique property: it is an expression that can create a variable binding. This means that we need to define (and test) what happens when a default parameter itself creates a variable.

var x = 'outside';
var probe1, probe2, probeBody;

(function(
    _ = (eval('var x = "inside";'), probe1 = function() { return x; }),
    __ = probe2 = function() { return x; }
  ) {
  probeBody = function() { return x; };
}());

assert.sameValue(probe1(), 'inside');
assert.sameValue(probe2(), 'outside');
assert.sameValue(probeBody(), 'outside');

(source:language/expressions/function/scope-param-elem-var-close.js)

The specification dictates that there should be a dedicated variable scope for the default parameter. It goes further to say that each parameter should haveits own variable scope. That’s why in this test, we have defined bothprobe1 and probe2; only probe1 should “see” the x binding created byeval. Implementors who simply create a single scope shared by all parameters will not pass this test.

…and that concludes today’s tour through the sideshow. If you’re in the mood for more thrills, feel free to wander around at your heart’s content.

The Value of Things in Cages

It can be fun to gawk at the peculiarities of a programming language (“Wat” by Gary Bernhardt being a favorite). But while a healthy sense of irony is a requirement for all programmers, this exhibition doesn’t exist just to poke fun at JavaScript.

Many years ago, Bocoup’s own Ben Alman wrote about the various idiosyncracies in trystatements across browsers. Back then, it seemed all too easy for a bug to find its way into a web browser. Shortly after that, some major web site/library would come to depend on that behavior, and thenceforth the web platform would be stuck with the bug.

In Test262, we have a tool to automatically verify specification conformance with great precision. Not only that; the practice of writing tests for early-stage features often exposes errors and ambiguities in their design.

If this strange pageant is any indication, the test suite doesn’t prevent monsters from finding their way into the language… but that isn’t the goal. In aspiring to be a repository of all novel interactions, Test262 brings us closer to a world where every edge case is thoroughly understood and acceptedbefore it makes its way into our application code. Consider this post a celebration of the ever-improving web platform.

Comments

Contact Us

We'd love to hear from you. Get in touch!

Phone

+1 617-283-2807

Mail

P.O. Box 961436
Boston, MA 02196