Hazel reports · ^report(37) · agenda work in progress · started 2026-11-02, updated 2026-11-03 11:22 MST

Agenda: Matthew and Cyrus, Wednesday 2026-11-04, noon Mountain

The topics Matthew brings to his meeting with Cyrus on Wednesday 2026-11-04 at noon Mountain time (12:00 MST, Boulder), in three parts: performance, Hazel embedded in JS programs, and DSL tile editors as livelits. A work in progress: compiled over the two days before the meeting and updated as items move, so each item says where it stands, as of when, and what it needs from the meeting.

In short. As of 2026-11-03 11:22 MST.

  1. Performance: an edit and an undo each cost time in proportion to the program, and an undo about three edits. Measured on #2635 at five sizes: at 6,400 definitions an edit takes about 106 ms and an undo about 292, each followed by about 78 ms of statics. Two cheap fixes are on #2635 and took 31 to 46% off at the larger sizes; what is left is whole-program passes around incremental caches that already work. In space, Andrew Blinn's compaction of undo entries cut what a #2597 tab kept after 12 edits from 1,048 MB to 159; dev does not have it yet. Across the worker boundary, sharing keeps an edit's request small (Full Body: 27 KB, not 1.95 MB), but two slides were sent whole, about 200 ms a request: the cause is found, and a fix is being tested. Getting to O(log n) is a design question for the meeting (^report(38), both stories; ^report(36), in progress).
  2. Hazlets: Hazel embedded in a JS program's event handlers. A new word for the pattern Messed Up Dress Up uses, the first and so far only one. A demo of the game's scripts, the question ^report(34) asks (should the library be official?), and a prototype npm package, hazeljs 0.0.1, published on npm on 2026-11-02.
  3. DSL tile editors as a kind of livelit: Cyrus's idea, from about two weeks ago, which Matthew brings back. The livelit's literal is a Hazel value describing a grammar, its view is a tile editor for that grammar, and its expansion is the parse tree. Is this the shape Cyrus meant, and what should the first demo be?

The items, by section#

ItemStatus
1. Performance, and the cost of undo
1Undo in O(log n)? What an edit and an undo cost nowmeasured; three fixes on #2635 (one pushing today)
2How much sharing is lost passing data to and from the eval workermeasured; two slides sent whole, cause found, fix in testing
3Why #2635's evaluator takes half dev's steps on the PerfBudgets programexplained: fewer trampoline bounces, two of our commits
4#2635 is up to date with dev's merge daymerged; deck fix found, 12/12 on a trial branch
2. Hazlets: Hazel embedded in a JS program
5A demo: MUDU's game scriptsto show
6Should the library be official?proposal
7A prototype npm package: hazeljshazeljs 0.0.1 is on npm
3. DSL tile editors as a kind of livelit
8A livelit whose literal is a grammar and whose view is a tile editor for itidea

1. Performance, and the cost of undo#

What an edit and an undo cost as programs grow, and the questions next to it. Measured in Node on #2635 (livelits × modular editors). The whole investigation of undo, in time and in space, is ^report(38) (in progress, 2026-11-03); its time measurements are ^report(36); the series is ^report(35) and before.

1. Undo in O(log n)? What an edit and an undo cost now measured; three fixes on #2635 (one pushing today) #1 #

Asked by Matthew, 2026-11-02 · ^ticket(665) · as of 2026-11-03 08:57

Measured: a program of n top-level definitions (let v1 = v0 + 1 in …), an edit in the middle one, then undos and redos, each through the app's own update loop. From 400 to 6,400 definitions every step grows in proportion to n. At 6,400, after the fixes: an edit about 106 ms, an undo 292, a redo 184 (medians of 12), each followed by about 78 ms of statics. An undo costs about three edits and a redo about two. An undo is a redo plus a rebuild of the outline, which only the modular editors have: about a third of the undo, 39 ms at 3,200 definitions. A redo rebuilds it only when the program is not in the outline's memo of the last four, which is why the probe's last redo was slow. Even a redo that finds its outline costs 1.7 to 1.9 edits (^report(38), measured 2026-11-03, corrected 07:52). Pushing and popping the history itself costs almost nothing.

Found 2026-11-03: an undo rebuilt the syntax twice. The frame an undo schedules to realize the view counted as an edit, so it rebuilt in full the syntax the undo's own frame had just rebuilt. With it counted as an edit only when it changes the program, an undo is 40% faster and a redo 57% at 3,200 definitions; a redo then costs what an edit does (^report(38), the fix). And with the sidebar shown, every edit pays for the outline, about as much again as the edit itself, so making the outline's build touch only what changed matters for typing too, not only for undo.

In space: the history once kept a whole copy of the editor for every edit, and never cut it. On #2597 a Full Body tab kept 1,048 MB after 12 edits, 89 MB more with each. Andrew Blinn's compaction (6010128b7d, on #2469) keeps older entries without their derived data and always caps the history: 159 MB, level after the third edit. #2635 has it (52 MB); dev does not yet. It was found in October in five steps, after a test cap that never ran (^report(14)'s update). The whole story, and what is next for both costs, is in ^report(38).

Why it is linear: the incremental caches work (each step parses 1 or 2 definitions again, re-measures 1 or 2 chunks, compares 2 definitions' heads in statics), but around them are passes over every definition: rebuilding the top-level term, statics' walk down the chain of definitions, the outline (rebuilt on every undo and redo), layout's walk.

Fixed, on #2635 (394552d5d0, bfa36a6ede, pushed 2026-11-02): the auto-probe recomputed the whole top level's skeleton every frame, and completion suggestions were computed after every typed digit. 31 to 46% off each step at 3,200 and 6,400 definitions.

For the meeting: O(log n) per edit and per undo needs the top level as a balanced tree of definitions, not a chain of nested lets: each node keeps summaries (the names its subtree binds, its rows, its part of the skeleton), so each pass touches only the edited definition's path, and a snapshot shares every other node, so an undo can be a swap of roots. Is that the direction, and how much of it is worth doing before the livelits land? And dev's undo history still keeps every edit's whole editor state, never cut: should dev get the compaction now, or with #2469?

2. How much sharing is lost passing data to and from the eval worker measured; two slides sent whole, cause found, fix in testing #2 #

Asked by Matthew, 2026-11-02 · ^ticket(666) · as of 2026-11-03 11:22

Measured on #2635 in Node, on GitHub's machines (a dev build, one run each, ^ticket(666)'s probe), with the request built the way the app builds it: one piece per definition, the pieces the worker already holds left out, and the rest shared.

Found: two slides were never sent in pieces. The cause is found, and a fix is being tested.

For the meeting: Fix the double constructor entries in statics, where they arise, or have the cut count each key once? And is the reply's lost identity worth fixing before livelit views move to the worker (^ticket(18))?

3. Why #2635's evaluator takes half dev's steps on the PerfBudgets program explained: fewer trampoline bounces, two of our commits #3 #

Asked by Matthew, 2026-11-02 · ^ticket(667) · as of 2026-11-03 08:13

Cyrus's PerfBudgets test (#2678) counts the evaluator's trampoline steps on a fixed program: 206,834 on dev, 96,438 on #2635 after this week's dev merge.

Why (2026-11-03): the same reductions take fewer trampoline bounces on #2635, every form alike. A probe of 16 small programs counts about twice dev's steps on dev for each (a bare literal: 12 steps on dev, 0 on #2635). Two of our October commits, on #2597's line and so on #2635 but not on dev, remove the bounces: d0c56fb090 (binding a value that is already done calls its continuation directly) is half the gap, since #2635 with it reverted counts 152,746; 602498d7aa (the evaluator skips binds that only pass a value through) is, by its own measure, most of the rest. The first is mostly counting (allocation 4% lower, time inside the noise); the second was 16 to 25% faster on a call-heavy program.

For the meeting: Bring d0c56fb090 and 602498d7aa to dev as a small PR of their own? Dev's PerfBudgets count, and its budget, would drop by about half.

4. #2635 is up to date with dev's merge day merged; deck fix found, 12/12 on a trial branch #4 #

Asked by Matthew, 2026-11-02 · ^ticket(663) · as of 2026-11-03 08:34

Dev's 10-29 merge day (149 commits: curried function definitions #2403, .rei interfaces, PerfBudgets #2678, the quick tests #2714 and more) merged into #2635 at 2cab658ef1. CI passed (Fast checks, Full tests, Static checks), and the 8 long-known Full-test failures are gone. The Firefox deck check failed on a workflow line the merge left stale (it ran dune through opam, which dev's toolchain step leaves off the path).

Since: the fix, 1d17541aaf, pushed 2026-11-02 16:29: the deck builds and runs. Three of its 12 cases failed, all on the eye that reveals a livelit's syntax, and a rerun failed a different set. The cause is the test harness, not the app (2026-11-03): dev's c206b02c25 gives a build of any branch but dev an IndexedDB database of its own, hazel:<branch>, and the deck's reset between cases (/fresh, in scripts/dev_serve.py) cleared only hazel, so each case met the edits of the one before. With the reset clearing the branch's database too (8ee77cecbe, on a trial branch), all 12 cases pass. Not yet on #2635. Fast checks, Full tests and Static checks were green throughout.

For the meeting: The landing order: #2469 into dev, then #2635 retargeted to dev?

2. Hazlets: Hazel embedded in a JS program#

Hazlet, our shorthand from here on: Hazel embedded as a library within the event-handler system of a JavaScript program, so that some of the program's handlers are Hazel functions it calls with events and whose results it acts on. Messed Up Dress Up (MUDU), the game Matthew makes with M and N, is the canonical example, and so far the only one: five of its rooms hand their events to Hazel rules, on_event : (Globals, Event) -> Cmd, through a draft library, hazel-lib.js (#2711, with #2719 stacked on it). ^report(33) is a case study of those rules; ^report(34) asks whether the library should be official. ^report(39) gathers the Hazlet work so far: what runs today, where each piece stands, and what is next (in progress, 2026-11-03).

5. A demo: MUDU's game scripts to show #5 #

Asked by Matthew, 2026-11-02 · as of 2026-11-02 10:35

The game, live at matthewhammer.org/kids-choice-games/dress-up/: its Scripting tab, where a room's rule is written in Hazel, in Fumola or in JS and runs as the room's event handler. ^report(32) times the game loading and checking every script in each of the three. ^report(33) reads what the rules meet today (a per-room view of the game, not its state) and proposes names for every place and thing, and types built on them.

For the meeting: Cyrus's read of the rules' types, and of what a Hazlet should give its handlers: a view built per room, as today, or the program's whole state under readable names?

6. Should the library be official? proposal #6 #

Asked by Matthew, 2026-10-26 · as of 2026-11-02 10:35

^report(34)'s suggestion: merge #2711 and #2719 as an experimental library, built on dev and labelled so; promise stability only for the small core (analyze, run, the value mapping) once a second Hazlet besides MUDU has used it.

For the meeting: Merge as experimental, yes or no; and who else might build the second Hazlet?

7. A prototype npm package: hazeljs hazeljs 0.0.1 is on npm #7 #

Asked by Matthew, 2026-11-02 · ^ticket(668) · as of 2026-11-03 07:15

A task for Claude: what it takes to publish a prototype npm package that anyone can install and use, documented the way npm packages are. Split into linked tickets: the package's build for node and the browser (^ticket(669)), TypeScript declarations (^ticket(670)), the README and a CHANGELOG (^ticket(671)), examples and an install check on GitHub (^ticket(672)), the name, license, versions and who publishes (^ticket(673)), and MUDU loading its Hazel from the published package (^ticket(674)).

A first version, the same day: hazeljs 0.0.1 (the name is Matthew's pick; Hazlet stays the name of the pattern), built from #2719 at 0d66c36618: hazel-lib.js, entry points for require and import and a browser script tag, TypeScript declarations, and a README the npm way, which sends readers to hazel.org and to the GitHub contributors for who makes Hazel. 648.5 kB packed (2.8 MB unpacked). Installed from its tarball into a fresh project, every example in the README runs. Found on the way: a module of about 150 members overflows Node's default stack, and the ms field always reads 0; both are in the README's limits.

Published 2026-11-02 at 11:03 MST, under Matthew's npm account: npmjs.com/package/hazeljs, so npm install hazeljs works. Checked from the registry, not the tarball: a fresh project installs it and runs the README's examples through require and import, and jsDelivr and unpkg both serve hazel-lib.js for a script tag. npm also lists a placeholder it made first, 0.0.0-stage, to be deprecated. Not done yet: the browser and TypeScript examples, the install check on GitHub, and MUDU loading its Hazel from the package.

Matthew's next ask: the trap room live in ^report(34), its Hazel rule editable in Hazel's tiles and the room answering each edit (^ticket(675)); not started.

For the meeting: Keep it under Matthew's npm account, or move it to a Hazel organization on npm (@hazelgrove)? And is the package the library alone, or also the Hazlet pattern: a helper that turns a program of modules into JS event handlers, as MUDU uses it?

3. DSL tile editors as a kind of livelit#

Cyrus's idea, from about two weeks before this meeting, which Matthew brings back for discussion: a new kind of livelit, perhaps built in, for languages defined inside Hazel.

8. A livelit whose literal is a grammar and whose view is a tile editor for it idea #8 #

Asked by Cyrus, late October (his idea; Matthew brings it back, 2026-11-02) · as of 2026-11-02 10:35

Related so far. Cyrus's interest in DSLs in Hazel as tile-based livelits, and his pointer to language boxes (Diekmann and Tratt's Eco editor: each box has its own lexer and parser, and is one token to its parent) for #2542, the shared-tokenizer limit on sub-language tiles. Fumola as tiles: a sort in Hazel's own tile grammar (#2529), and ^fumola_wip on the remote-splices branch (2026-09-29), a livelit that edits Fumola code as tiles, a first prototype of a DSL as a tile livelit. Hazel's own tiles come from a fixed table of forms in its source (src/haz3lcore/lang/Form.re); this would make the grammar a value a program writes.

A question the parser raises: Menhir generates OCaml code when Hazel is built, so on its own it cannot read a grammar written in a running program. A grammar given at run time needs a parser built at run time, such as an Earley parser, which accepts any context-free grammar, or tables built in the browser. Or the grammar's definition could be compiled ahead of time for built-in DSLs only.

For the meeting: Is this the shape Cyrus has in mind for DSLs in Hazel? Built-in, or user-defined livelits? Grammars given at run time, or compiled ahead of time? And what is the first demo: a small grammar (arithmetic, or a JSON-like data language), its tile editor, and its expansion as a Hazel value?

If there is time#

How this agenda is kept#