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:
// 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?
foo: while (true) {
while (true) {
if ($) continue foo;
if ($$) break foo;
break;
}
break;
}
It would transform into something like this;
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.