Phase 7a: affinity annotations + fix parser escape sequences

Add :affinity :client/:server/:auto annotations to defcomp, with
render-target function combining affinity + IO analysis. Includes
spec (eval.sx, deps.sx), tests, Python evaluator, and demo page.

Fix critical bug: Python SX parser _ESCAPE_MAP was missing \r and \0,
causing bootstrapped JS parser to treat 'r' as whitespace — breaking
all client-side SX parsing. Also add \0 to JS string emitter and
fix serializer round-tripping for \r and \0.

Reserved word escaping: bootstrappers now auto-append _ to identifiers
colliding with JS/Python reserved words (e.g. default → default_,
final → final_), so the spec never needs to avoid host language keywords.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 23:53:33 +00:00
parent 81d8e55fb0
commit a70ff2b153
19 changed files with 540 additions and 224 deletions

View File

@@ -565,7 +565,7 @@ def _sf_defkeyframes(expr: list, env: dict) -> Any:
def _sf_defcomp(expr: list, env: dict) -> Component:
"""``(defcomp ~name (&key param1 param2 &rest children) body)``"""
"""``(defcomp ~name (&key ...) [:affinity :client|:server] body)``"""
if len(expr) < 4:
raise EvalError("defcomp requires name, params, and body")
name_sym = expr[1]
@@ -593,21 +593,38 @@ def _sf_defcomp(expr: list, env: dict) -> Component:
params.append(p.name)
else:
params.append(p.name)
# Skip children param name after &rest
elif isinstance(p, str):
params.append(p)
# Body is always last element; keyword annotations between params and body
body = expr[-1]
affinity = _defcomp_kwarg(expr, "affinity", "auto")
comp = Component(
name=comp_name,
params=params,
has_children=has_children,
body=expr[3],
body=body,
closure=dict(env),
affinity=affinity,
)
env[name_sym.name] = comp
return comp
def _defcomp_kwarg(expr: list, key: str, default: str) -> str:
"""Extract a keyword annotation from defcomp, e.g. :affinity :client."""
# Scan from index 3 to second-to-last for :key value pairs
for i in range(3, len(expr) - 1):
item = expr[i]
if isinstance(item, Keyword) and item.name == key:
val = expr[i + 1]
if isinstance(val, Keyword):
return val.name
return str(val)
return default
def _sf_begin(expr: list, env: dict) -> Any:
if len(expr) < 2:
return NIL