Fix spread serialization in aser/async-aser wire format

Spread values from make-spread were crashing the wire format serializer:
- serialize() had no "spread" case, fell through to (str val) producing
  Python repr "<shared.sx.ref.sx_ref._Spread...>" which was treated as
  an undefined symbol
- aser-call/async-aser-call didn't handle spread children — now merges
  spread attrs as keyword args into the parent element
- aser-fragment/async-aser-fragment didn't filter spreads — now filters
  them (fragments have no parent element to merge into)
- serialize() now handles spread type: (make-spread {:key "val"})

Added 3 aser-spreads tests. All 562 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 12:20:16 +00:00
parent 427dee13f0
commit 859aad4333
6 changed files with 88 additions and 33 deletions

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-13T11:02:33Z";
var SX_VERSION = "2026-03-13T12:16:43Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -1510,7 +1510,7 @@ continue; } else { return NIL; } } };
})(); };
// sx-serialize
var sxSerialize = function(val) { return (function() { var _m = typeOf(val); if (_m == "nil") return "nil"; if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "number") return (String(val)); if (_m == "string") return (String("\"") + String(escapeString(val)) + String("\"")); if (_m == "symbol") return symbolName(val); if (_m == "keyword") return (String(":") + String(keywordName(val))); if (_m == "list") return (String("(") + String(join(" ", map(sxSerialize, val))) + String(")")); if (_m == "dict") return sxSerializeDict(val); if (_m == "sx-expr") return sxExprSource(val); return (String(val)); })(); };
var sxSerialize = function(val) { return (function() { var _m = typeOf(val); if (_m == "nil") return "nil"; if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "number") return (String(val)); if (_m == "string") return (String("\"") + String(escapeString(val)) + String("\"")); if (_m == "symbol") return symbolName(val); if (_m == "keyword") return (String(":") + String(keywordName(val))); if (_m == "list") return (String("(") + String(join(" ", map(sxSerialize, val))) + String(")")); if (_m == "dict") return sxSerializeDict(val); if (_m == "sx-expr") return sxExprSource(val); if (_m == "spread") return (String("(make-spread ") + String(sxSerializeDict(spreadAttrs(val))) + String(")")); return (String(val)); })(); };
// sx-serialize-dict
var sxSerializeDict = function(d) { return (String("{") + String(join(" ", reduce(function(acc, key) { return concat(acc, [(String(":") + String(key)), sxSerialize(dictGet(d, key))]); }, [], keys(d)))) + String("}")); };
@@ -1786,7 +1786,11 @@ return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if
})() : (function() {
var val = aser(arg, env);
if (isSxTruthy(!isSxTruthy(isNil(val)))) {
(isSxTruthy((typeOf(val) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? append_b(parts, serialize(item)) : NIL); }, val) : append_b(parts, serialize(val)));
(isSxTruthy(isSpread(val)) ? forEach(function(k) { return (function() {
var v = dictGet(spreadAttrs(val), k);
parts.push((String(":") + String(k)));
return append_b(parts, serialize(v));
})(); }, keys(spreadAttrs(val))) : (isSxTruthy((typeOf(val) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? append_b(parts, serialize(item)) : NIL); }, val) : append_b(parts, serialize(val))));
}
return (i = (i + 1));
})())); } }