Open Source Adventures: Episode 60: How Opal Ruby represents basic data types
Opal Ruby compiles Ruby code to JavaScript. In this episode we're not interested in actually running the code, just in seeing what it does.
Hello, World!
Opal Ruby code needs pretty big runtime, and it also generates source maps by default, and is generally very verbose. To inspect just the generated code we need to add some flag:
$ opal --no-source-map -cOE hello.rb
puts "Hello, World!"
Opal.queue(function(Opal) {/* Generated by Opal 1.5.0 */
var self = Opal.top, nil = Opal.nil;
Opal.add_stubs('puts');
return self.$puts("Hello, World!")
});
Some things we can notice:
- it doesn't look anything like code anyone would actually write by hand
- it's all ES5 code without any features like
let
or=>
- Ruby methods all get
$
prefix to avoid conflicts with JavaScript
Going forward I'll just show the interesting parts of the generated code, not all the wrappers.
Booleans and Nils
a = true
b = false
c = nil
Becomes:
a = true;
b = false;
c = nil;
true
and false
are standard JavaScript values, but nil
is Opal.nil
object as its semantics are completely unrelated to JavaScript null
or undefined
.
Booleans have compatible semantics, so they won't cause issues. nil
getting into JavaScript world, or null
/ undefined
leaking into Opal world might very well cause problems with interoperability.
Numbers
a = -420
b = 6.9
c = a + b
d = 999_999_999_999_999_999
e = a.abs
Becomes:
a = -420;
b = 6.9;
c = $rb_plus(a, b);
d = 999999999999999999;
e = a.$abs();
So many things to unpack here:
- JavaScript has only one number type, so everything turns into a float
- semantics of even basic arithmetic operations do not match, so Opal needs to compile
a + b
as$rb_plus(a, b)
everywhere, incuring very considerable performance penalty - that
d
which we specified as999999999999999999
actually becomes1000000000000000000
as JavaScript lacks integer precision number
prototype got extended by extra methods like$abs
- and for that matter$+
etc.
rb_plus
is a fairly simple function, and in principle a smart compiler could completely remove the overhead, but I don't think browser JITs can realistically deal with it:
function(l,r) { return (typeof(l) === 'number' && typeof(r) === 'number') ? l + r : l['$+'](r); }
Also in principle, some type analysis on Opal side could replace some $rb_plus
calls with +
when both arguments are known to be number
s.
This has high change of causing incompatibility with Ruby code, as 5/2
is 2
in regular Ruby and 2.5
in Opal Ruby.
Strings
a = "world"
b = :foo
c = "Hello, #{a}!"
Becomes:
a = "world";
b = "foo";
c = "Hello, " + (a) + "!";
Ruby has mutable String
s and immutable Symbol
s. JavaScript has only immutable string
s.
Notably string interpolation is not compiled to ES6 string interpolation, it's compiled to ES5 +
chains.
This can cause compatibility problems with any Ruby code that expects mutable strings, which is fairly common for building them with <<
. At least such code raises exceptions instead of having different behavior, so it's easy to identify affected code.
It will also be incompatible with any Ruby code that expects Symbols to be separate from Strings, like to mark special values.
Arrays
a = []
b = [10, 20, 30]
b[2] = 40
c = b[0]
d = b[-1]
Becomes:
a = [];
b = [10, 20, 30];
b['$[]='](2, 40);
c = b['$[]'](0);
d = b['$[]'](-1);
Arrays compile to JavaScript arrays, and they're compatible enough. Unfortunately JavaScript arrays don't have negative numbers indexing from the end, so all access needs to go through wrappers, at significant performance cost, and even type analysis wouldn't save us here.
Hashes
a = {}
b = { 10 => 20, 30 => 40 }
c = { hello: "world" }
Becomes:
a = $hash2([], {});
b = $hash(10, 20, 30, 40);
c = $hash2(["hello"], {"hello": "world"});
There's nothing in pre-ES6 JavaScript that even resembles Ruby's Hash
es. Using JavaScript objects
which stringify all keys would break pretty much all Ruby code except for the most trivial kind.
ES6+ one might use Map
, but Opal Ruby doesn't use ES6+ features, and in any case, Map
has very poor interoperability with regular JavaScript, so it's not clear it would be much of a win.
Story so far
Looking at even the most basic data types we ran into long list of incompatibilities with Ruby code, issues with JavaScript interoperability, and performance problems. And it's not like Opal Ruby is doing something wrong - any improvement in one of these areas would make another worse.
Coming next
Over the next few episodes we'll continue exploring Opal Ruby.