CodeBrief

Hacking the CoffeeScript Redux Compiler

CoffeeScript is great. I'm not going to spend any time backing that statement up in this article, but you are free to stop reading if it's not your cup of tea.

My main gripe with CoffeeScript, however, is a small impedance mismatch with the Ember.js object model. Creating classes using the new operator in coffeescript does not translate well to the Ember convention of creating object's via the create method.1

Specifically, consider the following CoffeeScript:

class Developer
class Brogrammer extends Developer
b = new Brogrammer

Running this through the CoffeeScript compiler generates something along the lines of:

var b, Brogrammer, Developer;
Developer = function () {
  function Developer() {
  }
  return Developer;
}();
Brogrammer = function (super$) {
  extends$(Brogrammer, super$);
  function Brogrammer() {
  }
  return Brogrammer;
}(Developer);
b = new Brogrammer;
function isOwn$(o, p) {
  return {}.hasOwnProperty.call(o, p);
}
function extends$(child, parent) {
  var key;
  for (key in parent)
    if (isOwn$(parent, key))
      child[key] = parent[key];
  function ctor() {
    this.constructor = child;
  }
  ctor.prototype = parent.prototype;
  child.prototype = new ctor;
  child.__super__ = parent.prototype;
  return child;
}

When dealing with Ember.js, however, it would be desirable to output the following:

var b, Brogrammer, Developer;
Developer = Ember.Object.extend();
Brogrammer = Developer.extend();
b = Brogrammer.create();

Fortunately, I have been following the CoffeeScript Redux Project by Michael Ficarra closely and am excited by its potential. From its own description, it aims to be a "rewrite of the CoffeeScript compiler with proper compiler design principles and a focus on robustness and extensibility." Although not complete, extensibility is exactly what we need here and it is far enough along for a proof of concept.

In this post I am going to detail exactly that: how one would go about creating a modified, Ember-specific version of the CS compiler which conforms to the Ember object model.

Compiler Overview

In order to make this modification, it is important to grok the structure of the CSR compiler. Below are the steps the coffee binary goes through during compilation:

  1. Preprocessing - Perform lexical analysis and convert the input into tokens.
  2. Parsing - Construct the CoffeeScript abstract syntax tree (AST). This is written using PEG.js.
  3. Optimising - Perform optimizations and simplify the AST.
  4. Compiling - Compile the CS AST into a JavaScript AST.
  5. Code generation - Output JS, this uses escodegen.

Since we are not adding any new syntax to CoffeeScript itself, our primitive proof of concept only has to worry about the compilation phase: we need only modify the generated JavaScript AST to use Ember's conventions.

Modifying the Code

After checking out the code from the GitHub repo, let's try and test out the existing behavior. Create a file called test.coffee in the root of the repository and populate it with the example at the top of this post. Then run:

make && ./bin/coffee --debug --js < test.coffee

This will output a lot of information, including the AST trees and the final javascript. The outputted javascript should be close to the output I gave at the beginning of this post.

For the purposes of this blog post, there are three main changes that need to be made:

  1. Base class definitions should use Ember.Object.extend();
  2. Subclasses should call extend on the base class.
  3. Class instantiation should use create instead of new.

The code for the compilation phase is predictably located in src/compiler.coffee. This file contains a set of rules for translating CoffeeScript AST nodes to Javascript AST nodes. For reference, the potential JS nodes are defined in src/js-nodes.coffee. For the first two changes above, the pertinent code is located in compiler.coffee near the code:

    [CS.Class, ({nameAssignee, parent, name, ctor, block, compile}) ->
      args = []
      params = []
      parentRef = genSym 'super'
      block = forceBlock block
      ...

As CoffeeScript supports executable class bodies, bound methods, etc., the logic inside here is non-trivial. However, looking towards the bottom of the method, we see that it returns an expression consisting of:

new JS.CallExpression (new JS.FunctionExpression null, params, block), args

This represents the outer function of a CoffeeScript class definition. For ember, we want to change this to a call to Ember.Object.extend. The following code does exactly that:

new JS.CallExpression memberAccess(memberAccess(new JS.Identifier('Ember'), 'Object'), 'extend'), []

We also need to comment out the existing inheritance logic and change the target of the create expression when the class definition is extending another class. I am not going to include the code here, but feel free to check out the repo which contains these changes.

To modify new to use create, the change is rather trivial. Simply change:

[CS.NewOp, ({ctor, arguments: args}) -> new JS.NewExpression ctor, args]

to:

[CS.NewOp, ({ctor, arguments: args}) -> new JS.CallExpression memberAccess(ctor, 'create'), []]

This changes the CoffeeScript new expression from outputting a JS new expression to outputting a JS call expression. If you now re-run the command you should see the following output:

➜  CoffeeScriptRedux git:(ember) ✗ ./bin/coffee --js < test.coffee 
// Generated by CoffeeScript 2.0.0
void function () {
  var b, Brogrammer, Developer;
  Developer = Ember.Object.extend();
  Brogrammer = Developer.extend();
  b = Brogrammer.create();
}.call(this);

That is all their is to it! The compiler now outputs class definitions using the Ember object-model. You can view the working code on github.

As a disclaimer, this simple prototype is an experiment and will not provide much real-world value. It is only the beginning of an Ember-infused CoffeeScript fork. A more sophisticated version would take into account class bodies, mixins, bindings, getters/setters and other Ember goodness, far beyond the scope of this post. But stay tuned :).

1. Technically, Ember classes actually are compatible with the new operator, but there is no way to pass in a hash parameter as the class body for initialization.

comments powered by Disqus