Elimination of continue

2023-12-29

I got back into working on my Preval project this year. The first step of Preval is to break down JS constructs into other constructs and eliminate variance in code. In a way it's an attempt to reduce JS to a MISC (a Minimal Instruction Set Computer) with as goal minimalizing the variations of code to have to worry about.

In this episode I look at the JS continue keyword.

I wrote this a few months ago but a broken blog and lack of wanting to dig into its ancient php prevented me from posting it so here you go

The keyword is used in two different ways:

- One: without a label. In this case code flow jumps back to the nearest loop boundary and continues with evaluating the loop header and then the body.
- Two: with a label. In this case the code flow jumps to that label and proceeds with the body of that label. The label must be the parent of a loop statement. This is actually a syntactical requirement.

I'm already running into the problem where the restriction of the label of the labeled continue variant is causing me head aches and so I was wondering I couldn't just eliminate it. And I guess we can. But there's a price to pay.

Short of function magic, which would add even more unwanted complexity into the mix, a continue can be emulated by a break and an outer while (true) loop. In that case, all continue keywords are changed with (unlabeled) break keywords and all unlabeled break keywords are changed into labeled break keywords. The label will target the outer loop.

Note: in my normalized state, all loops are regular while statements with true as their test. Rather than test the condition, the inside will break on the same condition.

In code, that looks roughly like this:

Code:
// from
while (true) {
if ($) continue;
else break;
}

// to ->

outer: {
while (true) {
while (true) {
if ($) break;
else break outer;
}
}
}

Note the block wrapper. That's an invariant in my normalized code. In fact, the labeled continue is the only construct that currently requires violating that invariant. So I'm happy to see it go.

That said, the price is an extra loop layer. This is essentially what the continue is, though. So I'm not sure just how high that price is down the line.

As an(other) aside, I may also normalize break to be always labeled. It's another variant to remove and since my normalized state has labels as globally unique names, it would actually make it easier to figure out jump targets for any break. I have to support labeled break anyhow (I lean on it to eliminate switch statements) so I may as well go all in on that.

Now comes the hard part: labeled continue.

When we can guarantee the loop target for a regular continue to be no further than any break then we can safely do the above transformation. But what if the continue jumps across a loop boundary?

Code:
foo: while (true) {
while (true) {
if ($) continue foo;
if ($$) break foo;
break;
}
break;
}

It would transform into something like this;

Code:
bar: {
while (true) {
foo: {
while (true) {
while (true) {
if ($) break foo;
if ($$) break bar;
else break;
}
}
}
break bar;
}
}

So the "foo" loop is wrapped in another layer and the continue changes to a break, with the same label. This will cause code flow to continue after the "foo" loop and then loop again to start with the "foo" loop again. The continue semantics is preserved.

I think this would execute the same logic as before. This does require us to transform all break statements that were targeting the "foo" loop (labeled or unlabeled) to a labeled statement targeting "bar" instead. That's not too hard.

Once labeled continue is eliminated I can force labels on break and make some nice assumptions about jump targets and invariants around loop code flow. Being able to reference label nodes is actually a welcome addition, although that's already solved fairly easily today.

Later on, if I ever get there, the nested loop can probably be reversed by detecting the construct and inverting the trick and/or collapsing the artificial duplication.