Four fors

2024-10-21

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 whileloop.

You can find Preval on github: github.com/pvdz/preval
There 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?

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

Code:
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.