Hazel reports · ^report(20) · agenda work in progress · started 2026-10-19, updated 2026-10-21 09:59
Agenda: Matthew and Cyrus, Wednesday 2026-10-21
The topics Matthew brings to his meeting with Cyrus on Wednesday 2026-10-21. Most are Cyrus's own questions from their meeting on Friday 2026-10-16. 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-10-21 09:59.
- #2635 meets today's #2469: modular editors is about to land, and Andrew's overnight push put Modules II part 1 inside it; merged into #2635 this morning (pushed 09:35, CI running; checks and targeted tests pass here). One question: should #2635 carry Modules II parts 2 and 3, or wait for them to reach dev?
- #2635's CI is green: every failure fixed. The three causes
^report(18)found are fixed, one of them a real bug in incremental statics; with #2699 merged, Fast checks and Full tests both passed, for the first time since 2026-10-01, and again on its newest head,9933091a99(2026-10-20 23:05). - Cyrus's two tiles asks: what is missing is selection, not tiles. The explainer's examples and the livelit model views are already tiles; only the revealed model can be selected (
^report(19)). - Long types in Γ: a draft PR, #2698, stacked on #2676 (the cursor inspector's long types).
- The performance PRs' correctness: an argument for each of the 19, in a report to come, with more correctness tests. The corners and weak hashes it found are fixed on their branches.
- The merge plan for dev: which PRs are first, and how the livelits PRs meet #2469 once it lands.
Outline for Cyrus
Matthew's outline for the meeting (drafted 2026-10-21 07:16), the items below in the order to raise them.
- Livelits × modular editors (#2635) is the priority, now that modular editors (#2469) is about to land. #2635 is green on CI (Fast checks and Full tests). Andrew's overnight push put Modules II part 1 (#2493) inside #2469, so this morning I merged today's #2469 into #2635 (pushed at 09:35, CI running; the release check and the targeted test groups pass here). Most of the 80 conflicting files were the same Modules II code both branches carry under different commits; the real work was the statics, the editor's layout code and the eval worker, where we took Andrew's designs (per-member module analysis, per-definition worker requests) and kept the livelit and splice pieces. We adopted Alexander's newest-binding rule for capitalized names after a dot. (Items 1 and 9.)
- For you: livelit signatures need abstract type members, which is Modules II part 3. Once #2469 is in dev, should #2635 carry parts 2 and 3 (#2494, #2495, in Alexander's current text), or wait for them to reach dev?
- And the path to dev: retarget #2635 to dev once #2469 lands, and retire the older livelits PR (#2597)?
- Correctness of the performance PRs (your Friday ask): a report argues, for each PR, why it computes the same types and values as before; drafted, published after one more read. The tests PR (#2700: incremental evaluation checked against evaluation from scratch, over the slide corpus) is ready for review: who reviews it, and which PRs get their own tests first? (Items 7 and 8.)
- CI: every PR here is green: both livelits PRs, the JS library (#2711), the performance PRs and the test PRs. Proposal: require the Fast checks job for merging into dev (6 to 7 minutes on a PR to dev). (Items 2 and 10.)
- Look-only views as read-only tiles: the revealed livelit model now selects whole tiles. One shared read-only, selectable view for every look-only surface, starting with the syntax explainer's examples? (Items 3 and 4.)
- If there is time: Γ's long types (#2698, on #2676) and which unfold mode is the default; each documentation slide timed on CI (on #2635 most open in about 3 s), keep the job on every PR?; Hazel as a JS library (#2711) with a Scripting panel in Dress Up: worth a direction? (Items 5, 12 and 13.)
The items
| Item | Status | |
|---|---|---|
| 1 | #2635 meets today's #2469: the merge, and the path to dev | merged; the slowdown fixed |
| 2 | #2635's CI: why it failed, and a fix for every failure | green |
| 3 | Livelits' model views as read-only tiles, with the tiles' selection UX | both gaps fixed, seen in the app |
| 4 | The explainer's examples as read-only tiles | queued |
| 5 | Long types in Γ, laid out as the result type is | on both PRs |
| 6 | Why expanding Html said it was not found | answered, fixed |
| 7 | Do the performance PRs keep typing and evaluation correct? | drafted |
| 8 | Guarding correctness in CI, as the performance work caches more | proposal |
| 9 | The merge plan for dev | for discussion |
| 10 | Require the test jobs for merging into dev | for discussion |
| 11 | Undo: what it should restore, and what keeping it costs | for discussion |
| 12 | How long each documentation slide takes to open, timed on GitHub | measured on both PRs |
| 13 | Hazel tiles for a game's logic: Messed Up Dress Up as a Scratch-like world | ideation, measured; a first demo built |
1. #2635 meets today's #2469: the merge, and the path to dev merged; the slowdown fixed #1
The priority this morning. Modular editors (#2469) is about to merge into dev, and #2635 (livelits × modular editors) is the livelits side that meets it. #2635 is green on CI at 9933091a99 (Fast checks and Full tests). Overnight, Andrew's #2469 came to stack Modules II part 1 (#2493) and to send module values to the worker in pieces: about 200 commits #2635 lacks, and merging them conflicts in 80 files (233 blocks).
Mapped: most of it is the same Modules II work on both sides under different commits. #2635 carries a September copy of parts 1 to 3; Alexander's current parts 2 and 3 (#2494, #2495, rebased 2026-10-20) match it almost line for line, and merging those files against his current text resolves 27 of them with no conflicts. The real work is the statics, the editor's layout code (#2469's token chunks against #2635's splices) and the eval worker (#2469's per-definition pieces replace #2635's request sharing): Andrew's designs taken, the livelit and splice pieces kept. Matthew's calls: adopt Alexander's newest-binding rule for a capitalized name after a dot (376dd4a076); take #2469's per-member module analysis and worker pieces.
Since, 09:35: merged and pushed (5cb0d88a46): every conflict resolved; the release check clean and the targeted groups passing here (module statics 208 tests, Typ, DefStatics parity and delta, eval pieces, module roots, measured chunks, every editing group 857, incremental evaluation, user livelits, and more); a read-only review found nothing of either side lost. #2635's CI is running on it. Four changes the merge made, each with a passing test, wait on Matthew's sign-off, and three of #2635's speed-ups replaced by #2469's designs are to be re-timed.
Re-timed, 09:57: Kids' Choice's eye-size slider, on GitHub's machines (eight runners, the deployed builds before and after the merge taking turns): releases after the first take 20 to 30% longer after the merge (median 196 to 292 ms against 154 to 262 before, slower in all 16 pairs); the first release after load shows no clear change. Three of #2635's own speed-ups were replaced by #2469's designs in the merge (sharing the eval request, MakeTerm's memo handoff, the layout batching); finding which costs the time is next (^ticket(513)).
The slowdown found and fixed, 12:43 (^ticket(513), 2b080af683 on #2635). #2469's per-definition pieces had replaced #2635's sharing of the eval request, and a livelit release changes one definition, posted whole as a piece: about 492 KB per release where the shared request had been about 16 KB. Now a changed piece is posted shared again (the values the worker already has as references), and its eval-info entries are one per term, not one per id. Kids' Choice's eye-size slider on GitHub's machines (eight runners, each timing #2635 before the merge, after it, and with the fix, 24 releases each): releases take 203.5 ms with the fix against 280 ms after the merge and 251.5 ms before it (medians), faster than both on every runner; a release posts about 11 KB. An adversarial review's confirmed protocol issues are fixed in the same push, with a test.
14:18: #2635's CI is green on the fix (2b080af683: Fast checks and Full tests).
For the meeting: Livelit signatures need abstract type members, which is Modules II part 3: once #2469 is in dev, should #2635 carry parts 2 and 3 (#2494, #2495, in Alexander's current text) or wait for them to reach dev? And the path to dev: retarget #2635 to dev once #2469 lands, and retire #2597?
2. #2635's CI: why it failed, and a fix for every failure green #2
^report(18) found three causes, all from merging #2469 (modular editors) into the livelits branch, with neither branch broken on its own: 8 DefStaticsParity module cases (modules analyzed whole since the merge), one typed-table test (Editing.TableDecorations 1, from dev's canonical completion, #2374), and one mismatch in the slide corpus. As of 2026-10-19 each has a fix:
- the table test's input, so that its hidden error still comes from table syntax (
27f100e37c); - the corpus mismatch, a real bug in incremental statics: a livelit use was not analyzed again when the livelit it applies changed, so after an edit it kept its old expansion (
1131a457e4); - the 8 module cases, by #2699, which brings Andrew Blinn's per-member module analysis on Modules II (
a6b5478f52, from #2687) into #2635, with one fix of its own: a livelit definition is no longer analyzed as a module (223357135a).
On GitHub, #2699 passed Fast checks and Full tests and was merged into #2635 (90ed4505bf): #2635's Fast checks now pass, the first time since the branch was made on 2026-10-01; its Full tests passed too (1 h 32 min, done 15:06), so #2635 is green on 90ed4505bf. At 15:10 one more commit went on top, a full-screen button for a livelit's picture (5961401df2), and its checks passed too (Fast checks and Full tests on 5961401df2, by 16:30). #2635's conflicts with its base are resolved too (cf325d4f61). The whole story, with today's update, is ^report(18).
What happened after it, step by step: ^report(18)'s five next steps, two done, two partly and one for this meeting, and what was still open (#2597's table test, since fixed by 799d0ab70a on 2026-10-20; a conflict in one file, since resolved; the merge plan): ^report(23).
Since, 2026-10-20: both livelits PRs are up to date with dev and mergeable. #2597 merged dev (canonical completion, the settings panel, selectable text without the clipboard shim, the ascription fix) and is green on its own branch, Fast checks and Full tests (6fbc8268fc); #2635 merged dev and the new #2597 and is green the same way (d14229ee66). Both have since taken the fix for item 5 (2cf1b59c37; #2635's merge 3d7d5baca0), #2597 green on Fast checks. One risk found on the way: #2635's quick suite took 11.5 minutes on d14229ee66, then hit the step's 20-minute limit on 3d7d5baca0 and stopped there with no test failing; on the three runs since, it passed in about 18 minutes each (17.9, 18.4, 17.9), close to the limit (#2597's takes 6 to 9). Rerun on the newest commits, both PRs passed Fast checks and Full tests: #2597 on 3a9b3cec53 (with the fix for a module livelit re-printing as Drawing. ^slider), #2635 on 1f40dae305.
Since, 13:00: both PRs carry Γ's unfold and its setting on the Colors slide (item 4): #2597 at 2cd7b96f24, #2635 at a6eca82049, each with its colour tests passing here; both then passed Fast checks and Full tests on GitHub: #2597 on 2cd7b96f24 (by 13:28, still its head), #2635 on a6eca82049 (by 14:25).
Since, 21:48: GitHub showed #2635 in conflict with its base again: item 4's Γ unfold had gone onto both branches as separate commits, and they met in one spot of Globals.re. Merged #2597's branch into #2635, keeping #2635's side (9933091a99); the merged tree is identical to the commit that was green, a6eca82049. #2635 is mergeable again. On it, Fast checks passed at 22:08 (the quick suite in 17.9 minutes); Full tests passed at 23:05: #2635 is green on 9933091a99.
For the meeting: Confirm both PRs are green. Whether the quick suite's 20-minute limit should rise, or #2635's slowest suites move to Full tests. The corpus bug is the kind the correctness items below are about: the parity tests caught it.
3. Livelits' model views as read-only tiles, with the tiles' selection UX both gaps fixed, seen in the app #3
^report(19): a livelit's model, revealed with its eye, is the one view outside the editor that already has read-only tiles with a caret, Shift+Arrow selection, the inspector and the right-click menu. Two gaps, seen on #2635: a mouse drag over the model selects by character, not by tile (its ends do not snap to tiles, as they do in the editor); and while the revealed model has focus, the rest of the editor draws blank until the eye closes. A livelit's head and params lines are HTML inputs, by design.
Both gaps fixed (2026-10-19, on #2635 as 1b7a03e8e5 and #2597 as 54e5a66bf2, ^ticket(387)): a drag inside the model's splice had been hardwired to go by character; it now goes by tile, as in the editor, unless character-level mouse is on (a new test, Editing.Splice 16). And the code view's memo had lost its frame argument in a merge, so the model's sub-editor and the editor shared one cached drawing, and the editor drew the model's rows; the argument is back. Seen in the app (2026-10-20) on #2635's build: on Editable Parameters, with the baseline slider's model revealed, a drag across pct=50 selects whole tiles, a drag inside baseline takes no stray letters (the inspector names the variable), and the editor stays drawn while the model has focus.
For the meeting: Is the revealed model's read-only selection what Cyrus wants on every look-only view? ^report(19) suggests one shared read-only, selectable view for all 16 of them.
4. The explainer's examples as read-only tiles queued #4
^report(19): the examples are already tiles, in a locked cell editor. They take focus but no caret, no selection and no menu. The inline code in the explanations is plain text. So, as with the model views, the ask comes down to selection and the menu.
For the meeting: The same question; and should the inline code become tiles too?
5. Long types in Γ, laid out as the result type is on both PRs #5
#2698 (a draft, stacked on #2676, which does the same for the cursor inspector): a toggle on each entry in Γ lays a long type, or an alias's definition, out under its name in at most 60 columns, and Γ grows to 40% of the window while one is open. On the tutorial, the toggle appears on the 147 of 249 entries that are cut off. Later steps: unfold all, unfold the caret's variable by itself, and module members one per line.
A related bug (^ticket(396)): opening Γ on #2635's Scenes slide freezes the browser. A lead: even on a 4-line program Γ draws 6,802 tokens, because the context holds the prelude's types (ColorScheme, ColorRoles, …) and each is printed whole as tiles.
Since, 2026-10-20: on both livelits PRs, each entry in Γ can show its type in full, and every entry has a right-click menu: Show in full, Show on one line, Go to definition (^ticket(468)). How the unfold is offered is a Hazel setting on the Colors slide, ^unfold: always (the toggle drawn, as before, and the default), on hover, or by right-click only, so each design can be tried in place. An entry past the size budget of ^ticket(396) shows its outline and has nothing to unfold; the Scenes slide no longer freezes.
For the meeting: Review #2676, then #2698? Which of the later steps matter? And which way to offer the unfold: always, on hover, or by right-click?
6. Why expanding Html said it was not found answered, fixed #6
Why: Html is a module, not a type. The HTML type is its member Html.T, where the old global alias HTML lives now. The way to ask the full view what a type is, type X = Html in ..., names Html as a type, which it is not; the inspector said only "Html not found" (and "HTML not found" for the old name). type X = Html.T in ... was fine. Reproduced on GitHub: type X = Html in 1 and type X = HTML in 1 each mark the name as a free type variable. Fixed on #2597 (2cf1b59c37) and merged into #2635 (3d7d5baca0, 2026-10-20 10:34): the inspector now says "Html is a module, not a type; its types: Html.T, Html.App", and for an old alias "HTML not found: it is Html.T now"; the full view of type X = Html.T in also shows what Html.T is. Four tests.
For the meeting: Whether the message is enough, or a module's name should also work where a type is expected (meaning its T).
7. Do the performance PRs keep typing and evaluation correct? drafted #7
Cyrus asked for more analysis and coverage of functional correctness. For each PR against #2609 (the performance tracking issue), a report will argue why it computes the same types and values as before: what it caches, skips or shares, the key it is looked up by, what invalidates it, and what it assumes. Any PR whose argument does not hold is called out. And more correctness tests: a new PR to dev, and tests on each PR.
One such check exists already: the DefStaticsParity tests compare incremental statics with a cold analysis and with monolithic statics after each edit. They caught the livelit bug fixed on 2026-10-19 (the first item).
Drafted on 2026-10-19: the report, ^report(21), with an argument for each of the 19 PRs; it goes up after Matthew has read it. The tests are a ticket of their own.
The tests, as a PR to dev: #2700 (out of draft and ready for review since 2026-10-20 13:33, no reviewer yet; green on Fast checks and Full tests) turns the incremental-against-fresh property back on for programs that evaluate to a value, adds six recursive-let cases, and compares incremental with fresh evaluation after 150 literal edits across the 73 corpus programs. Checked against a deliberately broken reuse check: the recursive cases and the corpus test catch it; the random property alone does not.
Fixed on their branches, 2026-10-19: the two PRs the report found changing a result in a corner, both confirmed by running. #2610's load fallback no longer turns x - y into x ¿ - y when it splits a long program just after an operand: the joined pieces are remolded once, with three split tests. #2651 again reports the error for a module definition that names its own module (module F = fun x -> F(x)), and keeps, with a test, its other change: a module under a one-member signature now evaluates to the module itself.
Hashes strengthened, 2026-10-19. Three PRs treat a structural hash as equality, so a collision would give a wrong answer, and two of those hashes let some collisions happen more easily than chance: a difference in one high bit of a part cancelled a second such difference. #2673's term hash now runs a finalizer per lane after every step (9de7d81838), and #2661's type hash, which #2681 uses too, has three such lanes, about 90 bits (67f676852b). Each comes with a test that flips every bit in every two of three parts. #2673 also gets a test pinning the two corners where its one walk decides differently from the old check, both safe (e1edf94a12), and #2675's description now names its one visible change: an outer binding that a recursive let shadows is flagged unused, which is right (confirmed with hazel analyze -W). #2610's fix passed its CI, and so did the three hash commits (#2661, #2673, #2681: Fast checks and Full tests).
The 19 PRs
For the meeting: Is an argument per PR, plus the tests, what Cyrus had in mind? Which PRs first?
8. Guarding correctness in CI, as the performance work caches more proposal #8
What guards results today, and what would guard the caches and reuse the performance PRs add (#2609). Today: DefStaticsParity checks that statics redone per definition agree with statics from scratch; the performance budgets (#2678) check cost, not results; and #2700 (ready for review since 2026-10-20, green on Fast checks and Full tests, up to date with dev; it changes tests only) turns the incremental-against-fresh property back on for evaluation results and adds Evaluator.IncrCorpus: 150 literal edits over the 73 corpus programs, each incremental result checked against evaluation from scratch. It was checked by mutation: with the reuse check made to ignore changed dependencies, it fails.
Proposed, in this order:
- Every cache has an off switch, and CI compares on against off. One differential harness over the slide corpus: types, marks, elaborations (ids aside), values, probes and test results, after random edits and from scratch, with each cache on and off. A performance PR registers its switch and gets the check. #2700's corpus test is its first case.
- The guards are tested too. For each cache, a deliberately wrong variant (a dependency ignored, a hash made to collide) must make the suite fail; run weekly, beside the extended tests. A guard that cannot fail guards nothing.
- Hash keys are checked for soundness. Equal hash implies equal term, for the type hash (#2661, #2681) and the term hash (#2673), with a census over the corpus.
- Every load path gives the same program. The fast loader, the recovering parser (#2610's segmented one), a saved slide's round trip and typing.
^report(21)found #2610's fallback turningx - yintox ¿ -y;^ticket(472)is checking a saved Scenes slide whose livelit uses came back broken. - The stack is tested merged.
^report(18)'s regressions came from merging two branches each green alone (#2469 into the livelits branch). A scheduled job that merges dev, #2469, #2597 and the performance PRs and runs both suites would find those before anyone merges. - The checks are required (the item on gating dev, below): Fast checks required for dev, after quarantining
Evaluator.Properties. - Each performance PR says what it caches, as
^report(21)writes each one up: the key, what invalidates it, what it assumes, and the test that fails if the invalidation is wrong.
Since, 2026-10-20 14:30: proposal 3's checks, added (passing here, and green on GitHub since: Fast checks and Full tests on all three): #2661, the type hash: every type in the corpus's info maps, 92,503 of them in 2,260 hashes, and no two types of one hash that equality tells apart; and over 500 random pairs, the remembered consistency answer equals the direct one, both ways. #2673, the term hash: 45,320 subterms of the corpus, no collision; and the hashed reuse check against the one it replaced at 67,980 nodes (copies, new ids, a literal edited): never laxer, stricter at 18, which only loses a reuse. #2610, the load fallback: nine inputs only the fallback reads (unfinished forms, a concave hole, the three confirmed split cases) agree with the character-by-character parser as text and as terms, and a sweep of every split position over three corpus programs agrees (in CI); run once here over all 29 programs under 1,500 bytes, 2,900 split positions, it agrees too.
Since, 22:18: two of the test PRs, the budgets (#2678) and the recursive-let budget (#2690), had fallen into conflict with dev in the test list (dev's #2679 added its own tests at the same place). Both now merge dev, keeping both lines, and pass Fast checks and Full tests on GitHub: #2678 at 01a15b50f9, #2690 at 5c8cd5f345.
For the meeting: Which of these Cyrus wants, in which order, and which should become required checks; and will you review #2700 (tests only, ready)? The hash and parser checks of proposal 3 are on #2661, #2673 and #2610 (below).
9. The merge plan for dev for discussion #9
Cyrus's plan, broadly: first the PRs that have been waiting, then Andrew's modular editors (#2469, with the standard library, #2687), then the livelits and performance PRs, in some order. The livelits work is #2597 (the full livelits branch) and #2635 (livelits × modular editors, which merges #2469 into it). Most of the performance PRs are against dev already.
Merged into dev on 2026-10-19: #2679 (an ascription pushed through a value gives the value back), the second of the performance PRs to land, after #2602 (jump to a piece by id without walking the caret), merged on 2026-09-29.
For the meeting: Which PRs are the ones that have been waiting? Once #2469 lands, do the livelits PRs meet it through #2635, or by #2597 merging dev? And the order of the performance PRs.
10. Require the test jobs for merging into dev for discussion #10
dev is protected but requires no status check, so a red CI run, or a broken performance budget (#2678), is a signal, not a gate. A proposal for the maintainers: require the Fast checks job (about 6 to 7 minutes on a PR to dev, #2678's budgets included; 15 to 20 minutes on the branches that carry #2469's modular editors, which is what dev can expect once #2469 lands), keep Full tests informational or require it too, and first quarantine the tests that fail by chance (Evaluator.Properties).
For the meeting: Cyrus's view, as a maintainer.
11. Undo: what it should restore, and what keeping it costs for discussion #11
Continuing the undo discussion. ^report(14) measured a tab on #2597 while one slider on the Full Body slide was moved again and again: it kept about 92 MB more after every edit and did not level off, and what it kept was the undo history, holding whole versions of the editor's state (types and statics, hashes of values, terms, layout), one per edit. Its plots follow four builds over twelve edits: #2597 as built (+1,048 MB after edit 12), the history capped at two states (levels off near +290 MB), the compaction from #2469 ported (near +160 MB), and #2635, which has the compaction (+52 MB). Those numbers are #2597 as it was before 2026-10-15: since then #2597 carries the compaction port (d41c7a0298: derived caches dropped, the stack capped at 250 entries) and keeps only the newest undo and redo entries whole (f8ae6f2b9c), so today's #2597 is the plots' “compaction ported” build, not the +1,048 MB one.
For the meeting: What an undo should bring back, and so what each entry has to keep; and how far back, at what memory cost (the plots in ^report(14)).
12. How long each documentation slide takes to open, timed on GitHub measured on both PRs #12
Matthew asked for the time to open every documentation slide, each livelit slide among them, on GitHub's machines rather than his, what dominates each one's cost, and to keep tracking it with the performance PRs applied. A job on a branch of each PR opens every slide in headless Chrome, each in a fresh tab with its saved state cleared, and records when the page stops changing, with a CPU profile of the page and of the eval worker. Each profile's time in js_of_ocaml's runtime and the OCaml libraries is charged, through the source maps, to the nearest Hazel function that called it.
#2635 (run 39524173735, today): all 58 slides open and settle, 264 s in all. Most take 2.7 to 3.4 s. The slowest are Dynamic Row or Column (12.5 s, parsing 45% of it; 4.2 s on #2597), the playground's Witch (12.1 s) and Full Body (12.0 s), the perf-mega-4k stress slide (11.3 s), Scenes (9.9 s) and Shoes (8.3 s). The figure slides are two-thirds to four-fifths page script (Shoes 6.2 of 8.3 s, Full Body and Witch 9.7 of 12 s); alongside it, the eval worker runs 2.8 to 3.6 s (4.8 s on Full Body and Witch, 5.5 s on Scenes).
Where the time goes, over all slides (213 s of script, charged to the nearest Hazel caller): the view 55 s, terms 45 s, parsing 41 s, statics 23 s, layout 13 s, evaluation 10 s, the runtime with no Hazel caller 14 s, projectors 4 s. Most slides are mixed, the view first (30 to 40%) and parsing second. The Hazel functions charged with the most library time: TermBase.re 11.1 s, Main.re 6.5 s, ConstructorMap.re 4.5 s, and the worker's own bookkeeping, WorkerMetrics.re 4.3 s and WorkerServer.re 6.2 s.
#2597 (run 39524190956, today; its previous run, 39482019681, had lost the one slide after Witch, Scenes, to a DevTools call that never answered, and the job now records such a slide and goes on): 52 of its 55 slides open and settle, 479 s in all. Most take 2.2 to 2.9 s, as on #2635. The playground's figure slides do not: Shirt and Pants 35 s, Hair 40 s, Arms 40 s, Mouth 42 s, Eyes 48 s, Hands 59 s, Shoes 71 s (on #2635, 6 to 8 s each); Full Body and Witch are still not open at the 3-minute limit, and Scenes keeps the page too busy to answer past it.
Where #2597's time goes (362 s of script): statics 143 s, parsing 83 s, terms 81 s, the view 32 s. On the figure slides statics is about half, terms a quarter, parsing a fifth; ConstructorMap.re alone is charged 45 s (4.5 s on #2635), then LabeledTuple.re 19 s, Skel.re 13 s and Form.re 17 s. #2635 carries #2469's statics work, which #2597 does not; that is most likely most of the difference.
On Matthew's laptop, Scenes's evaluation times out on #2635's build too, so its livelits do not draw at all there.
For the meeting: Whether to keep the job on both PRs and on dev, so each PR's effect on opening times shows; whether #2597's figure slides (35 to 71 s, two not opening at all) are worth fixing on #2597 itself or only through #2635; and on #2635, which to take first: parsing on the slowest slide, the view's share on every slide, or the worker's bookkeeping, about 10 s over the 58 slides.
13. Hazel tiles for a game's logic: Messed Up Dress Up as a Scratch-like world ideation, measured; a first demo built #13
An idea, partly Matthew's to shape. Livelits show Hazel can express a great deal, but not yet fast enough to be a game's engine or its drawing. The game's logic is another matter, especially simple event declarations: when this happens in the game, queue that action, perhaps at once. Hazel tiles inside Messed Up Dress Up, customizing its behavior over its world, people and objects, would make each drive the other: more play, and more research. Such rules are small and run seldom next to the game's frames, so they fit Hazel's evaluator as it is.
Against Languages of Play (Martens and Hammer, 2017; HTML). The paper treats each game's interface as a programming language of player intent: its syntax the intents the game recognizes, its operational semantics the mechanics as inference rules for a step from a state and an intent to a new state and a response, its type system a contextual interface in which only what makes sense in the current state can be said; play traces are straight-line programs, and player skills general ones, with branching on the game's response, parameters typed by the resources they need, repetition and parallel composition, which it offers as a scripting language for modding and co-creative play.
- Where they meet: "when this happens, queue that" is the paper's skill as a program, made reactive: a rule fires on a game event rather than being issued by the player, and its body is the same shape as the paper's do-then-case-on-the-response.
- Where the paper goes further, and Hazel could follow: Dress Up's intents (walk, climb, sit, open, pull a lever, dress) as a typed syntax; the context (where the friend is, what is near) as the rules' type system, so a rule that can never apply is a type error rather than a silent no-op; the mechanics as inference rules, which Hazel can already hold as derivations, with the paper's "never gets stuck" as a property checked where the rules are written; play traces as structured values to query, as Hazel's probes already are.
- What Hazel adds that the paper only anticipates (it cites Hazelnut): rules that are meaningful while half-written, with holes; the game's own controls inside a rule, as livelits (a slider for a jump's height, a picker for a place); and proof beside program in one editor.
- Steps, smallest first: the events and intents as a Hazel sum type; rules as Hazel values the game reads into its queue; the context as types; traces the game logs as values Hazel can query; later, mechanics as inference rules.
Measured (^ticket(402), on #2635's code, the test build under node on a GitHub runner): a lever pulled flips every trapdoor a table of lever-to-trapdoor links names, written in plain Hazel, and again monadic (the rule returns commands as data, which a game would interpret against its globals, as Matthew asks: "we need global variables for simple games --- most scripts use them"). Statics and elaboration, once: 8 to 70 ms. Per pull: 0.7 ms with 10 links (0.95 ms monadic), 5.7 ms with 100, 65 ms with 1,000: linear in the table, since a list is scanned each pull. A trap room is a few links, so a twentieth of a 60-frames-a-second frame; drawing is what Hazel cannot do in time yet. The monad itself, $GameUpdate, with variables named by Fumola-like symbols, is ^ticket(479).
Sketched (^ticket(479)): the trap room's lever as a $GameUpdate program, a monad beside livelits' $UpdateCmd, its commands run by the game and each awaited (a walk takes time; the program resumes after), the game's variables named by typed, Fumola-like symbols:
# $GameUpdate: commands the game runs, each awaited, with do as for $UpdateCmd #
# pure(a) | get(@sym) / set(@sym, v) the game's variables, by symbol #
# walk_to(@thing) | anim(name, ms) | wait(ms) | emit(event) queued, e.g. Fell(pit) #
# A symbol is a pointer into the game's saved state, typed by what it holds: #
# @room.lever(L) : Sym(Bool), @room.wiring : Sym(Wiring), @access.lever_far : Sym(Bool) #
let pull_room_lever = fun side : Side ->
do far <- get(@access.lever_far) in
do _ <- (if far then pure(()) else walk_to(@room.lever_thing(side))) in
do up <- get(@room.lever(side)) in
do _ <- set(@room.lever(side), !up) in
do _ <- anim("lever-turn", 1400) in
do wiring <- get(@room.wiring) in # normal, reversed or random #
do opened <- swing_pits(pits_worked(wiring, side)) in
do at <- get(@friend.place) in
if over_any(at, opened) then emit(Fell(at)) else pure(())
inIn the game today that is about 150 lines of JavaScript (the room's levers, its boulder levers, the placed levers' links, the walk to a lever and the pull, the pits' wiring). In Hazel, by estimate: the rules 60 to 90 lines; the monad's declaration in the builtins, as $UpdateCmd's is, 40; and in the game a command interpreter, about 100 lines of JavaScript, which keeps every animation and walk where it is now. Per pull, ~1 ms of Hazel (as measured above), the animations' time aside. What is new to Hazel: commands that suspend for the game, and symbols as typed pointers into a store Hazel does not own.
What it would take (^ticket(480)) for a Scripting panel in Dress Up, an editor per room, starting with the castle's dungeon:
- Hazel as a JS library, a second build entry beside the app's:
analyze(text)andevaluate(text)on the page's own thread (rules this small need no worker), thenmountEditor(element, {text, onChange, ...}), one code editor in any page. The editor assumes the whole app today (its settings, focus handling and keys at the document, its stylesheet unscoped), so that is the main work; and Dress Up's own keys move the friend, so keys must stay in the editor that has focus. About 7 MB of script, loaded only when the panel opens. - One global program, composed from the panel's editors:
module Common = {...} in module Dungeon1 = {...} in ..., each room's editor a module named by its room, Common in scope in all of them; Dress Up makes the names (and refuses a duplicate). #2469's modular editors, which #2635 carries, already show one program as a cell per definition: the closest start. - The
$GameUpdatemonad (sketched above) as a builtin, and its interpreter in the game. - The hook: on an event (a lever pulled, a room entered), the game asks the program's handler for that room and runs the commands it returns.
Order: 1's analyze/evaluate and 3 first, with the rules in plain text, which runs the dungeon's logic end to end; then the editor, then the panel.
Built since (2026-10-20): Hazel as a JS library, a draft PR (#2711, ^ticket(481)): Hazel.run(text) returns a program's errors and its value as plain JS, a Hazel function as a JS function. And a 🧩 Scripting panel in Dress Up, a PR of its own (^ticket(482)): an editor per room, and the castle trap room's red levers run by its rule, on_event : (Globals, Event) -> Cmd, a command tree the game interprets. Tonight a bug in the library kept a rule with an ascribed parameter stuck on an event sent from the page (a constructor from JS had no type, so the parameter's ascription could not go through it); fixed on #2711, with tests. hazel.org now serves the fixed build, and #2711 is green on Fast checks and Full tests; with it the demo's levers work in the game on the Scripting panel's branch (kids-choice-games PR #1, not merged: the published game does not have the panel yet): a pull runs the trap room's rule, and the rule's command flips the lever. Still open, for Matthew: a constructor from the page with no value, whose tag the parameter's type lacks, is not refused at the ascription; a check for that is built and tested on a local branch, not pushed.
For the meeting: Whether this is worth a direction; which of the paper's layers to show first (typed intents and contextual types look nearest); and what is Matthew's to decide: which events and actions first, where the tiles sit in Dress Up, and who writes rules (M and N among them).
If there is time
- Livelit views evaluated in the worker (option B of
^report(5);^ticket(18)): prototyped on a branch cut from #2635 on 2026-10-17 (perf/2635-views-in-worker-costly,ee885708ac; no PR, and not on #2635 itself; a view that takes 8 ms or more runs in the worker), not yet measured in a browser. - Node budgets beside step budgets: draft #2697 (
^ticket(19)). - Three ports offered to #2469, with
^report(13)'s numbers (^ticket(54); offered on #2469 on 2026-10-19).
How this agenda is kept
- Started 2026-10-19 from the meeting's ticket on the work dashboard (
^ticket(389)), which collects the topics as they come in; Matthew adds topics through Wednesday. - Each item gives who asked, its status and the time that status is from. PRs are hazelgrove/hazel's; commits are on the PR named beside them.
- Each item has a copy item chip (
^ticket(475)): it copies the item's number, its id (which stays when items are reordered), its title and its link. Pasted to Claude with a request (move it up or down, work on it now, to be readier to discuss it), it names the item exactly; the agenda is changed and republished in that turn. - Fact-checked against GitHub on the evening of 2026-10-20: each of the 162 claims GitHub can show (a PR's state, a run's result or length, a commit on a branch) checked by one reader and every stale or wrong one re-checked by a second trying to refute it; 18 corrected that night, 16 that GitHub cannot show (measurements, things seen in the app) left as they were.
- The data: in this page's
<script type="application/json" id="report-data">element and in020/data/agenda.json; the page is made by020/drivers/gen020.py.