puffnfresh/roy

Variable scopes mismatching between Roy and JavaScript

Opened this issue · 6 comments

Problem

The variable scopes in JavaScript are whole function; however scopes in Roy are only after the let declaration.
This results in referencing uninitialized variables.

Sample Code

let x = 1
let f = \() ->
  console.log(x)
  let x = true
  x
console.log(f())

Expected

1
true

Actual

undefined
true

Cause

The code is compiled into

var x = 1;
var f = function() {
    console.log((x));
    var x = true;
    return x;
};
console.log(f());

The scope in JavaScript are whole function, thus the code is equivalent to

var x = 1;
var f = function() {
    var x;
    console.log((x));
    x = true;
    return x;
};
console.log(f());

Possible Fix

I guess there are two ways (or more?) but neither is satisfactory.

Option1: Adapting to Roy semantics

Compile the code into

var x = 1;
var f = function() {
    console.log((x));
    return (function() {
      var x = true;
      return x;
    })();
};
console.log(f());

or

var x = 1;
var f = function() {
    console.log((x));
    var x1 = true; // renaming the variable
    return x1;
};
console.log(f());

Both mess compiled code to some extent.

Option 2: Adapting to JavaScript semantics

Raise error if referencing uninitialized variable.

This is inconvenient and not intuitive.

Match expression has same problem.
The code below outputs undefined.

data Option a =
  Some a | None

let x = 2

match (None ())
  case None = console.log x
  case (Some x) = console.log x

Compiled code:

...

var x = 2;
(function() {
    if(None() instanceof None) {
        return console.log(x);
    } else if(None() instanceof Some) {
        var x = None()._0;
        return console.log(x);
    }
})();

which is equivalent to

...

var x = 2;
(function() {
    var x;
    if(None() instanceof None) {
        return console.log(x);
    } else if(None() instanceof Some) {
        x = None()._0;
        return console.log(x);
    }
})();

My original idea with let binding was to generate option 1 when a conflict was detected.

But I want the compiler to be smart enough to not have to place function expressions everywhere; it should only put them in places that a native JavaScript developer would want to place them.

I think it is acceptable. There seems to be no ideal solutions.

Yes, Option 1 is very acceptable to me. My preferred compilation:

var x = 1;
var f = function() {
    console.log(x);
    return function(x) {
      return x;
    }(true);
};
console.log(f());

For expressions without side effects (or if the new var is referenced at most once), the instances in the continuation can be rewritten to the assigned value:

let x = 1
let f = \() ->
  console.log(x)
  true
console.log(f())
var x = 1;
var f = function() {
    console.log(x);
    return true;
};
console.log(f());

... which is the "ideal" solution, though unfortunately not possible in all cases. IIFEs would be needed for things like this:

let x = 1
let f = \() ->
  console.log(x)
  let x = g()
  console.log(x)
  x
console.log(f())
var x = 1;
var f = function() {
    var __temp;
    console.log(x);
    return function(x){
      console.log(x);
      return x;
    }(g());
};
console.log(f());

The drawback of IIFEs for things like this is that they interfere with TCO - see the TCO PR discussion.

Given that, I think renaming the shadowed var (e.g. var x2 = ...) is the best remaining option.