Experimenting with sweet.js macros

Last week I decided to give sweet.js a try. Here are a couple of interesting examples that I cooked up, together with some explanations of how they work.

Coffeescript’s do syntax, with a twist

Coffeescript’s do syntax allows us to set the parameter values for an IIFE1 at the top of the function, rather than at the bottom. Here is a sweet.js macro that does just that:

macro $do {
    case ($($x:ident = $y:expr) (,) ...) $body => {
        (function ($x (,) ...) $body)($y (,) ...)
    }
}

$do (a = 1, b = 2) {
  return a + b;
}

/* result:
(function (a$15, b$16) {
    return a$15 + b$16;
}(1, 2));
*/

$do was used instead of do as sweet.js is presently unable to override reserved keywords. However, there is an outstanding bug to fix this.

The tricky part of this macro is the pattern ($($x:ident = $y:expr) (,) ...), which matches sequences like (), (a=b), and (a=b, c=d). The ellipsis behaves much like the Kleene star does in regular expressions, matching zero or more of the preceding pattern. Since $x = $y consists of multiple tokens, we need to group them in $() to ensure the ellipsis gets applied to all of them. Finally, (,) indicates that , is a separator, meaning that commas will get matched only if they are between two $x = $y groups, but not if they are trailing commas.

Observe that when we use ellipsis-captured values in our macro body, they must be followed by ellipses as well; the macro compiler will throw an error otherwise. Also, it is worth noting that if (,) was not followed by an ellipsis, it will not match a comma, but the literal sequence of “open paren, comma, close paren”.

One thing I’ve always wanted in Coffeescript was the ability to name the function, so we could call it recursively. Let us add an identifier to our do construct:

macro $do {
    case $name ($($x = $y) (,) ...) $body => {
        (function $name ($x (,) ...) $body)($y (,) ...)
    }
}

Now we can do a one-off calculation of a BST height succinctly:

var height = $do getHeight(node = root) {
    if (node === null) return 0;
    return Math.max(getHeight(node.left), getHeight(node.right)) + 1;
}

An efficient forEach

For starters, here’s an each macro that desugars into a plain for loop, allowing for functional syntax without the function call overhead:

macro each {
    case ($list:ident, ($x:ident) { $line ... }) => {
        for (var i = 0; i < $list.length; i++) {
            var $x = $list[i];
            $line ...
        }
    }
}

var list = [1,2,3];
var i = 10;
each(list, (x) { console.log(x) })
console.log(i); // still 10!

/* compiled result:
var list$6 = [ 1, 2, 3 ];
var i$10 = 10;
for (var i = 0; i < list$6.length; i++) {
    var x$11 = list$6[i];
    console.log(x$11 + 1);
}
console.log(i$10);
*/

Observe that variables are renamed such that the two usages of i do not conflict, even though both are placed in the same scope after the macro expansion. This is sweet.js’ hygiene algorithm at work.

Unfortunately, desugaring map and filter in the same way is a bit more tricky because of the need to extract the return value of the function. I’m still working on it.

Chained Comparisons

Languages like Coffeescript and Python allow us to chain together comparisons, such that expressions like x < y < z are equivalent to x < y && y < z. Note that the problem is complicated by the fact that we don’t want to insert && between non-relational operators: i.e. we don’t want to translate x && y < z to x && y && y < z.

This gist demonstrates how to implement the same feature with sweet.js. A couple of things are worth noting:

One limitation of this macro is that it only works in our special if and while statements. I reckon that when infix macros are added, this macro can be generalized to handle relational expressions that appear in other places as well.

Getting your feet wet

If you would like to learn more about sweet.js, read the docs, subscribe to the mailing list, and / or hang out on IRC at #sweet.js on irc.mozilla.org. Admittedly, sweet.js is definitely still in a rather alpha state – the lack of clear error reports make debugging a challenge, for instance – but it’s certainly ready for experiments. And Tim Disney (its creator) has been very helpful with explaining (and fixing) the problems I’ve encountered. Thanks Tim!


  1. Immediately Invoked Function Expression.

Related Posts

Jez Ng 12 November 2012
blog comments powered by Disqus