From 7329b1d24224771733a2d5c127bfbb14a1dbe043 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 22 Apr 2026 21:58:48 +0000 Subject: [PATCH] HS test generator: add eval-hs-locals for run(...) tests with locals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests using `run("expr", {locals: {x}})` were being translated to SX like (let ((x val)) (eval-hs "expr") (assert= it EXPECTED)) That never worked: `it` is bound inside eval-hs's handler closure, not in the outer SX scope, so the assertion errored "Undefined symbol: it". Meanwhile `x` (bound by the outer let) wasn't reachable from the eval-expr-cek'd handler either, so any script referencing `x` resolved via global lookup — silently yielding stale values from earlier tests. New `eval-hs-locals` helper injects locals as fn parameters of the handler wrapper: (fn (me arr str ...) (let ((it nil) (event nil)) it)) It's applied with the caller's values, returning the final `it`. The generator now emits `(assert= (eval-hs-locals "..." (list ...)) EXP)` for all four expect() patterns when locals are present. New baseline: 1,055 / 1,496 pass (70.5%, up from 1,022 / 1,496 = 68.3%). 29 additional tests now pass — mostly `pick` (where locals are the vehicle for passing arr/str test fixtures) plus cascades in comparisonOperator, asExpression, mathOperator, etc. Note: the remaining `pick` wins in this batch also depend on local edits to lib/hyperscript/parser.sx and compiler.sx (not included here; they're intertwined with pre-existing in-flight HS runtime work). Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/playwright/generate-sx-tests.py | 52 ++++++++++++++++++++------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/tests/playwright/generate-sx-tests.py b/tests/playwright/generate-sx-tests.py index 14fb2123..0fcae83e 100644 --- a/tests/playwright/generate-sx-tests.py +++ b/tests/playwright/generate-sx-tests.py @@ -1119,36 +1119,40 @@ def generate_eval_only_test(test, idx): if run_match: hs_expr = extract_hs_expr(run_match.group(2)) locals_str = run_match.group(3).strip() - # Parse locals: {key: val, ...} - local_bindings = [] + # Parse locals: {key: val, ...}. Collect (name, value-sx) pairs. + local_pairs = [] for lm in re.finditer(r'(\w+)\s*:\s*(.+?)(?:,\s*(?=\w+\s*:)|$)', locals_str): lname = lm.group(1) lval = js_val_to_sx(lm.group(2).strip().rstrip(',')) - local_bindings.append(f'({lname} {lval})') + local_pairs.append((lname, lval)) + # SX list of (symbol value) pairs for eval-hs-locals + locals_sx = '(list ' + ' '.join(f'(list (quote {n}) {v})' for n, v in local_pairs) + ')' if local_pairs else '(list)' - # Find expect().toBe() or .toEqual() + # Find expect().toBe() or .toEqual(). eval-hs/eval-hs-locals return + # the final value of `it` after the script runs, so assert on the + # return value directly — `it` is not in the outer SX scope. for m in re.finditer(r'expect\([^)]*\)\.toBe\(([^)]+)\)', body): expected_sx = js_val_to_sx(m.group(1)) - if local_bindings: - assertions.append(f' (let ({" ".join(local_bindings)}) (eval-hs "{hs_expr}") (assert= it {expected_sx}))') + if local_pairs: + assertions.append(f' (assert= (eval-hs-locals "{hs_expr}" {locals_sx}) {expected_sx})') else: assertions.append(f' (assert= (eval-hs "{hs_expr}") {expected_sx})') for m in re.finditer(r'expect\([^)]*\)\.toEqual\((\[.*?\])\)', body, re.DOTALL): expected_sx = js_val_to_sx(m.group(1)) - if local_bindings: - assertions.append(f' (let ({" ".join(local_bindings)}) (eval-hs "{hs_expr}") (assert= it {expected_sx}))') + if local_pairs: + assertions.append(f' (assert= (eval-hs-locals "{hs_expr}" {locals_sx}) {expected_sx})') else: assertions.append(f' (assert= (eval-hs "{hs_expr}") {expected_sx})') for m in re.finditer(r'expect\([^)]*\)\.toContain\(([^)]+)\)', body): expected_sx = js_val_to_sx(m.group(1)) - if local_bindings: - assertions.append(f' (let ({" ".join(local_bindings)}) (eval-hs "{hs_expr}") (assert (not (nil? it))))') + if local_pairs: + assertions.append(f' (assert (not (nil? (eval-hs-locals "{hs_expr}" {locals_sx}))))') else: assertions.append(f' (assert (not (nil? (eval-hs "{hs_expr}"))))') for m in re.finditer(r'expect\([^)]*\)\.toHaveLength\((\d+)\)', body): length = m.group(1) - if local_bindings: - assertions.append(f' (let ({" ".join(local_bindings)}) (eval-hs "{hs_expr}") (assert= (len it) {length}))') + if local_pairs: + assertions.append(f' (assert= (len (eval-hs-locals "{hs_expr}" {locals_sx})) {length})') else: assertions.append(f' (assert= (len (eval-hs "{hs_expr}")) {length})') @@ -1461,7 +1465,7 @@ output.append('(define hs-cleanup!') output.append(' (fn ()') output.append(' (dom-set-inner-html (dom-body) "")))') output.append('') -output.append(';; Evaluate a hyperscript expression and return its result.') +output.append(';; Evaluate a hyperscript expression and return the last-expression value.') output.append(';; Compiles the expression, wraps in a thunk, evaluates, returns result.') output.append('(define eval-hs') output.append(' (fn (src)') @@ -1477,6 +1481,28 @@ output.append(' (nth _e 1)') output.append(' (raise _e))))') output.append(' (handler nil))))))') output.append('') +output.append(';; Evaluate a hyperscript expression with locals. bindings = list of (symbol value).') +output.append(';; The locals are injected as fn params so they resolve in the handler body.') +output.append('(define eval-hs-locals') +output.append(' (fn (src bindings)') +output.append(' (let ((sx (hs-to-sx (hs-compile src))))') +output.append(' (let ((names (map (fn (b) (first b)) bindings))') +output.append(' (vals (map (fn (b) (nth b 1)) bindings)))') +output.append(' (let ((param-list (cons (quote me) names)))') +output.append(' (let ((wrapper (list (quote fn) param-list') +output.append(' (list (quote let)') +output.append(' (list (list (quote it) nil) (list (quote event) nil))') +output.append(' sx (quote it)))))') +output.append(' (let ((handler (eval-expr-cek wrapper)))') +output.append(' (guard') +output.append(' (_e') +output.append(' (true') +output.append(' (if') +output.append(' (and (list? _e) (= (first _e) "hs-return"))') +output.append(' (nth _e 1)') +output.append(' (raise _e))))') +output.append(' (apply handler (cons nil vals))))))))))') +output.append('') output.append(';; Evaluate with a specific me value (for "I am between" etc.)') output.append('(define eval-hs-with-me') output.append(' (fn (src me-val)')