OCaml VM browser: safe equality, thunk trampolining, platform functions, nav pipeline

Core runtime fixes:
- Safe equality (=, !=): physical equality for dicts/lambdas/signals,
  structural only for acyclic types. Prevents infinite loops on circular
  signal subscriber chains.
- contains?: same safe comparison (physical first, structural for simple types)
- Thunk trampolining in as_number and to_string: leaked thunks auto-resolve
  instead of showing <thunk> or erroring "Expected number, got thunk"
- Diagnostic first error: shows actual type received

Island hydration fixes:
- adapter-dom.sx: skip scope-emit for spreads inside islands (was tripling classes)
- schedule-idle: wrap callback to absorb requestIdleCallback deadline arg
- home-stepper: remove spread-specific highlighting (all tokens same style per step)

Platform functions (boot-helpers.sx):
- fetch-request: 3-arg interface (config, success-fn, error-fn) with promise chain
- build-request-body: form serialization for GET/POST
- strip-component-scripts / extract-response-css: SX text processing
- Navigation: bind-boost-link, bind-client-route-click via execute-request
- Loading state: show-indicator, disable-elements, clear-loading-state
- DOM extras: dom-remove, dom-attr-list (name/value pairs), dom-child-list (SX list),
  dom-is-active-element?, dom-is-input-element?, dom-is-child-of?, dom-on,
  dom-parse-html-document, dom-body-inner-html, create-script-clone
- All remaining stubs: csrf-token, loaded-component-names, observe-intersection,
  event-source-connect/listen, with-transition, cross-origin?, etc.

Navigation pipeline:
- browser-push-state/replace-state: accept 1-arg (URL only) or 3-arg
- boot.sx: wire popstate listener to handle-popstate
- URL updates working via handle-history + pushState fix

Morph debugging (WIP):
- dom-child-list returns proper SX list (was JS Array)
- dom-query accepts optional root element for scoped queries
- Navigation fetches and renders SX responses, URL updates, but morph
  doesn't replace content div (investigating dom-child-list on new elements)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 12:57:24 +00:00
parent 5aea9d2678
commit 07bbcaf1bb
14 changed files with 41905 additions and 50 deletions

View File

@@ -29,8 +29,11 @@
;; --------------------------------------------------------------------------
(define dom-create-element
(fn (tag)
(host-call (dom-document) "createElement" tag)))
(fn (tag &rest ns-arg)
(let ((ns (if (and ns-arg (not (empty? ns-arg))) (first ns-arg) nil)))
(if ns
(host-call (dom-document) "createElementNS" ns tag)
(host-call (dom-document) "createElement" tag)))))
(define create-text-node
(fn (s)
@@ -40,6 +43,10 @@
(fn ()
(host-call (dom-document) "createDocumentFragment")))
(define create-comment
(fn (text)
(host-call (dom-document) "createComment" (or text ""))))
;; --------------------------------------------------------------------------
;; Tree manipulation
@@ -60,6 +67,54 @@
(when (and parent child)
(host-call parent "insertBefore" child ref))))
(define dom-insert-after
(fn (ref node)
"Insert node after ref in the same parent."
(let ((parent (host-get ref "parentNode"))
(next (host-get ref "nextSibling")))
(when parent
(if next
(host-call parent "insertBefore" node next)
(host-call parent "appendChild" node))))))
(define dom-remove
(fn (el)
(when el (host-call el "remove"))))
(define dom-is-active-element?
(fn (el)
(let ((active (host-get (dom-document) "activeElement")))
(if (and active el)
(identical? el active)
false))))
(define dom-is-input-element?
(fn (el)
(let ((tag (upper (or (dom-tag-name el) ""))))
(or (= tag "INPUT") (= tag "TEXTAREA") (= tag "SELECT")))))
(define dom-is-child-of?
(fn (child parent)
(and child parent (host-call parent "contains" child))))
(define dom-on
(fn (el event-name handler)
(host-call el "addEventListener" event-name (host-callback handler))))
(define dom-attr-list
(fn (el)
;; Return list of (name value) pairs for all attributes on the element.
(let ((attrs (host-get el "attributes"))
(result (list)))
(when attrs
(let ((n (host-get attrs "length")))
(let loop ((i 0))
(when (< i n)
(let ((attr (host-call attrs "item" i)))
(append! result (list (host-get attr "name") (host-get attr "value"))))
(loop (+ i 1))))))
result)))
(define dom-remove-child
(fn (parent child)
(when (and parent child)
@@ -80,16 +135,29 @@
;; --------------------------------------------------------------------------
(define dom-query
(fn (sel)
(host-call (dom-document) "querySelector" sel)))
(fn (root-or-sel &rest rest)
(if (empty? rest)
;; Single arg: selector on document
(host-call (dom-document) "querySelector" root-or-sel)
;; Two args: root element + selector
(host-call root-or-sel "querySelector" (first rest)))))
(define dom-query-all
(fn (root sel)
(if (nil? sel)
;; Single arg: query document
(host-call (dom-document) "querySelectorAll" root)
;; Two args: query within root
(host-call root "querySelectorAll" sel))))
"Query DOM and return an SX list (not a host NodeList)."
(let ((node-list (if (nil? sel)
(host-call (dom-document) "querySelectorAll" root)
(host-call root "querySelectorAll" sel))))
;; Convert NodeList → SX list by indexing
(if (nil? node-list)
(list)
(let ((n (host-get node-list "length"))
(result (list)))
(let loop ((i 0))
(when (< i n)
(append! result (host-call node-list "item" i))
(loop (+ i 1))))
result)))))
(define dom-query-by-id
(fn (id)
@@ -226,13 +294,46 @@
(define dom-child-list
(fn (el)
"Return child nodes as an SX list."
(if el
(host-call (host-global "Array") "from" (host-get el "childNodes"))
(let ((nl (host-get el "childNodes"))
(n (host-get nl "length"))
(result (list)))
(let loop ((i 0))
(when (< i n)
(append! result (host-call nl "item" i))
(loop (+ i 1))))
result)
(list))))
(define dom-is-fragment?
(fn (el) (= (host-get el "nodeType") 11)))
(define dom-child-nodes
(fn (el)
"Return child nodes as an SX list."
(if el
(let ((nl (host-get el "childNodes"))
(n (host-get nl "length"))
(result (list)))
(let loop ((i 0))
(when (< i n)
(append! result (host-call nl "item" i))
(loop (+ i 1))))
result)
(list))))
(define dom-remove-children-after
(fn (marker)
"Remove all siblings after marker node."
(let ((parent (dom-parent marker)))
(when parent
(let loop ()
(let ((next (dom-next-sibling marker)))
(when next
(host-call parent "removeChild" next)
(loop))))))))
(define dom-focus
(fn (el) (when el (host-call el "focus"))))
@@ -254,6 +355,17 @@
;; Return cleanup function
(fn () (host-call el "removeEventListener" event-name cb)))))
;; dom-add-listener — addEventListener with optional options
;; Used by orchestration.sx: (dom-add-listener el event handler opts)
(define dom-add-listener
(fn (el event-name handler &rest opts)
(let ((cb (host-callback handler)))
(if (and opts (not (empty? opts)))
(host-call el "addEventListener" event-name cb (first opts))
(host-call el "addEventListener" event-name cb))
;; Return cleanup function
(fn () (host-call el "removeEventListener" event-name cb)))))
(define dom-dispatch
(fn (el event-name detail)
(let ((evt (host-new "CustomEvent" event-name