Scala.js Semantics

and how they support performance and JavaScript interop

Scala.js logo

Sébastien Doeraene @sjrdoeraene

June 10, 2015 -- Scala Days Amsterdam

LAMP, lamp.epfl.ch
École polytechnique fédérale de Lausanne
 

EPFL logo             Scala logo

Compiling to JavaScript

Hundreds of languages compile to JavaScript

Languages that don't compile to JS are weird

Compiling to JavaScript: Problem solved!

What are the hard parts, then?

The hard parts

  • Interoperability with JavaScript code
  • Performance

Rely on Semantics

WAT moment

JavaScript


["10", "10", "10", "10"].map(parseInt)
    

10,NaN,2,3
    

[parseInt("10", 0), parseInt("10", 1),
    parseInt("10", 2), parseInt("10", 3)]
    

["10", "10", "10", "10"].map(s => parseInt(s)) // ES 6 FTW
    

10,10,10,10
    

Scala


new Array[Any](3).toSet()
    

false
    

new Array[Any](3).toSet
    

Set(null)
    

new Array[Any](3).toSet.apply(()) // contains () ?
    

Scala.js


new js.Array[Any](3).toSet()
    

true
    

new js.Array[Any](3).toSet
    

Set(undefined)
    

() == js.undefined
    

true
    

Scala.js Semantics

Overview

  • Discrepancies wrt Scala/JVM
    • Primitive data types
    • Undefined behaviors
  • Interoperability-specific features
    • Calling JavaScript code from Scala.js
    • Exporting Scala.js code to JavaScript
  • Optimizations

Primitive data types

Primitive numeric data types

  • JS only has doubles
  • What should we do about Byte, Short, Int, Float?
  • What about Long?
  • What about Char?

Int

  • Even though JS has no ints, JS VMs do, as an optimization
  • Bitwise ops use int32 (among others)

Encoding Int operations

Scala.jsJavaScript
a + b(a + b) | 0
a - b(a - b) | 0
a * bMath.imul(a, b)
a / b(a / b) | 0
a % b(a % b) | 0

Int semantics

Scala.js gives you true, efficient Ints on the JS platform

Primitive numeric data types

Data typeJVM semanticsFast
Int check check
Byte, Short, Char check check
Long check close
Float close check
Float (strict) check close
Double check check

No boxing for primitive types (except Chars)

  • Obvious win for performance
  • Also a win for interoperability with JS (see later)

Requires two changes in the semantics, wrt Scala/JVM

toString()

TypeScala/JVMScala.js
Integers, Char, BooleanSame on both platforms
1.0.toString1.01
().toString()undefined

isInstanceOf[T]

Based on the value rather than the type

1.0, -3Byte, Short, Int, Double
128, -200Short, Int, Double
2147483647Int, Double
2147483648Double
1.5, -0.0, NaN, ±∞Double

Undefined behaviors

ClassCastException on the JVM


val s = "hello"
s.asInstanceOf[List[String]]
    
  • Casting to an incorrect type is a bug
  • Yet the JVM is very nice to you, and specifies that it will throw a ClassCastException

java.lang.ClassCastException: java.lang.String
  cannot be cast to scala.collection.immutable.List
    

ClassCastException on JavaScript

  • No such thing
  • Scala.js has to encode the checks with user code
  • Big problem: up to 100% overhead on the execution time

Solution: declare undefined behavior

  • Scala.js specifies that an erroneous cast is undefined behavior
  • No need to check: cast is a no-op
  • Undefined behavior means anything can happen

Problems with undefined behavior

  • Anything can happen means very poor debuggability
  • Solution: dev mode with checks and prod mode without
  • aka fastOpt and fullOpt

Checked undefined behaviors

  • dev mode -- Fatal behavior
    • Throw UndefinedBehaviorError
    • Fatal error not matched by case NonFatal(e)
    • Provides debuggability with instant failure
  • prod mode -- Unchecked behavior
    • Behave arbitrarily
    • Provides performance (cast = no-op)
  • Opt-in Compliant behavior
    • Throw ClassCastException, as on the JVM
    • Provides portability if you really need it (but slow)

Other undefined behaviors

  • NullPointerException
  • ArrayIndexOutOfBoundsException* and StringIndexOutOfBoundsException*
  • ArrayStoreException*
  • ArithmeticException* (such as integer division by 0)
  • StackOverflowError and other VirtualMachineErrors

Currently all unchecked

* Will eventually have Fatal/Unchecked/Compliant settings

JavaScript interoperability

JavaScript interoperability


def main(pre: html.Pre): Unit = {
  val xhr = new dom.XMLHttpRequest()
  xhr.open("GET",
      "http://api.openweathermap.org/" +
      "data/2.5/weather?q=Singapore")
  xhr.onload = { (e: dom.Event) =>
    if (xhr.status == 200)
      pre.textContent = xhr.responseText
  }
  xhr.send()
}
    

trait js.Any extends scala.AnyRef

class XMLHttpRequest extends /*...*/ js.Any {
  def status: Int = js.native
  def responseText: String = js.native
  var onload: js.Function1[Event, _] = js.native

  def open(method: String, url: String,
      async: Boolean = js.native, ...): Unit = js.native
  def send(data: js.Any = js.native): Unit = js.native
}
    

Semantics of raw JS operations

Scala.jsJavaScript semantics
new Foo(a, b)new global.Foo(a, b)
xhr.open(a, b, c, d)xhr.open(a, b, c, d)
xhr.open(a, b)xhr.open(a, b)
xhr.statusxhr.status
pre.textContent = vpre.textContent = v

global is the JavaScript global object

JavaScript interoperability


def main(pre: html.Pre): Unit = {
  val xhr = new dom.XMLHttpRequest()
  xhr.open("GET",
      "http://api.openweathermap.org/" +
      "data/2.5/weather?q=Singapore")
  xhr.onload = { (e: dom.Event) =>
    if (xhr.status == 200)
      pre.textContent = xhr.responseText
  }
  xhr.send()
}
    

Type correspondence

Scala.js valuesJavaScript type
primitive numbers except Longsnumber
true and falseboolean
non-null stringsstring
()undefined
nullnull

Type non-correspondence

Instances of other Scala classes (non-raw JS types) are opaque to JavaScript

  • Scala lambdas are not JS functions
  • Scala Arrays are not JS arrays
  • Chars and Longs are not JS numbers nor strings
  • Value classes are not seen as their underlying value

JavaScript functions


trait js.Function1[-T1, +R] extends /*...*/ js.Any {
  def apply(arg1: T1): R = js.native
}

implicit def fromFunction1[T1, R](
    f: T1 => R): js.Function1[T1, R] = <primitive>
    

Returns a JavaScript function g such that:

JavaScriptScala.js semantics
g(a)f.apply(a)

Embedding JavaScript code?

StackOverflow question about embedding JavaScript code

js.Dynamic


def main(pre: js.Dynamic): Unit = {
  val xhr = js.Dynamic.newInstance(
      js.Dynamic.global.XMLHttpRequest)()
  xhr.open("GET", "http://api.openweathermap.org/...")
  xhr.onload = { (e: js.Dynamic) =>
    if (xhr.status == 200)
      pre.textContent = xhr.responseText
  }
  xhr.send()
}
    

Don't trust facade types / JavaScript code

Gitter question about invalid result types in facades
  • Values coming back from JS are systematically cast to their "advertised" result type
  • If the cast is invalid, it is an undefined behavior

Exports

Exports

  • Reminder: values of any Scala class are opaque to JavaScript
  • Exports are here to enable explicitly a bit of transparency towards JavaScript

Exports


package pck
@JSExport class Point(x: Double, y: Double) {
  @JSExport def abs(): Double = Math.hypot(x, y)
}
@JSExport object MyApp {
  @JSExport def main(): Unit = println("hello")
}
    
JavaScriptScala.js semantics
new global.pck.Point(x, y)new Point(x, y)
p.abs()p.abs()
global.pck.MyApp().main()MyApp.main()

The only standard @JSExport


package java.lang

class Object {
  ...
  @JSExport def toString(): String = ???
}
    
JavaScriptScala.js semantics
("" + foo) /
  foo.toString()
foo.toString()

Optimizations

Closed world

  • At link time, we know the entire Scala.js program
  • There is no reflection
  • Only @JSExport'ed entities can be accessed from JavaScript
  • Facade types have JS semantics, so this applies to them as well

Conclusion: we live in a closed world

Closed world

The closed world assumption is a heaven for optimizations


val (white, black) = allSquares.foldLeft((0, 0)) {
  case ((white, black), square) =>
    square.owner match {
      case White    => (white+1, black)
      case Black    => (white, black+1)
      case NoPlayer => (white, black)
    }
}
    

var xs = this.allSquares$1;
var start = 0;
var end = xs.u["length"];
var z_1 = 0;
var z_2 = 0;
while (true) {
  if ((start === end)) {
    var x1_1 = z_1;
    var x1_2 = z_2;
    break
  } else {
    var temp$start = ((1 + start) | 0);
    var x1$1 = xs.u[start];
    var white = z_1;
    var black = z_2;
    var x1$2 = x1$1.$$undowner$1;
    if (($m_Lreversi_White$() === x1$2)) {
      var temp$z_1 = ((1 + white) | 0);
      var temp$z_2 = black
    } else if (($m_Lreversi_Black$() === x1$2)) {
      var temp$z_1 = white;
      var temp$z_2 = ((1 + black) | 0)
    } else if (($m_Lreversi_NoPlayer$() === x1$2)) {
      var temp$z_1 = white;
      var temp$z_2 = black
    } else {
      throw new $c_s_MatchError().init___O(x1$2)
    };
    start = temp$start;
    z_1 = temp$z_1;
    z_2 = temp$z_2;
    continue
  }
};
var white$1 = x1_1;
var black$1 = x1_2;
    

println("Hello world!")
3 times {
  println("Hi!")
}

implicit class IntTimes(val self: Int) extends AnyVal {
  def times(body: => Unit): Unit = {
    for (i <- 0 until self)
      body
  }
}
    

this.println__O__V("Hello world!");
var i = 0;
var count = 0;
while ((i !== 3)) {
  var v1 = i;
  $m_Lhelloworld_HelloWorld$().println__O__V("Hi!");
  count = ((1 + count) | 0);
  i = ((1 + i) | 0)
}
    

Are we fast yet?

Scala.js logo

scala-js.org

  • It's Scala!
  • It talks to JavaScript!
  • It's fast!