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

@@ -139,6 +139,32 @@ class JSEmitter:
"callable?": "isCallable",
"lambda?": "isLambda",
"component?": "isComponent",
"island?": "isIsland",
"make-island": "makeIsland",
"make-signal": "makeSignal",
"signal?": "isSignal",
"signal-value": "signalValue",
"signal-set-value!": "signalSetValue",
"signal-subscribers": "signalSubscribers",
"signal-add-sub!": "signalAddSub",
"signal-remove-sub!": "signalRemoveSub",
"signal-deps": "signalDeps",
"signal-set-deps!": "signalSetDeps",
"set-tracking-context!": "setTrackingContext",
"get-tracking-context": "getTrackingContext",
"make-tracking-context": "makeTrackingContext",
"tracking-context-deps": "trackingContextDeps",
"tracking-context-add-dep!": "trackingContextAddDep",
"tracking-context-notify-fn": "trackingContextNotifyFn",
"identical?": "isIdentical",
"notify-subscribers": "notifySubscribers",
"flush-subscribers": "flushSubscribers",
"dispose-computed": "disposeComputed",
"with-island-scope": "withIslandScope",
"register-in-scope": "registerInScope",
"*batch-depth*": "_batchDepth",
"*batch-queue*": "_batchQueue",
"*island-scope*": "_islandScope",
"macro?": "isMacro",
"primitive?": "isPrimitive",
"get-primitive": "getPrimitive",
@@ -166,6 +192,10 @@ class JSEmitter:
"render-list-to-html": "renderListToHtml",
"render-html-element": "renderHtmlElement",
"render-html-component": "renderHtmlComponent",
"render-html-island": "renderHtmlIsland",
"serialize-island-state": "serializeIslandState",
"json-serialize": "jsonSerialize",
"empty-dict?": "isEmptyDict",
"parse-element-args": "parseElementArgs",
"render-attrs": "renderAttrs",
"aser-list": "aserList",
@@ -191,6 +221,7 @@ class JSEmitter:
"sf-lambda": "sfLambda",
"sf-define": "sfDefine",
"sf-defcomp": "sfDefcomp",
"sf-defisland": "sfDefisland",
"defcomp-kwarg": "defcompKwarg",
"sf-defmacro": "sfDefmacro",
"sf-begin": "sfBegin",
@@ -240,6 +271,11 @@ class JSEmitter:
"render-dom-form?": "isRenderDomForm",
"dispatch-render-form": "dispatchRenderForm",
"render-lambda-dom": "renderLambdaDom",
"render-dom-island": "renderDomIsland",
"reactive-text": "reactiveText",
"reactive-attr": "reactiveAttr",
"reactive-fragment": "reactiveFragment",
"reactive-list": "reactiveList",
"dom-create-element": "domCreateElement",
"dom-append": "domAppend",
"dom-set-attr": "domSetAttr",
@@ -281,6 +317,11 @@ class JSEmitter:
"dom-query": "domQuery",
"dom-query-all": "domQueryAll",
"dom-tag-name": "domTagName",
"create-comment": "createComment",
"dom-remove": "domRemove",
"dom-child-nodes": "domChildNodes",
"dom-remove-children-after": "domRemoveChildrenAfter",
"dom-set-data": "domSetData",
"dict-has?": "dictHas",
"dict-delete!": "dictDelete",
"process-bindings": "processBindings",
@@ -605,17 +646,39 @@ class JSEmitter:
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
params_str = ", ".join(param_names)
# Build rest-param preamble if needed
rest_preamble = ""
if rest_name:
n = len(param_names)
rest_preamble = f"var {rest_name} = Array.prototype.slice.call(arguments, {n}); "
if len(body) == 1:
body_js = self.emit(body[0])
if rest_preamble:
return f"function({params_str}) {{ {rest_preamble}return {body_js}; }}"
return f"function({params_str}) {{ return {body_js}; }}"
# Multi-expression body: statements then return last
parts = []
if rest_preamble:
parts.append(rest_preamble.rstrip())
for b in body[:-1]:
parts.append(self.emit_statement(b))
parts.append(f"return {self.emit(body[-1])};")
@@ -1045,6 +1108,7 @@ ADAPTER_DEPS = {
SPEC_MODULES = {
"deps": ("deps.sx", "deps (component dependency analysis)"),
"router": ("router.sx", "router (client-side route matching)"),
"signals": ("signals.sx", "signals (reactive signal runtime)"),
}
@@ -1833,6 +1897,9 @@ def compile_ref_to_js(
if sm not in SPEC_MODULES:
raise ValueError(f"Unknown spec module: {sm!r}. Valid: {', '.join(SPEC_MODULES)}")
spec_mod_set.add(sm)
# dom adapter uses signal runtime for reactive islands
if "dom" in adapter_set and "signals" in SPEC_MODULES:
spec_mod_set.add("signals")
# boot.sx uses parse-route-pattern from router.sx
if "boot" in adapter_set:
spec_mod_set.add("router")
@@ -1877,6 +1944,7 @@ def compile_ref_to_js(
has_orch = "orchestration" in adapter_set
has_boot = "boot" in adapter_set
has_parser = "parser" in adapter_set
has_signals = "signals" in spec_mod_set
adapter_label = "+".join(sorted(adapter_set)) if adapter_set else "core-only"
# Determine which primitive modules to include
@@ -1925,7 +1993,7 @@ def compile_ref_to_js(
parts.append(CONTINUATIONS_JS)
if has_dom:
parts.append(ASYNC_IO_JS)
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router))
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router, has_signals))
parts.append(EPILOGUE)
from datetime import datetime, timezone
build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
@@ -1984,6 +2052,29 @@ PREAMBLE = '''\
}
Component.prototype._component = true;
function Island(name, params, hasChildren, body, closure) {
this.name = name;
this.params = params;
this.hasChildren = hasChildren;
this.body = body;
this.closure = closure || {};
}
Island.prototype._island = true;
function SxSignal(value) {
this.value = value;
this.subscribers = [];
this.deps = [];
}
SxSignal.prototype._signal = true;
function TrackingCtx(notifyFn) {
this.notifyFn = notifyFn;
this.deps = [];
}
var _trackingContext = null;
function Macro(params, restParam, body, closure, name) {
this.params = params;
this.restParam = restParam;
@@ -2240,6 +2331,8 @@ PLATFORM_JS_PRE = '''
if (x._thunk) return "thunk";
if (x._lambda) return "lambda";
if (x._component) return "component";
if (x._island) return "island";
if (x._signal) return "signal";
if (x._macro) return "macro";
if (x._raw) return "raw-html";
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
@@ -2287,7 +2380,41 @@ PLATFORM_JS_PRE = '''
function isCallable(x) { return typeof x === "function" || (x != null && x._lambda === true); }
function isLambda(x) { return x != null && x._lambda === true; }
function isComponent(x) { return x != null && x._component === true; }
function isIsland(x) { return x != null && x._island === true; }
function isMacro(x) { return x != null && x._macro === true; }
function isIdentical(a, b) { return a === b; }
// Island platform
function makeIsland(name, params, hasChildren, body, env) {
return new Island(name, params, hasChildren, body, merge(env));
}
// Signal platform
function makeSignal(value) { return new SxSignal(value); }
function isSignal(x) { return x != null && x._signal === true; }
function signalValue(s) { return s.value; }
function signalSetValue(s, v) { s.value = v; }
function signalSubscribers(s) { return s.subscribers.slice(); }
function signalAddSub(s, fn) { if (s.subscribers.indexOf(fn) < 0) s.subscribers.push(fn); }
function signalRemoveSub(s, fn) { var i = s.subscribers.indexOf(fn); if (i >= 0) s.subscribers.splice(i, 1); }
function signalDeps(s) { return s.deps.slice(); }
function signalSetDeps(s, deps) { s.deps = Array.isArray(deps) ? deps.slice() : []; }
function setTrackingContext(ctx) { _trackingContext = ctx; }
function getTrackingContext() { return _trackingContext || NIL; }
function makeTrackingContext(notifyFn) { return new TrackingCtx(notifyFn); }
function trackingContextDeps(ctx) { return ctx ? ctx.deps : []; }
function trackingContextAddDep(ctx, s) { if (ctx && ctx.deps.indexOf(s) < 0) ctx.deps.push(s); }
function trackingContextNotifyFn(ctx) { return ctx ? ctx.notifyFn : NIL; }
// JSON / dict helpers for island state serialization
function jsonSerialize(obj) {
try { return JSON.stringify(obj); } catch(e) { return "{}"; }
}
function isEmptyDict(d) {
if (!d || typeof d !== "object") return true;
for (var k in d) if (d.hasOwnProperty(k)) return false;
return true;
}
function envHas(env, name) { return name in env; }
function envGet(env, name) { return env[name]; }
@@ -2616,6 +2743,10 @@ PLATFORM_DOM_JS = """
return _hasDom ? document.createTextNode(s) : null;
}
function createComment(s) {
return _hasDom ? document.createComment(s || "") : null;
}
function createFragment() {
return _hasDom ? document.createDocumentFragment() : null;
}
@@ -2750,6 +2881,23 @@ PLATFORM_DOM_JS = """
function domTagName(el) { return el && el.tagName ? el.tagName : ""; }
// Island DOM helpers
function domRemove(node) {
if (node && node.parentNode) node.parentNode.removeChild(node);
}
function domChildNodes(el) {
if (!el || !el.childNodes) return [];
return Array.prototype.slice.call(el.childNodes);
}
function domRemoveChildrenAfter(marker) {
if (!marker || !marker.parentNode) return;
var parent = marker.parentNode;
while (marker.nextSibling) parent.removeChild(marker.nextSibling);
}
function domSetData(el, key, val) {
if (el) { if (!el._sxData) el._sxData = {}; el._sxData[key] = val; }
}
// =========================================================================
// Performance overrides — replace transpiled spec with imperative JS
// =========================================================================
@@ -3868,7 +4016,7 @@ def fixups_js(has_html, has_sx, has_dom):
return "\n".join(lines)
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps=False, has_router=False):
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps=False, has_router=False, has_signals=False):
# Parser: use compiled sxParse from parser.sx, or inline a minimal fallback
if has_parser:
parser = '''
@@ -4020,6 +4168,16 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has
api_lines.append(' registerIoDeps: typeof registerIoDeps === "function" ? registerIoDeps : null,')
api_lines.append(' asyncRender: typeof asyncSxRenderWithEnv === "function" ? asyncSxRenderWithEnv : null,')
api_lines.append(' asyncRenderToDom: typeof asyncRenderToDom === "function" ? asyncRenderToDom : null,')
if has_signals:
api_lines.append(' signal: signal,')
api_lines.append(' deref: deref,')
api_lines.append(' reset: reset_b,')
api_lines.append(' swap: swap_b,')
api_lines.append(' computed: computed,')
api_lines.append(' effect: effect,')
api_lines.append(' batch: batch,')
api_lines.append(' isSignal: isSignal,')
api_lines.append(' makeSignal: makeSignal,')
api_lines.append(f' _version: "{version}"')
api_lines.append(' };')
api_lines.append('')