Compiler · Feb 9, 2021

ReScript 9.0

Featuring a new external stdlib configuration, some syntax improvements and a small breaking change for nested records.
Hongbo Zhang
Compiler & Build System


We are happy to announce ReScript 9.0!

ReScript is a robustly typed language that compiles to efficient and human-readable JavaScript. It comes with one of the fastest build toolchains and offers first class support for interoperating with ReactJS and other existing JavaScript code.

Use npm to install the newest 9.0.1 release with the following command:

npm install bs-platform@9.0.1 --save-dev

You can also try our new release in the Online Playground.

In this post we will highlight the most notable changes. The full changelog for this release can be found here.

Compiler Improvements

New External Stdlib Configuration

This is a long-awaited feature request.

Our compiler comes with a set of stdlib modules (such as Belt, Pervasives, etc.) for core functionality. Compiled ReScript code relies on the JS runtime version of these stdlib modules.

In previous versions, users couldn't ship their compiled JS code without defining a package.json dependency on bs-platform. Whenever a ReScript developer wanted to publish a package just for pure JS consumption / lean container deployment, they were required to use a bundler to bundle up their library / stdlib code, which made things way more complex and harder to understand.

To fix this problem, we now publish our pre-compiled stdlib JS files as a separate npm package called @rescript/std. Each new bs-platform release has a matching @rescript/std release for runtime compatibility.

We also introduced a new configuration within our bsconfig.json file to tell the compiler to use our pre-compiled package instead:

{ /* ... */ "external-stdlib" : "@rescript/std" }

With this configuration set, compiled JS code will now point to the defined external-stdlib path:

Belt.Array.forEach([1, 2, 3], (num) => Js.log(num))

The JavaScript output above was compiled with an es6 target, but will also work with commonjs.

Important: When using this option, you need to make sure that the version number of bs-platform and @rescript/std matches with the same version number in your package.json file, otherwise you'll eventually run into runtime problems due to mismatching stdlib behavior!

To prevent unnecessary complications, only use this feature when...

  • You want to ship a library for JS / TS consumers without making them depend on bs-platform

  • You can't depend on bs-platform due to toolchain size (docker containers, low-storage deployment devices, etc)

Less Bundle Bloat when Adding ReScript

With each release we keep a close eye on generating code that is optimized for tree-shaking. We also believe that we reached a milestone where ReScript reliably produces output that has almost no impact on our final JS bundle-sizes (this is what we call our "zero-cost" philosophy).

The bundled code is almost ReScript runtime free because our generated library code fits the tree-shaking principle really well. Tools like esbuild can easily drop unnecessary code and make sure that the final code stays lean.

We made a small demo repo and added the precompiled JS bundles to demonstrate what we've achieved. Check it out!

Improved Code Generation for Pattern Matching

We fine-tuned our pattern matching engine to optimize the JS output even more. Here is an example of a pretty substantial optimization, based on this issue:

type test = | NoArg | AnotherNoArg | OtherArg(int) let test = x => switch x { | NoArg => true | _ => false }

The snippet above will compile to the following JS output:

9.0 Output8.4 Output
function test(x){
  return x === 0

As you can see, the 9.0 compiler removes all the unnecessary typeof checks!

This is possible because our optimizer will try to analyze several predicates and get rid of redundant ones. More diffs can be found here.

Another important improvement is that we fixed the pattern match offset issue, which lead to the consequence that magic numbers will not be generated for complex pattern matches anymore.

For those interested in the details, here is a representative diff resulting from this cleanup:

function is_space(param){ - var switcher = param - 9 | 0; - if (switcher > 4 || switcher < 0) { - return switcher == 23 ; + if (param > 13 || param < 9) { + return param === 32; } else { - return switcher !== 2; + return param != 11; } }

Syntax Improvements

when -> if

Starting from 9.0, when clauses within a switch statement will automatically convert to the if keyword instead.

New (9.0)Old (8.4)
switch person1 {
| Student({reportCard: {gpa}}) if gpa < 0.5 =>
  Js.log("What's happening")
| _ => () // do nothing

The when keyword is deprecated. The syntax will continue supporting it and the formatter will automatically convert to if, for a pain-free upgrade.

Cleaner Polyvariant Syntax

Polyvariants with invalid identifier names (e.g. names including hypens -), don't require any special escaping syntax anymore:

New (9.0)Old (8.4)
type animation = [ #"ease-in" | #"ease-out" ]

We introduced this change to allow easier interop with existing JS string enums. In pure ReScript code, we'd still recommend our users to stick with valid identifier names instead (e.g. easeIn instead of ease-in).

Breaking Changes

This release comes with a minor breaking change that shouldn't have much impact on the upgrade of existing codebases.

Nested Records within Objects

Previously, if you wrote {"user": {age: 10}}, the inner record was interpreted as an object instead of a record ({"user": {"age": 10}}); this is a byproduct of some internal interop transformation details; with the ReScript syntax, this went from understandable to confusing, so we're changing it so that the inner record is indeed now treated as a record. This is an obvious fix, but a breaking change if you were accidentally leveraging that nested record as object.

Here is a code example before and after the change. Note how the user record secretly turns into a ReScipt object in the previous version:

9.0 Example8.4 Example
type user = {
  age: int 

let data = {
  "user": {
    age: 1 

// This is the way: `age` should be usable via record accessor
let age = data["user"].age

More discussions on this change can be found here.

Closing Note

We only highlighted a few user-facing features, but there are also some pretty interesting internal changes happening right now.

For example, we are tinkering with the idea on using WASM to replace Camlp4, and we are also working on a generalized visitor pattern that doesn't require objects.

We will discuss these topics in a separate development post, but we are already excited about the new possibilities this will bring within the compiler toolchain.

Happy Hacking!

Want to read more?
Back to Overview