HS: parser+compiler — toggle for-in lookahead, throttled/debounced modifiers (-2 skips)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 51s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 51s
parser.sx parse-toggle-cmd: when seeing 'toggle .foo for', peek the following two tokens. If they are '<ident> in', it is a for-in loop and toggle does NOT consume 'for' as a duration clause. Restores the trailing for-in to the command list. parser.sx parse-on (handler modifiers): recognize 'throttled at <ms>' and 'debounced at <ms>' as handler modifiers. Captured as :throttle / :debounce kwargs in the on-form parts list. compiler.sx emit-on: pre-extract :throttle / :debounce from parts via new _strip-throttle-debounce helper before scan-on, then wrap the built handler with (hs-throttle! handler ms) or (hs-debounce! handler ms). runtime.sx: hs-throttle! — closure with __hs-last-fire timestamp, fires immediately and drops events arriving within ms of the last fire. hs-debounce! — closure with __hs-timer, clears any pending timer and schedules a new setTimeout(handler, ms) so only the last burst event fires. Both formerly-architectural skips now pass: - "toggle does not consume a following for-in loop" - "throttled at <time> drops events within the window" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -218,6 +218,27 @@
|
||||
((parts (rest ast)))
|
||||
(let
|
||||
((event-name (first parts)))
|
||||
;; Pre-extract :throttle and :debounce kwargs (handler-wrapping modifiers)
|
||||
;; from parts so scan-on doesn't need extra params. Stored as closure vars
|
||||
;; that the handler-build step inside scan-on can read.
|
||||
(define _throttle-ms nil)
|
||||
(define _debounce-ms nil)
|
||||
(define
|
||||
_strip-throttle-debounce
|
||||
(fn
|
||||
(lst)
|
||||
(cond
|
||||
((<= (len lst) 1) lst)
|
||||
((= (first lst) :throttle)
|
||||
(do
|
||||
(set! _throttle-ms (nth lst 1))
|
||||
(_strip-throttle-debounce (rest (rest lst)))))
|
||||
((= (first lst) :debounce)
|
||||
(do
|
||||
(set! _debounce-ms (nth lst 1))
|
||||
(_strip-throttle-debounce (rest (rest lst)))))
|
||||
(true
|
||||
(cons (first lst) (_strip-throttle-debounce (rest lst)))))))
|
||||
(define
|
||||
scan-on
|
||||
(fn
|
||||
@@ -250,6 +271,13 @@
|
||||
((wrapped-body (if catch-info (let ((var (make-symbol (nth catch-info 0))) (catch-body (hs-to-sx (nth catch-info 1)))) (if finally-info (list (quote let) (list (list (quote __hs-exc) nil) (list (quote __hs-reraise) false)) (list (quote do) (list (quote guard) (list var (list true (list (quote let) (list (list var (list (quote host-hs-normalize-exc) var))) (list (quote guard) (list (quote __inner-exc) (list true (list (quote do) (list (quote set!) (quote __hs-exc) (quote __inner-exc)) (list (quote set!) (quote __hs-reraise) true)))) catch-body)))) compiled-body) (hs-to-sx finally-info) (list (quote when) (quote __hs-reraise) (list (quote raise) (quote __hs-exc))))) (list (quote let) (list (list (quote __hs-exc) nil) (list (quote __hs-reraise) false)) (list (quote do) (list (quote guard) (list var (list true (list (quote let) (list (list var (list (quote host-hs-normalize-exc) var))) (list (quote guard) (list (quote __inner-exc) (list true (list (quote do) (list (quote set!) (quote __hs-exc) (quote __inner-exc)) (list (quote set!) (quote __hs-reraise) true)))) catch-body)))) compiled-body) (list (quote when) (quote __hs-reraise) (list (quote raise) (quote __hs-exc))))))) (if finally-info (list (quote do) compiled-body (hs-to-sx finally-info)) compiled-body))))
|
||||
(let
|
||||
((handler (let ((uses-the-result? (fn (expr) (cond ((= expr (quote the-result)) true) ((list? expr) (some (fn (x) (uses-the-result? x)) expr)) (true false))))) (let ((base-handler (list (quote fn) (list (quote event)) (if (uses-the-result? wrapped-body) (list (quote let) (list (list (quote the-result) nil)) wrapped-body) wrapped-body)))) (if count-filter-info (let ((mn (get count-filter-info "min")) (mx (get count-filter-info "max"))) (list (quote let) (list (list (quote __hs-count) 0)) (list (quote fn) (list (quote event)) (list (quote begin) (list (quote set!) (quote __hs-count) (list (quote +) (quote __hs-count) 1)) (list (quote when) (if (= mx -1) (list (quote >=) (quote __hs-count) mn) (list (quote and) (list (quote >=) (quote __hs-count) mn) (list (quote <=) (quote __hs-count) mx))) (nth base-handler 2)))))) base-handler)))))
|
||||
(let
|
||||
((handler (cond
|
||||
(_throttle-ms
|
||||
(list (quote hs-throttle!) handler (hs-to-sx _throttle-ms)))
|
||||
(_debounce-ms
|
||||
(list (quote hs-debounce!) handler (hs-to-sx _debounce-ms)))
|
||||
(true handler))))
|
||||
(let
|
||||
((on-call (if every? (list (quote hs-on-every) target event-name handler) (list (quote hs-on) target event-name handler))))
|
||||
(cond
|
||||
@@ -309,7 +337,7 @@
|
||||
(first pair)
|
||||
handler))
|
||||
or-sources)))
|
||||
on-call)))))))))))))
|
||||
on-call))))))))))))))
|
||||
((= (first items) :from)
|
||||
(scan-on
|
||||
(rest (rest items))
|
||||
@@ -453,7 +481,7 @@
|
||||
count-filter-info
|
||||
elsewhere?
|
||||
or-sources)))))
|
||||
(scan-on (rest parts) nil nil false nil nil nil nil nil false nil)))))
|
||||
(scan-on (_strip-throttle-debounce (rest parts)) nil nil false nil nil nil nil nil false nil)))))
|
||||
(define
|
||||
emit-send
|
||||
(fn
|
||||
|
||||
@@ -1347,7 +1347,17 @@
|
||||
cls
|
||||
(first extra-classes)
|
||||
tgt))
|
||||
((match-kw "for")
|
||||
((and
|
||||
(= (tp-type) "keyword") (= (tp-val) "for")
|
||||
;; Only consume 'for' as a duration clause if the next
|
||||
;; token is NOT '<ident> in ...' — that pattern is a
|
||||
;; for-in loop, not a toggle duration.
|
||||
(not
|
||||
(and
|
||||
(> (len tokens) (+ p 2))
|
||||
(= (get (nth tokens (+ p 1)) "type") "ident")
|
||||
(= (get (nth tokens (+ p 2)) "value") "in")))
|
||||
(do (adv!) true))
|
||||
(let
|
||||
((dur (parse-expr)))
|
||||
(list (quote toggle-class-for) cls tgt dur)))
|
||||
@@ -3079,7 +3089,17 @@
|
||||
(= (tp-val) "queue"))
|
||||
(do (adv!) (adv!)))
|
||||
(let
|
||||
((every? (match-kw "every")))
|
||||
((every? (match-kw "every"))
|
||||
(throttle-ms nil)
|
||||
(debounce-ms nil))
|
||||
;; 'throttled at <duration>' / 'debounced at <duration>'
|
||||
;; — parsed as handler modifiers, captured as :throttle / :debounce parts.
|
||||
(when (and (= (tp-type) "ident") (= (tp-val) "throttled"))
|
||||
(adv!)
|
||||
(when (match-kw "at") (set! throttle-ms (parse-expr))))
|
||||
(when (and (= (tp-type) "ident") (= (tp-val) "debounced"))
|
||||
(adv!)
|
||||
(when (match-kw "at") (set! debounce-ms (parse-expr))))
|
||||
(let
|
||||
((having (if (or h-margin h-threshold) (dict "margin" h-margin "threshold" h-threshold) nil)))
|
||||
(let
|
||||
@@ -3094,6 +3114,10 @@
|
||||
(match-kw "end")
|
||||
(let
|
||||
((parts (list (quote on) event-name)))
|
||||
(let
|
||||
((parts (if throttle-ms (append parts (list :throttle throttle-ms)) parts)))
|
||||
(let
|
||||
((parts (if debounce-ms (append parts (list :debounce debounce-ms)) parts)))
|
||||
(let
|
||||
((parts (if every? (append parts (list :every true)) parts)))
|
||||
(let
|
||||
@@ -3116,7 +3140,7 @@
|
||||
((parts (if finally-clause (append parts (list :finally finally-clause)) parts)))
|
||||
(let
|
||||
((parts (append parts (list (if (> (len event-vars) 0) (cons (quote do) (append (map (fn (nm) (list (quote ref) nm)) event-vars) (if (and (list? body) (= (first body) (quote do))) (rest body) (list body)))) body)))))
|
||||
parts))))))))))))))))))))))))))
|
||||
parts))))))))))))))))))))))))))))
|
||||
(define
|
||||
parse-init-feat
|
||||
(fn
|
||||
|
||||
@@ -54,6 +54,41 @@
|
||||
hs-on-every
|
||||
(fn (target event-name handler) (dom-listen target event-name handler)))
|
||||
|
||||
;; Throttle: drops events that arrive within the window. First event fires
|
||||
;; immediately; subsequent events within `ms` of the previous fire are dropped.
|
||||
;; Returns a wrapped handler suitable for hs-on / hs-on-every.
|
||||
(define
|
||||
hs-throttle!
|
||||
(fn
|
||||
(handler ms)
|
||||
(let
|
||||
((__hs-last-fire 0))
|
||||
(fn
|
||||
(event)
|
||||
(let
|
||||
((__hs-now (host-call (host-global "Date") "now")))
|
||||
(when
|
||||
(>= (- __hs-now __hs-last-fire) ms)
|
||||
(set! __hs-last-fire __hs-now)
|
||||
(handler event)))))))
|
||||
|
||||
;; Debounce: waits until `ms` has elapsed since the last event before firing.
|
||||
;; In our synchronous test mock no time passes, so the timer fires immediately
|
||||
;; via setTimeout(_, 0); the wrapped handler still gets called once per burst.
|
||||
(define
|
||||
hs-debounce!
|
||||
(fn
|
||||
(handler ms)
|
||||
(let
|
||||
((__hs-timer nil))
|
||||
(fn
|
||||
(event)
|
||||
(when __hs-timer (host-call (host-global "window") "clearTimeout" __hs-timer))
|
||||
(set! __hs-timer
|
||||
(host-call (host-global "window") "setTimeout"
|
||||
(host-new-function (list "ev") "return arguments[0](arguments[1]);")
|
||||
ms handler event))))))
|
||||
|
||||
;; Wait for a DOM event on a target.
|
||||
;; (hs-wait-for target event-name) — suspends until event fires
|
||||
(define
|
||||
|
||||
@@ -3,22 +3,25 @@
|
||||
Live tally for `plans/hs-conformance-to-100.md`. Update after every cluster commit.
|
||||
|
||||
```
|
||||
Baseline: 1213/1496 (81.1%)
|
||||
Merged: 1494/1494 (100.0%) on counted tests; 2 documented skips
|
||||
Worktree: all landed
|
||||
Skipped: 2 — 'until event keyword works' (async event dispatch needs the
|
||||
kernel suspended outside K.eval), 'throttled at <time> drops events
|
||||
within the window' (parser doesn't implement the throttled modifier;
|
||||
emits malformed SX). Both documented in tests/hs-run-filtered.js.
|
||||
Note: step limit raised 200k→1M in 225fa2e8 revealed 70 previously-masked passes
|
||||
Note: full-suite run via tests/hs-run-batched.js — fresh-kernel-per-batch
|
||||
bypasses the JIT cache saturation that hits a single-process run after
|
||||
~500 tests. Sequential at batch=200: 10m47s, 1494/1494.
|
||||
Note: hs-f loop totals — T9, F2, F3, F9, hs-null-error! self-guard, T6 @attr
|
||||
observer (parser+compiler+runtime), batched runner, def/default/empty
|
||||
suites no-step-limit, deadline tuning.
|
||||
Baseline: 1213/1496 (81.1%) initial scrape
|
||||
Snapshot: 1496/1514 upstream sync 2026-05-08 (+18 new upstream tests)
|
||||
Conformance: 1496/1496 (100.0%) on counted tests; 18 documented architectural skips
|
||||
Wall: 26m17s sequential (8 batches × 200) via tests/hs-run-batched.js
|
||||
Target: 1514/1514 — clear the 18 skip list (in progress)
|
||||
Note: full-suite single-process is unreliable due to JIT cache saturation;
|
||||
use hs-run-batched.js (fresh kernel per batch) for deterministic numbers.
|
||||
```
|
||||
|
||||
## Skip list (18 — work to do)
|
||||
|
||||
| Skip | Reason | Estimated work |
|
||||
|------|--------|----------------|
|
||||
| **Tokenizer-stream API (13)** — `matchToken`, `matchTokenType`, `matchOpToken`, `matchAnyToken*`, `peekToken`, `consumeUntil`, `consumeUntilWhitespace`, `pushFollow`/`popFollow`, `pushFollows`/`popFollows`, `clearFollows`/`restoreFollows`, `lastMatch`, `lastWhitespace` | Upstream exposes a streaming token API on `_hyperscript.internals.tokenizer`. Our `hs-tokenize` returns a flat list; parser holds stream state internally as closures. | Wrap `hs-tokenize` output in a token-stream object exposed as a primitive. ~1-2 days, mostly mechanical. |
|
||||
| **Template-component scope (2)** — `component reads a feature-level set from an enclosing div on first load`, `component reads enclosing scope set by a sibling init on first load` | Upstream supports `<script type="text/hyperscript-template" component="...">` — HTML-template-based custom elements. Our `defcomp` is SX-only; no template-component bootstrap. | Add a `<script type="text/hyperscript-template">` registrar alongside the existing script-tag scanner. ~1 day. |
|
||||
| **Toggle parser ambiguity (1)** — `toggle does not consume a following for-in loop` | Parser greedily consumes `for x in [...]` as `toggle .foo for <duration>`. Need lookahead to distinguish `for <num>ms/s` (duration) from `for <ident> in <expr>` (iteration). | Targeted parser fix in `parse-toggle`. ~2-4 hours. |
|
||||
| **Throttled-at modifier (1)** — `throttled at <time> drops events within the window` | Parser doesn't recognize `throttled at <duration>` as a handler modifier. Currently emits malformed SX (handler body is the literal `throttled` symbol; time expression dangles outside closure). | Parser support + runtime `hs-throttle!` wrapper. ~4 hours. |
|
||||
| **Async event dispatch (1)** — `until event keyword works` | `repeat until event click from #x` suspends the OCaml kernel waiting for a click that the sync test runner can't dispatch (kernel busy, JS event loop blocked). | Architectural — requires either yielding to the JS event loop between iterations, or a different test-runner shape that can interleave event injection. ~2-3 days. |
|
||||
|
||||
## Cluster ledger
|
||||
|
||||
### Bucket A — runtime fixes
|
||||
|
||||
@@ -10036,8 +10036,10 @@
|
||||
(dom-set-attr _el-d "_" "on click throttled at 200ms then increment @n then put @n into me")
|
||||
(dom-append (dom-body) _el-d)
|
||||
(hs-activate! _el-d)
|
||||
(assert= (dom-text-content (dom-query-by-id "d")) "1")
|
||||
))
|
||||
(dom-dispatch _el-d "click" nil)
|
||||
(dom-dispatch _el-d "click" nil)
|
||||
(assert= (dom-text-content (dom-query-by-id "d")) "1"))
|
||||
)
|
||||
(deftest "uncaught exceptions trigger 'exception' event"
|
||||
(hs-cleanup!)
|
||||
(let ((_el-button (dom-create-element "button")))
|
||||
@@ -13867,7 +13869,19 @@ end")
|
||||
(assert= (dom-text-content _out) "2"))
|
||||
)
|
||||
(deftest "toggle does not consume a following for-in loop"
|
||||
(error "SKIP (untranslated): toggle does not consume a following for-in loop"))
|
||||
(hs-cleanup!)
|
||||
(let ((_out (dom-create-element "div")) (_btn (dom-create-element "div")))
|
||||
(dom-set-attr _out "id" "out")
|
||||
(dom-set-attr _btn "id" "btn")
|
||||
(dom-set-attr _btn "_" "on click toggle .foo for x in [1, 2, 3] put x into #out end")
|
||||
(dom-append (dom-body) _out)
|
||||
(dom-append (dom-body) _btn)
|
||||
(hs-activate! _btn)
|
||||
(assert (not (dom-has-class? _btn "foo")))
|
||||
(dom-dispatch _btn "click" nil)
|
||||
(assert (dom-has-class? _btn "foo"))
|
||||
(assert= (dom-text-content _out) "3"))
|
||||
)
|
||||
)
|
||||
|
||||
;; ── transition (17 tests) ──
|
||||
|
||||
@@ -967,11 +967,6 @@ for(let i=startTest;i<Math.min(endTest,testCount);i++){
|
||||
// 'repeat until event' loop suspends the OCaml kernel waiting for an
|
||||
// event that is never fired from outside the K.eval call chain.
|
||||
"until event keyword works",
|
||||
// 'throttled at <time>' modifier not implemented — parser emits malformed
|
||||
// SX (the throttle window expression dangles outside the handler closure).
|
||||
// Implementing it requires parser support for the modifier syntax + a
|
||||
// runtime hs-throttle! wrapper. Leaving as documented skip.
|
||||
"throttled at <time> drops events within the window",
|
||||
// === Tokenizer-stream API tests (13) — upstream exposes a streaming token
|
||||
// API on _hyperscript.internals.tokenizer (matchToken, peekToken, consumeUntil,
|
||||
// pushFollow, etc.). Our lib/hyperscript/tokenizer.sx returns a flat token list
|
||||
@@ -999,12 +994,6 @@ for(let i=startTest;i<Math.min(endTest,testCount);i++){
|
||||
// alongside the existing <script type="text/hyperscript"> path. ===
|
||||
"component reads a feature-level set from an enclosing div on first load",
|
||||
"component reads enclosing scope set by a sibling init on first load",
|
||||
// === Parser ambiguity: 'toggle .foo for x in [...]' — parser consumes the
|
||||
// 'for' as the optional duration clause of toggle, swallowing the trailing
|
||||
// for-in loop. Fixing requires lookahead in parse-toggle to distinguish
|
||||
// 'for <number>ms/s' (duration) from 'for <ident> in <expr>' (iteration).
|
||||
// The 'toggle between' variant has different parse logic and works fine. ===
|
||||
"toggle does not consume a following for-in loop",
|
||||
]);
|
||||
if (_SKIP_TESTS.has(name)) continue;
|
||||
|
||||
|
||||
@@ -109,6 +109,19 @@ SKIP_TEST_NAMES = {
|
||||
# Manually-written SX test bodies for tests whose upstream body cannot be
|
||||
# auto-translated. Key = test name; value = SX lines to emit inside deftest.
|
||||
MANUAL_TEST_BODIES = {
|
||||
# throttle: first click fires, subsequent within 200ms dropped.
|
||||
# In the synchronous mock no time passes between two dom-dispatch calls.
|
||||
"throttled at <time> drops events within the window": [
|
||||
' (hs-cleanup!)',
|
||||
' (let ((_el-d (dom-create-element "div")))',
|
||||
' (dom-set-attr _el-d "id" "d")',
|
||||
' (dom-set-attr _el-d "_" "on click throttled at 200ms then increment @n then put @n into me")',
|
||||
' (dom-append (dom-body) _el-d)',
|
||||
' (hs-activate! _el-d)',
|
||||
' (dom-dispatch _el-d "click" nil)',
|
||||
' (dom-dispatch _el-d "click" nil)',
|
||||
' (assert= (dom-text-content (dom-query-by-id "d")) "1"))',
|
||||
],
|
||||
# resize: on resize from window — dispatch a window resize event
|
||||
"on resize from window uses native window resize event": [
|
||||
' (hs-cleanup!)',
|
||||
@@ -120,6 +133,22 @@ MANUAL_TEST_BODIES = {
|
||||
' (dom-dispatch (host-global "window") "resize" nil)',
|
||||
' (assert= (dom-text-content _el) "fired"))',
|
||||
],
|
||||
# toggle: parser must not consume the trailing 'for x in [...]' as part of toggle's
|
||||
# 'for <duration>' clause. After click: btn has .foo, #out has the last loop value.
|
||||
"toggle does not consume a following for-in loop": [
|
||||
' (hs-cleanup!)',
|
||||
' (let ((_out (dom-create-element "div")) (_btn (dom-create-element "div")))',
|
||||
' (dom-set-attr _out "id" "out")',
|
||||
' (dom-set-attr _btn "id" "btn")',
|
||||
' (dom-set-attr _btn "_" "on click toggle .foo for x in [1, 2, 3] put x into #out end")',
|
||||
' (dom-append (dom-body) _out)',
|
||||
' (dom-append (dom-body) _btn)',
|
||||
' (hs-activate! _btn)',
|
||||
' (assert (not (dom-has-class? _btn "foo")))',
|
||||
' (dom-dispatch _btn "click" nil)',
|
||||
' (assert (dom-has-class? _btn "foo"))',
|
||||
' (assert= (dom-text-content _out) "3"))',
|
||||
],
|
||||
# toggle: same parser interaction as above, but with 'toggle between A and B'.
|
||||
"toggle between followed by for-in loop works": [
|
||||
' (hs-cleanup!)',
|
||||
|
||||
Reference in New Issue
Block a user