Skip to content

Instantly share code, notes, and snippets.

@bobzhang
Last active January 5, 2023 18:17
Show Gist options
  • Star 23 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bobzhang/9f27a5a0bd730e8d3503bf5d058a58a7 to your computer and use it in GitHub Desktop.
Save bobzhang/9f27a5a0bd730e8d3503bf5d058a58a7 to your computer and use it in GitHub Desktop.
Type safe Alt-JS language comparison
A list of languages which compile to JS (Elm, Purescript, OCaml)
(Inspired by this thread: https://groups.google.com/forum/#!topic/elm-discuss/Um7WIBTq9xU)
They all support curry calling convention by default.
Some interesting results:
1. `min` is curried function, only OCaml(BuckleScript) managed to optimize this overhead.
2. All optimize the self tail call
3. Only BuckleScript and PureScript type-specialized comparison functoin (>=) and inlined
4. PureScript generate a raise function which does not make sense
5. Only BuckleScript and PureScript generates module by module
Try websites:
http://elm-lang.org/try
http://try.purescript.org/
http://bloomberg.github.io/bucklescript/js-demo/
Only BuckleScript work offline (the compiler is compiled into JS too)
range : Int
range = 1000000000
testProg : Int -> Int
testProg n = -- do some work
let lmt = min (n + 100000000) range in
let loop i =
if i >= lmt then i else
loop (i + 1) in loop n
var _user$project$Temp1482759649866537$range = 1000000000;
var _user$project$Temp1482759649866537$testProg = function (n) {
var lmt = A2(_elm_lang$core$Basics$min, n + 10000000000, _user$project$Temp1482759649866537$range);
var loop = function (i) {
loop:
while (true) {
if (_elm_lang$core$Native_Utils.cmp(i, lmt) > -1) {
return i;
} else {
var _v0 = i + 1;
i = _v0;
continue loop;
}
}
};
return loop(n);
};
let range = 1000000000
let testProg n =
let lmt = Pervasives.min (n + 100000000) range
in
let rec loop i =
if i >= lmt
then i
else loop (i + 1)
in
loop n
// Generated by BUCKLESCRIPT VERSION 1.2.1 , PLEASE EDIT WITH CARE
'use strict';
var Pervasives = require("stdlib/pervasives");
function testProg(n) {
var lmt = Pervasives.min(n + 100000000 | 0, 1000000000);
var _i = n;
while(true) {
var i = _i;
if (i >= lmt) {
return i;
}
else {
_i = i + 1 | 0;
continue ;
}
};
}
var range = 1000000000;
exports.range = range;
exports.testProg = testProg;
module Main where
import Prelude
range = 1000000000
testProg n = -- do some work
let lmt = min (n + 100000000) range
in
let loop i =
if i >= lmt
then i
else loop (i + 1)
in
loop n
"use strict";
var Prelude = require("../Prelude");
var Data_Ord = require("../Data.Ord");
var Data_Semiring = require("../Data.Semiring");
var range = 1000000000;
var testProg = function (n) {
var lmt = Data_Ord.min(Data_Ord.ordInt)(n + 100000000 | 0)(range);
var loop = function (__copy_i) {
var i = __copy_i;
tco: while (true) {
var $0 = i >= lmt;
if ($0) {
return i;
};
if (!$0) {
var __tco_i = i + 1 | 0;
i = __tco_i;
continue tco;
};
throw new Error("Failed pattern match at Main line 9, column 8 - line 11, column 26: " + [ $0.constructor.name ]);
};
};
return loop(n);
};
module.exports = {
range: range,
testProg: testProg
};
@raysegantii
Copy link

raysegantii commented Dec 31, 2016

In ClojureScript:

(ns test-prog.core)

(def range-limit 1000000000)

(defn test-prog [n]
  (let [lmt (min (+ n 1000000000) range-limit)]
    (defn test-loop [i]
      (if (>= i lmt) i (recur (+ i 1))))))

compiles to

// Compiled by ClojureScript 1.9.293 {}
goog.provide('test_prog.core');
goog.require('cljs.core');
test_prog.core.range_limit = (1000000000);
test_prog.core.test_prog = (function test_prog$core$test_prog(n){
var lmt = (function (){var x__6533__auto__ = (n + (1000000000));
var y__6534__auto__ = test_prog.core.range_limit;
return ((x__6533__auto__ < y__6534__auto__) ? x__6533__auto__ : y__6534__auto__);
})();
return (
test_prog.core.test_loop = ((function (lmt){
return (function test_prog$core$test_prog_$_test_loop(i){
while(true){
if((i >= lmt)){
return i;
} else {
var G__7357 = (i + (1));
i = G__7357;
continue;
}
break;
}
});})(lmt))
)
;
});

@bobzhang
Copy link
Author

@raysegantii cool, just make sure is it the output of clojurescript (not through google closure)? Since the output of all other compilers does not go another bundler for fair comparison

@raysegantii
Copy link

@bobzhang yes the output has not went through google closure for optimizations (:optimization was set to :none).

@sjrd
Copy link

sjrd commented Jan 18, 2017

Scala.js:

package helloworld

import scala.scalajs.js
import js.annotation._

object HelloWorld extends js.JSApp {
  val range = 1000000000

  def testProg(n: Int): Int = {
    val lmt = (n + 100000000) min range
    def loop(i: Int): Int =
      if (i >= lmt) i
      else loop(i + 1)
    loop(n)
  }

  def main() {
    testProg(100000)
  }
}

Relevant part of the produced code:

$c_Lhelloworld_HelloWorld$.prototype.testProg__I__I = (function(n) {
  var x = ((100000000 + n) | 0);
  var that = this.range$1;
  var lmt = ((x < that) ? x : that); // <-- optimized `min` here
  return this.loop$1__p1__I__I__I(n, lmt)
});
$c_Lhelloworld_HelloWorld$.prototype.loop$1__p1__I__I__I = (function(i, lmt$1) {
  _loop: while (true) {
    if ((i >= lmt$1)) {
      return i
    } else {
      i = ((1 + i) | 0);
      continue _loop
    }
  }
});

But in the above code, min is not really curried (it does have a few indirections, as an extension method of primitive Ints). Currying is not typical of Scala code. Here is a variant with an explicitly defined, truly curried min function:

  def min(a: Int): Int => Int = b => Math.min(a, b)

  def testProg(n: Int): Int = {
    val lmt = min(n + 100000000)(range)
    def loop(i: Int): Int =
      if (i >= lmt) i
      else loop(i + 1)
    loop(n)
  }
$c_Lhelloworld_HelloWorld$.prototype.min__I__F1 = (function(a) {
  return new $c_sjsr_AnonFunction1().init___sjs_js_Function1((function(a$1) {
    return (function(b$2) {
      var b = (b$2 | 0);
      return ((a$1 < b) ? a$1 : b)
    })
  })(a))
});
$c_Lhelloworld_HelloWorld$.prototype.testProg__I__I = (function(n) {
  var lmt = this.min__I__F1(((100000000 + n) | 0)).apply$mcII$sp__I__I(this.range$1);
  return this.loop$1__p1__I__I__I(n, lmt)
});
$c_Lhelloworld_HelloWorld$.prototype.loop$1__p1__I__I__I = (function(i, lmt$1) {
  ... // same
});

Because currying is not common in Scala, Scala.js won't specifically try to optimize for it. However, it you ask it nicely (by putting @inline on min), you get the same code as before. So it is able to optimize currying away; it just doesn't have the heuristics to do it by default, because it's not typical.

@bobzhang
Copy link
Author

@sjrd thanks for your nice example, currying optimization is a quite advanced optimization (and important to curry based languages), it needs more sophisticated analysis, inlining can only solve some very simple cases.

@be5invis
Copy link

HERE IS IDRIS (with https://github.com/rbarreiro/idrisjs as backend):

module Main

range : Int
range = 1000000000

testProg : Int -> Int
testProg n = loop n
where
	lmt : Int
	lmt = min (n + 100000000) range

	loop : Int -> Int
	loop i = if i >= lmt then i else loop (i + 1)

main : IO()
main = printLn $ testProg 0

INTO (Prelude functions have been removed due to Idris uses WPO)

var idris_Main_46_testProg_58_lmt_58_0 = 
   (function(){
      return function(idris__123_e_95_0_125_){
         return idris_Prelude_46_Interfaces_46_Prelude_46_Interfaces_46__64_Prelude_46_Interfaces_46_Ord_36_Int_58__33_min_58_0((idris__123_e_95_0_125_ + 100000000))(1000000000)
      }
   })()


var idris_Main_46_testProg_58_loop_58_0 = 
   (function(){
      return function(idris__123_e_95_0_125_){
         return function(idris__123_e_95_1_125_){
            while(true){
               var cgIdris_2 = idris_Prelude_46_Interfaces_46_Prelude_46_Interfaces_46__64_Prelude_46_Interfaces_46_Ord_36_Int_58__33__62__61__58_0(idris__123_e_95_1_125_)(idris_Main_46_testProg_58_lmt_58_0(idris__123_e_95_0_125_));
               switch(cgIdris_2[0]){
                  case 0:
                     var cgIdris_3 = idris__123_e_95_0_125_;
                     var cgIdris_4 = (idris__123_e_95_1_125_ + 1);
                     idris__123_e_95_0_125_ = cgIdris_3;
                     idris__123_e_95_1_125_ = cgIdris_4;
                     continue;
                     break;
                  case 1:
                     return idris__123_e_95_1_125_;
                     break;
               }
               ;
               break
            }
         }
      }
   })()

var idris_Main_46_main = 
   (function(){
      return idris_Prelude_46_Interactive_46_putStr_39_(null)((idris_Prelude_46_Show_46_primNumShow(null)(idris_prim_95__95_toStrInt)([0])(idris_Main_46_testProg_58_loop_58_0(0)(0)) + "\n"))
   })()

@be5invis
Copy link

After some optimization:

function q_Main$$main(){
   return q_Prelude$Interactive$$putStr_39_(null, (q_Prelude$Show$$primNumShow(null, u_prim____toStrInt, [0], x_Main_46_testProg_58_loop_58_0(0, 0)) + "\n"))
}

function _runMain$0(){
   return js_idris_force(q_Main$$main()(null))
}

function x_Main_46_testProg_58_loop_58_0(_e$0, _e$1){
   while(true){
      var cgIdris_2 = q_Prelude$Interfaces$$Prelude_46_Interfaces_46__64_Prelude_46_Interfaces_46_Ord_36_Int_58__33__62__61__58_0(_e$1, x_Main_46_testProg_58_lmt_58_0(_e$0));
      switch(cgIdris_2[0]){
         case 0:
            var cgIdris_3 = _e$0;
            var cgIdris_4 = (_e$1 + 1);
            _e$0 = cgIdris_3;
            _e$1 = cgIdris_4;
            continue;
            break;
         case 1:
            return _e$1;
            break;
      }
      ;
      break
   }
}

function x_Main_46_testProg_58_lmt_58_0(_e$0){
   return q_Prelude$Interfaces$$Prelude_46_Interfaces_46__64_Prelude_46_Interfaces_46_Ord_36_Int_58__33_min_58_0((_e$0 + 100000000), 1000000000)
}

@be5invis
Copy link

Continue

function x_Main_46_testProg_58_lmt_58_0(_e$0){
   return q_Prelude$Interfaces$$Prelude_46_Interfaces_46__64_Prelude_46_Interfaces_46_Ord_36_Int_58__33_min_58_0((_e$0 + 100000000), 1000000000)
}

function x_Main_46_testProg_58_loop_58_0(_e$0, _e$1){
   while(true){
      var cgIdris_2 = q_Prelude$Interfaces$$Prelude_46_Interfaces_46__64_Prelude_46_Interfaces_46_Ord_36_Int_58__33__62__61__58_0(_e$1, x_Main_46_testProg_58_lmt_58_0(_e$0));
      if((cgIdris_2 === 0)) {
         var cgIdris_3 = _e$0;
         var cgIdris_4 = (_e$1 + 1);
         _e$0 = cgIdris_3;
         _e$1 = cgIdris_4;
         continue
      } else {
         return _e$1
      }
      ;
      break
   }
}

var q_Main$$main = (function(){
   return q_Prelude$Interactive$$putStr_39_(null, (q_Prelude$Show$$primNumShow(null, u_prim____toStrInt, 0, x_Main_46_testProg_58_loop_58_0(0, 0)) + "\n"))
})()

var _runMain$0 = (function(){
   return js_idris_force(q_Main$$main(null))
})()

@be5invis
Copy link

be5invis commented Apr 25, 2017

Continue: Specialize constructor, test and projector of Bool and Maybe.

function x_Main_46_testProg_58_lmt_58_0(_e$0){
   return q_Prelude$Interfaces$$Prelude_46_Interfaces_46__64_Prelude_46_Interfaces_46_Ord_36_Int_58__33_min_58_0((_e$0 + 100000000), 1000000000)
}

function x_Main_46_testProg_58_loop_58_0(_e$0, _e$1){
   while(true){
      var cgIdris_2 = q_Prelude$Interfaces$$Prelude_46_Interfaces_46__64_Prelude_46_Interfaces_46_Ord_36_Int_58__33__62__61__58_0(_e$1, x_Main_46_testProg_58_lmt_58_0(_e$0));
      if(( !cgIdris_2)) {
         var cgIdris_3 = _e$0;
         var cgIdris_4 = (_e$1 + 1);
         _e$0 = cgIdris_3;
         _e$1 = cgIdris_4;
         continue
      } else {
         return _e$1
      }
      ;
      break
   }
}

var q_Main$$main = (function(){
   return q_Prelude$Interactive$$putStr_39_(null, 
     (q_Prelude$Show$$primNumShow(null, u_prim____toStrInt, 0, 
         x_Main_46_testProg_58_loop_58_0(0, 0)) + "\n"))
})()

var _runMain$0 = (function(){
   return js_idris_force(q_Main$$main(null))
})()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment