Hazel HTML - #2115
Open
disconcision wants to merge 121 commits into
Open
Conversation
# Conflicts: # src/haz3lcore/projectors/ProjectorCore.re # src/haz3lcore/projectors/ProjectorInit.re # src/language/term/Typ.re
Documents a staged plan for building a full-featured HTML/DOM wrapper for Hazel programs, including: - Expanded Html element types (structural, forms, tables, semantic) - Expanded Attr types with typed common attributes + string fallback - Event data types (KeyEvent, MouseEvent) - Cmd system for fire-and-forget effects (focus, scroll, clipboard) - Sub system for event source subscriptions (resize, keyboard, time) - App viewer integration options Uses "self-modifying" pattern (handlers return Html -> Html) to work without type parameters. Notes future work to parameterize over message type once Hazel gains type parameters. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add KeyEvent and MouseEvent product types for event data - Expand HTML.t with ~40 element constructors (forms, tables, semantic sections) - Expand HTML.attr with ~45 attribute/event constructors - Add mouse/keyboard event handlers with proper JS interop - Update HazelDOM.re renderer to support all new elements and attributes - Use Node.create() escape hatch for HTML5 elements without Virtual_dom functions - Fix constructor registration to handle non-sum types (product types) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Cmd type to BuiltinsADT.re with variants: - CmdNone, Batch(List(Cmd)) - Focus(String), Blur(String), ScrollIntoView(String), ScrollTo(String, Float, Float) - CopyToClipboard(String), Delay(Float, Html -> Html), Log(String) - Create CmdRunner.re to interpret Cmd values as Ui_effect.t - Commands are fire-and-forget effects that work without type parameters - Delay command uses Bonsai.Effect.Expert.handle for async state updates Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Sub type to BuiltinsADT.re with variants: - SubNone, SubBatch(List(Sub)) - OnResize((Html, Int, Int) -> Html) - OnVisibilityChange((Html, Bool) -> Html) - OnDocumentKeyDown/OnDocumentKeyUp((Html, KeyEvent) -> Html) - Every(Float, (Html, Float) -> Html) - AnimationFrame((Html, Float) -> Html) - Create SubManager.re to manage subscription lifecycle - Track event listener IDs for proper cleanup - Rename Cmd.Batch to CmdBatch for consistency with SubBatch Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add App type as product: ((HTML, Cmd), HTML -> Sub) - init provides initial state and startup command - subscriptions function returns event sources based on current state - Foundation for projector-based app runner Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add process_handler_result() to detect (Html, Cmd) tuples - Event handlers now support returning either Html or (Html, Cmd) - When handler returns tuple, CmdRunner executes the command - Backwards compatible: plain Html returns still work Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Mark completed deliverables in Phases 1-5 - Add implementation status summary at top - Note remaining work: Sub lifecycle integration, App runner, error boundaries Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add projector_id and subscriptions fields to HazelDOM.t - Add global active_subscriptions registry keyed by projector ID - Add manage_subscriptions() to set up/clean up subs on render - HTMLProj passes projector_id for subscription tracking - Subscriptions are cleaned up when projector re-renders Note: App type detection for automatic subscription extraction is still TODO - currently subscriptions must be passed explicitly. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- detect_app() checks if expression is App type: ((HTML, Cmd), HTML -> Sub) - When App detected, extracts html model and evaluates subscriptions function - Subscriptions are passed to HazelDOM for lifecycle management - Plain Html expressions continue to work as before Note: init_cmd from App is detected but not yet executed on startup. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create hazel-html-implementation.md with complete technical reference - Architecture overview with diagram - All types with full constructor listings - Self-modifying pattern explanation - Runtime components (HazelDOM, CmdRunner, SubManager) - Usage examples for plain HTML, commands, and full App - Known limitations and future enhancements - Update hazel-html-plan.md - Point to implementation doc as primary reference - Mark completed items - Note deviations from original plan Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Run the startup command from App init tuple via CmdRunner when the projector first renders an App type expression. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…at load Remove the [PERF] printf timers (TimeUtil.log_time + call sites in CachedStatics/Statics/Evaluator) and the profiling counter refs baked into Typ.normalize/meet, keeping every optimization intact. The Var-Rec meet fast path now verifies the alias body against the Rec (=== or syntactic equal) instead of trusting the bound name alone. Ascriptions' builtin ctx is set once at Evaluator module load rather than inside evaluate, so stepper/PatternMatch/Unboxing entry points no longer depend on an evaluation having run first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…vents Consolidate the triplicated app/HTML shape detection and DHExp extraction helpers into projectors/MvuShape (HTML constructor names now derived from BuiltinsADT.HTML.t, so they can't drift). Fix the Elm-mode error branches in on_input/on_mouse/on_key, which injected an error-HTML node into the user's update function as if it were a Msg — they now report and ignore. Implement OnSubmit (prevent-default + dispatch) instead of silently doing nothing. Remove dead code (is_cmd, AnimationHandle) and the web-layer [PERF] timers + step-trace console logs. KeyEvent/MouseEvent become labeled tuples (key/code/ctrl/... and x/y/button/...; clientX/clientY renamed) so handlers can use projection like e.key instead of positional destructuring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Convert let-bound lambdas to the let f(args) = ... sugar (including annotated forms let f(x: A) : R = ...) across all 15 examples, and switch event consumers to labeled projection (e.key etc.). All examples verified via hazel run; inline test suites pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the Globals.app_view_state singleton with apps: AppStore.t (Id.Map) in the State stratum. Entries split state (model) from memos (update/view/subs closures, html) and own their subscription handles; subscription reconciliation moves from render into the update path, retiring HazelDOM's module-global active_subscriptions Hashtbl — HazelDOM.go is now render-only, and timers no longer reset phase on unrelated renders. AppView* actions are id-keyed (sidebar keeps a synthetic id). On re-init after program re-evaluation, the old model survives iff view(old_model) still evaluates — live-editing the program keeps app state when compatible. GC implemented, wiring deferred (TODO at MakeActive). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Projector model strings are opaque to statics (audited every kind: sliders/checkbox/textarea/livelits commit through SetSyntax, which stays Full), so Project(SetModel) now classifies as Layout: CachedSyntax and measured/shape maps rebuild, CachedStatics is reused, and no worker re-eval is scheduled. New SetModelQuiet variant (non-historic twin) lets drag gestures coalesce: HTMLProj resize pushes one history entry on the first tick and streams quiet updates after, so undo restores pre-drag size in one step. Autosave still fires via a new Updated.save field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Self-modifying mode is Elm with update = apply: a handler always produces
a msg, and only the commit target differs. HazelDOM.t shrinks to
{inject, view_term, commit} with commit = State | Syntax — State lets
AppStore evaluate update(msg, model), Syntax lets HTMLProj evaluate
msg(model) and splice via SetSyntax. Payload events build the msg as
`fun m -> handler((m, payload))` (payload_transform), so the per-handler
update_fn branching disappears; Cmd results still run, and Delay works
uniformly since a delayed msg is just another transform.
Cuts, all legacy-side: the 2- and 3-tuple app shapes and their detection,
SetAppViewModel and the sidebar's dual inject, OnMousedown, the
Checkbox/Radio/Range element constructors (Input + Type covers them),
CmdRunner's update_fn and its legacy Delay reinterpretation, and
process_handler_result. Node/Create/BoolAttr escape hatches stay.
Test_MVU drops the legacy-shape cases and adds three for the unified
syntax-commit path (handler-as-msg, payload_transform, (Html, Cmd)).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add placement (Inline | Sidebar) to the projector instance, defaulted via [@sexp.default] so documents serialized before the field still load (verified against the six projector-bearing doc slides). Alt+S on an indicated projector, or the context menu, toggles it: the code site keeps a chip — kind icon plus ~10 chars of abbreviated syntax — and the projector's primary view renders as a card in a new Projectors sidebar panel, in syntax order, with jump-to-source and undock. Routing is generic, so this works for every projector kind without per-projector changes: mk_view is unchanged and placement-agnostic, and split_views picks chip-vs-inline the same way refractors already pick skip_inline. Overlay and offside keep rendering at the code site in both placements. ProjectorChip owns the chip's segment and its Shape so the reserved footprint and the drawn width can't drift. TogglePlacement is a Layout edit (footprint changes, semantics can't), historic, one undo step. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An Elm 4-tuple anywhere in a program can now be projected, so an app no longer has to be the last expression — and since projectors have placement, it can be docked to the sidebar with Alt+S. One projector, two commit modes, chosen by the live value: an app commits to AppStore (State), bare HTML keeps splicing into the document (Syntax). Detection is two-phase because init only sees pre-eval syntax: init accepts permissively (HTML, 4-tuple literal, Var, Ap) and view decides authoritatively from the evaluated value. That value comes from the probe system via Projector.dynamics — which turned out to be a dead flag, its consumer removed when legacy probe code was dropped from maketerm. Rewired it in CachedStatics: dynamics-requesting projectors' ids join the refractor probe ids, additively, so nothing changes for projectors that don't ask. Core can't depend on web, so AppBridge holds function refs (inert by default) that AppBridgeInstall populates — the Ascriptions.ctx_ref precedent. ProjectorView's ViewCache gains an app-version key, since store updates are an input the cache otherwise can't see; non-app kinds pass 0 so a ticking app doesn't invalidate every projector on screen. Checkpoints: an entry's model is serialized into the projector model after 2s idle, but only when closure-free, via the quiet non-historic write. On restore it must both evaluate and yield HTML under the current view — Hazel is gradual, so a wrong-shaped model returns Indet rather than failing, and eval-success alone would accept stale state. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
From testing the docked-projector feature: Cards appeared in reverse syntax order — MakeTerm builds projector_list by prepending, so it is reverse-syntax; sidebar_views now sorts against the reversed list. The list itself is left alone: action idx values index the unreversed one. The chip is now uniform and fold-like: a single ⇥ glyph for every kind (no per-kind icon, no abbreviated syntax), two columns wide, with the shard backing always drawn. Per-kind CSS was the reason chips looked different from each other — some kinds hide or recolor that backing — so a `chipped` class neutralizes it. Clicking a chip opens the Projectors panel, expanding the sidebar when collapsed. Multi-line projectors collapsed to one line when docked: they size themselves with height:100% against the box the code editor gives them, which the panel didn't provide. The panel now sets an explicit height from the projector's own placeholder shape. Fixes a pre-existing crash this made reachable: TextArea/Livelit/Probe focus handlers asserted their element exists, but it doesn't when the projector isn't drawn at the code site — docked, or culled from the viewport, which could already crash before this branch. They no-op instead, and key_handoff no longer offers arrow keys to a docked projector. detect_app_kind also accepts a labeled app tuple — (init=…, update=…, view=…, subs=…) matched by name, so order is free — alongside the positional form. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panel was still showing cards in the wrong order after the previous attempt, because that attempt was built on a wrong assumption: I took projector_list to be reverse-syntax-order (MakeTerm prepends to it) and reversed it. But MakeTerm logs projectors during a skel-driven descent, so the list follows traversal order, which is neither source order nor its reverse in general. Sort by the projector's measured origin (row, col) instead. That is the same measurement used to absolutely position it at the code site, so it is on-screen order by construction and depends on no list ordering. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The old handle computed size from absolute cursor position against a container rect it re-read every move, using a px-per-char ratio derived from offsetWidth/cols rather than the real font metrics. So error scaled with drag distance, and because resizing reflows the document, the reference moved under the drag — hence bouncing. The handle also lived inside the overflow:auto wrapper, so once app content overflowed it anchored to the content's corner instead of the visible box. Now: anchor cursor position and starting cols/rows at pointerdown and size from the delta, using the editor's true col_width/row_height, never re-reading geometry mid-drag — immune to reflow by construction. Handles are siblings of the scrolling wrapper, each with its own 6px gutter: right edge resizes width, bottom edge height, corner both. Docked shows only the bottom edge; width belongs to the sidebar. All pointer events now, with lostpointercapture handled so a lost capture can't strand a drag, and a click that never moves no longer emits a model update. Two framework gaps closed to make this possible: View.args now carries col_width/row_height (projectors had no access to font metrics, which live web-side), and View.status carries placement (projectors could not tell whether they were docked). Both are generally useful. Also fixes a latent staleness bug this surfaced: ViewCache keyed on everything except font metrics, so cached views kept closures over the old cell size across a zoom or font-size change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The App View sidebar tab rendered the app at the bottom of the program. An app can now be projected anywhere and docked to the Projectors panel, so the tab is redundant and goes, along with its plumbing: Sidebar's cell_editor parameter, Page.Update.get_cell_editor, AppStore.sidebar_id, and the RefreshAppView and ResetAppView actions, which only the panel dispatched (RefreshAppView was a byte-for-byte duplicate of InitAppView's handler). InitAppView and AppViewMsg stay -- the projector path uses both through AppBridge. The checkpoint guard was rejecting every function, which is stricter than it needs to be: the evaluator substitutes environments away, so a function reaching a model is already closed, and a closed function is ordinary syntax that serializes and restores. is_closure_free becomes is_checkpointable and rejects only terms carrying a captured environment. Environments turn out to be serializable, so that is not the reason: Closure(env_init, _) round-trips at 228 KB, which is the reason -- along with the evaluator invariant saying it should never arrive. FixF with an environment is rejected on the same grounds. Round-trips verified per form: closed Fun, TypFun, FixF without an environment, BuiltinFun, and a function inside a labelled model. Note what the guard does not prove: closedness. Only evaluate guarantees that, and restore's render check is not an airtight backstop, since a view can embed a function without applying it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Clicking a card moved the caret only when the target happened to be in the active editor, and even then left it invisible. Both sidebars were injecting ActiveEditor(Move(Goal(TileId(id)))), which searches only the active editor and skips the global JumpToTile handler -- the one that resolves which cell holds the id and schedules focus, without which the caret stays hidden, since #caret is visibility:hidden unless the editor has :focus. Cross-editor was the case it could never serve, and in Exercises and Tutorial modes the panel is pinned to a fixed cell, so panel-shows-one-cell-while-another-is-active is the normal state. The card header's syntax budget was 10 characters -- sized for the inline chip back when the chip displayed syntax. The chip has been a single glyph since, so the constant only served the card, where the sidebar affords about 32. Renamed to say so. The chip's reserved width was always separate from it, so the footprint is unchanged. Card headers drop their per-kind icon: the syntax identifies the row and the space is better spent on it. That was kind_icon's only caller, so it goes too. Tab icon is a library rather than a chest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
disconcision
marked this pull request as ready for review
July 27, 2026 08:13
`make ci` runs `dune runtest --instrument-with bisect_ppx --force`, which is a good deal heavier than the local `@test-quick` loop: `@runtest` includes the Slow QCheck property tests that `@test-quick` skips, coverage instrumentation slows every test, and `--force` disables caching. It has been overrunning 20 minutes on several branches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A sum type compared unequal to a structural copy of itself whenever it had two variants with the same constructor name, so `Typ.meet` of such a sum with itself returned None — spurious type errors on a state users reach by typing a variant. The stepper/evaluator confluence property was failing on it; both engines were producing identical terms and equality was wrong. Cause: ba0c45b replaced venn_regions' linear List.partition scan with a Hashtbl index, but Hashtbl.add stacks duplicate keys and find_opt and remove are LIFO, so with names repeated the pairing crossed: xs[0] with ys[1], xs[1] with ys[0]. That commit also computed the right region from a Hashtbl.fold, i.e. in arbitrary order, and had quietly dropped two BadEntry behaviours. Restores dev's pairing semantics while keeping the O(n) index: index ys keeping the first entry per name, consume every ys entry sharing a matched name as List.partition did, and derive the right region by filtering the original ys so its order is deterministic again. Still linear for large ADTs — the only scan left is over BadEntries, of which real ADTs have none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The suite went from 5m31s on dev to 23m38s on this branch. Cause: the expression generator emits constructors named `A` and `B` in its minimal-names mode, and this branch adds an HTML anchor tag as a builtin constructor named `A`. So half the constructors in every random program resolved to `<a>`, whose type expands to 7,836 nodes — 4.5M normalize steps across 300 programs, against 14,828 with non-colliding names. Names now avoid the builtin constructors, derived from the builtin context rather than listed by hand, since the free-form branch could also generate Div, Text, Br, Main, Form, Label, Code and Span. Statics.Properties goes from over 600s to 87s (dev: 88.6s) and the full suite to 5m53s. This also restores coverage these properties had quietly lost: they mean to test arbitrary user-defined constructors, and half of them were testing HTML typing instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This reverts commit 86bd694.
Normalizing to compare is what made HTML programs slow in statics.
`Var("HTML")` expands to ~7,800 nodes — its 46 variants re-expand `Attr`,
whose 48 variants re-expand the event types — and both sides expand
unconditionally before the comparison starts, so the cost is paid even
when the types differ at the head.
In practice the two sides are the same type. Instrumenting the example
programs: 100% of calls in mvu-counter, 97% in lunar-growth, 92% in
todo-list, 85% in gameoflife are settled by a syntactic check. Equal
types normalize equally, so checking that first is sound and skips the
expansion. Time in normalize: mvu-counter 244ms -> 0, gameoflife 310ms
-> 3ms, lunar-growth 1681ms -> 46ms.
The remainder — types that are equal only after alias resolution, and
those that genuinely differ — still normalize. Bailing out lazily on the
latter would want a comparison that resolves as it descends; not done
here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neither had property coverage: Test_Equality was unit tests only, and Statics.Properties either checks that statics doesn't crash or gathers statistics without asserting. So a sum comparing unequal to a copy of itself — which reached the editor as spurious type errors while typing a variant — was caught only incidentally, by the evaluator/stepper confluence property, and only after it had shipped. Five properties over random types: a type equals a distinct copy of itself; a type meets a distinct copy of itself (the same thing stated as the user-visible symptom); equality is symmetric, since the bug was order-dependent pairing and asymmetry is the shape worth pinning rather than the one instance; equal types normalize equally, which is the soundness condition fresh_ascription's fast path depends on; and normalize is idempotent. Copies go through a sexp round trip, since comparing a value with itself can short-circuit on physical equality and miss exactly this class. Verified against the real defect: restoring the buggy venn_regions fails three of the five in 0.6s, where the confluence property took ~100s and found it by accident. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The examples were written in February against logic that the probe study has since revised. Three are re-derived from the July study tasks with views ported onto the newer logic — crop-plotter, which replaces emojipaint with the same idea reskinned as planting a grove, plus harvest-streak and nutrient-rotation. Tictactoe takes the study's Sprout/Shroom board; its players are renamed to match, since a view that says "X's turn" over 🌱 and 🍄 reads wrong. Note the two study sources are DEBUGGING tasks and ship planted bugs: harvest-streak compares a quality to itself so every harvest continues a streak, and nutrient-rotation's Nightshade boosts phosphorus where its own comment and test expect nitrogen. Both fail their own tests. Our logic was kept in those two places. The seven keepers get light night-garden theming — moonflowers, a watering timer, a planting list, a firefly steered with the arrow keys, a garden that spreads by Conway's rules — renaming captions and identifiers without touching what each example teaches. Dead/Alive and Glider stay as they are. todo-list's input row becomes a real Form with OnSubmit, Required and a Disabled submit button, which is the first coverage OnSubmit has had. Dropped: animation (AnimationFrame can't reach 60fps against the current whole-page vdom floor), tamagotchi and lunar-growth to keep the set tight, emojipaint as superseded, and _test_eval as a working file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The eleven example programs are now slides under docs/mvu/, encoded with `hazel slide-encode` and registered alongside the existing documentation. Each opens as a working Elm-style app: project the 4-tuple at the bottom to run it. Module names carry an Mvu prefix because `include_subdirs unqualified` flattens the namespace, and Timer and TodoList are names the web library could plausibly want later. The encoded segments are 4.2MB of source and add about the same to the dev bundle, which is the cost of shipping working programs rather than listings. The .hz files stay the editable source; re-encode after editing them or the slide and the file drift apart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Value and TextArea set only the `value` attribute. Once a user has typed into an input the browser stops mirroring that attribute onto the displayed value, so an app could never clear or reset a field: after a form submit the typed text stayed visible while the model believed it was empty, and a `Disabled(input == "")` submit button went dead with text still on screen. Set the property alongside the attribute (Attr.value_prop). Also flip the runtime's update call to `update(model, action)`, matching the argument order the example programs now use. The order lives in the tuple AppStore.dispatch builds, so changing only the program signatures silently passed the action where the model belongs; views then rendered unevaluated source text. Neither `analyze` nor `test` catches that class of bug, since tests apply `update` directly and never exercise the runtime's call site. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Style: the examples were on a GitHub-dark palette that read as foreign widgets inside a warm parchment editor. They now use Hazel's own palette through var(), so they track it rather than snapshotting it, with the editor's own conventions — hairline BR1 rules, small radii mixed with square edges, one typeface, four sizes, and colour only where it carries meaning. Root cards have no padding so section rules run the full card width; buttons sit in uniform grids. Firefly keeps a dark sky, now a portrait grove so it fits the sidebar without a horizontal scrollbar, and Seed Catalog's night mode reuses that same sky. API: `Msg` becomes `Action` (five examples already said Action), and update takes the model first. That matches fold_left's (acc, elem) and Hazel's subject-first convention, so update folds straight over a list of actions — four programs had a shim existing only to flip the arguments, now gone. The nine programs that never issue a command return a bare Model and lift it with a local noCmd at the tuple, keeping the 4-tuple's shape uniform; the runtime tolerates a bare model via a warning fallback, but relying on it would put two update types through one slot. Seed Catalog issues real commands and returns the pair directly. Also: drop full-app, use real emoji where a field was named `emoji` and held ASCII, replace tictactoe's staircase of nested cases with the winning lines as data, give Seed Catalog named field setters instead of respelling six fields per branch, and root out vertical margins, which overflow the projector box at any size and leave a permanent scrollbar. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The encoded slides move from src/web/init/docs/mvu to their own library, mirroring src/b2t2, and register under an "MVU / " prefix so they collapse into a nested dropdown like the B2T2 and Derivations slides. regen-slides.sh is now the way to rebuild them. Nothing detects a stale encoding, so an edited .hz with no re-encode silently keeps shipping the old program. Beyond a plain slide-encode it has to do four things: - Strip leading indentation. Hazel computes indentation at layout time and renders literal leading spaces on top of it, so baked-in indentation shows up doubled and drifting. slide-decode hides this, printing raw segment text rather than the layout. - Wrap the trailing tuple in ^^html(...) so a slide opens with its app already running, and switch the projector to placement Sidebar. - Set a per-program size. HTMLProj defaults to 40x12 character cells, which clips all but the counter; the sizes here are measured from each app's rendered content height, with headroom for the three that grow as you use them. - Format the result, since slide-encode's output is not ocamlformat-clean and would otherwise fail the next `make test-quick` on the promotion alone. check-comments.py guards the trap that Hazel comments are single-line: one spanning two lines does not error at the delimiter, it silently reparses the following text as code and surfaces as a pile of unrelated static errors. A lone `#` on its own line does the same, which is the natural thing to write when separating a title from a description. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AppBridge was 56% comment and ProjectorChip 40%. Keep the non-obvious parts — why core reaches web through refs at all, why the chip owns both its glyph width and its reserved shape — and drop the restatements of what the code says plainly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Matches src/mvu and the "MVU / ..." slide group, so the source directory, the generated library and the docs all use the one name for the concept. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A slide stores a serialized zipper AND a backup_text, and DocSlides.ReparseBackuptext asserts that parsing the text reproduces the segment. Only what the TEXT can express survives, so a slide can carry a projector only in its default model state. The MVU slides broke that three ways; all ten failed. Placement now lives in the invoke token (`^^html_sidebar`), threaded through Token, Triggers, ProjectorPerform and ProjectorInit. The emit side was the actual bug: projector_to_invoke built text from the kind alone and dropped pr.placement, so a docked projector could never round-trip at all. HTMLProj no longer keeps `exp` in its model. It duplicated the projector's own syntax, nothing ever updated it — so as a fallback it would render the expression as it was when the projector was first added — and it embedded ids, which a reparse regenerates. It is the only projector that stored a term; the rest are unit, an enum or a small record. Its one reader now takes the syntax as the sole source, and an unconvertible syntax renders "no value yet" rather than a stale app. Docked HTML projectors size to their content instead of to ui.rows, so the slides no longer bake a per-app size either. The hand-tuned sizes only existed because the docked box was pinned at 40x12 and clipped; the panel now fits the app exactly. Vertical resize is hidden while docked, since the app decides its own height. Also raise the CI test timeout to 60 minutes: `make ci` runs the suite under bisect_ppx with --force, and the doc-slide tests dominate it. 20 was not enough once these slides landed, and neither was 30. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Kept the Ascriptions builtin-ctx init alongside dev's extracted Trampoline; gave the HTML projector a ProjectorCatalog entry; threaded the Updated.save field through agentCore's fold; adapted Test_MVU to the eval_info-based evaluate signature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Elm-style web apps as Hazel programs. Write a 4-tuple
(init, update, view, subs)anywhere in a program, project it, and it runs:Editing
updatewhile it runs rebinds the live app and keeps its state, as long as the newviewstill accepts the old model.HTML,Attr,Cmd,SubandKeyEvent/MouseEventare builtin types;HazelDOMrenders values of them to vdom. App state lives inAppStore, an id-keyed store in page state rather than in the syntax — models hold closures, andSetModelis an edit action, so putting it in the tree would mean whole-document statics per message. Dispatch isis_edit=falseand non-historic, so clicking a button doesn't touch statics or undo.The model comes first in
update, matchingfold_left's(acc, elem)and Hazel's subject-first convention, soupdatefolds directly over a list of actions.updatealways returns(Model, Cmd); a program that issues no commands can return a bareModeland lift it with a two-linenoCmdat the tuple, which keeps the 4-tuple's shape uniform. (The runtime tolerates a bare model via a warning fallback, but relying on it would put twoupdatetypes through one slot.)There is one HTML projector, not two. It picks its commit target per render from the projected expression's live value: a 4-tuple gets state commit, where the model lives in
AppStore; bare HTML gets syntax commit, where the expression itself is the model and an action is anHtml -> Htmltransform spliced back in. So "self-modifying HTML" is not a separate feature, just what the projector does when handed HTML — and it only works on closed HTML, since evaluation erases variables before the splice.To facilitate displaying HTML apps alongside code, all projectors gain a generic placement toggle (
Inline | Sidebar): Alt+S on any projector docks it to a new Projectors panel, leaving a chip at the code site.Ten example apps in
hazel-programs/mvu/, each also shipping as a documentation slide under MVU / … with its app already running and docked.regen-slides.shre-encodes the slides from the.hzsources; nothing detects a stale encoding, so that script is the only correct way to edit them. The generated slides insrc/mvu/are most of this PR's diff — ~54k lines, in line with the existingsrc/b2t2(5.8 MB) andsrc/web/init/docs(1.3 MB) slide libraries.Also here because the ~50-variant recursive builtin types made it necessary:
ConstructorMap.venn_regionswas O(n^2), builtin constructor annotations now stay in compactVarform instead of expanding, and physical-equality fast paths were added toTyp.meet/normalize/Equality.docs/mvu.mdis the feature doc: program shape, the type and event reference, the design decisions and the known limitations.Known caveats:
initaccepts anyVar/Ap(the value can only be checked after evaluation), so the HTML context-menu entry offers itself broadly;AppStore.gcis implemented but unwired; the Projectors panel shows only the reported editor's projectors; subscriptions are torn down and recreated on every model change, so timers restart their countdown each tick. Opening a slide shows the code plus a chip, and the app appears once the Projectors panel is revealed — the panel choice then persists, so that is one click per browser profile rather than per slide.Not caused by this branch but worth flagging, since interactive apps make it visible: an MVU dispatch is dominated by
IncrEval.remove_pat_bindings(~50%) and UUID minting during evaluation (~30%). Measured, with a writeup, but untouched here.