diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index d657b2d..1a36e68 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-11T03:56:09Z"; + var SX_VERSION = "2026-03-11T04:41:27Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -312,6 +312,7 @@ PRIMITIVES["even?"] = function(n) { return n % 2 === 0; }; PRIMITIVES["zero?"] = function(n) { return n === 0; }; PRIMITIVES["boolean?"] = function(x) { return x === true || x === false; }; + PRIMITIVES["component-affinity"] = componentAffinity; // core.strings @@ -579,6 +580,92 @@ return makeThunk(componentBody(comp), local); }; + // ========================================================================= + // Platform: deps module — component dependency analysis + // ========================================================================= + + function componentDeps(c) { + return c.deps ? c.deps.slice() : []; + } + + function componentSetDeps(c, deps) { + c.deps = deps; + } + + function componentCssClasses(c) { + return c.cssClasses ? c.cssClasses.slice() : []; + } + + function envComponents(env) { + var names = []; + for (var k in env) { + var v = env[k]; + if (v && (v._component || v._macro)) names.push(k); + } + return names; + } + + function regexFindAll(pattern, source) { + var re = new RegExp(pattern, "g"); + var results = []; + var m; + while ((m = re.exec(source)) !== null) { + if (m[1] !== undefined) results.push(m[1]); + else results.push(m[0]); + } + return results; + } + + function scanCssClasses(source) { + var classes = {}; + var result = []; + var m; + var re1 = /:class\s+"([^"]*)"/g; + while ((m = re1.exec(source)) !== null) { + var parts = m[1].split(/\s+/); + for (var i = 0; i < parts.length; i++) { + if (parts[i] && !classes[parts[i]]) { + classes[parts[i]] = true; + result.push(parts[i]); + } + } + } + var re2 = /:class\s+\(str\s+((?:"[^"]*"\s*)+)\)/g; + while ((m = re2.exec(source)) !== null) { + var re3 = /"([^"]*)"/g; + var m2; + while ((m2 = re3.exec(m[1])) !== null) { + var parts2 = m2[1].split(/\s+/); + for (var j = 0; j < parts2.length; j++) { + if (parts2[j] && !classes[parts2[j]]) { + classes[parts2[j]] = true; + result.push(parts2[j]); + } + } + } + } + var re4 = /;;\s*@css\s+(.+)/g; + while ((m = re4.exec(source)) !== null) { + var parts3 = m[1].split(/\s+/); + for (var k = 0; k < parts3.length; k++) { + if (parts3[k] && !classes[parts3[k]]) { + classes[parts3[k]] = true; + result.push(parts3[k]); + } + } + } + return result; + } + + function componentIoRefs(c) { + return c.ioRefs ? c.ioRefs.slice() : []; + } + + function componentSetIoRefs(c, refs) { + c.ioRefs = refs; + } + + // ========================================================================= // Platform interface — Parser // ========================================================================= @@ -3185,6 +3272,167 @@ callExpr.push(dictGet(kwargs, k)); } } var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), sxHydrateIslands(NIL), processElements(NIL)); }; + // === Transpiled from deps (component dependency analysis) === + + // scan-refs + var scanRefs = function(node) { return (function() { + var refs = []; + scanRefsWalk(node, refs); + return refs; +})(); }; + + // scan-refs-walk + var scanRefsWalk = function(node, refs) { return (isSxTruthy((typeOf(node) == "symbol")) ? (function() { + var name = symbolName(node); + return (isSxTruthy(startsWith(name, "~")) ? (isSxTruthy(!isSxTruthy(contains(refs, name))) ? append_b(refs, name) : NIL) : NIL); +})() : (isSxTruthy((typeOf(node) == "list")) ? forEach(function(item) { return scanRefsWalk(item, refs); }, node) : (isSxTruthy((typeOf(node) == "dict")) ? forEach(function(key) { return scanRefsWalk(dictGet(node, key), refs); }, keys(node)) : NIL))); }; + + // transitive-deps-walk + var transitiveDepsWalk = function(n, seen, env) { return (isSxTruthy(!isSxTruthy(contains(seen, n))) ? (append_b(seen, n), (function() { + var val = envGet(env, n); + return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(ref) { return transitiveDepsWalk(ref, seen, env); }, scanRefs(componentBody(val))) : (isSxTruthy((typeOf(val) == "macro")) ? forEach(function(ref) { return transitiveDepsWalk(ref, seen, env); }, scanRefs(macroBody(val))) : NIL)); +})()) : NIL); }; + + // transitive-deps + var transitiveDeps = function(name, env) { return (function() { + var seen = []; + var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name))); + transitiveDepsWalk(key, seen, env); + return filter(function(x) { return !isSxTruthy((x == key)); }, seen); +})(); }; + + // compute-all-deps + var computeAllDeps = function(env) { return forEach(function(name) { return (function() { + var val = envGet(env, name); + return (isSxTruthy((typeOf(val) == "component")) ? componentSetDeps(val, transitiveDeps(name, env)) : NIL); +})(); }, envComponents(env)); }; + + // scan-components-from-source + var scanComponentsFromSource = function(source) { return (function() { + var matches = regexFindAll("\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)", source); + return map(function(m) { return (String("~") + String(m)); }, matches); +})(); }; + + // components-needed + var componentsNeeded = function(pageSource, env) { return (function() { + var direct = scanComponentsFromSource(pageSource); + var allNeeded = []; + { var _c = direct; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(allNeeded, name)))) { + allNeeded.push(name); +} +(function() { + var val = envGet(env, name); + return (function() { + var deps = (isSxTruthy((isSxTruthy((typeOf(val) == "component")) && !isSxTruthy(isEmpty(componentDeps(val))))) ? componentDeps(val) : transitiveDeps(name, env)); + return forEach(function(dep) { return (isSxTruthy(!isSxTruthy(contains(allNeeded, dep))) ? append_b(allNeeded, dep) : NIL); }, deps); +})(); +})(); } } + return allNeeded; +})(); }; + + // page-component-bundle + var pageComponentBundle = function(pageSource, env) { return componentsNeeded(pageSource, env); }; + + // page-css-classes + var pageCssClasses = function(pageSource, env) { return (function() { + var needed = componentsNeeded(pageSource, env); + var classes = []; + { var _c = needed; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; (function() { + var val = envGet(env, name); + return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(cls) { return (isSxTruthy(!isSxTruthy(contains(classes, cls))) ? append_b(classes, cls) : NIL); }, componentCssClasses(val)) : NIL); +})(); } } + { var _c = scanCssClasses(pageSource); for (var _i = 0; _i < _c.length; _i++) { var cls = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(classes, cls)))) { + classes.push(cls); +} } } + return classes; +})(); }; + + // scan-io-refs-walk + var scanIoRefsWalk = function(node, ioNames, refs) { return (isSxTruthy((typeOf(node) == "symbol")) ? (function() { + var name = symbolName(node); + return (isSxTruthy(contains(ioNames, name)) ? (isSxTruthy(!isSxTruthy(contains(refs, name))) ? append_b(refs, name) : NIL) : NIL); +})() : (isSxTruthy((typeOf(node) == "list")) ? forEach(function(item) { return scanIoRefsWalk(item, ioNames, refs); }, node) : (isSxTruthy((typeOf(node) == "dict")) ? forEach(function(key) { return scanIoRefsWalk(dictGet(node, key), ioNames, refs); }, keys(node)) : NIL))); }; + + // scan-io-refs + var scanIoRefs = function(node, ioNames) { return (function() { + var refs = []; + scanIoRefsWalk(node, ioNames, refs); + return refs; +})(); }; + + // transitive-io-refs-walk + var transitiveIoRefsWalk = function(n, seen, allRefs, env, ioNames) { return (isSxTruthy(!isSxTruthy(contains(seen, n))) ? (append_b(seen, n), (function() { + var val = envGet(env, n); + return (isSxTruthy((typeOf(val) == "component")) ? (forEach(function(ref) { return (isSxTruthy(!isSxTruthy(contains(allRefs, ref))) ? append_b(allRefs, ref) : NIL); }, scanIoRefs(componentBody(val), ioNames)), forEach(function(dep) { return transitiveIoRefsWalk(dep, seen, allRefs, env, ioNames); }, scanRefs(componentBody(val)))) : (isSxTruthy((typeOf(val) == "macro")) ? (forEach(function(ref) { return (isSxTruthy(!isSxTruthy(contains(allRefs, ref))) ? append_b(allRefs, ref) : NIL); }, scanIoRefs(macroBody(val), ioNames)), forEach(function(dep) { return transitiveIoRefsWalk(dep, seen, allRefs, env, ioNames); }, scanRefs(macroBody(val)))) : NIL)); +})()) : NIL); }; + + // transitive-io-refs + var transitiveIoRefs = function(name, env, ioNames) { return (function() { + var allRefs = []; + var seen = []; + var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name))); + transitiveIoRefsWalk(key, seen, allRefs, env, ioNames); + return allRefs; +})(); }; + + // compute-all-io-refs + var computeAllIoRefs = function(env, ioNames) { return forEach(function(name) { return (function() { + var val = envGet(env, name); + return (isSxTruthy((typeOf(val) == "component")) ? componentSetIoRefs(val, transitiveIoRefs(name, env, ioNames)) : NIL); +})(); }, envComponents(env)); }; + + // component-io-refs-cached + var componentIoRefsCached = function(name, env, ioNames) { return (function() { + var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name))); + return (function() { + var val = envGet(env, key); + return (isSxTruthy((isSxTruthy((typeOf(val) == "component")) && isSxTruthy(!isSxTruthy(isNil(componentIoRefs(val)))) && !isSxTruthy(isEmpty(componentIoRefs(val))))) ? componentIoRefs(val) : transitiveIoRefs(name, env, ioNames)); +})(); +})(); }; + + // component-pure? + var componentPure_p = function(name, env, ioNames) { return (function() { + var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name))); + return (function() { + var val = envGet(env, key); + return (isSxTruthy((isSxTruthy((typeOf(val) == "component")) && !isSxTruthy(isNil(componentIoRefs(val))))) ? isEmpty(componentIoRefs(val)) : isEmpty(transitiveIoRefs(name, env, ioNames))); +})(); +})(); }; + + // render-target + var renderTarget = function(name, env, ioNames) { return (function() { + var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name))); + return (function() { + var val = envGet(env, key); + return (isSxTruthy(!isSxTruthy((typeOf(val) == "component"))) ? "server" : (function() { + var affinity = componentAffinity(val); + return (isSxTruthy((affinity == "server")) ? "server" : (isSxTruthy((affinity == "client")) ? "client" : (isSxTruthy(!isSxTruthy(componentPure_p(name, env, ioNames))) ? "server" : "client"))); +})()); +})(); +})(); }; + + // page-render-plan + var pageRenderPlan = function(pageSource, env, ioNames) { return (function() { + var needed = componentsNeeded(pageSource, env); + var compTargets = {}; + var serverList = []; + var clientList = []; + var ioDeps = []; + { var _c = needed; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; (function() { + var target = renderTarget(name, env, ioNames); + compTargets[name] = target; + return (isSxTruthy((target == "server")) ? (append_b(serverList, name), forEach(function(ioRef) { return (isSxTruthy(!isSxTruthy(contains(ioDeps, ioRef))) ? append_b(ioDeps, ioRef) : NIL); }, componentIoRefsCached(name, env, ioNames))) : append_b(clientList, name)); +})(); } } + return {"components": compTargets, "server": serverList, "client": clientList, "io-deps": ioDeps}; +})(); }; + + // env-components + var envComponents = function(env) { return filter(function(k) { return (function() { + var v = envGet(env, k); + return sxOr(isComponent(v), isMacro(v)); +})(); }, keys(env)); }; + + // === Transpiled from router (client-side route matching) === // split-path-segments @@ -4813,6 +5061,35 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { if (typeof domOuterHtml === "function") PRIMITIVES["dom-outer-html"] = domOuterHtml; if (typeof domInnerHtml === "function") PRIMITIVES["dom-inner-html"] = domInnerHtml; + // Expose deps module functions as primitives so runtime-evaluated SX code + // (e.g. test-deps.sx in browser) can call them + // Platform functions (from PLATFORM_DEPS_JS) + PRIMITIVES["component-deps"] = componentDeps; + PRIMITIVES["component-set-deps!"] = componentSetDeps; + PRIMITIVES["component-css-classes"] = componentCssClasses; + PRIMITIVES["env-components"] = envComponents; + PRIMITIVES["regex-find-all"] = regexFindAll; + PRIMITIVES["scan-css-classes"] = scanCssClasses; + // Transpiled functions (from deps.sx) + PRIMITIVES["scan-refs"] = scanRefs; + PRIMITIVES["scan-refs-walk"] = scanRefsWalk; + PRIMITIVES["transitive-deps"] = transitiveDeps; + PRIMITIVES["transitive-deps-walk"] = transitiveDepsWalk; + PRIMITIVES["compute-all-deps"] = computeAllDeps; + PRIMITIVES["scan-components-from-source"] = scanComponentsFromSource; + PRIMITIVES["components-needed"] = componentsNeeded; + PRIMITIVES["page-component-bundle"] = pageComponentBundle; + PRIMITIVES["page-css-classes"] = pageCssClasses; + PRIMITIVES["scan-io-refs-walk"] = scanIoRefsWalk; + PRIMITIVES["scan-io-refs"] = scanIoRefs; + PRIMITIVES["transitive-io-refs-walk"] = transitiveIoRefsWalk; + PRIMITIVES["transitive-io-refs"] = transitiveIoRefs; + PRIMITIVES["compute-all-io-refs"] = computeAllIoRefs; + PRIMITIVES["component-io-refs-cached"] = componentIoRefsCached; + PRIMITIVES["component-pure?"] = componentPure_p; + PRIMITIVES["render-target"] = renderTarget; + PRIMITIVES["page-render-plan"] = pageRenderPlan; + // ========================================================================= // Async IO: Promise-aware rendering for client-side IO primitives // ========================================================================= @@ -5535,6 +5812,17 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { hydrateIslands: typeof sxHydrateIslands === "function" ? sxHydrateIslands : null, disposeIsland: typeof disposeIsland === "function" ? disposeIsland : null, init: typeof bootInit === "function" ? bootInit : null, + scanRefs: scanRefs, + scanComponentsFromSource: scanComponentsFromSource, + transitiveDeps: transitiveDeps, + computeAllDeps: computeAllDeps, + componentsNeeded: componentsNeeded, + pageComponentBundle: pageComponentBundle, + pageCssClasses: pageCssClasses, + scanIoRefs: scanIoRefs, + transitiveIoRefs: transitiveIoRefs, + computeAllIoRefs: computeAllIoRefs, + componentPure_p: componentPure_p, splitPathSegments: splitPathSegments, parseRoutePattern: parseRoutePattern, matchRoute: matchRoute, diff --git a/shared/sx/ref/bootstrap_js.py b/shared/sx/ref/bootstrap_js.py index aae9f10..1af6eaa 100644 --- a/shared/sx/ref/bootstrap_js.py +++ b/shared/sx/ref/bootstrap_js.py @@ -2,12 +2,8 @@ """ Bootstrap compiler: reference SX evaluator → JavaScript. -Reads the .sx reference specification and emits a standalone JavaScript -evaluator (sx-browser.js) that runs in the browser. - -The compiler translates the restricted SX subset used in eval.sx/render.sx -into idiomatic JavaScript. Platform interface functions are emitted as -native JS implementations. +This is now a thin shim that delegates to run_js_sx.py (the self-hosting +bootstrapper). The hand-written JSEmitter has been replaced by js.sx. Usage: python bootstrap_js.py @@ -17,4367 +13,25 @@ from __future__ import annotations import os import sys -# Add project root to path for imports _HERE = os.path.dirname(os.path.abspath(__file__)) _PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", "..")) -sys.path.insert(0, _PROJECT) +if _PROJECT not in sys.path: + sys.path.insert(0, _PROJECT) + +# Re-export everything that consumers import from this module. +# Canonical source is now run_js_sx.py (self-hosting via js.sx) and platform_js.py. +from shared.sx.ref.run_js_sx import compile_ref_to_js, load_js_sx # noqa: F401 +from shared.sx.ref.platform_js import ( # noqa: F401 + extract_defines, + ADAPTER_FILES, ADAPTER_DEPS, SPEC_MODULES, EXTENSION_NAMES, + PREAMBLE, PLATFORM_JS_PRE, PLATFORM_JS_POST, + PRIMITIVES_JS_MODULES, _ALL_JS_MODULES, _assemble_primitives_js, + PLATFORM_DEPS_JS, PLATFORM_PARSER_JS, PLATFORM_DOM_JS, + PLATFORM_ENGINE_PURE_JS, PLATFORM_ORCHESTRATION_JS, PLATFORM_BOOT_JS, + CONTINUATIONS_JS, ASYNC_IO_JS, + fixups_js, public_api_js, EPILOGUE, +) -from shared.sx.parser import parse_all -from shared.sx.types import Symbol, Keyword, NIL as SX_NIL - -# --------------------------------------------------------------------------- -# SX → JavaScript transpiler -# --------------------------------------------------------------------------- - -# JS reserved words — SX parameter/variable names that collide get _ suffix -_JS_RESERVED = frozenset({ - "abstract", "arguments", "await", "boolean", "break", "byte", "case", - "catch", "char", "class", "const", "continue", "debugger", "default", - "delete", "do", "double", "else", "enum", "eval", "export", "extends", - "final", "finally", "float", "for", "function", "goto", "if", - "implements", "import", "in", "instanceof", "int", "interface", "let", - "long", "native", "new", "package", "private", "protected", "public", - "return", "short", "static", "super", "switch", "synchronized", "this", - "throw", "throws", "transient", "try", "typeof", "var", "void", - "volatile", "while", "with", "yield", -}) - - -class JSEmitter: - """Transpile an SX AST node to JavaScript source code.""" - - def __init__(self): - self.indent = 0 - - def emit(self, expr) -> str: - """Emit a JS expression from an SX AST node.""" - # Bool MUST be checked before int (bool is subclass of int in Python) - if isinstance(expr, bool): - return "true" if expr else "false" - if isinstance(expr, (int, float)): - return str(expr) - if isinstance(expr, str): - return self._js_string(expr) - if expr is None or expr is SX_NIL: - return "NIL" - if isinstance(expr, Symbol): - return self._emit_symbol(expr.name) - if isinstance(expr, Keyword): - return self._js_string(expr.name) - if isinstance(expr, dict): - return self._emit_native_dict(expr) - if isinstance(expr, list): - return self._emit_list(expr) - return str(expr) - - def emit_statement(self, expr) -> str: - """Emit a JS statement (with semicolon) from an SX AST node.""" - if isinstance(expr, list) and expr: - head = expr[0] - if isinstance(head, Symbol): - name = head.name - if name == "define": - return self._emit_define(expr) - if name == "set!": - return f"{self._mangle(expr[1].name)} = {self.emit(expr[2])};" - if name == "when": - return self._emit_when_stmt(expr) - if name == "do" or name == "begin": - return "\n".join(self.emit_statement(e) for e in expr[1:]) - if name == "for-each": - return self._emit_for_each_stmt(expr) - if name == "dict-set!": - return f"{self.emit(expr[1])}[{self.emit(expr[2])}] = {self.emit(expr[3])};" - if name == "append!": - return f"{self.emit(expr[1])}.push({self.emit(expr[2])});" - if name == "env-set!": - return f"{self.emit(expr[1])}[{self.emit(expr[2])}] = {self.emit(expr[3])};" - if name == "set-lambda-name!": - return f"{self.emit(expr[1])}.name = {self.emit(expr[2])};" - return f"{self.emit(expr)};" - - # --- Symbol emission --- - - def _emit_symbol(self, name: str) -> str: - # Map SX names to JS names - return self._mangle(name) - - # Explicit SX→JS name mappings. Auto-mangle (kebab→camelCase, ?→_p, !→_b) - # is only a fallback. Every platform symbol used in spec .sx files MUST have - # an entry here — relying on auto-mangle is fragile and has caused runtime - # errors (e.g. has-key? → hasKey_p instead of dictHas). - RENAMES = { - "nil": "NIL", - "true": "true", - "false": "false", - "nil?": "isNil", - "type-of": "typeOf", - "symbol-name": "symbolName", - "keyword-name": "keywordName", - "make-lambda": "makeLambda", - "make-component": "makeComponent", - "make-macro": "makeMacro", - "make-thunk": "makeThunk", - "make-symbol": "makeSymbol", - "make-keyword": "makeKeyword", - "lambda-params": "lambdaParams", - "lambda-body": "lambdaBody", - "lambda-closure": "lambdaClosure", - "lambda-name": "lambdaName", - "set-lambda-name!": "setLambdaName", - "component-params": "componentParams", - "component-body": "componentBody", - "component-closure": "componentClosure", - "component-has-children?": "componentHasChildren", - "component-name": "componentName", - "component-affinity": "componentAffinity", - "macro-params": "macroParams", - "macro-rest-param": "macroRestParam", - "macro-body": "macroBody", - "macro-closure": "macroClosure", - "thunk?": "isThunk", - "thunk-expr": "thunkExpr", - "thunk-env": "thunkEnv", - "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", - "*store-registry*": "_storeRegistry", - "def-store": "defStore", - "use-store": "useStore", - "clear-stores": "clearStores", - "emit-event": "emitEvent", - "on-event": "onEvent", - "bridge-event": "bridgeEvent", - "macro?": "isMacro", - "primitive?": "isPrimitive", - "get-primitive": "getPrimitive", - "env-has?": "envHas", - "env-get": "envGet", - "env-set!": "envSet", - "env-extend": "envExtend", - "env-merge": "envMerge", - "dict-set!": "dictSet", - "dict-get": "dictGet", - "eval-expr": "evalExpr", - "eval-list": "evalList", - "eval-call": "evalCall", - "is-render-expr?": "isRenderExpr", - "render-expr": "renderExpr", - "render-active?": "renderActiveP", - "set-render-active!": "setRenderActiveB", - "call-lambda": "callLambda", - "call-component": "callComponent", - "parse-keyword-args": "parseKeywordArgs", - "parse-comp-params": "parseCompParams", - "parse-macro-params": "parseMacroParams", - "expand-macro": "expandMacro", - "render-to-html": "renderToHtml", - "render-to-sx": "renderToSx", - "render-value-to-html": "renderValueToHtml", - "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", - "aser-fragment": "aserFragment", - "aser-call": "aserCall", - "aser-special": "aserSpecial", - "eval-case-aser": "evalCaseAser", - "sx-serialize": "sxSerialize", - "sx-serialize-dict": "sxSerializeDict", - "sx-expr-source": "sxExprSource", - "sf-if": "sfIf", - "sf-when": "sfWhen", - "sf-cond": "sfCond", - "sf-cond-scheme": "sfCondScheme", - "sf-cond-clojure": "sfCondClojure", - "sf-case": "sfCase", - "sf-case-loop": "sfCaseLoop", - "sf-and": "sfAnd", - "sf-or": "sfOr", - "sf-let": "sfLet", - "sf-named-let": "sfNamedLet", - "sf-letrec": "sfLetrec", - "sf-dynamic-wind": "sfDynamicWind", - "push-wind!": "pushWind", - "pop-wind!": "popWind", - "call-thunk": "callThunk", - "sf-lambda": "sfLambda", - "sf-define": "sfDefine", - "sf-defcomp": "sfDefcomp", - "sf-defisland": "sfDefisland", - "defcomp-kwarg": "defcompKwarg", - "sf-defmacro": "sfDefmacro", - "sf-begin": "sfBegin", - "sf-quote": "sfQuote", - "sf-quasiquote": "sfQuasiquote", - "sf-thread-first": "sfThreadFirst", - "sf-set!": "sfSetBang", - "qq-expand": "qqExpand", - "ho-map": "hoMap", - "ho-map-indexed": "hoMapIndexed", - "ho-filter": "hoFilter", - "ho-reduce": "hoReduce", - "ho-some": "hoSome", - "ho-every": "hoEvery", - "ho-for-each": "hoForEach", - "sf-defstyle": "sfDefstyle", - "kf-name": "kfName", - "special-form?": "isSpecialForm", - "ho-form?": "isHoForm", - "strip-prefix": "stripPrefix", - "escape-html": "escapeHtml", - "escape-attr": "escapeAttr", - "escape-string": "escapeString", - "raw-html-content": "rawHtmlContent", - "HTML_TAGS": "HTML_TAGS", - "VOID_ELEMENTS": "VOID_ELEMENTS", - "BOOLEAN_ATTRS": "BOOLEAN_ATTRS", - # render.sx core - "definition-form?": "isDefinitionForm", - # adapter-html.sx - "RENDER_HTML_FORMS": "RENDER_HTML_FORMS", - "render-html-form?": "isRenderHtmlForm", - "dispatch-html-form": "dispatchHtmlForm", - "render-lambda-html": "renderLambdaHtml", - "make-raw-html": "makeRawHtml", - # adapter-dom.sx - "SVG_NS": "SVG_NS", - "MATH_NS": "MATH_NS", - "render-to-dom": "renderToDom", - "render-dom-list": "renderDomList", - "render-dom-element": "renderDomElement", - "render-dom-component": "renderDomComponent", - "render-dom-fragment": "renderDomFragment", - "render-dom-raw": "renderDomRaw", - "render-dom-unknown-component": "renderDomUnknownComponent", - "RENDER_DOM_FORMS": "RENDER_DOM_FORMS", - "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", - "dom-get-attr": "domGetAttr", - "dom-remove-attr": "domRemoveAttr", - "dom-has-attr?": "domHasAttr", - "dom-parse-html": "domParseHtml", - "dom-clone": "domClone", - "create-text-node": "createTextNode", - "create-fragment": "createFragment", - "dom-parent": "domParent", - "dom-id": "domId", - "dom-node-type": "domNodeType", - "dom-node-name": "domNodeName", - "dom-text-content": "domTextContent", - "dom-set-text-content": "domSetTextContent", - "dom-is-fragment?": "domIsFragment", - "dom-is-child-of?": "domIsChildOf", - "dom-is-active-element?": "domIsActiveElement", - "dom-is-input-element?": "domIsInputElement", - "dom-first-child": "domFirstChild", - "dom-next-sibling": "domNextSibling", - "dom-child-list": "domChildList", - "dom-attr-list": "domAttrList", - "dom-insert-before": "domInsertBefore", - "dom-insert-after": "domInsertAfter", - "dom-prepend": "domPrepend", - "dom-remove-child": "domRemoveChild", - "dom-replace-child": "domReplaceChild", - "dom-set-inner-html": "domSetInnerHtml", - "dom-insert-adjacent-html": "domInsertAdjacentHtml", - "dom-get-style": "domGetStyle", - "dom-set-style": "domSetStyle", - "dom-get-prop": "domGetProp", - "dom-set-prop": "domSetProp", - "dom-add-class": "domAddClass", - "dom-remove-class": "domRemoveClass", - "dom-dispatch": "domDispatch", - "dom-listen": "domListen", - "event-detail": "eventDetail", - "dom-query": "domQuery", - "dom-ensure-element": "domEnsureElement", - "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", - "dom-get-data": "domGetData", - "dom-inner-html": "domInnerHtml", - "json-parse": "jsonParse", - "dict-has?": "dictHas", - "has-key?": "dictHas", - "dict-delete!": "dictDelete", - "process-bindings": "processBindings", - "eval-cond": "evalCond", - "eval-cond-scheme": "evalCondScheme", - "eval-cond-clojure": "evalCondClojure", - "for-each-indexed": "forEachIndexed", - "index-of": "indexOf_", - "component-has-children?": "componentHasChildren", - "component-affinity": "componentAffinity", - # engine.sx - "ENGINE_VERBS": "ENGINE_VERBS", - "DEFAULT_SWAP": "DEFAULT_SWAP", - "parse-time": "parseTime", - "parse-trigger-spec": "parseTriggerSpec", - "default-trigger": "defaultTrigger", - "get-verb-info": "getVerbInfo", - "build-request-headers": "buildRequestHeaders", - "process-response-headers": "processResponseHeaders", - "parse-swap-spec": "parseSwapSpec", - "parse-retry-spec": "parseRetrySpec", - "next-retry-ms": "nextRetryMs", - "filter-params": "filterParams", - "resolve-target": "resolveTarget", - "apply-optimistic": "applyOptimistic", - "revert-optimistic": "revertOptimistic", - "find-oob-swaps": "findOobSwaps", - "morph-node": "morphNode", - "sync-attrs": "syncAttrs", - "morph-children": "morphChildren", - "swap-dom-nodes": "swapDomNodes", - "insert-remaining-siblings": "insertRemainingSiblings", - "swap-html-string": "swapHtmlString", - "handle-history": "handleHistory", - "PRELOAD_TTL": "PRELOAD_TTL", - "preload-cache-get": "preloadCacheGet", - "preload-cache-set": "preloadCacheSet", - "classify-trigger": "classifyTrigger", - "should-boost-link?": "shouldBoostLink", - "should-boost-form?": "shouldBoostForm", - "parse-sse-swap": "parseSseSwap", - # engine.sx orchestration - "_preload-cache": "_preloadCache", - "_css-hash": "_cssHash", - "dispatch-trigger-events": "dispatchTriggerEvents", - "init-css-tracking": "initCssTracking", - "execute-request": "executeRequest", - "do-fetch": "doFetch", - "handle-fetch-success": "handleFetchSuccess", - "handle-sx-response": "handleSxResponse", - "handle-html-response": "handleHtmlResponse", - "handle-retry": "handleRetry", - "bind-triggers": "bindTriggers", - "bind-event": "bindEvent", - "post-swap": "postSwap", - "activate-scripts": "activateScripts", - "process-oob-swaps": "processOobSwaps", - "hoist-head-elements": "hoistHeadElements", - "process-boosted": "processBoosted", - "boost-descendants": "boostDescendants", - "process-sse": "processSse", - "bind-sse": "bindSse", - "bind-sse-swap": "bindSseSwap", - "bind-inline-handlers": "bindInlineHandlers", - "process-emit-elements": "processEmitElements", - "bind-preload-for": "bindPreloadFor", - "do-preload": "doPreload", - "VERB_SELECTOR": "VERB_SELECTOR", - "process-elements": "processElements", - "process-one": "processOne", - "handle-popstate": "handlePopstate", - "engine-init": "engineInit", - # engine orchestration platform - "promise-resolve": "promiseResolve", - "promise-then": "promiseThen", - "promise-catch": "promiseCatch", - "promise-delayed": "promiseDelayed", - "abort-previous": "abortPrevious", - "track-controller": "trackController", - "abort-previous-target": "abortPreviousTarget", - "track-controller-target": "trackControllerTarget", - "new-abort-controller": "newAbortController", - "controller-signal": "controllerSignal", - "abort-error?": "isAbortError", - "set-timeout": "setTimeout_", - "set-interval": "setInterval_", - "clear-timeout": "clearTimeout_", - "clear-interval": "clearInterval_", - "request-animation-frame": "requestAnimationFrame_", - "csrf-token": "csrfToken", - "cross-origin?": "isCrossOrigin", - "loaded-component-names": "loadedComponentNames", - "build-request-body": "buildRequestBody", - "show-indicator": "showIndicator", - "disable-elements": "disableElements", - "clear-loading-state": "clearLoadingState", - "fetch-request": "fetchRequest", - "fetch-location": "fetchLocation", - "fetch-and-restore": "fetchAndRestore", - "fetch-streaming": "fetchStreaming", - "fetch-preload": "fetchPreload", - "dom-query-by-id": "domQueryById", - "dom-matches?": "domMatches", - "dom-closest": "domClosest", - "dom-body": "domBody", - "dom-has-class?": "domHasClass", - "dom-append-to-head": "domAppendToHead", - "dom-parse-html-document": "domParseHtmlDocument", - "dom-outer-html": "domOuterHtml", - "dom-body-inner-html": "domBodyInnerHtml", - "prevent-default": "preventDefault_", - "stop-propagation": "stopPropagation_", - "dom-focus": "domFocus", - "try-catch": "tryCatch", - "error-message": "errorMessage", - "schedule-idle": "scheduleIdle", - "element-value": "elementValue", - "validate-for-request": "validateForRequest", - "with-transition": "withTransition", - "observe-intersection": "observeIntersection", - "event-source-connect": "eventSourceConnect", - "event-source-listen": "eventSourceListen", - "bind-boost-link": "bindBoostLink", - "bind-boost-form": "bindBoostForm", - "bind-client-route-link": "bindClientRouteLink", - "bind-client-route-click": "bindClientRouteClick", - "try-client-route": "tryClientRoute", - "try-eval-content": "tryEvalContent", - "try-async-eval-content": "tryAsyncEvalContent", - "register-io-deps": "registerIoDeps", - "url-pathname": "urlPathname", - "bind-preload": "bindPreload", - "mark-processed!": "markProcessed", - "is-processed?": "isProcessed", - "create-script-clone": "createScriptClone", - "sx-render": "sxRender", - "sx-process-scripts": "sxProcessScripts", - "sx-hydrate": "sxHydrate", - "strip-component-scripts": "stripComponentScripts", - "extract-response-css": "extractResponseCss", - "select-from-container": "selectFromContainer", - "children-to-fragment": "childrenToFragment", - "select-html-from-doc": "selectHtmlFromDoc", - "try-parse-json": "tryParseJson", - "process-css-response": "processCssResponse", - "browser-location-href": "browserLocationHref", - "browser-same-origin?": "browserSameOrigin", - "browser-push-state": "browserPushState", - "browser-replace-state": "browserReplaceState", - "browser-navigate": "browserNavigate", - "browser-reload": "browserReload", - "browser-scroll-to": "browserScrollTo", - "browser-media-matches?": "browserMediaMatches", - "browser-confirm": "browserConfirm", - "browser-prompt": "browserPrompt", - "now-ms": "nowMs", - "parse-header-value": "parseHeaderValue", - "replace": "replace_", - "whitespace?": "isWhitespace", - "digit?": "isDigit", - "ident-start?": "isIdentStart", - "ident-char?": "isIdentChar", - "parse-number": "parseNumber", - "sx-expr-source": "sxExprSource", - "starts-with?": "startsWith", - "ends-with?": "endsWith", - "contains?": "contains", - "empty?": "isEmpty", - "odd?": "isOdd", - "even?": "isEven", - "zero?": "isZero", - "number?": "isNumber", - "string?": "isString", - "list?": "isList", - "dict?": "isDict", - "every?": "isEvery", - "map-indexed": "mapIndexed", - "for-each": "forEach", - "map-dict": "mapDict", - "chunk-every": "chunkEvery", - "zip-pairs": "zipPairs", - "strip-tags": "stripTags", - "format-date": "formatDate", - "format-decimal": "formatDecimal", - "parse-int": "parseInt_", - # boot.sx - "HEAD_HOIST_SELECTOR": "HEAD_HOIST_SELECTOR", - "hoist-head-elements-full": "hoistHeadElementsFull", - "sx-mount": "sxMount", - "sx-hydrate-elements": "sxHydrateElements", - "sx-update-element": "sxUpdateElement", - "sx-render-component": "sxRenderComponent", - "process-sx-scripts": "processSxScripts", - "process-component-script": "processComponentScript", - "SX_VERSION": "SX_VERSION", - "boot-init": "bootInit", - "sx-hydrate-islands": "sxHydrateIslands", - "hydrate-island": "hydrateIsland", - "dispose-island": "disposeIsland", - "resolve-suspense": "resolveSuspense", - "resolve-mount-target": "resolveMountTarget", - "sx-render-with-env": "sxRenderWithEnv", - "get-render-env": "getRenderEnv", - "merge-envs": "mergeEnvs", - "sx-load-components": "sxLoadComponents", - "set-document-title": "setDocumentTitle", - "remove-head-element": "removeHeadElement", - "query-sx-scripts": "querySxScripts", - "local-storage-get": "localStorageGet", - "local-storage-set": "localStorageSet", - "local-storage-remove": "localStorageRemove", - "set-sx-comp-cookie": "setSxCompCookie", - "clear-sx-comp-cookie": "clearSxCompCookie", - "parse-env-attr": "parseEnvAttr", - "store-env-attr": "storeEnvAttr", - "to-kebab": "toKebab", - "log-info": "logInfo", - "log-warn": "logWarn", - "log-parse-error": "logParseError", - "_page-routes": "_pageRoutes", - "process-page-scripts": "processPageScripts", - "query-page-scripts": "queryPageScripts", - # deps.sx - "scan-refs": "scanRefs", - "scan-refs-walk": "scanRefsWalk", - "transitive-deps": "transitiveDeps", - "compute-all-deps": "computeAllDeps", - "scan-components-from-source": "scanComponentsFromSource", - "components-needed": "componentsNeeded", - "page-component-bundle": "pageComponentBundle", - "page-css-classes": "pageCssClasses", - "component-deps": "componentDeps", - "component-set-deps!": "componentSetDeps", - "component-css-classes": "componentCssClasses", - "component-io-refs": "componentIoRefs", - "component-set-io-refs!": "componentSetIoRefs", - "env-components": "envComponents", - "regex-find-all": "regexFindAll", - "scan-css-classes": "scanCssClasses", - # deps.sx IO detection - "scan-io-refs": "scanIoRefs", - "scan-io-refs-walk": "scanIoRefsWalk", - "transitive-io-refs": "transitiveIoRefs", - "compute-all-io-refs": "computeAllIoRefs", - "component-io-refs-cached": "componentIoRefsCached", - "component-pure?": "componentPure_p", - "render-target": "renderTarget", - "page-render-plan": "pageRenderPlan", - # router.sx - "split-path-segments": "splitPathSegments", - "make-route-segment": "makeRouteSegment", - "parse-route-pattern": "parseRoutePattern", - "match-route-segments": "matchRouteSegments", - "match-route": "matchRoute", - "find-matching-route": "findMatchingRoute", - "for-each-indexed": "forEachIndexed", - } - - def _mangle(self, name: str) -> str: - """Convert SX identifier to valid JS identifier.""" - if name in self.RENAMES: - return self.RENAMES[name] - # General mangling: replace - with camelCase, ? with _p, ! with _b - result = name - if result.endswith("?"): - result = result[:-1] + "_p" - if result.endswith("!"): - result = result[:-1] + "_b" - # Kebab to camel - parts = result.split("-") - if len(parts) > 1: - result = parts[0] + "".join(p.capitalize() for p in parts[1:]) - # Escape JS reserved words - if result in _JS_RESERVED: - result = result + "_" - return result - - # --- List emission --- - - def _emit_list(self, expr: list) -> str: - if not expr: - return "[]" - head = expr[0] - if not isinstance(head, Symbol): - # Data list - return "[" + ", ".join(self.emit(x) for x in expr) + "]" - name = head.name - handler = getattr(self, f"_sf_{name.replace('-', '_').replace('!', '_b').replace('?', '_p')}", None) - if handler: - return handler(expr) - # Built-in forms - if name == "fn" or name == "lambda": - return self._emit_fn(expr) - if name == "let" or name == "let*": - return self._emit_let(expr) - if name == "if": - return self._emit_if(expr) - if name == "when": - return self._emit_when(expr) - if name == "cond": - return self._emit_cond(expr) - if name == "case": - return self._emit_case(expr) - if name == "and": - return self._emit_and(expr) - if name == "or": - return self._emit_or(expr) - if name == "not": - return f"!isSxTruthy({self.emit(expr[1])})" - if name == "do" or name == "begin": - return self._emit_do(expr) - if name == "list": - return "[" + ", ".join(self.emit(x) for x in expr[1:]) + "]" - if name == "dict": - return self._emit_dict_literal(expr) - if name == "quote": - return self._emit_quote(expr[1]) - if name == "set!": - return f"({self._mangle(expr[1].name)} = {self.emit(expr[2])})" - if name == "str": - parts = [self.emit(x) for x in expr[1:]] - return "(" + " + ".join(f'String({p})' for p in parts) + ")" - # Infix operators - if name in ("+", "-", "*", "/", "=", "!=", "<", ">", "<=", ">=", "mod"): - return self._emit_infix(name, expr[1:]) - if name == "inc": - return f"({self.emit(expr[1])} + 1)" - if name == "dec": - return f"({self.emit(expr[1])} - 1)" - - # Regular function call - fn_name = self._mangle(name) - args = ", ".join(self.emit(x) for x in expr[1:]) - return f"{fn_name}({args})" - - # --- Special form emitters --- - - def _emit_fn(self, expr) -> str: - params = expr[1] - body = expr[2:] - param_names = [] - 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])};") - inner = "\n".join(parts) - return f"function({params_str}) {{ {inner} }}" - - def _emit_let(self, expr) -> str: - bindings = expr[1] - body = expr[2:] - parts = ["(function() {"] - if isinstance(bindings, list): - if bindings and isinstance(bindings[0], list): - # Scheme-style: ((name val) ...) - for b in bindings: - vname = b[0].name if isinstance(b[0], Symbol) else str(b[0]) - parts.append(f" var {self._mangle(vname)} = {self.emit(b[1])};") - else: - # Clojure-style: (name val name val ...) - for i in range(0, len(bindings), 2): - vname = bindings[i].name if isinstance(bindings[i], Symbol) else str(bindings[i]) - parts.append(f" var {self._mangle(vname)} = {self.emit(bindings[i + 1])};") - for b_expr in body[:-1]: - parts.append(f" {self.emit_statement(b_expr)}") - parts.append(f" return {self.emit(body[-1])};") - parts.append("})()") - return "\n".join(parts) - - def _emit_if(self, expr) -> str: - cond = self.emit(expr[1]) - then = self.emit(expr[2]) - els = self.emit(expr[3]) if len(expr) > 3 else "NIL" - return f"(isSxTruthy({cond}) ? {then} : {els})" - - def _emit_when(self, expr) -> str: - cond = self.emit(expr[1]) - body_parts = expr[2:] - if len(body_parts) == 1: - return f"(isSxTruthy({cond}) ? {self.emit(body_parts[0])} : NIL)" - body = self._emit_do_inner(body_parts) - return f"(isSxTruthy({cond}) ? {body} : NIL)" - - def _emit_when_stmt(self, expr) -> str: - cond = self.emit(expr[1]) - body_parts = expr[2:] - stmts = "\n".join(f" {self.emit_statement(e)}" for e in body_parts) - return f"if (isSxTruthy({cond})) {{\n{stmts}\n}}" - - def _emit_cond(self, expr) -> str: - clauses = expr[1:] - if not clauses: - return "NIL" - # Determine style ONCE: Scheme-style if every element is a 2-element - # list AND no bare keywords appear (bare :else = Clojure). - is_scheme = ( - all(isinstance(c, list) and len(c) == 2 for c in clauses) - and not any(isinstance(c, Keyword) for c in clauses) - ) - if is_scheme: - return self._cond_scheme(clauses) - return self._cond_clojure(clauses) - - def _cond_scheme(self, clauses) -> str: - if not clauses: - return "NIL" - clause = clauses[0] - test = clause[0] - body = clause[1] - if isinstance(test, Symbol) and test.name in ("else", ":else"): - return self.emit(body) - if isinstance(test, Keyword) and test.name == "else": - return self.emit(body) - return f"(isSxTruthy({self.emit(test)}) ? {self.emit(body)} : {self._cond_scheme(clauses[1:])})" - - def _cond_clojure(self, clauses) -> str: - if len(clauses) < 2: - return "NIL" - test = clauses[0] - body = clauses[1] - if isinstance(test, Keyword) and test.name == "else": - return self.emit(body) - if isinstance(test, Symbol) and test.name in ("else", ":else"): - return self.emit(body) - return f"(isSxTruthy({self.emit(test)}) ? {self.emit(body)} : {self._cond_clojure(clauses[2:])})" - - def _emit_case(self, expr) -> str: - match_expr = self.emit(expr[1]) - clauses = expr[2:] - return f"(function() {{ var _m = {match_expr}; {self._case_chain(clauses)} }})()" - - def _case_chain(self, clauses) -> str: - if len(clauses) < 2: - return "return NIL;" - test = clauses[0] - body = clauses[1] - if isinstance(test, Keyword) and test.name == "else": - return f"return {self.emit(body)};" - if isinstance(test, Symbol) and test.name in ("else", ":else"): - return f"return {self.emit(body)};" - return f"if (_m == {self.emit(test)}) return {self.emit(body)}; {self._case_chain(clauses[2:])}" - - def _emit_and(self, expr) -> str: - parts = [self.emit(x) for x in expr[1:]] - return "(" + " && ".join(f"isSxTruthy({p})" for p in parts[:-1]) + (" && " if len(parts) > 1 else "") + parts[-1] + ")" - - def _emit_or(self, expr) -> str: - if len(expr) == 2: - return self.emit(expr[1]) - parts = [self.emit(x) for x in expr[1:]] - # Use a helper that returns the first truthy value - return f"sxOr({', '.join(parts)})" - - def _emit_do(self, expr) -> str: - return self._emit_do_inner(expr[1:]) - - def _emit_do_inner(self, exprs) -> str: - if len(exprs) == 1: - return self.emit(exprs[0]) - parts = [self.emit(e) for e in exprs] - return "(" + ", ".join(parts) + ")" - - def _emit_native_dict(self, expr: dict) -> str: - """Emit a native Python dict (from parser's {:key val} syntax).""" - parts = [] - for key, val in expr.items(): - parts.append(f"{self._js_string(key)}: {self.emit(val)}") - return "{" + ", ".join(parts) + "}" - - def _emit_dict_literal(self, expr) -> str: - pairs = expr[1:] - parts = [] - i = 0 - while i < len(pairs) - 1: - key = pairs[i] - val = pairs[i + 1] - if isinstance(key, Keyword): - parts.append(f"{self._js_string(key.name)}: {self.emit(val)}") - else: - parts.append(f"[{self.emit(key)}]: {self.emit(val)}") - i += 2 - return "{" + ", ".join(parts) + "}" - - def _emit_infix(self, op: str, args: list) -> str: - JS_OPS = {"=": "==", "!=": "!=", "mod": "%"} - js_op = JS_OPS.get(op, op) - if len(args) == 1 and op == "-": - return f"(-{self.emit(args[0])})" - return f"({self.emit(args[0])} {js_op} {self.emit(args[1])})" - - def _emit_define(self, expr) -> str: - name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1]) - # Detect zero-arg self-tail-recursive functions and emit as while loops - fn_expr = expr[2] if len(expr) > 2 else None - if (fn_expr and isinstance(fn_expr, list) and fn_expr - and isinstance(fn_expr[0], Symbol) and fn_expr[0].name in ("fn", "lambda") - and isinstance(fn_expr[1], list) and len(fn_expr[1]) == 0 - and self._is_self_tail_recursive(name, fn_expr[2:])): - body = fn_expr[2:] - loop_body = self._emit_loop_body(name, body) - return f"var {self._mangle(name)} = function() {{ while(true) {{ {loop_body} }} }};" - val = self.emit(fn_expr) if fn_expr is not None else "NIL" - return f"var {self._mangle(name)} = {val};" - - def _is_self_tail_recursive(self, name: str, body: list) -> bool: - """Check if a function body contains tail calls to itself.""" - if not body: - return False - last = body[-1] - return self._has_tail_call(name, last) - - def _has_tail_call(self, name: str, expr) -> bool: - """Check if expr has a tail call to name in any branch.""" - if not isinstance(expr, list) or not expr: - return False - head = expr[0] - if not isinstance(head, Symbol): - return False - h = head.name - # Direct tail call - if h == name: - return True - # Branching forms — check if any branch tail-calls - if h == "if": - return (self._has_tail_call(name, expr[2]) - or (len(expr) > 3 and self._has_tail_call(name, expr[3]))) - if h == "when": - return any(self._has_tail_call(name, e) for e in expr[2:]) - if h == "cond": - for clause in expr[1:]: - if isinstance(clause, list) and len(clause) == 2: - if self._has_tail_call(name, clause[1]): - return True - elif isinstance(clause, Keyword): - continue - elif isinstance(clause, list): - if self._has_tail_call(name, clause): - return True - else: - if self._has_tail_call(name, clause): - return True - return False - if h in ("do", "begin"): - return self._has_tail_call(name, expr[-1]) if len(expr) > 1 else False - if h == "let" or h == "let*": - return self._has_tail_call(name, expr[-1]) if len(expr) > 2 else False - return False - - def _emit_loop_body(self, name: str, body: list) -> str: - """Emit a function body as while-loop statements. - - Replaces tail-self-calls with `continue` and non-recursive exits with - `return`. - """ - if not body: - return "return NIL;" - # Emit side-effect statements first, then the tail expression as loop logic - parts = [] - for b in body[:-1]: - parts.append(self.emit_statement(b)) - parts.append(self._emit_tail_as_stmt(name, body[-1])) - return "\n".join(parts) - - def _emit_tail_as_stmt(self, name: str, expr) -> str: - """Emit an expression in tail position as loop statements. - - Tail-self-calls → continue; other exits → return expr; - """ - if not isinstance(expr, list) or not expr: - return f"return {self.emit(expr)};" - - head = expr[0] - if not isinstance(head, Symbol): - return f"return {self.emit(expr)};" - - h = head.name - - # Direct tail call to self → continue - if h == name: - return "continue;" - - # (do stmt1 stmt2 ... tail) → emit stmts then recurse on tail - if h in ("do", "begin"): - stmts = [] - for e in expr[1:-1]: - stmts.append(self.emit_statement(e)) - stmts.append(self._emit_tail_as_stmt(name, expr[-1])) - return "\n".join(stmts) - - # (if cond then else) → if/else with tail handling in each branch - if h == "if": - cond = self.emit(expr[1]) - then_branch = self._emit_tail_as_stmt(name, expr[2]) - else_branch = self._emit_tail_as_stmt(name, expr[3]) if len(expr) > 3 else "return NIL;" - return f"if (isSxTruthy({cond})) {{ {then_branch} }} else {{ {else_branch} }}" - - # (when cond body...) → if (cond) { body... } else { return NIL; } - if h == "when": - cond = self.emit(expr[1]) - body_parts = expr[2:] - if not body_parts: - return f"if (isSxTruthy({cond})) {{}} else {{ return NIL; }}" - stmts = [] - for e in body_parts[:-1]: - stmts.append(self.emit_statement(e)) - stmts.append(self._emit_tail_as_stmt(name, body_parts[-1])) - inner = "\n".join(stmts) - return f"if (isSxTruthy({cond})) {{ {inner} }} else {{ return NIL; }}" - - # (cond clause1 clause2 ...) → if/else if/else chain - if h == "cond": - return self._emit_cond_as_loop_stmt(name, expr[1:]) - - # (let ((bindings)) body...) → { var ...; tail } - if h in ("let", "let*"): - bindings = expr[1] - body = expr[2:] - parts = [] - if isinstance(bindings, list): - if bindings and isinstance(bindings[0], list): - for b in bindings: - vname = b[0].name if isinstance(b[0], Symbol) else str(b[0]) - parts.append(f"var {self._mangle(vname)} = {self.emit(b[1])};") - else: - for i in range(0, len(bindings), 2): - vname = bindings[i].name if isinstance(bindings[i], Symbol) else str(bindings[i]) - parts.append(f"var {self._mangle(vname)} = {self.emit(bindings[i + 1])};") - for b_expr in body[:-1]: - parts.append(self.emit_statement(b_expr)) - parts.append(self._emit_tail_as_stmt(name, body[-1])) - inner = "\n".join(parts) - return f"{{ {inner} }}" - - # Not a tail call to self — regular return - return f"return {self.emit(expr)};" - - def _emit_cond_as_loop_stmt(self, name: str, clauses) -> str: - """Emit cond clauses as if/else if/else for loop body.""" - if not clauses: - return "return NIL;" - - # Detect style: Scheme vs Clojure (same as _emit_cond) - is_scheme = ( - all(isinstance(c, list) and len(c) == 2 for c in clauses) - and not any(isinstance(c, Keyword) for c in clauses) - ) - if is_scheme: - return self._cond_scheme_loop(name, clauses) - return self._cond_clojure_loop(name, clauses) - - def _cond_scheme_loop(self, name: str, clauses) -> str: - parts = [] - for i, clause in enumerate(clauses): - cond_expr = clause[0] - body_expr = clause[1] - # Check for :else / else - is_else = (isinstance(cond_expr, Keyword) and cond_expr.name == "else") or \ - (isinstance(cond_expr, Symbol) and cond_expr.name == "else") or \ - (isinstance(cond_expr, bool) and cond_expr is True) - if is_else: - parts.append(f"{{ {self._emit_tail_as_stmt(name, body_expr)} }}") - break - prefix = "if" if i == 0 else "else if" - cond = self.emit(cond_expr) - body = self._emit_tail_as_stmt(name, body_expr) - parts.append(f"{prefix} (isSxTruthy({cond})) {{ {body} }}") - else: - parts.append("else { return NIL; }") - return " ".join(parts) - - def _cond_clojure_loop(self, name: str, clauses) -> str: - parts = [] - i = 0 - clause_idx = 0 - has_else = False - while i < len(clauses): - c = clauses[i] - if isinstance(c, Keyword) and c.name == "else": - if i + 1 < len(clauses): - parts.append(f"else {{ {self._emit_tail_as_stmt(name, clauses[i + 1])} }}") - has_else = True - break - if i + 1 < len(clauses): - prefix = "if" if clause_idx == 0 else "else if" - cond = self.emit(c) - body = self._emit_tail_as_stmt(name, clauses[i + 1]) - parts.append(f"{prefix} (isSxTruthy({cond})) {{ {body} }}") - i += 2 - else: - parts.append(f"else {{ {self._emit_tail_as_stmt(name, c)} }}") - has_else = True - i += 1 - clause_idx += 1 - if not has_else: - parts.append("else { return NIL; }") - return " ".join(parts) - - def _emit_for_each_stmt(self, expr) -> str: - fn_expr = expr[1] - coll_expr = expr[2] - coll = self.emit(coll_expr) - # If fn is an inline lambda, emit a for loop - if isinstance(fn_expr, list) and fn_expr[0] == Symbol("fn"): - params = fn_expr[1] - body = fn_expr[2:] - p = params[0].name if isinstance(params[0], Symbol) else str(params[0]) - p_js = self._mangle(p) - body_js = "\n".join(self.emit_statement(b) for b in body) - return f"{{ var _c = {coll}; for (var _i = 0; _i < _c.length; _i++) {{ var {p_js} = _c[_i]; {body_js} }} }}" - fn = self.emit(fn_expr) - return f"{{ var _c = {coll}; for (var _i = 0; _i < _c.length; _i++) {{ {fn}(_c[_i]); }} }}" - - def _emit_quote(self, expr) -> str: - """Emit a quoted expression as a JS literal AST.""" - if isinstance(expr, bool): - return "true" if expr else "false" - if isinstance(expr, (int, float)): - return str(expr) - if isinstance(expr, str): - return self._js_string(expr) - if expr is None or expr is SX_NIL: - return "NIL" - if isinstance(expr, Symbol): - return f'new Symbol({self._js_string(expr.name)})' - if isinstance(expr, Keyword): - return f'new Keyword({self._js_string(expr.name)})' - if isinstance(expr, list): - return "[" + ", ".join(self._emit_quote(x) for x in expr) + "]" - return str(expr) - - def _js_string(self, s: str) -> str: - return '"' + s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t").replace("\0", "\\0") + '"' - - -# --------------------------------------------------------------------------- -# Bootstrap compiler -# --------------------------------------------------------------------------- - -def extract_defines(source: str) -> list[tuple[str, list]]: - """Parse .sx source, return list of (name, define-expr) for top-level defines.""" - exprs = parse_all(source) - defines = [] - for expr in exprs: - if isinstance(expr, list) and expr and isinstance(expr[0], Symbol): - if expr[0].name == "define": - name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1]) - defines.append((name, expr)) - return defines - - -ADAPTER_FILES = { - "parser": ("parser.sx", "parser"), - "html": ("adapter-html.sx", "adapter-html"), - "sx": ("adapter-sx.sx", "adapter-sx"), - "dom": ("adapter-dom.sx", "adapter-dom"), - "engine": ("engine.sx", "engine"), - "orchestration": ("orchestration.sx","orchestration"), - "boot": ("boot.sx", "boot"), -} - -# Dependencies -ADAPTER_DEPS = { - "engine": ["dom"], - "orchestration": ["engine", "dom"], - "boot": ["dom", "engine", "orchestration", "parser"], - "parser": [], -} - -SPEC_MODULES = { - "deps": ("deps.sx", "deps (component dependency analysis)"), - "router": ("router.sx", "router (client-side route matching)"), - "signals": ("signals.sx", "signals (reactive signal runtime)"), -} - - -EXTENSION_NAMES = {"continuations"} - -CONTINUATIONS_JS = ''' - // ========================================================================= - // Extension: Delimited continuations (shift/reset) - // ========================================================================= - - function Continuation(fn) { this.fn = fn; } - Continuation.prototype._continuation = true; - Continuation.prototype.call = function(value) { return this.fn(value !== undefined ? value : NIL); }; - - function ShiftSignal(kName, body, env) { - this.kName = kName; - this.body = body; - this.env = env; - } - - PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; }; - - // Platform accessors — exposed as primitives so user SX code can call them - PRIMITIVES["component-affinity"] = componentAffinity; - - var _resetResume = []; - - function sfReset(args, env) { - var body = args[0]; - try { - return trampoline(evalExpr(body, env)); - } catch (e) { - if (e instanceof ShiftSignal) { - var sig = e; - var cont = new Continuation(function(value) { - if (value === undefined) value = NIL; - _resetResume.push(value); - try { - return trampoline(evalExpr(body, env)); - } finally { - _resetResume.pop(); - } - }); - var sigEnv = merge(sig.env); - sigEnv[sig.kName] = cont; - return trampoline(evalExpr(sig.body, sigEnv)); - } - throw e; - } - } - - function sfShift(args, env) { - if (_resetResume.length > 0) { - return _resetResume[_resetResume.length - 1]; - } - var kName = symbolName(args[0]); - var body = args[1]; - throw new ShiftSignal(kName, body, env); - } - - // Wrap evalList to intercept reset/shift - var _baseEvalList = evalList; - evalList = function(expr, env) { - var head = expr[0]; - if (isSym(head)) { - var name = head.name; - if (name === "reset") return sfReset(expr.slice(1), env); - if (name === "shift") return sfShift(expr.slice(1), env); - } - return _baseEvalList(expr, env); - }; - - // Wrap aserSpecial to handle reset/shift in SX wire mode - if (typeof aserSpecial === "function") { - var _baseAserSpecial = aserSpecial; - aserSpecial = function(name, expr, env) { - if (name === "reset") return sfReset(expr.slice(1), env); - if (name === "shift") return sfShift(expr.slice(1), env); - return _baseAserSpecial(name, expr, env); - }; - } - - // Wrap typeOf to recognize continuations - var _baseTypeOf = typeOf; - typeOf = function(x) { - if (x != null && x._continuation) return "continuation"; - return _baseTypeOf(x); - }; -''' - - -ASYNC_IO_JS = ''' - // ========================================================================= - // Async IO: Promise-aware rendering for client-side IO primitives - // ========================================================================= - // - // IO primitives (query, current-user, etc.) return Promises on the client. - // asyncRenderToDom walks the component tree; when it encounters an IO - // primitive, it awaits the Promise and continues rendering. - // - // The sync evaluator/renderer is untouched. This is a separate async path - // used only when a page's component tree contains IO references. - - var IO_PRIMITIVES = {}; - - function registerIoPrimitive(name, fn) { - IO_PRIMITIVES[name] = fn; - } - - function isPromise(x) { - return x != null && typeof x === "object" && typeof x.then === "function"; - } - - // Async trampoline: resolves thunks, awaits Promises - function asyncTrampoline(val) { - if (isPromise(val)) return val.then(asyncTrampoline); - if (isThunk(val)) return asyncTrampoline(evalExpr(thunkExpr(val), thunkEnv(val))); - return val; - } - - // Async eval: like trampoline(evalExpr(...)) but handles IO primitives - function asyncEval(expr, env) { - // Intercept IO primitive calls at the AST level - if (Array.isArray(expr) && expr.length > 0) { - var head = expr[0]; - if (head && head._sym) { - var name = head.name; - if (IO_PRIMITIVES[name]) { - // Evaluate args, then call the IO primitive - return asyncEvalIoCall(name, expr.slice(1), env); - } - } - } - // Non-IO: use sync eval, but result might be a thunk - var result = evalExpr(expr, env); - return asyncTrampoline(result); - } - - function asyncEvalIoCall(name, rawArgs, env) { - // Parse keyword args and positional args, evaluating each (may be async) - var kwargs = {}; - var args = []; - var promises = []; - var i = 0; - while (i < rawArgs.length) { - var arg = rawArgs[i]; - if (arg && arg._kw && (i + 1) < rawArgs.length) { - var kName = arg.name; - var kVal = asyncEval(rawArgs[i + 1], env); - if (isPromise(kVal)) { - (function(k) { promises.push(kVal.then(function(v) { kwargs[k] = v; })); })(kName); - } else { - kwargs[kName] = kVal; - } - i += 2; - } else { - var aVal = asyncEval(arg, env); - if (isPromise(aVal)) { - (function(idx) { promises.push(aVal.then(function(v) { args[idx] = v; })); })(args.length); - args.push(null); // placeholder - } else { - args.push(aVal); - } - i++; - } - } - var ioFn = IO_PRIMITIVES[name]; - if (promises.length > 0) { - return Promise.all(promises).then(function() { return ioFn(args, kwargs); }); - } - return ioFn(args, kwargs); - } - - // Async render-to-dom: returns Promise or Node - function asyncRenderToDom(expr, env, ns) { - // Literals - if (expr === NIL || expr === null || expr === undefined) return null; - if (expr === true || expr === false) return null; - if (typeof expr === "string") return document.createTextNode(expr); - if (typeof expr === "number") return document.createTextNode(String(expr)); - - // Symbol -> async eval then render - if (expr && expr._sym) { - var val = asyncEval(expr, env); - if (isPromise(val)) return val.then(function(v) { return asyncRenderToDom(v, env, ns); }); - return asyncRenderToDom(val, env, ns); - } - - // Keyword - if (expr && expr._kw) return document.createTextNode(expr.name); - - // DocumentFragment / DOM nodes pass through - if (expr instanceof DocumentFragment || (expr && expr.nodeType)) return expr; - - // Dict -> skip - if (expr && typeof expr === "object" && !Array.isArray(expr)) return null; - - // List - if (!Array.isArray(expr) || expr.length === 0) return null; - - var head = expr[0]; - if (!head) return null; - - // Symbol head - if (head._sym) { - var hname = head.name; - - // IO primitive - if (IO_PRIMITIVES[hname]) { - var ioResult = asyncEval(expr, env); - if (isPromise(ioResult)) return ioResult.then(function(v) { return asyncRenderToDom(v, env, ns); }); - return asyncRenderToDom(ioResult, env, ns); - } - - // Fragment - if (hname === "<>") return asyncRenderChildren(expr.slice(1), env, ns); - - // raw! - if (hname === "raw!") { - return asyncEvalRaw(expr.slice(1), env); - } - - // Special forms that need async handling - if (hname === "if") return asyncRenderIf(expr, env, ns); - if (hname === "when") return asyncRenderWhen(expr, env, ns); - if (hname === "cond") return asyncRenderCond(expr, env, ns); - if (hname === "case") return asyncRenderCase(expr, env, ns); - if (hname === "let" || hname === "let*") return asyncRenderLet(expr, env, ns); - if (hname === "begin" || hname === "do") return asyncRenderChildren(expr.slice(1), env, ns); - if (hname === "map") return asyncRenderMap(expr, env, ns); - if (hname === "map-indexed") return asyncRenderMapIndexed(expr, env, ns); - if (hname === "for-each") return asyncRenderMap(expr, env, ns); - - // define/defcomp/defmacro — eval for side effects - if (hname === "define" || hname === "defcomp" || hname === "defmacro" || - hname === "defstyle" || hname === "defhandler") { - trampoline(evalExpr(expr, env)); - return null; - } - - // quote - if (hname === "quote") return null; - - // lambda/fn - if (hname === "lambda" || hname === "fn") { - trampoline(evalExpr(expr, env)); - return null; - } - - // and/or — eval and render result - if (hname === "and" || hname === "or" || hname === "->") { - var aoResult = asyncEval(expr, env); - if (isPromise(aoResult)) return aoResult.then(function(v) { return asyncRenderToDom(v, env, ns); }); - return asyncRenderToDom(aoResult, env, ns); - } - - // set! - if (hname === "set!") { - asyncEval(expr, env); - return null; - } - - // Component or Island - if (hname.charAt(0) === "~") { - var comp = env[hname]; - if (comp && comp._island) return renderDomIsland(comp, expr.slice(1), env, ns); - if (comp && comp._component) return asyncRenderComponent(comp, expr.slice(1), env, ns); - if (comp && comp._macro) { - var expanded = trampoline(expandMacro(comp, expr.slice(1), env)); - return asyncRenderToDom(expanded, env, ns); - } - } - - // Macro - if (env[hname] && env[hname]._macro) { - var mac = env[hname]; - var expanded = trampoline(expandMacro(mac, expr.slice(1), env)); - return asyncRenderToDom(expanded, env, ns); - } - - // HTML tag - if (typeof renderDomElement === "function" && contains(HTML_TAGS, hname)) { - return asyncRenderElement(hname, expr.slice(1), env, ns); - } - - // html: prefix - if (hname.indexOf("html:") === 0) { - return asyncRenderElement(hname.slice(5), expr.slice(1), env, ns); - } - - // Custom element - if (hname.indexOf("-") >= 0 && expr.length > 1 && expr[1] && expr[1]._kw) { - return asyncRenderElement(hname, expr.slice(1), env, ns); - } - - // SVG context - if (ns) return asyncRenderElement(hname, expr.slice(1), env, ns); - - // Fallback: eval and render - var fResult = asyncEval(expr, env); - if (isPromise(fResult)) return fResult.then(function(v) { return asyncRenderToDom(v, env, ns); }); - return asyncRenderToDom(fResult, env, ns); - } - - // Non-symbol head: eval call - var cResult = asyncEval(expr, env); - if (isPromise(cResult)) return cResult.then(function(v) { return asyncRenderToDom(v, env, ns); }); - return asyncRenderToDom(cResult, env, ns); - } - - function asyncRenderChildren(exprs, env, ns) { - var frag = document.createDocumentFragment(); - var pending = []; - for (var i = 0; i < exprs.length; i++) { - var result = asyncRenderToDom(exprs[i], env, ns); - if (isPromise(result)) { - // Insert placeholder, replace when resolved - var placeholder = document.createComment("async"); - frag.appendChild(placeholder); - (function(ph) { - pending.push(result.then(function(node) { - if (node) ph.parentNode.replaceChild(node, ph); - else ph.parentNode.removeChild(ph); - })); - })(placeholder); - } else if (result) { - frag.appendChild(result); - } - } - if (pending.length > 0) { - return Promise.all(pending).then(function() { return frag; }); - } - return frag; - } - - function asyncRenderElement(tag, args, env, ns) { - var newNs = tag === "svg" ? SVG_NS : tag === "math" ? MATH_NS : ns; - var el = domCreateElement(tag, newNs); - var pending = []; - var isVoid = contains(VOID_ELEMENTS, tag); - for (var i = 0; i < args.length; i++) { - var arg = args[i]; - if (arg && arg._kw && (i + 1) < args.length) { - var attrName = arg.name; - var attrVal = asyncEval(args[i + 1], env); - i++; - if (isPromise(attrVal)) { - (function(an, av) { - pending.push(av.then(function(v) { - if (!isNil(v) && v !== false) { - if (contains(BOOLEAN_ATTRS, an)) { if (isSxTruthy(v)) el.setAttribute(an, ""); } - else if (v === true) el.setAttribute(an, ""); - else el.setAttribute(an, String(v)); - } - })); - })(attrName, attrVal); - } else { - if (!isNil(attrVal) && attrVal !== false) { - if (contains(BOOLEAN_ATTRS, attrName)) { - if (isSxTruthy(attrVal)) el.setAttribute(attrName, ""); - } else if (attrVal === true) { - el.setAttribute(attrName, ""); - } else { - el.setAttribute(attrName, String(attrVal)); - } - } - } - } else if (!isVoid) { - var child = asyncRenderToDom(arg, env, newNs); - if (isPromise(child)) { - var placeholder = document.createComment("async"); - el.appendChild(placeholder); - (function(ph) { - pending.push(child.then(function(node) { - if (node) ph.parentNode.replaceChild(node, ph); - else ph.parentNode.removeChild(ph); - })); - })(placeholder); - } else if (child) { - el.appendChild(child); - } - } - } - if (pending.length > 0) return Promise.all(pending).then(function() { return el; }); - return el; - } - - function asyncRenderComponent(comp, args, env, ns) { - var kwargs = {}; - var children = []; - var pending = []; - for (var i = 0; i < args.length; i++) { - var arg = args[i]; - if (arg && arg._kw && (i + 1) < args.length) { - var kName = arg.name; - var kVal = asyncEval(args[i + 1], env); - if (isPromise(kVal)) { - (function(k) { pending.push(kVal.then(function(v) { kwargs[k] = v; })); })(kName); - } else { - kwargs[kName] = kVal; - } - i++; - } else { - children.push(arg); - } - } - - function doRender() { - var local = Object.create(componentClosure(comp)); - for (var k in env) if (env.hasOwnProperty(k)) local[k] = env[k]; - var params = componentParams(comp); - for (var j = 0; j < params.length; j++) { - local[params[j]] = params[j] in kwargs ? kwargs[params[j]] : NIL; - } - if (componentHasChildren(comp)) { - var childResult = asyncRenderChildren(children, env, ns); - if (isPromise(childResult)) { - return childResult.then(function(childFrag) { - local["children"] = childFrag; - return asyncRenderToDom(componentBody(comp), local, ns); - }); - } - local["children"] = childResult; - } - return asyncRenderToDom(componentBody(comp), local, ns); - } - - if (pending.length > 0) return Promise.all(pending).then(doRender); - return doRender(); - } - - function asyncRenderIf(expr, env, ns) { - var cond = asyncEval(expr[1], env); - if (isPromise(cond)) { - return cond.then(function(v) { - return isSxTruthy(v) - ? asyncRenderToDom(expr[2], env, ns) - : (expr.length > 3 ? asyncRenderToDom(expr[3], env, ns) : null); - }); - } - return isSxTruthy(cond) - ? asyncRenderToDom(expr[2], env, ns) - : (expr.length > 3 ? asyncRenderToDom(expr[3], env, ns) : null); - } - - function asyncRenderWhen(expr, env, ns) { - var cond = asyncEval(expr[1], env); - if (isPromise(cond)) { - return cond.then(function(v) { - return isSxTruthy(v) ? asyncRenderChildren(expr.slice(2), env, ns) : null; - }); - } - return isSxTruthy(cond) ? asyncRenderChildren(expr.slice(2), env, ns) : null; - } - - function asyncRenderCond(expr, env, ns) { - var clauses = expr.slice(1); - function step(idx) { - if (idx >= clauses.length) return null; - var clause = clauses[idx]; - if (!Array.isArray(clause) || clause.length < 2) return step(idx + 1); - var test = clause[0]; - if ((test && test._sym && (test.name === "else" || test.name === ":else")) || - (test && test._kw && test.name === "else")) { - return asyncRenderToDom(clause[1], env, ns); - } - var v = asyncEval(test, env); - if (isPromise(v)) return v.then(function(r) { return isSxTruthy(r) ? asyncRenderToDom(clause[1], env, ns) : step(idx + 1); }); - return isSxTruthy(v) ? asyncRenderToDom(clause[1], env, ns) : step(idx + 1); - } - return step(0); - } - - function asyncRenderCase(expr, env, ns) { - var matchVal = asyncEval(expr[1], env); - function doCase(mv) { - var clauses = expr.slice(2); - for (var i = 0; i < clauses.length - 1; i += 2) { - var test = clauses[i]; - if ((test && test._kw && test.name === "else") || - (test && test._sym && (test.name === "else" || test.name === ":else"))) { - return asyncRenderToDom(clauses[i + 1], env, ns); - } - var tv = trampoline(evalExpr(test, env)); - if (mv === tv || (typeof mv === "string" && typeof tv === "string" && mv === tv)) { - return asyncRenderToDom(clauses[i + 1], env, ns); - } - } - return null; - } - if (isPromise(matchVal)) return matchVal.then(doCase); - return doCase(matchVal); - } - - function asyncRenderLet(expr, env, ns) { - var bindings = expr[1]; - var local = Object.create(env); - for (var k in env) if (env.hasOwnProperty(k)) local[k] = env[k]; - function bindStep(idx) { - if (!Array.isArray(bindings)) return asyncRenderChildren(expr.slice(2), local, ns); - // Nested pairs: ((a 1) (b 2)) - if (bindings.length > 0 && Array.isArray(bindings[0])) { - if (idx >= bindings.length) return asyncRenderChildren(expr.slice(2), local, ns); - var b = bindings[idx]; - var vname = b[0]._sym ? b[0].name : String(b[0]); - var val = asyncEval(b[1], local); - if (isPromise(val)) return val.then(function(v) { local[vname] = v; return bindStep(idx + 1); }); - local[vname] = val; - return bindStep(idx + 1); - } - // Flat pairs: (a 1 b 2) - if (idx >= bindings.length) return asyncRenderChildren(expr.slice(2), local, ns); - var vn = bindings[idx]._sym ? bindings[idx].name : String(bindings[idx]); - var vv = asyncEval(bindings[idx + 1], local); - if (isPromise(vv)) return vv.then(function(v) { local[vn] = v; return bindStep(idx + 2); }); - local[vn] = vv; - return bindStep(idx + 2); - } - return bindStep(0); - } - - function asyncRenderMap(expr, env, ns) { - var fn = asyncEval(expr[1], env); - var coll = asyncEval(expr[2], env); - function doMap(f, c) { - if (!Array.isArray(c)) return null; - var frag = document.createDocumentFragment(); - var pending = []; - for (var i = 0; i < c.length; i++) { - var item = c[i]; - var result; - if (f && f._lambda) { - var lenv = Object.create(f.closure || env); - for (var k in env) if (env.hasOwnProperty(k)) lenv[k] = env[k]; - lenv[f.params[0]] = item; - result = asyncRenderToDom(f.body, lenv, null); - } else if (typeof f === "function") { - var r = f(item); - result = isPromise(r) ? r.then(function(v) { return asyncRenderToDom(v, env, null); }) : asyncRenderToDom(r, env, null); - } else { - result = asyncRenderToDom(item, env, null); - } - if (isPromise(result)) { - var ph = document.createComment("async"); - frag.appendChild(ph); - (function(p) { pending.push(result.then(function(n) { if (n) p.parentNode.replaceChild(n, p); else p.parentNode.removeChild(p); })); })(ph); - } else if (result) { - frag.appendChild(result); - } - } - if (pending.length) return Promise.all(pending).then(function() { return frag; }); - return frag; - } - if (isPromise(fn) || isPromise(coll)) { - return Promise.all([isPromise(fn) ? fn : Promise.resolve(fn), isPromise(coll) ? coll : Promise.resolve(coll)]) - .then(function(r) { return doMap(r[0], r[1]); }); - } - return doMap(fn, coll); - } - - function asyncRenderMapIndexed(expr, env, ns) { - var fn = asyncEval(expr[1], env); - var coll = asyncEval(expr[2], env); - function doMap(f, c) { - if (!Array.isArray(c)) return null; - var frag = document.createDocumentFragment(); - var pending = []; - for (var i = 0; i < c.length; i++) { - var item = c[i]; - var result; - if (f && f._lambda) { - var lenv = Object.create(f.closure || env); - for (var k in env) if (env.hasOwnProperty(k)) lenv[k] = env[k]; - lenv[f.params[0]] = i; - lenv[f.params[1]] = item; - result = asyncRenderToDom(f.body, lenv, null); - } else if (typeof f === "function") { - var r = f(i, item); - result = isPromise(r) ? r.then(function(v) { return asyncRenderToDom(v, env, null); }) : asyncRenderToDom(r, env, null); - } else { - result = asyncRenderToDom(item, env, null); - } - if (isPromise(result)) { - var ph = document.createComment("async"); - frag.appendChild(ph); - (function(p) { pending.push(result.then(function(n) { if (n) p.parentNode.replaceChild(n, p); else p.parentNode.removeChild(p); })); })(ph); - } else if (result) { - frag.appendChild(result); - } - } - if (pending.length) return Promise.all(pending).then(function() { return frag; }); - return frag; - } - if (isPromise(fn) || isPromise(coll)) { - return Promise.all([isPromise(fn) ? fn : Promise.resolve(fn), isPromise(coll) ? coll : Promise.resolve(coll)]) - .then(function(r) { return doMap(r[0], r[1]); }); - } - return doMap(fn, coll); - } - - function asyncEvalRaw(args, env) { - var parts = []; - var pending = []; - for (var i = 0; i < args.length; i++) { - var val = asyncEval(args[i], env); - if (isPromise(val)) { - (function(idx) { - pending.push(val.then(function(v) { parts[idx] = v; })); - })(parts.length); - parts.push(null); - } else { - parts.push(val); - } - } - function assemble() { - var html = ""; - for (var j = 0; j < parts.length; j++) { - var p = parts[j]; - if (p && p._rawHtml) html += p.html; - else if (typeof p === "string") html += p; - else if (p != null && !isNil(p)) html += String(p); - } - var el = document.createElement("span"); - el.innerHTML = html; - var frag = document.createDocumentFragment(); - while (el.firstChild) frag.appendChild(el.firstChild); - return frag; - } - if (pending.length) return Promise.all(pending).then(assemble); - return assemble(); - } - - // Async version of sxRenderWithEnv — returns Promise - function asyncSxRenderWithEnv(source, extraEnv) { - var env = extraEnv ? merge(componentEnv, extraEnv) : componentEnv; - var exprs = parse(source); - if (!_hasDom) return Promise.resolve(null); - return asyncRenderChildren(exprs, env, null); - } - - // IO proxy cache: key → { value, expires } - var _ioCache = {}; - var IO_CACHE_TTL = 300000; // 5 minutes - - // Register a server-proxied IO primitive: fetches from /sx/io/ - // Uses GET for short args, POST for long payloads (URL length safety). - // Results are cached client-side by (name + args) with a TTL. - function registerProxiedIo(name) { - registerIoPrimitive(name, function(args, kwargs) { - // Cache key: name + serialized args - var cacheKey = name; - for (var ci = 0; ci < args.length; ci++) cacheKey += "\0" + String(args[ci]); - for (var ck in kwargs) { - if (kwargs.hasOwnProperty(ck)) cacheKey += "\0" + ck + "=" + String(kwargs[ck]); - } - var cached = _ioCache[cacheKey]; - if (cached && cached.expires > Date.now()) return cached.value; - - var url = "/sx/io/" + encodeURIComponent(name); - var qs = []; - for (var i = 0; i < args.length; i++) { - qs.push("_arg" + i + "=" + encodeURIComponent(String(args[i]))); - } - for (var k in kwargs) { - if (kwargs.hasOwnProperty(k)) { - qs.push(encodeURIComponent(k) + "=" + encodeURIComponent(String(kwargs[k]))); - } - } - var queryStr = qs.join("&"); - var fetchOpts; - if (queryStr.length > 1500) { - // POST with JSON body for long payloads - var sArgs = []; - for (var j = 0; j < args.length; j++) sArgs.push(String(args[j])); - var sKwargs = {}; - for (var kk in kwargs) { - if (kwargs.hasOwnProperty(kk)) sKwargs[kk] = String(kwargs[kk]); - } - var postHeaders = { "SX-Request": "true", "Content-Type": "application/json" }; - var csrf = csrfToken(); - if (csrf && csrf !== NIL) postHeaders["X-CSRFToken"] = csrf; - fetchOpts = { - method: "POST", - headers: postHeaders, - body: JSON.stringify({ args: sArgs, kwargs: sKwargs }) - }; - } else { - if (queryStr) url += "?" + queryStr; - fetchOpts = { headers: { "SX-Request": "true" } }; - } - var result = fetch(url, fetchOpts) - .then(function(resp) { - if (!resp.ok) { - logWarn("sx:io " + name + " failed " + resp.status); - return NIL; - } - return resp.text(); - }) - .then(function(text) { - if (!text || text === "nil") return NIL; - try { - var exprs = parse(text); - var val = exprs.length === 1 ? exprs[0] : exprs; - _ioCache[cacheKey] = { value: val, expires: Date.now() + IO_CACHE_TTL }; - return val; - } catch (e) { - logWarn("sx:io " + name + " parse error: " + (e && e.message ? e.message : e)); - return NIL; - } - }) - .catch(function(e) { - logWarn("sx:io " + name + " network error: " + (e && e.message ? e.message : e)); - return NIL; - }); - // Cache the in-flight promise too (dedup concurrent calls for same args) - _ioCache[cacheKey] = { value: result, expires: Date.now() + IO_CACHE_TTL }; - return result; - }); - } - - // Register IO deps as proxied primitives (idempotent, called per-page) - function registerIoDeps(names) { - if (!names || !names.length) return; - var registered = 0; - for (var i = 0; i < names.length; i++) { - var name = names[i]; - if (!IO_PRIMITIVES[name]) { - registerProxiedIo(name); - registered++; - } - } - if (registered > 0) { - logInfo("sx:io registered " + registered + " proxied primitives: " + names.join(", ")); - } - } -''' - - -def compile_ref_to_js( - adapters: list[str] | None = None, - modules: list[str] | None = None, - extensions: list[str] | None = None, - spec_modules: list[str] | None = None, -) -> str: - """Read reference .sx files and emit JavaScript. - - Args: - adapters: List of adapter names to include. - Valid names: html, sx, dom, engine. - None = include all adapters. - modules: List of primitive module names to include. - core.* are always included. stdlib.* are opt-in. - None = include all modules (backward compatible). - extensions: List of optional extensions to include. - Valid names: continuations. - None = no extensions. - spec_modules: List of spec module names to include. - Valid names: deps. - None = no spec modules. - """ - ref_dir = os.path.dirname(os.path.abspath(__file__)) - emitter = JSEmitter() - - # Platform JS blocks keyed by adapter name - adapter_platform = { - "parser": PLATFORM_PARSER_JS, - "dom": PLATFORM_DOM_JS, - "engine": PLATFORM_ENGINE_PURE_JS, - "orchestration": PLATFORM_ORCHESTRATION_JS, - "boot": PLATFORM_BOOT_JS, - } - - # Resolve adapter set - if adapters is None: - adapter_set = set(ADAPTER_FILES.keys()) - else: - adapter_set = set() - for a in adapters: - if a not in ADAPTER_FILES: - raise ValueError(f"Unknown adapter: {a!r}. Valid: {', '.join(ADAPTER_FILES)}") - adapter_set.add(a) - # Pull in dependencies - for dep in ADAPTER_DEPS.get(a, []): - adapter_set.add(dep) - - # Resolve spec modules - spec_mod_set = set() - if spec_modules: - for sm in spec_modules: - 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") - has_deps = "deps" in spec_mod_set - has_router = "router" in spec_mod_set - - # Core files always included, then selected adapters, then spec modules - sx_files = [ - ("eval.sx", "eval"), - ("render.sx", "render (core)"), - ] - for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "boot"): - if name in adapter_set: - sx_files.append(ADAPTER_FILES[name]) - for name in sorted(spec_mod_set): - sx_files.append(SPEC_MODULES[name]) - - all_sections = [] - for filename, label in sx_files: - filepath = os.path.join(ref_dir, filename) - if not os.path.exists(filepath): - continue - with open(filepath) as f: - src = f.read() - defines = extract_defines(src) - all_sections.append((label, defines)) - - # Resolve extensions - ext_set = set() - if extensions: - for e in extensions: - if e not in EXTENSION_NAMES: - raise ValueError(f"Unknown extension: {e!r}. Valid: {', '.join(EXTENSION_NAMES)}") - ext_set.add(e) - has_continuations = "continuations" in ext_set - - # Build output - has_html = "html" in adapter_set - has_sx = "sx" in adapter_set - has_dom = "dom" in adapter_set - has_engine = "engine" in adapter_set - 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 - prim_modules = None # None = all - if modules is not None: - prim_modules = [m for m in _ALL_JS_MODULES if m.startswith("core.")] - for m in modules: - if m not in prim_modules: - if m not in PRIMITIVES_JS_MODULES: - raise ValueError(f"Unknown module: {m!r}. Valid: {', '.join(PRIMITIVES_JS_MODULES)}") - prim_modules.append(m) - - parts = [] - parts.append(PREAMBLE) - parts.append(PLATFORM_JS_PRE) - parts.append('\n // =========================================================================') - parts.append(' // Primitives') - parts.append(' // =========================================================================\n') - parts.append(' var PRIMITIVES = {};') - parts.append(_assemble_primitives_js(prim_modules)) - parts.append(PLATFORM_JS_POST) - - if has_deps: - parts.append(PLATFORM_DEPS_JS) - - # Parser platform must come before compiled parser.sx - if has_parser: - parts.append(adapter_platform["parser"]) - - for label, defines in all_sections: - parts.append(f"\n // === Transpiled from {label} ===\n") - for name, expr in defines: - parts.append(f" // {name}") - parts.append(f" {emitter.emit_statement(expr)}") - parts.append("") - - # Platform JS for selected adapters - if not has_dom: - parts.append("\n var _hasDom = false;\n") - for name in ("dom", "engine", "orchestration", "boot"): - if name in adapter_set and name in adapter_platform: - parts.append(adapter_platform[name]) - - parts.append(fixups_js(has_html, has_sx, has_dom, has_signals)) - if has_continuations: - 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, has_signals)) - parts.append(EPILOGUE) - from datetime import datetime, timezone - build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") - return "\n".join(parts).replace("BUILD_TIMESTAMP", build_ts) - - -# --------------------------------------------------------------------------- -# Static JS sections -# --------------------------------------------------------------------------- - -PREAMBLE = '''\ -/** - * sx-ref.js — Generated from reference SX evaluator specification. - * - * Bootstrap-compiled from shared/sx/ref/{eval,render,primitives}.sx - * Compare against hand-written sx.js for correctness verification. - * - * DO NOT EDIT — regenerate with: python bootstrap_js.py - */ -;(function(global) { - "use strict"; - - // ========================================================================= - // Types - // ========================================================================= - - var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "BUILD_TIMESTAMP"; - - function isNil(x) { return x === NIL || x === null || x === undefined; } - function isSxTruthy(x) { return x !== false && !isNil(x); } - - function Symbol(name) { this.name = name; } - Symbol.prototype.toString = function() { return this.name; }; - Symbol.prototype._sym = true; - - function Keyword(name) { this.name = name; } - Keyword.prototype.toString = function() { return ":" + this.name; }; - Keyword.prototype._kw = true; - - function Lambda(params, body, closure, name) { - this.params = params; - this.body = body; - this.closure = closure || {}; - this.name = name || null; - } - Lambda.prototype._lambda = true; - - function Component(name, params, hasChildren, body, closure, affinity) { - this.name = name; - this.params = params; - this.hasChildren = hasChildren; - this.body = body; - this.closure = closure || {}; - this.affinity = affinity || "auto"; - } - 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; - this.body = body; - this.closure = closure || {}; - this.name = name || null; - } - Macro.prototype._macro = true; - - function Thunk(expr, env) { this.expr = expr; this.env = env; } - Thunk.prototype._thunk = true; - - function RawHTML(html) { this.html = html; } - RawHTML.prototype._raw = true; - - function isSym(x) { return x != null && x._sym === true; } - function isKw(x) { return x != null && x._kw === true; } - - function merge() { - var out = {}; - for (var i = 0; i < arguments.length; i++) { - var d = arguments[i]; - if (d) for (var k in d) out[k] = d[k]; - } - return out; - } - - function sxOr() { - for (var i = 0; i < arguments.length; i++) { - if (isSxTruthy(arguments[i])) return arguments[i]; - } - return arguments.length ? arguments[arguments.length - 1] : false; - }''' - -# --------------------------------------------------------------------------- -# Primitive modules — JS implementations keyed by spec module name. -# core.* modules are always included; stdlib.* are opt-in. -# --------------------------------------------------------------------------- - -PRIMITIVES_JS_MODULES: dict[str, str] = { - "core.arithmetic": ''' - // core.arithmetic - PRIMITIVES["+"] = function() { var s = 0; for (var i = 0; i < arguments.length; i++) s += arguments[i]; return s; }; - PRIMITIVES["-"] = function(a, b) { return arguments.length === 1 ? -a : a - b; }; - PRIMITIVES["*"] = function() { var s = 1; for (var i = 0; i < arguments.length; i++) s *= arguments[i]; return s; }; - PRIMITIVES["/"] = function(a, b) { return a / b; }; - PRIMITIVES["mod"] = function(a, b) { return a % b; }; - PRIMITIVES["inc"] = function(n) { return n + 1; }; - PRIMITIVES["dec"] = function(n) { return n - 1; }; - PRIMITIVES["abs"] = Math.abs; - PRIMITIVES["floor"] = Math.floor; - PRIMITIVES["ceil"] = Math.ceil; - PRIMITIVES["round"] = function(x, n) { - if (n === undefined || n === 0) return Math.round(x); - var f = Math.pow(10, n); return Math.round(x * f) / f; - }; - PRIMITIVES["min"] = Math.min; - PRIMITIVES["max"] = Math.max; - PRIMITIVES["sqrt"] = Math.sqrt; - PRIMITIVES["pow"] = Math.pow; - PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); }; -''', - - "core.comparison": ''' - // core.comparison - PRIMITIVES["="] = function(a, b) { return a === b; }; - PRIMITIVES["!="] = function(a, b) { return a !== b; }; - PRIMITIVES["<"] = function(a, b) { return a < b; }; - PRIMITIVES[">"] = function(a, b) { return a > b; }; - PRIMITIVES["<="] = function(a, b) { return a <= b; }; - PRIMITIVES[">="] = function(a, b) { return a >= b; }; -''', - - "core.logic": ''' - // core.logic - PRIMITIVES["not"] = function(x) { return !isSxTruthy(x); }; -''', - - "core.predicates": ''' - // core.predicates - PRIMITIVES["nil?"] = isNil; - PRIMITIVES["number?"] = function(x) { return typeof x === "number"; }; - PRIMITIVES["string?"] = function(x) { return typeof x === "string"; }; - PRIMITIVES["list?"] = Array.isArray; - PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; }; - PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); }; - PRIMITIVES["contains?"] = function(c, k) { - if (typeof c === "string") return c.indexOf(String(k)) !== -1; - if (Array.isArray(c)) return c.indexOf(k) !== -1; - return k in c; - }; - PRIMITIVES["odd?"] = function(n) { return n % 2 !== 0; }; - PRIMITIVES["even?"] = function(n) { return n % 2 === 0; }; - PRIMITIVES["zero?"] = function(n) { return n === 0; }; - PRIMITIVES["boolean?"] = function(x) { return x === true || x === false; }; -''', - - "core.strings": ''' - // core.strings - PRIMITIVES["str"] = function() { - var p = []; - for (var i = 0; i < arguments.length; i++) { - var v = arguments[i]; if (isNil(v)) continue; p.push(String(v)); - } - return p.join(""); - }; - PRIMITIVES["upper"] = function(s) { return String(s).toUpperCase(); }; - PRIMITIVES["lower"] = function(s) { return String(s).toLowerCase(); }; - PRIMITIVES["trim"] = function(s) { return String(s).trim(); }; - PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); }; - PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); }; - PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); }; - PRIMITIVES["index-of"] = function(s, needle, from) { return String(s).indexOf(needle, from || 0); }; - PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; }; - PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; }; - PRIMITIVES["slice"] = function(c, a, b) { return b !== undefined ? c.slice(a, b) : c.slice(a); }; - PRIMITIVES["substring"] = function(s, a, b) { return String(s).substring(a, b); }; - PRIMITIVES["string-length"] = function(s) { return String(s).length; }; - PRIMITIVES["string-contains?"] = function(s, sub) { return String(s).indexOf(String(sub)) !== -1; }; - PRIMITIVES["concat"] = function() { - var out = []; - for (var i = 0; i < arguments.length; i++) if (!isNil(arguments[i])) out = out.concat(arguments[i]); - return out; - }; -''', - - "core.collections": ''' - // core.collections - PRIMITIVES["list"] = function() { return Array.prototype.slice.call(arguments); }; - PRIMITIVES["dict"] = function() { - var d = {}; - for (var i = 0; i < arguments.length - 1; i += 2) d[arguments[i]] = arguments[i + 1]; - return d; - }; - PRIMITIVES["range"] = function(a, b, step) { - var r = []; step = step || 1; - for (var i = a; step > 0 ? i < b : i > b; i += step) r.push(i); - return r; - }; - PRIMITIVES["get"] = function(c, k, def) { var v = (c && c[k]); return v !== undefined ? v : (def !== undefined ? def : NIL); }; - PRIMITIVES["len"] = function(c) { return Array.isArray(c) ? c.length : typeof c === "string" ? c.length : Object.keys(c).length; }; - PRIMITIVES["first"] = function(c) { return c && c.length > 0 ? c[0] : NIL; }; - PRIMITIVES["last"] = function(c) { return c && c.length > 0 ? c[c.length - 1] : NIL; }; - PRIMITIVES["rest"] = function(c) { return c ? c.slice(1) : []; }; - PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; }; - PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); }; - PRIMITIVES["append"] = function(c, x) { return (c || []).concat([x]); }; - PRIMITIVES["append!"] = function(arr, x) { arr.push(x); return arr; }; - PRIMITIVES["chunk-every"] = function(c, n) { - var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r; - }; - PRIMITIVES["zip-pairs"] = function(c) { - var r = []; for (var i = 0; i < c.length - 1; i++) r.push([c[i], c[i + 1]]); return r; - }; - PRIMITIVES["reverse"] = function(c) { return Array.isArray(c) ? c.slice().reverse() : String(c).split("").reverse().join(""); }; - PRIMITIVES["flatten"] = function(c) { - var out = []; - function walk(a) { for (var i = 0; i < a.length; i++) Array.isArray(a[i]) ? walk(a[i]) : out.push(a[i]); } - walk(c || []); return out; - }; -''', - - "core.dict": ''' - // core.dict - PRIMITIVES["keys"] = function(d) { return Object.keys(d || {}); }; - PRIMITIVES["vals"] = function(d) { var r = []; for (var k in d) r.push(d[k]); return r; }; - PRIMITIVES["merge"] = function() { - var out = {}; - for (var i = 0; i < arguments.length; i++) { var d = arguments[i]; if (d && !isNil(d)) for (var k in d) out[k] = d[k]; } - return out; - }; - PRIMITIVES["assoc"] = function(d) { - var out = {}; if (d && !isNil(d)) for (var k in d) out[k] = d[k]; - for (var i = 1; i < arguments.length - 1; i += 2) out[arguments[i]] = arguments[i + 1]; - return out; - }; - PRIMITIVES["dissoc"] = function(d) { - var out = {}; for (var k in d) out[k] = d[k]; - for (var i = 1; i < arguments.length; i++) delete out[arguments[i]]; - return out; - }; - PRIMITIVES["dict-set!"] = function(d, k, v) { d[k] = v; return v; }; - PRIMITIVES["has-key?"] = function(d, k) { return d !== null && d !== undefined && k in d; }; - PRIMITIVES["into"] = function(target, coll) { - if (Array.isArray(target)) return Array.isArray(coll) ? coll.slice() : Object.entries(coll); - var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; } - return r; - }; -''', - - "stdlib.format": ''' - // stdlib.format - PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); }; - PRIMITIVES["parse-int"] = function(v, d) { var n = parseInt(v, 10); return isNaN(n) ? (d || 0) : n; }; - PRIMITIVES["format-date"] = function(s, fmt) { - if (!s) return ""; - try { - var d = new Date(s); - if (isNaN(d.getTime())) return String(s); - var months = ["January","February","March","April","May","June","July","August","September","October","November","December"]; - var short_months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; - return fmt.replace(/%-d/g, d.getDate()).replace(/%d/g, ("0"+d.getDate()).slice(-2)) - .replace(/%B/g, months[d.getMonth()]).replace(/%b/g, short_months[d.getMonth()]) - .replace(/%Y/g, d.getFullYear()).replace(/%m/g, ("0"+(d.getMonth()+1)).slice(-2)) - .replace(/%H/g, ("0"+d.getHours()).slice(-2)).replace(/%M/g, ("0"+d.getMinutes()).slice(-2)); - } catch (e) { return String(s); } - }; - PRIMITIVES["parse-datetime"] = function(s) { return s ? String(s) : NIL; }; -''', - - "stdlib.text": ''' - // stdlib.text - PRIMITIVES["pluralize"] = function(n, s, p) { - if (s || (p && p !== "s")) return n == 1 ? (s || "") : (p || "s"); - return n == 1 ? "" : "s"; - }; - PRIMITIVES["escape"] = function(s) { - return String(s).replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'"); - }; - PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); }; -''', - - "stdlib.debug": ''' - // stdlib.debug - PRIMITIVES["assert"] = function(cond, msg) { - if (!isSxTruthy(cond)) throw new Error("Assertion error: " + (msg || "Assertion failed")); - return true; - }; -''', -} - -# Modules to include by default (all) -_ALL_JS_MODULES = list(PRIMITIVES_JS_MODULES.keys()) - -# Selected primitive modules for current compilation (None = all) - - -def _assemble_primitives_js(modules: list[str] | None = None) -> str: - """Assemble JS primitive code from selected modules. - - If modules is None, all modules are included. - Core modules are always included regardless of the list. - """ - if modules is None: - modules = _ALL_JS_MODULES - parts = [] - for mod in modules: - if mod in PRIMITIVES_JS_MODULES: - parts.append(PRIMITIVES_JS_MODULES[mod]) - return "\n".join(parts) - - -PLATFORM_JS_PRE = ''' - // ========================================================================= - // Platform interface — JS implementation - // ========================================================================= - - function typeOf(x) { - if (isNil(x)) return "nil"; - if (typeof x === "number") return "number"; - if (typeof x === "string") return "string"; - if (typeof x === "boolean") return "boolean"; - if (x._sym) return "symbol"; - if (x._kw) return "keyword"; - 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"; - if (Array.isArray(x)) return "list"; - if (typeof x === "object") return "dict"; - return "unknown"; - } - - function symbolName(s) { return s.name; } - function keywordName(k) { return k.name; } - function makeSymbol(n) { return new Symbol(n); } - function makeKeyword(n) { return new Keyword(n); } - - function makeLambda(params, body, env) { return new Lambda(params, body, merge(env)); } - function makeComponent(name, params, hasChildren, body, env, affinity) { - return new Component(name, params, hasChildren, body, merge(env), affinity); - } - function makeMacro(params, restParam, body, env, name) { - return new Macro(params, restParam, body, merge(env), name); - } - function makeThunk(expr, env) { return new Thunk(expr, env); } - - function lambdaParams(f) { return f.params; } - function lambdaBody(f) { return f.body; } - function lambdaClosure(f) { return f.closure; } - function lambdaName(f) { return f.name; } - function setLambdaName(f, n) { f.name = n; } - - function componentParams(c) { return c.params; } - function componentBody(c) { return c.body; } - function componentClosure(c) { return c.closure; } - function componentHasChildren(c) { return c.hasChildren; } - function componentName(c) { return c.name; } - function componentAffinity(c) { return c.affinity || "auto"; } - - function macroParams(m) { return m.params; } - function macroRestParam(m) { return m.restParam; } - function macroBody(m) { return m.body; } - function macroClosure(m) { return m.closure; } - - function isThunk(x) { return x != null && x._thunk === true; } - function thunkExpr(t) { return t.expr; } - function thunkEnv(t) { return t.env; } - - 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; } - - // invoke — call any callable (native fn or SX lambda) with args. - // Transpiled code emits direct calls f(args) which fail on SX lambdas - // from runtime-evaluated island bodies. invoke dispatches correctly. - function invoke() { - var f = arguments[0]; - var args = Array.prototype.slice.call(arguments, 1); - if (isLambda(f)) return trampoline(callLambda(f, args, lambdaClosure(f))); - if (typeof f === 'function') return f.apply(null, args); - return 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]; } - function envSet(env, name, val) { env[name] = val; } - function envExtend(env) { return Object.create(env); } - function envMerge(base, overlay) { - var child = Object.create(base); - if (overlay) for (var k in overlay) if (overlay.hasOwnProperty(k)) child[k] = overlay[k]; - return child; - } - - function dictSet(d, k, v) { d[k] = v; return v; } - function dictGet(d, k) { var v = d[k]; return v !== undefined ? v : NIL; } - - // Render-expression detection — lets the evaluator delegate to the active adapter. - // Matches HTML tags, SVG tags, <>, raw!, ~components, html: prefix, custom elements. - // Placeholder — overridden by transpiled version from render.sx - function isRenderExpr(expr) { return false; } - - // Render dispatch — call the active adapter's render function. - // Set by each adapter when loaded; defaults to identity (no rendering). - var _renderExprFn = null; - - // Render mode flag — set by render-to-html/aser, checked by eval-list. - // When false, render expressions fall through to evalCall. - var _renderMode = false; - function renderActiveP() { return _renderMode; } - function setRenderActiveB(val) { _renderMode = !!val; } - - function renderExpr(expr, env) { - if (_renderExprFn) return _renderExprFn(expr, env); - // No adapter loaded — fall through to evalCall - return evalCall(first(expr), rest(expr), env); - } - - function stripPrefix(s, prefix) { - return s.indexOf(prefix) === 0 ? s.slice(prefix.length) : s; - } - - function error(msg) { throw new Error(msg); } - function inspect(x) { return JSON.stringify(x); } - -''' - -PLATFORM_JS_POST = ''' - function isPrimitive(name) { return name in PRIMITIVES; } - function getPrimitive(name) { return PRIMITIVES[name]; } - - // Higher-order helpers used by the transpiled code - function map(fn, coll) { return coll.map(fn); } - function mapIndexed(fn, coll) { return coll.map(function(item, i) { return fn(i, item); }); } - function filter(fn, coll) { return coll.filter(function(x) { return isSxTruthy(fn(x)); }); } - function reduce(fn, init, coll) { - var acc = init; - for (var i = 0; i < coll.length; i++) acc = fn(acc, coll[i]); - return acc; - } - function some(fn, coll) { - for (var i = 0; i < coll.length; i++) { var r = fn(coll[i]); if (isSxTruthy(r)) return r; } - return NIL; - } - function forEach(fn, coll) { for (var i = 0; i < coll.length; i++) fn(coll[i]); return NIL; } - function isEvery(fn, coll) { - for (var i = 0; i < coll.length; i++) { if (!isSxTruthy(fn(coll[i]))) return false; } - return true; - } - function mapDict(fn, d) { var r = {}; for (var k in d) r[k] = fn(k, d[k]); return r; } - - // List primitives used directly by transpiled code - var len = PRIMITIVES["len"]; - var first = PRIMITIVES["first"]; - var last = PRIMITIVES["last"]; - var rest = PRIMITIVES["rest"]; - var nth = PRIMITIVES["nth"]; - var cons = PRIMITIVES["cons"]; - var append = PRIMITIVES["append"]; - var isEmpty = PRIMITIVES["empty?"]; - var contains = PRIMITIVES["contains?"]; - var startsWith = PRIMITIVES["starts-with?"]; - var slice = PRIMITIVES["slice"]; - var concat = PRIMITIVES["concat"]; - var str = PRIMITIVES["str"]; - var join = PRIMITIVES["join"]; - var keys = PRIMITIVES["keys"]; - var get = PRIMITIVES["get"]; - var assoc = PRIMITIVES["assoc"]; - var range = PRIMITIVES["range"]; - function zip(a, b) { var r = []; for (var i = 0; i < Math.min(a.length, b.length); i++) r.push([a[i], b[i]]); return r; } - function append_b(arr, x) { arr.push(x); return arr; } - var apply = function(f, args) { - if (isLambda(f)) return trampoline(callLambda(f, args, lambdaClosure(f))); - return f.apply(null, args); - }; - - // Additional primitive aliases used by adapter/engine transpiled code - var split = PRIMITIVES["split"]; - var trim = PRIMITIVES["trim"]; - var upper = PRIMITIVES["upper"]; - var lower = PRIMITIVES["lower"]; - var replace_ = function(s, old, nw) { return s.split(old).join(nw); }; - var endsWith = PRIMITIVES["ends-with?"]; - var parseInt_ = PRIMITIVES["parse-int"]; - var dict_fn = PRIMITIVES["dict"]; - - // HTML rendering helpers - function escapeHtml(s) { - return String(s).replace(/&/g,"&").replace(//g,">").replace(/"/g,"""); - } - function escapeAttr(s) { return escapeHtml(s); } - function rawHtmlContent(r) { return r.html; } - function makeRawHtml(s) { return { _raw: true, html: s }; } - function sxExprSource(x) { return x && x.source ? x.source : String(x); } - - // Placeholders — overridden by transpiled spec from parser.sx / adapter-sx.sx - function serialize(val) { return String(val); } - function isSpecialForm(n) { return false; } - function isHoForm(n) { return false; } - - // processBindings and evalCond — now specced in render.sx, bootstrapped above - - function isDefinitionForm(name) { - return name === "define" || name === "defcomp" || name === "defmacro" || - name === "defstyle" || name === "defhandler"; - } - - function indexOf_(s, ch) { - return typeof s === "string" ? s.indexOf(ch) : -1; - } - - function dictHas(d, k) { return d != null && k in d; } - function dictDelete(d, k) { delete d[k]; } - - function forEachIndexed(fn, coll) { - for (var i = 0; i < coll.length; i++) fn(i, coll[i]); - return NIL; - } - - // ========================================================================= - // Performance overrides — evaluator hot path - // ========================================================================= - - // Override parseKeywordArgs: imperative loop instead of reduce+assoc - parseKeywordArgs = function(rawArgs, env) { - var kwargs = {}; - var children = []; - for (var i = 0; i < rawArgs.length; i++) { - var arg = rawArgs[i]; - if (arg && arg._kw && (i + 1) < rawArgs.length) { - kwargs[arg.name] = trampoline(evalExpr(rawArgs[i + 1], env)); - i++; - } else { - children.push(trampoline(evalExpr(arg, env))); - } - } - return [kwargs, children]; - }; - - // Override callComponent: use prototype chain env, imperative kwarg binding - callComponent = function(comp, rawArgs, env) { - var kwargs = {}; - var children = []; - for (var i = 0; i < rawArgs.length; i++) { - var arg = rawArgs[i]; - if (arg && arg._kw && (i + 1) < rawArgs.length) { - kwargs[arg.name] = trampoline(evalExpr(rawArgs[i + 1], env)); - i++; - } else { - children.push(trampoline(evalExpr(arg, env))); - } - } - var local = Object.create(componentClosure(comp)); - for (var k in env) if (env.hasOwnProperty(k)) local[k] = env[k]; - var params = componentParams(comp); - for (var j = 0; j < params.length; j++) { - var p = params[j]; - local[p] = p in kwargs ? kwargs[p] : NIL; - } - if (componentHasChildren(comp)) { - local["children"] = children; - } - return makeThunk(componentBody(comp), local); - };''' - -PLATFORM_DEPS_JS = ''' - // ========================================================================= - // Platform: deps module — component dependency analysis - // ========================================================================= - - function componentDeps(c) { - return c.deps ? c.deps.slice() : []; - } - - function componentSetDeps(c, deps) { - c.deps = deps; - } - - function componentCssClasses(c) { - return c.cssClasses ? c.cssClasses.slice() : []; - } - - function envComponents(env) { - var names = []; - for (var k in env) { - var v = env[k]; - if (v && (v._component || v._macro)) names.push(k); - } - return names; - } - - function regexFindAll(pattern, source) { - var re = new RegExp(pattern, "g"); - var results = []; - var m; - while ((m = re.exec(source)) !== null) { - if (m[1] !== undefined) results.push(m[1]); - else results.push(m[0]); - } - return results; - } - - function scanCssClasses(source) { - var classes = {}; - var result = []; - var m; - var re1 = /:class\\s+"([^"]*)"/g; - while ((m = re1.exec(source)) !== null) { - var parts = m[1].split(/\\s+/); - for (var i = 0; i < parts.length; i++) { - if (parts[i] && !classes[parts[i]]) { - classes[parts[i]] = true; - result.push(parts[i]); - } - } - } - var re2 = /:class\\s+\\(str\\s+((?:"[^"]*"\\s*)+)\\)/g; - while ((m = re2.exec(source)) !== null) { - var re3 = /"([^"]*)"/g; - var m2; - while ((m2 = re3.exec(m[1])) !== null) { - var parts2 = m2[1].split(/\\s+/); - for (var j = 0; j < parts2.length; j++) { - if (parts2[j] && !classes[parts2[j]]) { - classes[parts2[j]] = true; - result.push(parts2[j]); - } - } - } - } - var re4 = /;;\\s*@css\\s+(.+)/g; - while ((m = re4.exec(source)) !== null) { - var parts3 = m[1].split(/\\s+/); - for (var k = 0; k < parts3.length; k++) { - if (parts3[k] && !classes[parts3[k]]) { - classes[parts3[k]] = true; - result.push(parts3[k]); - } - } - } - return result; - } - - function componentIoRefs(c) { - return c.ioRefs ? c.ioRefs.slice() : []; - } - - function componentSetIoRefs(c, refs) { - c.ioRefs = refs; - } -''' - -PLATFORM_PARSER_JS = r""" - // ========================================================================= - // Platform interface — Parser - // ========================================================================= - // Character classification derived from the grammar: - // ident-start → [a-zA-Z_~*+\-><=/!?&] - // ident-char → ident-start + [0-9.:\/\[\]#,] - - var _identStartRe = /[a-zA-Z_~*+\-><=/!?&]/; - var _identCharRe = /[a-zA-Z0-9_~*+\-><=/!?.:&/\[\]#,]/; - - function isIdentStart(ch) { return _identStartRe.test(ch); } - function isIdentChar(ch) { return _identCharRe.test(ch); } - function parseNumber(s) { return Number(s); } - function escapeString(s) { - return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t"); - } - function sxExprSource(e) { return typeof e === "string" ? e : String(e); } -""" - -PLATFORM_DOM_JS = """ - // ========================================================================= - // Platform interface — DOM adapter (browser-only) - // ========================================================================= - - var _hasDom = typeof document !== "undefined"; - - // Register DOM adapter as the render dispatch target for the evaluator. - _renderExprFn = function(expr, env) { return renderToDom(expr, env, null); }; - _renderMode = true; // Browser always evaluates in render context. - - var SVG_NS = "http://www.w3.org/2000/svg"; - var MATH_NS = "http://www.w3.org/1998/Math/MathML"; - - function domCreateElement(tag, ns) { - if (!_hasDom) return null; - if (ns && ns !== NIL) return document.createElementNS(ns, tag); - return document.createElement(tag); - } - - function createTextNode(s) { - return _hasDom ? document.createTextNode(s) : null; - } - - function createComment(s) { - return _hasDom ? document.createComment(s || "") : null; - } - - function createFragment() { - return _hasDom ? document.createDocumentFragment() : null; - } - - function domAppend(parent, child) { - if (parent && child) parent.appendChild(child); - } - - function domPrepend(parent, child) { - if (parent && child) parent.insertBefore(child, parent.firstChild); - } - - function domSetAttr(el, name, val) { - if (el && el.setAttribute) el.setAttribute(name, val); - } - - function domGetAttr(el, name) { - if (!el || !el.getAttribute) return NIL; - var v = el.getAttribute(name); - return v === null ? NIL : v; - } - - function domRemoveAttr(el, name) { - if (el && el.removeAttribute) el.removeAttribute(name); - } - - function domHasAttr(el, name) { - return !!(el && el.hasAttribute && el.hasAttribute(name)); - } - - function domParseHtml(html) { - if (!_hasDom) return null; - var tpl = document.createElement("template"); - tpl.innerHTML = html; - return tpl.content; - } - - function domClone(node) { - return node && node.cloneNode ? node.cloneNode(true) : node; - } - - function domParent(el) { return el ? el.parentNode : null; } - function domId(el) { return el && el.id ? el.id : NIL; } - function domNodeType(el) { return el ? el.nodeType : 0; } - function domNodeName(el) { return el ? el.nodeName : ""; } - function domTextContent(el) { return el ? el.textContent || el.nodeValue || "" : ""; } - function domSetTextContent(el, s) { if (el) { if (el.nodeType === 3 || el.nodeType === 8) el.nodeValue = s; else el.textContent = s; } } - function domIsFragment(el) { return el ? el.nodeType === 11 : false; } - function domIsChildOf(child, parent) { return !!(parent && child && child.parentNode === parent); } - function domIsActiveElement(el) { return _hasDom && el === document.activeElement; } - function domIsInputElement(el) { - if (!el || !el.tagName) return false; - var t = el.tagName; - return t === "INPUT" || t === "TEXTAREA" || t === "SELECT"; - } - function domFirstChild(el) { return el ? el.firstChild : null; } - function domNextSibling(el) { return el ? el.nextSibling : null; } - - function domChildList(el) { - if (!el || !el.childNodes) return []; - return Array.prototype.slice.call(el.childNodes); - } - - function domAttrList(el) { - if (!el || !el.attributes) return []; - var r = []; - for (var i = 0; i < el.attributes.length; i++) { - r.push([el.attributes[i].name, el.attributes[i].value]); - } - return r; - } - - function domInsertBefore(parent, node, ref) { - if (parent && node) parent.insertBefore(node, ref || null); - } - - function domInsertAfter(ref, node) { - if (ref && ref.parentNode && node) { - ref.parentNode.insertBefore(node, ref.nextSibling); - } - } - - function domRemoveChild(parent, child) { - if (parent && child && child.parentNode === parent) parent.removeChild(child); - } - - function domReplaceChild(parent, newChild, oldChild) { - if (parent && newChild && oldChild) parent.replaceChild(newChild, oldChild); - } - - function domSetInnerHtml(el, html) { - if (el) el.innerHTML = html; - } - - function domInsertAdjacentHtml(el, pos, html) { - if (el && el.insertAdjacentHTML) el.insertAdjacentHTML(pos, html); - } - - function domGetStyle(el, prop) { - return el && el.style ? el.style[prop] || "" : ""; - } - - function domSetStyle(el, prop, val) { - if (el && el.style) el.style[prop] = val; - } - - function domGetProp(el, name) { return el ? el[name] : NIL; } - function domSetProp(el, name, val) { if (el) el[name] = val; } - - function domAddClass(el, cls) { - if (el && el.classList) el.classList.add(cls); - } - - function domRemoveClass(el, cls) { - if (el && el.classList) el.classList.remove(cls); - } - - function domDispatch(el, name, detail) { - if (!_hasDom || !el) return false; - var evt = new CustomEvent(name, { bubbles: true, cancelable: true, detail: detail || {} }); - return el.dispatchEvent(evt); - } - - function domListen(el, name, handler) { - if (!_hasDom || !el) return function() {}; - // Wrap SX lambdas from runtime-evaluated island code into native fns - var wrapped = isLambda(handler) - ? function(e) { try { invoke(handler, e); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } } - : handler; - if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler)); - el.addEventListener(name, wrapped); - return function() { el.removeEventListener(name, wrapped); }; - } - - function eventDetail(e) { - return (e && e.detail != null) ? e.detail : nil; - } - - function domQuery(sel) { - return _hasDom ? document.querySelector(sel) : null; - } - - function domEnsureElement(sel) { - if (!_hasDom) return null; - var el = document.querySelector(sel); - if (el) return el; - // Parse #id selector → create div with that id, append to body - if (sel.charAt(0) === '#') { - el = document.createElement('div'); - el.id = sel.slice(1); - document.body.appendChild(el); - return el; - } - return null; - } - - function domQueryAll(root, sel) { - if (!root || !root.querySelectorAll) return []; - return Array.prototype.slice.call(root.querySelectorAll(sel)); - } - - 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; } - } - function domGetData(el, key) { - return (el && el._sxData) ? (el._sxData[key] != null ? el._sxData[key] : nil) : nil; - } - function domInnerHtml(el) { - return (el && el.innerHTML != null) ? el.innerHTML : ""; - } - function jsonParse(s) { - try { return JSON.parse(s); } catch(e) { return {}; } - } - - // renderDomComponent and renderDomElement are transpiled from - // adapter-dom.sx — no imperative overrides needed. -""" - -PLATFORM_ENGINE_PURE_JS = """ - // ========================================================================= - // Platform interface — Engine pure logic (browser + node compatible) - // ========================================================================= - - function browserLocationHref() { - return typeof location !== "undefined" ? location.href : ""; - } - - function browserSameOrigin(url) { - try { return new URL(url, location.href).origin === location.origin; } - catch (e) { return true; } - } - - function browserPushState(url) { - if (typeof history !== "undefined") { - try { history.pushState({ sxUrl: url, scrollY: typeof window !== "undefined" ? window.scrollY : 0 }, "", url); } - catch (e) {} - } - } - - function browserReplaceState(url) { - if (typeof history !== "undefined") { - try { history.replaceState({ sxUrl: url, scrollY: typeof window !== "undefined" ? window.scrollY : 0 }, "", url); } - catch (e) {} - } - } - - function nowMs() { return Date.now(); } - - function parseHeaderValue(s) { - if (!s) return null; - try { - if (s.charAt(0) === "{" && s.charAt(1) === ":") return parse(s); - return JSON.parse(s); - } catch (e) { return null; } - } -""" - -PLATFORM_ORCHESTRATION_JS = """ - // ========================================================================= - // Platform interface — Orchestration (browser-only) - // ========================================================================= - - // --- Browser/Network --- - - function browserNavigate(url) { - if (typeof location !== "undefined") location.assign(url); - } - - function browserReload() { - if (typeof location !== "undefined") location.reload(); - } - - function browserScrollTo(x, y) { - if (typeof window !== "undefined") window.scrollTo(x, y); - } - - function browserMediaMatches(query) { - if (typeof window === "undefined") return false; - return window.matchMedia(query).matches; - } - - function browserConfirm(msg) { - if (typeof window === "undefined") return false; - return window.confirm(msg); - } - - function browserPrompt(msg) { - if (typeof window === "undefined") return NIL; - var r = window.prompt(msg); - return r === null ? NIL : r; - } - - function csrfToken() { - if (!_hasDom) return NIL; - var m = document.querySelector('meta[name="csrf-token"]'); - return m ? m.getAttribute("content") : NIL; - } - - function isCrossOrigin(url) { - try { - var h = new URL(url, location.href).hostname; - return h !== location.hostname && - (h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0); - } catch (e) { return false; } - } - - // --- Promises --- - - function promiseResolve(val) { return Promise.resolve(val); } - - function promiseThen(p, onResolve, onReject) { - if (!p || !p.then) return p; - return onReject ? p.then(onResolve, onReject) : p.then(onResolve); - } - - function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; } - - function promiseDelayed(ms, value) { - return new Promise(function(resolve) { - setTimeout(function() { resolve(value); }, ms); - }); - } - - // --- Abort controllers --- - - var _controllers = typeof WeakMap !== "undefined" ? new WeakMap() : null; - - function abortPrevious(el) { - if (_controllers) { - var prev = _controllers.get(el); - if (prev) prev.abort(); - } - } - - function trackController(el, ctrl) { - if (_controllers) _controllers.set(el, ctrl); - } - - var _targetControllers = typeof WeakMap !== "undefined" ? new WeakMap() : null; - - function abortPreviousTarget(el) { - if (_targetControllers) { - var prev = _targetControllers.get(el); - if (prev) prev.abort(); - } - } - - function trackControllerTarget(el, ctrl) { - if (_targetControllers) _targetControllers.set(el, ctrl); - } - - function newAbortController() { - return typeof AbortController !== "undefined" ? new AbortController() : { signal: null, abort: function() {} }; - } - - function controllerSignal(ctrl) { return ctrl ? ctrl.signal : null; } - - function isAbortError(err) { return err && err.name === "AbortError"; } - - // --- Timers --- - - function _wrapSxFn(fn) { - if (fn && fn._lambda) { - return function() { return trampoline(callLambda(fn, [], lambdaClosure(fn))); }; - } - return fn; - } - function setTimeout_(fn, ms) { return setTimeout(_wrapSxFn(fn), ms || 0); } - function setInterval_(fn, ms) { return setInterval(_wrapSxFn(fn), ms || 1000); } - function clearTimeout_(id) { clearTimeout(id); } - function clearInterval_(id) { clearInterval(id); } - function requestAnimationFrame_(fn) { - var cb = _wrapSxFn(fn); - if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(cb); - else setTimeout(cb, 16); - } - - // --- Fetch --- - - function fetchRequest(config, successFn, errorFn) { - var opts = { method: config.method, headers: config.headers }; - if (config.signal) opts.signal = config.signal; - if (config.body && config.method !== "GET") opts.body = config.body; - if (config["cross-origin"]) opts.credentials = "include"; - - var p = (config.preloaded && config.preloaded !== NIL) - ? Promise.resolve({ - ok: true, status: 200, - headers: new Headers({ "Content-Type": config.preloaded["content-type"] || "" }), - text: function() { return Promise.resolve(config.preloaded.text); } - }) - : fetch(config.url, opts); - - return p.then(function(resp) { - return resp.text().then(function(text) { - var getHeader = function(name) { - var v = resp.headers.get(name); - return v === null ? NIL : v; - }; - return successFn(resp.ok, resp.status, getHeader, text); - }); - }).catch(function(err) { - return errorFn(err); - }); - } - - function fetchLocation(headerVal) { - if (!_hasDom) return; - var locUrl = headerVal; - try { var obj = JSON.parse(headerVal); locUrl = obj.path || obj; } catch (e) {} - fetch(locUrl, { headers: { "SX-Request": "true" } }).then(function(r) { - return r.text().then(function(t) { - var main = document.getElementById("main-panel"); - if (main) { - main.innerHTML = t; - postSwap(main); - try { history.pushState({ sxUrl: locUrl }, "", locUrl); } catch (e) {} - } - }); - }); - } - - function fetchAndRestore(main, url, headers, scrollY) { - var opts = { headers: headers }; - try { - var h = new URL(url, location.href).hostname; - if (h !== location.hostname && - (h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0)) { - opts.credentials = "include"; - } - } catch (e) {} - - fetch(url, opts).then(function(resp) { - return resp.text().then(function(text) { - text = stripComponentScripts(text); - text = extractResponseCss(text); - text = text.trim(); - if (text.charAt(0) === "(") { - try { - var dom = sxRender(text); - var container = document.createElement("div"); - container.appendChild(dom); - processOobSwaps(container, function(t, oob, s) { - swapDomNodes(t, oob, s); - sxHydrate(t); - processElements(t); - }); - var newMain = container.querySelector("#main-panel"); - morphChildren(main, newMain || container); - postSwap(main); - if (typeof window !== "undefined") window.scrollTo(0, scrollY || 0); - } catch (err) { - console.error("sx-ref popstate error:", err); - location.reload(); - } - } else { - var parser = new DOMParser(); - var doc = parser.parseFromString(text, "text/html"); - var newMain = doc.getElementById("main-panel"); - if (newMain) { - morphChildren(main, newMain); - postSwap(main); - if (typeof window !== "undefined") window.scrollTo(0, scrollY || 0); - } else { - location.reload(); - } - } - }); - }).catch(function() { location.reload(); }); - } - - function fetchStreaming(target, url, headers) { - // Streaming fetch for multi-stream pages. - // First chunk = OOB SX swap (shell with skeletons). - // Subsequent chunks = __sxResolve script tags filling suspense slots. - var opts = { headers: headers }; - try { - var h = new URL(url, location.href).hostname; - if (h !== location.hostname && - (h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0)) { - opts.credentials = "include"; - } - } catch (e) {} - - fetch(url, opts).then(function(resp) { - if (!resp.ok || !resp.body) { - // Fallback: non-streaming - return resp.text().then(function(text) { - text = stripComponentScripts(text); - text = extractResponseCss(text); - text = text.trim(); - if (text.charAt(0) === "(") { - var dom = sxRender(text); - var container = document.createElement("div"); - container.appendChild(dom); - processOobSwaps(container, function(t, oob, s) { - swapDomNodes(t, oob, s); - sxHydrate(t); - processElements(t); - }); - var newMain = container.querySelector("#main-panel"); - morphChildren(target, newMain || container); - postSwap(target); - } - }); - } - - var reader = resp.body.getReader(); - var decoder = new TextDecoder(); - var buffer = ""; - var initialSwapDone = false; - // Regex to match __sxResolve script tags - var RESOLVE_START = ""; - - function processResolveScripts() { - // Strip and load any extra component defs before resolve scripts - buffer = stripSxScripts(buffer); - var idx; - while ((idx = buffer.indexOf(RESOLVE_START)) >= 0) { - var endIdx = buffer.indexOf(RESOLVE_END, idx); - if (endIdx < 0) break; // incomplete, wait for more data - var argsStr = buffer.substring(idx + RESOLVE_START.length, endIdx); - buffer = buffer.substring(endIdx + RESOLVE_END.length); - // argsStr is: "stream-id","sx source" - var commaIdx = argsStr.indexOf(","); - if (commaIdx >= 0) { - try { - var id = JSON.parse(argsStr.substring(0, commaIdx)); - var sx = JSON.parse(argsStr.substring(commaIdx + 1)); - if (typeof Sx !== "undefined" && Sx.resolveSuspense) { - Sx.resolveSuspense(id, sx); - } - } catch (e) { - console.error("[sx-ref] resolve parse error:", e); - } - } - } - } - - function pump() { - return reader.read().then(function(result) { - buffer += decoder.decode(result.value || new Uint8Array(), { stream: !result.done }); - - if (!initialSwapDone) { - // Look for the first resolve script — everything before it is OOB content - var scriptIdx = buffer.indexOf(" (without data-components or data-init). - // These contain extra component defs from streaming resolve chunks. - // data-init scripts are preserved for process-sx-scripts to evaluate as side effects. - var SxObj = typeof Sx !== "undefined" ? Sx : null; - return text.replace(/]*type="text\\/sx"[^>]*>[\\s\\S]*?<\\/script>/gi, - function(match) { - if (/data-init/.test(match)) return match; // preserve data-init scripts - var m = match.match(/]*>([\\s\\S]*?)<\\/script>/i); - if (m && SxObj && SxObj.loadComponents) SxObj.loadComponents(m[1]); - return ""; - }); - } - - function extractResponseCss(text) { - if (!_hasDom) return text; - var target = document.getElementById("sx-css"); - if (!target) return text; - return text.replace(/]*data-sx-css[^>]*>([\\s\\S]*?)<\\/style>/gi, - function(_, css) { target.textContent += css; return ""; }); - } - - function selectFromContainer(container, sel) { - var frag = document.createDocumentFragment(); - sel.split(",").forEach(function(s) { - container.querySelectorAll(s.trim()).forEach(function(m) { frag.appendChild(m); }); - }); - return frag; - } - - function childrenToFragment(container) { - var frag = document.createDocumentFragment(); - while (container.firstChild) frag.appendChild(container.firstChild); - return frag; - } - - function selectHtmlFromDoc(doc, sel) { - var parts = sel.split(",").map(function(s) { return s.trim(); }); - var frags = []; - parts.forEach(function(s) { - doc.querySelectorAll(s).forEach(function(m) { frags.push(m.outerHTML); }); - }); - return frags.join(""); - } - - // --- Parsing --- - - function tryParseJson(s) { - if (!s) return NIL; - try { return JSON.parse(s); } catch (e) { return NIL; } - } -""" - -PLATFORM_BOOT_JS = """ - // ========================================================================= - // Platform interface — Boot (mount, hydrate, scripts, cookies) - // ========================================================================= - - function resolveMountTarget(target) { - if (typeof target === "string") return _hasDom ? document.querySelector(target) : null; - return target; - } - - function sxRenderWithEnv(source, extraEnv) { - var env = extraEnv ? merge(componentEnv, extraEnv) : componentEnv; - var exprs = parse(source); - if (!_hasDom) return null; - var frag = document.createDocumentFragment(); - for (var i = 0; i < exprs.length; i++) { - var node = renderToDom(exprs[i], env, null); - if (node) frag.appendChild(node); - } - return frag; - } - - function getRenderEnv(extraEnv) { - return extraEnv ? merge(componentEnv, extraEnv) : componentEnv; - } - - function mergeEnvs(base, newEnv) { - return newEnv ? merge(componentEnv, base, newEnv) : merge(componentEnv, base); - } - - function sxLoadComponents(text) { - try { - var exprs = parse(text); - for (var i = 0; i < exprs.length; i++) trampoline(evalExpr(exprs[i], componentEnv)); - } catch (err) { - logParseError("loadComponents", text, err); - throw err; - } - } - - function setDocumentTitle(s) { - if (_hasDom) document.title = s || ""; - } - - function removeHeadElement(sel) { - if (!_hasDom) return; - var old = document.head.querySelector(sel); - if (old) old.parentNode.removeChild(old); - } - - function querySxScripts(root) { - if (!_hasDom) return []; - var r = (root && root !== NIL) ? root : document; - return Array.prototype.slice.call( - r.querySelectorAll('script[type="text/sx"]')); - } - - function queryPageScripts() { - if (!_hasDom) return []; - return Array.prototype.slice.call( - document.querySelectorAll('script[type="text/sx-pages"]')); - } - - // --- localStorage --- - - function localStorageGet(key) { - try { var v = localStorage.getItem(key); return v === null ? NIL : v; } - catch (e) { return NIL; } - } - - function localStorageSet(key, val) { - try { localStorage.setItem(key, val); } catch (e) {} - } - - function localStorageRemove(key) { - try { localStorage.removeItem(key); } catch (e) {} - } - - // --- Cookies --- - - function setSxCompCookie(hash) { - if (_hasDom) document.cookie = "sx-comp-hash=" + hash + ";path=/;max-age=31536000;SameSite=Lax"; - } - - function clearSxCompCookie() { - if (_hasDom) document.cookie = "sx-comp-hash=;path=/;max-age=0;SameSite=Lax"; - } - - // --- Env helpers --- - - function parseEnvAttr(el) { - var attr = el && el.getAttribute ? el.getAttribute("data-sx-env") : null; - if (!attr) return {}; - try { return JSON.parse(attr); } catch (e) { return {}; } - } - - function storeEnvAttr(el, base, newEnv) { - var merged = merge(base, newEnv); - if (el && el.setAttribute) el.setAttribute("data-sx-env", JSON.stringify(merged)); - } - - function toKebab(s) { return s.replace(/_/g, "-"); } - - // --- Logging --- - - function logInfo(msg) { - if (typeof console !== "undefined") console.log("[sx-ref] " + msg); - } - - function logWarn(msg) { - if (typeof console !== "undefined") console.warn("[sx-ref] " + msg); - } - - function logParseError(label, text, err) { - if (typeof console === "undefined") return; - var msg = err && err.message ? err.message : String(err); - var colMatch = msg.match(/col (\\d+)/); - var lineMatch = msg.match(/line (\\d+)/); - if (colMatch && text) { - var errLine = lineMatch ? parseInt(lineMatch[1]) : 1; - var errCol = parseInt(colMatch[1]); - var lines = text.split("\\n"); - var pos = 0; - for (var i = 0; i < errLine - 1 && i < lines.length; i++) pos += lines[i].length + 1; - pos += errCol; - var ws = 80; - var start = Math.max(0, pos - ws); - var end = Math.min(text.length, pos + ws); - console.error("[sx-ref] " + label + ":", msg, - "\\n around error (pos ~" + pos + "):", - "\\n \\u00ab" + text.substring(start, pos) + "\\u26d4" + text.substring(pos, end) + "\\u00bb"); - } else { - console.error("[sx-ref] " + label + ":", msg); - } - } - -""" - -def fixups_js(has_html, has_sx, has_dom, has_signals=False): - lines = [''' - // ========================================================================= - // Post-transpilation fixups - // ========================================================================= - // The reference spec's call-lambda only handles Lambda objects, but HO forms - // (map, reduce, etc.) may receive native primitives. Wrap to handle both. - var _rawCallLambda = callLambda; - callLambda = function(f, args, callerEnv) { - if (typeof f === "function") return f.apply(null, args); - return _rawCallLambda(f, args, callerEnv); - }; - - // Expose render functions as primitives so SX code can call them'''] - if has_html: - lines.append(' if (typeof renderToHtml === "function") PRIMITIVES["render-to-html"] = renderToHtml;') - if has_sx: - lines.append(' if (typeof renderToSx === "function") PRIMITIVES["render-to-sx"] = renderToSx;') - lines.append(' if (typeof aser === "function") PRIMITIVES["aser"] = aser;') - if has_dom: - lines.append(' if (typeof renderToDom === "function") PRIMITIVES["render-to-dom"] = renderToDom;') - if has_signals: - lines.append(''' - // Expose signal functions as primitives so runtime-evaluated SX code - // (e.g. island bodies from .sx files) can call them - PRIMITIVES["signal"] = signal; - PRIMITIVES["signal?"] = isSignal; - PRIMITIVES["deref"] = deref; - PRIMITIVES["reset!"] = reset_b; - PRIMITIVES["swap!"] = swap_b; - PRIMITIVES["computed"] = computed; - PRIMITIVES["effect"] = effect; - PRIMITIVES["batch"] = batch; - // Timer primitives for island code - PRIMITIVES["set-interval"] = setInterval_; - PRIMITIVES["clear-interval"] = clearInterval_; - // Reactive DOM helpers for island code - PRIMITIVES["reactive-text"] = reactiveText; - PRIMITIVES["create-text-node"] = createTextNode; - PRIMITIVES["dom-set-text-content"] = domSetTextContent; - PRIMITIVES["dom-listen"] = domListen; - PRIMITIVES["dom-dispatch"] = domDispatch; - PRIMITIVES["event-detail"] = eventDetail; - PRIMITIVES["resource"] = resource; - PRIMITIVES["promise-delayed"] = promiseDelayed; - PRIMITIVES["promise-then"] = promiseThen; - PRIMITIVES["def-store"] = defStore; - PRIMITIVES["use-store"] = useStore; - PRIMITIVES["emit-event"] = emitEvent; - PRIMITIVES["on-event"] = onEvent; - PRIMITIVES["bridge-event"] = bridgeEvent; - // DOM primitives for island code - PRIMITIVES["dom-focus"] = domFocus; - PRIMITIVES["dom-tag-name"] = domTagName; - PRIMITIVES["dom-get-prop"] = domGetProp; - PRIMITIVES["stop-propagation"] = stopPropagation_; - PRIMITIVES["error-message"] = errorMessage; - PRIMITIVES["schedule-idle"] = scheduleIdle; - PRIMITIVES["invoke"] = invoke; - PRIMITIVES["error"] = function(msg) { throw new Error(msg); }; - PRIMITIVES["filter"] = filter; - // DOM primitives for sx-on:* handlers and data-init scripts - if (typeof domBody === "function") PRIMITIVES["dom-body"] = domBody; - if (typeof domQuery === "function") PRIMITIVES["dom-query"] = domQuery; - if (typeof domQueryAll === "function") PRIMITIVES["dom-query-all"] = domQueryAll; - if (typeof domQueryById === "function") PRIMITIVES["dom-query-by-id"] = domQueryById; - if (typeof domSetAttr === "function") PRIMITIVES["dom-set-attr"] = domSetAttr; - if (typeof domGetAttr === "function") PRIMITIVES["dom-get-attr"] = domGetAttr; - if (typeof domRemoveAttr === "function") PRIMITIVES["dom-remove-attr"] = domRemoveAttr; - if (typeof domHasAttr === "function") PRIMITIVES["dom-has-attr?"] = domHasAttr; - if (typeof domAddClass === "function") PRIMITIVES["dom-add-class"] = domAddClass; - if (typeof domRemoveClass === "function") PRIMITIVES["dom-remove-class"] = domRemoveClass; - if (typeof domHasClass === "function") PRIMITIVES["dom-has-class?"] = domHasClass; - if (typeof domClosest === "function") PRIMITIVES["dom-closest"] = domClosest; - if (typeof domMatches === "function") PRIMITIVES["dom-matches?"] = domMatches; - if (typeof preventDefault_ === "function") PRIMITIVES["prevent-default"] = preventDefault_; - if (typeof elementValue === "function") PRIMITIVES["element-value"] = elementValue; - if (typeof domOuterHtml === "function") PRIMITIVES["dom-outer-html"] = domOuterHtml; - if (typeof domInnerHtml === "function") PRIMITIVES["dom-inner-html"] = domInnerHtml;''') - 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, has_signals=False): - # Parser: use compiled sxParse from parser.sx, or inline a minimal fallback - if has_parser: - parser = ''' - // Parser — compiled from parser.sx (see PLATFORM_PARSER_JS for ident char classes) - var parse = sxParse;''' - else: - parser = r''' - // Minimal fallback parser (no parser adapter) - function parse(text) { - throw new Error("Parser adapter not included — cannot parse SX source at runtime"); - }''' - - # Public API — conditional on adapters - api_lines = [parser, ''' - // ========================================================================= - // Public API - // ========================================================================= - - var componentEnv = {}; - - function loadComponents(source) { - var exprs = parse(source); - for (var i = 0; i < exprs.length; i++) { - trampoline(evalExpr(exprs[i], componentEnv)); - } - }'''] - - # render() — auto-dispatches based on available adapters - if has_html and has_dom: - api_lines.append(''' - function render(source) { - if (!_hasDom) { - var exprs = parse(source); - var parts = []; - for (var i = 0; i < exprs.length; i++) parts.push(renderToHtml(exprs[i], merge(componentEnv))); - return parts.join(""); - } - var exprs = parse(source); - var frag = document.createDocumentFragment(); - for (var i = 0; i < exprs.length; i++) frag.appendChild(renderToDom(exprs[i], merge(componentEnv), null)); - return frag; - }''') - elif has_dom: - api_lines.append(''' - function render(source) { - var exprs = parse(source); - var frag = document.createDocumentFragment(); - for (var i = 0; i < exprs.length; i++) frag.appendChild(renderToDom(exprs[i], merge(componentEnv), null)); - return frag; - }''') - elif has_html: - api_lines.append(''' - function render(source) { - var exprs = parse(source); - var parts = []; - for (var i = 0; i < exprs.length; i++) parts.push(renderToHtml(exprs[i], merge(componentEnv))); - return parts.join(""); - }''') - else: - api_lines.append(''' - function render(source) { - var exprs = parse(source); - var results = []; - for (var i = 0; i < exprs.length; i++) results.push(trampoline(evalExpr(exprs[i], merge(componentEnv)))); - return results.length === 1 ? results[0] : results; - }''') - - # renderToString helper - if has_html: - api_lines.append(''' - function renderToString(source) { - var exprs = parse(source); - var parts = []; - for (var i = 0; i < exprs.length; i++) parts.push(renderToHtml(exprs[i], merge(componentEnv))); - return parts.join(""); - }''') - - # Build Sx object - version = f"ref-2.0 ({adapter_label}, bootstrap-compiled)" - api_lines.append(f''' - var Sx = {{ - VERSION: "ref-2.0", - parse: parse, - parseAll: parse, - eval: function(expr, env) {{ return trampoline(evalExpr(expr, env || merge(componentEnv))); }}, - loadComponents: loadComponents, - render: render,{"" if has_html else ""} - {"renderToString: renderToString," if has_html else ""} - serialize: serialize, - NIL: NIL, - Symbol: Symbol, - Keyword: Keyword, - isTruthy: isSxTruthy, - isNil: isNil, - componentEnv: componentEnv,''') - - if has_html: - api_lines.append(' renderToHtml: function(expr, env) { return renderToHtml(expr, env || merge(componentEnv)); },') - if has_sx: - api_lines.append(' renderToSx: function(expr, env) { return renderToSx(expr, env || merge(componentEnv)); },') - if has_dom: - api_lines.append(' renderToDom: _hasDom ? function(expr, env, ns) { return renderToDom(expr, env || merge(componentEnv), ns || null); } : null,') - if has_engine: - api_lines.append(' parseTriggerSpec: typeof parseTriggerSpec === "function" ? parseTriggerSpec : null,') - api_lines.append(' parseTime: typeof parseTime === "function" ? parseTime : null,') - api_lines.append(' defaultTrigger: typeof defaultTrigger === "function" ? defaultTrigger : null,') - api_lines.append(' parseSwapSpec: typeof parseSwapSpec === "function" ? parseSwapSpec : null,') - api_lines.append(' parseRetrySpec: typeof parseRetrySpec === "function" ? parseRetrySpec : null,') - api_lines.append(' nextRetryMs: typeof nextRetryMs === "function" ? nextRetryMs : null,') - api_lines.append(' filterParams: typeof filterParams === "function" ? filterParams : null,') - api_lines.append(' morphNode: typeof morphNode === "function" ? morphNode : null,') - api_lines.append(' morphChildren: typeof morphChildren === "function" ? morphChildren : null,') - api_lines.append(' swapDomNodes: typeof swapDomNodes === "function" ? swapDomNodes : null,') - if has_orch: - api_lines.append(' process: typeof processElements === "function" ? processElements : null,') - api_lines.append(' executeRequest: typeof executeRequest === "function" ? executeRequest : null,') - api_lines.append(' postSwap: typeof postSwap === "function" ? postSwap : null,') - if has_boot: - api_lines.append(' processScripts: typeof processSxScripts === "function" ? processSxScripts : null,') - api_lines.append(' mount: typeof sxMount === "function" ? sxMount : null,') - api_lines.append(' hydrate: typeof sxHydrateElements === "function" ? sxHydrateElements : null,') - api_lines.append(' update: typeof sxUpdateElement === "function" ? sxUpdateElement : null,') - api_lines.append(' renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null,') - api_lines.append(' getEnv: function() { return componentEnv; },') - api_lines.append(' resolveSuspense: typeof resolveSuspense === "function" ? resolveSuspense : null,') - api_lines.append(' hydrateIslands: typeof sxHydrateIslands === "function" ? sxHydrateIslands : null,') - api_lines.append(' disposeIsland: typeof disposeIsland === "function" ? disposeIsland : null,') - api_lines.append(' init: typeof bootInit === "function" ? bootInit : null,') - elif has_orch: - api_lines.append(' init: typeof engineInit === "function" ? engineInit : null,') - if has_deps: - api_lines.append(' scanRefs: scanRefs,') - api_lines.append(' scanComponentsFromSource: scanComponentsFromSource,') - api_lines.append(' transitiveDeps: transitiveDeps,') - api_lines.append(' computeAllDeps: computeAllDeps,') - api_lines.append(' componentsNeeded: componentsNeeded,') - api_lines.append(' pageComponentBundle: pageComponentBundle,') - api_lines.append(' pageCssClasses: pageCssClasses,') - api_lines.append(' scanIoRefs: scanIoRefs,') - api_lines.append(' transitiveIoRefs: transitiveIoRefs,') - api_lines.append(' computeAllIoRefs: computeAllIoRefs,') - api_lines.append(' componentPure_p: componentPure_p,') - if has_router: - api_lines.append(' splitPathSegments: splitPathSegments,') - api_lines.append(' parseRoutePattern: parseRoutePattern,') - api_lines.append(' matchRoute: matchRoute,') - api_lines.append(' findMatchingRoute: findMatchingRoute,') - - if has_dom: - api_lines.append(' registerIo: typeof registerIoPrimitive === "function" ? registerIoPrimitive : null,') - 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(' defStore: defStore,') - api_lines.append(' useStore: useStore,') - api_lines.append(' clearStores: clearStores,') - api_lines.append(' emitEvent: emitEvent,') - api_lines.append(' onEvent: onEvent,') - api_lines.append(' bridgeEvent: bridgeEvent,') - api_lines.append(f' _version: "{version}"') - api_lines.append(' };') - api_lines.append('') - if has_orch: - api_lines.append(''' - // --- Popstate listener --- - if (typeof window !== "undefined") { - window.addEventListener("popstate", function(e) { - handlePopstate(e && e.state ? e.state.scrollY || 0 : 0); - }); - }''') - if has_boot: - api_lines.append(''' - // --- Auto-init --- - if (typeof document !== "undefined") { - var _sxInit = function() { - bootInit(); - // Process any suspense resolutions that arrived before init - if (global.__sxPending) { - for (var pi = 0; pi < global.__sxPending.length; pi++) { - resolveSuspense(global.__sxPending[pi].id, global.__sxPending[pi].sx); - } - global.__sxPending = null; - } - // Set up direct resolution for future chunks - global.__sxResolve = function(id, sx) { resolveSuspense(id, sx); }; - // Register service worker for offline data caching - if ("serviceWorker" in navigator) { - navigator.serviceWorker.register("/sx-sw.js", { scope: "/" }).then(function(reg) { - logInfo("sx:sw registered (scope: " + reg.scope + ")"); - }).catch(function(err) { - logWarn("sx:sw registration failed: " + (err && err.message ? err.message : err)); - }); - } - }; - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", _sxInit); - } else { - _sxInit(); - } - }''') - elif has_orch: - api_lines.append(''' - // --- Auto-init --- - if (typeof document !== "undefined") { - var _sxInit = function() { engineInit(); }; - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", _sxInit); - } else { - _sxInit(); - } - }''') - - api_lines.append(' if (typeof module !== "undefined" && module.exports) module.exports = Sx;') - api_lines.append(' else global.Sx = Sx;') - - return "\n".join(api_lines) - -EPILOGUE = ''' -})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);''' - - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- if __name__ == "__main__": import argparse @@ -4390,7 +44,7 @@ if __name__ == "__main__": help="Comma-separated extensions (continuations). Default: none.") p.add_argument("--spec-modules", help="Comma-separated spec modules (deps). Default: none.") - default_output = os.path.join(os.path.dirname(__file__), "..", "..", "static", "scripts", "sx-browser.js") + default_output = os.path.join(_HERE, "..", "..", "static", "scripts", "sx-browser.js") p.add_argument("--output", "-o", default=default_output, help="Output file (default: shared/static/scripts/sx-browser.js)") args = p.parse_args() diff --git a/shared/sx/ref/eval.sx b/shared/sx/ref/eval.sx index b525811..1aa55db 100644 --- a/shared/sx/ref/eval.sx +++ b/shared/sx/ref/eval.sx @@ -656,8 +656,8 @@ (let ((spliced (trampoline (eval-expr (nth item 1) env)))) (if (= (type-of spliced) "list") (concat result spliced) - (if (nil? spliced) result (append result spliced)))) - (append result (qq-expand item env)))) + (if (nil? spliced) result (concat result (list spliced))))) + (concat result (list (qq-expand item env))))) (list) template))))))) diff --git a/shared/sx/ref/js.sx b/shared/sx/ref/js.sx index 33223cf..cea6379 100644 --- a/shared/sx/ref/js.sx +++ b/shared/sx/ref/js.sx @@ -124,6 +124,8 @@ "eval-call" "evalCall" "is-render-expr?" "isRenderExpr" "render-expr" "renderExpr" + "render-active?" "renderActiveP" + "set-render-active!" "setRenderActiveB" "call-lambda" "callLambda" "call-component" "callComponent" "parse-keyword-args" "parseKeywordArgs" diff --git a/shared/sx/ref/platform_py.py b/shared/sx/ref/platform_py.py index d7d3619..fcbb7ba 100644 --- a/shared/sx/ref/platform_py.py +++ b/shared/sx/ref/platform_py.py @@ -774,7 +774,7 @@ PRIMITIVES["last"] = lambda c: c[-1] if c and _b_len(c) > 0 else NIL PRIMITIVES["rest"] = lambda c: c[1:] if c else [] PRIMITIVES["nth"] = lambda c, n: c[n] if c and 0 <= n < _b_len(c) else NIL PRIMITIVES["cons"] = lambda x, c: [x] + (c or []) -PRIMITIVES["append"] = lambda c, x: (c or []) + [x] +PRIMITIVES["append"] = lambda c, x: (c or []) + (x if isinstance(x, list) else [x]) PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)] PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)] ''', diff --git a/shared/sx/ref/run_js_sx.py b/shared/sx/ref/run_js_sx.py index 3a33efd..ebe478b 100644 --- a/shared/sx/ref/run_js_sx.py +++ b/shared/sx/ref/run_js_sx.py @@ -1,16 +1,14 @@ #!/usr/bin/env python3 """ -Bootstrap runner: execute js.sx against spec files to produce sx-ref.js. +Bootstrap compiler: js.sx (self-hosting SX-to-JS translator) → sx-browser.js. -This is the G1 bootstrapper — js.sx (SX-to-JavaScript translator written in SX) -is loaded into the Python evaluator, which then uses it to translate the -spec .sx files into JavaScript. - -The output (transpiled defines only) should be identical to what -bootstrap_js.py's JSEmitter produces. +This is the canonical JS bootstrapper. js.sx is loaded into the Python evaluator, +which uses it to translate the .sx spec files into JavaScript. Platform code +(types, primitives, DOM interface) comes from platform_js.py. Usage: - python run_js_sx.py > /tmp/sx_ref_g1.js + python run_js_sx.py # stdout + python run_js_sx.py -o shared/static/scripts/sx-browser.js # file """ from __future__ import annotations @@ -19,14 +17,32 @@ import sys _HERE = os.path.dirname(os.path.abspath(__file__)) _PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", "..")) -sys.path.insert(0, _PROJECT) +if _PROJECT not in sys.path: + sys.path.insert(0, _PROJECT) from shared.sx.parser import parse_all from shared.sx.types import Symbol +from shared.sx.ref.platform_js import ( + extract_defines, + ADAPTER_FILES, ADAPTER_DEPS, SPEC_MODULES, EXTENSION_NAMES, + PREAMBLE, PLATFORM_JS_PRE, PLATFORM_JS_POST, + PRIMITIVES_JS_MODULES, _ALL_JS_MODULES, _assemble_primitives_js, + PLATFORM_DEPS_JS, PLATFORM_PARSER_JS, PLATFORM_DOM_JS, + PLATFORM_ENGINE_PURE_JS, PLATFORM_ORCHESTRATION_JS, PLATFORM_BOOT_JS, + CONTINUATIONS_JS, ASYNC_IO_JS, + fixups_js, public_api_js, EPILOGUE, +) + + +_js_sx_env = None # cached def load_js_sx() -> dict: """Load js.sx into an evaluator environment and return it.""" + global _js_sx_env + if _js_sx_env is not None: + return _js_sx_env + js_sx_path = os.path.join(_HERE, "js.sx") with open(js_sx_path) as f: source = f.read() @@ -39,63 +55,187 @@ def load_js_sx() -> dict: for expr in exprs: evaluate(expr, env) + _js_sx_env = env return env -def extract_defines(source: str) -> list[tuple[str, list]]: - """Parse .sx source, return list of (name, define-expr) for top-level defines.""" - exprs = parse_all(source) - defines = [] - for expr in exprs: - if isinstance(expr, list) and expr and isinstance(expr[0], Symbol): - if expr[0].name == "define": - name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1]) - defines.append((name, expr)) - return defines +def compile_ref_to_js( + adapters: list[str] | None = None, + modules: list[str] | None = None, + extensions: list[str] | None = None, + spec_modules: list[str] | None = None, +) -> str: + """Compile SX spec files to JavaScript using js.sx. - -def main(): + Args: + adapters: List of adapter names to include. None = all. + modules: List of primitive module names. None = all. + extensions: List of extensions (continuations). None = none. + spec_modules: List of spec modules (deps, router, signals). None = auto. + """ + from datetime import datetime, timezone from shared.sx.evaluator import evaluate - # Load js.sx into evaluator + ref_dir = _HERE env = load_js_sx() - # Same file list and order as bootstrap_js.py compile_ref_to_js() with all adapters + # Resolve adapter set + if adapters is None: + adapter_set = set(ADAPTER_FILES.keys()) + else: + adapter_set = set() + for a in adapters: + if a not in ADAPTER_FILES: + raise ValueError(f"Unknown adapter: {a!r}. Valid: {', '.join(ADAPTER_FILES)}") + adapter_set.add(a) + for dep in ADAPTER_DEPS.get(a, []): + adapter_set.add(dep) + + # Resolve spec modules + spec_mod_set = set() + if spec_modules: + for sm in spec_modules: + if sm not in SPEC_MODULES: + raise ValueError(f"Unknown spec module: {sm!r}. Valid: {', '.join(SPEC_MODULES)}") + spec_mod_set.add(sm) + if "dom" in adapter_set and "signals" in SPEC_MODULES: + spec_mod_set.add("signals") + if "boot" in adapter_set: + spec_mod_set.add("router") + spec_mod_set.add("deps") + has_deps = "deps" in spec_mod_set + has_router = "router" in spec_mod_set + + # Resolve extensions + ext_set = set() + if extensions: + for e in extensions: + if e not in EXTENSION_NAMES: + raise ValueError(f"Unknown extension: {e!r}. Valid: {', '.join(EXTENSION_NAMES)}") + ext_set.add(e) + has_continuations = "continuations" in ext_set + + # Build file list: core + adapters + spec modules sx_files = [ ("eval.sx", "eval"), ("render.sx", "render (core)"), - ("parser.sx", "parser"), - ("adapter-html.sx", "adapter-html"), - ("adapter-sx.sx", "adapter-sx"), - ("adapter-dom.sx", "adapter-dom"), - ("engine.sx", "engine"), - ("orchestration.sx", "orchestration"), - ("boot.sx", "boot"), - ("deps.sx", "deps (component dependency analysis)"), - ("router.sx", "router (client-side route matching)"), - ("signals.sx", "signals (reactive signal runtime)"), ] + for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "boot"): + if name in adapter_set: + sx_files.append(ADAPTER_FILES[name]) + for name in sorted(spec_mod_set): + sx_files.append(SPEC_MODULES[name]) + + has_html = "html" in adapter_set + has_sx = "sx" in adapter_set + has_dom = "dom" in adapter_set + has_engine = "engine" in adapter_set + 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" + + # Platform JS blocks keyed by adapter name + adapter_platform = { + "parser": PLATFORM_PARSER_JS, + "dom": PLATFORM_DOM_JS, + "engine": PLATFORM_ENGINE_PURE_JS, + "orchestration": PLATFORM_ORCHESTRATION_JS, + "boot": PLATFORM_BOOT_JS, + } + + # Determine primitive modules + prim_modules = None + if modules is not None: + prim_modules = [m for m in _ALL_JS_MODULES if m.startswith("core.")] + for m in modules: + if m not in prim_modules: + if m not in PRIMITIVES_JS_MODULES: + raise ValueError(f"Unknown module: {m!r}. Valid: {', '.join(PRIMITIVES_JS_MODULES)}") + prim_modules.append(m) + + # Build output + parts = [] + parts.append(PREAMBLE) + parts.append(PLATFORM_JS_PRE) + parts.append('\n // =========================================================================') + parts.append(' // Primitives') + parts.append(' // =========================================================================\n') + parts.append(' var PRIMITIVES = {};') + parts.append(_assemble_primitives_js(prim_modules)) + parts.append(PLATFORM_JS_POST) + + if has_deps: + parts.append(PLATFORM_DEPS_JS) + + if has_parser: + parts.append(adapter_platform["parser"]) # Translate each spec file using js.sx for filename, label in sx_files: - filepath = os.path.join(_HERE, filename) + filepath = os.path.join(ref_dir, filename) if not os.path.exists(filepath): continue with open(filepath) as f: src = f.read() defines = extract_defines(src) - # Convert defines to SX-compatible format sx_defines = [[name, expr] for name, expr in defines] - print(f"\n // === Transpiled from {label} ===\n") + parts.append(f"\n // === Transpiled from {label} ===\n") env["_defines"] = sx_defines result = evaluate( [Symbol("js-translate-file"), Symbol("_defines")], env, ) - print(result) + parts.append(result) + + # Platform JS for selected adapters + if not has_dom: + parts.append("\n var _hasDom = false;\n") + for name in ("dom", "engine", "orchestration", "boot"): + if name in adapter_set and name in adapter_platform: + parts.append(adapter_platform[name]) + + parts.append(fixups_js(has_html, has_sx, has_dom, has_signals, has_deps)) + if has_continuations: + 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, has_signals)) + parts.append(EPILOGUE) + + build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + return "\n".join(parts).replace("BUILD_TIMESTAMP", build_ts) if __name__ == "__main__": - main() + import argparse + p = argparse.ArgumentParser(description="Bootstrap-compile SX reference spec to JavaScript via js.sx") + p.add_argument("--adapters", "-a", + help="Comma-separated adapter list (html,sx,dom,engine). Default: all") + p.add_argument("--modules", "-m", + help="Comma-separated primitive modules (core.* always included). Default: all") + p.add_argument("--extensions", + help="Comma-separated extensions (continuations). Default: none.") + p.add_argument("--spec-modules", + help="Comma-separated spec modules (deps). Default: none.") + default_output = os.path.join(_HERE, "..", "..", "static", "scripts", "sx-browser.js") + p.add_argument("--output", "-o", default=default_output, + help="Output file (default: shared/static/scripts/sx-browser.js)") + args = p.parse_args() + + adapters = args.adapters.split(",") if args.adapters else None + modules = args.modules.split(",") if args.modules else None + extensions = args.extensions.split(",") if args.extensions else None + spec_modules = args.spec_modules.split(",") if args.spec_modules else None + js = compile_ref_to_js(adapters, modules, extensions, spec_modules) + + with open(args.output, "w") as f: + f.write(js) + included = ", ".join(adapters) if adapters else "all" + mods = ", ".join(modules) if modules else "all" + ext_label = ", ".join(extensions) if extensions else "none" + print(f"Wrote {args.output} ({len(js)} bytes, adapters: {included}, modules: {mods}, extensions: {ext_label})", + file=sys.stderr) diff --git a/shared/sx/ref/sx_ref.py b/shared/sx/ref/sx_ref.py index d00d343..f5e59e7 100644 --- a/shared/sx/ref/sx_ref.py +++ b/shared/sx/ref/sx_ref.py @@ -745,7 +745,7 @@ PRIMITIVES["last"] = lambda c: c[-1] if c and _b_len(c) > 0 else NIL PRIMITIVES["rest"] = lambda c: c[1:] if c else [] PRIMITIVES["nth"] = lambda c, n: c[n] if c and 0 <= n < _b_len(c) else NIL PRIMITIVES["cons"] = lambda x, c: [x] + (c or []) -PRIMITIVES["append"] = lambda c, x: (c or []) + [x] +PRIMITIVES["append"] = lambda c, x: (c or []) + (x if isinstance(x, list) else [x]) PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)] PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)] diff --git a/shared/sx/ref/test-eval.sx b/shared/sx/ref/test-eval.sx index 17bbc00..7e8c4a8 100644 --- a/shared/sx/ref/test-eval.sx +++ b/shared/sx/ref/test-eval.sx @@ -545,9 +545,12 @@ ;; -------------------------------------------------------------------------- -;; defpage +;; Server-only tests — skip in browser (defpage, streaming functions) +;; These require forms.sx which is only loaded server-side. ;; -------------------------------------------------------------------------- +(when (get (try-call (fn () stream-chunk-id)) "ok") + (defsuite "defpage" (deftest "basic defpage returns page-def" (let ((p (defpage test-basic :path "/test" :auth :public :content (div "hello")))) @@ -716,3 +719,5 @@ :content (~chunk :val val)))) (assert-equal true (get p "stream")) (assert-true (not (nil? (get p "shell"))))))) + +) ;; end (when has-server-forms?) diff --git a/shared/sx/relations.py b/shared/sx/relations.py index f96c76a..ffca5a7 100644 --- a/shared/sx/relations.py +++ b/shared/sx/relations.py @@ -4,11 +4,14 @@ Relation registry — declarative entity relationship definitions. Relations are defined as s-expressions using ``defrelation`` and stored in a global registry. All services load the same definitions at startup via ``load_relation_registry()``. + +No evaluator dependency — defrelation forms are parsed directly from the +AST since they're just structured data (keyword args → RelationDef). """ from __future__ import annotations -from shared.sx.types import RelationDef +from shared.sx.types import Keyword, RelationDef, Symbol # --------------------------------------------------------------------------- @@ -48,6 +51,102 @@ def clear_registry() -> None: _RELATION_REGISTRY.clear() +# --------------------------------------------------------------------------- +# defrelation parsing — direct AST walk, no evaluator needed +# --------------------------------------------------------------------------- + +_VALID_CARDINALITIES = {"one-to-one", "one-to-many", "many-to-many"} +_VALID_NAV = {"submenu", "tab", "badge", "inline", "hidden"} + + +class RelationError(Exception): + """Error parsing a defrelation form.""" + pass + + +def _parse_defrelation(expr: list) -> RelationDef: + """Parse a (defrelation :name :key val ...) AST into a RelationDef.""" + if len(expr) < 2: + raise RelationError("defrelation requires a name") + + name_kw = expr[1] + if not isinstance(name_kw, Keyword): + raise RelationError( + f"defrelation name must be a keyword, got {type(name_kw).__name__}" + ) + rel_name = name_kw.name + + # Parse keyword args + kwargs: dict[str, str | None] = {} + i = 2 + while i < len(expr): + key = expr[i] + if isinstance(key, Keyword): + if i + 1 < len(expr): + val = expr[i + 1] + kwargs[key.name] = val.name if isinstance(val, Keyword) else val + i += 2 + else: + kwargs[key.name] = None + i += 1 + else: + i += 1 + + for field in ("from", "to", "cardinality"): + if field not in kwargs: + raise RelationError( + f"defrelation {rel_name} missing required :{field}" + ) + + card = kwargs["cardinality"] + if card not in _VALID_CARDINALITIES: + raise RelationError( + f"defrelation {rel_name}: invalid cardinality {card!r}, " + f"expected one of {_VALID_CARDINALITIES}" + ) + + nav = kwargs.get("nav", "hidden") + if nav not in _VALID_NAV: + raise RelationError( + f"defrelation {rel_name}: invalid nav {nav!r}, " + f"expected one of {_VALID_NAV}" + ) + + return RelationDef( + name=rel_name, + from_type=kwargs["from"], + to_type=kwargs["to"], + cardinality=card, + inverse=kwargs.get("inverse"), + nav=nav, + nav_icon=kwargs.get("nav-icon"), + nav_label=kwargs.get("nav-label"), + ) + + +def evaluate_defrelation(expr: list) -> RelationDef: + """Parse a defrelation form, register it, and return the RelationDef. + + Also handles (begin (defrelation ...) ...) wrappers. + """ + if not isinstance(expr, list) or not expr: + raise RelationError(f"Expected list expression, got {type(expr).__name__}") + + head = expr[0] + if isinstance(head, Symbol) and head.name == "begin": + result = None + for child in expr[1:]: + result = evaluate_defrelation(child) + return result + + if not (isinstance(head, Symbol) and head.name == "defrelation"): + raise RelationError(f"Expected defrelation, got {head}") + + defn = _parse_defrelation(expr) + register_relation(defn) + return defn + + # --------------------------------------------------------------------------- # Built-in relation definitions (s-expression source) # --------------------------------------------------------------------------- @@ -94,8 +193,7 @@ _BUILTIN_RELATIONS = ''' def load_relation_registry() -> None: """Parse built-in defrelation s-expressions and populate the registry.""" - from shared.sx.evaluator import evaluate from shared.sx.parser import parse tree = parse(_BUILTIN_RELATIONS) - evaluate(tree) + evaluate_defrelation(tree) diff --git a/shared/sx/tests/test_bootstrapper.py b/shared/sx/tests/test_bootstrapper.py index f2f0ba8..4fb6416 100644 --- a/shared/sx/tests/test_bootstrapper.py +++ b/shared/sx/tests/test_bootstrapper.py @@ -1,4 +1,4 @@ -"""Test bootstrapper transpilation: JSEmitter and PyEmitter.""" +"""Test bootstrapper transpilation: js.sx and py.sx.""" from __future__ import annotations import os @@ -6,49 +6,38 @@ import re import pytest from shared.sx.parser import parse, parse_all -from shared.sx.ref.bootstrap_js import ( - JSEmitter, +from shared.sx.ref.run_js_sx import compile_ref_to_js, load_js_sx +from shared.sx.ref.platform_js import ( ADAPTER_FILES, SPEC_MODULES, extract_defines, - compile_ref_to_js, ) from shared.sx.ref.bootstrap_py import PyEmitter from shared.sx.types import Symbol, Keyword -class TestJSEmitterNativeDict: - """JS bootstrapper must handle native Python dicts from {:key val} syntax.""" +class TestJsSxTranslation: + """js.sx self-hosting bootstrapper handles all SX constructs.""" - def test_simple_string_values(self): - expr = parse('{"name" "hello"}') - assert isinstance(expr, dict) - js = JSEmitter().emit(expr) - assert js == '{"name": "hello"}' + def _translate(self, sx_source: str) -> str: + """Translate a single SX expression to JS using js.sx.""" + from shared.sx.evaluator import evaluate + env = load_js_sx() + expr = parse(sx_source) + env["_def_expr"] = expr + return evaluate( + [Symbol("js-expr"), Symbol("_def_expr")], env + ) - def test_function_call_value(self): - """Dict value containing a function call must emit the call, not raw AST.""" - expr = parse('{"parsed" (parse-route-pattern (get page "path"))}') - js = JSEmitter().emit(expr) - assert "parseRoutePattern" in js - assert "Symbol" not in js - assert js == '{"parsed": parseRoutePattern(get(page, "path"))}' + def test_simple_dict(self): + result = self._translate('{"name" "hello"}') + assert '"name"' in result + assert '"hello"' in result - def test_multiple_keys(self): - expr = parse('{"a" 1 "b" (+ x 2)}') - js = JSEmitter().emit(expr) - assert '"a": 1' in js - assert '"b": (x + 2)' in js - - def test_nested_dict(self): - expr = parse('{"outer" {"inner" 42}}') - js = JSEmitter().emit(expr) - assert '{"outer": {"inner": 42}}' == js - - def test_nil_value(self): - expr = parse('{"key" nil}') - js = JSEmitter().emit(expr) - assert '"key": NIL' in js + def test_function_call_in_dict(self): + result = self._translate('{"parsed" (parse-route-pattern (get page "path"))}') + assert "parseRoutePattern" in result + assert "Symbol" not in result class TestPyEmitterNativeDict: @@ -92,11 +81,7 @@ class TestPlatformMapping: """Verify compiled JS output contains all spec-defined functions.""" def test_compiled_defines_present_in_js(self): - """Every top-level define from spec files must appear in compiled JS output. - - Catches: spec modules not included, _mangle producing wrong names for - defines, transpilation silently dropping definitions. - """ + """Every top-level define from spec files must appear in compiled JS output.""" js_output = compile_ref_to_js( spec_modules=list(SPEC_MODULES.keys()), ) @@ -117,10 +102,17 @@ class TestPlatformMapping: for name, _expr in extract_defines(open(filepath).read()): all_defs.add(name) - emitter = JSEmitter() + # Use js.sx RENAMES to map SX names → JS names + env = load_js_sx() + renames = env.get("js-renames", {}) + missing = [] for sx_name in sorted(all_defs): - js_name = emitter._mangle(sx_name) + # Check if there's an explicit rename + js_name = renames.get(sx_name) + if js_name is None: + # Auto-mangle: hyphens → camelCase, ! → _b, ? → _p + js_name = _auto_mangle(sx_name) if js_name not in defined_in_js: missing.append(f"{sx_name} → {js_name}") @@ -131,41 +123,29 @@ class TestPlatformMapping: + "\n ".join(missing) ) - def test_renames_values_are_unique(self): - """RENAMES should not map different SX names to the same JS name. - Duplicate JS names would cause one definition to silently shadow another. - """ - renames = JSEmitter.RENAMES - seen: dict[str, str] = {} - dupes = [] - for sx_name, js_name in sorted(renames.items()): - if js_name in seen: - # Allow intentional aliases (e.g. has-key? and dict-has? - # both → dictHas) - dupes.append( - f" {sx_name} → {js_name} (same as {seen[js_name]})" - ) - else: - seen[js_name] = sx_name - - # Intentional aliases — these are expected duplicates - # (e.g. has-key? and dict-has? both map to dictHas) - # Don't fail for these, just document them - # The test serves as a warning for accidental duplicates +def _auto_mangle(name: str) -> str: + """Approximate js.sx's auto-mangle for SX name → JS identifier.""" + # Remove ~, replace ?, !, -, *, > + n = name + if n.startswith("~"): + n = n[1:] + n = n.replace("?", "_p").replace("!", "_b") + n = n.replace("->", "_to_").replace(">=", "_gte").replace("<=", "_lte") + n = n.replace(">", "_gt").replace("<", "_lt") + n = n.replace("*", "_star_").replace("/", "_slash_") + # camelCase from hyphens + parts = n.split("-") + if len(parts) > 1: + n = parts[0] + "".join(p.capitalize() for p in parts[1:]) + return n class TestPrimitivesRegistration: """Functions callable from runtime-evaluated SX must be in PRIMITIVES[...].""" def test_declared_primitives_registered(self): - """Every primitive declared in primitives.sx must have a PRIMITIVES[...] entry. - - Primitives are called from runtime-evaluated SX (island bodies, user - components) via getPrimitive(). If a primitive is declared in - primitives.sx but not in PRIMITIVES[...], island code gets - "Undefined symbol" errors. - """ + """Every primitive declared in primitives.sx must have a PRIMITIVES[...] entry.""" from shared.sx.ref.boundary_parser import parse_primitives_sx declared = parse_primitives_sx() @@ -173,8 +153,7 @@ class TestPrimitivesRegistration: js_output = compile_ref_to_js() registered = set(re.findall(r'PRIMITIVES\["([^"]+)"\]', js_output)) - # Aliases — declared in primitives.sx under alternate names but - # served via canonical PRIMITIVES entries + # Aliases aliases = { "downcase": "lower", "upcase": "upper", @@ -186,7 +165,7 @@ class TestPrimitivesRegistration: if alias in declared and canonical in registered: declared = declared - {alias} - # Extension-only primitives (require continuations extension) + # Extension-only primitives extension_only = {"continuation?"} declared = declared - extension_only @@ -199,12 +178,7 @@ class TestPrimitivesRegistration: ) def test_signal_runtime_primitives_registered(self): - """Signal/reactive functions used by island bodies must be in PRIMITIVES. - - These are the reactive primitives that island SX code calls via - getPrimitive(). If any is missing, islands with reactive state fail - at runtime. - """ + """Signal/reactive functions used by island bodies must be in PRIMITIVES.""" required = { "signal", "signal?", "deref", "reset!", "swap!", "computed", "effect", "batch", "resource", diff --git a/shared/sx/tests/test_relations.py b/shared/sx/tests/test_relations.py index bdd8c0e..47816a8 100644 --- a/shared/sx/tests/test_relations.py +++ b/shared/sx/tests/test_relations.py @@ -2,11 +2,12 @@ import pytest -from shared.sx.evaluator import evaluate, EvalError from shared.sx.parser import parse from shared.sx.relations import ( + RelationError, _RELATION_REGISTRY, clear_registry, + evaluate_defrelation, get_relation, load_relation_registry, relations_from, @@ -38,7 +39,7 @@ class TestDefrelation: :nav-icon "fa fa-shopping-bag" :nav-label "markets") ''') - result = evaluate(tree) + result = evaluate_defrelation(tree) assert isinstance(result, RelationDef) assert result.name == "page->market" assert result.from_type == "page" @@ -54,7 +55,7 @@ class TestDefrelation: (defrelation :a->b :from "a" :to "b" :cardinality :one-to-one :nav :hidden) ''') - evaluate(tree) + evaluate_defrelation(tree) assert get_relation("a->b") is not None assert get_relation("a->b").cardinality == "one-to-one" @@ -64,7 +65,7 @@ class TestDefrelation: :from "page" :to "menu_node" :cardinality :one-to-one :nav :hidden) ''') - result = evaluate(tree) + result = evaluate_defrelation(tree) assert result.cardinality == "one-to-one" assert result.inverse is None assert result.nav == "hidden" @@ -79,7 +80,7 @@ class TestDefrelation: :nav-icon "fa fa-file-alt" :nav-label "events") ''') - result = evaluate(tree) + result = evaluate_defrelation(tree) assert result.cardinality == "many-to-many" def test_default_nav_is_hidden(self): @@ -87,7 +88,7 @@ class TestDefrelation: (defrelation :x->y :from "x" :to "y" :cardinality :one-to-many) ''') - result = evaluate(tree) + result = evaluate_defrelation(tree) assert result.nav == "hidden" def test_invalid_cardinality_raises(self): @@ -95,42 +96,42 @@ class TestDefrelation: (defrelation :bad :from "a" :to "b" :cardinality :wrong) ''') - with pytest.raises(EvalError, match="invalid cardinality"): - evaluate(tree) + with pytest.raises(RelationError, match="invalid cardinality"): + evaluate_defrelation(tree) def test_invalid_nav_raises(self): tree = parse(''' (defrelation :bad :from "a" :to "b" :cardinality :one-to-one :nav :bogus) ''') - with pytest.raises(EvalError, match="invalid nav"): - evaluate(tree) + with pytest.raises(RelationError, match="invalid nav"): + evaluate_defrelation(tree) def test_missing_from_raises(self): tree = parse(''' (defrelation :bad :to "b" :cardinality :one-to-one) ''') - with pytest.raises(EvalError, match="missing required :from"): - evaluate(tree) + with pytest.raises(RelationError, match="missing required :from"): + evaluate_defrelation(tree) def test_missing_to_raises(self): tree = parse(''' (defrelation :bad :from "a" :cardinality :one-to-one) ''') - with pytest.raises(EvalError, match="missing required :to"): - evaluate(tree) + with pytest.raises(RelationError, match="missing required :to"): + evaluate_defrelation(tree) def test_missing_cardinality_raises(self): tree = parse(''' (defrelation :bad :from "a" :to "b") ''') - with pytest.raises(EvalError, match="missing required :cardinality"): - evaluate(tree) + with pytest.raises(RelationError, match="missing required :cardinality"): + evaluate_defrelation(tree) def test_name_must_be_keyword(self): tree = parse('(defrelation "not-keyword" :from "a" :to "b" :cardinality :one-to-one)') - with pytest.raises(EvalError, match="must be a keyword"): - evaluate(tree) + with pytest.raises(RelationError, match="must be a keyword"): + evaluate_defrelation(tree) # --------------------------------------------------------------------------- @@ -154,7 +155,7 @@ class TestRegistry: :from "page" :to "menu_node" :cardinality :one-to-one :nav :hidden)) ''') - evaluate(tree) + evaluate_defrelation(tree) def test_get_relation(self): self._load_sample() diff --git a/sx/sxc/pages/helpers.py b/sx/sxc/pages/helpers.py index 2579d1e..102147d 100644 --- a/sx/sxc/pages/helpers.py +++ b/sx/sxc/pages/helpers.py @@ -384,57 +384,47 @@ def _self_hosting_data(ref_dir: str) -> dict: def _js_self_hosting_data(ref_dir: str) -> dict: - """Run js.sx live: load into evaluator, translate spec files, diff against G0.""" + """Run js.sx live: load into evaluator, translate all spec defines.""" import os - from shared.sx.parser import parse_all from shared.sx.types import Symbol - from shared.sx.evaluator import evaluate, make_env - from shared.sx.ref.bootstrap_js import extract_defines, JSEmitter + from shared.sx.evaluator import evaluate + from shared.sx.ref.run_js_sx import load_js_sx + from shared.sx.ref.platform_js import extract_defines try: js_sx_path = os.path.join(ref_dir, "js.sx") with open(js_sx_path, encoding="utf-8") as f: js_sx_source = f.read() - exprs = parse_all(js_sx_source) - env = make_env() - for expr in exprs: - evaluate(expr, env) - - emitter = JSEmitter() + env = load_js_sx() # All spec files all_files = sorted( f for f in os.listdir(ref_dir) if f.endswith(".sx") ) total = 0 - matched = 0 for filename in all_files: filepath = os.path.join(ref_dir, filename) with open(filepath, encoding="utf-8") as f: src = f.read() defines = extract_defines(src) for name, expr in defines: - g0_stmt = emitter.emit_statement(expr) env["_def_expr"] = expr - g1_stmt = evaluate( + evaluate( [Symbol("js-statement"), Symbol("_def_expr")], env ) total += 1 - if g0_stmt.strip() == g1_stmt.strip(): - matched += 1 - status = "identical" if matched == total else "mismatch" + status = "ok" except Exception as e: js_sx_source = f";; error loading js.sx: {e}" - matched, total = 0, 0 + total = 0 status = "error" return { "bootstrapper-not-found": None, "js-sx-source": js_sx_source, - "defines-matched": str(matched), "defines-total": str(total), "js-sx-lines": str(len(js_sx_source.splitlines())), "verification-status": status,