Rewriting oddities

2013-06-06

The life of a code rewriter is certainly an interesting. Sometimes :)

So in this case rewriting means that we take input JavaScript source code and transform it somehow. The output will then do something special, usually wrapping parts of the input or maybe removing parts of it.

In this case we needed to wrap all assignments. The main problems here are initializers of var declarations, including those inside for statements. There are two for statements; the for-in and the for-loop. So we need to transform three types of statements. And any assignment in an expression, but those are trivial so we will take those for granted now.

The easiest one is the var-statement of course. To wrap initializers while keeping semantics in tact, we need to separate them into two statements. And therefor we also need to wrap those into a block statement because it would split one statement into two and that might go wrong:

Code:
if (foo) var bar = 10;

// ->

if (foo) var bar; wrap(bar,

The block wrap will fix this while keeping semantics in tact since blocks in JS don't have a scope (they're effectively a NOOP, except for grouping a set of statements into one).

Code:
if (foo) var bar = 10;

// ->

if (foo) { var bar; wrap(bar, 10); }

Ok so that was easy. Now we can't really apply the same trick for the for-statement:

Code:
for (var foo = 10; ...; ...) ...;

// ->

for (var foo; wrap(foo, 10); ... ; ...) ...;

Clearly, this is not a valid rewrite. However, we know that the var can only exist in the first part of the for header. We also know that this first part is only an initializer. Since it is, there is semantically no difference between running it before the for or as part of the initializer. None whatsoever, actually. This rewrite is perfectly safe:

Code:
for (var foo = 10; ...; ...) ...;

// ->

var foo = 10; for(; ...; ...) ...;

// so ->

var foo; wrap(foo, 10); for(; ...; ...) ...;

// don't forget the parens ->

{ var foo; wrap(foo, 10); for(; ...; ...) ...; }

Now this seems like a perfectly valid rewrite that applies to generic JS. However, it is not because there is one edge case that will cause a crash when you don't take it into account... Labels!

Code:
repeat: for (var foo = 10; ...; ...) ...;

// ->

repeat: { var foo; wrap(foo, 10); for(; ...; ...) ...; }

Think this is valid? Then you too forgot that you can jump to a label using break and continue. And while break only requires a valid label in the statement context, continue also requires itself AND the label (if any) to be used to be within a loop. And it is okay to continue to a label directly prefixed to a for, but not to a label any higher up the statement hierarchy. So to fix this, we need to do the rewriting such that this label restriction is not violated.

Code:
repeat: { var foo; wrap(foo, 10); for(; ...; ...) continue repeat; }

// becomes

{ var foo; wrap(foo, 10); repeat: for(; ...; ...) continue repeat; }

Huzah! Browser happy now.

(Note that rewriting assignments requires a bit more than just wrap(varname, value); because there's no such thing as a pointer in JS)