public class ClosureExtractor extends CAstRewriterExt
A CAst rewriter for extracting bits of code into one-shot closures. What to extract is determined by an ExtractionPolicy.

For instance, a ForInBodyExtractionPolicy extracts the body of every for-in loop in the program, whereas a CorrelatedPairExtractionPolicy extracts pieces of code containing correlated property reads and writes of the same property.

As an example, consider the following function:

   function extend(dest, src) {
     for(var p in src)
       dest[p] = src[p];
   }
 

Under both ForInBodyExtractionPolicy and CorrelatedPairExtractionPolicy, this should be transformed into

   function extend(dest, src) {
     for(var p in src)
       (function _forin_body_0(p) {
         dest[p] = src[p];
       })(p);
   }
 

There are four issues to be considered here.

  • References to this:

    If the code to extract contains references to this, these references have to be rewritten; otherwise they would refer to the global object in the transformed code.

    We do this by giving the extracted function an extra parameter thi$, and rewriting this to thi$ within the extracted code.

    For instance,

       Object.prototype.extend = function(src) {
         for(var p in src)
           this[p] = src[p];
       }
       

    becomes

       Object.prototype.extend = function(src) {
         for(var p in src)
           (function _forin_body_0(p, thi$) {
             thi$[p] = src[p];
           })(p, this);
       }
       
  • Local variable declarations:

    Local variable declarations inside the extracted code have to be hoisted to the enclosing function; otherwise they would become local variables of the extracted function instead.

    This is already taken care of by the translation from Rhino's AST to CAst.

    Optionally, the policy can request that one local variable of the surrounding function be turned into a local variable of the extracted closure. The rewriter checks that this is possible: the code to extract must not contain function calls or new expressions, and it must not contain break, continue, or return statements. The former requirement prevents a called function from observing a different value of the local variable than before. The latter requirement is necessary because the final value of the localised variable needs to be returned and assigned to its counterpart in the surrounding function; since non-local jumps are encoded by special return values (see next item), this would no longer be possible.

  • break, continue, return:

    A break or continue statement within the extracted loop body that refers to the loop itself or an enclosing loop would become invalid in the transformed code. A return statement would no longer return from the enclosing function, but instead from the extracted function.

    We transform all three statements into return statements returning an object literal with a property type indicating whether this is a 'goto' (i.e., break or return) or a 'return'. In the former case, the 'target' property contains an integer identifying the jump target; in the latter case, the 'value' property contains the value to return.

    The return value of the extracted function is then examined to determine whether it completed normally (i.e., returned undefined), or whether it returned an object indicating special control flow.

    For example, consider this code from MooTools:

       for(var style in Element.ShortStyles) {
         if(property != style)
           continue;
         for(var s in Element.ShortStyles[style])
           result.push(this.getStyle(s));
         return result.join(' ');
       }
       

    Under ForInBodyExtractionPolicy, this is transformed into

       for(var style in Element.ShortStyles) {
         var s;
         re$ = (function _forin_body_0(style, thi$) {
           if(property != style)
             return { type: 'goto', target: 1 };
           for(s in Element.ShortStyles[style]) {
             (function _forin_body_2(s) {
               result.push(thi$.getStyle(s));
             })(s);
           }
           return { type: 'return', value: result.join(' ') };
         })(style, this);
         if(re$) {
           if(re$.type == 'return')
             return re$.value;
           if(re$.type == 'goto') {
             if(re$.target == 1)
               continue;
           }
         }
       }
       

    Note that at the CAst level, break and continue are represented as goto statements, which simplifies the translation somewhat. The numerical encoding of jump targets does not matter as long as the extracted function and the fixup code agree on which number represents which label.

  • Assignment to loop variable:

    The loop body may assign to the loop variable. If the variable is referenced after the loop, this assignment needs to be propagated back to the enclosing function in the extracted code.

    TODO: This is not handled at the moment.

Finally, note that exceptions do not need to be handled specially.

Author:
mschaefer