Nov 13, 2018

A Change of Undefined Behavior in BuckleScript 4.0.7

Hongbo Zhang
Compiler & Build System

Important: This is an archived blog post, kept for historical reasons. Please note that this information might be outdated.

What is this about?

In the latest BuckleScript release, we introduced a minor change in the codegen which broken some user libraries. Note this change only broke the code in the FFI boundary(the interop between JS).

In the early days of BuckleScript, there is no built-in uncurried calling convention support, since OCaml is a curried language, which means every function has arity one, so there is no way to express that a function has arity zero, this makes some interop challenging. In the mocha unit test library, it expects its callback to be function of arity zero.

To work around this issue, before this release, we did a small codegen optimization, for a function of type unit -> unit, if its argument is not used, we remove its argument in the output.

RE
let f: unit => int = () => 3; let f_used: unit => unit = x => Js.log(x);

Output JS prior to v4.0.7:

JS
function f (){ return 3 } function f_used (x){ console.log(x) }

To make this hack work, in the application side, for a curried function application, we treat the function of arity 0 and arity 1 in the same way, this still works since curried function application could only happen on the ocaml function.

This trick is unintuitive, it makes code generated less predictable and it is not relevant any more, since we added native uncurried calling convention support later.

Therefore, we generate JS code in a more consistent style in this release:

RE
let f: unit => int = () => 3;
JS
function f (param){ return 3 }

So in your FFI code, if you have a callback which is expected to be of arity zero, use unit -> unit [@bs] or unit -> unit [@bs.uncurry], it is 100% correct. Note our previous trick will only make unit -> unit work most time, but it can not provide any guarantee.

Since we removed the trick, the curried runtime does not treat function of arity 0 and arity 1 in the same way, so if you have code like this

RE
let f: unit => int = [%bs.raw {|function () { return 3 }|}];

It is not correct any more, the fix would be:

RE
let f: unit => int = [%bs.raw {|function(param) { return 3 }|}];

Or

RE
let f: (. unit) => int = [%bs.raw {|function() { return 3 }|}];

FFI is a double edge sword, it is a must have to ship your product, yet it is tricky, and there may be some undefined behavior you rely on but don't recognize, it is encouarged to always test your FFI in the boundary.

Want to read more?
Back to Overview