Transpile signals.sx to JS and Python via bootstrappers

Both bootstrappers now handle the full signal runtime:
- &rest lambda params → JS arguments.slice / Python *args
- Signal/Island/TrackingContext platform functions in both hosts
- RENAMES for all signal, island, tracking, and reactive DOM identifiers
- signals auto-included with DOM adapter (JS) and HTML adapter (Python)
- Signal API exports on Sx object (signal, deref, reset, swap, computed, effect, batch)
- New DOM primitives: createComment, domRemove, domChildNodes, domRemoveChildrenAfter, domSetData
- jsonSerialize/isEmptyDict for island state serialization
- Demo HTML page exercising all signal primitives

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 10:17:16 +00:00
parent 26320abd64
commit fe289287ec
5 changed files with 2536 additions and 511 deletions

View File

@@ -148,6 +148,32 @@ class PyEmitter:
"callable?": "is_callable",
"lambda?": "is_lambda",
"component?": "is_component",
"island?": "is_island",
"make-island": "make_island",
"make-signal": "make_signal",
"signal?": "is_signal",
"signal-value": "signal_value",
"signal-set-value!": "signal_set_value",
"signal-subscribers": "signal_subscribers",
"signal-add-sub!": "signal_add_sub",
"signal-remove-sub!": "signal_remove_sub",
"signal-deps": "signal_deps",
"signal-set-deps!": "signal_set_deps",
"set-tracking-context!": "set_tracking_context",
"get-tracking-context": "get_tracking_context",
"make-tracking-context": "make_tracking_context",
"tracking-context-deps": "tracking_context_deps",
"tracking-context-add-dep!": "tracking_context_add_dep",
"tracking-context-notify-fn": "tracking_context_notify_fn",
"identical?": "is_identical",
"notify-subscribers": "notify_subscribers",
"flush-subscribers": "flush_subscribers",
"dispose-computed": "dispose_computed",
"with-island-scope": "with_island_scope",
"register-in-scope": "register_in_scope",
"*batch-depth*": "_batch_depth",
"*batch-queue*": "_batch_queue",
"*island-scope*": "_island_scope",
"macro?": "is_macro",
"primitive?": "is_primitive",
"get-primitive": "get_primitive",
@@ -232,6 +258,11 @@ class PyEmitter:
"dispatch-html-form": "dispatch_html_form",
"render-lambda-html": "render_lambda_html",
"make-raw-html": "make_raw_html",
"render-html-island": "render_html_island",
"serialize-island-state": "serialize_island_state",
"json-serialize": "json_serialize",
"empty-dict?": "is_empty_dict",
"sf-defisland": "sf_defisland",
# adapter-sx.sx
"render-to-sx": "render_to_sx",
"aser": "aser",
@@ -379,11 +410,26 @@ class PyEmitter:
params = expr[1]
body = expr[2:]
param_names = []
for p in params:
rest_name = None
i = 0
while i < len(params):
p = params[i]
if isinstance(p, Symbol) and p.name == "&rest":
# Next param is the rest parameter
if i + 1 < len(params):
rest_name = self._mangle(params[i + 1].name if isinstance(params[i + 1], Symbol) else str(params[i + 1]))
i += 2
continue
else:
i += 1
continue
if isinstance(p, Symbol):
param_names.append(self._mangle(p.name))
else:
param_names.append(str(p))
i += 1
if rest_name:
param_names.append(f"*{rest_name}")
params_str = ", ".join(param_names)
if len(body) == 1:
body_py = self.emit(body[0])
@@ -867,6 +913,7 @@ SPEC_MODULES = {
"deps": ("deps.sx", "deps (component dependency analysis)"),
"router": ("router.sx", "router (client-side route matching)"),
"engine": ("engine.sx", "engine (fetch/swap/trigger pure logic)"),
"signals": ("signals.sx", "signals (reactive signal runtime)"),
}
@@ -1005,6 +1052,9 @@ def compile_ref_to_py(
raise ValueError(f"Unknown spec module: {sm!r}. Valid: {', '.join(SPEC_MODULES)}")
spec_mod_set.add(sm)
has_deps = "deps" in spec_mod_set
# html adapter uses signal runtime for server-side island rendering
if "html" in adapter_set and "signals" in SPEC_MODULES:
spec_mod_set.add("signals")
# Core files always included, then selected adapters, then spec modules
sx_files = [
@@ -1092,7 +1142,7 @@ from typing import Any
# =========================================================================
from shared.sx.types import (
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro,
NIL, Symbol, Keyword, Lambda, Component, Island, Continuation, Macro,
HandlerDef, QueryDef, ActionDef, PageDef, _ShiftSignal,
)
from shared.sx.parser import SxExpr
@@ -1197,6 +1247,10 @@ def type_of(x):
return "lambda"
if isinstance(x, Component):
return "component"
if isinstance(x, Island):
return "island"
if isinstance(x, _Signal):
return "signal"
if isinstance(x, Macro):
return "macro"
if isinstance(x, _RawHTML):
@@ -1235,6 +1289,11 @@ def make_component(name, params, has_children, body, env, affinity="auto"):
body=body, closure=dict(env), affinity=str(affinity) if affinity else "auto")
def make_island(name, params, has_children, body, env):
return Island(name=name, params=list(params), has_children=has_children,
body=body, closure=dict(env))
def make_macro(params, rest_param, body, env, name=None):
return Macro(params=list(params), rest_param=rest_param, body=body,
closure=dict(env), name=name)
@@ -1369,6 +1428,119 @@ def is_macro(x):
return isinstance(x, Macro)
def is_island(x):
return isinstance(x, Island)
def is_identical(a, b):
return a is b
# -------------------------------------------------------------------------
# Signal platform -- reactive state primitives
# -------------------------------------------------------------------------
class _Signal:
"""Reactive signal container."""
__slots__ = ("value", "subscribers", "deps")
def __init__(self, value):
self.value = value
self.subscribers = []
self.deps = []
class _TrackingContext:
"""Context for discovering signal dependencies."""
__slots__ = ("notify_fn", "deps")
def __init__(self, notify_fn):
self.notify_fn = notify_fn
self.deps = []
_tracking_context = None
def make_signal(value):
return _Signal(value)
def is_signal(x):
return isinstance(x, _Signal)
def signal_value(s):
return s.value if isinstance(s, _Signal) else s
def signal_set_value(s, v):
if isinstance(s, _Signal):
s.value = v
def signal_subscribers(s):
return list(s.subscribers) if isinstance(s, _Signal) else []
def signal_add_sub(s, fn):
if isinstance(s, _Signal) and fn not in s.subscribers:
s.subscribers.append(fn)
def signal_remove_sub(s, fn):
if isinstance(s, _Signal) and fn in s.subscribers:
s.subscribers.remove(fn)
def signal_deps(s):
return list(s.deps) if isinstance(s, _Signal) else []
def signal_set_deps(s, deps):
if isinstance(s, _Signal):
s.deps = list(deps) if isinstance(deps, list) else []
def set_tracking_context(ctx):
global _tracking_context
_tracking_context = ctx
def get_tracking_context():
global _tracking_context
return _tracking_context if _tracking_context is not None else NIL
def make_tracking_context(notify_fn):
return _TrackingContext(notify_fn)
def tracking_context_deps(ctx):
return ctx.deps if isinstance(ctx, _TrackingContext) else []
def tracking_context_add_dep(ctx, s):
if isinstance(ctx, _TrackingContext) and s not in ctx.deps:
ctx.deps.append(s)
def tracking_context_notify_fn(ctx):
return ctx.notify_fn if isinstance(ctx, _TrackingContext) else NIL
def json_serialize(obj):
import json
try:
return json.dumps(obj)
except (TypeError, ValueError):
return "{}"
def is_empty_dict(d):
if not isinstance(d, dict):
return True
return len(d) == 0
def env_has(env, name):
return name in env