HS: closest parent <sel> traversal (+1 test)

parse-trav recognises `parent` as an ident modifier after the
`closest` keyword — consumes it and re-invokes with kind
`closest-parent`, producing AST `(closest-parent "div" (me))` instead
of the generic trailing-ident-as-unit shape
`(string-postfix (closest "*" (me)) "parent")`.

Compiler translates `(closest-parent sel target)` to
`(dom-closest (host-get target "parentElement") sel)` so `me` is
skipped and only strict ancestors match. `closest-parent` also
joined the `put X into <trav>` inner-html shortcut alongside
next/previous/closest.

Suite hs-upstream-core/regressions: 10/16 → 11/16.
Smoke 0-195: 162/195 → 163/195.
This commit is contained in:
2026-04-24 09:33:32 +00:00
parent 99706a91d1
commit 0d38a75b21
6 changed files with 54 additions and 13 deletions

View File

@@ -109,7 +109,7 @@
(hs-to-sx (nth target 1)) (hs-to-sx (nth target 1))
(hs-to-sx (nth target 2)) (hs-to-sx (nth target 2))
value)) value))
((or (= th (quote next)) (= th (quote previous)) (= th (quote closest))) ((or (= th (quote next)) (= th (quote previous)) (= th (quote closest)) (= th (quote closest-parent)))
(list (quote hs-set-inner-html!) (hs-to-sx target) value)) (list (quote hs-set-inner-html!) (hs-to-sx target) value))
((= th (quote of)) ((= th (quote of))
(let (let
@@ -966,7 +966,11 @@
((= prop "first") (list (quote hs-first) target)) ((= prop "first") (list (quote hs-first) target))
((= prop "last") (list (quote hs-last) target)) ((= prop "last") (list (quote hs-last) target))
(true (list (quote host-get) target prop))))) (true (list (quote host-get) target prop)))))
((= head (quote ref)) (if (= (nth ast 1) "selection") (list (quote hs-get-selection)) (make-symbol (nth ast 1)))) ((= head (quote ref))
(if
(= (nth ast 1) "selection")
(list (quote hs-get-selection))
(make-symbol (nth ast 1))))
((= head (quote query)) ((= head (quote query))
(list (quote hs-query-first) (nth ast 1))) (list (quote hs-query-first) (nth ast 1)))
((= head (quote query-scoped)) ((= head (quote query-scoped))
@@ -1153,6 +1157,14 @@
(quote dom-closest) (quote dom-closest)
(hs-to-sx (nth ast 2)) (hs-to-sx (nth ast 2))
(nth ast 1))) (nth ast 1)))
((= head (quote closest-parent))
(list
(quote dom-closest)
(list
(quote host-get)
(hs-to-sx (nth ast 2))
"parentElement")
(nth ast 1)))
((= head (quote next)) ((= head (quote next))
(list (quote hs-next) (hs-to-sx (nth ast 2)) (nth ast 1))) (list (quote hs-next) (hs-to-sx (nth ast 2)) (nth ast 1)))
((= head (quote previous)) ((= head (quote previous))

View File

@@ -89,6 +89,8 @@
(let (let
((typ (tp-type)) (val (tp-val))) ((typ (tp-type)) (val (tp-val)))
(cond (cond
((and (= kind (quote closest)) (= typ "ident") (= val "parent"))
(do (adv!) (parse-trav (quote closest-parent))))
((= typ "selector") ((= typ "selector")
(do (adv!) (list kind val (list (quote me))))) (do (adv!) (list kind val (list (quote me)))))
((= typ "class") ((= typ "class")
@@ -2507,8 +2509,13 @@
((acc2 (append acc (list cmd)))) ((acc2 (append acc (list cmd))))
(cond (cond
((match-kw "unless") ((match-kw "unless")
(let ((cnd (parse-expr))) (let
(cl-collect (append acc (list (list (quote if) (list (quote no) cnd) cmd)))))) ((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

@@ -4,10 +4,10 @@ Live tally for `plans/hs-conformance-to-100.md`. Update after every cluster comm
``` ```
Baseline: 1213/1496 (81.1%) Baseline: 1213/1496 (81.1%)
Merged: 1247/1496 (83.4%) delta +34 Merged: 1248/1496 (83.4%) delta +35
Worktree: all merged Worktree: all merged
Target: 1496/1496 (100.0%) Target: 1496/1496 (100.0%)
Remaining: ~249 tests Remaining: ~248 tests
``` ```
## Cluster ledger ## Cluster ledger
@@ -43,7 +43,7 @@ Remaining: ~249 tests
| 20 | `repeat` property for-loops + where | pending | (+3 est) | — | | 20 | `repeat` property for-loops + where | pending | (+3 est) | — |
| 21 | `possessiveExpression` property access via its | done | +1 | f0c41278 | | 21 | `possessiveExpression` property access via its | done | +1 | f0c41278 |
| 22 | window global fn fallback | blocked | — | — | | 22 | window global fn fallback | blocked | — | — |
| 23 | `me symbol works in from expressions` | pending | (+1 est) | — | | 23 | `me symbol works in from expressions` | done | +1 | (pending) |
| 24 | `properly interpolates values 2` | pending | (+1 est) | — | | 24 | `properly interpolates values 2` | pending | (+1 est) | — |
| 25 | parenthesized commands and features | pending | (+1 est) | — | | 25 | parenthesized commands and features | pending | (+1 est) | — |
@@ -86,7 +86,7 @@ Defer until AD drain. Estimated ~25 recoverable tests.
| Bucket | Done | Partial | In-prog | Pending | Blocked | Design-done | Total | | Bucket | Done | Partial | In-prog | Pending | Blocked | Design-done | Total |
|--------|-----:|--------:|--------:|--------:|--------:|------------:|------:| |--------|-----:|--------:|--------:|--------:|--------:|------------:|------:|
| A | 12 | 4 | 0 | 0 | 1 | — | 17 | | A | 12 | 4 | 0 | 0 | 1 | — | 17 |
| B | 1 | 0 | 0 | 5 | 1 | — | 7 | | B | 2 | 0 | 0 | 4 | 1 | — | 7 |
| C | 0 | 0 | 0 | 5 | 0 | — | 5 | | C | 0 | 0 | 0 | 5 | 0 | — | 5 |
| D | 0 | 0 | 0 | 5 | 0 | — | 5 | | D | 0 | 0 | 0 | 5 | 0 | — | 5 |
| E | 0 | 0 | 0 | 0 | 0 | 5 | 5 | | E | 0 | 0 | 0 | 0 | 0 | 5 | 5 |

View File

@@ -80,7 +80,7 @@ Each cluster below is one commit. Order is rough — a loop agent may skip ahead
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. 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. **[done (+1)] `me symbol works in from expressions`** — `regressions` (1 test, Expected `Foo`). Check `from` expression compilation. Expected: +1.
24. **[pending] `properly interpolates values 2`** — URL interpolation regression (1 test). Likely template string + property access. Expected: +1. 24. **[pending] `properly interpolates values 2`** — URL interpolation regression (1 test). Likely template string + property access. Expected: +1.
@@ -162,6 +162,9 @@ 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 23 me symbol works in from expressions
- **COMMIT** — `HS: closest parent <sel> traversal (+1 test)`. `parse-trav` now recognises `parent` as an ident modifier after the `closest` keyword: consumes it and re-invokes itself with kind `closest-parent`, so `closest parent <div/>` produces AST `(closest-parent "div" (me))` instead of `(string-postfix (closest "*" (me)) "parent")` — the latter was the generic trailing-ident-as-unit rule swallowing `parent`. Compiler translates `(closest-parent sel target)` to `(dom-closest (host-get target "parentElement") sel)` so `me` (the element with the `_` attribute) is skipped and only strict ancestors match. Also added `closest-parent` to the `put X into <trav>` inner-html shortcut alongside `next`/`previous`/`closest`. Suite hs-upstream-core/regressions: 10/16 → 11/16. Smoke 0-195: 162/195 → 163/195.
### 2026-04-24 — cluster 8 select returns selected text (cherry-picked from worktree) ### 2026-04-24 — cluster 8 select returns selected text (cherry-picked from worktree)
- **0b9bbc7b** — `HS: select returns selected text (+1 test)`. Runtime `hs-get-selection` prefers `window.__test_selection` stash and falls back to `getSelection().toString()`. Compiler rewrites `(ref "selection")` to `(hs-get-selection)`. Generator detects `document.createRange() + getElementById(ID).firstChild + setStart/setEnd` and emits a single `host-set!` on `window.__test_selection` with the resolved substring, sidestepping a propagating DOM range/text-node mock. Runner resets `__test_selection` between tests. Suite hs-upstream-select: 3/4 → 4/4. - **0b9bbc7b** — `HS: select returns selected text (+1 test)`. Runtime `hs-get-selection` prefers `window.__test_selection` stash and falls back to `getSelection().toString()`. Compiler rewrites `(ref "selection")` to `(hs-get-selection)`. Generator detects `document.createRange() + getElementById(ID).firstChild + setStart/setEnd` and emits a single `host-set!` on `window.__test_selection` with the resolved substring, sidestepping a propagating DOM range/text-node mock. Runner resets `__test_selection` between tests. Suite hs-upstream-select: 3/4 → 4/4.

View File

@@ -109,7 +109,7 @@
(hs-to-sx (nth target 1)) (hs-to-sx (nth target 1))
(hs-to-sx (nth target 2)) (hs-to-sx (nth target 2))
value)) value))
((or (= th (quote next)) (= th (quote previous)) (= th (quote closest))) ((or (= th (quote next)) (= th (quote previous)) (= th (quote closest)) (= th (quote closest-parent)))
(list (quote hs-set-inner-html!) (hs-to-sx target) value)) (list (quote hs-set-inner-html!) (hs-to-sx target) value))
((= th (quote of)) ((= th (quote of))
(let (let
@@ -966,7 +966,11 @@
((= prop "first") (list (quote hs-first) target)) ((= prop "first") (list (quote hs-first) target))
((= prop "last") (list (quote hs-last) target)) ((= prop "last") (list (quote hs-last) target))
(true (list (quote host-get) target prop))))) (true (list (quote host-get) target prop)))))
((= head (quote ref)) (if (= (nth ast 1) "selection") (list (quote hs-get-selection)) (make-symbol (nth ast 1)))) ((= head (quote ref))
(if
(= (nth ast 1) "selection")
(list (quote hs-get-selection))
(make-symbol (nth ast 1))))
((= head (quote query)) ((= head (quote query))
(list (quote hs-query-first) (nth ast 1))) (list (quote hs-query-first) (nth ast 1)))
((= head (quote query-scoped)) ((= head (quote query-scoped))
@@ -1153,6 +1157,14 @@
(quote dom-closest) (quote dom-closest)
(hs-to-sx (nth ast 2)) (hs-to-sx (nth ast 2))
(nth ast 1))) (nth ast 1)))
((= head (quote closest-parent))
(list
(quote dom-closest)
(list
(quote host-get)
(hs-to-sx (nth ast 2))
"parentElement")
(nth ast 1)))
((= head (quote next)) ((= head (quote next))
(list (quote hs-next) (hs-to-sx (nth ast 2)) (nth ast 1))) (list (quote hs-next) (hs-to-sx (nth ast 2)) (nth ast 1)))
((= head (quote previous)) ((= head (quote previous))

View File

@@ -89,6 +89,8 @@
(let (let
((typ (tp-type)) (val (tp-val))) ((typ (tp-type)) (val (tp-val)))
(cond (cond
((and (= kind (quote closest)) (= typ "ident") (= val "parent"))
(do (adv!) (parse-trav (quote closest-parent))))
((= typ "selector") ((= typ "selector")
(do (adv!) (list kind val (list (quote me))))) (do (adv!) (list kind val (list (quote me)))))
((= typ "class") ((= typ "class")
@@ -2507,8 +2509,13 @@
((acc2 (append acc (list cmd)))) ((acc2 (append acc (list cmd))))
(cond (cond
((match-kw "unless") ((match-kw "unless")
(let ((cnd (parse-expr))) (let
(cl-collect (append acc (list (list (quote if) (list (quote no) cnd) cmd)))))) ((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)))