Add provide/context/emit!/emitted — render-time dynamic scope

Four new primitives for scoped downward value passing and upward
accumulation through the render tree. Specced in .sx, bootstrapped
to Python and JS across all adapters (eval, html, sx, dom, async).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 02:58:21 +00:00
parent 41097eeef9
commit ea2b71cfa3
14 changed files with 436 additions and 18 deletions

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-13T02:18:19Z";
var SX_VERSION = "2026-03-13T02:54:01Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -87,6 +87,7 @@
SxSpread.prototype._spread = true;
var _collectBuckets = {};
var _provideStacks = {};
function isSym(x) { return x != null && x._sym === true; }
function isKw(x) { return x != null && x._kw === true; }
@@ -162,6 +163,35 @@
if (_collectBuckets[bucket]) _collectBuckets[bucket] = [];
}
function providePush(name, value) {
if (!_provideStacks[name]) _provideStacks[name] = [];
_provideStacks[name].push({value: value !== undefined ? value : NIL, emitted: []});
}
function providePop(name) {
if (_provideStacks[name] && _provideStacks[name].length) _provideStacks[name].pop();
}
function sxContext(name) {
if (_provideStacks[name] && _provideStacks[name].length) {
return _provideStacks[name][_provideStacks[name].length - 1].value;
}
if (arguments.length > 1) return arguments[1];
throw new Error("No provider for: " + name);
}
function sxEmit(name, value) {
if (_provideStacks[name] && _provideStacks[name].length) {
_provideStacks[name][_provideStacks[name].length - 1].emitted.push(value);
} else {
throw new Error("No provider for emit!: " + name);
}
return NIL;
}
function sxEmitted(name) {
if (_provideStacks[name] && _provideStacks[name].length) {
return _provideStacks[name][_provideStacks[name].length - 1].emitted.slice();
}
return [];
}
function lambdaParams(f) { return f.params; }
function lambdaBody(f) { return f.body; }
function lambdaClosure(f) { return f.closure; }
@@ -495,6 +525,12 @@
PRIMITIVES["collect!"] = sxCollect;
PRIMITIVES["collected"] = sxCollected;
PRIMITIVES["clear-collected!"] = sxClearCollected;
// provide/context/emit! — render-time dynamic scope
PRIMITIVES["provide-push!"] = providePush;
PRIMITIVES["provide-pop!"] = providePop;
PRIMITIVES["context"] = sxContext;
PRIMITIVES["emit!"] = sxEmit;
PRIMITIVES["emitted"] = sxEmitted;
function isPrimitive(name) { return name in PRIMITIVES; }
@@ -760,10 +796,10 @@
var args = rest(expr);
return (isSxTruthy(!isSxTruthy(sxOr((typeOf(head) == "symbol"), (typeOf(head) == "lambda"), (typeOf(head) == "list")))) ? map(function(x) { return trampoline(evalExpr(x, env)); }, expr) : (isSxTruthy((typeOf(head) == "symbol")) ? (function() {
var name = symbolName(head);
return (isSxTruthy((name == "if")) ? sfIf(args, env) : (isSxTruthy((name == "when")) ? sfWhen(args, env) : (isSxTruthy((name == "cond")) ? sfCond(args, env) : (isSxTruthy((name == "case")) ? sfCase(args, env) : (isSxTruthy((name == "and")) ? sfAnd(args, env) : (isSxTruthy((name == "or")) ? sfOr(args, env) : (isSxTruthy((name == "let")) ? sfLet(args, env) : (isSxTruthy((name == "let*")) ? sfLet(args, env) : (isSxTruthy((name == "letrec")) ? sfLetrec(args, env) : (isSxTruthy((name == "lambda")) ? sfLambda(args, env) : (isSxTruthy((name == "fn")) ? sfLambda(args, env) : (isSxTruthy((name == "define")) ? sfDefine(args, env) : (isSxTruthy((name == "defcomp")) ? sfDefcomp(args, env) : (isSxTruthy((name == "defisland")) ? sfDefisland(args, env) : (isSxTruthy((name == "defmacro")) ? sfDefmacro(args, env) : (isSxTruthy((name == "defstyle")) ? sfDefstyle(args, env) : (isSxTruthy((name == "defhandler")) ? sfDefhandler(args, env) : (isSxTruthy((name == "defpage")) ? sfDefpage(args, env) : (isSxTruthy((name == "defquery")) ? sfDefquery(args, env) : (isSxTruthy((name == "defaction")) ? sfDefaction(args, env) : (isSxTruthy((name == "deftype")) ? sfDeftype(args, env) : (isSxTruthy((name == "defeffect")) ? sfDefeffect(args, env) : (isSxTruthy((name == "begin")) ? sfBegin(args, env) : (isSxTruthy((name == "do")) ? sfBegin(args, env) : (isSxTruthy((name == "quote")) ? sfQuote(args, env) : (isSxTruthy((name == "quasiquote")) ? sfQuasiquote(args, env) : (isSxTruthy((name == "->")) ? sfThreadFirst(args, env) : (isSxTruthy((name == "set!")) ? sfSetBang(args, env) : (isSxTruthy((name == "reset")) ? sfReset(args, env) : (isSxTruthy((name == "shift")) ? sfShift(args, env) : (isSxTruthy((name == "dynamic-wind")) ? sfDynamicWind(args, env) : (isSxTruthy((name == "map")) ? hoMap(args, env) : (isSxTruthy((name == "map-indexed")) ? hoMapIndexed(args, env) : (isSxTruthy((name == "filter")) ? hoFilter(args, env) : (isSxTruthy((name == "reduce")) ? hoReduce(args, env) : (isSxTruthy((name == "some")) ? hoSome(args, env) : (isSxTruthy((name == "every?")) ? hoEvery(args, env) : (isSxTruthy((name == "for-each")) ? hoForEach(args, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? (function() {
return (isSxTruthy((name == "if")) ? sfIf(args, env) : (isSxTruthy((name == "when")) ? sfWhen(args, env) : (isSxTruthy((name == "cond")) ? sfCond(args, env) : (isSxTruthy((name == "case")) ? sfCase(args, env) : (isSxTruthy((name == "and")) ? sfAnd(args, env) : (isSxTruthy((name == "or")) ? sfOr(args, env) : (isSxTruthy((name == "let")) ? sfLet(args, env) : (isSxTruthy((name == "let*")) ? sfLet(args, env) : (isSxTruthy((name == "letrec")) ? sfLetrec(args, env) : (isSxTruthy((name == "lambda")) ? sfLambda(args, env) : (isSxTruthy((name == "fn")) ? sfLambda(args, env) : (isSxTruthy((name == "define")) ? sfDefine(args, env) : (isSxTruthy((name == "defcomp")) ? sfDefcomp(args, env) : (isSxTruthy((name == "defisland")) ? sfDefisland(args, env) : (isSxTruthy((name == "defmacro")) ? sfDefmacro(args, env) : (isSxTruthy((name == "defstyle")) ? sfDefstyle(args, env) : (isSxTruthy((name == "defhandler")) ? sfDefhandler(args, env) : (isSxTruthy((name == "defpage")) ? sfDefpage(args, env) : (isSxTruthy((name == "defquery")) ? sfDefquery(args, env) : (isSxTruthy((name == "defaction")) ? sfDefaction(args, env) : (isSxTruthy((name == "deftype")) ? sfDeftype(args, env) : (isSxTruthy((name == "defeffect")) ? sfDefeffect(args, env) : (isSxTruthy((name == "begin")) ? sfBegin(args, env) : (isSxTruthy((name == "do")) ? sfBegin(args, env) : (isSxTruthy((name == "quote")) ? sfQuote(args, env) : (isSxTruthy((name == "quasiquote")) ? sfQuasiquote(args, env) : (isSxTruthy((name == "->")) ? sfThreadFirst(args, env) : (isSxTruthy((name == "set!")) ? sfSetBang(args, env) : (isSxTruthy((name == "reset")) ? sfReset(args, env) : (isSxTruthy((name == "shift")) ? sfShift(args, env) : (isSxTruthy((name == "dynamic-wind")) ? sfDynamicWind(args, env) : (isSxTruthy((name == "provide")) ? sfProvide(args, env) : (isSxTruthy((name == "map")) ? hoMap(args, env) : (isSxTruthy((name == "map-indexed")) ? hoMapIndexed(args, env) : (isSxTruthy((name == "filter")) ? hoFilter(args, env) : (isSxTruthy((name == "reduce")) ? hoReduce(args, env) : (isSxTruthy((name == "some")) ? hoSome(args, env) : (isSxTruthy((name == "every?")) ? hoEvery(args, env) : (isSxTruthy((name == "for-each")) ? hoForEach(args, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? (function() {
var mac = envGet(env, name);
return makeThunk(expandMacro(mac, args, env), env);
})() : (isSxTruthy((isSxTruthy(renderActiveP()) && isRenderExpr(expr))) ? renderExpr(expr, env) : evalCall(head, args, env)))))))))))))))))))))))))))))))))))))))));
})() : (isSxTruthy((isSxTruthy(renderActiveP()) && isRenderExpr(expr))) ? renderExpr(expr, env) : evalCall(head, args, env))))))))))))))))))))))))))))))))))))))))));
})() : evalCall(head, args, env)));
})(); };
@@ -1170,6 +1206,18 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
})();
})(); };
// sf-provide
var sfProvide = function(args, env) { return (function() {
var name = trampoline(evalExpr(first(args), env));
var val = trampoline(evalExpr(nth(args, 1), env));
var bodyExprs = slice(args, 2);
var result = NIL;
providePush(name, val);
{ var _c = bodyExprs; for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; result = trampoline(evalExpr(e, env)); } }
providePop(name);
return result;
})(); };
// expand-macro
var expandMacro = function(mac, rawArgs, env) { return (function() {
var local = envMerge(macroClosure(mac), env);
@@ -1468,7 +1516,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
var renderValueToHtml = function(val, env) { return (function() { var _m = typeOf(val); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(val); if (_m == "number") return (String(val)); if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "list") return renderListToHtml(val, env); if (_m == "raw-html") return rawHtmlContent(val); if (_m == "spread") return val; return escapeHtml((String(val))); })(); };
// RENDER_HTML_FORMS
var RENDER_HTML_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "defhandler", "deftype", "defeffect", "map", "map-indexed", "filter", "for-each"];
var RENDER_HTML_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "defhandler", "deftype", "defeffect", "map", "map-indexed", "filter", "for-each", "provide"];
// render-html-form?
var isRenderHtmlForm = function(name) { return contains(RENDER_HTML_FORMS, name); };
@@ -1517,7 +1565,18 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
var f = trampoline(evalExpr(nth(expr, 1), env));
var coll = trampoline(evalExpr(nth(expr, 2), env));
return join("", filter(function(r) { return !isSxTruthy(isSpread(r)); }, map(function(item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [item], env) : renderToHtml(apply(f, [item]), env)); }, coll)));
})() : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))))))))); };
})() : (isSxTruthy((name == "provide")) ? (function() {
var provName = trampoline(evalExpr(nth(expr, 1), env));
var provVal = trampoline(evalExpr(nth(expr, 2), env));
var bodyStart = 3;
var bodyCount = (len(expr) - 3);
providePush(provName, provVal);
return (function() {
var result = (isSxTruthy((bodyCount == 1)) ? renderToHtml(nth(expr, bodyStart), env) : join("", filter(function(r) { return !isSxTruthy(isSpread(r)); }, map(function(i) { return renderToHtml(nth(expr, i), env); }, range(bodyStart, (bodyStart + bodyCount))))));
providePop(provName);
return result;
})();
})() : renderValueToHtml(trampoline(evalExpr(expr, env)), env))))))))))))); };
// render-lambda-html
var renderLambdaHtml = function(f, args, env) { return (function() {
@@ -1722,7 +1781,7 @@ return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if
})(); };
// SPECIAL_FORM_NAMES
var SPECIAL_FORM_NAMES = ["if", "when", "cond", "case", "and", "or", "let", "let*", "lambda", "fn", "define", "defcomp", "defmacro", "defstyle", "defhandler", "defpage", "defquery", "defaction", "defrelation", "begin", "do", "quote", "quasiquote", "->", "set!", "letrec", "dynamic-wind", "defisland", "deftype", "defeffect"];
var SPECIAL_FORM_NAMES = ["if", "when", "cond", "case", "and", "or", "let", "let*", "lambda", "fn", "define", "defcomp", "defmacro", "defstyle", "defhandler", "defpage", "defquery", "defaction", "defrelation", "begin", "do", "quote", "quasiquote", "->", "set!", "letrec", "dynamic-wind", "defisland", "deftype", "defeffect", "provide"];
// HO_FORM_NAMES
var HO_FORM_NAMES = ["map", "map-indexed", "filter", "reduce", "some", "every?", "for-each"];
@@ -1793,7 +1852,15 @@ return result; }, args);
return append_b(results, aser(lambdaBody(f), local));
})() : invoke(f, item)); } }
return (isSxTruthy(isEmpty(results)) ? NIL : results);
})() : (isSxTruthy((name == "defisland")) ? (trampoline(evalExpr(expr, env)), serialize(expr)) : (isSxTruthy(sxOr((name == "define"), (name == "defcomp"), (name == "defmacro"), (name == "defstyle"), (name == "defhandler"), (name == "defpage"), (name == "defquery"), (name == "defaction"), (name == "defrelation"), (name == "deftype"), (name == "defeffect"))) ? (trampoline(evalExpr(expr, env)), NIL) : trampoline(evalExpr(expr, env)))))))))))))));
})() : (isSxTruthy((name == "defisland")) ? (trampoline(evalExpr(expr, env)), serialize(expr)) : (isSxTruthy(sxOr((name == "define"), (name == "defcomp"), (name == "defmacro"), (name == "defstyle"), (name == "defhandler"), (name == "defpage"), (name == "defquery"), (name == "defaction"), (name == "defrelation"), (name == "deftype"), (name == "defeffect"))) ? (trampoline(evalExpr(expr, env)), NIL) : (isSxTruthy((name == "provide")) ? (function() {
var provName = trampoline(evalExpr(first(args), env));
var provVal = trampoline(evalExpr(nth(args, 1), env));
var result = NIL;
providePush(provName, provVal);
{ var _c = slice(args, 2); for (var _i = 0; _i < _c.length; _i++) { var body = _c[_i]; result = aser(body, env); } }
providePop(provName);
return result;
})() : trampoline(evalExpr(expr, env))))))))))))))));
})(); };
// eval-case-aser
@@ -1926,7 +1993,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
var renderDomUnknownComponent = function(name) { return error((String("Unknown component: ") + String(name))); };
// RENDER_DOM_FORMS
var RENDER_DOM_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "defhandler", "map", "map-indexed", "filter", "for-each", "portal", "error-boundary"];
var RENDER_DOM_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "defhandler", "map", "map-indexed", "filter", "for-each", "portal", "error-boundary", "provide"];
// render-dom-form?
var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); };
@@ -2060,7 +2127,15 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
return domAppend(frag, val);
})(); } }
return frag;
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns)))))))))))))); };
})() : (isSxTruthy((name == "provide")) ? (function() {
var provName = trampoline(evalExpr(nth(expr, 1), env));
var provVal = trampoline(evalExpr(nth(expr, 2), env));
var frag = createFragment();
providePush(provName, provVal);
{ var _c = range(3, len(expr)); for (var _i = 0; _i < _c.length; _i++) { var i = _c[_i]; domAppend(frag, renderToDom(nth(expr, i), env, ns)); } }
providePop(provName);
return frag;
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns))))))))))))))); };
// render-lambda-dom
var renderLambdaDom = function(f, args, env, ns) { return (function() {
@@ -6453,6 +6528,11 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
collect: sxCollect,
collected: sxCollected,
clearCollected: sxClearCollected,
providePush: providePush,
providePop: providePop,
context: sxContext,
emit: sxEmit,
emitted: sxEmitted,
_version: "ref-2.0 (boot+dom+engine+html+orchestration+parser+sx, bootstrap-compiled)"
};