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

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:
2026-05-08 07:16:27 +00:00
parent 982b9d6be6
commit d0b358eca2
7 changed files with 155 additions and 33 deletions

View File

@@ -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;

View File

@@ -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!)',