From c8d90952dd66bd5a587be6f291689870504db89b Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Fri, 9 Oct 2026 16:05:07 +0200 Subject: [PATCH 1/2] fix(docs): align manual prose with ReScript v12 Correct string APIs, reserved names, Unicode support, removed Int64 types, pattern-matching diagnostics, dirname output, and the native binary path. Distinguish the current build system from legacy tooling and benchmarks. Validate generated API routes so broken links are no longer ignored. Fixes #1399 Part of #1421 --- apps/docs/markdown-pages/docs/manual/api.mdx | 2 +- .../docs/manual/bind-to-js-function.mdx | 2 +- .../docs/manual/build-performance.mdx | 48 +++++------ .../manual/pattern-matching-destructuring.mdx | 10 +-- .../docs/manual/primitive-types.mdx | 4 +- .../docs/manual/reserved-keywords.mdx | 54 ++----------- .../docs/manual/shared-data-types.mdx | 6 +- apps/docs/package.json | 2 +- .../scripts/__tests__/test-hrefs.test.mjs | 80 +++++++++++++++++++ apps/docs/scripts/test-hrefs.mjs | 25 +++++- 10 files changed, 147 insertions(+), 86 deletions(-) create mode 100644 apps/docs/scripts/__tests__/test-hrefs.test.mjs diff --git a/apps/docs/markdown-pages/docs/manual/api.mdx b/apps/docs/markdown-pages/docs/manual/api.mdx index c46915dea..c2fd77001 100644 --- a/apps/docs/markdown-pages/docs/manual/api.mdx +++ b/apps/docs/markdown-pages/docs/manual/api.mdx @@ -17,4 +17,4 @@ When possible it will compile directly to the equivalent JavaScript API, for exa - [WebAPI](https://rescript-lang.github.io/experimental-rescript-webapi/): Experimental, but we expect it to be stable very soon. Similar to how the Stdlib allows interacting with move of the JavaScript API this library allows you to work with Web APIs like `fetch`, `navigator`, `localStorage`, etc... - [Belt](/docs/manual/api/belt): Immutable collections and extra helpers not available in JavaScript / [Stdlib](/docs/manual/api/stdlib). -- [Dom](/docs/manual/api/stdlibdom): Deprecated DOM-related types and modules. You should use the `WebAPI` library instead. +- [Dom](/docs/manual/api/dom): Deprecated DOM-related types and modules. You should use the `WebAPI` library instead. diff --git a/apps/docs/markdown-pages/docs/manual/bind-to-js-function.mdx b/apps/docs/markdown-pages/docs/manual/bind-to-js-function.mdx index f7d319961..3cbe88ef5 100644 --- a/apps/docs/markdown-pages/docs/manual/bind-to-js-function.mdx +++ b/apps/docs/markdown-pages/docs/manual/bind-to-js-function.mdx @@ -15,7 +15,7 @@ Binding a JS function is like binding any other value: ```res // Import nodejs' path.dirname @module("path") external dirname: string => string = "dirname" -let root = dirname("/User/github") // returns "User" +let root = dirname("/User/github") // returns "/User" ``` ```js diff --git a/apps/docs/markdown-pages/docs/manual/build-performance.mdx b/apps/docs/markdown-pages/docs/manual/build-performance.mdx index 7af0ea73a..f1097f4a8 100644 --- a/apps/docs/markdown-pages/docs/manual/build-performance.mdx +++ b/apps/docs/markdown-pages/docs/manual/build-performance.mdx @@ -11,9 +11,23 @@ order: 7 ReScript considers performance at install time, build time and run time as a serious feature; it's one of those things you don't notice until you realize it's missing. -## Profile Your Build +## Under the Hood + +Since ReScript v12, `rescript build` uses a native build system written in Rust. It reads `rescript.json`, tracks dependencies between modules and packages, and invokes the compiler directly. See [Reforging the ReScript Build System](../../blog/reforging-build-system.mdx) for more about its architecture. + +## The JS Wrapper + +The `rescript` command is a thin Node.js wrapper around the native `rescript.exe` binary. Both building and watching are handled by the native binary. -Sometime your build can be slow due to some confused infra setups. We provide an interactive visualization of your build's performance via `bstracing`: +The binary ships in a platform-specific package at `node_modules/@rescript/{your-platform}/bin/rescript.exe`, for example `node_modules/@rescript/darwin-arm64/bin/rescript.exe` on an Apple Silicon Mac. The wrapper selects the package for your platform and sets `RESCRIPT_RUNTIME` to the installed runtime package's path. Prefer `rescript build` and `rescript watch` so this setup is handled for you. + +## Legacy Build System + +The pre-v12 build system remains available through `rescript-legacy`. It uses [Ninja](https://ninja-build.org), a cross-platform, low-level build system. It reads `rescript.json`, generates Ninja build files in `lib/bs`, and runs Ninja to execute the compiler commands. + +### Profile Legacy Builds + +For builds made with `rescript-legacy`, `bstracing` turns Ninja's build log into a performance trace: ```sh ./node_modules/.bin/bstracing @@ -28,29 +42,17 @@ Run the above command at your ReScript project's root; it'll spit out a JSON fil caption="Screenshot of bstracing result" /> -## Under the Hood - -ReScript itself uses a build system under the hood, called [Ninja](https://ninja-build.org). Ninja is like Make, but cross-platform, minimal, focuses on perf and destined to be more of a low-level building block than a full-blown build system. In this regard, Ninja's a great implementation detail for `rescript`. - -ReScript reads into `rescript.json` and generates the Ninja build file in `lib/bs`. The file contains the low-level compiler commands, namespacing rules, intermediate artifacts generation & others. It then runs `ninja` for the actual build. - -## The JS Wrapper - -`rescript` itself is a Node.js wrapper which takes care of some miscellaneous tasks, plus the watcher. The lower-level, watcher-less, fast native `rescript` is called `rescript.exe`. It's located at `node_modules/rescript/{your-platform}/rescript.exe`. +### Historical Measurements -If you don't need the watcher, you can run said `rescript.exe`. This side-steps Node.js' long startup time, which can be in the order of `100ms`. Our editor plugin finds and uses this native `rescript.exe` for better performance. +The following measurements were made with the pre-v12 Ninja-based build system. They are not benchmarks of the current build system. -## Numbers +A native build of a small project took around `70ms`, compared with roughly twice that through the old JS wrapper. That wrapper also provided watch mode. -Raw `rescript.exe` build on a small project should be around `70ms`. This doubles when you use the JS `rescript` wrapper which comes with a watcher, which is practically faster since you don't manually run the build at every change (though you should opt for the raw `rescript.exe` for programmatic usage, e.g. inserting rescript into your existing JS build pipeline). +A no-op build (when no file changed) took around `15ms`. An incremental rebuild of a single file took around `70ms` too. -No-op build (when no file's changed) should be around `15ms`. Incremental rebuild (described soon) of a single file in a project is around `70ms` too. +#### Extreme Test -Cleaning the artifacts should be instantaneous. - -### Extreme Test - -We've stress-tested `rescript.exe` on a big project of 10,000 files (2 directories, 5000 files each, first 5000 no dependencies, last 5000 10 dependencies on files from the former directory) using https://github.com/rescript-lang/build-benchmark, on a Retina Macbook Pro Early 2015 (3.1 GHz Intel Core i7). +We stress-tested the old native build system on a big project of 10,000 files (2 directories, 5000 files each, first 5000 no dependencies, last 5000 10 dependencies on files from the former directory) using https://github.com/rescript-lang/build-benchmark, on a Retina Macbook Pro Early 2015 (3.1 GHz Intel Core i7). {/* TODO: better repro */} @@ -58,12 +60,6 @@ We've stress-tested `rescript.exe` on a big project of 10,000 files (2 directori - Clean build: \<3 minutes. - Incremental build: depends on the number of the dependents of the file. No dependent means `1s`. -### Stability - -`rescript` is a file-based build system. We don't do in-memory build, even if that speeds up the build a lot. In-memory builds risk memory leaks, out-of-memory errors, corrupt halfway build and others. Our watcher mode stays open for days or months with no leak. - -The watcher is also just a thin file watcher that calls `rescript.exe`. We don't like babysitting daemon processes. - ## Incrementality & Correctness ReScript doesn't take whole seconds to run every time. The bulk of the build performance comes from incremental build, aka re-building a previously built project when a few files changed. diff --git a/apps/docs/markdown-pages/docs/manual/pattern-matching-destructuring.mdx b/apps/docs/markdown-pages/docs/manual/pattern-matching-destructuring.mdx index e1b852d8d..04393a62a 100644 --- a/apps/docs/markdown-pages/docs/manual/pattern-matching-destructuring.mdx +++ b/apps/docs/markdown-pages/docs/manual/pattern-matching-destructuring.mdx @@ -121,7 +121,7 @@ var n = "John"; -You _can_ in theory destructure array and list at the top level too: +Array and list patterns can be non-exhaustive. These destructuring bindings are rejected at the module's top level: ```res nocheck let myArray = [1, 2, 3] @@ -133,7 +133,7 @@ let list{head, ...tail} = myList // 1 assigned to `head`, `list{2, 3}` assigned to tail ``` -But the array example is **highly disrecommended** (use tuple instead) and the list example will error on you. They're only there for completeness' sake. As you'll see below, the proper way of using destructuring array and list is using `switch`. +Both examples fail with `Such fragile pattern not allowed in the toplevel`: the array may have a different length, and the list may be empty. Use a tuple for a fixed number of elements, or use `switch` to destructure arrays and lists while handling the remaining cases. ## `switch` Based on Shape of Data @@ -1612,9 +1612,9 @@ Did you see what we removed? This time, we've omitted the handling of the case w Failing to handle every scenario of a value likely constitutes the majority of program bugs out there. This happens very often when you refactor a piece of code someone else wrote. Fortunately for ReScript, the compiler will tell you so: ``` -Warning 8: this pattern-matching is not exhaustive. -Here is an example of a value that is not matched: -Some({name: ""}) +Warning number 8 +You forgot to handle a possible case here, for example: +| Teacher({name: "", _}) ``` **BAM**! You've just erased an entire category of important bugs before you even ran the code. In fact, this is how most of nullable values is handled: diff --git a/apps/docs/markdown-pages/docs/manual/primitive-types.mdx b/apps/docs/markdown-pages/docs/manual/primitive-types.mdx index 131a2a8eb..4355fc55e 100644 --- a/apps/docs/markdown-pages/docs/manual/primitive-types.mdx +++ b/apps/docs/markdown-pages/docs/manual/primitive-types.mdx @@ -109,7 +109,7 @@ The more you overload the poor string type, the less the type system (or a teamm ## Char -ReScript has a type for a string with a single letter: +ReScript has a legacy `char` type: @@ -127,7 +127,7 @@ export { firstLetterOfAlphabet }; **Note**: Char doesn't support Unicode or UTF-8 and is therefore not recommended. -To convert a String to a Char, use `String.get("a", 0)`. To convert a Char to a String, use `String.make(1, 'a')`. +Prefer strings and the `String` module for text. `String.get("a", 0)` returns `Some("a")`, an `option`, rather than a `char`. It returns `None` for an out-of-range index. `String.make` converts a single value to a string: `String.make(1)` returns `"1"`. To repeat a string, use `String.repeat("a", 3)`, which returns `"aaa"`. ## Regular Expression diff --git a/apps/docs/markdown-pages/docs/manual/reserved-keywords.mdx b/apps/docs/markdown-pages/docs/manual/reserved-keywords.mdx index 5d392077d..52eb03fd4 100644 --- a/apps/docs/markdown-pages/docs/manual/reserved-keywords.mdx +++ b/apps/docs/markdown-pages/docs/manual/reserved-keywords.mdx @@ -15,68 +15,28 @@ order: 27 - `and` - `as` - `assert` - -{/* - `begin` */} - -{/* - `class` */} - +- `await` - `constraint` - -{/* - `do` */} -{/* - `done` */} - - `else` - {/* - `end` */} - {/* - `esfun` */} - `exception` - `external` - -* `false` -* `for` - {/* - `fun` */} - {/* - `function` */} - {/* - `functor` */} - +- `false` +- `for` - `if` - `in` - `include` - {/* - `inherit` */} - {/* - `initializer` */} - -* `lazy` -* `let` - +- `let` - `module` - `mutable` - -{/* - `new` */} -{/* - `nonrec` */} - -{/* - `object` */} - - `of` - `open` - {/* - `or` */} - -{/* - `pri` */} -{/* - `pub` */} - +- `private` - `rec` - -{/* - `sig` */} -{/* - `struct` */} - - `switch` - -{/* - `then` */} - - `true` - `try` - `type` - -{/* - `val` */} -{/* - `virtual` */} - - `when` - `while` -- `with` + +`async`, `break`, and `continue` are recognized in specific syntactic contexts, but can also be used as binding names. `list` and `dict` introduce literals when followed immediately by `{`; they can otherwise be used as names. `lazy` and `with` are not reserved. diff --git a/apps/docs/markdown-pages/docs/manual/shared-data-types.mdx b/apps/docs/markdown-pages/docs/manual/shared-data-types.mdx index bc00c31c3..1e3d248cd 100644 --- a/apps/docs/markdown-pages/docs/manual/shared-data-types.mdx +++ b/apps/docs/markdown-pages/docs/manual/shared-data-types.mdx @@ -16,8 +16,9 @@ ReScript values compile to their JavaScript equivalents directly, so **no data c **Shared, bidirectionally usable types**: -- String. ReScript strings are JavaScript strings, vice-versa. (Caveat: only our backtick string `` `hello 👋 ${personName}` `` supports unicode and interpolation). +- String. ReScript strings are JavaScript strings, vice-versa. Both double-quoted strings (`"hello 👋"`) and backtick strings support Unicode. Backtick strings also support interpolation, such as `` `hello 👋 ${personName}` ``. - Float. ReScript floats are JavaScript numbers, vice-versa. +- Big integer. ReScript `bigint` values, such as `42n`, are JavaScript big integers. Use the [BigInt API](/docs/manual/api/stdlib/bigint) for operations on them. - Array. Use the [Array API](/docs/manual/api/stdlib/array) for array operations. - Tuple. Compiles to an array at runtime. You can treat a fixed-sized, heterogenous JavaScript array as a ReScript tuple too. - Boolean. @@ -39,10 +40,11 @@ ReScript values compile to their JavaScript equivalents directly, so **no data c **Non-shared types (aka internal types)**: - Character. -- Int64. - Lazy values. - Everything else. Many of these are stable, which means that you can still serialize/deserialize them as-is without manual conversions. But we discourage actively peeking into their structure otherwise. These types require manual conversions if you want to export them for JavaScript consumption. For a seamless JavaScript/TypeScript integration experience, check out the [TypeScript Integration](./typescript-integration.mdx) page instead of doing conversions by hand. + +ReScript v12 no longer provides the legacy `int64` type or `Int64` module. Use `bigint` for large integers instead. diff --git a/apps/docs/package.json b/apps/docs/package.json index 1351b117e..e7350a978 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -19,7 +19,7 @@ "ci:homepage-performance": "node --test scripts/__tests__/homepage-compiler.test.mjs && node scripts/homepage-performance.mjs", "ci:test": "vitest --run --browser.headless", "ci:test:e2e": "cypress run --config-file cypress.homepage.config.mjs --browser chrome", - "ci:test:scripts": "yarn build:scripts && vitest run --config vitest.scripts.config.mjs && node --test scripts/__tests__/generate-llms.test.mjs scripts/__tests__/test-examples.test.mjs scripts/__tests__/test-runner.test.mjs", + "ci:test:scripts": "yarn build:scripts && vitest run --config vitest.scripts.config.mjs && node --test scripts/__tests__/generate-llms.test.mjs scripts/__tests__/test-examples.test.mjs scripts/__tests__/test-hrefs.test.mjs scripts/__tests__/test-runner.test.mjs", "ci:test:visual": "VISUAL_TESTS=1 vitest --run --browser.headless", "ci:test:visual:update": "VISUAL_TESTS=1 VISUAL_BASELINE_UPDATE=1 vitest --run --browser.headless --update", "clean:res": "rescript clean", diff --git a/apps/docs/scripts/__tests__/test-hrefs.test.mjs b/apps/docs/scripts/__tests__/test-hrefs.test.mjs new file mode 100644 index 000000000..b9d9287d5 --- /dev/null +++ b/apps/docs/scripts/__tests__/test-hrefs.test.mjs @@ -0,0 +1,80 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { execFileSync, spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const script = fileURLToPath(new URL("../test-hrefs.mjs", import.meta.url)); + +const check = (markdown) => { + const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "rescript-hrefs-")); + try { + // remark-validate-links discovers the repository root and URL through Git. + execFileSync("git", ["init", "--quiet"], { cwd: fixture }); + execFileSync( + "git", + [ + "remote", + "add", + "origin", + "https://github.com/rescript-lang/rescript-lang.org.git", + ], + { cwd: fixture }, + ); + const apiDir = path.join(fixture, "markdown-pages/docs/api"); + const manualDir = path.join(fixture, "markdown-pages/docs/manual"); + fs.mkdirSync(apiDir, { recursive: true }); + fs.mkdirSync(manualDir, { recursive: true }); + for (const library of ["stdlib", "belt", "dom"]) { + fs.writeFileSync( + path.join(apiDir, `${library}.json`), + JSON.stringify({ [library]: {}, [`${library}/example`]: {} }), + ); + } + fs.writeFileSync(path.join(manualDir, "api.mdx"), markdown); + return spawnSync(process.execPath, [script], { + cwd: fixture, + env: { ...process.env, CI: "1" }, + encoding: "utf8", + }); + } finally { + fs.rmSync(fixture, { recursive: true, force: true }); + } +}; + +test("accepts API routes generated from the JSON data", () => { + const result = check(` +[Stdlib](/docs/manual/api/stdlib) +[Dom](/docs/manual/api/dom) +[Nested module](/docs/manual/api/belt/example/) +[API entry](/docs/manual/api/stdlib/example#value-get) +`); + assert.equal(result.status, 0, result.stdout + result.stderr); + assert.match(result.stdout, /0 issues found/); +}); + +test("rejects the broken DOM route even beside valid API links", () => { + const result = check(` +[Stdlib](/docs/manual/api/stdlib) +[Dom](/docs/manual/api/stdlibdom) +`); + assert.equal(result.status, 1, result.stdout + result.stderr); + assert.match(result.stdout, /Cannot find file.*stdlibdom/); +}); + +test("rejects API module typos and unrelated missing pages", () => { + for (const href of [ + "/docs/manual/api/stdlib/typo", + "/docs/manual/missing-page", + "./nested/docs/manual/api/dom", + ]) { + const result = check(` +[Valid API](/docs/manual/api/dom) +[Missing page](${href}) +`); + assert.equal(result.status, 1, result.stdout + result.stderr); + assert.match(result.stdout, /Cannot find file/); + } +}); diff --git a/apps/docs/scripts/test-hrefs.mjs b/apps/docs/scripts/test-hrefs.mjs index ea7ae6491..b311aeb07 100644 --- a/apps/docs/scripts/test-hrefs.mjs +++ b/apps/docs/scripts/test-hrefs.mjs @@ -4,6 +4,23 @@ import { read } from "to-vfile"; import { reporter } from "vfile-reporter"; import * as fs from "fs/promises"; +// API pages are generated from JSON rather than Markdown files. Match their +// actual routes instead of suppressing every warning containing "api/". +const apiPaths = new Set([ + "docs/manual/api", + "docs/manual/api/introduction", + ...( + await Promise.all( + ["stdlib", "belt", "dom"].map(async (library) => { + const data = JSON.parse( + await fs.readFile(`markdown-pages/docs/api/${library}.json`, "utf8"), + ); + return Object.keys(data).map((key) => `docs/manual/api/${key}`); + }), + ) + ).flat(), +]); + const files = new Set( ...[await fs.readdir("markdown-pages", { recursive: true })], ); @@ -22,6 +39,13 @@ for (const file of files) { .use(remarkValidateLinks) .process(await read("markdown-pages/" + file)); + result.messages = result.messages.filter((message) => { + const target = message.reason.match( + /^Cannot find (?:file `|heading for `#[^`]+` in `)(?:\.\.\/)*(docs\/manual\/api(?:\/[^`]*)?)`/, + )?.[1]; + return !target || !apiPaths.has(target.replace(/\/$/, "")); + }); + const log = reporter(result, { quiet: true }); const warningMessage = log.replace(file, ""); @@ -47,7 +71,6 @@ for (const file of files) { if ( log && !allMissingExistInPublic && - !warningMessage.includes("api/") && // When running on CI it fails to ignore the link directly to the blog root // https://github.com/rescript-lang/rescript-lang.org/actions/runs/19520461368/job/55882556586?pr=1115#step:6:338 !warningMessage.includes("`../../blog`") && From 91ac3ce9f07738333700d572eef4c51dd553290a Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Fri, 9 Oct 2026 20:48:54 +0200 Subject: [PATCH 2/2] docs: assert the dirname example result Replace the return-value comment with an executable assertion and regenerate its JavaScript output. Verify the generated assertion passes for the documented result and rejects an incorrect expectation. Fixes #1399 --- .../docs/manual/bind-to-js-function.mdx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/docs/markdown-pages/docs/manual/bind-to-js-function.mdx b/apps/docs/markdown-pages/docs/manual/bind-to-js-function.mdx index 3cbe88ef5..75552c7e6 100644 --- a/apps/docs/markdown-pages/docs/manual/bind-to-js-function.mdx +++ b/apps/docs/markdown-pages/docs/manual/bind-to-js-function.mdx @@ -15,7 +15,8 @@ Binding a JS function is like binding any other value: ```res // Import nodejs' path.dirname @module("path") external dirname: string => string = "dirname" -let root = dirname("/User/github") // returns "/User" +let root = dirname("/User/github") +assert(root === "/User") ``` ```js @@ -23,6 +24,14 @@ import * as Path from "path"; let root = Path.dirname("/User/github"); +if (root !== "/User") { + throw { + RE_EXN_ID: "Assert_failure", + _1: ["Example.res", 4, 0], + Error: new Error(), + }; +} + export { root }; ```