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