BuckleScript is a backend for the OCaml compiler which emits JavaScript. It works with both vanilla OCaml and Reason, the whole compiler is compiled into JS (and ASM) so that you can play with it in the browser.
Note
|
Documentation under bucklescript.github.io match the master branch. If you find errors or omissions in this document, please don’t hesitate to submit an issue, sources are here. |
Why BuckleScript
Benefits of JavaScript platform
JavaScript is not just the browser language, it’s also the only existing cross platform language. It is truly everywhere: users don’t need to install binaries or use package managers to access software, just a link will work.
Another important factor is that the JavaScript VM is quite fast and keeps getting faster. The JavaScript platform is therefore increasingly capable of supporting large applications.
Problems of JavaScript && how BuckleScript solves them
BuckleScript is mainly designed to solve the problems of large scale JavaScript programming:
- Type-safety
-
OCaml offers an industrial-strength state-of-the-art type system and provides very strong type inference (i.e. No verbose type annotation required compared with TypeScript), which proves invaluable in managing large projects. OCaml’s type system is not just for tooling, it is a sound type system which means it is guaranteed that there will be no runtime type errors after type checking.
- High quality dead code elimination
-
A large amount of web-development relies on inclusion of code dependencies by copying or referencing CDNs (the very thing that makes JavaScript highly accessible), but this also introduces a lot of dead code. This impacts performance adversely when the JavaScript VM has to interpret code that will never be invoked. BuckleScript provides powerful dead-code elimination at all levels:
-
Function and module level elimination is facilitated by the sophistication of the type-system of OCaml and purity analysis.
-
At the global level BuckleScript generates code ready for dead-code elimination done by bundling tools such as the Google closure-compiler.
-
- Offline optimizations
-
JavaScript is a dynamic language, it takes a performance-hit for the VM to optimize code at runtime. While some JS engines circumvent the problem to some extent by caching, this is not available to all environments, and lack of a strong type system also limits the level of optimizations possible. Again, BuckleScript, using features of the OCaml type-system and compiler implementation is able to provide many optimizations during offline compilation, allowing the runtime code to be extremely fast.
- JS platform and Native platform
-
Run your programs on all platforms, but run your system faster under specific platforms. JavaScript is everywhere but it does not mean we have to run all apps in JS, under several platforms, for example, server side or iOS/Android native apps, when programs are written in OCaml, it can also be compiled to native code for better and reliable performance.
While a strong type-system helps in countering these problems, at the same time we hope to avoid some of the problems faced in using other offline transpilation systems:
- Slow compilation
-
OCaml byte-code compilation is known to be fast (one or two orders of magnitude faster than other similar languages: Scala or Haskell), BuckleScript shares the same property and compiles even faster since it saves the link time. See the speeds at work in the playground, the native backend is one order faster than the JS backend.
- Un-readable JS Code and hard to integrate with existing JS libraries
-
When compiling to JavaScript, many systems generate code, that while syntactically and semantically correct is not human-readable and very difficult to debug and profile. Our BuckleScript implementation and the multi-pass compilation strategy of OCaml, allows us to avoid name-mangling, and produce JavaScript code that is human-readable and easier to debug and maintain. More importantly, this makes integration with existing JS libraries much easier.
- Large JS output even for a simple program
-
In BuckleScript, a
Hello world
program generates 20 bytes JS code instead of 50K bytes. This is due to BuckleScript having an excellent integration with JS libs in that unlike most JS compilers, all BuckleScript’s runtime is written in OCaml itself so that these runtime libraries are only needed when user actually calls it. - Loss of code-structure
-
Many systems generate JavaScript code that is essentially a big ball of mud. We try to keep the original structure of the code by mapping one OCaml module to one JS module.
Installation
From npm (recommended for normal users)
npm install -g bs-platform
It supports all platform including Windows
Build from source in Release Mode
Note for dev mode, you are encouraged to work from there
-
Standard C toolchain(gcc,Makefile)
git clone https://github.com/bucklescript/bucklescript
The patched compiler is installed locally into your $(pwd)/bin
directory. To start using it temporarily, check if ocamlc.opt
and
ocamlopt.opt
exist in $(pwd)/bin
, and temporarily add the location
to your $(PATH)
(e.g. PATH=$(pwd)/bin:$PATH
).
cd bucklescript/vendor/ocaml
./configure -prefix `pwd`
make world && make install
The following directions assume you already have the correct version of
ocamlopt.opt
in your $PATH
, having followed the process described in
the previous section.
Change cwd
to project root directory
export BS_RELEASE_BUILD=1
make world && install
At the end, you should have a binary called bsc.exe
under lib/
directory, which you can add to your $PATH
.
Making use of OPAM
When working with OCaml we also recommend using opam package manager to install OCaml native toolchains, available here. You will benefit from the existing OCaml ecosystem.
Once you have opam
installed, ask opam
to switch to using our
version of the compiler:
opam update
opam switch 4.02.3+buckle-master
eval `opam config env`
Note that using this approach, the user can also install other OCaml tools easily.
Get Started
Using existing templates (@since 1.7.4 )
BuckleScript provide some project templates, to help people get started on board as fast as possible.
npm install -g bs-platform && bsb -init hello && cd hello && npm run build
First, it installs bs-platform
globally, then we use the command line tool bsb
to set up a sample project named hello
, then we run the build.
The output project layout is similar as below
hello>tree -d .
.
|---.vscode
├── lib
│ ├── bs
│ │ └── src
│ └── js
│ └── src
├── node_modules
│ └── bs-platform -> ...
└── src
To enter watch mode, you can run npm run watch
,
then you can edit src/demo.ml
and see it compiled on the fly, and changes are updated on lib/js/src/demo.js
If you happen to use VSCode as editor, we provide .vscode/tasks.json
as well, type Tasks>build
, it will enter watch mode, the great thing is that VSCode comes with
a Problems
panel which has a much nicer UI.
Currently there are not too many project templates, you can contribute more project templates here here,
First example by hand without samples
-
Create a directory called
hello
and createbsconfig.json
andpackage.json
as below:bsconfig.json{ "name" : "hello", "sources" : { "dir" : "src"} }
package.json{ "dependencies": { "bs-platform": "1.7.0" (1) }, "scripts" : { "build" : "bsb", "watch" : "bsb -w" (2) } }
-
Version should be updated accordingly
-
Watch mode when developing
-
-
Create
src/main_entry.ml
as below:src/main_entry.mllet () = print_endline "hello world"
-
Build the app
npm run build
Now you should see a file called lib/js/src/main_entry.js
generated as below:
// GENERATED CODE BY BUCKLESCRIPT VERSION 1.0.1 , PLEASE EDIT WITH CARE
'use strict';
console.log("hello world");
/* Not a pure module */ (1)
-
The compiler detects this module is impure due to the side effect.
User can also run watch
mode to see changes instantly while editing.
npm run watch
Tip
|
The working code is available here: |
An example with multiple modules
Now we want to create two modules, one file called fib.ml
which
exports fib
function, the other module called main_entry.ml
which
will call fib
.
-
Create a directory
fib
and create filesbsconfig.json
andpackage.json
bsconfig.json{ "name" : "helo", "sources" : { "dir" : "src"} }
package.json{ "dependencies": { "bs-platform": "1.7.0" }, "scripts" : { "build" : "bsb", "watch" : "bsb -w" } }
-
Create file
src/fib.ml
and filesrc/main_entry.ml
src/fib.mllet fib n = let rec aux n a b = if n = 0 then a else aux (n - 1) b (a+b) in aux n 1 1
src/main_entry.mllet () = for i = 0 to 10 do Js.log (Fib.fib i) (1) done
-
Js
module is a built-in module shipped with BuckleScript
-
-
Build the app
npm install npm run build node lib/js/src/main_entry.js
If everything goes well, you should see the output as below:
1
1
2
3
5
8
13
21
34
55
89
Built in NPM support
Build an OCaml library as a npm package
BuckleScript build system has built in support for NPM packages, please checkout the section about the bsb for NPM support.
Note
|
This section covers some basics of how NPM is supported internally, normal users are safe to skip this section. |
BuckleScript extends the OCaml compiler options with several flags to provide a better experience for NPM users.
In general, you are expected to see two kinds of build artifacts, the generated JS files and metadata which your OCaml dependencies rely on.
Since CommonJS has no namespaces, to allow JS files to live in different directories, we have a flag
bsc.exe -bs-package-name $npm_package_name -bs-package-output modulesystem:path/to/your/js/dir -c a.ml
By passing this flag, bsc.exe
will store your package_name
and
relative path to package.json
in .cmj
files. It will also generate
JS files in the directory you specified. You can, and are encouraged
to, store JavaScript files in a hierarchical directory.
For the binary artifacts (Note that this is not necessary if you only
want your libraries to be consumed by JS developers, and it has
benefit since end users don’t need these binary data any more), the
convention is to store all *.cm
data in a single directory
package.json/lib/ocaml
and Javascript files in a hierachical directory like package.json/lib/js
To use OCaml library as a npm package
If you follow the layout convention above, using an OCaml package is pretty straightforward:
bsc.exe -I path/to/ocaml/package/installed -c a.ml
Together
Your command line would be like this:
bsc.exe -I path/to/ocaml/package1/installed -I path/to/ocaml/package2/installed -bs-package-name $npm_package_name -bs-package-output commonjs:path/to/lib/js/ -c a.ml
Examples
Please visit https://github.com/bucklescript/bucklescript-addons for more examples.
JS Calling OCaml
Since BuckleScript guarantees that all OCaml functions are exported as is, no extra work is required to expose OCaml functions to JavaScript.
Caution
|
|
If users want to consume some OCaml features only available in OCaml but not in JS, we recommend users to export it as functions.
For example, data constructors are not available in JS
type t =
| Cons of int * t
| Nil
Currently, we recommend the user expose the constructor as a function so that it can be constructed from the JS side.
let cons x y = Cons (x,y)
let nil = Nil
Note
|
In the future, we will derive these functions to automate this process. |
BuckleScript annotations for Unicode and JS FFI support
To make OCaml work smoothly with JavaScript, we introduced several extensions to the OCaml language. These BuckleScript extensions facilitate the integration of native JavaScript code and improve the generated code.
Unicode support (@since 1.5.1)
Warning
|
In this section, we assume the source is encoded using UTF8. |
In OCaml, string is an immutable byte sequence (like GoLang), so if the user types some Unicode
Js.log "你好"
It will be translated into
console.log("\xe4\xbd\xa0\xe5\xa5\xbd");
Luckily, OCaml allows customized multiple-line string support, BuckleScript reserves the delimiter js
and j
.
Js.log {js|你好,
世界|js}
console.log("你好,\n世界");
Inside the js
delimiter, the escape convention is like JavaScript
Js.log {js|\x3f\u003f\b\t\n\v\f\r\0"'|js}
console.log("\x3f\u003f\b\t\n\v\f\r\0\"\'");
Unicode support with string interpolation (@since 1.7.0)
Like {js||js}
, {j||j}
not only allow unicode point, but also variable interpolation.
For example
let world = {j|世界|j}
let hello_world = {j|你好,$world|j}
Users can parenthesize the interpreted variable as below:
let hello_world = {j|你好,$(world)|j}
Note the syntax of interpolated variable is intentionally designed to be simple. Its lexical convention is as below:
identifier := leading_identifier_char identifier_chars
leading_identifier_char := 'a' .. 'z' | '_'
identifier_chars := 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '_' | '\'
FFI
Like TypeScript, when building type-safe bindings from JS to OCaml,
users have to write type declarations.
In OCaml, unlike TypeScript, users do not need to create a separate
.d.ts
file,
since the type declarations are an integral part of OCaml.
The FFI is divided into several components:
-
Binding to simple functions and values
-
Binding to higher-order functions
-
Binding to object literals
-
Binding to classes
-
Extensions to the language for debugger, regex, and embedding arbitrary JS code
Binding to simple JS functions values
This part is similar to traditional FFI, with syntax as described below:
external value-name : typexpr = external-declaration attributes
external-declaration := string-literal
Users need to declare types for foreign functions
(JS functions for BuckleScript or C functions for native compiler)
and provide customized attributes
.
Binding to global value: bs.val
external imul : int -> int -> int = "Math.imul" [@@bs.val]
type dom
(* Abstract type for the DOM *)
external dom : dom = "document" [@@bs.val]
bs.val
attribute is used to bind to a JavaScript value,
it can be a function or plain value.
Note
|
|
Scoped values: bs.scope
(@since 1.7.2)
In JS library, it is quite common to use a name as namespace,
for example, if the user want to write a binding to
vscode.commands.executeCommand
, assume vscode
is a module name,
the user needs to type commands
properly before typing executeCommand
, and in practice, it is rarely useful to call vscode.commands
alone, for this reason, we introduce a convient sugar: bs.scope
type param
external executeCommands : string -> param array -> unit = ""
[@@bs.scope "commands"] [@@bs.module "vscode"][@@bs.splice]
let f a b c =
executeCommands "hi" [|a;b;c|]
var Vscode = require("vscode");
function f(a, b, c) {
Vscode.commands.executeCommands("hi", a, b, c);
return /* () */0;
}
NOTE bs.scope
can also be chained as below:
external makeBuffer : int -> buffer = "Buffer"
[@@bs.new] [@@bs.scope "global"]
external hi : string = ""
[@@bs.module "z"] [@@bs.scope "a0", "a1", "a2"]
external ho : string = ""
[@@bs.val] [@@bs.scope "a0","a1","a2"]
external imul : int -> int -> int = ""
[@@bs.val] [@@bs.scope "Math"]
let f2 () =
makeBuffer 20 , hi , ho, imul 1 2
var Z = require("z");
function f2() {
return /* tuple */[
new (global.Buffer)(20),
Z.a0.a1.a2.hi,
a0.a1.a2.ho,
Math.imul(1, 2)
];
}
Binding to JavaScript constructor: bs.new
bs.new
is used to create a JavaScript object.
type t
external create_date : unit -> t = "Date" [@@bs.new]
let date = create_date ()
var date = new Date();
Binding to a value from a module: bs.module
external add : int -> int -> int = "add" [@@bs.module "x"]
external add2 : int -> int -> int = "add2"[@@bs.module "y", "U"] (1)
let f = add 3 4
let g = add2 3 4
-
"U"
will hint the compiler to generate a better name for the module, see output
var U = require("y");
var X = require("x");
var f = X.add(3, 4);
var g = U.add2(3, 4);
Note
|
|
Binding the whole module as a value or function
type http
external http : http = "http" [@@bs.module] (1)
-
external-declaration
is the module name
Note
|
|
Binding to method: bs.send
, bs.send.pipe
bs.send
helps the user send a message to a JS object.
type id (** Abstract type for id object *)
external get_by_id : dom -> string -> id =
"getElementById" [@@bs.send]
The object is always the first argument and actual arguments follow.
get_by_id dom "xx"
document.getElementById("xx");
bs.send.pipe
is similar to bs.send
except that the first argument, i.e, the object,
is put in the position of last argument to help user write in a chaining style:
external map : ('a -> 'b [@bs]) -> 'b array =
"" [@@bs.send.pipe: 'a array] (1)
external forEach: ('a -> unit [@bs]) -> 'a array =
"" [@@bs.send.pipe: 'a array]
let test arr =
arr
|> map (fun [@bs] x -> x + 1)
|> forEach (fun [@bs] x -> Js.log x)
-
For the
[@bs]
attribute in the callback, see Binding to callbacks (higher-order function)
Note
|
|
Binding to dynamic key access/set: bs.set_index
, bs.get_index
This attribute allows dynamic access to a JavaScript property
type t
external create : int -> t = "Int32Array" [@@bs.new]
external get : t -> int -> int = "" [@@bs.get_index]
external set : t -> int -> int -> unit = "" [@@bs.set_index]
let _ =
let i32arr = (create 3) in
set i32arr 0 42;
Js.log (get i32arr 0)
var i32arr = new Int32Array(3);
i32arr[0] = 42;
console.log(i32arr[0]);
Splice calling convention: bs.splice
In JS, it is quite common to have a function take variadic arguments. BuckleScript supports typing homogeneous variadic arguments. For example,
external join : string array -> string = "" [@@bs.module "path"] [@@bs.splice]
let v = join [| "a"; "b"|]
var Path = require("path");
var v = Path.join("a","b");
Note
|
For the external call, if the |
Special types on external declarations: bs.string
, bs.int
, bs.ignore
, bs.as
, bs.unwrap
Using polymorphic variant to model enums and string types
There are several patterns heavily used in existing JavaScript codebases, for example, the string type is used a lot. BuckleScript FFI allows the user to model string type in a safe way by using annotated polymorphic variant.
external readFileSync :
name:string ->
([ `utf8
| `my_name [@bs.as "ascii"] (1)
] [@bs.string]) ->
string = ""
[@@bs.module "fs"]
let _ =
readFileSync ~name:"xx.txt" `my_name
-
Here we intentionally made an example to show how to customize a name
var Fs = require("fs");
Fs.readFileSync("xx.txt", "ascii");
Polymorphic variants can also be used to model enums.
external test_int_type :
([ `on_closed
| `on_open [@bs.as 3] (2)
| `in_bin (3)
]
[@bs.int]) -> int =
"" [@@bs.val]
let _ =
test_int_type `in_bin
-
`on_closed will be encoded as 0
-
`on_open will be 3 due to the attribute
bs.as
-
`in_bin will be 4
test_int_type(4);
Using polymorphic variant to model event listener
BuckleScript models this in a type-safe way by using annotated polymorphic variants.
type readline
external on :
(
[ `close of unit -> unit
| `line of string -> unit
] (1)
[@bs.string])
-> readline = "" [@@bs.send.pipe: readline]
let register rl =
rl
|> on (`close (fun event -> () ))
|> on (`line (fun line -> print_endline line))
-
This is a very powerful typing: each event can have its own different types.
function register(rl) {
return rl.on("close", function () {
return /* () */0;
})
.on("line", function (line) {
console.log(line);
return /* () */0;
});
}
Using polymorphic variant to model arguments of multiple possible types (@since 1.8.3)
Sometimes a JavaScript function will accept an argument that could have different types depending upon how it’s used.
function padLeft(string, padding) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
You can model such a function in BuckleScript using [@bs.unwrap]
.
external padLeft :
string
-> ([ `String of string
| `Int of int
] [@bs.unwrap])
-> string
= "" [@@bs.val]
Polymorphic variants with [@bs.unwrap]
will "unwrap" the variant at the call site so that the JavaScript function is called with the underlying value.
let _ = padLeft "Hello World" (`Int 4)
let _ = padLeft "Hello World" (`String "bs: ")
Output:
padLeft("Hello World", 4);
padLeft("Hello World", "bs: ");
Warning
|
|
Phantom Arguments and ad-hoc polymorphism
bs.ignore
allows arguments to be erased after passing to JS functional call, the side effect will
still be recorded.
For example,
external add : (int [@bs.ignore]) -> int -> int -> int = ""
[@@bs.val]
let v = add 0 1 2 (1)
-
the first argument will be erased
var v = add (1,2);
This is very useful to combine GADT:
type _ kind =
| Float : float kind
| String : string kind
external add : ('a kind [@bs.ignore]) -> 'a -> 'a -> 'a = "" [@@bs.val]
let () =
Js.log (add Float 3.0 2.0);
Js.log (add String "x" "y");
console.log(add(3.0, 2.0));
console.log(add("x", "y"));
User can also have a payload for the GADT:
let string_of_kind (type t) (kind : t kind) =
match kind with
| Float -> "float"
| String -> "string"
external add_dyn : ('a kind [@bs.ignore]) -> string -> 'a -> 'a -> 'a = ""
[@@bs.val]
let add2 k x y =
add_dyn k (string_of_kind k) x y
Note
|
Using a GADT as a |
Fixed Arguments
Contrary to the Phantom arguments, _ [@bs.as]
is introduced to attach constant
data.
For example:
external process_on_exit : (_ [@bs.as "exit"]) -> (int -> unit) -> unit =
"process.on" [@@bs.val]
let () =
process_on_exit (fun exit_code ->
Js.log( "error code: " ^ string_of_int exit_code ))
process.on("exit", function (exit_code) {
console.log("error code: " + exit_code);
return /* () */0;
});
It can also be used in combination with other attributes, for example:
type process
external on_exit : (_ [@bs.as "exit"]) -> (int -> unit) -> process =
"on" [@@bs.send.pipe: process]
let register (p : process) =
p |> on_exit (fun i -> Js.log i)
function register(p) {
return p.on("exit", (function (i) {
console.log(i);
return /* () */0;
}));
}
external io_config :
stdio:(_ [@bs.as "inherit"]) -> cwd:string -> unit -> _ = "" [@@bs.obj]
let config = io_config ~cwd:"." ()
var config = {
stdio: "inherit",
cwd: "."
};
Fixed Arguments with arbitrary JSON literal (@since 1.7.0)
So the payload can be more flexiblie with JSON literal support
type t
external x: t = "" [@@bs.val]
external on_exit_slice5 :
int
-> (_ [@bs.as 3])
-> (_ [@bs.as {json|true|json}])
-> (_ [@bs.as {json|false|json}])
-> (_ [@bs.as {json|"你好"|json}])
-> (_ [@bs.as {json| ["你好",1,2,3] |json}])
-> (_ [@bs.as {json| [{ "arr" : ["你好",1,2,3], "encoding" : "utf8"}] |json}])
-> (_ [@bs.as {json| [{ "arr" : ["你好",1,2,3], "encoding" : "utf8"}] |json}])
-> (_ [@bs.as "xxx"])
-> ([`a|`b|`c] [@bs.int])
-> (_ [@bs.as "yyy"])
-> ([`a|`b|`c] [@bs.string])
-> int array
-> unit
=
"xx" [@@bs.send.pipe: t] [@@bs.splice]
let _ = x |> on_exit_slice5 __LINE__ `a `b [|1;2;3;4;5|]
x.xx(114, 3, true, false, ("你好"), ( ["你好",1,2,3] ), ( [{ "arr" : ["你好",1,2,3], "encoding" : "utf8"}] ), ( [{ "arr" : ["你好",1,2,3], "encoding" : "utf8"}] ), "xxx", 0, "yyy", "b", 1, 2, 3, 4, 5)
Binding to NodeJS special variables: bs.node
NodeJS has several file local variables: dirname
, filename
, _module
, and require
.
Their semantics are more like macros instead of functions.
BuckleScript provides built-in macro support for these variables:
let dirname : string option = [%bs.node __dirname]
let filename : string option = [%bs.node __filename]
let _module : Node.node_module option = [%bs.node _module]
let require : Node.node_require option = [%bs.node require]
Binding to callbacks (higher-order function)
Higher order functions are functions where the callback can be another function. For example, suppose JS has a map function as below:
function map (a, b, f){
var i = Math.min(a.length, b.length);
var c = new Array(i);
for(var j = 0; j < i; ++j){
c[j] = f(a[i],b[i])
}
return c ;
}
A naive external type declaration would be as below:
external map : 'a array -> 'b array -> ('a -> 'b -> 'c) -> 'c array = "" [@@bs.val]
Unfortunately, this is not completely correct. The issue is by
reading the type 'a → 'b → 'c
, it can be in several cases:
let f x y = x + y
let g x = let z = x + 1 in fun y -> x + z
In OCaml they all have the same type; however,
f
and g
may be compiled into functions with
different arities.
A naive compilation will compile f
as below:
let f = fun x -> fun y -> x + y
function f(x){
return function (y){
return x + y;
}
}
function g(x){
var z = x + 1 ;
return function (y){
return x + z ;
}
}
Its arity will be consistent but is 1 (returning another function); however, we expect its arity to be 2.
Bucklescript uses a more complex compilation strategy, compiling f
as
function f(x,y){
return x + y ;
}
No matter which strategy we use, existing typing rules cannot
guarantee a function of type 'a → 'b → 'c
will have arity 2.
[@bs]
for explicit uncurried callback
To solve this problem introduced by OCaml’s curried calling convention,
we support a special attribute [@bs]
at the type level.
external map : 'a array -> 'b array -> ('a -> 'b -> 'c [@bs]) -> 'c array
= "map" [@@bs.val]
Here ('a → 'b → 'c [@bs])
will always be of arity 2, in
general,
'a0 → 'a1 … 'aN → 'b0 [@bs]
is the same as
'a0 → 'a1 … 'aN → 'b0
except the former’s arity is guaranteed to be N
while the latter is
unknown.
To produce a function of type 'a0 → .. 'aN → 'b0 [@bs]
, as follows:
let f : 'a0 -> 'a1 -> .. 'b0 [@bs] =
fun [@bs] a0 a1 .. aN -> b0
let b : 'b0 = f a0 a1 a2 .. aN [@bs]
A special case for arity of 0:
let f : unit -> 'b0 [@bs] = fun [@bs] () -> b0
let b : 'b0 = f () [@bs]
Note that this extension to the OCaml language is sound. If you add an attribute in one place but miss it in other place, the type checker will complain.
Another more complex example:
type 'a return = int -> 'a [@bs]
type 'a u0 = int -> string -> 'a return [@bs] (1)
type 'a u1 = int -> string -> int -> 'a [@bs] (2)
type 'a u2 = int -> string -> (int -> 'a [@bs]) [@bs] (3)
-
u0
has arity of 2, return a function with arity 1 -
u1
has arity of 3 -
u2
has arity of 2, return a function with arity 1
[@bs.uncurry]
for implicit uncurried callback (@since 1.5.0)
Note the [@bs]
annotation already solved the problem completely, but it has a drawback
that it requires users to write [@bs]
both in definition site and call site.
For example:
external map : 'a array -> ('a -> 'b[@bs]) -> 'b array = "" [@@bs.send] (1)
map [|1;2;3|] (fun [@bs] x -> x + 1) (2)
-
[@bs]
annotation in definition site -
[@bs]
annotation in call site
This is less convenient for end users, so we introduce another implicit annotation [@bs.uncurry]
so that the compiler will automatically wrap the curried callback (from OCaml side) to JS uncurried callback. In this way, the [@bs.uncurry]
annotation is defined
only once.
external map : 'a array -> ('a -> 'b [@bs.uncurry]) -> 'b array = "" [@@bs.send] (1)
map [|1;2;3|] (fun x -> x+ 1) (2)
-
[@bs.uncurry]
annotation in definition site -
Idiomatic OCaml code
Note
|
In general, This means |
Uncurried calling convention as an optimization
As we discussed before, we can compile any OCaml function as arity 1 to support OCaml’s curried calling convention.
This model is simple and easy to implement, but the native compilation is very slow and expensive for all functions.
let f x y z = x + y + z
let a = f 1 2 3
let b = f 1 2
can be compiled as
function f(x){
return function (y){
return function (z){
return x + y + z
}
}
}
var a = f (1) (2) (3)
var b = f (1) (2)
But as you can see, this is highly inefficient, since the compiler
already saw the source definition of f
, it can be optimized as below:
function f(x,y,z) {return x + y + z}
var a = f(1,2,3)
var b = function(z){return f(1,2,z)}
BuckleScript does this optimization in the cross module level and tries to infer the arity as much as it can.
Callback optimization
However, such optimization will not work with higher-order functions, i.e, callbacks.
For example,
let app f x = f x
Since the arity of f
is unknown, the compiler can not do any optimization
(unless app
gets inlined), so we
have to generate code as below:
function app(f,x){
return Curry._1(f,x);
}
Curry._1
is a function to dynamically support the curried calling
convention.
Since we support the uncurried calling convention, you can write app
as below
let app f x = f x [@bs]
Now the type system will infer app
as type
('a →'b [@bs]) → 'a
and compile app
as
function app(f,x){
return f(x)
}
Note
|
In OCaml the compiler internally uncurries every function
declared as |
Bindings to this
based callbacks: bs.this
Many JS libraries have callbacks which rely on this
(the source), for
example:
x.onload = function(v){
console.log(this.response + v )
}
Here, this
would be the same as x
(actually depends on how onload
is called). It is clear that
it is not correct to declare x.onload
of type unit → unit [@bs]
.
Instead, we introduced a special attribute
bs.this
allowing us to type x
as below:
type x
external x: x = "" [@@bs.val]
external set_onload : x -> (x -> int -> unit [@bs.this]) -> unit = "onload" [@@bs.set]
external resp : x -> int = "response" [@@bs.get]
let _ =
set_onload x begin fun [@bs.this] o v ->
Js.log(resp o + v )
end
x.onload = (function (v) {
var o = this ; (1)
console.log(o.response + v | 0);
return /* () */0;
});
-
The first argument is automatically bound to
this
bs.this
is the same as bs
: except that its first parameter is
reserved for this
and for arity of 0, there is no need for a redundant unit
type:
let f : 'obj -> 'b [@bs.this] =
fun [@bs.this] obj -> ....
let f1 : 'obj -> 'a0 -> 'b [@bs.this] =
fun [@bs.this] obj a -> ...
Note
|
There is no way to consume a function of type
This was introduced mainly to be consumed by existing JS libraries.
User can also type |
Binding to JS objects
All JS objects of type 'a
are lifted to type 'a Js.t
to avoid
conflict with OCaml’s native object system (we support both OCaml’s
native object system and FFI to JS’s objects), ##
is used in JS’s
object method dispatch and field access, while #
is used in OCaml’s
object method dispatch.
OCaml supports object oriented style natively and provides structural type system.
OCaml’s object system has different runtime semantics from JS object, but they
share the same type system, all JS objects of type 'a
are typed as 'a Js.t
OCaml provides two kinds of syntaxes to model structural typing: < p1 : t1 >
style and
class type
style. They are mostly the same except that the latter is more feature rich
(supporting inheritance) but more verbose.
Simple object type
Suppose we have a JS file demo.js
which exports two properties: height
and width
:
exports.height = 3
exports.width = 3
There are different ways to writing binding to module demo
,
here we use OCaml objects to model module demo
external demo : < height : int ; width : int > Js.t = "" [@@bs.module]
There are two kinds of types on the method name:
-
normal type
< label : int > < label : int -> int > < label : int -> int [@bs]> < label : int -> int [@bs.this]>
-
method
< label : int -> int [@bs.meth] >
The difference is that for method
, the type system will force users to fulfill
its arguments all at the same time, since its semantics depends on this
in JavaScript.
For example:
let test f =
f##hi 1 (1)
let test2 f =
let u = f##hi in
u 1
let test3 f =
let u = f##hi in
u 1 [@bs]
-
##
is JS object property/method dispatch
The compiler would infer types differently
val test : < hi : int -> 'a [@bs.meth]; .. > -> 'a (1)
val test2 : < hi : int -> 'a ; .. > -> 'a
val test3 : < hi : int -> 'a [@bs]; .. >
-
..
is a row variable, which means the object can contain more methods.
Complex object type
Below is an example:
class type _rect = object
method height : int [@@bs.set]
method width : int [@@bs.set]
method draw : unit -> unit
end [@bs] (1)
type rect = _rect Js.t
-
class type
annotated with[@bs]
is treated as a JS class type, it needs to be lifted toJs.t
too.
For JS classes, methods with arrow types are treated as real methods
(automatically annotated with [@bs.meth]
)
while methods with non-arrow types
are treated as properties. Adding [@@bs.set]
to those methods will make them
mutable, which enables you to set them using #=
later. Dropping the [@@bs.set]
attribute makes the method/property immutable.
So the type rect
is the same as below:
type rect = < height : int ; width : int ; draw : unit -> unit [@bs.meth] > Js.t
How to consume JS property and methods
As we said: ##
is used in both object method dispatch and field access.
f##property (1)
f##property #= v
f##js_method args0 args1 args2 (2)
-
property get should not come with any argument as we discussed above, which will be checked by the compiler.
-
Here
method
is of arity 3.
Note
|
All JS method application is uncurried, JS’s method is not a function, this invariant can be guaranteed by OCaml’s type checker, a classic example shown below:
|
In BuckleScript
let fn = f0##f in
let a = fn 1 2
(* f##field a b would think `field` as a method *)
is different from
let b = f1##f 1 2
The compiler will infer as below:
val f0 : < f : int -> int -> int > Js.t
val f1 : < f : int -> int -> int [@bs.meth] > Js.t
If we type console
properly in OCaml, user could only write
console##log "fine"
let u = console##log
let () = u "fine" (1)
-
OCaml compiler will complain
Note
|
If a user were to make such a mistake, the type checker would
complain by saying it expected |
getter/setter annotation to JS properties (simplified @since 1.9.2)
Since OCaml’s object system does not have getters/setters, we introduced two
attributes bs.get
and bs.set
to help inform BuckleScript to compile
them as property getters/setters.
type y = <
height : int [@bs.set no_get] (1)
> Js.t
type y0 = <
height : int [@bs.set] [@bs.get null] (2)
> Js.t
type y1 = <
height : int [@bs.set] [@bs.get undefined] (3)
> Js.t
type y2 = <
height : int [@bs.set] [@bs.get nullable ] (4)
> Js.t
type y3 = <
height : int [@bs.get nullable] (5)
> Js.t
-
height
is setter only -
getter return
int Js.null
-
getter return
int Js.undefined
-
getter return
int Js.nullable
-
getter only, return
int Js.nullable
Note
|
Getter/Setter also applies to class type label |
Create JS objects using bs.obj
Not only can we create bindings to JS objects, but also we can create JS objects in a type safe way on the OCaml side:
let u = [%bs.obj { x = { y = { z = 3}}} ] (1)
-
bs.obj
extension is used to mark{}
as JS objects
var u = { x : { y : { z : 3 }}}}
The compiler would infer u
as type:
val u : < x : < y : < z : int > Js.t > Js.t > Js.t
To make it more symmetric, extension bs.obj
can also be applied
into the type level, so you can write:
val u : [%bs.obj: < x : < y : < z : int > > > ]
Users can also write expression and types together as below:
let u = [%bs.obj ( { x = { y = { z = 3 }}} : < x : < y : < z : int > > > ]
Objects in a collection also works:
let xs = [%bs.obj [| { x = 3 } ; { x = 3 } |] : < x : int > array ]
let ys = [%bs.obj [| { x = 3 } ; { x = 4 } |] ]
var xs = [ { x : 3 } , { x : 3 } ]
var ys = [ { x : 3 } , { x : 4 } ]
Create JS objects using external
bs.obj
can also be used as an attribute in external declarations, as below:
external make_config : hi:int -> lo:int -> unit -> t = "" [@@bs.obj]
let v = make_config ~hi:2 ~lo:3
var v = { hi : 2 , lo : 3 }
Option argument is also supported:
external make_config : hi:int -> ?lo:int -> unit -> t = "" [@@bs.obj] (1)
let u = make_config ~hi:3 ()
let v = make_config ~lo:2 ~hi:3 ()
-
In OCaml, the order of label does not matter, and the evaluation order of arguments is undefined. Since the order does not matter, to make sure the compiler realize all the arguments are fulfilled (including optional arguments), it is common to have a
unit
type before the result.
var u = {hi : 3}
var v = {hi : 3 , lo: 2}
Now, we can write JS style code in OCaml too (in a type safe way):
let u = [%bs.obj {
x = { y = { z = 3 } };
fn = fun [@bs] u v -> u + v (1)
} ]
let h = u##x##y##z
let a = u##fn
let b = a 1 2 [@bs]
-
fn
property is not method, it does not rely onthis
. We will show how to create JS method in OCaml later.
var u = { x : { y : { z : 3 } }, fn : function (u, v) {return u + v}}
var h = u.x.y.z
var a = u.fn
var b = a(1,2)
Note
|
When the field is an uncurried function, a short-hand syntax
The compiler will infer the type of
|
Create JS objects with this
semantics
The objects created above can not use this
in the method, this is supported in
BuckleScript too.
let v2 =
let x = 3. in
object (self) (1)
method hi x y = self##say x +. y
method say x = x *. self##x ()
method x () = x
end [@bs] (2)
-
self
is bound tothis
in generated JS code -
[@bs]
marksobject .. end
as a JS object
var v2 = {
hi: function (x, y) {
var self = this ;
return self.say(x) + y;
},
say: function (x) {
var self = this ;
return x * self.x();
},
x: function () {
return 3;
}
};
Compiler infers the type of v2
as below:
val v2 : <
hi : float -> float -> float [@bs.meth];
say : float -> float [@bs.meth];
x : unit -> float [@bs.meth]
> [@bs]
Below is another example to consume a JS object :
let f (u : rect) =
(* the type annotation is un-necessary,
but it gives better error message
*)
Js.log u##height;
Js.log u##width;
u##width #= 30;
u##height #= 30;
u##draw ()
function f(u){
console.log(u.height);
console.log(u.width);
u.width = 30;
u.height = 30;
return u.draw()
}
Object label translation convention
There are two cases, where we might want to do name mangling for a JS object method name.
First, in OCaml, some names are keywords, so we want to add an underscore to avoid a syntax error.
f##_open
f##_MAX_LENGTH
f.open
f.MAX_LENGTH
Second, it is common to have several types for a single method. To model this ad-hoc polymorphism, we introduced a small convention when translating object labels, which is occasionally useful as below
f##draw__cat (x,y)
f##draw__dog (x,y)
f.draw(x,y) // f.draw in JS can accept different types
f.draw(x,y)
Note
|
Rules
|
Return value checking (@since 1.5.1)
In general, the FFI code is error prone, and potentially will leak in
undefined
or null
values.
So we introduced auto coercion for return values to gain two benefits:
-
More safety for FFI code without performance cost (explained later).
-
More idiomatic OCaml code for users to consume the FFI.
Below is a contrived core example:
type element
type dom
external getElementById : string -> element option = ""
[@@bs.send.pipe:dom] [@@bs.return nullable] (1)
let test dom =
let elem = dom |> getElementById "haha" in
match elem with
| None -> 1
| Some ui -> Js.log ui ; 2
-
nullable
attribute will automatically convert null and undefined tooption
function test(dom) {
var elem = dom.getElementById("haha");
if (elem == null) {
return 1;
} else {
console.log(elem);
return 2;
}
}
Currently 4 directives are supported: null_to_opt
, undefined_to_opt
,
nullable
(introduced in @1.9.0) and identity
.
null_undefined_to_opt
works the same as nullable
,
but it is deprecated, nullable
is encouraged
Note
|
The three directives above require users to write literally When the return type is When the return type is
|
Mapping between JS values and OCaml values (@since 2.1.0)
BuckleScript already maps basic OCaml primitive types into JS primitive types, however, there are some cases where such direct mapping is technically challenging, we automate such process by providing a function to convert back and forth between idiomatic JS values and idiomatic OCaml values, the generated code is optimized for both performance and code size.
Mapping JS Int enums to OCaml enums (@since 2.1.0)
type t =
| A0
| A1
| A2
| A3
| A4
[@@bs.deriving jsConverter]
This will derive two functions
val tToJs : t -> int
val tFromJs : int -> t option
Note here by default Ai
is mapped into i
, but we can customie it
as follows:
type t =
| A0
| A1 [@bs.as 3]
| A2
| A3 [@bs.as 7]
| A4
[@@bs.deriving jsConverter]
In this case, A0
is 0, A1
is 3, A2
is 4, A3
is 7, A4
is 8.
Note the above derived js type is transparent: int
, if we want to
provide more type safety, we can annotate it as below:
type t =
| A0
| A1
| A2
| A3
| A4
[@@bs.deriving {jsConverter = newType} ]
In this case, it would generate two functions of such type, note newType
means
a new type declaration (by default to be abstract) is created:
val tToJs : t -> abs_t
val tFromJs : abs_t -> t
Note the derived type is slightly different, since abs_t
is abstrac type,
we assume it is well structured value, so it should always return value of type t
instead of t option
.
The bs.deriving
also applies to the signature file, so that users don’t have to
write generate functions manually
Mapping JS string enums to OCaml enums (@since 2.1.0)
type t = [
| `A0
| `A1
| `A2
| `A3
| `A4
]
[@@bs.deriving jsConverter]
This will derive two functions
val tToJs : t -> string
val tFromJs : string -> t option
Note here by default Ai
is mapped into "Ai"
, but we can customie it
as follows:
type t = [
| `A0
| `A1 [@bs.as "c"]
| `A2
| `A3 [@bs.as "d"]
| `A4
]
[@@bs.deriving jsConverter]
Note the above derived js type is transparent: string
, if we want to
provide more type safety, we can annotate it as below:
type t = [
| `A0
| `A1
| `A2
| `A3
| `A4
]
[@@bs.deriving {jsConverter = newType} ]
In this case, it would generate two functions of such type:
val tToJs : t -> abs_t
val tFromJs : abs_t -> t
Note the derived type is slightly different, since abs_t
is abstrac type,
we assume it is well structured value, so it should always return value of type
t
instead of type t option
.
The bs.deriving
also applies to the signature file, so that users don’t have to
write generate functions manually
Mapping JS objects to OCaml records (@since 2.1.0)
type t =
{
x : int ;
y : int
}
[@@bs.deriving jsConverter]
This will derive two functions:
val tToJs : t -> <x : int ; y : int > Js.t
val tFromJs : < x; int ; y : int ; .. > Js.t -> t
Note that the input of tFromJs
is open type so that it is more permissive.
Note the above derived js type is transparent, if we want to provide more type safety, we can annotate it as below:
type t =
{
x : int ;
y : int
}
[@@bs.deriving {jsConverter= newType}]
This will derive two functions:
val tToJs : t -> abs_t
val tFromJs : abs_t -> t
Embedding untyped Javascript code
Warning
|
This is not encouraged. The user should minimize and localize use cases of embedding raw JavaScript code, however, sometimes it’s necessary to get the job done. |
Detect global variable existence bs.external
(@since 1.5.1)
Before we dive into embedding arbitrary JS code, a quite common use case of embedding untyped JS code is detect a global variable (feature detection), Bucklescript provides a relatively type safe approach for such use case: bs.external
(or external
),
[%bs.external a_single_identifier]
is a value of _ option
type, see examples below
let test () =
match [%external __DEV__] with
| Some _ -> Js.log "dev mode"
| None -> Js.log "production mode"
function test() {
var match = typeof (__DEV__) === "undefined" ? undefined : (__DEV__);
if (match !== undefined) {
console.log("dev mode");
return /* () */0;
}
else {
console.log("production mode");
return /* () */0;
}
}
let test2 () =
match [%external __filename] with
| Some f -> Js.log f
| None -> Js.log "non node environment"
function test2() {
var match = typeof (__filename) === "undefined" ? undefined : (__filename);
if (match !== undefined) {
console.log(match);
return /* () */0;
}
else {
console.log("non node environment");
return /* () */0;
}
}
Embedding arbitrary JS code as an expression
let keys : t -> string array [@bs] = [%bs.raw "Object.keys" ]
let unsafe_lt : 'a -> 'a -> Js.boolean [@bs] = [%bs.raw{|function(x,y){return x < y}|}]
We highly recommend writing type annotations for such unsafe code. It is unsafe to refer to external OCaml symbols in raw JS code.
Embedding raw JS code as statements
[%%bs.raw{|
console.log ("hey");
|}]
Other examples:
let x : string = [%bs.raw{|"\x01\x02"|}]
It will be compiled into:
var x = "\x01\x02"
Polyfill of Math.imul
[%%bs.raw{|
// Math.imul polyfill
if (!Math.imul){
Math.imul = function (..) {..}
}
|}]
Warning
|
|
Debugger support
We introduced the extension bs.debugger
, for example:
let f x y =
[%bs.debugger];
x + y
which will be compiled into:
function f (x,y) {
debugger; // JavaScript developer tools will set an breakpoint and stop here
x + y;
}
Regex support
We introduced bs.re
for Javascript regex expressions:
let f = [%bs.re "/b/g"]
The compiler will infer f
has type Js.Re.t
and generate code as
below:
var f = /b/g
Note
|
Js.Re.t can be accessed and manipulated using the functions available in the Js.Re module.
|
Examples
Below is a simple example for the mocha library. For more examples, please visit https://github.com/bucklescript/bucklescript-addons
A simple example: binding to mocha unit test library
This is an example showing how to provide bindings to the mochajs unit test framework.
external describe : string -> (unit -> unit [@bs]) -> unit = "" [@@bs.val]
external it : string -> (unit -> unit [@bs]) -> unit = "" [@@bs.val]
Since, mochajs
is a test framework, we also need some assertion
tests. We can also describe the bindings to assert.deepEqual
from
the nodejs assert
library:
external eq : 'a -> 'a -> unit = "deepEqual" [@@bs.module "assert"]
On top of this we can write normal OCaml functions, for example:
let assert_equal = eq
let from_suites name suite =
describe name (fun [@bs] () ->
List.iter (fun (name, code) -> it name code) suite
)
The compiler would generate code as below:
var Assert = require("assert");
var List = require("bs-platform/lib/js/list");
function assert_equal(prim, prim$1) {
return Assert.deepEqual(prim, prim$1);
}
function from_suites(name, suite) {
return describe(name, function () {
return List.iter(function (param) {
return it(param[0], param[1]);
}, suite);
});
}
Exception handling between OCaml and JS (@since 1.7.0)
In the JS world, exception could be any data, while an OCaml exception is a structured data format and supports pattern matching. Catching an OCaml exception on JS side is therefore a no-op.
JS exceptions can be raised from OCaml by using the JS.Exn.raise*
functions, and can be caught as an OCaml exception of the type Js.Exn.Error
with the JS exception as it’s paylaod typed as Js.Exn.t
. The JS Exception can then either be manipulated with the accessor functions in Js.Exn
, or casted to a more appropriate type.
let () =
try
Js.Exn.raiseError "oops!"
with
| Js.Exn.Error e ->
match Js.Exn.message e with
| Some message -> Js.log {j|Error: $message|j}
| None -> Js.log "An unknown error occurred"
let maybeParsed =
match Js.Json.parseExn {| {"x" }|} with
| value -> Some value
| exception Js.Exn.Error e ->
Js.log (Js.Exn.message e);
None
Please consult the Js.Exn
API reference for more details
bs.open
: Type safe external data-source handling (@@since 1.7.0)
There are some cases, the data-source could either come from JS land or OCaml land, it is very hard to give precise type information.
For example, for an external promise whose creation could come from JS API, its failed value caused by Promise.reject
could be in any shape.
BuckleScript provides a solution to filter out OCaml structured exception data from the mixed data source, it preserves the type safety while allow users to deal with mixed source.
It makes use of OCaml’s extensible variant, so that users can mix values of type exn
with JS data-source
let handleData = function [@bs.open]
| Invalid_argument _ -> 0
| Not_found -> 1
| Sys_error _ -> 2
val handleData : 'a -> int option (1)
-
For any input source, as long as it matches the exception pattern (nested pattern match supported), the matched value is returned, otherwise return
None
.
Use cases
Take promise for example:
let v = Js.Promise.reject Not_found
let handlePromiseFailure = function [@bs.open]
| Not_found -> Js.log "Not found"; (Js.Promise.resolve ())
let () =
v
|> Js.Promise.catch (fun error ->
match handlePromiseFailure error with
| Some x -> x
| None -> raise UnhandledPromise
)
Js module
Js module is shipped with BuckleScript, both the namespace Js
and Node
are preserved.
Null and Undefined
BuckleScript does not deal with null
and undefined
on the language level.
Instead they are defined on the library level, represented by Js.Null.empty
and Js.Undefined.empty
(and Js.Null_undefined.empty
) types. But due to this
being Ocaml and all, we can’t just pass these values off as if it was any type
we’d like it to be, so in order to actually use them we need to lift the type we
want to use them with. For this purpose BuckleScript defines the type 'a Js.null
,
'a Js.undefined' and ’a Js.null_undefined
(for values that can be both
null
, undefined
and 'a
). As well as the corresponding modules Js.Null
,
Js.Undefined
and Js.Null_undefined
for working with these types.
Here’s an example showing Js.Null
used directly:
external get_prop_name_maybe : unit -> string Js.null = "" [@@bs.val]
external set_or_unset_prop : string -> int Js.null -> unit = "" [@@bs.val]
(* unset property if fond *)
let s = get_prop_name_maybe ()
let _ = Js.Null.iter s ((fun name -> set_or_unset_prop name Js.Null.empty) [@bs])
(* set some other property *)
let _ = set_or_unset_prop "some_other_property" (Js.Null.return "")
You might also want to map these types to option
for a friendlier and more
idiomatic API, if the distinction between null
and undefined
isn’t important
to maintain:
external try_some_thing : string Js.null -> int Js.null = ...
let try_some_thing_maybe (v : string option) : int option =
Js.Null.to_opt (try_some_thing (Js.Null.from_opt v))
Boolean
"Native" bool
is for Bob-ish reasons not represented as true
and false
literals in JavaScript. For interop you therefore need to use the Js.boolean
and its Js.true_
and Js.false_
values. There are also convenience functions
for coercing to and from bool
: Js.to_bool
and Js.Boolean.to_js_boolean
.
Extended compiler options
This section is only for people who want roll their own build system instead of using the recommended build system: BuckleScript build system: bsb
,
in general, it is safe to skip this section.
It also adds some tips for people who debug the compiler.
BuckleScript inherits the command line arguments of the OCaml compiler. It also adds several flags:
-bs-main (single directory build)
bsc.exe -bs-main Main
bsc.exe
will build module Main
and all its dependencies and when it
finishes, it will run node main.js
.
bsc.exe -c -bs-main Main
The same as above, but will not run node
.
-bs-files
So that you can do
bsc.exe -c -bs-files *.ml *.mli
the compiler will sort the order of input files before starting compilation.
BuckleScript supports two compilation modes: script mode and package
mode. In package mode, you have to provide package.json
on top and set the options
-bs-package-name
, -bs-package-output
. In script mode, such flags are not needed.
-bs-package-name
The project name of your project. The user is suggested to make it
consistent with the name
field in package.json
-bs-packge-output
The format is module_system:oupt/path/relative/to/package.json
Currently supported module systesms are: commonjs
, amdjs
and
es6
.
For example, when you want to use the es6
module system, you can do
things like this:
bsc.exe -bs-package-name your_package -bs-package-output es6:lib/es6 -c xx.ml
Note
|
The user can supply multiple -bs-package-output at the same time.
|
For example:
bsc.exe -bs-package-name name -bs-package-output commonjs:lib/js -bs-package-output amdjs:lib/amdjs -c x.ml
It will generate x.js
in lib/js
as a commonjs module, lib/amdjs
as an amdjs module at the same time.
You would then need a bundler for the different module systems:
webpack
supports commonjs
and amdjs
while
google closure compiler
supports all.
-bs-gen-tds
Trigger the generation of TypeScript .d.ts
files.
bsc.exe
has the ability to also emit .d.ts
for better interaction with
TypeScript. This is still experimental.
For more options, please see the documentation of bsc.exe -help
.
-bs-eval
bsc.exe -dparsetree -drawlambda -bs-eval 'Js.log "hello"'
[ (1)
structure_item (//toplevel//[1,0+0]..[1,0+14])
Pstr_eval
expression (//toplevel//[1,0+0]..[1,0+14])
Pexp_apply
expression (//toplevel//[1,0+0]..[1,0+6])
Pexp_ident "Js.log" (//toplevel//[1,0+0]..[1,0+6])
[
<label> ""
expression (//toplevel//[1,0+7]..[1,0+14])
Pexp_constant Const_string("hello",None)
]
]
(2)
(setglobal Bs_internal_eval! (seq (js_dump "hello") (makeblock 0)))
// Generated by BUCKLESCRIPT VERSION 1.0.2 , PLEASE EDIT WITH CARE
'use strict';
console.log("hello");
/* Not a pure module */
-
Output by flag -dparsetree
-
Output by flag -drawlambda
For this flag, it will not create any intermediate file, which is useful for learning or troubleshooting.
Semantic differences from other backends
This is particularly important when porting an existing OCaml application to JavaScript.
Custom data type
In OCaml, the C FFI allows the user to define a custom data type and
customize caml_compare
, caml_hash
behavior, etc. This is not
available in our backend (since we have no C FFI).
Physical (in)equality
In general, users should only use physical equality as an optimization technique, but not rely on its correctness, since it is tightly coupled with the runtime.
String char range
Currently, BuckleScript assumes that the char range is 0-255
. The user
should be careful when they pass a JavaScript string to the OCaml side. Note
that we are working on a solution for this problem.
Weak map
OCaml’s weak map is not available in BuckleScript. The weak pointer is replaced by a strict pointer.
Integers
OCaml has int
, int32
, nativeint
and int64
types.
- Both int32
and int64
in BuckleScript have the exact same semantics as OCaml.
- int
in BuckleScript is the same as int32
while in OCaml it’s platform dependent.
- nativeint
is treated as JavaScript float, except for division.
For example, Nativeint.div a b
will be translated into a /b | 0
.
Warning
|
|
Printf.printf
The Printf.print
implementation in BuckleScript requires a newline
(\n
) to trigger the printing. This behavior is not consistent with the
buffered behavior of native OCaml. The only potential problem we foresee
is that if the program terminates with no newline character, the text
will never be printed.
Obj module
We do our best to mimic the native compiler, but we have no guarantee and there are differences.
Hashtbl hash algorithm
BuckleScript uses the same algorithm as native OCaml but the output is different due to the runtime representation of int/int64/int32 and float.
Conditional compilation support - static if
It is quite common that people want to write code that works with different versions of compilers and libraries.
People used to use preprocessors like C preprocessor for the C family languages. In the OCaml community there are several preprocessors: cppo, ocp-pp, camlp4 IFDEF macros, optcomp and ppx optcomp.
Instead of using a preprocessor, BuckleScript adds language level static if compilation to the language.
It is less powerful than other preprocessors since it only supports static if (no #define
, #undefine
, #include
)
but there are several advantages.
-
It’s very small (only around 500 LOC) and highly efficient. There is no added pass, allowing everything to be done in a single pass. It is easy to rebuild the pre-processor in a stand alone file, with no dependencies on compiler libs to back-port it to old OCaml compilers.
-
It’s purely functional and type safe, easy to work with IDEs like Merlin.
Concrete syntax
static-if
| HASH-IF-BOL conditional-expression THEN (1)
tokens
(HASH-ELIF-BOL conditional-expression THEN) *
(ELSE-BOL tokens)?
HASH-END-BOL
conditional-expression
| conditional-expression && conditional-expression
| conditional-expression || conditional-expression
| atom-predicate
atom-predicate
| atom operator atom
| defined UIDENT
| undefined UIDENT
operator
| (= | < | > | <= | >= | =~ )
atom
| UIDENT | INT | STRING | FLOAT
-
IF-BOL means
#IF
should be in the beginning of a line
Typing rules
-
type of INT is
int
-
type of STRING is
string
-
type of FLOAT is
float
-
value of UIDENT comes from either built-in values (with documented types) or an environment variable, if it is literally
true
,false
then it isbool
, else if it is parsable byint_of_string
then it is of type int, else if it is parsable byfloat_of_string
then it is float, otherwise it would be string -
In
lhs operator rhs
,lhs
andrhs
are always the same type and return boolean.=~
is a semantic version operator which requires both sides to be string.
Evaluation rules are obvious.
=~
respect semantic version, for example, the underlying engine
semver Location.none "1.2.3" "~1.3.0" = false;;
semver Location.none "1.2.3" "^1.3.0" = true ;;
semver Location.none "1.2.3" ">1.3.0" = false ;;
semver Location.none "1.2.3" ">=1.3.0" = false ;;
semver Location.none "1.2.3" "<1.3.0" = true ;;
semver Location.none "1.2.3" "<=1.3.0" = true ;;
semver Location.none "1.2.3" "1.2.3" = true;;
Examples
type open_flag =
Unix.open_flag =
| O_RDONLY
| O_WRONLY
| O_RDWR
| O_NONBLOCK
| O_APPEND
| O_CREAT
| O_TRUNC
| O_EXCL
| O_NOCTTY
| O_DSYNC
| O_SYNC
| O_RSYNC
#if OCAML_VERSION =~ ">=3.13" then
| O_SHARE_DELETE
#end
#if OCAML_VERSION =~ ">=4.01" then
| O_CLOEXEC
#end
Built in variables and custom variables
ocamlscript>bsc.exe -bs-D CUSTOM_A="ghsigh" -bs-list-conditionals
OCAML_PATCH "BS"
BS_VERSION "1.2.1"
OS_TYPE "Unix"
BS true
CUSTOM_A "ghsigh"
WORD_SIZE 64
OCAML_VERSION "4.02.3+BS"
BIG_ENDIAN false
Changes to command line options
For BuckleScript users, nothing needs to be done (it is baked in the language level). For non BuckleScript users, we provide an external pre-processor, so it will work with other OCaml compilers too. Note that the bspp.ml is a stand alone file, so that it even works without compilation.
bsc.exe -c lwt_unix.mli
ocamlc -pp 'bspp.exe' -c lwt_unix.mli
ocamlc -pp 'ocaml -w -a bspp.ml' -c lwt_unix.mli
Warning
|
This is a very small extension to the OCaml language, it is backward compatible with OCaml language with such exceptions.
|
Build system support
The BuckleScript compilation model is similar to OCaml native compiler.
If b.ml
depends on a.ml
, you have to compile a.ml
and a.mli
first.
Note
|
The technical reason is that BuckleScript will generate intermediate
files with the extension |
BuckleScript build system: bsb
BuckleScript provides a native build tool on top of Google’s ninja-build. It is designed for a fast feedback loop (typically 100ms feedback loop) and works cross platform.
bsb
is a schema based build tool. The schema is
available. It is strongly recommended to check out the schema
after you finish reading the tutorials below.
If your editor supports JSON schema validation and auto-completion like VSCode, below is a configuration to help you get auto-completion and validation for free:
{
"json.schemas": [
{
"fileMatch": [
"bsconfig.json"
],
"url" : "file:///path/to/bucklescript/docs/docson/build-schema.json" (1)
}
],
// ....
}
The build system is installed as bsb.exe
in bs-platform/bin/bsb.exe
. Due to a known issue in npm,
we create a JS wrapper (which is accessible in .bin
too) so the user can call
either bsb
(slightly higher latency but symlinked into .bin
) or bsb.exe
Here is a minimal configuration:
{
"name": "test", // package name, required (1)
"sources": "src" (2)
}
-
It is an extension to JSON with comment support
-
Here we did not list files, so all
.ml
,.mli
,.re
,.rei
will be considered as source files
The entry point is bsb
.
It will check if there is already build.ninja
in the build directory,
and if not or it needs to be regenerated it will generate the file build.ninja
and delegate the hard work to ninja
.
The directory layout (after building) would be
. ├── lib │ ├── bs │ │ ├── src │ │ └── test │ ├── js │ │ ├── src │ │ └── test │ ├── amdjs (1) │ │ ├── src │ │ └── test │ ├── es6 (2) │ │ ├── src │ │ └── test │ └── ocaml ├── scripts ├── src └── test
-
Will generate AMDJS modules if flags are turned on
-
Will generate ES6 modules if flags are turned on
We wrap bsb.exe
as bsb
so that it will work across different platforms.
bsb (1)
bsb -w (2)
-
Do the plain build
-
Do the plain build and watch
Build with other BuckleScript dependencies
List your dependency in bs-dependencies
and install it via npm install
as below:
{
"name": "bs-string",
"version": "0.1.3",
"bs-dependencies": [
"bs-mocha" (1)
],
"sources": [
.. .
],
"generate-merlin" : false (2)
}
-
Yet another BuckleScript dependency
-
If true (default) bsb will generate merlin file for you
{
"dependencies": {
"bs-mocha": "0.1.5"
},
...
}
After your npm install
,
bsb -clean-world (1)
bsb -make-world (2)
bsb -w (3)
-
Clean the binary artifact of current build and your dependency
-
Build dependencies and lib itself
-
Start watching the project and whenever your changes are made,
bsb
will rebuild incrementally
You can also streamline the three commands as below:
bsb -clean-world -make-world -w
Mark your directory as dev only
Note sometimes, you have directories which are just tests that you don’t need your dependency to build. In that case you can mark it as dev only:
{
"sources" : {
"dir" : "test",
"type" : "dev" (1)
}
}
-
directory
test
is in dev mode, it will not be built when used as a dependency
A real world example of using bsb
Below is a json configuration for the bucklescript-tea: the Elm artchitecture in BuckleScript
{
"name": "bucklescript-tea",
"version": "0.1.3",
"sources": [
"src", (1)
{
"dir": "test",
"type": "dev" (2)
}
]
}
-
Source directory, by default it will export all units of this directory to users.
-
Dev directory, which will only be useful for developers of this project.
{
"name": "bucklescript-tea",
"version": "0.1.3",
"description": "TEA for Bucklescript",
"scripts": {
"build": "bsb",
"watch": "bsb -w",
"test": "echo \"Error: no test specified\" && exit 1"
},
"peerDependencies": {
"bs-platform": "^1.7.0" (1)
}
}
-
Here we list
bs-platform
as a peer dependency so that different repos shares the same compiler.
Now, if we have a repo bucklescript-have-tea which depends on bucklescript-tea
, its configurations are as below:
{
"name" : "bucklescript-have-tea",
"sources" : "src",
"bs-dependencies": [
"bucklescript-tea"
]
}
{
"name" : "bucklescript-have-tea",
"version" : "0.1.0",
"dependencies" : { "bucklescript-tea" : "^0.1.2" }, (1)
"peerDependencies" : { "bs-platform" : "^1.7.0" } (2)
}
-
List
bucklescript-tea
as dependency -
List
bs-platform
as peer dependency
Suppose you are in bucklescript-have-tea
top directory,
npm install (1)
npm install bs-platform (2)
./node_modules/.bin/bsb -clean-world -make-world -w (3)
-
Install the dependencies
-
Install peer dependencies
-
On Windows, it would be
.\node_modules\.bin\bsb -clean-world -make-world -w
You can also change the package-specs
to have another module format, for example, tweak your bsconfig.json
:
{
... ,
"package-specs" : ["amdjs", "commonjs"],
...
}
Rerun the command
bsb -clean-world -make-world
You will get both commonjs
and amdjs
support. In the end, we suggest you check out the schema and enjoy the build!
namespace support (@since 1.9.0)
OCaml treats file names as modules, so that users can only have unique file names in a project, BuckleScript solves the problem by scoping all modules by package.
Below is the bsconfig.json
for liba
, libb
(they share the same configuration in this example)
{
"name": "liba",
"version": "0.1.0",
"sources": "src",
"namespace": true
}
Now you have a library to use them
{
"name": "namespace",
"version": "0.1.0",
"sources": "src",
"bs-dependencies": [
"liba",
"libb"
]
}
Since liba
and libb
are configured using namespace, to use them in source code, it would be like
let v =
(Liba.Demo.v + Libb.Demo.v)
Note
|
In the same package, everything works the same whether it uses namespace or not, it only affects people who use your library |
In source build support (@since 1.9.0)
When user has an existing JS project, it makes sense to output the JS file in the same directory as vanilla JS,
so the schema added a key called in-source
so that the JS file is generated next to ML file.
Example:
{
...
"package-specs" : [{
"module": "commonjs",
"in-source": true
}]
}
Build using Make
Warning
|
|
BuckleScript distribution has bsdep.exe
which has the same interface as ocamldep
Here is a simple Makefile to get started:
OCAMLC=bsc.exe (1)
OCAMLDEP=bsdep.exe (2)
SOURCE_LIST := src_a src_b
SOURCE_MLI = $(addsuffix .mli, $(SOURCE_LIST))
SOURCE_ML = $(addsuffix .ml, $(SOURCE_LIST))
TARGETS := $(addsuffix .cmj, $(SOURCE_LIST))
INCLUDES=
all: $(TARGETS)
.mli:.cmi
$(OCAMLC) $(INCLUDES) $(COMPFLAGS) -c $<
.ml:.cmj:
$(OCAMLC) $(INCLUDES) $(COMPFLAGS) -c $<
-include .depend
depend:
$(OCAMLDEP) $(INCLUDES) $(SOURCE_ML) $(SOURCE_MLI) > .depend
-
bsc.exe is the BuckleScript compiler
-
ocamldep executable is part of the OCaml compiler installation
In theory, people need run make depend && make all
. make depend
will calculate dependencies
while make all
will do the job.
However in practice, people used to a file watch service, such as watchman for example, will need the JSON configuration:
[
"trigger", ".", {
"name": "build",
"expression": ["pcre", "(\\.(ml|mll|mly|mli|sh|sh)$|Makefile)"], (1)
"command": ["./build.sh"],
"append_files" : true
}
]
-
whenever such files are changed, it will trigger the
command
field to be run
make -r -j8 all (1)
make depend (2)
-
build
-
update the dependency
Now in your working directory, type watchman -j < build.json
and enjoy the lightning build speed.
Customize rules (generators support, @since 1.7.4)
It is quite common for programmers to use some preprocessors to generate some boilerplate code during developement.
Note preprocessors can be classified in two categories: one is system-dependent which must be delayed until running on the target machines and the other is system-independent (such as lex, yacc, m4, re2c, etc.) which could be executed anytime.
BuckleScript has built in support for conditional compilation for the second category. Since it is system-independent, we ask users to always generate such code and check it in before shipping, which would cut the dependencies for end users.
A typical example would be like this
{
"generators" : [
{ "name" : "ocamlyacc" ,
"command" : "ocamlyacc $in" }
],
...,
"sources" : {
"dir" : "src",
"generators" : [
{
"name" : "ocamlyacc",
"edge" : ["test.ml", "test.mli", ":", "test.mly"]
}
]
}
}
Note ocamlyacc
will generate test.ml
and test.mli
in the same directory with test.mly
. The developer should check in the generated file since then users would not need run ocamlyacc
again. This would apply to menhir
as well.
When a developer is working on their current project, bsb
will track the dependencies between test.ml
and test.mly
properly. When released
as a package, bsb
will cut such a dependency, so that users will
only need the generated test.ml
. To help test such behavior, developers can set it manually:
{
...,
"cut-generators" : true
}
With this setting bsb
will not regenerate test.ml
whenever test.mly
changes.
FAQ
-
How does IO work in the browser?
In general, it is very hard to simulate IO in the browser, we recommend users write bindings to NodeJS directly for server side, or use
Js.log
in client side, see discussions in #748 -
The compiler does not build?
In production mode, the compiler is a single file in
jscomp/bin/compiler.ml
. If it is not compiling, make sure you have the right OCaml compiler version. Currently the OCaml compiler is a submodule of BuckleScript. Make sure the exact commit hash matches (we only update the compiler occasionally). -
Which version of JavaScript syntax does BuckleScript target?
BuckleScript targets ES5.
-
Does BuckleScript work with merlin?
Yes, you need edit your
.merlin
file:B node_modules/bs-platform/lib/ocaml S node_modules/bs-platform/lib/ocaml FLG -ppx node_modules/bs-platform/bin/bsppx.exe
Note there is a upstream fix in Merlin, make sure your merlin is updated.
-
What polyfills does BuckleScript need?
-
Math.imul: This polyfill is needed for
int32
multiplication. BuckleScript provides this by default (when feature detection returns false), no action is required from the user. -
TypedArray: The TypedArray polyfill is not provided by BuckleScript and it’s the responsibility of the user to bundle the desired polyfill implementation with the BuckleScript generated code.
The following functions from OCaml stdlib require the TypedArray polyfill:
-
Int64.float_of_bits
-
Int64.bits_of_float
-
Int32.float_of_bits
-
Int32.bits_of_float
WarningFor the current BuckleScript version, if the user does not bundle the TypedArray polyfill, the JavaScript engine does not support it and user used functions mentioned above, the code will fail at runtime.
-
-
-
Uncurried functions cannot be polymorphic?
E.g. if you try to do this at toplevel:
let id : ('a -> 'a [@bs]) = ((fun v -> v) [@bs])
You’ll get this dreaded error message
Error: The type of this expression, ([ `Arity_1 of '_a ], '_a) Js.fn,
contains type variables that cannot be generalized
The issue here isn’t that the function is polymorphic. You can use (and probably have used) polymorphic uncurried functions without any problem as inline callbacks. But you can’t export them. The issue here is the combination of using the uncurried calling convention, polymorphism and exporting (by default). It’s an unfortunate limitation partly due to how OCaml’s type system incorporates side-effects, and partly due to how BuckleScript handles uncurrying. The simplest solution is in most cases to just not export it, by adding an interface to the module. Alternatively, if you really, really need to export it, you can do so in its curried form, and then wrap it in an uncurried lambda at the call site. E.g.:
lat _ = map (fun v -> id v [@bs])
High Level compiler workflow
The high level architecture is illustrated below:
Source Language | | (Reuse OCaml Parser) v Surface Syntax Tree | | (built in Syntax tree transformation) v Surface Syntax Tree | | (Reuse OCaml Type checker) v Typedtree | | (Reuse OCaml pattern match compiler and erase types) | (Patches to pass more information down to Lambda ) | OCaml Lambda IR | | v Buckle Lambda IR ------------------+ | ^ | | | 6 Lambda Passes (lam_* files) | | Optimization/inlining/dead code elimination | \ | | \ --------------------------+ | | Self tail call elimination | Constant folding + propagation V JS IR (J.ml) ---------------------+ | ^ | | | 6 JS Passes (js_* files) | | Optimization/inlining/dead code elimination | \ | | \ -------------------------+ | | Smart printer includes scope analysis | V Javascript Code
Design Principles
The current design of BuckleScript follows several high level principles. While those principles might change in the future, they are enforced today and can explain certain technical limitations BuckleScript has.
Lambda Representation
As pictured in the diagram above, BuckleScript is primarily based on the Lambda representation of the OCaml compiler. While this representation is quite rich, some information is lost from the upstream representation. The patch to the OCaml compiler tries to enrich this representation in a non-intrusive way (see next section).
Minimal Patch to the OCaml compiler
BuckleScript requires patches to the OCaml compiler. One of the main reasons is to enrich the Lambda representation so that the generated code is as nice as possible. A design goal is to keep those patches minimal and useful for the OCaml compiler in general so that they can later be integrated.
Note
|
A common question is to wonder why BuckleScript transpiles an OCaml record value to a JavaScript array while a more intuitive representation would be a JavaScript object. This technical decision is a direct consequence of the above 2 design principles: the Lambda layer assumes in a lot of places that a record value is an array and such modification would be too large of a change to OCaml compiler. |
Runtime representation
Below is a description of how OCaml values are encoded in JavaScript, the internal description means users should not rely on its actual encoding (and it is subject to change). We recommend that you write setter/getter functions to manipulate safely OCaml values from JavaScript.
For example, users should not rely on how OCaml list
is encoded in
JavaScript; instead, the OCaml stdlib provides three functions: List.cons
, List.hd
and
List.tl
. JavaScript code should only rely on those three functions.
Simple OCaml type
ocaml type | JavaScript type | ||
---|---|---|---|
int |
number |
||
nativeint |
number |
||
int32 |
number |
||
float |
number |
||
bool |
number
|
||
int64 |
Array of size two numbers |
||
char |
number for example:
|
||
string |
string |
||
bytes |
number array
|
||
'a array |
Array |
||
record |
Array internal For instance:
Output:
|
||
tuple |
Array For example:
|
||
|
internal For example:
|
||
list |
internal For example:
|
||
Variant |
internal (subject to change) Simple Variants: (Variants with only one non-nullary constructor)
Complex Variants: (Variants with more than one non-nullary constructor)
|
||
Polymorphic variant |
internal
|
||
exception |
internal |
||
extension |
internal |
||
object |
internal |
||
|
boolean For example:
Js module
Js.Boolean module
val to_js_boolean: bool -> Js.boolean |
||
|
Either Js.Null module
|
||
|
Either |
||
|
Either
This module’s null tests check for both |
Note
|
Js.to_opt is optimized when the option is not escaped
|
Note
|
In the future, we will have a debug mode, in which the corresponding js encoding will be instrumented with more information |
As we clarified before, the internal representation should not be relied upon. We are working to provide a ppx extension as below:
type t =
| A
| B of int * int
| C of int * int
| D [@@bs.deriving{export}]
So that it will a automatically provide constructing
and
destructing
functions:
val a : t
val b : int -> int -> t
val c : int -> int -> t
val d : int
val a_of_t : t -> bool
val d_of_t : t -> bool
val b_of_t : t -> (int * int ) Js.Null.t
val c_of_t : t -> (int * int ) Js.Null.t
Integration with Reason
You can play with Reason using the playground Facebook Reason
Note
|
The playgrounds are only for demos and might not be the latest
You should always use the command line as your production tool. |
There is a stand alone example here.
Contributions
First, thanks for your interest in contributing! There are plenty of ways to make contributions, like blogging, sharing your experience, open sourcing your libraries using BuckleScript. They are all deeply appreciated.
This section will focus on how to contribute to this repo.
Development set-up
-
Having opam installed
opam switch 4.02.3+buckle-master # use our OCaml compiler opam install camlp4 <1>
Camlp4
is used to generate OCaml code for processing large AST. (j.ml
file), if you don’t changej.ml
(most likely you won’t), so you probably don’t need it -
Having NodeJS installed
-
Having Make installed
-
OS: Mac/Linux (Note BuckleScript works on Windows, but the dev mode is not tested)
Below assume that you are working in jscomp
directory, and that you’ve followed instructions from [Minimal dependencies] to build OCaml.
Contributing to bsb.exe
The build target is
make -C bin bsb.exe
So whenever you change files relevant to the build tool bsb.exe
, try it and do some
test. If it works, send a pull request!
Note that for most binaries in BuckleScript, we also have a release mode, which will pack all relevant files into a single file. This is important, since it will cut all our dev-dependencies, so the user does not need install those tools.
You can verify it by typing
make snapshotml # see the diffs in jscomp/bin
But please don’t commit those changes in PR, it will cause painful merge conflicts.
Contributing to bsc.exe
make -C bin bsc.exe # build the compiler in dev mode
make libs # build all libs using the dev compiler
There is also a snapshot mode,
make snapshotml
This will actually snapshot your changes into a single ml file and used in npm distribution. But please don’t commit those changes in PR, it will cause painful merge conflicts.
Contributing to the runtime
BuckleScript runtime implementation is currently a mix of OCaml and
JavaScript. (jscomp/runtime
directory). The JavaScript code is defined
in the .ml
file using the bs.raw
syntax extension.
The goal is to implement the runtime purely in OCaml and you can help contribute.
Each new PR should include appropriate testing.
Currently all tests are in jscomp/test
directory and you should either
add a new test file or modify an existing test which covers the part of
the compiler you modified.
-
Add the filename in
jscomp/test/test.mllib
-
Add a suite test
The specification is in jscomp/test/mt.ml
For example some simple tests would be like:
let suites : _ Mt.pair_suites =
["hey", (fun _ -> Eq(true, 3 > 2));
"hi", (fun _ -> Neq(2,3));
"hello", (fun _ -> Approx(3.0, 3.0));
"throw", (fun _ -> ThrowAny(fun _ -> raise 3))
]
let () = Mt.from_pair_suites __FILE__ suites
-
Run the tests
Suppose you have mocha installed, if not, try npm install mocha
mocha -R list jscomp/test/your_test_file.js
To build libs, tests and run all tests:
make libs && make -C test all && npm test
-
See the coverage
npm run cover
Contributing to the documentation
You’ll need Asciidoctor installed and on your PATH
.
Go into site/docsource/
, modify the section you want, and run build.sh
.
You can check the build.compile
file for debug output.
Contributing to the API reference
The API reference is generated from doc comments in the source code. Here's a good example
Some tips and guidelines:
-
The first sentence or line should be a very short summary. This is used in indexes and by tools like merlin.
-
Ideally, every function should have at least one
@example
-
Cross-reference another definition with
{! identifier}
. But use them sparingly, they’re a bit verbose (currently, at least). -
Wrap non-cross-referenced identifiers and other code in
[ … ]
-
Escape
{
,}
,[
,]
and@
using\
-
It’s possible to use
{%html …}
to generate custom html, but use this very, very sparingly. -
A number of "documentation tags" are provided that would be nice to use, but unfortunately they’re often not supported for `external`s. Which is of course most of the API.
-
@param
usually doesn’t work. Use{b <param>} …
instead -
@returns
usually doesn’t work. Use{b returns} …
instead. -
Always use
@deprecated
when applicable. -
Always use
@raise
when applicable. -
Always provide a
@see
tag pointing to MDN for more information when available.
See Ocamldoc documentation for a lot more details.
To generate the html, run make docs
in jscomp/
.
Html generation uses a custom generator located in odoc_gen/
and custom styles located in docs/api_static
.
Comparisons
Difference from js_of_ocaml
Js_of_ocaml is a popular compiler which compiles OCaml’s bytecode into JavaScript. It is the
inspiration for this project, and has already been under development for
several years and is ready for production. In comparison, BuckleScript,
while moving fast, is still a very young project. BuckleScript’s
motivation, like js_of_ocaml
, is to unify the ubiquity of the
JavaScript platform and the truly sophisticated type system of OCaml,
however, there are some areas where we view things differently from
js_of_ocaml
. We describe below, some of these differences, and also
refer readers to some of the original informal
discussions.
-
Js_of_ocaml takes low-level bytecode from OCaml compiler, BuckleScript takes the high-level rawlambda representation from OCaml compiler
-
Js_of_ocaml focuses more on existing OCaml ecosystem(opam) while BuckleScript’s major goal is to target npm
-
Js_of_ocaml and BuckleScript have slightly different runtime encoding in several places, for example, BuckleScript encodes OCaml Array as JS Array while js_of_ocaml requires its index 0 to be of value 0.
Both projects are improving quickly, so this can change in the future!
Appendix A: Library API documentation
There is a small library that comes with bs-platform
, its documentation is
here.
Appendix B: CHANGES
1.1.2
-
Fixes
-
Bug fix with opam issues #831
-
-
Features
-
Provide bspp.exe for official compiler
-
1.1.1
-
Features
-
Add bsdep.exe #822
-
Conditional compilation support #820
-
Relax syntactic restrictions for all extension point #793 so that
bs.obj
,obj
,bs.raw
,raw
, etc will both work. Note that all attributes will still be qualified -
Suport bs.splice in bs.new #793
-
Complete `bs.splice ` support and documentation #798
-
1.03
-
Features
-
Add an option
-bs-no-warn-unused-bs-attribute
#787
-
-
Incompatible changes (due to proper Windows support):
-
bsc
,bspack
andbsppx
are renamed intobsc.exe
,bspack.exe
andbsppx.exe
-
no symlink from .bin any more.
Old symlinkstmp>ls -al node_modules/.bin/ total 96 drwxr-xr-x 14 hzhang295 staff 476 Sep 20 17:26 . drwxr-xr-x 4 hzhang295 staff 136 Sep 20 17:27 .. lrwxr-xr-x 1 hzhang295 staff 22 Sep 20 17:26 bsc -> ../bs-platform/bin/bsc lrwxr-xr-x 1 hzhang295 staff 25 Sep 20 17:26 bspack -> ../bs-platform/bin/bspack lrwxr-xr-x 1 hzhang295 staff 24 Sep 20 17:26 bsppx -> ../bs-platform/bin/bsppx
Now these symlinks are removed, you have to refer to
bs-platform/bin/bsc.exe
-