Unify scoped effects: scope as general primitive, provide as sugar
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 12m54s

- Add `scope` special form to eval.sx: (scope name body...) or
  (scope name :value v body...) — general dynamic scope primitive
- `provide` becomes sugar: (provide name value body...) calls scope
- Rename provide-push!/provide-pop! to scope-push!/scope-pop! throughout
  all adapters (async, dom, html, sx) and platform implementations
- Update boundary.sx: Tier 5 now "Scoped effects" with scope-push!/
  scope-pop! as primary, provide-push!/provide-pop! as aliases
- Add scope form handling to async adapter and aser wire format
- Update sx-browser.js, sx_ref.py (bootstrapped output)
- Add scopes.sx docs page, update provide/spreads/demo docs
- Update nav-data, page-functions, docs page definitions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 17:30:34 +00:00
parent 6ca46bb295
commit 11fdd1a840
23 changed files with 869 additions and 285 deletions

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-13T15:35:20Z";
var SX_VERSION = "2026-03-13T16:48:03Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -86,8 +86,7 @@
function SxSpread(attrs) { this.attrs = attrs || {}; }
SxSpread.prototype._spread = true;
var _collectBuckets = {};
var _provideStacks = {};
var _scopeStacks = {};
function isSym(x) { return x != null && x._sym === true; }
function isKw(x) { return x != null && x._kw === true; }
@@ -151,44 +150,54 @@
function isSpread(x) { return x != null && x._spread === true; }
function spreadAttrs(s) { return s && s._spread ? s.attrs : {}; }
function sxCollect(bucket, value) {
if (!_collectBuckets[bucket]) _collectBuckets[bucket] = [];
var items = _collectBuckets[bucket];
if (items.indexOf(value) === -1) items.push(value);
function scopePush(name, value) {
if (!_scopeStacks[name]) _scopeStacks[name] = [];
_scopeStacks[name].push({value: value !== undefined ? value : NIL, emitted: [], dedup: false});
}
function sxCollected(bucket) {
return _collectBuckets[bucket] ? _collectBuckets[bucket].slice() : [];
}
function sxClearCollected(bucket) {
if (_collectBuckets[bucket]) _collectBuckets[bucket] = [];
function scopePop(name) {
if (_scopeStacks[name] && _scopeStacks[name].length) _scopeStacks[name].pop();
}
// Aliases — provide-push!/provide-pop! map to scope-push!/scope-pop!
var providePush = scopePush;
var providePop = scopePop;
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 (_scopeStacks[name] && _scopeStacks[name].length) {
return _scopeStacks[name][_scopeStacks[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);
if (_scopeStacks[name] && _scopeStacks[name].length) {
var entry = _scopeStacks[name][_scopeStacks[name].length - 1];
if (entry.dedup && entry.emitted.indexOf(value) !== -1) return NIL;
entry.emitted.push(value);
}
return NIL;
}
function sxEmitted(name) {
if (_provideStacks[name] && _provideStacks[name].length) {
return _provideStacks[name][_provideStacks[name].length - 1].emitted.slice();
if (_scopeStacks[name] && _scopeStacks[name].length) {
return _scopeStacks[name][_scopeStacks[name].length - 1].emitted.slice();
}
return [];
}
function sxCollect(bucket, value) {
if (!_scopeStacks[bucket] || !_scopeStacks[bucket].length) {
if (!_scopeStacks[bucket]) _scopeStacks[bucket] = [];
_scopeStacks[bucket].push({value: NIL, emitted: [], dedup: true});
}
var entry = _scopeStacks[bucket][_scopeStacks[bucket].length - 1];
if (entry.emitted.indexOf(value) === -1) entry.emitted.push(value);
}
function sxCollected(bucket) {
return sxEmitted(bucket);
}
function sxClearCollected(bucket) {
if (_scopeStacks[bucket] && _scopeStacks[bucket].length) {
_scopeStacks[bucket][_scopeStacks[bucket].length - 1].emitted = [];
}
}
function lambdaParams(f) { return f.params; }
function lambdaBody(f) { return f.body; }
@@ -517,14 +526,17 @@
};
// stdlib.spread — spread + collect primitives
// stdlib.spread — spread + collect + scope primitives
PRIMITIVES["make-spread"] = makeSpread;
PRIMITIVES["spread?"] = isSpread;
PRIMITIVES["spread-attrs"] = spreadAttrs;
PRIMITIVES["collect!"] = sxCollect;
PRIMITIVES["collected"] = sxCollected;
PRIMITIVES["clear-collected!"] = sxClearCollected;
// provide/context/emit! — render-time dynamic scope
// scope — unified render-time dynamic scope
PRIMITIVES["scope-push!"] = scopePush;
PRIMITIVES["scope-pop!"] = scopePop;
// provide-push!/provide-pop! — aliases for scope-push!/scope-pop!
PRIMITIVES["provide-push!"] = providePush;
PRIMITIVES["provide-pop!"] = providePop;
PRIMITIVES["context"] = sxContext;
@@ -796,10 +808,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 == "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() {
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 == "scope")) ? sfScope(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)));
})(); };
@@ -1204,6 +1216,22 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
callThunk(after, env);
return result;
})();
})(); };
// sf-scope
var sfScope = function(args, env) { return (function() {
var name = trampoline(evalExpr(first(args), env));
var rest = slice(args, 1);
var val = NIL;
var bodyExprs = NIL;
(isSxTruthy((isSxTruthy((len(rest) >= 2)) && isSxTruthy((typeOf(first(rest)) == "keyword")) && (keywordName(first(rest)) == "value"))) ? ((val = trampoline(evalExpr(nth(rest, 1), env))), (bodyExprs = slice(rest, 2))) : (bodyExprs = rest));
scopePush(name, val);
return (function() {
var result = NIL;
{ var _c = bodyExprs; for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; result = trampoline(evalExpr(e, env)); } }
scopePop(name);
return result;
})();
})(); };
// sf-provide
@@ -1212,9 +1240,9 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var val = trampoline(evalExpr(nth(args, 1), env));
var bodyExprs = slice(args, 2);
var result = NIL;
providePush(name, val);
scopePush(name, val);
{ var _c = bodyExprs; for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; result = trampoline(evalExpr(e, env)); } }
providePop(name);
scopePop(name);
return result;
})(); };
@@ -1527,7 +1555,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 (sxEmit("element-attrs", spreadAttrs(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", "provide"];
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", "scope", "provide"];
// render-html-form?
var isRenderHtmlForm = function(name) { return contains(RENDER_HTML_FORMS, name); };
@@ -1567,18 +1595,30 @@ 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("", map(function(item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [item], env) : renderToHtml(apply(f, [item]), env)); }, coll));
})() : (isSxTruthy((name == "scope")) ? (function() {
var scopeName = trampoline(evalExpr(nth(expr, 1), env));
var restArgs = slice(expr, 2);
var scopeVal = NIL;
var bodyExprs = NIL;
(isSxTruthy((isSxTruthy((len(restArgs) >= 2)) && isSxTruthy((typeOf(first(restArgs)) == "keyword")) && (keywordName(first(restArgs)) == "value"))) ? ((scopeVal = trampoline(evalExpr(nth(restArgs, 1), env))), (bodyExprs = slice(restArgs, 2))) : (bodyExprs = restArgs));
scopePush(scopeName, scopeVal);
return (function() {
var result = (isSxTruthy((len(bodyExprs) == 1)) ? renderToHtml(first(bodyExprs), env) : join("", map(function(e) { return renderToHtml(e, env); }, bodyExprs)));
scopePop(scopeName);
return result;
})();
})() : (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);
scopePush(provName, provVal);
return (function() {
var result = (isSxTruthy((bodyCount == 1)) ? renderToHtml(nth(expr, bodyStart), env) : join("", map(function(i) { return renderToHtml(nth(expr, i), env); }, range(bodyStart, (bodyStart + bodyCount)))));
providePop(provName);
scopePop(provName);
return result;
})();
})() : renderValueToHtml(trampoline(evalExpr(expr, env)), env))))))))))))); };
})() : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))))))))))); };
// render-lambda-html
var renderLambdaHtml = function(f, args, env) { return (function() {
@@ -1615,10 +1655,10 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
var attrs = first(parsed);
var children = nth(parsed, 1);
var isVoid = contains(VOID_ELEMENTS, tag);
return (isSxTruthy(isVoid) ? (String("<") + String(tag) + String(renderAttrs(attrs)) + String(" />")) : (providePush("element-attrs", NIL), (function() {
return (isSxTruthy(isVoid) ? (String("<") + String(tag) + String(renderAttrs(attrs)) + String(" />")) : (scopePush("element-attrs", NIL), (function() {
var content = join("", map(function(c) { return renderToHtml(c, env); }, children));
{ var _c = sxEmitted("element-attrs"); for (var _i = 0; _i < _c.length; _i++) { var spreadDict = _c[_i]; mergeSpreadAttrs(attrs, spreadDict); } }
providePop("element-attrs");
scopePop("element-attrs");
return (String("<") + String(tag) + String(renderAttrs(attrs)) + String(">") + String(content) + String("</") + String(tag) + String(">"));
})()));
})(); };
@@ -1639,11 +1679,11 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
})(); }, {["i"]: 0, ["skip"]: false}, args);
return (function() {
var lakeAttrs = {["data-sx-lake"]: sxOr(lakeId, "")};
providePush("element-attrs", NIL);
scopePush("element-attrs", NIL);
return (function() {
var content = join("", map(function(c) { return renderToHtml(c, env); }, children));
{ var _c = sxEmitted("element-attrs"); for (var _i = 0; _i < _c.length; _i++) { var spreadDict = _c[_i]; mergeSpreadAttrs(lakeAttrs, spreadDict); } }
providePop("element-attrs");
scopePop("element-attrs");
return (String("<") + String(lakeTag) + String(renderAttrs(lakeAttrs)) + String(">") + String(content) + String("</") + String(lakeTag) + String(">"));
})();
})();
@@ -1665,11 +1705,11 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
})(); }, {["i"]: 0, ["skip"]: false}, args);
return (function() {
var marshAttrs = {["data-sx-marsh"]: sxOr(marshId, "")};
providePush("element-attrs", NIL);
scopePush("element-attrs", NIL);
return (function() {
var content = join("", map(function(c) { return renderToHtml(c, env); }, children));
{ var _c = sxEmitted("element-attrs"); for (var _i = 0; _i < _c.length; _i++) { var spreadDict = _c[_i]; mergeSpreadAttrs(marshAttrs, spreadDict); } }
providePop("element-attrs");
scopePop("element-attrs");
return (String("<") + String(marshTag) + String(renderAttrs(marshAttrs)) + String(">") + String(content) + String("</") + String(marshTag) + String(">"));
})();
})();
@@ -1754,7 +1794,7 @@ return (function() {
var childParts = [];
var skip = false;
var i = 0;
providePush("element-attrs", NIL);
scopePush("element-attrs", NIL);
{ var _c = args; for (var _i = 0; _i < _c.length; _i++) { var arg = _c[_i]; (isSxTruthy(skip) ? ((skip = false), (i = (i + 1))) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((i + 1) < len(args)))) ? (function() {
var val = aser(nth(args, (i + 1)), env);
if (isSxTruthy(!isSxTruthy(isNil(val)))) {
@@ -1775,7 +1815,7 @@ return (function() {
attrParts.push((String(":") + String(k)));
return append_b(attrParts, serialize(v));
})(); } } } }
providePop("element-attrs");
scopePop("element-attrs");
return (function() {
var parts = concat([name], attrParts, childParts);
return (String("(") + String(join(" ", parts)) + String(")"));
@@ -1783,7 +1823,7 @@ return (function() {
})(); };
// 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", "provide"];
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", "scope", "provide"];
// HO_FORM_NAMES
var HO_FORM_NAMES = ["map", "map-indexed", "filter", "reduce", "some", "every?", "for-each"];
@@ -1854,15 +1894,28 @@ 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) : (isSxTruthy((name == "provide")) ? (function() {
})() : (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 == "scope")) ? (function() {
var scopeName = trampoline(evalExpr(first(args), env));
var restArgs = rest(args);
var scopeVal = NIL;
var bodyArgs = NIL;
(isSxTruthy((isSxTruthy((len(restArgs) >= 2)) && isSxTruthy((typeOf(first(restArgs)) == "keyword")) && (keywordName(first(restArgs)) == "value"))) ? ((scopeVal = trampoline(evalExpr(nth(restArgs, 1), env))), (bodyArgs = slice(restArgs, 2))) : (bodyArgs = restArgs));
scopePush(scopeName, scopeVal);
return (function() {
var result = NIL;
{ var _c = bodyArgs; for (var _i = 0; _i < _c.length; _i++) { var body = _c[_i]; result = aser(body, env); } }
scopePop(scopeName);
return result;
})();
})() : (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);
scopePush(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);
scopePop(provName);
return result;
})() : trampoline(evalExpr(expr, env))))))))))))))));
})() : trampoline(evalExpr(expr, env)))))))))))))))));
})(); };
// eval-case-aser
@@ -1912,7 +1965,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
var renderDomElement = function(tag, args, env, ns) { return (function() {
var newNs = (isSxTruthy((tag == "svg")) ? SVG_NS : (isSxTruthy((tag == "math")) ? MATH_NS : ns));
var el = domCreateElement(tag, newNs);
providePush("element-attrs", NIL);
scopePush("element-attrs", NIL);
reduce(function(state, arg) { return (function() {
var skip = get(state, "skip");
return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() {
@@ -1950,7 +2003,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
return domSetAttr(el, "style", (isSxTruthy((isSxTruthy(existing) && !isSxTruthy((existing == "")))) ? (String(existing) + String(";") + String(val)) : val));
})() : domSetAttr(el, key, (String(val)))));
})(); } } } }
providePop("element-attrs");
scopePop("element-attrs");
return el;
})(); };
@@ -2007,7 +2060,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", "provide"];
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", "scope", "provide"];
// render-dom-form?
var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); };
@@ -2149,15 +2202,26 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
return domAppend(frag, val);
})(); } }
return frag;
})() : (isSxTruthy((name == "scope")) ? (function() {
var scopeName = trampoline(evalExpr(nth(expr, 1), env));
var restArgs = slice(expr, 2);
var scopeVal = NIL;
var bodyExprs = NIL;
var frag = createFragment();
(isSxTruthy((isSxTruthy((len(restArgs) >= 2)) && isSxTruthy((typeOf(first(restArgs)) == "keyword")) && (keywordName(first(restArgs)) == "value"))) ? ((scopeVal = trampoline(evalExpr(nth(restArgs, 1), env))), (bodyExprs = slice(restArgs, 2))) : (bodyExprs = restArgs));
scopePush(scopeName, scopeVal);
{ var _c = bodyExprs; for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; domAppend(frag, renderToDom(e, env, ns)); } }
scopePop(scopeName);
return frag;
})() : (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);
scopePush(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);
scopePop(provName);
return frag;
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns))))))))))))))); };
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns)))))))))))))))); };
// render-lambda-dom
var renderLambdaDom = function(f, args, env, ns) { return (function() {
@@ -6622,6 +6686,8 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
collect: sxCollect,
collected: sxCollected,
clearCollected: sxClearCollected,
scopePush: scopePush,
scopePop: scopePop,
providePush: providePush,
providePop: providePop,
context: sxContext,

View File

@@ -175,14 +175,14 @@
(svg-context-set! true)
nil))
(content-parts (list)))
(provide-push! "element-attrs" nil)
(scope-push! "element-attrs" nil)
(for-each
(fn (c) (append! content-parts (async-render c env ctx)))
children)
(for-each
(fn (spread-dict) (merge-spread-attrs attrs spread-dict))
(emitted "element-attrs"))
(provide-pop! "element-attrs")
(scope-pop! "element-attrs")
(when token (svg-context-reset! token))
(str "<" tag (render-attrs attrs) ">"
(join "" content-parts)
@@ -335,7 +335,7 @@
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
"deftype" "defeffect"
"map" "map-indexed" "filter" "for-each" "provide"))
"map" "map-indexed" "filter" "for-each" "scope" "provide"))
(define async-render-form? :effects []
(fn ((name :as string))
@@ -419,17 +419,37 @@
(coll (async-eval (nth expr 2) env ctx)))
(join "" (async-map-fn-render f coll env ctx)))
;; provide — render-time dynamic scope
;; scope — unified render-time dynamic scope
(= name "scope")
(let ((scope-name (async-eval (nth expr 1) env ctx))
(rest-args (slice expr 2))
(scope-val nil)
(body-exprs nil))
;; Check for :value keyword
(if (and (>= (len rest-args) 2)
(= (type-of (first rest-args)) "keyword")
(= (keyword-name (first rest-args)) "value"))
(do (set! scope-val (async-eval (nth rest-args 1) env ctx))
(set! body-exprs (slice rest-args 2)))
(set! body-exprs rest-args))
(scope-push! scope-name scope-val)
(let ((result (if (= (len body-exprs) 1)
(async-render (first body-exprs) env ctx)
(join "" (async-map-render body-exprs env ctx)))))
(scope-pop! scope-name)
result))
;; provide — sugar for scope with value
(= name "provide")
(let ((prov-name (async-eval (nth expr 1) env ctx))
(prov-val (async-eval (nth expr 2) env ctx))
(body-start 3)
(body-count (- (len expr) 3)))
(provide-push! prov-name prov-val)
(scope-push! prov-name prov-val)
(let ((result (if (= body-count 1)
(async-render (nth expr body-start) env ctx)
(join "" (async-map-render (slice expr body-start) env ctx)))))
(provide-pop! prov-name)
(scope-pop! prov-name)
result))
;; Fallback
@@ -847,7 +867,7 @@
(skip false)
(i 0))
;; Provide scope for spread emit!
(provide-push! "element-attrs" nil)
(scope-push! "element-attrs" nil)
(for-each
(fn (arg)
(if skip
@@ -890,7 +910,7 @@
(append! attr-parts (serialize v))))
(keys spread-dict)))
(emitted "element-attrs"))
(provide-pop! "element-attrs")
(scope-pop! "element-attrs")
(when token (svg-context-reset! token))
(let ((parts (concat (list name) attr-parts child-parts)))
(make-sx-expr (str "(" (join " " parts) ")"))))))
@@ -906,7 +926,7 @@
"define" "defcomp" "defmacro" "defstyle"
"defhandler" "defpage" "defquery" "defaction"
"begin" "do" "quote" "->" "set!" "defisland"
"deftype" "defeffect" "provide"))
"deftype" "defeffect" "scope" "provide"))
(define ASYNC_ASER_HO_NAMES
(list "map" "map-indexed" "filter" "for-each"))
@@ -1044,15 +1064,35 @@
(= name "deftype") (= name "defeffect"))
(do (async-eval expr env ctx) nil)
;; provide — render-time dynamic scope
;; scope — unified render-time dynamic scope
(= name "scope")
(let ((scope-name (async-eval (first args) env ctx))
(rest-args (rest args))
(scope-val nil)
(body-args nil))
;; Check for :value keyword
(if (and (>= (len rest-args) 2)
(= (type-of (first rest-args)) "keyword")
(= (keyword-name (first rest-args)) "value"))
(do (set! scope-val (async-eval (nth rest-args 1) env ctx))
(set! body-args (slice rest-args 2)))
(set! body-args rest-args))
(scope-push! scope-name scope-val)
(let ((result nil))
(for-each (fn (body) (set! result (async-aser body env ctx)))
body-args)
(scope-pop! scope-name)
result))
;; provide — sugar for scope with value
(= name "provide")
(let ((prov-name (async-eval (first args) env ctx))
(prov-val (async-eval (nth args 1) env ctx))
(result nil))
(provide-push! prov-name prov-val)
(scope-push! prov-name prov-val)
(for-each (fn (body) (set! result (async-aser body env ctx)))
(slice args 2))
(provide-pop! prov-name)
(scope-pop! prov-name)
result)
;; Fallback

View File

@@ -181,7 +181,7 @@
(el (dom-create-element tag new-ns)))
;; Provide scope for spread emit! — deeply nested spreads emit here
(provide-push! "element-attrs" nil)
(scope-push! "element-attrs" nil)
;; Process args: keywords → attrs, others → children
(reduce
@@ -269,7 +269,7 @@
(dom-set-attr el key (str val))))))
(keys spread-dict)))
(emitted "element-attrs"))
(provide-pop! "element-attrs")
(scope-pop! "element-attrs")
el)))
@@ -381,7 +381,7 @@
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
"map" "map-indexed" "filter" "for-each" "portal"
"error-boundary" "provide"))
"error-boundary" "scope" "provide"))
(define render-dom-form? :effects []
(fn ((name :as string))
@@ -637,17 +637,39 @@
coll)
frag)
;; provide — render-time dynamic scope
;; scope — unified render-time dynamic scope
(= name "scope")
(let ((scope-name (trampoline (eval-expr (nth expr 1) env)))
(rest-args (slice expr 2))
(scope-val nil)
(body-exprs nil)
(frag (create-fragment)))
;; Check for :value keyword
(if (and (>= (len rest-args) 2)
(= (type-of (first rest-args)) "keyword")
(= (keyword-name (first rest-args)) "value"))
(do (set! scope-val (trampoline (eval-expr (nth rest-args 1) env)))
(set! body-exprs (slice rest-args 2)))
(set! body-exprs rest-args))
(scope-push! scope-name scope-val)
(for-each
(fn (e)
(dom-append frag (render-to-dom e env ns)))
body-exprs)
(scope-pop! scope-name)
frag)
;; provide — sugar for scope with value
(= name "provide")
(let ((prov-name (trampoline (eval-expr (nth expr 1) env)))
(prov-val (trampoline (eval-expr (nth expr 2) env)))
(frag (create-fragment)))
(provide-push! prov-name prov-val)
(scope-push! prov-name prov-val)
(for-each
(fn (i)
(dom-append frag (render-to-dom (nth expr i) env ns)))
(range 3 (len expr)))
(provide-pop! prov-name)
(scope-pop! prov-name)
frag)
;; Fallback

View File

@@ -56,7 +56,7 @@
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
"deftype" "defeffect"
"map" "map-indexed" "filter" "for-each" "provide"))
"map" "map-indexed" "filter" "for-each" "scope" "provide"))
(define render-html-form? :effects []
(fn ((name :as string))
@@ -229,18 +229,38 @@
(render-to-html (apply f (list item)) env)))
coll)))
;; provide — render-time dynamic scope
;; scope — unified render-time dynamic scope
(= name "scope")
(let ((scope-name (trampoline (eval-expr (nth expr 1) env)))
(rest-args (slice expr 2))
(scope-val nil)
(body-exprs nil))
;; Check for :value keyword
(if (and (>= (len rest-args) 2)
(= (type-of (first rest-args)) "keyword")
(= (keyword-name (first rest-args)) "value"))
(do (set! scope-val (trampoline (eval-expr (nth rest-args 1) env)))
(set! body-exprs (slice rest-args 2)))
(set! body-exprs rest-args))
(scope-push! scope-name scope-val)
(let ((result (if (= (len body-exprs) 1)
(render-to-html (first body-exprs) env)
(join "" (map (fn (e) (render-to-html e env)) body-exprs)))))
(scope-pop! scope-name)
result))
;; provide — sugar for scope with value
(= name "provide")
(let ((prov-name (trampoline (eval-expr (nth expr 1) env)))
(prov-val (trampoline (eval-expr (nth expr 2) env)))
(body-start 3)
(body-count (- (len expr) 3)))
(provide-push! prov-name prov-val)
(scope-push! prov-name prov-val)
(let ((result (if (= body-count 1)
(render-to-html (nth expr body-start) env)
(join "" (map (fn (i) (render-to-html (nth expr i) env))
(range body-start (+ body-start body-count)))))))
(provide-pop! prov-name)
(scope-pop! prov-name)
result))
;; Fallback
@@ -314,12 +334,12 @@
(str "<" tag (render-attrs attrs) " />")
;; Provide scope for spread emit!
(do
(provide-push! "element-attrs" nil)
(scope-push! "element-attrs" nil)
(let ((content (join "" (map (fn (c) (render-to-html c env)) children))))
(for-each
(fn (spread-dict) (merge-spread-attrs attrs spread-dict))
(emitted "element-attrs"))
(provide-pop! "element-attrs")
(scope-pop! "element-attrs")
(str "<" tag (render-attrs attrs) ">"
content
"</" tag ">")))))))
@@ -359,12 +379,12 @@
args)
;; Provide scope for spread emit!
(let ((lake-attrs (dict "data-sx-lake" (or lake-id ""))))
(provide-push! "element-attrs" nil)
(scope-push! "element-attrs" nil)
(let ((content (join "" (map (fn (c) (render-to-html c env)) children))))
(for-each
(fn (spread-dict) (merge-spread-attrs lake-attrs spread-dict))
(emitted "element-attrs"))
(provide-pop! "element-attrs")
(scope-pop! "element-attrs")
(str "<" lake-tag (render-attrs lake-attrs) ">"
content
"</" lake-tag ">"))))))
@@ -407,12 +427,12 @@
args)
;; Provide scope for spread emit!
(let ((marsh-attrs (dict "data-sx-marsh" (or marsh-id ""))))
(provide-push! "element-attrs" nil)
(scope-push! "element-attrs" nil)
(let ((content (join "" (map (fn (c) (render-to-html c env)) children))))
(for-each
(fn (spread-dict) (merge-spread-attrs marsh-attrs spread-dict))
(emitted "element-attrs"))
(provide-pop! "element-attrs")
(scope-pop! "element-attrs")
(str "<" marsh-tag (render-attrs marsh-attrs) ">"
content
"</" marsh-tag ">"))))))

View File

@@ -144,7 +144,7 @@
(skip false)
(i 0))
;; Provide scope for spread emit!
(provide-push! "element-attrs" nil)
(scope-push! "element-attrs" nil)
(for-each
(fn (arg)
(if skip
@@ -179,7 +179,7 @@
(append! attr-parts (serialize v))))
(keys spread-dict)))
(emitted "element-attrs"))
(provide-pop! "element-attrs")
(scope-pop! "element-attrs")
(let ((parts (concat (list name) attr-parts child-parts)))
(str "(" (join " " parts) ")")))))
@@ -195,7 +195,7 @@
"defhandler" "defpage" "defquery" "defaction" "defrelation"
"begin" "do" "quote" "quasiquote"
"->" "set!" "letrec" "dynamic-wind" "defisland"
"deftype" "defeffect" "provide"))
"deftype" "defeffect" "scope" "provide"))
(define HO_FORM_NAMES
(list "map" "map-indexed" "filter" "reduce"
@@ -333,15 +333,35 @@
(= name "deftype") (= name "defeffect"))
(do (trampoline (eval-expr expr env)) nil)
;; provide — render-time dynamic scope
;; scope — unified render-time dynamic scope
(= name "scope")
(let ((scope-name (trampoline (eval-expr (first args) env)))
(rest-args (rest args))
(scope-val nil)
(body-args nil))
;; Check for :value keyword
(if (and (>= (len rest-args) 2)
(= (type-of (first rest-args)) "keyword")
(= (keyword-name (first rest-args)) "value"))
(do (set! scope-val (trampoline (eval-expr (nth rest-args 1) env)))
(set! body-args (slice rest-args 2)))
(set! body-args rest-args))
(scope-push! scope-name scope-val)
(let ((result nil))
(for-each (fn (body) (set! result (aser body env)))
body-args)
(scope-pop! scope-name)
result))
;; provide — sugar for scope with value
(= name "provide")
(let ((prov-name (trampoline (eval-expr (first args) env)))
(prov-val (trampoline (eval-expr (nth args 1) env)))
(result nil))
(provide-push! prov-name prov-val)
(scope-push! prov-name prov-val)
(for-each (fn (body) (set! result (aser body env)))
(slice args 2))
(provide-pop! prov-name)
(scope-pop! prov-name)
result)
;; Everything else — evaluate normally

View File

@@ -293,6 +293,8 @@ class PyEmitter:
"collect!": "sx_collect",
"collected": "sx_collected",
"clear-collected!": "sx_clear_collected",
"scope-push!": "scope_push",
"scope-pop!": "scope_pop",
"provide-push!": "provide_push",
"provide-pop!": "provide_pop",
"context": "sx_context",

View File

@@ -374,30 +374,44 @@
;; --------------------------------------------------------------------------
;; Tier 5: Dynamic scope — render-time provide/context/emit!
;; Tier 5: Scoped effects — unified render-time dynamic scope
;;
;; `provide` is a special form (not a primitive) that creates a named scope
;; with a value and an empty accumulator. `context` reads the value from the
;; nearest enclosing provider. `emit!` appends to the accumulator, `emitted`
;; reads the accumulated values.
;; `scope` is the general primitive. `provide` is sugar for scope-with-value.
;; Both `provide` and `scope` are special forms in the evaluator.
;;
;; The platform must implement per-name stacks. Each entry has a value and
;; an emitted list. `provide-push!`/`provide-pop!` manage the stack.
;; The platform must implement per-name stacks. Each entry has a value,
;; an emitted list, and a dedup flag. `scope-push!`/`scope-pop!` manage
;; the stack. `provide-push!`/`provide-pop!` are aliases.
;;
;; `collect!`/`collected`/`clear-collected!` (Tier 4) are backed by scopes:
;; collect! lazily creates a root scope with dedup=true, then emits into it.
;; --------------------------------------------------------------------------
(declare-tier :dynamic-scope :source "eval.sx")
(declare-tier :scoped-effects :source "eval.sx")
(declare-spread-primitive "scope-push!"
:params (name value)
:returns "nil"
:effects [mutation]
:doc "Push a scope with name and value. General form — provide-push! is an alias.")
(declare-spread-primitive "scope-pop!"
:params (name)
:returns "nil"
:effects [mutation]
:doc "Pop the most recent scope for name. General form — provide-pop! is an alias.")
(declare-spread-primitive "provide-push!"
:params (name value)
:returns "nil"
:effects [mutation]
:doc "Push a provider scope with name and value (platform internal).")
:doc "Alias for scope-push!. Push a scope with name and value.")
(declare-spread-primitive "provide-pop!"
:params (name)
:returns "nil"
:effects [mutation]
:doc "Pop the most recent provider scope for name (platform internal).")
:doc "Alias for scope-pop!. Pop the most recent scope for name.")
(declare-spread-primitive "context"
:params (name &rest default)

View File

@@ -162,6 +162,7 @@
(= name "reset") (sf-reset args env)
(= name "shift") (sf-shift args env)
(= name "dynamic-wind") (sf-dynamic-wind args env)
(= name "scope") (sf-scope args env)
(= name "provide") (sf-provide args env)
;; Higher-order forms
@@ -951,11 +952,35 @@
;; --------------------------------------------------------------------------
;; 6a2. provide — render-time dynamic scope
;; 6a2. scope — unified render-time dynamic scope primitive
;; --------------------------------------------------------------------------
;;
;; (provide name value body...) — push a named scope with value and empty
;; accumulator, evaluate body, pop scope. Returns last body result.
;; (scope name body...) or (scope name :value v body...)
;; Push a named scope with optional value and empty accumulator,
;; evaluate body, pop scope. Returns last body result.
;;
;; `provide` is sugar: (provide name value body...) = (scope name :value value body...)
(define sf-scope
(fn ((args :as list) (env :as dict))
(let ((name (trampoline (eval-expr (first args) env)))
(rest (slice args 1))
(val nil)
(body-exprs nil))
;; Check for :value keyword
(if (and (>= (len rest) 2) (= (type-of (first rest)) "keyword") (= (keyword-name (first rest)) "value"))
(do (set! val (trampoline (eval-expr (nth rest 1) env)))
(set! body-exprs (slice rest 2)))
(set! body-exprs rest))
(scope-push! name val)
(let ((result nil))
(for-each (fn (e) (set! result (trampoline (eval-expr e env)))) body-exprs)
(scope-pop! name)
result))))
;; provide — sugar for scope with a value
;; (provide name value body...) → (scope name :value value body...)
(define sf-provide
(fn ((args :as list) (env :as dict))
@@ -963,9 +988,9 @@
(val (trampoline (eval-expr (nth args 1) env)))
(body-exprs (slice args 2))
(result nil))
(provide-push! name val)
(scope-push! name val)
(for-each (fn (e) (set! result (trampoline (eval-expr e env)))) body-exprs)
(provide-pop! name)
(scope-pop! name)
result)))

View File

@@ -527,6 +527,8 @@
"collect!" "sxCollect"
"collected" "sxCollected"
"clear-collected!" "sxClearCollected"
"scope-push!" "scopePush"
"scope-pop!" "scopePop"
"provide-push!" "providePush"
"provide-pop!" "providePop"
"context" "sxContext"

View File

@@ -883,8 +883,7 @@ PREAMBLE = '''\
function SxSpread(attrs) { this.attrs = attrs || {}; }
SxSpread.prototype._spread = true;
var _collectBuckets = {};
var _provideStacks = {};
var _scopeStacks = {};
function isSym(x) { return x != null && x._sym === true; }
function isKw(x) { return x != null && x._kw === true; }
@@ -1098,14 +1097,17 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
''',
"stdlib.spread": '''
// stdlib.spread — spread + collect primitives
// stdlib.spread — spread + collect + scope primitives
PRIMITIVES["make-spread"] = makeSpread;
PRIMITIVES["spread?"] = isSpread;
PRIMITIVES["spread-attrs"] = spreadAttrs;
PRIMITIVES["collect!"] = sxCollect;
PRIMITIVES["collected"] = sxCollected;
PRIMITIVES["clear-collected!"] = sxClearCollected;
// provide/context/emit! — render-time dynamic scope
// scope — unified render-time dynamic scope
PRIMITIVES["scope-push!"] = scopePush;
PRIMITIVES["scope-pop!"] = scopePop;
// provide-push!/provide-pop! — aliases for scope-push!/scope-pop!
PRIMITIVES["provide-push!"] = providePush;
PRIMITIVES["provide-pop!"] = providePop;
PRIMITIVES["context"] = sxContext;
@@ -1174,44 +1176,54 @@ PLATFORM_JS_PRE = '''
function isSpread(x) { return x != null && x._spread === true; }
function spreadAttrs(s) { return s && s._spread ? s.attrs : {}; }
function sxCollect(bucket, value) {
if (!_collectBuckets[bucket]) _collectBuckets[bucket] = [];
var items = _collectBuckets[bucket];
if (items.indexOf(value) === -1) items.push(value);
function scopePush(name, value) {
if (!_scopeStacks[name]) _scopeStacks[name] = [];
_scopeStacks[name].push({value: value !== undefined ? value : NIL, emitted: [], dedup: false});
}
function sxCollected(bucket) {
return _collectBuckets[bucket] ? _collectBuckets[bucket].slice() : [];
}
function sxClearCollected(bucket) {
if (_collectBuckets[bucket]) _collectBuckets[bucket] = [];
function scopePop(name) {
if (_scopeStacks[name] && _scopeStacks[name].length) _scopeStacks[name].pop();
}
// Aliases — provide-push!/provide-pop! map to scope-push!/scope-pop!
var providePush = scopePush;
var providePop = scopePop;
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 (_scopeStacks[name] && _scopeStacks[name].length) {
return _scopeStacks[name][_scopeStacks[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);
if (_scopeStacks[name] && _scopeStacks[name].length) {
var entry = _scopeStacks[name][_scopeStacks[name].length - 1];
if (entry.dedup && entry.emitted.indexOf(value) !== -1) return NIL;
entry.emitted.push(value);
}
return NIL;
}
function sxEmitted(name) {
if (_provideStacks[name] && _provideStacks[name].length) {
return _provideStacks[name][_provideStacks[name].length - 1].emitted.slice();
if (_scopeStacks[name] && _scopeStacks[name].length) {
return _scopeStacks[name][_scopeStacks[name].length - 1].emitted.slice();
}
return [];
}
function sxCollect(bucket, value) {
if (!_scopeStacks[bucket] || !_scopeStacks[bucket].length) {
if (!_scopeStacks[bucket]) _scopeStacks[bucket] = [];
_scopeStacks[bucket].push({value: NIL, emitted: [], dedup: true});
}
var entry = _scopeStacks[bucket][_scopeStacks[bucket].length - 1];
if (entry.emitted.indexOf(value) === -1) entry.emitted.push(value);
}
function sxCollected(bucket) {
return sxEmitted(bucket);
}
function sxClearCollected(bucket) {
if (_scopeStacks[bucket] && _scopeStacks[bucket].length) {
_scopeStacks[bucket][_scopeStacks[bucket].length - 1].emitted = [];
}
}
function lambdaParams(f) { return f.params; }
function lambdaBody(f) { return f.body; }
@@ -3244,6 +3256,8 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has
api_lines.append(' collect: sxCollect,')
api_lines.append(' collected: sxCollected,')
api_lines.append(' clearCollected: sxClearCollected,')
api_lines.append(' scopePush: scopePush,')
api_lines.append(' scopePop: scopePop,')
api_lines.append(' providePush: providePush,')
api_lines.append(' providePop: providePop,')
api_lines.append(' context: sxContext,')

View File

@@ -91,51 +91,56 @@ class _Spread:
self.attrs = dict(attrs) if attrs else {}
# Render-time accumulator buckets (per render pass)
_collect_buckets: dict[str, list] = {}
# Unified scope stacks — backing store for provide/context/emit!/collect!
# Each entry: {"value": v, "emitted": [], "dedup": bool}
_scope_stacks: dict[str, list[dict]] = {}
def _collect_reset():
"""Reset all collect buckets (call at start of each render pass)."""
global _collect_buckets
_collect_buckets = {}
"""Reset all scope stacks (call at start of each render pass)."""
global _scope_stacks
_scope_stacks = {}
# Render-time dynamic scope stacks (provide/context/emit!)
_provide_stacks: dict[str, list[dict]] = {}
def scope_push(name, value=None):
"""Push a scope with name, value, and empty accumulator."""
_scope_stacks.setdefault(name, []).append({"value": value, "emitted": [], "dedup": False})
def provide_push(name, value=None):
"""Push a provider scope with name, value, and empty emitted list."""
_provide_stacks.setdefault(name, []).append({"value": value, "emitted": []})
def scope_pop(name):
"""Pop the most recent scope for name."""
if name in _scope_stacks and _scope_stacks[name]:
_scope_stacks[name].pop()
def provide_pop(name):
"""Pop the most recent provider scope for name."""
if name in _provide_stacks and _provide_stacks[name]:
_provide_stacks[name].pop()
# Aliases — provide-push!/provide-pop! map to scope-push!/scope-pop!
provide_push = scope_push
provide_pop = scope_pop
def sx_context(name, *default):
"""Read value from nearest enclosing provider. Error if no provider and no default."""
if name in _provide_stacks and _provide_stacks[name]:
return _provide_stacks[name][-1]["value"]
"""Read value from nearest enclosing scope. Error if no scope and no default."""
if name in _scope_stacks and _scope_stacks[name]:
return _scope_stacks[name][-1]["value"]
if default:
return default[0]
raise RuntimeError(f"No provider for: {name}")
def sx_emit(name, value):
"""Append value to nearest enclosing provider's accumulator. No-op if no provider."""
if name in _provide_stacks and _provide_stacks[name]:
_provide_stacks[name][-1]["emitted"].append(value)
"""Append value to nearest enclosing scope's accumulator. Respects dedup flag."""
if name in _scope_stacks and _scope_stacks[name]:
entry = _scope_stacks[name][-1]
if entry["dedup"] and value in entry["emitted"]:
return NIL
entry["emitted"].append(value)
return NIL
def sx_emitted(name):
"""Return list of values emitted into nearest matching provider."""
if name in _provide_stacks and _provide_stacks[name]:
return list(_provide_stacks[name][-1]["emitted"])
"""Return list of values emitted into nearest matching scope."""
if name in _scope_stacks and _scope_stacks[name]:
return list(_scope_stacks[name][-1]["emitted"])
return []
@@ -340,23 +345,23 @@ def spread_attrs(s):
def sx_collect(bucket, value):
"""Add value to named render-time accumulator (deduplicated)."""
if bucket not in _collect_buckets:
_collect_buckets[bucket] = []
items = _collect_buckets[bucket]
if value not in items:
items.append(value)
"""Add value to named scope accumulator (deduplicated). Lazily creates root scope."""
if bucket not in _scope_stacks or not _scope_stacks[bucket]:
_scope_stacks.setdefault(bucket, []).append({"value": None, "emitted": [], "dedup": True})
entry = _scope_stacks[bucket][-1]
if value not in entry["emitted"]:
entry["emitted"].append(value)
def sx_collected(bucket):
"""Return all values in named render-time accumulator."""
return list(_collect_buckets.get(bucket, []))
"""Return all values collected in named scope accumulator."""
return sx_emitted(bucket)
def sx_clear_collected(bucket):
"""Clear a named render-time accumulator bucket."""
if bucket in _collect_buckets:
_collect_buckets[bucket] = []
"""Clear nearest scope's accumulator for name."""
if bucket in _scope_stacks and _scope_stacks[bucket]:
_scope_stacks[bucket][-1]["emitted"] = []
def lambda_params(f):
@@ -974,14 +979,17 @@ PRIMITIVES["assert"] = lambda cond, msg="Assertion failed": (_ for _ in ()).thro
''',
"stdlib.spread": '''
# stdlib.spread — spread + collect primitives
# stdlib.spread — spread + collect + scope primitives
PRIMITIVES["make-spread"] = make_spread
PRIMITIVES["spread?"] = is_spread
PRIMITIVES["spread-attrs"] = spread_attrs
PRIMITIVES["collect!"] = sx_collect
PRIMITIVES["collected"] = sx_collected
PRIMITIVES["clear-collected!"] = sx_clear_collected
# provide/context/emit! — render-time dynamic scope
# scope — unified render-time dynamic scope
PRIMITIVES["scope-push!"] = scope_push
PRIMITIVES["scope-pop!"] = scope_pop
# provide-push!/provide-pop! — aliases for scope-push!/scope-pop!
PRIMITIVES["provide-push!"] = provide_push
PRIMITIVES["provide-pop!"] = provide_pop
PRIMITIVES["context"] = sx_context

View File

@@ -252,6 +252,8 @@
"collect!" "sx_collect"
"collected" "sx_collected"
"clear-collected!" "sx_clear_collected"
"scope-push!" "scope_push"
"scope-pop!" "scope_pop"
"provide-push!" "provide_push"
"provide-pop!" "provide_pop"
"context" "sx_context"

View File

@@ -269,11 +269,13 @@
;; (collected bucket) → list
;; (clear-collected! bucket) → void
;;
;; Dynamic scope (provide/context/emit!):
;; (provide-push! name val) → void
;; (provide-pop! name) → void
;; (context name &rest def) → value from nearest provider
;; (emit! name value) → void (append to provider accumulator)
;; Scoped effects (scope/provide/context/emit!):
;; (scope-push! name val) → void (general form)
;; (scope-pop! name) → void (general form)
;; (provide-push! name val) → alias for scope-push!
;; (provide-pop! name) → alias for scope-pop!
;; (context name &rest def) → value from nearest scope
;; (emit! name value) → void (append to scope accumulator)
;; (emitted name) → list of emitted values
;;
;; From parser.sx:

View File

@@ -50,51 +50,56 @@ class _Spread:
self.attrs = dict(attrs) if attrs else {}
# Render-time accumulator buckets (per render pass)
_collect_buckets: dict[str, list] = {}
# Unified scope stacks — backing store for provide/context/emit!/collect!
# Each entry: {"value": v, "emitted": [], "dedup": bool}
_scope_stacks: dict[str, list[dict]] = {}
def _collect_reset():
"""Reset all collect buckets (call at start of each render pass)."""
global _collect_buckets
_collect_buckets = {}
"""Reset all scope stacks (call at start of each render pass)."""
global _scope_stacks
_scope_stacks = {}
# Render-time dynamic scope stacks (provide/context/emit!)
_provide_stacks: dict[str, list[dict]] = {}
def scope_push(name, value=None):
"""Push a scope with name, value, and empty accumulator."""
_scope_stacks.setdefault(name, []).append({"value": value, "emitted": [], "dedup": False})
def provide_push(name, value=None):
"""Push a provider scope with name, value, and empty emitted list."""
_provide_stacks.setdefault(name, []).append({"value": value, "emitted": []})
def scope_pop(name):
"""Pop the most recent scope for name."""
if name in _scope_stacks and _scope_stacks[name]:
_scope_stacks[name].pop()
def provide_pop(name):
"""Pop the most recent provider scope for name."""
if name in _provide_stacks and _provide_stacks[name]:
_provide_stacks[name].pop()
# Aliases — provide-push!/provide-pop! map to scope-push!/scope-pop!
provide_push = scope_push
provide_pop = scope_pop
def sx_context(name, *default):
"""Read value from nearest enclosing provider. Error if no provider and no default."""
if name in _provide_stacks and _provide_stacks[name]:
return _provide_stacks[name][-1]["value"]
"""Read value from nearest enclosing scope. Error if no scope and no default."""
if name in _scope_stacks and _scope_stacks[name]:
return _scope_stacks[name][-1]["value"]
if default:
return default[0]
raise RuntimeError(f"No provider for: {name}")
def sx_emit(name, value):
"""Append value to nearest enclosing provider's accumulator. No-op if no provider."""
if name in _provide_stacks and _provide_stacks[name]:
_provide_stacks[name][-1]["emitted"].append(value)
"""Append value to nearest enclosing scope's accumulator. Respects dedup flag."""
if name in _scope_stacks and _scope_stacks[name]:
entry = _scope_stacks[name][-1]
if entry["dedup"] and value in entry["emitted"]:
return NIL
entry["emitted"].append(value)
return NIL
def sx_emitted(name):
"""Return list of values emitted into nearest matching provider."""
if name in _provide_stacks and _provide_stacks[name]:
return list(_provide_stacks[name][-1]["emitted"])
"""Return list of values emitted into nearest matching scope."""
if name in _scope_stacks and _scope_stacks[name]:
return list(_scope_stacks[name][-1]["emitted"])
return []
@@ -299,23 +304,23 @@ def spread_attrs(s):
def sx_collect(bucket, value):
"""Add value to named render-time accumulator (deduplicated)."""
if bucket not in _collect_buckets:
_collect_buckets[bucket] = []
items = _collect_buckets[bucket]
if value not in items:
items.append(value)
"""Add value to named scope accumulator (deduplicated). Lazily creates root scope."""
if bucket not in _scope_stacks or not _scope_stacks[bucket]:
_scope_stacks.setdefault(bucket, []).append({"value": None, "emitted": [], "dedup": True})
entry = _scope_stacks[bucket][-1]
if value not in entry["emitted"]:
entry["emitted"].append(value)
def sx_collected(bucket):
"""Return all values in named render-time accumulator."""
return list(_collect_buckets.get(bucket, []))
"""Return all values collected in named scope accumulator."""
return sx_emitted(bucket)
def sx_clear_collected(bucket):
"""Clear a named render-time accumulator bucket."""
if bucket in _collect_buckets:
_collect_buckets[bucket] = []
"""Clear nearest scope's accumulator for name."""
if bucket in _scope_stacks and _scope_stacks[bucket]:
_scope_stacks[bucket][-1]["emitted"] = []
def lambda_params(f):
@@ -937,14 +942,17 @@ def _strip_tags(s):
PRIMITIVES["assert"] = lambda cond, msg="Assertion failed": (_ for _ in ()).throw(RuntimeError(f"Assertion error: {msg}")) if not sx_truthy(cond) else True
# stdlib.spread — spread + collect primitives
# stdlib.spread — spread + collect + scope primitives
PRIMITIVES["make-spread"] = make_spread
PRIMITIVES["spread?"] = is_spread
PRIMITIVES["spread-attrs"] = spread_attrs
PRIMITIVES["collect!"] = sx_collect
PRIMITIVES["collected"] = sx_collected
PRIMITIVES["clear-collected!"] = sx_clear_collected
# provide/context/emit! — render-time dynamic scope
# scope — unified render-time dynamic scope
PRIMITIVES["scope-push!"] = scope_push
PRIMITIVES["scope-pop!"] = scope_pop
# provide-push!/provide-pop! — aliases for scope-push!/scope-pop!
PRIMITIVES["provide-push!"] = provide_push
PRIMITIVES["provide-pop!"] = provide_pop
PRIMITIVES["context"] = sx_context
@@ -1394,6 +1402,8 @@ def eval_list(expr, env):
return sf_shift(args, env)
elif sx_truthy((name == 'dynamic-wind')):
return sf_dynamic_wind(args, env)
elif sx_truthy((name == 'scope')):
return sf_scope(args, env)
elif sx_truthy((name == 'provide')):
return sf_provide(args, env)
elif sx_truthy((name == 'map')):
@@ -1887,6 +1897,25 @@ def sf_dynamic_wind(args, env):
call_thunk(after, env)
return result
# sf-scope
def sf_scope(args, env):
_cells = {}
name = trampoline(eval_expr(first(args), env))
rest = slice(args, 1)
val = NIL
body_exprs = NIL
if sx_truthy(((len(rest) >= 2) if not sx_truthy((len(rest) >= 2)) else ((type_of(first(rest)) == 'keyword') if not sx_truthy((type_of(first(rest)) == 'keyword')) else (keyword_name(first(rest)) == 'value')))):
val = trampoline(eval_expr(nth(rest, 1), env))
body_exprs = slice(rest, 2)
else:
body_exprs = rest
scope_push(name, val)
_cells['result'] = NIL
for e in body_exprs:
_cells['result'] = trampoline(eval_expr(e, env))
scope_pop(name)
return _cells['result']
# sf-provide
def sf_provide(args, env):
_cells = {}
@@ -1894,10 +1923,10 @@ def sf_provide(args, env):
val = trampoline(eval_expr(nth(args, 1), env))
body_exprs = slice(args, 2)
_cells['result'] = NIL
provide_push(name, val)
scope_push(name, val)
for e in body_exprs:
_cells['result'] = trampoline(eval_expr(e, env))
provide_pop(name)
scope_pop(name)
return _cells['result']
# expand-macro
@@ -2251,7 +2280,7 @@ def render_value_to_html(val, env):
return escape_html(sx_str(val))
# RENDER_HTML_FORMS
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_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'deftype', 'defeffect', 'map', 'map-indexed', 'filter', 'for-each', 'scope', 'provide']
# render-html-form?
def is_render_html_form(name):
@@ -2350,14 +2379,28 @@ def dispatch_html_form(name, expr, env):
f = trampoline(eval_expr(nth(expr, 1), env))
coll = trampoline(eval_expr(nth(expr, 2), env))
return join('', map(lambda item: (render_lambda_html(f, [item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [item]), env)), coll))
elif sx_truthy((name == 'scope')):
scope_name = trampoline(eval_expr(nth(expr, 1), env))
rest_args = slice(expr, 2)
scope_val = NIL
body_exprs = NIL
if sx_truthy(((len(rest_args) >= 2) if not sx_truthy((len(rest_args) >= 2)) else ((type_of(first(rest_args)) == 'keyword') if not sx_truthy((type_of(first(rest_args)) == 'keyword')) else (keyword_name(first(rest_args)) == 'value')))):
scope_val = trampoline(eval_expr(nth(rest_args, 1), env))
body_exprs = slice(rest_args, 2)
else:
body_exprs = rest_args
scope_push(scope_name, scope_val)
result = (render_to_html(first(body_exprs), env) if sx_truthy((len(body_exprs) == 1)) else join('', map(lambda e: render_to_html(e, env), body_exprs)))
scope_pop(scope_name)
return result
elif sx_truthy((name == 'provide')):
prov_name = trampoline(eval_expr(nth(expr, 1), env))
prov_val = trampoline(eval_expr(nth(expr, 2), env))
body_start = 3
body_count = (len(expr) - 3)
provide_push(prov_name, prov_val)
scope_push(prov_name, prov_val)
result = (render_to_html(nth(expr, body_start), env) if sx_truthy((body_count == 1)) else join('', map(lambda i: render_to_html(nth(expr, i), env), range(body_start, (body_start + body_count)))))
provide_pop(prov_name)
scope_pop(prov_name)
return result
else:
return render_value_to_html(trampoline(eval_expr(expr, env)), env)
@@ -2389,11 +2432,11 @@ def render_html_element(tag, args, env):
if sx_truthy(is_void):
return sx_str('<', tag, render_attrs(attrs), ' />')
else:
provide_push('element-attrs', NIL)
scope_push('element-attrs', NIL)
content = join('', map(lambda c: render_to_html(c, env), children))
for spread_dict in sx_emitted('element-attrs'):
merge_spread_attrs(attrs, spread_dict)
provide_pop('element-attrs')
scope_pop('element-attrs')
return sx_str('<', tag, render_attrs(attrs), '>', content, '</', tag, '>')
# render-html-lake
@@ -2404,11 +2447,11 @@ def render_html_lake(args, env):
children = []
reduce(lambda state, arg: (lambda skip: (assoc(state, 'skip', False, 'i', (get(state, 'i') + 1)) if sx_truthy(skip) else ((lambda kname: (lambda kval: _sx_begin((_sx_cell_set(_cells, 'lake_id', kval) if sx_truthy((kname == 'id')) else (_sx_cell_set(_cells, 'lake_tag', kval) if sx_truthy((kname == 'tag')) else NIL)), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(trampoline(eval_expr(nth(args, (get(state, 'i') + 1)), env))))(keyword_name(arg)) if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((get(state, 'i') + 1) < len(args)))) else _sx_begin(_sx_append(children, arg), assoc(state, 'i', (get(state, 'i') + 1))))))(get(state, 'skip')), {'i': 0, 'skip': False}, args)
lake_attrs = {'data-sx-lake': (_cells['lake_id'] if sx_truthy(_cells['lake_id']) else '')}
provide_push('element-attrs', NIL)
scope_push('element-attrs', NIL)
content = join('', map(lambda c: render_to_html(c, env), children))
for spread_dict in sx_emitted('element-attrs'):
merge_spread_attrs(lake_attrs, spread_dict)
provide_pop('element-attrs')
scope_pop('element-attrs')
return sx_str('<', _cells['lake_tag'], render_attrs(lake_attrs), '>', content, '</', _cells['lake_tag'], '>')
# render-html-marsh
@@ -2419,11 +2462,11 @@ def render_html_marsh(args, env):
children = []
reduce(lambda state, arg: (lambda skip: (assoc(state, 'skip', False, 'i', (get(state, 'i') + 1)) if sx_truthy(skip) else ((lambda kname: (lambda kval: _sx_begin((_sx_cell_set(_cells, 'marsh_id', kval) if sx_truthy((kname == 'id')) else (_sx_cell_set(_cells, 'marsh_tag', kval) if sx_truthy((kname == 'tag')) else (NIL if sx_truthy((kname == 'transform')) else NIL))), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(trampoline(eval_expr(nth(args, (get(state, 'i') + 1)), env))))(keyword_name(arg)) if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((get(state, 'i') + 1) < len(args)))) else _sx_begin(_sx_append(children, arg), assoc(state, 'i', (get(state, 'i') + 1))))))(get(state, 'skip')), {'i': 0, 'skip': False}, args)
marsh_attrs = {'data-sx-marsh': (_cells['marsh_id'] if sx_truthy(_cells['marsh_id']) else '')}
provide_push('element-attrs', NIL)
scope_push('element-attrs', NIL)
content = join('', map(lambda c: render_to_html(c, env), children))
for spread_dict in sx_emitted('element-attrs'):
merge_spread_attrs(marsh_attrs, spread_dict)
provide_pop('element-attrs')
scope_pop('element-attrs')
return sx_str('<', _cells['marsh_tag'], render_attrs(marsh_attrs), '>', content, '</', _cells['marsh_tag'], '>')
# render-html-island
@@ -2529,7 +2572,7 @@ def aser_call(name, args, env):
child_parts = []
_cells['skip'] = False
_cells['i'] = 0
provide_push('element-attrs', NIL)
scope_push('element-attrs', NIL)
for arg in args:
if sx_truthy(_cells['skip']):
_cells['skip'] = False
@@ -2557,12 +2600,12 @@ def aser_call(name, args, env):
v = dict_get(spread_dict, k)
attr_parts.append(sx_str(':', k))
attr_parts.append(serialize(v))
provide_pop('element-attrs')
scope_pop('element-attrs')
parts = concat([name], attr_parts, child_parts)
return sx_str('(', join(' ', parts), ')')
# SPECIAL_FORM_NAMES
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']
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', 'scope', 'provide']
# HO_FORM_NAMES
HO_FORM_NAMES = ['map', 'map-indexed', 'filter', 'reduce', 'some', 'every?', 'for-each']
@@ -2659,14 +2702,30 @@ def aser_special(name, expr, env):
elif sx_truthy(((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defhandler') if sx_truthy((name == 'defhandler')) else ((name == 'defpage') if sx_truthy((name == 'defpage')) else ((name == 'defquery') if sx_truthy((name == 'defquery')) else ((name == 'defaction') if sx_truthy((name == 'defaction')) else ((name == 'defrelation') if sx_truthy((name == 'defrelation')) else ((name == 'deftype') if sx_truthy((name == 'deftype')) else (name == 'defeffect')))))))))))):
trampoline(eval_expr(expr, env))
return NIL
elif sx_truthy((name == 'scope')):
scope_name = trampoline(eval_expr(first(args), env))
rest_args = rest(args)
scope_val = NIL
body_args = NIL
if sx_truthy(((len(rest_args) >= 2) if not sx_truthy((len(rest_args) >= 2)) else ((type_of(first(rest_args)) == 'keyword') if not sx_truthy((type_of(first(rest_args)) == 'keyword')) else (keyword_name(first(rest_args)) == 'value')))):
scope_val = trampoline(eval_expr(nth(rest_args, 1), env))
body_args = slice(rest_args, 2)
else:
body_args = rest_args
scope_push(scope_name, scope_val)
_cells['result'] = NIL
for body in body_args:
_cells['result'] = aser(body, env)
scope_pop(scope_name)
return _cells['result']
elif sx_truthy((name == 'provide')):
prov_name = trampoline(eval_expr(first(args), env))
prov_val = trampoline(eval_expr(nth(args, 1), env))
_cells['result'] = NIL
provide_push(prov_name, prov_val)
scope_push(prov_name, prov_val)
for body in slice(args, 2):
_cells['result'] = aser(body, env)
provide_pop(prov_name)
scope_pop(prov_name)
return _cells['result']
else:
return trampoline(eval_expr(expr, env))
@@ -3701,12 +3760,12 @@ async def async_render_element(tag, args, env, ctx):
else:
token = (svg_context_set(True) if sx_truthy(((tag == 'svg') if sx_truthy((tag == 'svg')) else (tag == 'math'))) else NIL)
content_parts = []
provide_push('element-attrs', NIL)
scope_push('element-attrs', NIL)
for c in children:
content_parts.append((await async_render(c, env, ctx)))
for spread_dict in sx_emitted('element-attrs'):
merge_spread_attrs(attrs, spread_dict)
provide_pop('element-attrs')
scope_pop('element-attrs')
if sx_truthy(token):
svg_context_reset(token)
return sx_str('<', tag, render_attrs(attrs), '>', join('', content_parts), '</', tag, '>')
@@ -3798,7 +3857,7 @@ async def async_map_render(exprs, env, ctx):
return results
# ASYNC_RENDER_FORMS
ASYNC_RENDER_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'deftype', 'defeffect', 'map', 'map-indexed', 'filter', 'for-each', 'provide']
ASYNC_RENDER_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'deftype', 'defeffect', 'map', 'map-indexed', 'filter', 'for-each', 'scope', 'provide']
# async-render-form?
def async_render_form_p(name):
@@ -3859,14 +3918,28 @@ async def dispatch_async_render_form(name, expr, env, ctx):
f = (await async_eval(nth(expr, 1), env, ctx))
coll = (await async_eval(nth(expr, 2), env, ctx))
return join('', (await async_map_fn_render(f, coll, env, ctx)))
elif sx_truthy((name == 'scope')):
scope_name = (await async_eval(nth(expr, 1), env, ctx))
rest_args = slice(expr, 2)
scope_val = NIL
body_exprs = NIL
if sx_truthy(((len(rest_args) >= 2) if not sx_truthy((len(rest_args) >= 2)) else ((type_of(first(rest_args)) == 'keyword') if not sx_truthy((type_of(first(rest_args)) == 'keyword')) else (keyword_name(first(rest_args)) == 'value')))):
scope_val = (await async_eval(nth(rest_args, 1), env, ctx))
body_exprs = slice(rest_args, 2)
else:
body_exprs = rest_args
scope_push(scope_name, scope_val)
result = ((await async_render(first(body_exprs), env, ctx)) if sx_truthy((len(body_exprs) == 1)) else join('', (await async_map_render(body_exprs, env, ctx))))
scope_pop(scope_name)
return result
elif sx_truthy((name == 'provide')):
prov_name = (await async_eval(nth(expr, 1), env, ctx))
prov_val = (await async_eval(nth(expr, 2), env, ctx))
body_start = 3
body_count = (len(expr) - 3)
provide_push(prov_name, prov_val)
scope_push(prov_name, prov_val)
result = ((await async_render(nth(expr, body_start), env, ctx)) if sx_truthy((body_count == 1)) else join('', (await async_map_render(slice(expr, body_start), env, ctx))))
provide_pop(prov_name)
scope_pop(prov_name)
return result
else:
return (await async_render((await async_eval(expr, env, ctx)), env, ctx))
@@ -4149,7 +4222,7 @@ async def async_aser_call(name, args, env, ctx):
child_parts = []
_cells['skip'] = False
_cells['i'] = 0
provide_push('element-attrs', NIL)
scope_push('element-attrs', NIL)
for arg in args:
if sx_truthy(_cells['skip']):
_cells['skip'] = False
@@ -4188,14 +4261,14 @@ async def async_aser_call(name, args, env, ctx):
v = dict_get(spread_dict, k)
attr_parts.append(sx_str(':', k))
attr_parts.append(serialize(v))
provide_pop('element-attrs')
scope_pop('element-attrs')
if sx_truthy(token):
svg_context_reset(token)
parts = concat([name], attr_parts, child_parts)
return make_sx_expr(sx_str('(', join(' ', parts), ')'))
# ASYNC_ASER_FORM_NAMES
ASYNC_ASER_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'begin', 'do', 'quote', '->', 'set!', 'defisland', 'deftype', 'defeffect', 'provide']
ASYNC_ASER_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'begin', 'do', 'quote', '->', 'set!', 'defisland', 'deftype', 'defeffect', 'scope', 'provide']
# ASYNC_ASER_HO_NAMES
ASYNC_ASER_HO_NAMES = ['map', 'map-indexed', 'filter', 'for-each']
@@ -4289,14 +4362,30 @@ async def dispatch_async_aser_form(name, expr, env, ctx):
elif sx_truthy(((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defhandler') if sx_truthy((name == 'defhandler')) else ((name == 'defpage') if sx_truthy((name == 'defpage')) else ((name == 'defquery') if sx_truthy((name == 'defquery')) else ((name == 'defaction') if sx_truthy((name == 'defaction')) else ((name == 'deftype') if sx_truthy((name == 'deftype')) else (name == 'defeffect'))))))))))):
(await async_eval(expr, env, ctx))
return NIL
elif sx_truthy((name == 'scope')):
scope_name = (await async_eval(first(args), env, ctx))
rest_args = rest(args)
scope_val = NIL
body_args = NIL
if sx_truthy(((len(rest_args) >= 2) if not sx_truthy((len(rest_args) >= 2)) else ((type_of(first(rest_args)) == 'keyword') if not sx_truthy((type_of(first(rest_args)) == 'keyword')) else (keyword_name(first(rest_args)) == 'value')))):
scope_val = (await async_eval(nth(rest_args, 1), env, ctx))
body_args = slice(rest_args, 2)
else:
body_args = rest_args
scope_push(scope_name, scope_val)
_cells['result'] = NIL
for body in body_args:
_cells['result'] = (await async_aser(body, env, ctx))
scope_pop(scope_name)
return _cells['result']
elif sx_truthy((name == 'provide')):
prov_name = (await async_eval(first(args), env, ctx))
prov_val = (await async_eval(nth(args, 1), env, ctx))
_cells['result'] = NIL
provide_push(prov_name, prov_val)
scope_push(prov_name, prov_val)
for body in slice(args, 2):
_cells['result'] = (await async_aser(body, env, ctx))
provide_pop(prov_name)
scope_pop(prov_name)
return _cells['result']
else:
return (await async_eval(expr, env, ctx))

View File

@@ -301,3 +301,46 @@
(deftest "spread in non-element context silently drops"
(assert-equal "hello"
(render-sx "(do (make-spread {:class \"card\"}) \"hello\")"))))
;; --------------------------------------------------------------------------
;; Scope tests — unified scope primitive
;; --------------------------------------------------------------------------
(defsuite "scope"
(deftest "scope with value and context"
(assert-equal "dark"
(render-sx "(scope \"sc-theme\" :value \"dark\" (context \"sc-theme\"))")))
(deftest "scope without value defaults to nil"
(assert-equal ""
(render-sx "(scope \"sc-nil\" (str (context \"sc-nil\")))")))
(deftest "scope with emit!/emitted"
(assert-equal "a,b"
(render-sx "(scope \"sc-emit\" (emit! \"sc-emit\" \"a\") (emit! \"sc-emit\" \"b\") (join \",\" (emitted \"sc-emit\")))")))
(deftest "provide is equivalent to scope with value"
(assert-equal "42"
(render-sx "(provide \"sc-prov\" 42 (str (context \"sc-prov\")))")))
(deftest "collect! works via scope (lazy root scope)"
(assert-equal "x,y"
(render-sx "(do (collect! \"sc-coll\" \"x\") (collect! \"sc-coll\" \"y\") (join \",\" (collected \"sc-coll\")))")))
(deftest "collect! deduplicates"
(assert-equal "a"
(render-sx "(do (collect! \"sc-dedup\" \"a\") (collect! \"sc-dedup\" \"a\") (join \",\" (collected \"sc-dedup\")))")))
(deftest "clear-collected! clears scope accumulator"
(assert-equal ""
(render-sx "(do (collect! \"sc-clear\" \"x\") (clear-collected! \"sc-clear\") (join \",\" (collected \"sc-clear\")))")))
(deftest "nested scope shadows outer"
(assert-equal "inner"
(render-sx "(scope \"sc-nest\" :value \"outer\" (scope \"sc-nest\" :value \"inner\" (context \"sc-nest\")))")))
(deftest "scope pops correctly after body"
(assert-equal "outer"
(render-sx "(scope \"sc-pop\" :value \"outer\" (scope \"sc-pop\" :value \"inner\" \"ignore\") (context \"sc-pop\"))"))))