In Preval my goal is to reduce code overhead that is useless at runtime. In the previous post I talked about eliminating the
finally
keyword. In this post I'll discuss the elimination of all four cases of the
for
loop, in favor of a regular
while
loop.
You can find Preval on github:
github.com/pvdz/prevalThere is also a REPL (consider it wip though):
pvdz.github.io/preval/web/The four cases
Puns aside, the JS language defines four types of
for
loops:
- for each
- for in
- for of
- await for of
You may consider some equal and that is fine :) They all end up as regular
while
loops in Preval, although I'll concede immediately that I'm cheating a little bit.
The problem
In Preval I'm trying to reduce the input source code to a "normalized" subset of the JS language. The goal is to reduce complexity within Preval, having to worry about fewer edge cases in the language. At the cost, of course, of expressiveness.
My hope is to be able to uncover patterns by looking at lower/simpler bits and pieces of the code even after compiling them down from higher level constructs.
In this context, loops are currently all compiled to simple
while (true)
loops.
This is trivial for the
do {} while (x);
loop and the
for (a; b; c) {}
loop.
The
for-in
and
for-of
loops are different though, because they only conditionally execute their body and they are depending on a mechanism that is not otherwise exposed in the language. Or well ...
Of
Ok, the
for-of
loop is driven by iterators. So maybe we can mimick what the
for-of
loop does and just turn that into a
while
instead?
for (const x of y) {}
// ->
let iterator = done[Symbol.iterator]();
let next = iterator.next();
while (!next.done) {
const x = next.value;
next = iterator.next();
}
That was easy.
We can eliminate this case of
for
(and similarly for the
await for
loop) and are now down to one
for
: the
for-in
loop. If we can eliminate that then the only loop Preval has to worry about is the
while (true)
. Conceptually that would simplify a whole lot of transforms and semantically the only "loop" to worry about is the
while
(ok and infinite recursion). Let's do it!
In
The property iteration is harder to eliminate because it has a bit of history. The properties of an object are not supposed to be ordered and yet the
for-in
loop has generally been considered to loop in a stable consistent order. To the point where the spec even talks about it. Additionally, the loop walks through own properties but also inherited properties, except for properties marked as
not enumerable
. This makes it awkward because we can't just
Object.keys(x)
and loop over those properties as that would not walk the prototype. In practice that would mostly be fine though.
The hack
To work around this I decided to cheat a little bit. The objective is to be able to reason about JS code. In that light, I've added
$forIn
and
$forOf
functions to the code. At runtime they are generators that yield all the values that the loop would yield.
In fact, this is what their value looks like in tests:
function * $forIn(obj) {
for (const key in obj) {
yield key;
}
}
function * $forOf(obj) {
for (const item of obj) {
yield item;
}
}
This should work out fine in JS but if I ever wanted to port this to something like Rust I may have to rethink this approach :p On the other hand, I can simply inject this helper function into the generated code so maybe it's no big deal either way.
Eliminated
So now all these loops get simplified to regular
while
loops! I still have to implement some more assumptions over accessing properties over the result of calling
.next()
and reading
.done
and
.value
on these results. But for now it means I can totally eliminate the
for
keyword from my minimal normalized JS subset and assume loops only ever appear in a
while (true)
form.