Open Source Adventures: Episode 60: How Opal Ruby represents basic data types
Tags: ruby, javascript
Published: 2022-05-23 17:48:55

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
```

```ruby
puts "Hello, World!"
```

```javascript
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

```ruby
a = true
b = false
c = nil
```

Becomes:

```javascript
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

```ruby
a = -420
b = 6.9
c = a + b
d = 999_999_999_999_999_999
e = a.abs
```

Becomes:

```javascript
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:

```javascript
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

```ruby
a = "world"
b = :foo
c = "Hello, #{a}!"
```

Becomes:

```javascript
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

```ruby
a = []
b = [10, 20, 30]
b[2] = 40
c = b[0]
d = b[-1]
```

Becomes:

```javascript
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

```ruby
a = {}
b = { 10 => 20, 30 => 40 }
c = { hello: "world" }
```

Becomes:

```javascript
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

[All the code is on GitHub](https://github.com/taw/open-source-adventures).

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.