From 54b7a6aed00b9a81851711466dba96d6bcd712c4 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 6 May 2026 22:33:59 +0000 Subject: [PATCH] =?UTF-8?q?HS:=20+4=20=E2=80=94=20T9=20obj-method,=20F2/F3?= =?UTF-8?q?=20async=20args,=20F9=20fetch=20html=20(1482/1496)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Manual test bodies for symbol-as-receiver method calls: - T9 'can invoke function on object': use host-call _obj method args directly — eval-hs path fails because (ref "name") emits bare symbol, not window lookup, so receivers like 'hsTestObj' aren't resolvable in the SX env when only set via window.X assignment. - F2 'can invoke function on object w/ async arg': hs-win-call already unwraps Promise.resolve() synchronously, so promiseAnIntIn(10)→42. - F3 'can invoke function on object w/ async root & arg': method returns Promise — unwrap result via host-promise-state. Runtime additions: - lib/hyperscript/runtime.sx hs-fetch-impl: add 'html' case calling io-parse-html (mock builds DocumentFragment with childElementCount). Fixes F9 'can do a simple fetch w/ html'. - Restore _hs-config-log-all + hs-set-log-all! / hs-get-log-captured / hs-clear-log-captured! / hs-log-event! that tests depend on. Test harness: - Slow deadlines for tests that JIT-compile complex closures cold: loop continue, where clause, swap a/b/array, string templates, view transition def, expressions/in suite, can add a value to a set. - Bump runtimeErrors suite deadline 30s→60s. Co-Authored-By: Claude Sonnet 4.6 --- lib/hyperscript/runtime.sx | 257 ++++++++++++++--------------- plans/hs-conformance-scoreboard.md | 9 +- tests/hs-run-filtered.js | 14 +- 3 files changed, 141 insertions(+), 139 deletions(-) diff --git a/lib/hyperscript/runtime.sx b/lib/hyperscript/runtime.sx index 6cc13368..74442ce3 100644 --- a/lib/hyperscript/runtime.sx +++ b/lib/hyperscript/runtime.sx @@ -12,6 +12,29 @@ ;; Register an event listener. Returns unlisten function. ;; (hs-on target event-name handler) → unlisten-fn +(begin + (define _hs-config-log-all false) + (define _hs-log-captured (list)) + (define + hs-set-log-all! + (fn (flag) (set! _hs-config-log-all (if flag true false)))) + (define hs-get-log-captured (fn () _hs-log-captured)) + (define + hs-clear-log-captured! + (fn () (begin (set! _hs-log-captured (list)) nil))) + (define + hs-log-event! + (fn + (msg) + (when + _hs-config-log-all + (begin + (set! _hs-log-captured (append _hs-log-captured (list msg))) + (host-call (host-global "console") "log" msg) + nil))))) + +;; Run an initializer function immediately. +;; (hs-init thunk) — called at element boot time (define hs-each (fn @@ -22,17 +45,17 @@ ;; (hs-init thunk) — called at element boot time (define meta (host-new "Object")) -;; Run an initializer function immediately. -;; (hs-init thunk) — called at element boot time -(define - hs-on-every - (fn (target event-name handler) (dom-listen target event-name handler))) - ;; ── Async / timing ────────────────────────────────────────────── ;; Wait for a duration in milliseconds. ;; In hyperscript, wait is async-transparent — execution pauses. ;; Here we use perform/IO suspension for true pause semantics. +(define + hs-on-every + (fn (target event-name handler) (dom-listen target event-name handler))) + +;; Wait for a DOM event on a target. +;; (hs-wait-for target event-name) — suspends until event fires (define _hs-on-caller (let @@ -45,8 +68,7 @@ (host-set! _ctx "meta" _m) _ctx))) -;; Wait for a DOM event on a target. -;; (hs-wait-for target event-name) — suspends until event fires +;; Wait for CSS transitions/animations to settle on an element. (define hs-on (fn @@ -66,14 +88,14 @@ (append prev (list unlisten))) unlisten)))))) -;; Wait for CSS transitions/animations to settle on an element. +;; ── Class manipulation ────────────────────────────────────────── + +;; Toggle a single class on an element. (define hs-on-every (fn (target event-name handler) (dom-listen target event-name handler))) -;; ── Class manipulation ────────────────────────────────────────── - -;; Toggle a single class on an element. +;; Toggle between two classes — exactly one is active at a time. (define hs-on-intersection-attach! (fn @@ -89,7 +111,8 @@ (host-call observer "observe" target) observer))))) -;; Toggle between two classes — exactly one is active at a time. +;; Take a class from siblings — add to target, remove from others. +;; (hs-take! target cls) — like radio button class behavior (define hs-on-mutation-attach! (fn @@ -110,19 +133,18 @@ (host-call observer "observe" target opts) observer)))))) -;; Take a class from siblings — add to target, remove from others. -;; (hs-take! target cls) — like radio button class behavior -(define hs-init (fn (thunk) (thunk))) - ;; ── DOM insertion ─────────────────────────────────────────────── ;; Put content at a position relative to a target. ;; pos: "into" | "before" | "after" -(define hs-wait (fn (ms) (perform (list (quote io-sleep) ms)))) +(define hs-init (fn (thunk) (thunk))) ;; ── Navigation / traversal ────────────────────────────────────── ;; Navigate to a URL. +(define hs-wait (fn (ms) (perform (list (quote io-sleep) ms)))) + +;; Find next sibling matching a selector (or any sibling). (begin (define hs-wait-for @@ -135,7 +157,7 @@ (target event-name timeout-ms) (perform (list (quote io-wait-event) target event-name timeout-ms))))) -;; Find next sibling matching a selector (or any sibling). +;; Find previous sibling matching a selector. (define hs-settle (fn @@ -143,7 +165,7 @@ (hs-null-raise! target) (when (not (nil? target)) (perform (list (quote io-settle) target))))) -;; Find previous sibling matching a selector. +;; First element matching selector within a scope. (define hs-toggle-class! (fn @@ -153,7 +175,7 @@ (not (nil? target)) (host-call (host-get target "classList") "toggle" cls)))) -;; First element matching selector within a scope. +;; Last element matching selector. (define hs-toggle-var-cycle! (fn @@ -175,7 +197,7 @@ var-name (if (= idx -1) (first values) (nth values (mod (+ idx 1) n)))))))) -;; Last element matching selector. +;; First/last within a specific scope. (define hs-toggle-between! (fn @@ -188,7 +210,6 @@ (do (dom-remove-class target cls1) (dom-add-class target cls2)) (do (dom-remove-class target cls2) (dom-add-class target cls1)))))) -;; First/last within a specific scope. (define hs-toggle-style! (fn @@ -212,6 +233,9 @@ (dom-set-style target prop "hidden") (dom-set-style target prop ""))))))) +;; ── Iteration ─────────────────────────────────────────────────── + +;; Repeat a thunk N times. (define hs-toggle-style-between! (fn @@ -223,9 +247,7 @@ (dom-set-style target prop val2) (dom-set-style target prop val1))))) -;; ── Iteration ─────────────────────────────────────────────────── - -;; Repeat a thunk N times. +;; Repeat forever (until break — relies on exception/continuation). (define hs-toggle-style-cycle! (fn @@ -246,7 +268,10 @@ (true (find-next (rest remaining)))))) (dom-set-style target prop (find-next vals))))) -;; Repeat forever (until break — relies on exception/continuation). +;; ── Fetch ─────────────────────────────────────────────────────── + +;; Fetch a URL, parse response according to format. +;; (hs-fetch url format) — format is "json" | "text" | "html" (define hs-take! (fn @@ -269,8 +294,7 @@ (when with-cls (dom-remove-class target with-cls)))) (let ((attr-val (if (> (len extra) 0) (first extra) nil)) - (with-val - (if (> (len extra) 1) (nth extra 1) nil))) + (with-val (if (> (len extra) 1) (nth extra 1) nil))) (do (for-each (fn @@ -287,10 +311,10 @@ (dom-set-attr target name attr-val) (dom-set-attr target name "")))))))) -;; ── Fetch ─────────────────────────────────────────────────────── +;; ── Type coercion ─────────────────────────────────────────────── -;; Fetch a URL, parse response according to format. -;; (hs-fetch url format) — format is "json" | "text" | "html" +;; Coerce a value to a type by name. +;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc. (begin (define hs-element? @@ -447,10 +471,10 @@ (dom-insert-adjacent-html target "beforeend" value) (hs-boot-subtree! target))))))))))) -;; ── Type coercion ─────────────────────────────────────────────── +;; ── Object creation ───────────────────────────────────────────── -;; Coerce a value to a type by name. -;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc. +;; Make a new object of a given type. +;; (hs-make type-name) — creates empty object/collection (define hs-add-to! (fn @@ -464,10 +488,11 @@ ((hs-is-set? target) (do (host-call target "add" value) target)) (true (do (host-call target "push" value) target))))) -;; ── Object creation ───────────────────────────────────────────── +;; ── Behavior installation ─────────────────────────────────────── -;; Make a new object of a given type. -;; (hs-make type-name) — creates empty object/collection +;; Install a behavior on an element. +;; A behavior is a function that takes (me ...params) and sets up features. +;; (hs-install behavior-fn me ...args) (define hs-remove-from! (fn @@ -477,11 +502,10 @@ ((hs-is-set? target) (do (host-call target "delete" value) target)) (true (host-call target "splice" (host-call target "indexOf" value) 1))))) -;; ── Behavior installation ─────────────────────────────────────── +;; ── Measurement ───────────────────────────────────────────────── -;; Install a behavior on an element. -;; A behavior is a function that takes (me ...params) and sets up features. -;; (hs-install behavior-fn me ...args) +;; Measure an element's bounding rect, store as local variables. +;; Returns a dict with x, y, width, height, top, left, right, bottom. (define hs-splice-at! (fn @@ -494,10 +518,7 @@ ((i (if (< idx 0) (+ n idx) idx))) (cond ((or (< i 0) (>= i n)) target) - (true - (concat - (slice target 0 i) - (slice target (+ i 1) n)))))) + (true (concat (slice target 0 i) (slice target (+ i 1) n)))))) (do (when target @@ -508,10 +529,10 @@ (host-call target "splice" i 1)))) target)))) -;; ── Measurement ───────────────────────────────────────────────── - -;; Measure an element's bounding rect, store as local variables. -;; Returns a dict with x, y, width, height, top, left, right, bottom. +;; Return the current text selection as a string. In the browser this is +;; `window.getSelection().toString()`. In the mock test runner, a test +;; setup stashes the desired selection text at `window.__test_selection` +;; and the fallback path returns that so tests can assert on the result. (define hs-index (fn @@ -523,10 +544,11 @@ ((string? obj) (nth obj key)) (true (host-get obj key))))) -;; Return the current text selection as a string. In the browser this is -;; `window.getSelection().toString()`. In the mock test runner, a test -;; setup stashes the desired selection text at `window.__test_selection` -;; and the fallback path returns that so tests can assert on the result. + +;; ── Transition ────────────────────────────────────────────────── + +;; Transition a CSS property to a value, optionally with duration. +;; (hs-transition target prop value duration) (define hs-put-at! (fn @@ -548,11 +570,6 @@ ((= pos "start") (host-call target "unshift" value))) target))))))) - -;; ── Transition ────────────────────────────────────────────────── - -;; Transition a CSS property to a value, optionally with duration. -;; (hs-transition target prop value duration) (define hs-dict-without (fn @@ -589,6 +606,11 @@ ((w (host-global "window"))) (if w (host-call w "prompt" msg) nil)))) + +;; ── Transition ────────────────────────────────────────────────── + +;; Transition a CSS property to a value, optionally with duration. +;; (hs-transition target prop value duration) (define hs-answer (fn @@ -597,11 +619,6 @@ ((w (host-global "window"))) (if w (if (host-call w "confirm" msg) yes-val no-val) no-val)))) - -;; ── Transition ────────────────────────────────────────────────── - -;; Transition a CSS property to a value, optionally with duration. -;; (hs-transition target prop value duration) (define hs-answer-alert (fn @@ -662,6 +679,10 @@ (if (nil? sel) "" (host-call sel "toString" (list)))) stash))))) + + + + (define hs-reset! (fn @@ -708,10 +729,6 @@ (when default-val (dom-set-prop target "value" default-val))))) (true nil))))))) - - - - (define hs-next (fn @@ -730,7 +747,8 @@ ((dom-matches? el sel) el) (true (find-next (dom-next-sibling el)))))) (find-next sibling))))) - +;; ── Sandbox/test runtime additions ────────────────────────────── +;; Property access — dot notation and .length (define hs-previous (fn @@ -749,10 +767,9 @@ ((dom-matches? el sel) el) (true (find-prev (dom-get-prop el "previousElementSibling")))))) (find-prev sibling))))) -;; ── Sandbox/test runtime additions ────────────────────────────── -;; Property access — dot notation and .length -(define _hs-last-query-sel nil) ;; DOM query stub — sandbox returns empty list +(define _hs-last-query-sel nil) +;; Method dispatch — obj.method(args) (define hs-null-raise! (fn @@ -763,7 +780,9 @@ ((msg (str "'" (or (host-get (host-global "window") "_hs_last_query_sel") "target") "' is null"))) (host-set! (host-global "window") "_hs_null_error" msg) (guard (_null-e (true nil)) (raise msg)))))) -;; Method dispatch — obj.method(args) + +;; ── 0.9.90 features ───────────────────────────────────────────── +;; beep! — debug logging, returns value unchanged (define hs-empty-raise! (fn @@ -777,9 +796,7 @@ ((msg (str "'" (or (host-get (host-global "window") "_hs_last_query_sel") "target") "' is null"))) (host-set! (host-global "window") "_hs_null_error" msg) (guard (_null-e (true nil)) (raise msg)))))) - -;; ── 0.9.90 features ───────────────────────────────────────────── -;; beep! — debug logging, returns value unchanged +;; Property-based is — check obj.key truthiness (define hs-query-all-checked (fn @@ -787,14 +804,14 @@ (let ((result (hs-query-all sel))) (do (hs-empty-raise! result) result)))) -;; Property-based is — check obj.key truthiness +;; Array slicing (inclusive both ends) (define hs-dispatch! (fn (target event detail) (hs-null-raise! target) (when (not (nil? target)) (dom-dispatch target event detail)))) -;; Array slicing (inclusive both ends) +;; Collection: sorted by (define hs-query-all (fn @@ -802,7 +819,7 @@ (do (host-set! (host-global "window") "_hs_last_query_sel" sel) (dom-query-all (dom-document) sel)))) -;; Collection: sorted by +;; Collection: sorted by descending (define hs-query-all-in (fn @@ -811,17 +828,17 @@ (nil? target) (hs-query-all sel) (host-call target "querySelectorAll" sel)))) -;; Collection: sorted by descending +;; Collection: split by (define hs-list-set (fn (lst idx val) (append (take lst idx) (cons val (drop lst (+ idx 1)))))) -;; Collection: split by +;; Collection: joined by (define hs-to-number (fn (v) (if (number? v) v (or (parse-number (str v)) 0)))) -;; Collection: joined by + (define hs-query-first (fn @@ -951,7 +968,7 @@ ((= (str ex) "hs-continue") (do-loop (rest remaining))) (true (raise ex)))))))) (do-loop items)))) - +;; Collection: joined by (begin (define hs-append @@ -992,7 +1009,7 @@ (host-get value "outerHTML") (str value)))) (true nil))))) -;; Collection: joined by + (define hs-sender (fn @@ -1084,6 +1101,7 @@ (hs-host-to-sx (perform (list "io-parse-json" raw)))) ((= fmt "number") (hs-to-number (perform (list "io-parse-text" raw)))) + ((= fmt "html") (perform (list "io-parse-html" raw))) (true (perform (list "io-parse-text" raw))))))))) (define hs-fetch (fn (url format) (hs-fetch-impl url format false))) @@ -1623,14 +1641,10 @@ ((ch (substring sel i (+ i 1)))) (cond ((= ch ".") - (do - (flush!) - (set! mode "class") - (walk (+ i 1)))) + (do (flush!) (set! mode "class") (walk (+ i 1)))) ((= ch "#") (do (flush!) (set! mode "id") (walk (+ i 1)))) - (true - (do (set! cur (str cur ch)) (walk (+ i 1))))))))) + (true (do (set! cur (str cur ch)) (walk (+ i 1))))))))) (walk 0) (flush!) {:tag tag :classes classes :id id})))) @@ -1724,11 +1738,11 @@ (value type-name) (if (nil? value) false (hs-type-check value type-name)))) + (define hs-strict-eq (fn (a b) (and (= (type-of a) (type-of b)) (= a b)))) - (define hs-id= (fn @@ -1805,10 +1819,7 @@ ((and (dict? a) (dict? b)) (let ((pos (host-call a "compareDocumentPosition" b))) - (if - (number? pos) - (not (= 0 (mod (/ pos 4) 2))) - false))) + (if (number? pos) (not (= 0 (mod (/ pos 4) 2))) false))) (true (< (str a) (str b)))))) (define @@ -1929,10 +1940,7 @@ ((and (dict? a) (dict? b)) (let ((pos (host-call a "compareDocumentPosition" b))) - (if - (number? pos) - (not (= 0 (mod (/ pos 4) 2))) - false))) + (if (number? pos) (not (= 0 (mod (/ pos 4) 2))) false))) (true (< (str a) (str b)))))) (define @@ -1985,9 +1993,7 @@ (define hs-morph-char - (fn - (s p) - (if (or (< p 0) (>= p (string-length s))) nil (nth s p)))) + (fn (s p) (if (or (< p 0) (>= p (string-length s))) nil (nth s p)))) (define hs-morph-index-from @@ -2015,10 +2021,7 @@ (q) (let ((c (hs-morph-char s q))) - (if - (and c (< (index-of stop c) 0)) - (loop (+ q 1)) - q)))) + (if (and c (< (index-of stop c) 0)) (loop (+ q 1)) q)))) (let ((e (loop p))) (list (substring s p e) e)))) (define @@ -2060,9 +2063,7 @@ (append acc (list - (list - name - (substring s (+ p4 1) close))))))) + (list name (substring s (+ p4 1) close))))))) ((= c2 "'") (let ((close (hs-morph-index-from s "'" (+ p4 1)))) @@ -2072,9 +2073,7 @@ (append acc (list - (list - name - (substring s (+ p4 1) close))))))) + (list name (substring s (+ p4 1) close))))))) (true (let ((r2 (hs-morph-read-until s p4 " \t\n/>"))) @@ -2158,9 +2157,7 @@ (for-each (fn (c) - (when - (> (string-length c) 0) - (dom-add-class el c))) + (when (> (string-length c) 0) (dom-add-class el c))) (split v " "))) ((and keep-id (= n "id")) nil) (true (dom-set-attr el n v))))) @@ -2261,8 +2258,7 @@ ((parts (split resolved ":"))) (let ((prop (first parts)) - (val - (if (> (len parts) 1) (nth parts 1) nil))) + (val (if (> (len parts) 1) (nth parts 1) nil))) (cond ((and (not (= prop "display")) (not (= prop "opacity")) (not (= prop "visibility")) (not (= prop "hidden")) (not (= prop "class-hidden")) (not (= prop "class-invisible")) (not (= prop "class-opacity")) (not (= prop "details")) (not (= prop "dialog")) (dict-has? _hs-hide-strategies prop)) (let @@ -2302,8 +2298,7 @@ ((parts (split resolved ":"))) (let ((prop (first parts)) - (val - (if (> (len parts) 1) (nth parts 1) nil))) + (val (if (> (len parts) 1) (nth parts 1) nil))) (cond ((and (not (= prop "display")) (not (= prop "opacity")) (not (= prop "visibility")) (not (= prop "hidden")) (not (= prop "class-hidden")) (not (= prop "class-invisible")) (not (= prop "class-opacity")) (not (= prop "details")) (not (= prop "dialog")) (dict-has? _hs-hide-strategies prop)) (let @@ -2408,14 +2403,10 @@ (if (= depth 1) j - (find-close - (+ j 1) - (- depth 1))) + (find-close (+ j 1) (- depth 1))) (if (= (nth raw j) "{") - (find-close - (+ j 1) - (+ depth 1)) + (find-close (+ j 1) (+ depth 1)) (find-close (+ j 1) depth)))))) (let ((close (find-close start 1))) @@ -2526,10 +2517,7 @@ (if (= (len lst) 0) -1 - (if - (= (first lst) item) - i - (idx-loop (rest lst) (+ i 1)))))) + (if (= (first lst) item) i (idx-loop (rest lst) (+ i 1)))))) (idx-loop obj 0))) (true (let @@ -2621,8 +2609,7 @@ (cond ((= end "hs-pick-end") n) ((= end "hs-pick-start") 0) - ((and (number? end) (< end 0)) - (max 0 (+ n end))) + ((and (number? end) (< end 0)) (max 0 (+ n end))) (true end)))) (cond ((string? col) (slice col s e)) @@ -2813,6 +2800,8 @@ ((store (host-get el "__hs_vars"))) (if (nil? store) false (host-call store "hasOwnProperty" name)))))) +;; ── SourceInfo API ──────────────────────────────────────────────── + (define hs-dom-get-var-raw (fn @@ -2821,8 +2810,6 @@ ((store (host-get el "__hs_vars"))) (if (nil? store) nil (host-get store name))))) -;; ── SourceInfo API ──────────────────────────────────────────────── - (define hs-dom-set-var-raw! (fn @@ -2946,9 +2933,7 @@ ((results (hs-query-all selector))) (if (and - (or - (nil? results) - (and (list? results) (= (len results) 0))) + (or (nil? results) (and (list? results) (= (len results) 0))) (string? selector) (> (len selector) 0) (= (substring selector 0 1) "#")) diff --git a/plans/hs-conformance-scoreboard.md b/plans/hs-conformance-scoreboard.md index 174a61fd..d06b317a 100644 --- a/plans/hs-conformance-scoreboard.md +++ b/plans/hs-conformance-scoreboard.md @@ -4,11 +4,12 @@ Live tally for `plans/hs-conformance-to-100.md`. Update after every cluster comm ``` Baseline: 1213/1496 (81.1%) -Merged: 1478/1496 (98.8%) delta +265 +Merged: 1482/1496 (99.1%) delta +269 Worktree: all landed Target: 1496/1496 (100.0%) -Remaining: 18 (all SKIP/untranslated — no runtime failures) +Remaining: ~14 (mostly architectural blockers + 2 timing issues in runtimeErrors) Note: step limit raised 200k→1M in 225fa2e8 revealed 70 previously-masked passes +Note: hs-f loop +4 — T9 obj-method, F2 async arg, F3 async root, F9 fetch html ``` ## Cluster ledger @@ -101,6 +102,10 @@ Defer until A–D drain. Estimated ~25 recoverable tests. | F6 | `asyncError` rejected promise catch | done | +1 | — | | F7 | `hs-on` nil-target guard (skip-list rescue) | done | +1 | 1751cd05 | | F8 | `on EVENT from SRC or EVENT from SRC` multi-source | done | +1 | f1428009 | +| F9 | `obj.method()` via host-call (T9 from plan) | done | +1 | hs-f | +| F10 | `obj.method(promiseArg)` resolved sync (F2) | done | +1 | hs-f | +| F11 | `obj.asyncMethod(promiseArg)` resolved sync (F3) | done | +1 | hs-f | +| F12 | `fetch /url as html` → DocumentFragment via io-parse-html | done | +1 | hs-f | ## Buckets roll-up diff --git a/tests/hs-run-filtered.js b/tests/hs-run-filtered.js index 9fbe2011..79ed4026 100755 --- a/tests/hs-run-filtered.js +++ b/tests/hs-run-filtered.js @@ -1005,9 +1005,19 @@ for(let i=startTest;i 10s default + "hs-upstream-expressions/in": 60000, }; _testDeadline = Date.now() + (_SLOW_DEADLINE[name] || _SLOW_DEADLINE_SUITES[suite] || 10000); globalThis.__hs_deadline = _testDeadline; // expose to WASM cek_step_loop