Step 17b: bytecode-compiled text-layout, WASM library import fix

- text-layout.sx added to WASM bytecode pipeline (9K compiled)
- Fix multi-list map calls (map-indexed + nth instead of map fn list1 list2)
- pretext-layout-lines and pretext-position-line moved to library exports
- Browser load-sxbc: handle VmSuspended for import, copy library exports
  to global_env after module load (define-library export fix)
- compile-modules.js: text-layout in SOURCE_MAP, FILES, and entry deps
- Island uses library functions (break-lines, pretext-layout-lines)
  instead of inlining — runs on bytecode VM when exports resolve

Known issue: define-library exports don't propagate to browser global env
yet. The load-sxbc import suspension handler resumes correctly but
bind_import_set doesn't fire. Needs deeper investigation into how the
WASM kernel's define-library registers exports vs how other libraries
(adapter-html, tw) make their exports available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 16:37:04 +00:00
parent 676ec6dd2b
commit 699dd5ad69
17 changed files with 2270 additions and 1662 deletions

View File

@@ -198,23 +198,54 @@ def parse_checks(check):
all_checks.append(('skip', part[:60], None, None))
# Deduplicate: keep last per (type, name, key)
# Deduplicate: keep last per (element, property).
# Pre-action and post-action assertions for the same property get the same key,
# so only the post-action assertion (the last one) survives.
seen = {}
for c in all_checks:
key = (c[0], c[1], c[2] if c[0] == 'class' else None)
typ, name = c[0], c[1]
if typ in ('class',):
key = (name, 'class', c[2])
elif typ in ('innerHTML', 'textContent'):
key = (name, 'content')
elif typ in ('style', 'computedStyle'):
key = (name, 'style', c[2])
elif typ in ('attr', 'hasAttr'):
key = (name, 'attr', c[2])
elif typ in ('noParent', 'hasParent'):
key = (name, 'parent')
elif typ in ('value',):
key = (name, 'value')
else:
key = (typ, name, c[2])
seen[key] = c
return list(seen.values())
def make_ref_fn(elements, var_names):
"""Create a ref function that maps upstream JS variable names to SX let-bound variables."""
tag_to_var = {}
"""Create a ref function that maps upstream JS variable names to SX let-bound variables.
Upstream naming conventions:
- div, form, button, select — first element of that tag type
- d1, d2, d3 — elements by position (1-indexed)
- div1, div2, div3 — divs by position among same tag (1-indexed)
- bar, btn, A, B — elements by ID
"""
# Map tag → first UNNAMED element of that tag (no id)
tag_to_unnamed = {}
# Map tag → list of vars for elements of that tag (ordered)
tag_to_all = {}
id_to_var = {}
last_var = var_names[-1] if var_names else '_el-div'
first_var = var_names[0] if var_names else '_el-div'
for i, el in enumerate(elements):
tag_to_var[el['tag']] = var_names[i]
tag = el['tag']
if tag not in tag_to_unnamed and not el['id']:
tag_to_unnamed[tag] = var_names[i]
if tag not in tag_to_all:
tag_to_all[tag] = []
tag_to_all[tag].append(var_names[i])
if el['id']:
id_to_var[el['id']] = var_names[i]
@@ -223,12 +254,42 @@ def make_ref_fn(elements, var_names):
'output'}
def ref(name):
if name in tags:
return tag_to_var.get(name, last_var)
# Exact ID match first
if name in id_to_var:
return id_to_var[name]
if re.match(r'^[a-z]+\d*$', name) and len(elements) > 0:
return last_var
# Bare tag name → first UNNAMED element of that tag (upstream convention:
# named elements use their ID, unnamed use their tag)
if name in tags:
if name in tag_to_unnamed:
return tag_to_unnamed[name]
# Fallback: first element of that tag (even if named)
return tag_to_all.get(name, [first_var])[0]
# Tag + number: div1→1st div, div2→2nd div, form1→1st form, etc.
m = re.match(r'^([a-z]+)(\d+)$', name)
if m:
tag_part, num = m.group(1), int(m.group(2))
if tag_part in tag_to_all:
idx = num - 1 # 1-indexed
if 0 <= idx < len(tag_to_all[tag_part]):
return tag_to_all[tag_part][idx]
# Positional: d1→1st element, d2→2nd, d3→3rd, etc.
m = re.match(r'^d(\d+)$', name)
if m:
idx = int(m.group(1)) - 1 # 1-indexed
if 0 <= idx < len(var_names):
return var_names[idx]
# Short aliases: btn → look up as ID
if name == 'btn':
return id_to_var.get('btn', tag_to_unnamed.get('button', first_var))
# Single-letter or short lowercase → try as ID, fallback to first element
if re.match(r'^[a-z]+$', name) and len(elements) > 0:
return first_var
return f'(dom-query-by-id "{name}")'
return ref
@@ -442,6 +503,8 @@ def emit_element_setup(lines, elements, var_names):
# Clean up: collapse spaces, dedupe then
hs_val = re.sub(r'\s+', ' ', hs_val)
hs_val = re.sub(r'(then\s*)+then', 'then', hs_val)
# Don't insert 'then' between event name and first command in 'on' handlers
hs_val = re.sub(r'\bon (\w[\w.:+-]*) then\b', r'on \1 ', hs_val)
hs_val = hs_val.strip()
if not hs_val:
lines.append(f' (dom-append (dom-body) {var})')