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:

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):

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:

“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…

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.resolvecan return a Promise that has been
resolved with itself. It just takes a little more effort:

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.call—spec,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.

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.

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.

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.