Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/markdown-pages/docs/manual/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
11 changes: 10 additions & 1 deletion apps/docs/markdown-pages/docs/manual/bind-to-js-function.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,23 @@ 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
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 };
```

Expand Down
48 changes: 22 additions & 26 deletions apps/docs/markdown-pages/docs/manual/build-performance.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,42 +42,24 @@ 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 */}

- No-op build of 10k files: `800ms` (the minimum amount of time required to check the mtimes of 10k files).
- 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.
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/markdown-pages/docs/manual/primitive-types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:

<CodeTab labels={["ReScript", "JS Output"]}>

Expand All @@ -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<string>`, 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

Expand Down
54 changes: 7 additions & 47 deletions apps/docs/markdown-pages/docs/manual/reserved-keywords.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
6 changes: 4 additions & 2 deletions apps/docs/markdown-pages/docs/manual/shared-data-types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
2 changes: 1 addition & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
80 changes: 80 additions & 0 deletions apps/docs/scripts/__tests__/test-hrefs.test.mjs
Original file line number Diff line number Diff line change
@@ -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/);
}
});
25 changes: 24 additions & 1 deletion apps/docs/scripts/test-hrefs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 })],
);
Expand All @@ -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, "");
Expand All @@ -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`") &&
Expand Down
Loading