I want to stress, though, that I'm not singling Opal out here. I think Opal is a really cool project, and I hope it works well for lots of people. It's just a good example language since it's on HN right now.
Let's compile this Ruby code to JS with Opal:
a = 0
10000000.times do
a = a + 1
end
puts a
Opal gives us: /* Generated by Opal 0.6.2 */
(function($opal) {
var $a, $b, TMP_1, self = $opal.top, $scope = $opal, nil = $opal.nil, $breaker = $opal.breaker, $slice = $opal.slice, a = nil;
$opal.add_stubs(['$times', '$+', '$puts']);
a = 0;
($a = ($b = (10000000)).$times, $a._p = (TMP_1 = function(){var self = TMP_1._s || this;
return a = a['$+'](1)}, TMP_1._s = self, TMP_1), $a).call($b);
return self.$puts(a);
})(Opal);
We'll compare it to some vanilla JS: var a = 0;
for (var i = 0; i < 10000000; i++) {
a = a + 1;
}
I don't care at all that the generated code is a little funny looking. That's fine. Opal's code is actually pretty readable to me. What is a problem for some (many?) users is the performance.You'll note that Opal did not compile "a + 1" to "a + 1". Instead it generated "a['$+'](1)". That's because Ruby's arithmetic semantics are different from JavaScript's. To implement those semantics correctly, it needs to use a method call instead of using the built-in arithmetic.
We can profile the two using this fiddle: http://jsfiddle.net/3UtNf/1/
On my laptop, the Opal code is 264 times slower than the raw JS code. In other words, it runs at 0.3% of the speed of the JS code. Now imagine sacrificing that much perf on a mobile device. That's enough to make the language unsuitable for many real-world use cases.
This isn't intractable, though. You just need to compile math down to real JS arithmetic operators when JS's semantics line up with your language's (which typically means, when you're suring you've got numbers and not some other type with a user-defined operator).
Determining where you can do that is the hard part. It requires type analysis. Doing that well in a language that doesn't have a sound static type system requires whole-program analysis. It's extremely complex, monolithic, and leads to very strange output code.
This is why, for example, Dart's dart2js compiler is so complex and heavyweight. It does do this kind of analysis. It compiles this Dart program:
main() {
var a = 0;
for (var i = 0; i < 10000000; i++) {
a = a + 1;
}
print(a);
}
to this JS: function() {
var a, i, line;
for (a = 0, i = 0; i < 10000000; ++i)
++a;
line = "" + a;
H.printString(line);
}
That has the same performance as the JS code. The reason it does this is because it knows "a" is a number. If we change the Dart code to: class Foo {
operator +(other) => this;
}
main() {
var a = new Foo();
for (var i = 0; i < 10000000; i++) {
a = a + 1;
}
print(a);
}
Then generated JS changes completely: function() {
var i, line;
for (i = 0; i < 10000000; ++i)
;
line = H.S(new Q.Foo());
H.printString(line);
}
Interestingly, here you can see the compiler understood that the "+" operator on Foo always returns the same object and was able to inline the call to it and then hoist it out of the loop completely.This is the kind of stuff you need to do if you want to have a language that compiles to JS and (unlike, say CoffeeScript and TypeScript) has semantics that aren't very very similar to JS.