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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user