Skip to content

Conversation

@disconcision
Copy link
Member

Adds an Elm-like vdom wrapper via Hazel builtins. Subsumes #1812.

disconcision and others added 30 commits July 19, 2025 23:46
# 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 <[email protected]>
- 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 <[email protected]>
- 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 <[email protected]>
- 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 <[email protected]>
- 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 <[email protected]>
- 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 <[email protected]>
- 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 <[email protected]>
- 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 <[email protected]>
- 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 <[email protected]>
- 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 <[email protected]>
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 <[email protected]>
disconcision and others added 27 commits February 5, 2026 03:16
- Mark HTMLProj init improvements as complete
- Mark example programs as complete
- Mark sidebar panel as partially complete (placeholder)
- Note architectural limitations for resize and sidebar features

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Thread CellEditor.Model.t through to AppViewPanel so it can access
the evaluation result. When the evaluated expression is a valid HTML
type (Div, Span, Text, etc.), render it using HazelDOM.render_elem.

- Add get_cell_editor to Page.re to extract full CellEditor
- Update Sidebar.re to accept and pass cell_editor parameter
- Rewrite AppViewPanel to render HTML from evaluation results
- Show instructions when no valid HTML result is available

Co-Authored-By: Claude Opus 4.5 <[email protected]>
The HTMLProj model now includes UI state (width, height, resizing mode).
A resize handle on the right edge allows dragging to change width.
The projector wrapper applies size styles from the UI state.

- Add ui_state type with width, height, resizing fields
- Add actions: SetWidth, SetHeight, StartResize, StopResize, ResetSize
- Update view to wrap content in resizable container
- Add resize handle with mousedown/mousemove handlers

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Event handlers in the App View sidebar now work:
- Add app_view_html field to Globals.Model for runtime HTML state
- Add SetAppViewHtml/ResetAppView actions to Globals.Action
- Wire inject in Sidebar.re to dispatch SetAppViewHtml
- Update AppViewPanel to prefer state over evaluation result
- Add Reset button to return to evaluation result

When an event handler fires in the App View, it updates the global
state, which triggers a re-render with the new HTML.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
The App View sidebar now properly handles both plain HTML and full App
types ((HTML, Cmd), HTML -> Sub):

- Add detect_app and looks_like_app functions
- Add evaluate helper for evaluating subscription expressions
- Run init command when App type is detected
- Evaluate and pass subscriptions to HazelDOM
- Fall back to plain HTML rendering for non-App expressions

This enables full app functionality in the sidebar, including commands
and subscriptions (timer, keyboard events, animation frames, etc.)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Wrap HTML rendering and App handling in try/catch blocks to gracefully
handle runtime errors:

- render_html_content catches exceptions during HazelDOM rendering
- App type handling catches exceptions during cmd/sub evaluation
- Errors display a red error box with the exception message

This prevents crashes from bubbling up and breaking the entire UI.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Mark completed items in the plan:
- Resizable HTML projector with drag handle
- App View sidebar panel with full interactivity
- Interactive state management with Reset button
- Error boundaries for graceful failure handling
- Example programs

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Hazel uses 'fun x -> body' for lambdas, not '=>'.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Browser profiling showed 95%+ of cursor movement time was spent in
Equality.typ comparing Sum types via ConstructorMap operations.

The root cause was venn_regions using List.partition for each element,
giving O(n²) complexity. With the HTML type having many constructors,
this was devastating for performance (~900ms per cursor movement).

Fix: Add physical equality short-circuits (===) to avoid expensive
structural comparison when comparing identical objects:

- Equality.re: typ and exp functions return immediately if t1 === t2
- ConstructorMap.re: equal, meet, match_synswitch check physical
  equality first, and equal also checks length mismatch early

This works because the statics system often compares the same type
objects against themselves due to AST sharing.

Result: Cursor movement is now responsive (<100ms vs 900ms+).

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Root cause: Typ.normalize and all_ids_temp traverse entire type
  structure for every expression during elaboration
- For large types like HTML (~40 constructors), this is expensive
- Added experimental Typ.normalize memoization (with soundness caveat)
- Profiling shows 13k+ normalize calls for simple counter program
- Low cache hit rate suggests types are not being shared/reused
- Documents remaining questions and future directions

Co-Authored-By: Claude Opus 4.5 <[email protected]>
WARNING: This optimization may not be semantically sound.

The cache keys by type ID only, ignoring context. This could be
unsafe if the same type ID appears in different contexts where
it should normalize differently (e.g., shadowed type aliases).

For concrete types like HTML (no free type variables), this
should be safe. But the general case needs more analysis.

Changes:
- Typ.re: Add normalize_cache (Id.Map), memoize normalize results
- Elaborator.re: Reset cache at start of elaboration, add instrumentation

Performance improvement observed but not fully validated.
Consider reverting if issues arise.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit includes work-in-progress changes:

1. Performance instrumentation (TimeUtil, CachedStatics, Statics, Evaluator,
   CodeViewable, CodeWithStatics, Page) - timing logs for profiling

2. MVU architecture changes (Globals, AppViewPanel, Sidebar, Page) -
   moving evaluation from view to update handler

These changes are useful for debugging but may need cleanup.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Refactor the event system from model-passing (Html -> Html handlers)
to message-based dispatch (handlers produce msgs routed through update).

Architecture:
- Apps are 4-tuples: (init_model, update, view, subs)
- update: (msg, model) -> model (or (model, Cmd))
- Handlers: OnClick(msg), OnInput(String -> msg), etc.
- Dual-mode via update_fn: option(DHExp.t) on HazelDOM.t
  Some = Elm mode, None = legacy mode (inline projector unchanged)

Key changes:
- Add update_fn field to HazelDOM.t, SubManager.context, CmdRunner.context
- Add AppViewMsg action + handler in Page.re with evaluate_direct
  (skips re-elaboration for already-evaluated runtime values)
- Dual-mode handler dispatch in HazelDOM (on_, on_input, on_mouse, on_key)
- Dual-mode SubManager.apply_handler and CmdRunner.Delay
- Smart inject in Sidebar.re: AppViewMsg for Elm apps, SetAppViewModel for legacy
- 4-tuple detection in AppViewPanel.re (ElmApp vs LegacyMvuApp)
- Update BuiltinsADT.re handler types to use Unknown(Internal) for msg/model
- strip_wrappers utility to handle Asc/Closure/Parens from evaluator
- Improved fallback rendering: abbreviated Hazel code instead of raw AST dump
- Update example programs for new architecture

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Fallback view now renders abbreviated Hazel code using
  Abbreviate → ExpToSegment → ProjectorView.flex_code pipeline
  instead of raw DHExp.show text dump
- Add type annotations to MVU example programs (mvu-counter, timer, full-app)
- Note: full-app update left unannotated (polymorphic return: model or (model, Cmd))

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- keyboard-game.hz: Rewritten as 4-tuple MVU app with inline max/min helpers,
  OnDocumentKeyDown(fun key_event -> key_event) subscription pattern
- animation.hz: Rewritten as 4-tuple MVU app with nested pair model,
  AnimationFrame(fun timestamp -> timestamp) subscription pattern
- counter.hz: Added missing fun keywords (legacy mode unchanged)
- todo-list.hz: Added missing fun keywords (legacy mode unchanged)

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…l_ids_temp/fix_typ_ids

- Add physical equality (===) check at top of Typ.meet: 12x statics speedup
  (252ms -> 21ms for full_app). Unconditionally correct since meet is idempotent.
- Add === short-circuits to ConstructorMap.meet/equal/match_synswitch and
  Equality.typ/exp.
- Remove unsound normalize cache (57 test failures, inconsistent perf).
- Disable all_ids_temp and fix_typ_ids in Elaborator: 1.8x elab speedup
  (1273ms -> 703ms). These traversals replaced all type IDs with temporaries
  then reassigned real IDs — the traversal cost itself was 55% of elab time.
- Add benchmark harness (bench/bench.re) with profiling counters.
- Add elaboration profiling instrumentation for normalize, match_synswitch,
  all_ids_temp, fix_typ_ids breakdown.
- Document findings in docs/large-sum-type-performance.md.
- Fix constructor collision in sum type tests (A->Qux, B->Baz).

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Elaborator: remove eager normalize from elaborated_type/elaborated_pat_type.
Normalize now only at specific use sites: uexp_elab return (once per program),
fresh_ascription (with fast_equal shortcut + ctx param), case-specific calls
(Asc, Constructor, TypAp). Let label rearrangement uses weak_head_normalize.

full_app elab: 1,273ms -> 151ms (8.4x speedup). Meet calls: 89K -> 184.
Sum meets in elab: 786 -> 0 (eliminated).

Benchmark: add post-eval statics profiling (Statics.mk on evaluated result).
Reveals post-eval statics as new bottleneck: 2,353ms for full_app (102x slower
than pre-eval statics) due to physical equality broken by web worker boundary.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Post-eval statics for full_app: 2,249ms → ~1,270ms (-44%).

1. Hash-based venn_regions in ConstructorMap (O(n²) → O(n+m))
2. Skip subst in Rec-Rec meet when type variable names match
3. Allocation-preserving subst (return original when unchanged)
4. Unknown-Unknown meet: skip allocation when provenances equal
5. Arrow/List/TupLabel meet: return original when children unchanged
6. ConstructorMap.map: preserve physical equality for normalize

Also: unthunk IdTagged.IdTag.temp to avoid unnecessary allocations.
Update performance investigation docs with benchmark results.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Change IdTag.temp() to IdTag.temp (constant, not thunked) across
  CachedStatics, Exp, TPat, TempGrammar, Test_Menhir, Test_Statics_Prelude
- Reformat BuiltinsADT.re Sub/App definitions for readability
- Disable always_render in ExpToSegment fold_fun_if (perf concern)
- Update keyboard-game example to extract key name from event tuple
- Remove unused counter.hz example (superseded by MVU counter)

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Fix AnimationFrame subscription: controlled recursive loop with proper
  cleanup via running ref and EventHandle
- Fix keyboard event capture: use capture phase (Js._true) so
  OnDocumentKeyDown/Up subscriptions fire before editor handlers
- Add cleanup_sidebar_subscriptions for proper ResetAppView cleanup
- Add stable sidebar projector ID for consistent subscription management
- Refactor Page.re evaluate helpers for clarity
- Add Test_MVU.re with comprehensive MVU architecture tests

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…path

Post-eval statics on HTML apps (47-constructor sum type) dropped from
2,133ms to 3.7ms — a 577x improvement. Sum meets: 17,733 → 0.

Root cause: ctr_ana_typ unrolls Rec types, producing expanded types that
survive through meet results into constructor annotations. Post-eval
statics then does expensive O(n^2) structural comparison on every meet.

Fix has three parts:

1. compact_builtin_recs (Elaborator.re): Replaces Rec("HTML",...) back
   to Var("HTML") in constructor type annotations for unshadowed builtin
   aliases. fresh_ascription stores unnormalized types so Asc nodes keep
   compact Var references.

2. Lazy resolution (Ascriptions.re): Instead of assuming pre-normalized
   types, resolves Var references lazily via weak_head_normalize with a
   builtin context (set via Ascriptions.set_ctx in Evaluator.re). This
   is architecturally aligned with future type closures.

3. Var-Rec fast path (Typ.meet): When meet(Var(name), Rec(name,...))
   encounters a Var that resolves to the same-named Rec type alias,
   returns the compact Var form directly without structural comparison.

Also adds meet profiling counters (phys_eq, var_expand, unknown),
enhanced shape diagnostics in bench.re, regression tests for
constructor annotation compactness, and Statics.re optimization
to reuse existing constructor type annotations.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…, add CLI test command

- Rewrite all 5 existing MVU programs (timer, full-app, animation,
  keyboard-game, todo-list) to use labeled tuples and sum type messages
  instead of positional tuples and bare values. Add test suites (49 tests).
- Add 2 new MVU programs: emojipaint (3x3 emoji grid) and tictactoe
  (with win/draw detection), both with HTML views and tests.
- Fix critical runtime bug in Page.re: Tuple([m, c]) pattern matched any
  2-element tuple as (model, cmd), breaking labeled tuple models with 2
  fields. Now checks is_cmd(c) guard before splitting.
- Add CLI `test` command for running .hz test suites from command line.
  Upgrade `analyze` with Rust-style error output (line numbers, carets).
- Fix keyboard focus: Page.re key_handler now yields to focused
  INPUT/TEXTAREA/SELECT elements instead of intercepting all keystrokes.
- Add debug logging to HazelDOM render_elem for diagnosing render failures.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Documents the Elm-style MVU system: program structure, event handlers,
commands, subscriptions, runtime dispatch cycle, divergences from Elm,
and known limitations. Legacy self-modifying pattern documented in
isolated section with specific entanglement points listed.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Replace the inline(10) placeholder with a Block(rows-1) placeholder sized
in character units (default 40×12). Add corner drag resize using pointer
capture: during drag, cursor position is converted to char units and
SetDimensions is dispatched when values change, giving live feedback as
the editor layout reflows.

Fix framework bug where SetModel was not treated as an edit in Action.is_edit,
preventing CachedSyntax from recomputing the shape_map when projector models
changed.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Change AppViewState.update_fn from DHExp.t to option(DHExp.t) to cleanly
distinguish Elm apps (Some) from legacy apps (None) at the type level.
This replaces runtime function-shape inspection in Sidebar.re and the
is_cmd check in Page.re.

Remove debug print_endline calls from HazelDOM.re, SubManager.re, and
Page.re. Update MVU docs to reflect the simplified architecture. Add
MVU-render CSS height rule.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
@disconcision disconcision mentioned this pull request Feb 9, 2026
4 tasks
@disconcision
Copy link
Member Author

Screenshot 2026-02-10 at 9 21 46 PM Screenshot 2026-02-10 at 9 21 30 PM Screenshot 2026-02-10 at 9 21 12 PM Screenshot 2026-02-10 at 9 20 52 PM Screenshot 2026-02-10 at 9 20 40 PM Screenshot 2026-02-10 at 9 20 30 PM Screenshot 2026-02-10 at 9 20 17 PM Screenshot 2026-02-10 at 9 20 09 PM Screenshot 2026-02-10 at 9 20 01 PM Screenshot 2026-02-10 at 9 19 50 PM Screenshot 2026-02-10 at 9 19 28 PM Screenshot 2026-02-10 at 9 19 14 PM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant