|
| 1 | +--- |
| 2 | +author: hongbo |
| 3 | +date: "2021-02-09" |
| 4 | +previewImg: https://res.cloudinary.com/dmm9n7v9f/image/upload/v1612974395/Reason%20Association/rescript-lang.org/compiler_release_9_0_szd11o.jpg |
| 5 | +category: compiler |
| 6 | +title: ReScript 9.0 |
| 7 | +badge: release |
| 8 | +description: | |
| 9 | + Featuring a new external stdlib configuration, some syntax improvements and a small breaking change for nested records. |
| 10 | +--- |
| 11 | + |
| 12 | +## Introduction |
| 13 | + |
| 14 | +We are happy to announce ReScript 9.0! |
| 15 | + |
| 16 | +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. |
| 17 | + |
| 18 | +Use `npm` to install the newest [9.0.0 release](https://www.npmjs.com/package/bs-platform/v/9.0.0) with the following command: |
| 19 | + |
| 20 | +``` |
| 21 | +npm install [email protected] --save-dev |
| 22 | +``` |
| 23 | + |
| 24 | +You can also try our new release in the [Online Playground](/try). |
| 25 | + |
| 26 | +In this post we will highlight the most notable changes. The full changelog for this release can be found [here](https://github.com/rescript-lang/rescript-compiler/blob/master/Changes.md#90). |
| 27 | + |
| 28 | +## Compiler Improvements |
| 29 | + |
| 30 | +### New External Stdlib Configuration |
| 31 | + |
| 32 | +This is a long-awaited [feature request](https://github.com/rescript-lang/rescript-compiler/pull/2171). |
| 33 | + |
| 34 | +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. |
| 35 | + |
| 36 | +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. |
| 37 | + |
| 38 | +To fix this problem, we now publish our pre-compiled stdlib JS files as a separate npm package called [`@rescript/std`](https://www.npmjs.com/package/@rescript/std). Each new `bs-platform` release has a matching `@rescript/std` release for runtime compatibility. |
| 39 | + |
| 40 | +We also introduced a new configuration within our `bsconfig.json` file to tell the compiler to use our pre-compiled package instead: |
| 41 | + |
| 42 | +```json |
| 43 | +{ |
| 44 | + /* ... */ |
| 45 | + "external-stdlib" : "@rescript/std" |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | +With this configuration set, compiled JS code will now point to the defined `external-stdlib` path: |
| 50 | + |
| 51 | +<CodeTab labels={["ReScript", "JavaScript"]}> |
| 52 | + |
| 53 | +```res |
| 54 | +Belt.Array.forEach([1, 2, 3], (num) => Js.log(num)) |
| 55 | +``` |
| 56 | + |
| 57 | +```js |
| 58 | +// Note the import path starting with "@rescript/std" |
| 59 | +import * as Array from "@rescript/std/lib/es6/belt_Array.mjs"; |
| 60 | + |
| 61 | +Belt_Array.forEach([ |
| 62 | + 1, |
| 63 | + 2, |
| 64 | + 3 |
| 65 | + ], (function (num) { |
| 66 | + console.log(num); |
| 67 | + |
| 68 | + })); |
| 69 | +``` |
| 70 | + |
| 71 | +</CodeTab> |
| 72 | + |
| 73 | +The JavaScript output above was compiled with an `es6` target, but will also work with `commonjs`. |
| 74 | + |
| 75 | +**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! |
| 76 | + |
| 77 | +To prevent unnecessary complications, only use this feature when... |
| 78 | +- You want to ship a library for JS / TS consumers without making them depend on `bs-platform` |
| 79 | +- You can't depend on `bs-platform` due to toolchain size (docker containers, low-storage deployment devices, etc) |
| 80 | + |
| 81 | +### Less Bundle Bloat when Adding ReScript |
| 82 | + |
| 83 | +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). |
| 84 | + |
| 85 | +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. |
| 86 | + |
| 87 | +We made a small [demo repo](https://github.com/bobzhang/zero-cost-rescript) and added the precompiled JS bundles to demonstrate what we've achieved. Check it out! |
| 88 | + |
| 89 | +### Improved Code Generation for Pattern Matching |
| 90 | + |
| 91 | +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](https://github.com/rescript-lang/rescript-compiler/issues/4924): |
| 92 | + |
| 93 | +```res |
| 94 | +type test = |
| 95 | + | NoArg |
| 96 | + | AnotherNoArg |
| 97 | + | OtherArg(int) |
| 98 | +
|
| 99 | +let test = x => |
| 100 | + switch x { |
| 101 | + | NoArg => true |
| 102 | + | _ => false |
| 103 | + } |
| 104 | +``` |
| 105 | + |
| 106 | +The snippet above will compile to the following JS output: |
| 107 | + |
| 108 | +<CodeTab labels={["9.0 Output", "8.4 Output" ]}> |
| 109 | + |
| 110 | +```js |
| 111 | +function test(x){ |
| 112 | + return x === 0 |
| 113 | +} |
| 114 | +``` |
| 115 | + |
| 116 | +```js |
| 117 | +function test(x) { |
| 118 | + if (typeof x === "number") { |
| 119 | + return x === 0; |
| 120 | + } else { |
| 121 | + return false; |
| 122 | + } |
| 123 | +} |
| 124 | +``` |
| 125 | + |
| 126 | + |
| 127 | +</CodeTab> |
| 128 | + |
| 129 | +As you can see, the 9.0 compiler removes all the unnecessary `typeof` checks! |
| 130 | + |
| 131 | +This is possible because our optimizer will try to analyze several predicates and get rid of redundant ones. More diffs can be found [here](https://github.com/rescript-lang/rescript-compiler/pull/4927/files?file-filters%5B%5D=.js). |
| 132 | + |
| 133 | +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. |
| 134 | + |
| 135 | +For those interested in the details, here is a representative diff resulting from this cleanup: |
| 136 | + |
| 137 | +```diff |
| 138 | +function is_space(param){ |
| 139 | +- var switcher = param - 9 | 0; |
| 140 | +- if (switcher > 4 || switcher < 0) { |
| 141 | +- return switcher == 23 ; |
| 142 | ++ if (param > 13 || param < 9) { |
| 143 | ++ return param === 32; |
| 144 | + } else { |
| 145 | +- return switcher !== 2; |
| 146 | ++ return param != 11; |
| 147 | + } |
| 148 | +} |
| 149 | +``` |
| 150 | + |
| 151 | +## Syntax Improvements |
| 152 | + |
| 153 | +### `when` -> `if` |
| 154 | + |
| 155 | +Starting from 9.0, [`when` clauses](/docs/manual/latest/pattern-matching-destructuring#when-clause) within a `switch` statement will automatically convert to the `if` keyword instead. |
| 156 | + |
| 157 | +<CodeTab labels={["New (9.0)", "Old (8.4)"]}> |
| 158 | + |
| 159 | +```res |
| 160 | +switch person1 { |
| 161 | +| Student({reportCard: {gpa}}) if gpa < 0.5 => |
| 162 | + Js.log("What's happening") |
| 163 | +| _ => () // do nothing |
| 164 | +} |
| 165 | +``` |
| 166 | + |
| 167 | +```res |
| 168 | +switch person1 { |
| 169 | +| Student({reportCard: {gpa}}) when gpa < 0.5 => |
| 170 | + Js.log("What's happening") |
| 171 | +| _ => () // do nothing |
| 172 | +} |
| 173 | +``` |
| 174 | + |
| 175 | +</CodeTab> |
| 176 | + |
| 177 | +The `when` keyword is deprecated. The syntax will continue supporting it and the formatter will automatically convert to `if`, for a pain-free upgrade. |
| 178 | + |
| 179 | +### Cleaner Polyvariant Syntax |
| 180 | + |
| 181 | +Polyvariants with invalid identifier names (e.g. names including hypens `-`), don't require any special escaping syntax anymore: |
| 182 | + |
| 183 | +<CodeTab labels={["New (9.0)", "Old (8.4)"]}> |
| 184 | + |
| 185 | +```res |
| 186 | +type animation = [ #"ease-in" | #"ease-out" ] |
| 187 | +``` |
| 188 | + |
| 189 | +```res |
| 190 | +type animation = [ #\"ease-in" | #\"ease-out" ] |
| 191 | +``` |
| 192 | + |
| 193 | +</CodeTab> |
| 194 | + |
| 195 | +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`). |
| 196 | + |
| 197 | + |
| 198 | +## Breaking Changes |
| 199 | + |
| 200 | +This release comes with a minor breaking change that shouldn't have much impact on the upgrade of existing codebases. |
| 201 | + |
| 202 | +### Nested Records within Objects |
| 203 | + |
| 204 | +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. |
| 205 | + |
| 206 | +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: |
| 207 | + |
| 208 | +<CodeTab labels={["9.0 Example", "8.4 Example" ]}> |
| 209 | + |
| 210 | +```res |
| 211 | +type user = { |
| 212 | + age: int |
| 213 | +} |
| 214 | +
|
| 215 | +let data = { |
| 216 | + "user": { |
| 217 | + age: 1 |
| 218 | + } |
| 219 | +} |
| 220 | +
|
| 221 | +// This is the way: `age` should be usable via record accessor |
| 222 | +let age = data["user"].age |
| 223 | +``` |
| 224 | + |
| 225 | +```res |
| 226 | +type user = { |
| 227 | + age: int |
| 228 | +} |
| 229 | +
|
| 230 | +let data = { |
| 231 | + "user": { |
| 232 | + age: 1 |
| 233 | + } |
| 234 | +} |
| 235 | +
|
| 236 | +// This was the problem: The record implicitly turned |
| 237 | +// into a ReScript object (which is confusing) |
| 238 | +let age = data["user"]["age"] |
| 239 | +``` |
| 240 | + |
| 241 | +</CodeTab> |
| 242 | + |
| 243 | +More discussions on this change can be found [here](https://forum.rescript-lang.org/t/fixing-the-semantics-of-nested-objects-breaking-changes/976). |
| 244 | + |
| 245 | +## Closing Note |
| 246 | + |
| 247 | +We only highlighted a few user-facing features, but there are also some pretty interesting internal changes happening right now. |
| 248 | + |
| 249 | +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. |
| 250 | + |
| 251 | +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. |
| 252 | + |
| 253 | + |
| 254 | +Happy Hacking! |
0 commit comments