The resulting JavaScript produced with Relooper is:

var label=0;

switch(A()){

case 0:

{

B();

label=3;

break;

}

case 1:

{

label=3;

break;

}

case 2:

{

label=4;

break;

}

}

if(label===3){

C();

label=4;

}

if(label===4){

D();

}

E();

As you can see, Relooper is using the label variable here, but it is quite obviously not needed, as you can see in this alternative manual solution:

d:do{

e:do{

switch(A()){

case 0:

B();

break;

case 1:

break;

case 2:

break d;

default:

break e;

}

C();

}while(0);

D();

}while(0);

E();

If we keep adding cases to the switch, Relooper will add more and more if statements checking on the label variable. In real code bases we found some generated code that uselessly sets, check and immediately resets the label variable dozens of times. And to make this worse, these aberrations usually happen in places where the control flow is complex for performance reason, like the main loop of a zip compressor, a python interpreter, or a ray tracing engine.

Due to this and similar issues, we decided to get rid of Relooper and find a better algorithm.

Looking for prior art

As it often turns out in computer science, something very similar to our problem was thoroughly investigated and solved back in the ’70s: at the time people were arguing about the use and misuse of goto statements in programs, and proposing alternative constructs less powerful but sufficient to convert programs using gotos into a structured control flow form.

A particularly interesting result for our problem is that as long as the CFG is reducible, we can convert it to a structured form consisting of loops, conditional branches , and multilevel continue and break statements (see this paper for an overview of this and similar results).

These are exactly the constructs that we have! And it is not a coincidence, since programming languages added constructs like multilevel break and continue statements specifically to get rid of arbitrary goto statements.

But what is a reducible CFG?

According to Wikipedia, a reducible CFG is one with edges that can be partitioned into two disjoint sets: forward edges, and back edges, such that:

Forward edges form a directed acyclic graph (DAG) with all nodes reachable from the entry node.

For all back edges (A, B), node B dominates node A.

In simpler words, all loops need to be single entry. Most C/C++ code produces a reducible CFG, so this result is very encouraging!

The Stackifier algorithm

Putting aside irreducible CFG for a moment, how can we compile any reducible CFG into JavaScript without using the label variable?

We can leverage the definition of reducible CFG to build our algorithm:

Since the forward edges form a DAG, we can sort the blocks in topological ordering (For every forward edge A-B, A comes before B in the ordering).

(For every forward edge A-B, A comes before B in the ordering). We choose any topological ordering (in general, there are multiple valid ones), with the constraint that once a loop starts all the subsequent blocks must be dominated by the loop header, until all the loop blocks have appeared.

We enclose each loop in a loop scope (a while(1){/*...*/} in JavaScript and a loop...end in WebAssembly). All the back edges become continue statements (possibly labeled in case of nested scopes).

(a in JavaScript and a in WebAssembly). All the back edges become statements (possibly labeled in case of nested scopes). Now we are left with the forward edges. If the source and destination blocks are consecutive in the topological order, we don’t need to do anything.

Otherwise, we need to place a block scope (a do{/*...*/}while(0) in JavaScript, and a block...end in WebAssembly), such that the destination block is just after the end of the scope. In principle we can put all the scope openings at the beginning of the function, but in practice JavaScript engines don’t like deeply nested scopes and throw an error beyond a certain limit (around 1000 for a script with just empty nested scopes, less in real code). We place the opening just outside of the outermost scope that closes between the break and the end of the scope.

Not all arbitrary placements of scopes are valid. We cannot intertwine two of them, only nest them:

// NOT ALLOWED

while(1){

do{

}

while(0); // OK

while(1){

do{

}while(0);

}

How do we know that our algorithm never produces intertwined scopes? Since we chose an ordering such that all the loops contain only blocks dominated by the loop header, by definition there can be no edge with the source outside of the loop and the destination inside (except when the destination is the loop header, which is fine), so we never need to put a block scope that ends in the middle of a loop but starts outside.

As an example, let’s take the Example CFG from the beginning, and process it with Stackifier: