From 19f5bf7d72668caad9545ed567614c5001c8ffc5 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 23 Apr 2026 11:41:47 +0000 Subject: [PATCH] =?UTF-8?q?HS=20test=20generator:=20bind=20run()=20`me:`?= =?UTF-8?q?=20and=20balanced-brace=20locals=20=E2=80=94=20+2=20comparisonO?= =?UTF-8?q?perator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related Pattern 1 bugs: 1. The locals capture used `\\{([^}]+)\\}` (greedy non-`}` chars), so `locals: { that: [1, 2, 3] }` truncated at the first `,` inside `[...]` and bound `that` to `"[1"`. Switched to balanced-brace extraction + `split_top_level` so nested arrays/objects survive. 2. `{ me: }` was only forwarded to the SX runtime when X was a single integer (eval-hs-with-me only accepts numbers). For `me: [1, 2, 3]` or `me: 1` alongside other locals, `me` was silently dropped, so `I contain that` couldn't see its receiver. Now any non-numeric `me` value is bound as a local (`(list (quote me) )`); a numeric `me` alongside other locals/setups is also bound, so the HS expr always sees its `me`. comparisonOperator 79/83 → 81/83 (+2: contains/includes works with arrays). bind unchanged (43/44). Co-Authored-By: Claude Opus 4.7 (1M context) --- spec/tests/test-hyperscript-behavioral.sx | 8 ++-- tests/playwright/generate-sx-tests.py | 48 +++++++++++++++++++---- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/spec/tests/test-hyperscript-behavioral.sx b/spec/tests/test-hyperscript-behavioral.sx index 040bd2e8..19732286 100644 --- a/spec/tests/test-hyperscript-behavioral.sx +++ b/spec/tests/test-hyperscript-behavioral.sx @@ -4341,8 +4341,8 @@ (assert= (eval-hs "'Hello World' contains 'missing' ignoring case") false) ) (deftest "contains works with arrays" - (assert= (eval-hs-locals "I contain that" (list (list (quote that) 1))) true) - (assert= (eval-hs-locals "that contains me" (list (list (quote that) "[1"))) true) + (assert= (eval-hs-locals "I contain that" (list (list (quote that) 1) (list (quote me) (list 1 2 3)))) true) + (assert= (eval-hs-locals "that contains me" (list (list (quote that) (list 1 2 3)) (list (quote me) 1))) true) ) (deftest "contains works with css literals" (hs-cleanup!) @@ -4501,8 +4501,8 @@ (assert= (eval-hs-locals "foobar does not include foo" (list (list (quote foo) "foo") (list (quote foobar) "foobar"))) false) ) (deftest "includes works with arrays" - (assert= (eval-hs-locals "I include that" (list (list (quote that) 1))) true) - (assert= (eval-hs-locals "that includes me" (list (list (quote that) "[1"))) true) + (assert= (eval-hs-locals "I include that" (list (list (quote that) 1) (list (quote me) (list 1 2 3)))) true) + (assert= (eval-hs-locals "that includes me" (list (list (quote that) (list 1 2 3)) (list (quote me) 1))) true) ) (deftest "includes works with css literals" (hs-cleanup!) diff --git a/tests/playwright/generate-sx-tests.py b/tests/playwright/generate-sx-tests.py index 120892ea..a4bef01b 100644 --- a/tests/playwright/generate-sx-tests.py +++ b/tests/playwright/generate-sx-tests.py @@ -1281,16 +1281,48 @@ def generate_eval_only_test(test, idx): hs_expr = extract_hs_expr(m.group(2)) opts_str = m.group(3) or '' expected_sx = js_val_to_sx(m.group(4)) - # Check for { me: X } or { locals: { x: X, y: Y } } in opts - me_match = re.search(r'\bme:\s*(\d+)', opts_str) - locals_match = re.search(r'locals:\s*\{([^}]+)\}', opts_str) + # Check for { me: X } or { locals: { x: X, y: Y } } in opts. + # Numeric me uses eval-hs-with-me; other me values get bound as a local. + me_num_match = re.search(r'\bme:\s*(\d+)\b', opts_str) + me_val_match = re.search(r'\bme:\s*(\[[^\]]*\]|\{[^}]*\}|"[^"]*"|\'[^\']*\')', opts_str) + # Locals: balanced-brace extraction so nested arrays/objects don't truncate. + locals_idx = opts_str.find('locals:') extra = [] - if locals_match: - for lm in re.finditer(r'(\w+)\s*:\s*([^,}]+)', locals_match.group(1)): - extra.append((lm.group(1), js_val_to_sx(lm.group(2).strip()))) - if me_match and not (window_setups or extra): - assertions.append(f' (assert= (eval-hs-with-me "{hs_expr}" {me_match.group(1)}) {expected_sx})') + if locals_idx >= 0: + open_idx = opts_str.find('{', locals_idx) + if open_idx >= 0: + depth, in_str, end_idx = 1, None, -1 + for i in range(open_idx + 1, len(opts_str)): + ch = opts_str[i] + if in_str: + if ch == in_str and opts_str[i - 1] != '\\': + in_str = None + continue + if ch in ('"', "'", '`'): + in_str = ch + continue + if ch == '{': + depth += 1 + elif ch == '}': + depth -= 1 + if depth == 0: + end_idx = i + break + if end_idx > open_idx: + for kv in split_top_level(opts_str[open_idx + 1:end_idx]): + kv = kv.strip() + m2 = re.match(r'^(\w+)\s*:\s*(.+)$', kv, re.DOTALL) + if m2: + extra.append((m2.group(1), js_val_to_sx(m2.group(2).strip()))) + if me_val_match: + extra.append(('me', js_val_to_sx(me_val_match.group(1)))) + if me_num_match and not (window_setups or extra): + assertions.append(f' (assert= (eval-hs-with-me "{hs_expr}" {me_num_match.group(1)}) {expected_sx})') else: + # If there are other locals/setups but `me: ` is present too, + # bind it as a local so the HS expression can see it. + if me_num_match and not me_val_match: + extra.append(('me', me_num_match.group(1))) assertions.append(emit_eval(hs_expr, expected_sx, extra)) # Pattern 1b: Inline — run("expr", opts).toEqual([...])