Generator: context-aware variable refs — 444/831 (53%, +20)

Fixed ref() to map upstream JS variable names to let-bound SX variables
using element context (tag→var, id→var, make-return→last-var). Fixes
if (0→14/19), put (14→18), on (20→23), and other categories where the
upstream test uses make() return variables like d1, div, btn.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 15:11:03 +00:00
parent e98aedf803
commit ce4579badb
3 changed files with 495 additions and 470 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -56,57 +56,42 @@ def parse_html(html):
Parser().feed(html)
return elements
def parse_action(action):
def parse_action(action, ref):
"""Convert upstream action to SX. Returns list of SX expressions."""
if not action or action == '(see body)':
return []
exprs = []
# Split on ';' for multi-step actions
for part in action.split(';'):
part = part.strip()
if not part:
continue
# Pattern: var.click()
m = re.match(r'(\w+)\.click\(\)', part)
if m:
name = m.group(1)
exprs.append(f'(dom-dispatch {ref(name)} "click" nil)')
exprs.append(f'(dom-dispatch {ref(m.group(1))} "click" nil)')
continue
# Pattern: var.dispatchEvent(new CustomEvent("name"))
m = re.match(r'(\w+)\.dispatchEvent\(new CustomEvent\("(\w+)"\)\)', part)
m = re.match(r'(\w+)\.dispatchEvent\(new CustomEvent\("([\w:.-]+)"', part)
if m:
exprs.append(f'(dom-dispatch {ref(m.group(1))} "{m.group(2)}" nil)')
continue
# Pattern: var.dispatchEvent(new CustomEvent("name", {detail: {...}}))
m = re.match(r'(\w+)\.dispatchEvent\(new CustomEvent\("(\w+)"', part)
if m:
exprs.append(f'(dom-dispatch {ref(m.group(1))} "{m.group(2)}" nil)')
continue
# Pattern: var.setAttribute("name", "value")
m = re.match(r'(\w+)\.setAttribute\("(\w+)",\s*"([^"]*)"\)', part)
m = re.match(r'(\w+)\.setAttribute\("([\w-]+)",\s*"([^"]*)"\)', part)
if m:
exprs.append(f'(dom-set-attr {ref(m.group(1))} "{m.group(2)}" "{m.group(3)}")')
continue
# Pattern: var.focus()
m = re.match(r'(\w+)\.focus\(\)', part)
if m:
exprs.append(f'(dom-focus {ref(m.group(1))})')
continue
# Pattern: var.appendChild(document.createElement("TAG"))
m = re.match(r'(\w+)\.appendChild\(document\.createElement\("(\w+)"\)', part)
if m:
exprs.append(f'(dom-append {ref(m.group(1))} (dom-create-element "{m.group(2)}"))')
continue
# Skip unrecognized
# Sanitize comment — remove all chars that SX parser treats specially
safe = re.sub(r'[\'\"$@`(),;\\#\[\]{}]', '_', part[:40])
exprs.append(f';; SKIP action: {safe}')
@@ -203,17 +188,42 @@ def parse_checks(check):
return list(seen.values())
def ref(name):
"""Convert a JS variable name to SX element reference.
For IDs we use dom-query-by-id at runtime (safer than variable refs).
For tags we use the let-bound variable."""
tags = {'div', 'form', 'button', 'input', 'span', 'p', 'a', 'section', 'ul', 'li'}
if name in tags:
return f'_el-{name}'
# ID references — use dom-query-by-id for reliability
return f'(dom-query-by-id "{name}")'
def make_ref_fn(elements, var_names):
"""Create a ref function that maps upstream JS variable names to SX let-bound variables.
def check_to_sx(check):
Upstream patterns:
- 'div', 'form' etc. → last element with that tag (the make() return value)
- 'd1', 'bar', 'p1' etc. → element with that ID, or last element if no ID match
"""
# Build mappings
tag_to_var = {} # tag -> last var with that tag
id_to_var = {} # id -> var
last_var = var_names[-1] if var_names else '_el-div'
for i, el in enumerate(elements):
tag_to_var[el['tag']] = var_names[i]
if el['id']:
id_to_var[el['id']] = var_names[i]
tags = {'div', 'form', 'button', 'input', 'span', 'p', 'a', 'section',
'ul', 'li', 'select', 'textarea', 'details', 'dialog', 'template'}
def ref(name):
if name in tags:
return tag_to_var.get(name, last_var)
if name in id_to_var:
return id_to_var[name]
# make() return variable pattern: d1, d2, div1, btn1, etc.
# These refer to the last element created by make()
if re.match(r'^[a-z]+\d*$', name) and len(elements) > 0:
# If there's an element with this as id, use dom-query-by-id
# Otherwise it's the make() return var — use last element
return last_var
return f'(dom-query-by-id "{name}")'
return ref
def check_to_sx(check, ref):
"""Convert a parsed check tuple to an SX assertion."""
typ, name, key, val = check
r = ref(name)
@@ -250,8 +260,6 @@ def check_to_sx(check):
def generate_test(test, idx):
"""Generate SX deftest for an upstream test."""
elements = parse_html(test['html'])
actions = parse_action(test['action'])
checks = parse_checks(test['check'])
if not elements and not test.get('html', '').strip():
# eval-only test — no HTML at all
@@ -277,6 +285,13 @@ def generate_test(test, idx):
used_names.add(var)
var_names.append(var)
# Create ref function with element context
ref = make_ref_fn(elements, var_names)
# Parse actions and checks with context-aware ref
actions = parse_action(test['action'], ref)
checks = parse_checks(test['check'])
# Create elements
bindings = []
for i, el in enumerate(elements):
@@ -305,7 +320,6 @@ def generate_test(test, idx):
# Escape for SX double-quoted string
hs_escaped = hs_val.replace('\\', '\\\\').replace('"', '\\"')
lines.append(f' (dom-set-attr {var} "_" "{hs_escaped}")')
all_hs_sources.add(hs_escaped)
for aname, aval in el['attrs'].items():
if '\\' in aval or '\n' in aval or aname.startswith('['):
lines.append(f' ;; SKIP attr {aname} (contains special chars)')
@@ -322,7 +336,7 @@ def generate_test(test, idx):
# Assertions
for check in checks:
sx = check_to_sx(check)
sx = check_to_sx(check, ref)
lines.append(f' {sx}')
lines.append(' ))') # close let + deftest

View File

@@ -256,6 +256,17 @@ test.describe('Hyperscript behavioral tests', () => {
for (const [t, n] of Object.entries(errTypes).sort((a,b) => b[1] - a[1])) {
console.log(` ${t}: ${n}`);
}
// Show sample failures per type
for (const [t, n] of Object.entries(errTypes).sort((a,b) => b[1] - a[1])) {
const samples = results.filter(r => !r.p).filter(r => {
const e = r.e || '';
if (t === 'crash') return e.includes('callFn');
if (t === 'stub') return e.includes('NOT IMPLEMENTED');
if (t === 'timeout') return e === 'TIMEOUT';
return false;
}).slice(0, 3);
for (const s of samples) console.log(` ${s.s}/${s.n}: ${(s.e||'').slice(0, 120)}`);
}
expect(results.length).toBeGreaterThanOrEqual(830);
expect(passed).toBeGreaterThanOrEqual(420);