Skip to content

Commit 369f3dd

Browse files
authored
Fix BigInt serialization through web worker postMessage (#2071)
2 parents bb54345 + 199ff81 commit 369f3dd

File tree

6 files changed

+1383
-12
lines changed

6 files changed

+1383
-12
lines changed

.github/workflows/deploy_branches.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ jobs:
5151
eval $(opam env)
5252
export OPAMYES=1
5353
opam clean --all-switches --download-cache --logs --repo-cache --unused-repositories
54+
- name: Setup zarith native BigInt runtime
55+
run: opam exec -- make setup-zarith
56+
working-directory: ./source
5457
- name: Build Release
5558
run: |
5659
export DUNE_CACHE=enabled

Makefile

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,23 @@ TEST_DIR="$(shell pwd)/_build/default/test"
22
HTML_DIR="$(shell pwd)/_build/default/src/web/www"
33
SERVER="http://0.0.0.0:8000/"
44

5-
.PHONY: all deps change-deps setup-instructor setup-student dev dev-helper dev-student fmt watch watch-release release release-student grade echo-html-dir serve serve2 repl test clean
5+
.PHONY: all deps change-deps setup-instructor setup-student dev dev-helper dev-student fmt watch watch-release release release-student grade echo-html-dir serve serve2 repl test clean setup-zarith
66

77
all: dev
88

9+
# Install native BigInt runtime for zarith_stubs_js to fix WebWorker postMessage serialization.
10+
# The vendored runtime.js uses native JS BigInt (from zarith_stubs_js v0.17.0) which survives
11+
# structured clone, unlike the BigInteger.js library used in older versions.
12+
setup-zarith:
13+
@echo "Installing native BigInt zarith runtime..."
14+
@cp vendor/zarith_native_bigint_runtime.js "$$(opam var lib)/zarith_stubs_js/runtime.js"
15+
916
deps:
1017
opam repo add archive git+https://github.com/ocaml/opam-repository-archive
1118
opam update
1219
opam install ./hazel.opam.locked --deps-only --with-test --with-doc
1320
npm install
21+
$(MAKE) setup-zarith
1422

1523
change-deps:
1624
opam update
@@ -25,7 +33,7 @@ setup-instructor:
2533
setup-student:
2634
cp src/web/exercises/settings/ExerciseSettings_student.re src/web/exercises/settings/ExerciseSettings.re
2735

28-
dev-helper:
36+
dev-helper: setup-zarith
2937
dune fmt --auto-promote || true
3038
dune build @ocaml-index @src/fmt --auto-promote src --profile dev
3139

@@ -36,16 +44,16 @@ dev-student: setup-student dev-helper
3644
fmt:
3745
dune fmt --auto-promote
3846

39-
watch: setup-instructor
47+
watch: setup-instructor setup-zarith
4048
dune build @ocaml-index @src/fmt --auto-promote src --profile dev --watch
4149

42-
watch-release: setup-instructor
50+
watch-release: setup-instructor setup-zarith
4351
dune build @src/fmt --auto-promote src --profile release --watch
4452

45-
release: setup-instructor
53+
release: setup-instructor setup-zarith
4654
dune build @src/fmt --auto-promote src --profile release
4755

48-
release-student: setup-student
56+
release-student: setup-student setup-zarith
4957
dune build @src/fmt --auto-promote src --profile dev # Uses dev profile for performance reasons. It may be worth it to retest since the ocaml upgrade
5058

5159
grade:
@@ -86,7 +94,7 @@ coverage:
8694
dune runtest --instrument-with bisect_ppx --force
8795
bisect-ppx-report summary
8896

89-
ci:
97+
ci: setup-zarith
9098
dune build --profile dev
9199
dune runtest --instrument-with bisect_ppx --force
92100

docs/bigint-webworker-fix.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# BigInt WebWorker Serialization Fix
2+
3+
This document explains the vendored zarith runtime and why it's needed.
4+
5+
**Vendored file:** `vendor/zarith_native_bigint_runtime.js`
6+
**Source:** `zarith_stubs_js` v0.17.0
7+
**Purpose:** Fixes BigInt serialization through WebWorker `postMessage`
8+
9+
### The Problem
10+
11+
Hazel uses a WebWorker for evaluation to avoid blocking the UI. Data (including
12+
the AST with BigInt values) is sent to/from the worker via `postMessage`, which
13+
uses the browser's [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm).
14+
15+
The `zarith_stubs_js` package (v0.16.x) that provides BigInt support for
16+
js_of_ocaml uses the [BigInteger.js](https://github.com/peterolson/BigInteger.js)
17+
library internally. BigInteger.js creates custom JavaScript objects with
18+
prototype methods like `.lt()`, `.add()`, etc. When these objects pass through
19+
structured clone, **the prototype chain is lost** - the data survives but the
20+
methods don't. This caused crashes when displaying evaluation results containing
21+
large integers.
22+
23+
### The Solution
24+
25+
Starting in v0.17.0, `zarith_stubs_js` switched to using **native JavaScript
26+
`BigInt`** instead of BigInteger.js. Native `BigInt` is a primitive type that
27+
fully survives structured clone.
28+
29+
However, we cannot simply upgrade to zarith_stubs_js v0.17.0 because it requires
30+
upgrading the Jane Street packages. At the time of this fix (January 2026),
31+
bonsai v0.17.0 on opam constrains `js_of_ocaml < 5.7.0`, and versions of
32+
`js_of_ocaml-compiler` before 5.7.0 require `ocaml < 5.2`. Note that bonsai's
33+
master branch on GitHub has updated to require `js_of_ocaml >= 6.0.0`, so a
34+
future bonsai release should resolve this constraint issue.
35+
36+
### Our Approach
37+
38+
We vendor the `runtime.js` from zarith_stubs_js v0.17.0 and copy it into the
39+
opam switch at build time, replacing the v0.16.x version. This gives us native
40+
BigInt support without upgrading the package.
41+
42+
The `Makefile` has a `setup-zarith` target that performs this copy:
43+
44+
```makefile
45+
setup-zarith:
46+
cp vendor/zarith_native_bigint_runtime.js "$$(opam var lib)/zarith_stubs_js/runtime.js"
47+
```
48+
49+
This target is automatically run before builds (`make dev`, `make release`, etc.)
50+
and in CI.
51+
52+
### Alternatives Considered
53+
54+
1. **Full package upgrade** - Blocked by OCaml version / js_of_ocaml constraints
55+
as described above.
56+
57+
2. **O(n) serialization shim** - Our previous approach: recursively walk the
58+
entire message payload before/after `postMessage`, converting BigInts to
59+
tagged strings (`{__hazel_bigint__: "123"}`) and back. This worked but added
60+
overhead proportional to message size (4 traversals per worker round-trip).
61+
62+
### Future Considerations
63+
64+
- **When a new bonsai release is published to opam** (with the js_of_ocaml >= 6.0.0
65+
constraint from master), we should be able to remove this vendored file and do
66+
a proper upgrade. At that point:
67+
1. Upgrade zarith_stubs_js to v0.17.0+ via opam
68+
2. Remove `vendor/zarith_native_bigint_runtime.js`
69+
3. Remove the `setup-zarith` target from the Makefile
70+
4. Remove the `setup-zarith` dependencies from other Makefile targets
71+
72+
- **If zarith_stubs_js changes its internal representation again**, we may need
73+
to update the vendored file. Check the [zarith_stubs_js releases](https://github.com/janestreet/zarith_stubs_js)
74+
for changes.
75+
76+
- **The vendored runtime.js API is stable** - it implements the same `ml_z_*`
77+
functions as the original, just with native BigInt instead of BigInteger.js.
78+
There should be no compatibility issues.
79+
80+
### Related Files
81+
82+
- `vendor/zarith_native_bigint_runtime.js` - The vendored runtime file
83+
- `src/web/util/WorkerClient.re` - Client-side worker communication
84+
- `src/web/util/WorkerServer.re` - Worker-side message handling
85+
- `Makefile` - `setup-zarith` target
86+
- `.github/workflows/deploy_branches.yml` - CI setup-zarith step

src/web/dune

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
(library
99
(name workerServer)
1010
(modules WorkerServer)
11+
(wrapped false)
1112
(instrumentation
1213
(backend bisect_ppx))
1314
(libraries language)
1415
(js_of_ocaml)
1516
(preprocess
16-
(pps ppx_yojson_conv ppx_sexp_conv ppx_deriving.show)))
17+
(pps js_of_ocaml-ppx ppx_yojson_conv ppx_sexp_conv ppx_deriving.show)))
1718

1819
(library
1920
(name web)

src/web/util/WorkerServer.re

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ let work = (req_value: Request.value): Response.value => {
4646
};
4747
};
4848

49-
let on_request = (req: Request.t): unit =>
50-
req
51-
|> List.map(((k, v)) => (k, work(v)))
52-
|> Js_of_ocaml.Worker.post_message;
49+
let on_request = (req: Request.t): unit => {
50+
let resp: Response.t = req |> List.map(((k, v)) => (k, work(v)));
51+
Js_of_ocaml.Worker.post_message(resp);
52+
};
5353

5454
let start = () => Js_of_ocaml.Worker.set_onmessage(on_request);

0 commit comments

Comments
 (0)