Hazel reports · ^report(34) · Hazel as a JS library · 2026-10-26 · work in progress · after ^report(33)
An Official Hazel JS Library?
Since 2026-10-20 a game in daily use, Messed Up Dress Up (MUDU), runs its rules in Hazel through a draft library, hazel-lib.js (#2711, with #2719 stacked on it). Should Hazel make such a library official: a supported way for any web page to analyze, run and show Hazel programs? This report lays out what the draft does, what the game has taught us, the uses beyond it, and the costs.
Short answers.
What it is today: one script, 643 KB over the wire (2.7 MB unpacked; the full app is 1.7 MB), that puts
window.Hazelon a page:analyzeandruna text,analyzeProgramandrunProgramfor a program of named modules, with Hazel values coming back as plain JS values and JS functions going in and out both ways. Plus an embed mode: Hazel's own tiles in an<iframe>, showing (and, on a further branch, editing) a program the page posts.For: it already works for a real, typed, event-driven use; it is small to keep (one OCaml module and a doc over the existing evaluator); and it opens Hazel to every page that wants live, typed, hole-tolerant code: lessons, docs, games, other editors.
Against: an API is a promise. It fixes how Hazel values look in JS, ties pages to builds that today are branch URLs with no versions, runs on the page's own thread, and puts an evaluator in pages that may run code they did not write.
Suggestion: make it official in two steps: first merge #2711 and #2719 as an experimental library (labelled so, built on
dev), then promise stability only for the small core (analyze,run, the value mapping) once a second user besides MUDU has used it.
Update, 2026-11-02: a first npm release, hazeljs 0.0.1, built from #2719 at 0d66c36618. For Hazel's core team: beyond dev it carries only the library's own 11 commits, from #2711 and #2719. It has no modular editors (#2469), no livelits, and none of the performance PRs still open against dev. Three of those commits touch Hazel's core, and the release is 24 PRs behind today's dev (below).
What the draft library does#
| Call | What it gives |
|---|---|
Hazel.analyze(text) | {errors}: the program's error messages. |
Hazel.run(text) | {errors, value}. Errors do not stop a run: Hazel evaluates around its holes, so the page decides which errors it accepts. |
Hazel.analyzeProgram(spec), runProgram(spec) | A program from named modules' texts (one editor each), a preamble and a tail; errors, holes and unfinished brackets per module; ok when there are none. |
| Values | numbers, booleans, strings; lists as arrays; () as null; labeled tuples as objects; constructors as {tag, value}; functions as JS functions, called with converted arguments. |
Embed mode (index.html#embed) | Hazel's own tiles and error marks for a program the page posts, read-only on #2719; selectable and editable, with themes, fonts, line numbers and go-to-definition, on the embed-editable branch (12 commits, no PR yet). |
Sources: docs/hazel-as-a-js-library.md on #2719's branch; src/web/HazelLibCore.re; 17 test cases (HazelLib), under node with the rest of the suite. Sizes measured 2026-10-26 from hazel.org's branch builds.
What the game has shown#
MUDU's 📜 Scripting panel gives five kinds of room a rule each: a function from the game's event (a lever pulled, a way taken) to a tree of commands the game carries out (^report(33) describes the rules and their types). The same rules can be written in Hazel (the default), in JS, or in Fumola, which makes a fair comparison.
- Types earn their keep. The rules' vocabulary is sums (
Pull(L),Way(N), a room's exits), checked where a rule's parameter is annotated, so a rule that returns the wrong command is an error in the editor, not a bug in play. The JS version of the same rule has no such check. - Holes are a feature in a game. A rule half written still runs: the game falls back to its built-in behaviour while
okis false, so kids can edit while they play. - Calls cost more than they should.
^report(25)took one call apart: the World room's rule, 40 ms a call at a 16×16 map against 0.003 ms for the same rule in JS. Every call pays a floor that grows with its argument (8.5 ms at 16×16), because the library turns the JS argument into Hazel syntax and evaluates it back into the value it already was; and a typed helper re-checked its annotation on every step, which #2681 removes (144 ms to 20.6 ms). Passing the rule only what it needs made it flat (0.9 ms). - Reading can be slow. Reading the program is 21 to 81 ms for the panel's modules (well formed), but up to 1.8 s when a module has an unclosed bracket, because only the recovering parser can read it (its time grows with the square of the module's length). The first load of the library and its first analysis took about a second (the game's own log: "analyzed and run in 1148 ms").
- What the game had to build itself: the outline of a program (modules, types, functions) from regexes over the text, and which row a click lands on. Modular editors (#2469) already know these; the open question on #2711 is how the library should take #2469 as a dependency.
- Gaps found by use: constructors declared in one module were not usable in the next ("Constructor is not defined"), fixed separately by #2729; and a constructor from JS whose tag the sum lacks, which a fix kept on one laptop (
js-library-tagcheck) makes stay stuck under its ascription.
Try it: the trap room, on this page#
Added 2026-11-03, a first version of the live trap room (^ticket(675)). This is the game itself, Messed Up Dress Up 0.0.475 or later, opened in the castle's trap room with its 📜 Scripting tab showing (the link ?try=trap, ^ticket(676)). Its Hazel comes from the npm package, hazeljs 0.0.1 (since 0.0.476, ^ticket(674)). Pull a red lever, or walk onto a pit, and the room's Hazel rule, TrapRoom's on_event, says what happens; edit the rule in the tab, and the next pull answers the edited rule. Nothing done here is saved: a game you play on this site is as you left it.
^ticket(677), ^ticket(678)).Uses beyond the game#
| Use | What the library gives it |
|---|---|
| Teaching pages and courses | Exercises checked in the student's page: analyze for type errors, run for tests, without hosting the whole Hazel app. |
| Docs and papers with live examples | An example's value computed from its text where it is shown, so it cannot go stale; the embed frame to show it as tiles. |
| Other games and toys | MUDU's pattern, rules as typed functions from events to commands, for any game that wants players to script it. |
| Other editors | A notebook, a slide tool or a web IDE with Hazel cells: the embed frame for the tiles, the library for the values. |
| Livelits in other pages | A livelit's model and view as Hazel values a host page can drive (today they live only inside Hazel). |
| Testing and tooling | Scripts that run Hazel programs under node (the library's own tests already do), for checks in other projects' CI. |
For making it official#
- It is small and mostly done. One module over the existing statics and evaluator, a doc and tests. Merging costs little review; keeping it costs a test run.
- It shows Hazel where people are. Hazel's ideas (holes that still run, typed edit states) are hardest to show in the abstract; a page that runs a kid's half-written rule is an easy demonstration.
- Real use finds real bugs. MUDU's daily use found the module-constructor gap and the JS constructor check within days.
- Official means findable. A stable URL and a version beat a branch build named in someone's code; today a page must point at
hazel.org/build/<branch>/, which moves when the branch does.
Against, or costs to plan for#
- An API is a promise. The JS shape of Hazel values (
{tag, value}, tuples as arrays, labeled tuples as objects) becomes hard to change once pages depend on it. Hazel's own types keep moving (modules, labeled tuples, type functions). - Versions and builds. Branch builds have no versions. An official library needs releases (a version in the file,
Hazel.versionalready says "0.1 (draft)"), and a place that keeps old ones, or pages break whendevmoves. - The call boundary. Converting a JS value by building Hazel syntax and evaluating it is the floor under every call (
^report(25)); an official library should hand Hazel values in directly, and say how big an argument a call should take. - Size and the main thread. 643 KB is fine for a game, heavy for a docs page with one example; and the library runs on the page's own thread, so a slow analysis (the 1.8 s case) freezes the page. Hazel's app already has an evaluation worker; the library does not use one yet.
- Running code a page did not write. A page that runs a visitor's Hazel program runs it with the page's own rights, as any in-page evaluator would. The library should say so, and a worker would help here too.
- Two surfaces to keep in step. The embed frame is Hazel's whole app in a mode; every change to the app can change the frame. Its messages need the same care as the library's calls.
- Team time. Someone has to own it: answer issues, review changes, decide what is in the stable core.
Update, 2026-11-02: the npm release, and what it is built on#
A first, experimental release of the library is packaged for npm as hazeljs 0.0.1: hazel-lib.js as one file, entry points for Node's require and import and for a page's <script> (from a CDN that serves npm packages), TypeScript declarations, and a README. Its README sends readers to hazel.org, and to the contributors on GitHub, for who makes Hazel. The name is Matthew's choice. Here, for Hazel's core team, is exactly what the release contains.
Built from dev of 2026-10-19 and two PRs, nothing else#
The library file is built from hazelgrove/hazel at 0d66c36618, the head of #2719 (branch js-library-embed), with GITHUB_SHA set so that Hazel.build names that commit. That commit is dev at 962a7cb009 (2026-10-19) plus 11 commits, and no merges, so no other branch is in it:
| From | Commits | What they change |
|---|---|---|
#2711, the library (js-library, head 77059807e9) | 6 | New files only: src/web/HazelLib.re and HazelLibCore.re (the API and the value mapping), docs/hazel-as-a-js-library.md, test/Test_HazelLib.re, and their dune rules. Nothing else in the app calls them. |
#2719, programs of modules and embed mode (js-library-embed, head 0d66c36618) | 5 | The library's analyzeProgram and runProgram; embed mode, a new mode of the app entered only from index.html#embed (src/web/embed/, two lines in Main.re, embed.css); and the core changes below. |
What touches Hazel's core (src/haz3lcore, 830 lines added and 31 removed, all in #2719's commits 61d8bf6f2b, 4538863089 and 0d66c36618):
derived/ModuleProgram.re, new: joins named modules' texts into one program (the preamble, thenmodule Name = { body } infor each, then the tail) and attributes each error, hole and unfinished bracket to its module.lang/MakeTerm.re: an editor rooted at the module sort (Mod) is read inside a module-body tile, so its term is the module expression of its members. Every other root is read as before.FastParse.re: a module body whose last member is unfinished (let x =) is read again with a hole after it, so it stays on the fast parser instead of falling back to the slow, recovering one.TyDi/ErrorPrint.re: each error can be listed with the id of the term it is on.
Not in it#
- Modular editors (#2469), and so not its outline and navigation: what next's step 3 below, still open.
- The livelits (#2635, #2597).
- The performance PRs still open against
dev. Of the 19 that^report(21)reviewed for correctness (besides #2635), one had reacheddevby 2026-10-19 and is in the release: #2679 (an ascription pushed through a value gives the value back when it already has the type). Two more, #2672 and #2678, reacheddevon 2026-10-29 and are not. The rest are still open, among them #2603, #2606, #2608, #2610, #2614, #2651, #2652, #2661, #2671, #2673, #2675 and #2681, whose typed-value fast path the game's rules want (step 4a below).
Behind today's dev#
The release is 151 commits, 24 PRs, behind dev as of 90d1005c9e (2026-10-29). Those that change what a program means or how it reads: curried function definitions (#2403), folded constructor heads in pattern matches (#2391), and streaming evaluation in read-only cells (#2683). Those that will matter for merging the library: the .rei interfaces for src/util and leaf modules (#2439, #2440), which the library's new uses must be declared in, and the evaluation budgets in CI (#2678). A trial merge of 0d66c36618 into today's dev conflicts in one file, src/web/dune (the modules lists); whether it builds against the new interfaces is not checked yet.
Found while packaging#
- Stack depth. In Node, a module of about 150 members overflows the default stack. A module of 400 members, run with a larger stack, analyzes in about 2.3 s. Measured once each, in Node 22 on a laptop.
- The
msfield ofanalyzeProgramandrunProgramreads 0 in Node, whatever the call takes: a bug in how the library reads the clock, not yet traced.
Both are in the README's limits.
What next#
- Ready #2711: move the stranded constructor fix onto it, bring its description up to date (the editor in a page moved to #2719 and beyond), and take it out of draft.
- Open a PR for the embed frame's 12 commits (
embed-editable), stacked on #2719, so the whole of what MUDU uses is under review. - Decide the #2469 question (outline and navigation from modular editors) before the library grows its own.
- Convert JS values directly into Hazel values at the call boundary, not through syntax re-evaluated (
^report(25)'s floor), and merge #2681, which the game's typed rules need. - Run analysis in a worker in the library, as the app does, so a slow read never freezes a page.
- Version it: a build per release at a stable URL,
Hazel.versiona real number, and the stable core named in the doc. - Find a second user (a course page, or the docs' own examples) before promising stability beyond the core.
How this was put together#
From the branches' code and docs (#2711, #2719, embed-editable), ^report(25)'s measurements of a call, MUDU's own use of them (its 25-scripting.js and event log), and sizes fetched from hazel.org's branch builds on 2026-10-26. The timing figures are the doc's own, measured under node on a laptop; nothing new was measured for this draft.
Updates#
2026-10-26: first draft, from ^ticket(635), for Matthew to read before it goes out.
2026-11-02: the npm release and what it is built on (above), for Hazel's core team (Matthew's ask, ^ticket(668)).