Phase 7a: affinity annotations + fix parser escape sequences

Add :affinity :client/:server/:auto annotations to defcomp, with
render-target function combining affinity + IO analysis. Includes
spec (eval.sx, deps.sx), tests, Python evaluator, and demo page.

Fix critical bug: Python SX parser _ESCAPE_MAP was missing \r and \0,
causing bootstrapped JS parser to treat 'r' as whitespace — breaking
all client-side SX parsing. Also add \0 to JS string emitter and
fix serializer round-tripping for \r and \0.

Reserved word escaping: bootstrappers now auto-append _ to identifiers
colliding with JS/Python reserved words (e.g. default → default_,
final → final_), so the spec never needs to avoid host language keywords.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 23:53:33 +00:00
parent 81d8e55fb0
commit a70ff2b153
19 changed files with 540 additions and 224 deletions

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-07T21:45:27Z";
var SX_VERSION = "2026-03-07T23:47:29Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -35,12 +35,13 @@
}
Lambda.prototype._lambda = true;
function Component(name, params, hasChildren, body, closure) {
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;
@@ -116,8 +117,8 @@
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) {
return new Component(name, params, hasChildren, 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);
@@ -135,6 +136,7 @@
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; }
@@ -771,18 +773,32 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var sfDefcomp = function(args, env) { return (function() {
var nameSym = first(args);
var paramsRaw = nth(args, 1);
var body = nth(args, 2);
var body = last(args);
var compName = stripPrefix(symbolName(nameSym), "~");
var parsed = parseCompParams(paramsRaw);
var params = first(parsed);
var hasChildren = nth(parsed, 1);
var affinity = defcompKwarg(args, "affinity", "auto");
return (function() {
var comp = makeComponent(compName, params, hasChildren, body, env);
var comp = makeComponent(compName, params, hasChildren, body, env, affinity);
env[symbolName(nameSym)] = comp;
return comp;
})();
})(); };
// defcomp-kwarg
var defcompKwarg = function(args, key, default_) { return (function() {
var end = (len(args) - 1);
var result = default_;
{ var _c = range(2, end, 1); for (var _i = 0; _i < _c.length; _i++) { var i = _c[_i]; if (isSxTruthy((isSxTruthy((typeOf(nth(args, i)) == "keyword")) && isSxTruthy((keywordName(nth(args, i)) == key)) && ((i + 1) < end)))) {
(function() {
var val = nth(args, (i + 1));
return (result = (isSxTruthy((typeOf(val) == "keyword")) ? keywordName(val) : val));
})();
} } }
return result;
})(); };
// parse-comp-params
var parseCompParams = function(paramsExpr) { return (function() {
var params = [];
@@ -1057,7 +1073,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var skipComment = function() { while(true) { if (isSxTruthy((isSxTruthy((pos < lenSrc)) && !isSxTruthy((nth(source, pos) == "\n"))))) { pos = (pos + 1);
continue; } else { return NIL; } } };
var skipWs = function() { while(true) { if (isSxTruthy((pos < lenSrc))) { { var ch = nth(source, pos);
if (isSxTruthy(sxOr((ch == " "), (ch == "\t"), (ch == "\n"), (ch == "\\r")))) { pos = (pos + 1);
if (isSxTruthy(sxOr((ch == " "), (ch == "\t"), (ch == "\n"), (ch == "\r")))) { pos = (pos + 1);
continue; } else if (isSxTruthy((ch == ";"))) { pos = (pos + 1);
skipComment();
continue; } else { return NIL; } } } else { return NIL; } } };
@@ -1068,7 +1084,7 @@ return (function() {
if (isSxTruthy((ch == "\""))) { pos = (pos + 1);
return NIL; } else if (isSxTruthy((ch == "\\"))) { pos = (pos + 1);
{ var esc = nth(source, pos);
buf = (String(buf) + String((isSxTruthy((esc == "n")) ? "\n" : (isSxTruthy((esc == "t")) ? "\t" : (isSxTruthy((esc == "r")) ? "\\r" : esc)))));
buf = (String(buf) + String((isSxTruthy((esc == "n")) ? "\n" : (isSxTruthy((esc == "t")) ? "\t" : (isSxTruthy((esc == "r")) ? "\r" : esc)))));
pos = (pos + 1);
continue; } } else { buf = (String(buf) + String(ch));
pos = (pos + 1);
@@ -1818,9 +1834,9 @@ return forEach(function(attr) { return (isSxTruthy(!isSxTruthy(domHasAttr(newEl,
var handleSxResponse = function(el, target, text, swapStyle, useTransition) { return (function() {
var cleaned = stripComponentScripts(text);
return (function() {
var final = extractResponseCss(cleaned);
var final_ = extractResponseCss(cleaned);
return (function() {
var trimmed = trim(final);
var trimmed = trim(final_);
return (isSxTruthy(!isSxTruthy(isEmpty(trimmed))) ? (function() {
var rendered = sxRender(trimmed);
var container = domCreateElement("div", NIL);
@@ -2265,7 +2281,7 @@ return (_styleCache = {}); };
// resolve-style
var resolveStyle = function(atoms) { return (function() {
var key = join("\\0", atoms);
var key = join("\0", atoms);
return (function() {
var cached = dictGet(_styleCache, key);
return (isSxTruthy(!isSxTruthy(isNil(cached))) ? cached : (function() {