I'm a bit stuck. Compiling Nulan to Racket is really easy and really awesome and really fast. The same is not true of JS. Because with JS, the compiler has to output a JS string, unlike in Racket where the compiler outputs an AST. The most significant issue I've found with this is that in Racket, I can take a function and splice it in directly. This gives me all kinds of nifty things like avoiding a variable lookup, partial information hiding, etc. This is not true in JS, where everything that isn't a literal has to be given a name. And inlining a literal function is much too verbose. So I gotta use names for things. Okay, fine, I figured out a naming scheme that I think will work well. But then I hit another snag... The fundamental problem has to do with dynamicism vs speed. I see two major ways to compile Nulan to JS: 1) Evaluate every top-level form incrementally. In other words, this program... (var foo 1)
(var bar 2)
(+ foo bar)
...would first compile and eval (var foo 1), then (var bar 2), and then (+ foo bar). This is how the Racket version of Nulan works. It's also how Arc works.This is great because there's no real distinction between compile-time and eval-time: macros can call functions, as long as those functions are defined before the macro is called. The downside is that this means the compiler needs to actually be available at eval-time. It also means that the Nulan source code needs to be reparsed and recompiled on the fly every time, rather than being compiled once. This is naturally going to be a lot slower than compiling it all at once. 2) Compile all the expressions at the same time. Meaning that this: (var foo 1)
(var bar 2)
(+ foo bar)
Would compile into something like this: var NULAN = (function (n) {
n.foo = 1,
n.bar = 2;
n.foo + n.bar;
return n
})(NULAN || {})
This string could be stored in a file, or evaluated all at once. If stored in a file, it would avoid the overhead of calling "eval" at runtime. This strategy can achieve speeds just as fast as hand-written JS.The problem with this strategy is that it forces a strict distinction between compile-time and eval-time: variables defined at compile-time (including macros) cannot use any variables defined at run-time. And run-time variables cannot use anything defined at compile-time. This also means that all of Nulan's stuff would need to be macros, basically. With strategy #1, I can actually do stuff like this: o["prn"] = console.log;
And now using (prn 1 2) is equivalent to using console.log(1, 2);But since everything is now macros, it would instead be like this: o["prn"] = new Macro(function () {
return ["call", [".", "console", "log"], [].map.call(arguments, macex)];
});
As you can see, this is clunkier. So, I'm torn. On the one hand, I really want the awesome speed of #2, but the flexibility of #1 is extremely useful, and I'm really used to programming in that way in Arc, which uses #1.Maybe what I really want is some kind of compiler that will compile to byte-code, and then have a small runtime portion that actually evaluates the byte-code... that may provide better speed while still keeping the same flexible semantics. |