HS runtime: empty/swap/compound events, host-set! fix — 403→423 (51%)

- Fix host-set → host-set! in emit-inc/emit-dec (increment/decrement properties)
- Implement empty/clear command: parser dispatch, compiler, polymorphic runtime
- Implement swap command: parser dispatch, compiler (let+do temp swap pattern)
- Add parse-compound-event-name: joins dot/colon tokens (example.event, htmx:load)
- Add hs-compile to source parser (was only in WASM deploy copy)
- Add clear/swap to tokenizer keywords and cmd-kw? list
- Generator: fix run() with extra args, String.raw support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 09:17:43 +00:00
parent e2fe070dd4
commit eaf3c88a36
9 changed files with 1159 additions and 35 deletions

View File

@@ -674,11 +674,12 @@ def extract_hs_expr(raw):
def generate_eval_only_test(test, idx):
"""Generate SX deftest for no-HTML tests using eval-hs.
Handles patterns:
- run("expr").toBe(val)
- expect(run("expr")).toBe(val)
- var result = await run(`expr`); expect(result).toBe(val)
- run("expr").toEqual([...])
- run("expr").toBe(val) or run("expr", opts).toBe(val)
- expect(run("expr")).toBe(val) or expect(run("expr", opts)).toBe(val)
- var result = await run(`expr`, opts); expect(result).toBe(val)
- run("expr").toEqual([...]) or run("expr").toEqual({...})
- run("expr").toThrow()
Also handles String.raw`expr` template literals.
"""
body = test.get('body', '')
lines = []
@@ -687,28 +688,47 @@ def generate_eval_only_test(test, idx):
assertions = []
# Pattern 1: Inline — run("expr").toBe(val) or expect(run("expr")).toBe(val)
# Shared sub-pattern for run() call with optional String.raw and extra args:
# run(QUOTE expr QUOTE) or run(QUOTE expr QUOTE, opts) or run(String.raw`expr`, opts)
# Extra args can contain nested parens/braces, so we allow anything non-greedy up to the
# matching close-paren by tracking that the close-paren follows the quote.
_Q = r'["\x27`]' # quote character class
_RUN_OPEN = r'(?:await\s+)?run\((?:String\.raw)?(' + _Q + r')(.+?)\1' # groups: (quote, expr)
_RUN_ARGS = r'(?:\s*,\s*[^)]*(?:\([^)]*\)[^)]*)*)*' # optional extra args with nested parens
# Pattern 1: Inline — expect(run("expr", opts)).toBe(val) or run("expr", opts).toBe(val)
for m in re.finditer(
r'(?:expect\()?(?:await\s+)?run\((["\x27`])(.+?)\1\)\)?\.toBe\(([^)]+)\)',
r'(?:expect\()?' + _RUN_OPEN + _RUN_ARGS + r'\)\)?\.toBe\(([^)]+)\)',
body, re.DOTALL
):
hs_expr = extract_hs_expr(m.group(2))
expected_sx = js_val_to_sx(m.group(3))
assertions.append(f' (assert= (eval-hs "{hs_expr}") {expected_sx})')
# Pattern 1b: Inline — run("expr").toEqual([...])
# Pattern 1b: Inline — run("expr", opts).toEqual([...])
for m in re.finditer(
r'(?:expect\()?(?:await\s+)?run\((["\x27`])(.+?)\1\)\)?\.toEqual\((\[.*?\])\)',
r'(?:expect\()?' + _RUN_OPEN + _RUN_ARGS + r'\)\)?\.toEqual\((\[.*?\])\)',
body, re.DOTALL
):
hs_expr = extract_hs_expr(m.group(2))
expected_sx = js_val_to_sx(m.group(3))
assertions.append(f' (assert= (eval-hs "{hs_expr}") {expected_sx})')
# Pattern 2: Two-line — var result = await run(`expr`); expect(result).toBe(val)
# Pattern 1c: Inline — run("expr", opts).toEqual({...})
if not assertions:
for m in re.finditer(
r'(?:expect\()?' + _RUN_OPEN + _RUN_ARGS + r'\)\)?\.toEqual\((\{.*?\})\)',
body, re.DOTALL
):
hs_expr = extract_hs_expr(m.group(2))
# Object toEqual — emit as dict assertion comment (can't fully convert JS objects to SX)
obj_str = m.group(3).strip()
assertions.append(f' ;; TODO: assert= (eval-hs "{hs_expr}") against {obj_str}')
# Pattern 2: Two-line — var result = await run(`expr`, opts); expect(result...).toBe/toEqual(val)
if not assertions:
run_match = re.search(
r'(?:var|let|const)\s+\w+\s*=\s*(?:await\s+)?run\((["\x27`])(.+?)\1\)',
r'(?:var|let|const)\s+\w+\s*=\s*' + _RUN_OPEN + _RUN_ARGS + r'\)',
body, re.DOTALL
)
if run_match:
@@ -722,7 +742,7 @@ def generate_eval_only_test(test, idx):
# Pattern 3: toThrow — expect(() => run("expr")).toThrow()
for m in re.finditer(
r'run\((["\x27`])(.+?)\1\).*?\.toThrow\(\)',
r'run\((?:String\.raw)?(["\x27`])(.+?)\1\).*?\.toThrow\(\)',
body, re.DOTALL
):
hs_expr = extract_hs_expr(m.group(2))