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 as 999999999999999999 actually becomes 1000000000000000000 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 numbers.

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 Strings and immutable Symbols. JavaScript has only immutable strings.

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 Hashes. 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

All the code is on GitHub.

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.