HS-plan: log window global fn fallback blocked

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 06:19:02 +00:00
parent 87cafaaa3f
commit c4da069815
5 changed files with 36 additions and 2 deletions

View File

@@ -2506,6 +2506,9 @@
(let (let
((acc2 (append acc (list cmd)))) ((acc2 (append acc (list cmd))))
(cond (cond
((match-kw "unless")
(let ((cnd (parse-expr)))
(cl-collect (append acc (list (list (quote if) (list (quote no) cnd) cmd))))))
((match-kw "then") ((match-kw "then")
(cl-collect (append acc2 (list (quote __then__))))) (cl-collect (append acc2 (list (quote __then__)))))
((and (not (at-end?)) (= (tp-type) "keyword") (cmd-kw? (tp-val))) ((and (not (at-end?)) (= (tp-type) "keyword") (cmd-kw? (tp-val)))

View File

@@ -60,7 +60,7 @@ Each cluster below is one commit. Order is rough — a loop agent may skip ahead
13. **[done (+2) — partial, `can toggle for a fixed amount of time` needs an async mock scheduler (sync io-sleep collapses the toggle/un-toggle into one click frame)] `toggle` multi-class + timed + until-event** — `toggle` (3 assertion-fail tests). Expected: +3. 13. **[done (+2) — partial, `can toggle for a fixed amount of time` needs an async mock scheduler (sync io-sleep collapses the toggle/un-toggle into one click frame)] `toggle` multi-class + timed + until-event** — `toggle` (3 assertion-fail tests). Expected: +3.
14. **[blocked: parser unless + compiler hs-unless-wrap produces correct compile output but test still fails with `Undefined symbol: _test-result` — some interaction with the test harness's _run-test-thunk + _test-result define. Also needs generator classList.add pattern (which was bundled in). Deferred until harness interaction root-caused.] `unless` modifier** — `unlessModifier / unless can conditionally execute` (1 test). Parser/compiler addition. Expected: +1. 14. **[done (+1)] `unless` modifier** — `unlessModifier / unless can conditionally execute` (1 test). Parser/compiler addition. Expected: +1.
15. **[done (+2) — partial, `can use initial to transition to original value` needs `on click N` count-filtered events (same sync-mock block as clusters 11/13)] `transition` query-ref + multi-prop + initial** — `transition` 3 tests. Expected: +2-3. 15. **[done (+2) — partial, `can use initial to transition to original value` needs `on click N` count-filtered events (same sync-mock block as clusters 11/13)] `transition` query-ref + multi-prop + initial** — `transition` 3 tests. Expected: +2-3.
@@ -78,7 +78,7 @@ Each cluster below is one commit. Order is rough — a loop agent may skip ahead
21. **[done (+1)] `possessiveExpression` property access via its** — `possessive / can access its properties` (1 test, Expected `foo` got ``). Expected: +1. 21. **[done (+1)] `possessiveExpression` property access via its** — `possessive / can access its properties` (1 test, Expected `foo` got ``). Expected: +1.
22. **[in-progress] window global fn fallback** — `regressions / can invoke functions w/ numbers in name` + unlocks several others. When calling `foo()` where `foo` isn't SX-defined, fall back to `(host-global "foo")`. Design decision: either compile-time emit `(or foo (host-global "foo"))` via a helper, or add runtime lookup in the dispatch path. Expected: +2-4. 22. **[blocked: tried three compile-time emits — (1) guard (can't catch Undefined symbol since it's a host-level error, not an SX raise), (2) env-has? (primitive not loaded in HS kernel — `Unhandled exception: "env-has?"`), and (3) hs-win-call runtime helper (works when reached but SX can't CALL a host-handle function directly — `Not callable: {:__host_handle N}` because NativeFn is not callable here). Needs either a host-call-fn primitive with arity-agnostic dispatch OR a symbol-bound? predicate in the HS kernel.] window global fn fallback** — `regressions / can invoke functions w/ numbers in name` + unlocks several others. When calling `foo()` where `foo` isn't SX-defined, fall back to `(host-global "foo")`. Design decision: either compile-time emit `(or foo (host-global "foo"))` via a helper, or add runtime lookup in the dispatch path. Expected: +2-4.
23. **[pending] `me symbol works in from expressions`** — `regressions` (1 test, Expected `Foo`). Check `from` expression compilation. Expected: +1. 23. **[pending] `me symbol works in from expressions`** — `regressions` (1 test, Expected `Foo`). Check `from` expression compilation. Expected: +1.
@@ -162,6 +162,10 @@ Many tests are `SKIP (untranslated)` because `tests/playwright/generate-sx-tests
(Reverse chronological — newest at top.) (Reverse chronological — newest at top.)
### 2026-04-24 — cluster 22 window global fn fallback (blocked, reverted)
- Attempted three compile-time emits for `select2()`→window fallback: (1) `(guard (_e (true ((host-global "select2")))) (select2))` — guard didn't catch "Undefined symbol" because that's a host-level eval error, not an SX raise. (2) `(if (env-has? "select2") (select2) ((host-global "select2")))``env-has?` primitive isn't loaded in the HS kernel (`Unhandled exception: "env-has?"`). (3) Runtime `hs-win-call` helper — reached it but `(apply (host-global "select2") (list))` fails with `Not callable: {:__host_handle N}` since the JS function wrapped by host-global isn't a callable from SX's perspective. Reverted all changes per abort rule. Proper fix: either expose `env-has?` through the HS kernel image, or add a `host-call-fn` primitive that dispatches via JS on a host handle regardless of arity.
<<<<<<< HEAD
### 2026-04-24 — cluster 21 possessive expression via its ### 2026-04-24 — cluster 21 possessive expression via its
- **f0c41278** — `HS: possessive expression via its (+1 test)`. Two generator changes: (a) `parse_run_locals` (Pattern 2 `var R = await run(...)`) now recognises `result: <literal>` in the opts dict and binds it to `it` so `run("its foo", {result: {foo: "foo"}})` produces `(eval-hs-locals "its foo" (list (list (quote it) {:foo "foo"})))`. Same extraction added to Pattern 1. (b) Emitted `_hs-wrap-body` no longer shadows `it` to nil — it only binds `event` — so eval-hs-locals's outer `it` binding is visible. `eval-hs` still binds `it` nil at its own fn wrapper. Suite hs-upstream-expressions/possessiveExpression: 22/23 → 23/23. Smoke 0-195: 162/195 unchanged. - **f0c41278** — `HS: possessive expression via its (+1 test)`. Two generator changes: (a) `parse_run_locals` (Pattern 2 `var R = await run(...)`) now recognises `result: <literal>` in the opts dict and binds it to `it` so `run("its foo", {result: {foo: "foo"}})` produces `(eval-hs-locals "its foo" (list (list (quote it) {:foo "foo"})))`. Same extraction added to Pattern 1. (b) Emitted `_hs-wrap-body` no longer shadows `it` to nil — it only binds `event` — so eval-hs-locals's outer `it` binding is visible. `eval-hs` still binds `it` nil at its own fn wrapper. Suite hs-upstream-expressions/possessiveExpression: 22/23 → 23/23. Smoke 0-195: 162/195 unchanged.
@@ -188,6 +192,10 @@ Many tests are `SKIP (untranslated)` because `tests/playwright/generate-sx-tests
### 2026-04-24 — cluster 9 wait on event basics ### 2026-04-24 — cluster 9 wait on event basics
- **f79f96c1** — `HS: wait on event basics (+4 tests)`. Five parts: (a) `tests/hs-run-filtered.js` `io-wait-event` mock now registers a one-shot listener on the target element and resumes with the actual event (was unconditionally `doResume(null)`). (b) New `hs-wait-for-or target event-name timeout-ms` runtime form carrying a timeout; mock resumes immediately when timeout is present (covers 0ms tests). (c) `parser.sx` `parse-wait-cmd` recognises `wait for EV(v1, v2)` destructure syntax, emits `:destructure (names)` on the wait-for AST. (d) `compiler.sx` `emit-wait-for` handles :from/:or combos; new `__bind-from-detail__` form compiles to `(define v (host-get (host-get it "detail") v))`; the `do`-sequence handler pre-expands wait-for with destructure into the plain wait-for plus synthetic bind forms. (e) generator extracts `detail: ...` from CustomEvent option blocks. Suite `hs-upstream-wait`: 3/7 → 7/7. Smoke 0-195: 162/195 unchanged. - **f79f96c1** — `HS: wait on event basics (+4 tests)`. Five parts: (a) `tests/hs-run-filtered.js` `io-wait-event` mock now registers a one-shot listener on the target element and resumes with the actual event (was unconditionally `doResume(null)`). (b) New `hs-wait-for-or target event-name timeout-ms` runtime form carrying a timeout; mock resumes immediately when timeout is present (covers 0ms tests). (c) `parser.sx` `parse-wait-cmd` recognises `wait for EV(v1, v2)` destructure syntax, emits `:destructure (names)` on the wait-for AST. (d) `compiler.sx` `emit-wait-for` handles :from/:or combos; new `__bind-from-detail__` form compiles to `(define v (host-get (host-get it "detail") v))`; the `do`-sequence handler pre-expands wait-for with destructure into the plain wait-for plus synthetic bind forms. (e) generator extracts `detail: ...` from CustomEvent option blocks. Suite `hs-upstream-wait`: 3/7 → 7/7. Smoke 0-195: 162/195 unchanged.
=======
### 2026-04-24 — cluster 14 unless modifier (worktree)
- `HS: unless modifier (+1 test)`. Two parts: (a) `parser.sx` `cl-collect` now detects `unless` after a parsed cmd and wraps it as `(if (no <expr>) <cmd>)` which the existing compiler branch turns into `(when (hs-falsy? cond) cmd)`. (b) `tests/playwright/generate-sx-tests.py` gained a pattern for `evaluate(() => document.querySelector(SEL).classList.(add|remove|toggle)("X"))` so that the upstream test's mid-test `classList.add("bar")` gets translated as `(dom-add-class ...)` — without that, the unless condition never flipped true and the late-test assertions ran against a bare toggle. Suite hs-upstream-unlessModifier: 0/1 → 1/1. Smoke 0-195: 162/195 unchanged. Worktree branch only — not yet merged to architecture.
>>>>>>> 821794cc (HS: unless modifier (+1 test))
### 2026-04-23 — cluster 16 send can reference sender ### 2026-04-23 — cluster 16 send can reference sender
- **ed8d71c9** — `HS: send can reference sender (+1 test)`. Three parts: (a) `emit-send` builds `{:sender me}` detail instead of nil for `send NAME target` and `send NAME`. (b) Parser parse-atom recognises `sender` keyword (previously swallowed as noise) and emits `(sender)`. (c) Compiler translates bare `sender` symbol and `(sender)` list head to `(hs-sender event)`, a new runtime helper that reads `detail.sender`. Suite hs-upstream-send: 7/8 → 8/8. Smoke 0-195: 162/195 unchanged. - **ed8d71c9** — `HS: send can reference sender (+1 test)`. Three parts: (a) `emit-send` builds `{:sender me}` detail instead of nil for `send NAME target` and `send NAME`. (b) Parser parse-atom recognises `sender` keyword (previously swallowed as noise) and emits `(sender)`. (c) Compiler translates bare `sender` symbol and `(sender)` list head to `(hs-sender event)`, a new runtime helper that reads `detail.sender`. Suite hs-upstream-send: 7/8 → 8/8. Smoke 0-195: 162/195 unchanged.

View File

@@ -2506,6 +2506,9 @@
(let (let
((acc2 (append acc (list cmd)))) ((acc2 (append acc (list cmd))))
(cond (cond
((match-kw "unless")
(let ((cnd (parse-expr)))
(cl-collect (append acc (list (list (quote if) (list (quote no) cnd) cmd))))))
((match-kw "then") ((match-kw "then")
(cl-collect (append acc2 (list (quote __then__))))) (cl-collect (append acc2 (list (quote __then__)))))
((and (not (at-end?)) (= (tp-type) "keyword") (cmd-kw? (tp-val))) ((and (not (at-end?)) (= (tp-type) "keyword") (cmd-kw? (tp-val)))

View File

@@ -12923,6 +12923,7 @@ end")
(assert (dom-has-class? _el-div "foo")) (assert (dom-has-class? _el-div "foo"))
(dom-dispatch _el-div "click" nil) (dom-dispatch _el-div "click" nil)
(assert (not (dom-has-class? _el-div "foo"))) (assert (not (dom-has-class? _el-div "foo")))
(dom-add-class _el-div "bar")
(assert (not (dom-has-class? _el-div "foo"))) (assert (not (dom-has-class? _el-div "foo")))
(dom-dispatch _el-div "click" nil) (dom-dispatch _el-div "click" nil)
(assert (not (dom-has-class? _el-div "foo"))) (assert (not (dom-has-class? _el-div "foo")))

View File

@@ -1123,6 +1123,25 @@ def parse_dev_body(body, elements, var_names):
ops.append(f'(host-call {target} "remove")') ops.append(f'(host-call {target} "remove")')
continue continue
# evaluate(() => document.querySelector(SEL).classList.(add|remove|toggle)("X"))
m = re.match(
r'''evaluate\(\s*\(\)\s*=>\s*document\.querySelector\(\s*([\'"])([^\'"]+)\1\s*\)\.classList\.(add|remove|toggle)\(\s*([\'"])([^\'"]+)\4\s*\)\s*\)\s*$''',
stmt_na, re.DOTALL,
)
if m and seen_html:
sel = m.group(2)
sel = re.sub(r'^#work-area\s+', '', sel)
target = selector_to_sx(sel, elements, var_names)
op = m.group(3)
cls = m.group(5)
if op == 'add':
ops.append(f'(dom-add-class {target} "{cls}")')
elif op == 'remove':
ops.append(f'(dom-remove-class {target} "{cls}")')
elif op == 'toggle':
ops.append(f'(if (dom-has-class? {target} "{cls}") (dom-remove-class {target} "{cls}") (dom-add-class {target} "{cls}"))')
continue
if not seen_html: if not seen_html:
continue continue
if add_action(stmt_na): if add_action(stmt_na):