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 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 isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); } function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -86,8 +86,7 @@
function SxSpread(attrs) { this.attrs = attrs || {}; } function SxSpread(attrs) { this.attrs = attrs || {}; }
SxSpread.prototype._spread = true; SxSpread.prototype._spread = true;
var _collectBuckets = {}; var _scopeStacks = {};
var _provideStacks = {};
function isSym(x) { return x != null && x._sym === true; } function isSym(x) { return x != null && x._sym === true; }
function isKw(x) { return x != null && x._kw === true; } function isKw(x) { return x != null && x._kw === true; }
@@ -151,44 +150,54 @@
function isSpread(x) { return x != null && x._spread === true; } function isSpread(x) { return x != null && x._spread === true; }
function spreadAttrs(s) { return s && s._spread ? s.attrs : {}; } function spreadAttrs(s) { return s && s._spread ? s.attrs : {}; }
function sxCollect(bucket, value) { function scopePush(name, value) {
if (!_collectBuckets[bucket]) _collectBuckets[bucket] = []; if (!_scopeStacks[name]) _scopeStacks[name] = [];
var items = _collectBuckets[bucket]; _scopeStacks[name].push({value: value !== undefined ? value : NIL, emitted: [], dedup: false});
if (items.indexOf(value) === -1) items.push(value);
} }
function sxCollected(bucket) { function scopePop(name) {
return _collectBuckets[bucket] ? _collectBuckets[bucket].slice() : []; if (_scopeStacks[name] && _scopeStacks[name].length) _scopeStacks[name].pop();
}
function sxClearCollected(bucket) {
if (_collectBuckets[bucket]) _collectBuckets[bucket] = [];
} }
// 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) { function sxContext(name) {
if (_provideStacks[name] && _provideStacks[name].length) { if (_scopeStacks[name] && _scopeStacks[name].length) {
return _provideStacks[name][_provideStacks[name].length - 1].value; return _scopeStacks[name][_scopeStacks[name].length - 1].value;
} }
if (arguments.length > 1) return arguments[1]; if (arguments.length > 1) return arguments[1];
throw new Error("No provider for: " + name); throw new Error("No provider for: " + name);
} }
function sxEmit(name, value) { function sxEmit(name, value) {
if (_provideStacks[name] && _provideStacks[name].length) { if (_scopeStacks[name] && _scopeStacks[name].length) {
_provideStacks[name][_provideStacks[name].length - 1].emitted.push(value); var entry = _scopeStacks[name][_scopeStacks[name].length - 1];
if (entry.dedup && entry.emitted.indexOf(value) !== -1) return NIL;
entry.emitted.push(value);
} }
return NIL; return NIL;
} }
function sxEmitted(name) { function sxEmitted(name) {
if (_provideStacks[name] && _provideStacks[name].length) { if (_scopeStacks[name] && _scopeStacks[name].length) {
return _provideStacks[name][_provideStacks[name].length - 1].emitted.slice(); return _scopeStacks[name][_scopeStacks[name].length - 1].emitted.slice();
} }
return []; 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 lambdaParams(f) { return f.params; }
function lambdaBody(f) { return f.body; } 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["make-spread"] = makeSpread;
PRIMITIVES["spread?"] = isSpread; PRIMITIVES["spread?"] = isSpread;
PRIMITIVES["spread-attrs"] = spreadAttrs; PRIMITIVES["spread-attrs"] = spreadAttrs;
PRIMITIVES["collect!"] = sxCollect; PRIMITIVES["collect!"] = sxCollect;
PRIMITIVES["collected"] = sxCollected; PRIMITIVES["collected"] = sxCollected;
PRIMITIVES["clear-collected!"] = sxClearCollected; 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-push!"] = providePush;
PRIMITIVES["provide-pop!"] = providePop; PRIMITIVES["provide-pop!"] = providePop;
PRIMITIVES["context"] = sxContext; PRIMITIVES["context"] = sxContext;
@@ -796,10 +808,10 @@
var args = rest(expr); 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() { 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); 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); var mac = envGet(env, name);
return makeThunk(expandMacro(mac, args, env), env); 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))); })() : evalCall(head, args, env)));
})(); }; })(); };
@@ -1204,6 +1216,22 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
callThunk(after, env); callThunk(after, env);
return result; 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 // 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 val = trampoline(evalExpr(nth(args, 1), env));
var bodyExprs = slice(args, 2); var bodyExprs = slice(args, 2);
var result = NIL; 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)); } } { 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; 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))); })(); }; 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 // 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? // render-html-form?
var isRenderHtmlForm = function(name) { return contains(RENDER_HTML_FORMS, name); }; 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 f = trampoline(evalExpr(nth(expr, 1), env));
var coll = trampoline(evalExpr(nth(expr, 2), 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)); 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() { })() : (isSxTruthy((name == "provide")) ? (function() {
var provName = trampoline(evalExpr(nth(expr, 1), env)); var provName = trampoline(evalExpr(nth(expr, 1), env));
var provVal = trampoline(evalExpr(nth(expr, 2), env)); var provVal = trampoline(evalExpr(nth(expr, 2), env));
var bodyStart = 3; var bodyStart = 3;
var bodyCount = (len(expr) - 3); var bodyCount = (len(expr) - 3);
providePush(provName, provVal); scopePush(provName, provVal);
return (function() { 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))))); 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; return result;
})(); })();
})() : renderValueToHtml(trampoline(evalExpr(expr, env)), env))))))))))))); }; })() : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))))))))))); };
// render-lambda-html // render-lambda-html
var renderLambdaHtml = function(f, args, env) { return (function() { 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 attrs = first(parsed);
var children = nth(parsed, 1); var children = nth(parsed, 1);
var isVoid = contains(VOID_ELEMENTS, tag); 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 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); } } { 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(">")); 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); })(); }, {["i"]: 0, ["skip"]: false}, args);
return (function() { return (function() {
var lakeAttrs = {["data-sx-lake"]: sxOr(lakeId, "")}; var lakeAttrs = {["data-sx-lake"]: sxOr(lakeId, "")};
providePush("element-attrs", NIL); scopePush("element-attrs", NIL);
return (function() { return (function() {
var content = join("", map(function(c) { return renderToHtml(c, env); }, children)); 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); } } { 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(">")); 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); })(); }, {["i"]: 0, ["skip"]: false}, args);
return (function() { return (function() {
var marshAttrs = {["data-sx-marsh"]: sxOr(marshId, "")}; var marshAttrs = {["data-sx-marsh"]: sxOr(marshId, "")};
providePush("element-attrs", NIL); scopePush("element-attrs", NIL);
return (function() { return (function() {
var content = join("", map(function(c) { return renderToHtml(c, env); }, children)); 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); } } { 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(">")); return (String("<") + String(marshTag) + String(renderAttrs(marshAttrs)) + String(">") + String(content) + String("</") + String(marshTag) + String(">"));
})(); })();
})(); })();
@@ -1754,7 +1794,7 @@ return (function() {
var childParts = []; var childParts = [];
var skip = false; var skip = false;
var i = 0; 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 _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); var val = aser(nth(args, (i + 1)), env);
if (isSxTruthy(!isSxTruthy(isNil(val)))) { if (isSxTruthy(!isSxTruthy(isNil(val)))) {
@@ -1775,7 +1815,7 @@ return (function() {
attrParts.push((String(":") + String(k))); attrParts.push((String(":") + String(k)));
return append_b(attrParts, serialize(v)); return append_b(attrParts, serialize(v));
})(); } } } } })(); } } } }
providePop("element-attrs"); scopePop("element-attrs");
return (function() { return (function() {
var parts = concat([name], attrParts, childParts); var parts = concat([name], attrParts, childParts);
return (String("(") + String(join(" ", parts)) + String(")")); return (String("(") + String(join(" ", parts)) + String(")"));
@@ -1783,7 +1823,7 @@ return (function() {
})(); }; })(); };
// SPECIAL_FORM_NAMES // 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 // HO_FORM_NAMES
var HO_FORM_NAMES = ["map", "map-indexed", "filter", "reduce", "some", "every?", "for-each"]; 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)); return append_b(results, aser(lambdaBody(f), local));
})() : invoke(f, item)); } } })() : invoke(f, item)); } }
return (isSxTruthy(isEmpty(results)) ? NIL : results); 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 provName = trampoline(evalExpr(first(args), env));
var provVal = trampoline(evalExpr(nth(args, 1), env)); var provVal = trampoline(evalExpr(nth(args, 1), env));
var result = NIL; 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); } } { 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; return result;
})() : trampoline(evalExpr(expr, env)))))))))))))))); })() : trampoline(evalExpr(expr, env)))))))))))))))));
})(); }; })(); };
// eval-case-aser // 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 renderDomElement = function(tag, args, env, ns) { return (function() {
var newNs = (isSxTruthy((tag == "svg")) ? SVG_NS : (isSxTruthy((tag == "math")) ? MATH_NS : ns)); var newNs = (isSxTruthy((tag == "svg")) ? SVG_NS : (isSxTruthy((tag == "math")) ? MATH_NS : ns));
var el = domCreateElement(tag, newNs); var el = domCreateElement(tag, newNs);
providePush("element-attrs", NIL); scopePush("element-attrs", NIL);
reduce(function(state, arg) { return (function() { reduce(function(state, arg) { return (function() {
var skip = get(state, "skip"); 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() { 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)); return domSetAttr(el, "style", (isSxTruthy((isSxTruthy(existing) && !isSxTruthy((existing == "")))) ? (String(existing) + String(";") + String(val)) : val));
})() : domSetAttr(el, key, (String(val))))); })() : domSetAttr(el, key, (String(val)))));
})(); } } } } })(); } } } }
providePop("element-attrs"); scopePop("element-attrs");
return el; 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))); }; var renderDomUnknownComponent = function(name) { return error((String("Unknown component: ") + String(name))); };
// RENDER_DOM_FORMS // 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? // render-dom-form?
var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); }; 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 domAppend(frag, val);
})(); } } })(); } }
return frag; 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() { })() : (isSxTruthy((name == "provide")) ? (function() {
var provName = trampoline(evalExpr(nth(expr, 1), env)); var provName = trampoline(evalExpr(nth(expr, 1), env));
var provVal = trampoline(evalExpr(nth(expr, 2), env)); var provVal = trampoline(evalExpr(nth(expr, 2), env));
var frag = createFragment(); 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)); } } { 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; return frag;
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns))))))))))))))); }; })() : renderToDom(trampoline(evalExpr(expr, env)), env, ns)))))))))))))))); };
// render-lambda-dom // render-lambda-dom
var renderLambdaDom = function(f, args, env, ns) { return (function() { var renderLambdaDom = function(f, args, env, ns) { return (function() {
@@ -6622,6 +6686,8 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
collect: sxCollect, collect: sxCollect,
collected: sxCollected, collected: sxCollected,
clearCollected: sxClearCollected, clearCollected: sxClearCollected,
scopePush: scopePush,
scopePop: scopePop,
providePush: providePush, providePush: providePush,
providePop: providePop, providePop: providePop,
context: sxContext, context: sxContext,

View File

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

View File

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

View File

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

View File

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

View File

@@ -293,6 +293,8 @@ class PyEmitter:
"collect!": "sx_collect", "collect!": "sx_collect",
"collected": "sx_collected", "collected": "sx_collected",
"clear-collected!": "sx_clear_collected", "clear-collected!": "sx_clear_collected",
"scope-push!": "scope_push",
"scope-pop!": "scope_pop",
"provide-push!": "provide_push", "provide-push!": "provide_push",
"provide-pop!": "provide_pop", "provide-pop!": "provide_pop",
"context": "sx_context", "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 ;; `scope` is the general primitive. `provide` is sugar for scope-with-value.
;; with a value and an empty accumulator. `context` reads the value from the ;; Both `provide` and `scope` are special forms in the evaluator.
;; nearest enclosing provider. `emit!` appends to the accumulator, `emitted`
;; reads the accumulated values.
;; ;;
;; The platform must implement per-name stacks. Each entry has a value and ;; The platform must implement per-name stacks. Each entry has a value,
;; an emitted list. `provide-push!`/`provide-pop!` manage the stack. ;; 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!" (declare-spread-primitive "provide-push!"
:params (name value) :params (name value)
:returns "nil" :returns "nil"
:effects [mutation] :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!" (declare-spread-primitive "provide-pop!"
:params (name) :params (name)
:returns "nil" :returns "nil"
:effects [mutation] :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" (declare-spread-primitive "context"
:params (name &rest default) :params (name &rest default)

View File

@@ -162,6 +162,7 @@
(= name "reset") (sf-reset args env) (= name "reset") (sf-reset args env)
(= name "shift") (sf-shift args env) (= name "shift") (sf-shift args env)
(= name "dynamic-wind") (sf-dynamic-wind args env) (= name "dynamic-wind") (sf-dynamic-wind args env)
(= name "scope") (sf-scope args env)
(= name "provide") (sf-provide args env) (= name "provide") (sf-provide args env)
;; Higher-order forms ;; 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 ;; (scope name body...) or (scope name :value v body...)
;; accumulator, evaluate body, pop scope. Returns last body result. ;; 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 (define sf-provide
(fn ((args :as list) (env :as dict)) (fn ((args :as list) (env :as dict))
@@ -963,9 +988,9 @@
(val (trampoline (eval-expr (nth args 1) env))) (val (trampoline (eval-expr (nth args 1) env)))
(body-exprs (slice args 2)) (body-exprs (slice args 2))
(result nil)) (result nil))
(provide-push! name val) (scope-push! name val)
(for-each (fn (e) (set! result (trampoline (eval-expr e env)))) body-exprs) (for-each (fn (e) (set! result (trampoline (eval-expr e env)))) body-exprs)
(provide-pop! name) (scope-pop! name)
result))) result)))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,51 +50,56 @@ class _Spread:
self.attrs = dict(attrs) if attrs else {} self.attrs = dict(attrs) if attrs else {}
# Render-time accumulator buckets (per render pass) # Unified scope stacks — backing store for provide/context/emit!/collect!
_collect_buckets: dict[str, list] = {} # Each entry: {"value": v, "emitted": [], "dedup": bool}
_scope_stacks: dict[str, list[dict]] = {}
def _collect_reset(): def _collect_reset():
"""Reset all collect buckets (call at start of each render pass).""" """Reset all scope stacks (call at start of each render pass)."""
global _collect_buckets global _scope_stacks
_collect_buckets = {} _scope_stacks = {}
# Render-time dynamic scope stacks (provide/context/emit!) def scope_push(name, value=None):
_provide_stacks: dict[str, list[dict]] = {} """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): def scope_pop(name):
"""Push a provider scope with name, value, and empty emitted list.""" """Pop the most recent scope for name."""
_provide_stacks.setdefault(name, []).append({"value": value, "emitted": []}) if name in _scope_stacks and _scope_stacks[name]:
_scope_stacks[name].pop()
def provide_pop(name): # Aliases — provide-push!/provide-pop! map to scope-push!/scope-pop!
"""Pop the most recent provider scope for name.""" provide_push = scope_push
if name in _provide_stacks and _provide_stacks[name]: provide_pop = scope_pop
_provide_stacks[name].pop()
def sx_context(name, *default): def sx_context(name, *default):
"""Read value from nearest enclosing provider. Error if no provider and no default.""" """Read value from nearest enclosing scope. Error if no scope and no default."""
if name in _provide_stacks and _provide_stacks[name]: if name in _scope_stacks and _scope_stacks[name]:
return _provide_stacks[name][-1]["value"] return _scope_stacks[name][-1]["value"]
if default: if default:
return default[0] return default[0]
raise RuntimeError(f"No provider for: {name}") raise RuntimeError(f"No provider for: {name}")
def sx_emit(name, value): def sx_emit(name, value):
"""Append value to nearest enclosing provider's accumulator. No-op if no provider.""" """Append value to nearest enclosing scope's accumulator. Respects dedup flag."""
if name in _provide_stacks and _provide_stacks[name]: if name in _scope_stacks and _scope_stacks[name]:
_provide_stacks[name][-1]["emitted"].append(value) entry = _scope_stacks[name][-1]
if entry["dedup"] and value in entry["emitted"]:
return NIL
entry["emitted"].append(value)
return NIL return NIL
def sx_emitted(name): def sx_emitted(name):
"""Return list of values emitted into nearest matching provider.""" """Return list of values emitted into nearest matching scope."""
if name in _provide_stacks and _provide_stacks[name]: if name in _scope_stacks and _scope_stacks[name]:
return list(_provide_stacks[name][-1]["emitted"]) return list(_scope_stacks[name][-1]["emitted"])
return [] return []
@@ -299,23 +304,23 @@ def spread_attrs(s):
def sx_collect(bucket, value): def sx_collect(bucket, value):
"""Add value to named render-time accumulator (deduplicated).""" """Add value to named scope accumulator (deduplicated). Lazily creates root scope."""
if bucket not in _collect_buckets: if bucket not in _scope_stacks or not _scope_stacks[bucket]:
_collect_buckets[bucket] = [] _scope_stacks.setdefault(bucket, []).append({"value": None, "emitted": [], "dedup": True})
items = _collect_buckets[bucket] entry = _scope_stacks[bucket][-1]
if value not in items: if value not in entry["emitted"]:
items.append(value) entry["emitted"].append(value)
def sx_collected(bucket): def sx_collected(bucket):
"""Return all values in named render-time accumulator.""" """Return all values collected in named scope accumulator."""
return list(_collect_buckets.get(bucket, [])) return sx_emitted(bucket)
def sx_clear_collected(bucket): def sx_clear_collected(bucket):
"""Clear a named render-time accumulator bucket.""" """Clear nearest scope's accumulator for name."""
if bucket in _collect_buckets: if bucket in _scope_stacks and _scope_stacks[bucket]:
_collect_buckets[bucket] = [] _scope_stacks[bucket][-1]["emitted"] = []
def lambda_params(f): 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 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["make-spread"] = make_spread
PRIMITIVES["spread?"] = is_spread PRIMITIVES["spread?"] = is_spread
PRIMITIVES["spread-attrs"] = spread_attrs PRIMITIVES["spread-attrs"] = spread_attrs
PRIMITIVES["collect!"] = sx_collect PRIMITIVES["collect!"] = sx_collect
PRIMITIVES["collected"] = sx_collected PRIMITIVES["collected"] = sx_collected
PRIMITIVES["clear-collected!"] = sx_clear_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-push!"] = provide_push
PRIMITIVES["provide-pop!"] = provide_pop PRIMITIVES["provide-pop!"] = provide_pop
PRIMITIVES["context"] = sx_context PRIMITIVES["context"] = sx_context
@@ -1394,6 +1402,8 @@ def eval_list(expr, env):
return sf_shift(args, env) return sf_shift(args, env)
elif sx_truthy((name == 'dynamic-wind')): elif sx_truthy((name == 'dynamic-wind')):
return sf_dynamic_wind(args, env) return sf_dynamic_wind(args, env)
elif sx_truthy((name == 'scope')):
return sf_scope(args, env)
elif sx_truthy((name == 'provide')): elif sx_truthy((name == 'provide')):
return sf_provide(args, env) return sf_provide(args, env)
elif sx_truthy((name == 'map')): elif sx_truthy((name == 'map')):
@@ -1887,6 +1897,25 @@ def sf_dynamic_wind(args, env):
call_thunk(after, env) call_thunk(after, env)
return result 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 # sf-provide
def sf_provide(args, env): def sf_provide(args, env):
_cells = {} _cells = {}
@@ -1894,10 +1923,10 @@ def sf_provide(args, env):
val = trampoline(eval_expr(nth(args, 1), env)) val = trampoline(eval_expr(nth(args, 1), env))
body_exprs = slice(args, 2) body_exprs = slice(args, 2)
_cells['result'] = NIL _cells['result'] = NIL
provide_push(name, val) scope_push(name, val)
for e in body_exprs: for e in body_exprs:
_cells['result'] = trampoline(eval_expr(e, env)) _cells['result'] = trampoline(eval_expr(e, env))
provide_pop(name) scope_pop(name)
return _cells['result'] return _cells['result']
# expand-macro # expand-macro
@@ -2251,7 +2280,7 @@ def render_value_to_html(val, env):
return escape_html(sx_str(val)) return escape_html(sx_str(val))
# RENDER_HTML_FORMS # 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? # render-html-form?
def is_render_html_form(name): 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)) f = trampoline(eval_expr(nth(expr, 1), env))
coll = trampoline(eval_expr(nth(expr, 2), 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)) 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')): elif sx_truthy((name == 'provide')):
prov_name = trampoline(eval_expr(nth(expr, 1), env)) prov_name = trampoline(eval_expr(nth(expr, 1), env))
prov_val = trampoline(eval_expr(nth(expr, 2), env)) prov_val = trampoline(eval_expr(nth(expr, 2), env))
body_start = 3 body_start = 3
body_count = (len(expr) - 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))))) 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 return result
else: else:
return render_value_to_html(trampoline(eval_expr(expr, env)), env) 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): if sx_truthy(is_void):
return sx_str('<', tag, render_attrs(attrs), ' />') return sx_str('<', tag, render_attrs(attrs), ' />')
else: else:
provide_push('element-attrs', NIL) scope_push('element-attrs', NIL)
content = join('', map(lambda c: render_to_html(c, env), children)) content = join('', map(lambda c: render_to_html(c, env), children))
for spread_dict in sx_emitted('element-attrs'): for spread_dict in sx_emitted('element-attrs'):
merge_spread_attrs(attrs, spread_dict) merge_spread_attrs(attrs, spread_dict)
provide_pop('element-attrs') scope_pop('element-attrs')
return sx_str('<', tag, render_attrs(attrs), '>', content, '</', tag, '>') return sx_str('<', tag, render_attrs(attrs), '>', content, '</', tag, '>')
# render-html-lake # render-html-lake
@@ -2404,11 +2447,11 @@ def render_html_lake(args, env):
children = [] 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) 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 '')} 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)) content = join('', map(lambda c: render_to_html(c, env), children))
for spread_dict in sx_emitted('element-attrs'): for spread_dict in sx_emitted('element-attrs'):
merge_spread_attrs(lake_attrs, spread_dict) 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'], '>') return sx_str('<', _cells['lake_tag'], render_attrs(lake_attrs), '>', content, '</', _cells['lake_tag'], '>')
# render-html-marsh # render-html-marsh
@@ -2419,11 +2462,11 @@ def render_html_marsh(args, env):
children = [] 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) 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 '')} 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)) content = join('', map(lambda c: render_to_html(c, env), children))
for spread_dict in sx_emitted('element-attrs'): for spread_dict in sx_emitted('element-attrs'):
merge_spread_attrs(marsh_attrs, spread_dict) 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'], '>') return sx_str('<', _cells['marsh_tag'], render_attrs(marsh_attrs), '>', content, '</', _cells['marsh_tag'], '>')
# render-html-island # render-html-island
@@ -2529,7 +2572,7 @@ def aser_call(name, args, env):
child_parts = [] child_parts = []
_cells['skip'] = False _cells['skip'] = False
_cells['i'] = 0 _cells['i'] = 0
provide_push('element-attrs', NIL) scope_push('element-attrs', NIL)
for arg in args: for arg in args:
if sx_truthy(_cells['skip']): if sx_truthy(_cells['skip']):
_cells['skip'] = False _cells['skip'] = False
@@ -2557,12 +2600,12 @@ def aser_call(name, args, env):
v = dict_get(spread_dict, k) v = dict_get(spread_dict, k)
attr_parts.append(sx_str(':', k)) attr_parts.append(sx_str(':', k))
attr_parts.append(serialize(v)) attr_parts.append(serialize(v))
provide_pop('element-attrs') scope_pop('element-attrs')
parts = concat([name], attr_parts, child_parts) parts = concat([name], attr_parts, child_parts)
return sx_str('(', join(' ', parts), ')') return sx_str('(', join(' ', parts), ')')
# SPECIAL_FORM_NAMES # 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
HO_FORM_NAMES = ['map', 'map-indexed', 'filter', 'reduce', 'some', 'every?', 'for-each'] 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')))))))))))): 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)) trampoline(eval_expr(expr, env))
return NIL 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')): elif sx_truthy((name == 'provide')):
prov_name = trampoline(eval_expr(first(args), env)) prov_name = trampoline(eval_expr(first(args), env))
prov_val = trampoline(eval_expr(nth(args, 1), env)) prov_val = trampoline(eval_expr(nth(args, 1), env))
_cells['result'] = NIL _cells['result'] = NIL
provide_push(prov_name, prov_val) scope_push(prov_name, prov_val)
for body in slice(args, 2): for body in slice(args, 2):
_cells['result'] = aser(body, env) _cells['result'] = aser(body, env)
provide_pop(prov_name) scope_pop(prov_name)
return _cells['result'] return _cells['result']
else: else:
return trampoline(eval_expr(expr, env)) return trampoline(eval_expr(expr, env))
@@ -3701,12 +3760,12 @@ async def async_render_element(tag, args, env, ctx):
else: else:
token = (svg_context_set(True) if sx_truthy(((tag == 'svg') if sx_truthy((tag == 'svg')) else (tag == 'math'))) else NIL) token = (svg_context_set(True) if sx_truthy(((tag == 'svg') if sx_truthy((tag == 'svg')) else (tag == 'math'))) else NIL)
content_parts = [] content_parts = []
provide_push('element-attrs', NIL) scope_push('element-attrs', NIL)
for c in children: for c in children:
content_parts.append((await async_render(c, env, ctx))) content_parts.append((await async_render(c, env, ctx)))
for spread_dict in sx_emitted('element-attrs'): for spread_dict in sx_emitted('element-attrs'):
merge_spread_attrs(attrs, spread_dict) merge_spread_attrs(attrs, spread_dict)
provide_pop('element-attrs') scope_pop('element-attrs')
if sx_truthy(token): if sx_truthy(token):
svg_context_reset(token) svg_context_reset(token)
return sx_str('<', tag, render_attrs(attrs), '>', join('', content_parts), '</', tag, '>') 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 return results
# ASYNC_RENDER_FORMS # 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? # async-render-form?
def async_render_form_p(name): 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)) f = (await async_eval(nth(expr, 1), env, ctx))
coll = (await async_eval(nth(expr, 2), env, ctx)) coll = (await async_eval(nth(expr, 2), env, ctx))
return join('', (await async_map_fn_render(f, coll, 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')): elif sx_truthy((name == 'provide')):
prov_name = (await async_eval(nth(expr, 1), env, ctx)) prov_name = (await async_eval(nth(expr, 1), env, ctx))
prov_val = (await async_eval(nth(expr, 2), env, ctx)) prov_val = (await async_eval(nth(expr, 2), env, ctx))
body_start = 3 body_start = 3
body_count = (len(expr) - 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)))) 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 return result
else: else:
return (await async_render((await async_eval(expr, env, ctx)), env, ctx)) 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 = [] child_parts = []
_cells['skip'] = False _cells['skip'] = False
_cells['i'] = 0 _cells['i'] = 0
provide_push('element-attrs', NIL) scope_push('element-attrs', NIL)
for arg in args: for arg in args:
if sx_truthy(_cells['skip']): if sx_truthy(_cells['skip']):
_cells['skip'] = False _cells['skip'] = False
@@ -4188,14 +4261,14 @@ async def async_aser_call(name, args, env, ctx):
v = dict_get(spread_dict, k) v = dict_get(spread_dict, k)
attr_parts.append(sx_str(':', k)) attr_parts.append(sx_str(':', k))
attr_parts.append(serialize(v)) attr_parts.append(serialize(v))
provide_pop('element-attrs') scope_pop('element-attrs')
if sx_truthy(token): if sx_truthy(token):
svg_context_reset(token) svg_context_reset(token)
parts = concat([name], attr_parts, child_parts) parts = concat([name], attr_parts, child_parts)
return make_sx_expr(sx_str('(', join(' ', parts), ')')) return make_sx_expr(sx_str('(', join(' ', parts), ')'))
# ASYNC_ASER_FORM_NAMES # 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
ASYNC_ASER_HO_NAMES = ['map', 'map-indexed', 'filter', 'for-each'] 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'))))))))))): 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)) (await async_eval(expr, env, ctx))
return NIL 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')): elif sx_truthy((name == 'provide')):
prov_name = (await async_eval(first(args), env, ctx)) prov_name = (await async_eval(first(args), env, ctx))
prov_val = (await async_eval(nth(args, 1), env, ctx)) prov_val = (await async_eval(nth(args, 1), env, ctx))
_cells['result'] = NIL _cells['result'] = NIL
provide_push(prov_name, prov_val) scope_push(prov_name, prov_val)
for body in slice(args, 2): for body in slice(args, 2):
_cells['result'] = (await async_aser(body, env, ctx)) _cells['result'] = (await async_aser(body, env, ctx))
provide_pop(prov_name) scope_pop(prov_name)
return _cells['result'] return _cells['result']
else: else:
return (await async_eval(expr, env, ctx)) return (await async_eval(expr, env, ctx))

View File

@@ -301,3 +301,46 @@
(deftest "spread in non-element context silently drops" (deftest "spread in non-element context silently drops"
(assert-equal "hello" (assert-equal "hello"
(render-sx "(do (make-spread {:class \"card\"}) \"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\"))"))))

View File

@@ -374,10 +374,12 @@
:children (list :children (list
{:label "Reference" :href "/sx/(geography.(hypermedia.(reference)))" :children reference-nav-items} {:label "Reference" :href "/sx/(geography.(hypermedia.(reference)))" :children reference-nav-items}
{:label "Examples" :href "/sx/(geography.(hypermedia.(example)))" :children examples-nav-items})} {:label "Examples" :href "/sx/(geography.(hypermedia.(example)))" :children examples-nav-items})}
{:label "Scopes" :href "/sx/(geography.(scopes))"
:summary "The unified primitive beneath provide, collect!, spreads, and islands. Named scope with downward value, upward accumulation, and a dedup flag."}
{:label "Provide / Emit!" :href "/sx/(geography.(provide))" {:label "Provide / Emit!" :href "/sx/(geography.(provide))"
:summary "Render-time dynamic scope — the substrate beneath spreads, CSSX, and script collection. Downward context, upward accumulation, one mechanism."} :summary "Sugar for scope-with-value. Render-time dynamic scope — the substrate beneath spreads, CSSX, and script collection."}
{:label "Spreads" :href "/sx/(geography.(spreads))" {:label "Spreads" :href "/sx/(geography.(spreads))"
:summary "Child-to-parent communication across render boundaries — spread, collect!, reactive-spread, built on provide/emit!."} :summary "Child-to-parent communication across render boundaries — spread, collect!, reactive-spread, built on scopes."}
{:label "Marshes" :href "/sx/(geography.(marshes))" {:label "Marshes" :href "/sx/(geography.(marshes))"
:summary "Where reactivity and hypermedia interpenetrate — server writes to signals, reactive transforms reshape server content, client state modifies how hypermedia is interpreted."} :summary "Where reactivity and hypermedia interpenetrate — server writes to signals, reactive transforms reshape server content, client state modifies how hypermedia is interpreted."}
{:label "Isomorphism" :href "/sx/(geography.(isomorphism))" :children isomorphism-nav-items})} {:label "Isomorphism" :href "/sx/(geography.(isomorphism))" :children isomorphism-nav-items})}

View File

@@ -60,6 +60,10 @@
"phase2" '(~reactive-islands/phase2/reactive-islands-phase2-content) "phase2" '(~reactive-islands/phase2/reactive-islands-phase2-content)
:else '(~reactive-islands/index/reactive-islands-index-content))))) :else '(~reactive-islands/index/reactive-islands-index-content)))))
(define scopes
(fn (content)
(if (nil? content) '(~geography/scopes-content) content)))
(define spreads (define spreads
(fn (content) (fn (content)
(if (nil? content) '(~geography/spreads-content) content))) (if (nil? content) '(~geography/spreads-content) content)))

View File

@@ -340,24 +340,27 @@
(p "The path from current SX to the scope primitive follows the existing plan " (p "The path from current SX to the scope primitive follows the existing plan "
"and adds two phases:") "and adds two phases:")
(~docs/subsection :title "Phase 1: provide/context/emit! (immediate)" (~docs/subsection :title "Phase 1: provide/context/emit! "
(p "Already planned. Implement render-time dynamic scope. Four primitives: " (p (strong "Complete. ") "Render-time dynamic scope. Four primitives: "
(code "provide") " (special form), " (code "context") ", " (code "emit!") ", " (code "provide") " (special form), " (code "context") ", " (code "emit!") ", "
(code "emitted") ". Platform provides " (code "provide-push!/provide-pop!") ".") (code "emitted") ". Platform provides " (code "scope-push!/scope-pop!") ". "
(p "This is " (code "scope") " with " (code ":propagation :render") " only. " "Spreads reimplemented on provide/emit!.")
"No change to islands or lakes. Pure addition.") (p "See "
(p (strong "Delivers: ") "render-time context, scoped accumulation, " (a :href "/sx/(geography.(provide))" :class "text-violet-600 hover:underline" "provide article")
"spread and collect reimplemented as sugar over provide/emit.")) " and "
(a :href "/sx/(geography.(spreads))" :class "text-violet-600 hover:underline" "spreads article")
"."))
(~docs/subsection :title "Phase 2: scope as the common form (next)" (~docs/subsection :title "Phase 2: scope as the common form "
(p "Introduce " (code "scope") " as the general form. " (p (strong "Complete. ") (code "scope") " is now the general form. "
(code "provide") " becomes sugar for " (code "(scope ... :propagation :render)") ". " (code "provide") " is sugar for " (code "(scope name :value v body...)") ". "
(code "defisland") " becomes sugar for " (code "(scope ... :propagation :reactive)") ". " (code "collect!") " creates a lazy root scope with deduplication. "
(code "lake") " becomes sugar for " (code "(scope ... :propagation :morph)") ".") "All adapters use " (code "scope-push!/scope-pop!") " directly.")
(p "The sugar forms remain — nobody writes " (code "scope") " directly in page code. " (p "The unified platform structure:")
"But the evaluator, adapters, and bootstrappers all dispatch through one mechanism.") (~docs/code :code (highlight "_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" "python"))
(p (strong "Delivers: ") "unified internal representation, reactive context (the new cell), " (p "See "
"simplified adapter code (one scope handler instead of three separate paths).")) (a :href "/sx/(geography.(scopes))" :class "text-violet-600 hover:underline" "scopes article")
"."))
(~docs/subsection :title "Phase 3: effect handlers (future)" (~docs/subsection :title "Phase 3: effect handlers (future)"
(p "Make propagation modes extensible. A " (code ":propagation") " value is a " (p "Make propagation modes extensible. A " (code ":propagation") " value is a "
@@ -437,8 +440,10 @@
"and composable. It's the last primitive SX needs.") "and composable. It's the last primitive SX needs.")
(~docs/note (~docs/note
(p (strong "Status: ") "Phase 1 (" (code "provide/context/emit!") ") is specced and " (p (strong "Status: ") "Phase 1 (" (code "provide/context/emit!") ") and "
"ready to build. Phase 2 (" (code "scope") " unification) follows naturally once " "Phase 2 (" (code "scope") " unification) are complete. "
"provide is working. Phase 3 (extensible handlers) is the research frontier — " "574 tests pass. All four adapters use " (code "scope-push!/scope-pop!") ". "
(code "collect!") " is backed by lazy scopes with dedup. "
"Phase 3 (extensible handlers) is the research frontier — "
"it may turn out that three modes are sufficient, or it may turn out that " "it may turn out that three modes are sufficient, or it may turn out that "
"user-defined modes unlock something unexpected."))))) "user-defined modes unlock something unexpected.")))))

View File

@@ -67,10 +67,12 @@
(~docs/page :title "Provide / Context / Emit!" (~docs/page :title "Provide / Context / Emit!"
(p :class "text-stone-500 text-sm italic mb-8" (p :class "text-stone-500 text-sm italic mb-8"
"Render-time dynamic scope. " (code "provide") " creates a named scope with a value " "Sugar for " (code "scope") " with a value. " (code "provide") " creates a named scope "
"and an accumulator. " (code "context") " reads the value downward. " "with a value and an accumulator. " (code "context") " reads the value downward. "
(code "emit!") " appends to the accumulator upward. " (code "emitted") " retrieves what was emitted. " (code "emit!") " appends to the accumulator upward. " (code "emitted") " retrieves what was emitted. "
"This is the substrate that spreads, CSSX, and script collection are built on.") "See "
(a :href "/sx/(geography.(scopes))" :class "text-violet-600 hover:underline" "scopes")
" for the unified primitive.")
;; ===================================================================== ;; =====================================================================
;; I. The four primitives ;; I. The four primitives
@@ -215,25 +217,26 @@
;; ===================================================================== ;; =====================================================================
(~docs/section :title "Platform implementation" :id "platform" (~docs/section :title "Platform implementation" :id "platform"
(p "Each platform (Python, JavaScript) must provide five operations. " (p (code "provide") " is sugar for " (code "scope") ". At the platform level, "
"The platform manages per-name stacks — each stack entry has a value and an " (code "provide-push!") " and " (code "provide-pop!") " are aliases for "
"emitted list.") (code "scope-push!") " and " (code "scope-pop!") ". All operations work on a unified "
(code "_scope_stacks") " data structure.")
(~docs/table (~docs/table
:headers (list "Platform primitive" "Purpose") :headers (list "Platform primitive" "Purpose")
:rows (list :rows (list
(list "provide-push!(name, value)" "Push a new scope with value and empty emitted list") (list "scope-push!(name, value)" "Push a new scope with value and empty accumulator")
(list "provide-pop!(name)" "Pop the most recent scope") (list "scope-pop!(name)" "Pop the most recent scope")
(list "context(name, ...default)" "Read value from nearest scope (error if missing and no default)") (list "context(name, ...default)" "Read value from nearest scope (error if missing and no default)")
(list "emit!(name, value)" "Append to nearest scope's emitted list (tolerant: no-op if missing)") (list "emit!(name, value)" "Append to nearest scope's accumulator (tolerant: no-op if missing)")
(list "emitted(name)" "Return list of emitted values from nearest scope"))) (list "emitted(name)" "Return accumulated values from nearest scope")))
(p (code "provide") " itself is a special form in " (p (code "provide") " is a special form in "
(a :href "/sx/(language.(spec.(explore.evaluator)))" :class "font-mono text-violet-600 hover:underline text-sm" "eval.sx") (a :href "/sx/(language.(spec.(explore.evaluator)))" :class "font-mono text-violet-600 hover:underline text-sm" "eval.sx")
" — it calls " (code "provide-push!") ", evaluates the body, " " — it calls " (code "scope-push!") ", evaluates the body, "
"then calls " (code "provide-pop!") ". The five platform primitives are declared in " "then calls " (code "scope-pop!") ". See "
(a :href "/sx/(language.(spec.(explore.boundary)))" :class "font-mono text-violet-600 hover:underline text-sm" "boundary.sx") (a :href "/sx/(geography.(scopes))" :class "text-violet-600 hover:underline" "scopes")
" (Tier 5: Dynamic scope).") " for the full unified platform.")
(~docs/note (~docs/note
(p (strong "Spec explorer: ") "See the provide/emit! primitives in " (p (strong "Spec explorer: ") "See the provide/emit! primitives in "

View File

@@ -11,73 +11,73 @@
(~docs/section :title "1. Signal + Computed + Effect" :id "demo-counter" (~docs/section :title "1. Signal + Computed + Effect" :id "demo-counter"
(p "A signal holds a value. A computed derives from it. Click the buttons — the counter and doubled value update instantly, no server round-trip.") (p "A signal holds a value. A computed derives from it. Click the buttons — the counter and doubled value update instantly, no server round-trip.")
(~reactive-islands/demo/counter :initial 0) (~reactive-islands/index/demo-counter :initial 0)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0)))\n (doubled (computed (fn () (* 2 (deref count))))))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! count dec)) \"\")\n (span (deref count))\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (p \"doubled: \" (deref doubled)))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0)))\n (doubled (computed (fn () (* 2 (deref count))))))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! count dec)) \"\")\n (span (deref count))\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (p \"doubled: \" (deref doubled)))))" "lisp"))
(p (code "(deref count)") " in a text position creates a reactive text node. When " (code "count") " changes, " (em "only that text node") " updates. " (code "doubled") " recomputes automatically. No diffing.")) (p (code "(deref count)") " in a text position creates a reactive text node. When " (code "count") " changes, " (em "only that text node") " updates. " (code "doubled") " recomputes automatically. No diffing."))
(~docs/section :title "2. Temperature Converter" :id "demo-temperature" (~docs/section :title "2. Temperature Converter" :id "demo-temperature"
(p "Two derived values from one signal. Click to change Celsius — Fahrenheit updates reactively.") (p "Two derived values from one signal. Click to change Celsius — Fahrenheit updates reactively.")
(~reactive-islands/demo/temperature) (~reactive-islands/index/demo-temperature)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/temperature ()\n (let ((celsius (signal 20)))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! celsius (fn (c) (- c 5)))) \"5\")\n (span (deref celsius))\n (button :on-click (fn (e) (swap! celsius (fn (c) (+ c 5)))) \"+5\")\n (span \"°C = \")\n (span (+ (* (deref celsius) 1.8) 32))\n (span \"°F\"))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/temperature ()\n (let ((celsius (signal 20)))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! celsius (fn (c) (- c 5)))) \"5\")\n (span (deref celsius))\n (button :on-click (fn (e) (swap! celsius (fn (c) (+ c 5)))) \"+5\")\n (span \"°C = \")\n (span (+ (* (deref celsius) 1.8) 32))\n (span \"°F\"))))" "lisp"))
(p "The actual implementation uses " (code "computed") " for Fahrenheit: " (code "(computed (fn () (+ (* (deref celsius) 1.8) 32)))") ". The " (code "(deref fahrenheit)") " in the span creates a reactive text node that updates when celsius changes.")) (p "The actual implementation uses " (code "computed") " for Fahrenheit: " (code "(computed (fn () (+ (* (deref celsius) 1.8) 32)))") ". The " (code "(deref fahrenheit)") " in the span creates a reactive text node that updates when celsius changes."))
(~docs/section :title "3. Effect + Cleanup: Stopwatch" :id "demo-stopwatch" (~docs/section :title "3. Effect + Cleanup: Stopwatch" :id "demo-stopwatch"
(p "Effects can return cleanup functions. This stopwatch starts a " (code "set-interval") " — the cleanup clears it when the running signal toggles off.") (p "Effects can return cleanup functions. This stopwatch starts a " (code "set-interval") " — the cleanup clears it when the running signal toggles off.")
(~reactive-islands/demo/stopwatch) (~reactive-islands/index/demo-stopwatch)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/stopwatch ()\n (let ((running (signal false))\n (elapsed (signal 0))\n (time-text (create-text-node \"0.0s\"))\n (btn-text (create-text-node \"Start\")))\n ;; Timer: effect creates interval, cleanup clears it\n (effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! elapsed inc)) 100)))\n (fn () (clear-interval id))))))\n ;; Display: updates text node when elapsed changes\n (effect (fn ()\n (let ((e (deref elapsed)))\n (dom-set-text-content time-text\n (str (floor (/ e 10)) \".\" (mod e 10) \"s\")))))\n ;; Button label\n (effect (fn ()\n (dom-set-text-content btn-text\n (if (deref running) \"Stop\" \"Start\"))))\n (div :class \"...\"\n (span time-text)\n (button :on-click (fn (e) (swap! running not)) btn-text)\n (button :on-click (fn (e)\n (reset! running false) (reset! elapsed 0)) \"Reset\"))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/stopwatch ()\n (let ((running (signal false))\n (elapsed (signal 0))\n (time-text (create-text-node \"0.0s\"))\n (btn-text (create-text-node \"Start\")))\n ;; Timer: effect creates interval, cleanup clears it\n (effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! elapsed inc)) 100)))\n (fn () (clear-interval id))))))\n ;; Display: updates text node when elapsed changes\n (effect (fn ()\n (let ((e (deref elapsed)))\n (dom-set-text-content time-text\n (str (floor (/ e 10)) \".\" (mod e 10) \"s\")))))\n ;; Button label\n (effect (fn ()\n (dom-set-text-content btn-text\n (if (deref running) \"Stop\" \"Start\"))))\n (div :class \"...\"\n (span time-text)\n (button :on-click (fn (e) (swap! running not)) btn-text)\n (button :on-click (fn (e)\n (reset! running false) (reset! elapsed 0)) \"Reset\"))))" "lisp"))
(p "Three effects, each tracking different signals. The timer effect's cleanup fires before each re-run — toggling " (code "running") " off clears the interval. No hook rules: effects can appear anywhere, in any order.")) (p "Three effects, each tracking different signals. The timer effect's cleanup fires before each re-run — toggling " (code "running") " off clears the interval. No hook rules: effects can appear anywhere, in any order."))
(~docs/section :title "4. Imperative Pattern" :id "demo-imperative" (~docs/section :title "4. Imperative Pattern" :id "demo-imperative"
(p "For complex reactivity (dynamic classes, conditional text), use the imperative pattern: " (code "create-text-node") " + " (code "effect") " + " (code "dom-set-text-content") ".") (p "For complex reactivity (dynamic classes, conditional text), use the imperative pattern: " (code "create-text-node") " + " (code "effect") " + " (code "dom-set-text-content") ".")
(~reactive-islands/demo/imperative) (~reactive-islands/index/demo-imperative)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/imperative ()\n (let ((count (signal 0))\n (text-node (create-text-node \"0\")))\n ;; Explicit effect: re-runs when count changes\n (effect (fn ()\n (dom-set-text-content text-node (str (deref count)))))\n (div :class \"...\"\n (span text-node)\n (button :on-click (fn (e) (swap! count inc)) \"+\"))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/imperative ()\n (let ((count (signal 0))\n (text-node (create-text-node \"0\")))\n ;; Explicit effect: re-runs when count changes\n (effect (fn ()\n (dom-set-text-content text-node (str (deref count)))))\n (div :class \"...\"\n (span text-node)\n (button :on-click (fn (e) (swap! count inc)) \"+\"))))" "lisp"))
(p "Two patterns exist: " (strong "declarative") " (" (code "(span (deref sig))") " — auto-reactive via " (code "reactive-text") ") and " (strong "imperative") " (" (code "create-text-node") " + " (code "effect") " — explicit, full control). Use declarative for simple text, imperative for dynamic classes, conditional DOM, or complex updates.")) (p "Two patterns exist: " (strong "declarative") " (" (code "(span (deref sig))") " — auto-reactive via " (code "reactive-text") ") and " (strong "imperative") " (" (code "create-text-node") " + " (code "effect") " — explicit, full control). Use declarative for simple text, imperative for dynamic classes, conditional DOM, or complex updates."))
(~docs/section :title "5. Reactive List" :id "demo-reactive-list" (~docs/section :title "5. Reactive List" :id "demo-reactive-list"
(p "When " (code "map") " is used with " (code "(deref signal)") " inside an island, it auto-upgrades to a reactive list. With " (code ":key") " attributes, existing DOM nodes are reused across updates — only additions, removals, and reorderings touch the DOM.") (p "When " (code "map") " is used with " (code "(deref signal)") " inside an island, it auto-upgrades to a reactive list. With " (code ":key") " attributes, existing DOM nodes are reused across updates — only additions, removals, and reorderings touch the DOM.")
(~reactive-islands/demo/reactive-list) (~reactive-islands/index/demo-reactive-list)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/reactive-list ()\n (let ((next-id (signal 1))\n (items (signal (list)))\n (add-item (fn (e)\n (batch (fn ()\n (swap! items (fn (old)\n (append old (dict \"id\" (deref next-id)\n \"text\" (str \"Item \" (deref next-id))))))\n (swap! next-id inc)))))\n (remove-item (fn (id)\n (swap! items (fn (old)\n (filter (fn (item) (not (= (get item \"id\") id))) old))))))\n (div\n (button :on-click add-item \"Add Item\")\n (span (deref (computed (fn () (len (deref items))))) \" items\")\n (ul\n (map (fn (item)\n (li :key (str (get item \"id\"))\n (span (get item \"text\"))\n (button :on-click (fn (e) (remove-item (get item \"id\"))) \"✕\")))\n (deref items))))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/reactive-list ()\n (let ((next-id (signal 1))\n (items (signal (list)))\n (add-item (fn (e)\n (batch (fn ()\n (swap! items (fn (old)\n (append old (dict \"id\" (deref next-id)\n \"text\" (str \"Item \" (deref next-id))))))\n (swap! next-id inc)))))\n (remove-item (fn (id)\n (swap! items (fn (old)\n (filter (fn (item) (not (= (get item \"id\") id))) old))))))\n (div\n (button :on-click add-item \"Add Item\")\n (span (deref (computed (fn () (len (deref items))))) \" items\")\n (ul\n (map (fn (item)\n (li :key (str (get item \"id\"))\n (span (get item \"text\"))\n (button :on-click (fn (e) (remove-item (get item \"id\"))) \"✕\")))\n (deref items))))))" "lisp"))
(p (code ":key") " identifies each list item. When items change, the reconciler matches old and new keys — reusing existing DOM nodes, inserting new ones, and removing stale ones. Without keys, the list falls back to clear-and-rerender. " (code "batch") " groups the two signal writes into one update pass.")) (p (code ":key") " identifies each list item. When items change, the reconciler matches old and new keys — reusing existing DOM nodes, inserting new ones, and removing stale ones. Without keys, the list falls back to clear-and-rerender. " (code "batch") " groups the two signal writes into one update pass."))
(~docs/section :title "6. Input Binding" :id "demo-input-binding" (~docs/section :title "6. Input Binding" :id "demo-input-binding"
(p "The " (code ":bind") " attribute creates a two-way link between a signal and a form element. Type in the input — the signal updates. Change the signal — the input updates. Works with text inputs, checkboxes, radios, textareas, and selects.") (p "The " (code ":bind") " attribute creates a two-way link between a signal and a form element. Type in the input — the signal updates. Change the signal — the input updates. Works with text inputs, checkboxes, radios, textareas, and selects.")
(~reactive-islands/demo/input-binding) (~reactive-islands/index/demo-input-binding)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/input-binding ()\n (let ((name (signal \"\"))\n (agreed (signal false)))\n (div\n (input :type \"text\" :bind name\n :placeholder \"Type your name...\")\n (span \"Hello, \" (strong (deref name)) \"!\")\n (input :type \"checkbox\" :bind agreed)\n (when (deref agreed)\n (p \"Thanks for agreeing!\")))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/input-binding ()\n (let ((name (signal \"\"))\n (agreed (signal false)))\n (div\n (input :type \"text\" :bind name\n :placeholder \"Type your name...\")\n (span \"Hello, \" (strong (deref name)) \"!\")\n (input :type \"checkbox\" :bind agreed)\n (when (deref agreed)\n (p \"Thanks for agreeing!\")))))" "lisp"))
(p (code ":bind") " detects the element type automatically — text inputs use " (code "value") " + " (code "input") " event, checkboxes use " (code "checked") " + " (code "change") " event. The effect only updates the DOM when the value actually changed, preventing cursor jump.")) (p (code ":bind") " detects the element type automatically — text inputs use " (code "value") " + " (code "input") " event, checkboxes use " (code "checked") " + " (code "change") " event. The effect only updates the DOM when the value actually changed, preventing cursor jump."))
(~docs/section :title "7. Portals" :id "demo-portal" (~docs/section :title "7. Portals" :id "demo-portal"
(p "A " (code "portal") " renders children into a DOM node " (em "outside") " the island's subtree. Essential for modals, tooltips, and toasts — anything that must escape " (code "overflow:hidden") " or z-index stacking.") (p "A " (code "portal") " renders children into a DOM node " (em "outside") " the island's subtree. Essential for modals, tooltips, and toasts — anything that must escape " (code "overflow:hidden") " or z-index stacking.")
(~reactive-islands/demo/portal) (~reactive-islands/index/demo-portal)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/portal ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n (if (deref open?) \"Close Modal\" \"Open Modal\"))\n (portal \"#portal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 ...\"\n :on-click (fn (e) (reset! open? false))\n (div :class \"bg-white rounded-lg p-6 ...\"\n :on-click (fn (e) (stop-propagation e))\n (h2 \"Portal Modal\")\n (p \"Rendered outside the island's DOM.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/portal ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n (if (deref open?) \"Close Modal\" \"Open Modal\"))\n (portal \"#portal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 ...\"\n :on-click (fn (e) (reset! open? false))\n (div :class \"bg-white rounded-lg p-6 ...\"\n :on-click (fn (e) (stop-propagation e))\n (h2 \"Portal Modal\")\n (p \"Rendered outside the island's DOM.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp"))
(p "The portal content lives in " (code "#portal-root") " (typically at the page body level), not inside the island. On island disposal, portal content is automatically removed from its target — the " (code "register-in-scope") " mechanism handles cleanup.")) (p "The portal content lives in " (code "#portal-root") " (typically at the page body level), not inside the island. On island disposal, portal content is automatically removed from its target — the " (code "register-in-scope") " mechanism handles cleanup."))
(~docs/section :title "8. Error Boundaries" :id "demo-error-boundary" (~docs/section :title "8. Error Boundaries" :id "demo-error-boundary"
(p "When an island's rendering or effect throws, " (code "error-boundary") " catches the error and renders a fallback. The fallback receives the error and a retry function. Partial effects created before the error are disposed automatically.") (p "When an island's rendering or effect throws, " (code "error-boundary") " catches the error and renders a fallback. The fallback receives the error and a retry function. Partial effects created before the error are disposed automatically.")
(~reactive-islands/demo/error-boundary) (~reactive-islands/index/demo-error-boundary)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/error-boundary ()\n (let ((throw? (signal false)))\n (error-boundary\n ;; Fallback: receives (err retry-fn)\n (fn (err retry-fn)\n (div :class \"p-3 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700\" (error-message err))\n (button :on-click (fn (e)\n (reset! throw? false) (invoke retry-fn))\n \"Retry\")))\n ;; Children: the happy path\n (do\n (when (deref throw?) (error \"Intentional explosion!\"))\n (p \"Everything is fine.\")))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/error-boundary ()\n (let ((throw? (signal false)))\n (error-boundary\n ;; Fallback: receives (err retry-fn)\n (fn (err retry-fn)\n (div :class \"p-3 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700\" (error-message err))\n (button :on-click (fn (e)\n (reset! throw? false) (invoke retry-fn))\n \"Retry\")))\n ;; Children: the happy path\n (do\n (when (deref throw?) (error \"Intentional explosion!\"))\n (p \"Everything is fine.\")))))" "lisp"))
(p "React equivalent: " (code "componentDidCatch") " / " (code "ErrorBoundary") ". SX's version is simpler — one form, not a class. The " (code "error-boundary") " form is a render-dom special form in " (code "adapter-dom.sx") ".")) (p "React equivalent: " (code "componentDidCatch") " / " (code "ErrorBoundary") ". SX's version is simpler — one form, not a class. The " (code "error-boundary") " form is a render-dom special form in " (code "adapter-dom.sx") "."))
(~docs/section :title "9. Refs — Imperative DOM Access" :id "demo-refs" (~docs/section :title "9. Refs — Imperative DOM Access" :id "demo-refs"
(p "The " (code ":ref") " attribute captures a DOM element handle into a dict. Use it for imperative operations: focusing, measuring, reading values.") (p "The " (code ":ref") " attribute captures a DOM element handle into a dict. Use it for imperative operations: focusing, measuring, reading values.")
(~reactive-islands/demo/refs) (~reactive-islands/index/demo-refs)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/refs ()\n (let ((my-ref (dict \"current\" nil))\n (msg (signal \"\")))\n (input :ref my-ref :type \"text\"\n :placeholder \"I can be focused programmatically\")\n (button :on-click (fn (e)\n (dom-focus (get my-ref \"current\")))\n \"Focus Input\")\n (button :on-click (fn (e)\n (let ((el (get my-ref \"current\")))\n (reset! msg (str \"value: \" (dom-get-prop el \"value\")))))\n \"Read Input\")\n (when (not (= (deref msg) \"\"))\n (p (deref msg)))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/refs ()\n (let ((my-ref (dict \"current\" nil))\n (msg (signal \"\")))\n (input :ref my-ref :type \"text\"\n :placeholder \"I can be focused programmatically\")\n (button :on-click (fn (e)\n (dom-focus (get my-ref \"current\")))\n \"Focus Input\")\n (button :on-click (fn (e)\n (let ((el (get my-ref \"current\")))\n (reset! msg (str \"value: \" (dom-get-prop el \"value\")))))\n \"Read Input\")\n (when (not (= (deref msg) \"\"))\n (p (deref msg)))))" "lisp"))
(p "React equivalent: " (code "useRef") ". In SX, a ref is just " (code "(dict \"current\" nil)") " — no special API. The " (code ":ref") " attribute sets " (code "(dict-set! ref \"current\" el)") " when the element is created. Read it with " (code "(get ref \"current\")") ".")) (p "React equivalent: " (code "useRef") ". In SX, a ref is just " (code "(dict \"current\" nil)") " — no special API. The " (code ":ref") " attribute sets " (code "(dict-set! ref \"current\" el)") " when the element is created. Read it with " (code "(get ref \"current\")") "."))
(~docs/section :title "10. Dynamic Class and Style" :id "demo-dynamic-class" (~docs/section :title "10. Dynamic Class and Style" :id "demo-dynamic-class"
(p "React uses " (code "className") " and " (code "style") " props with state. SX does the same — " (code "(deref signal)") " inside a " (code ":class") " or " (code ":style") " attribute creates a reactive binding. The attribute updates when the signal changes.") (p "React uses " (code "className") " and " (code "style") " props with state. SX does the same — " (code "(deref signal)") " inside a " (code ":class") " or " (code ":style") " attribute creates a reactive binding. The attribute updates when the signal changes.")
(~reactive-islands/demo/dynamic-class) (~reactive-islands/index/demo-dynamic-class)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/dynamic-class ()\n (let ((danger (signal false))\n (size (signal 16)))\n (div\n (button :on-click (fn (e) (swap! danger not))\n (if (deref danger) \"Safe mode\" \"Danger mode\"))\n (button :on-click (fn (e) (swap! size (fn (s) (+ s 2))))\n \"Bigger\")\n ;; Reactive class — recomputed when danger changes\n (div :class (str \"p-3 rounded font-medium \"\n (if (deref danger)\n \"bg-red-100 text-red-800\"\n \"bg-green-100 text-green-800\"))\n ;; Reactive style — recomputed when size changes\n :style (str \"font-size:\" (deref size) \"px\")\n \"This element's class and style are reactive.\"))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/dynamic-class ()\n (let ((danger (signal false))\n (size (signal 16)))\n (div\n (button :on-click (fn (e) (swap! danger not))\n (if (deref danger) \"Safe mode\" \"Danger mode\"))\n (button :on-click (fn (e) (swap! size (fn (s) (+ s 2))))\n \"Bigger\")\n ;; Reactive class — recomputed when danger changes\n (div :class (str \"p-3 rounded font-medium \"\n (if (deref danger)\n \"bg-red-100 text-red-800\"\n \"bg-green-100 text-green-800\"))\n ;; Reactive style — recomputed when size changes\n :style (str \"font-size:\" (deref size) \"px\")\n \"This element's class and style are reactive.\"))))" "lisp"))
(p "React equivalent: " (code "className={danger ? 'red' : 'green'}") " and " (code "style={{fontSize: size}}") ". In SX the " (code "str") " + " (code "if") " + " (code "deref") " pattern handles it — no " (code "classnames") " library needed. For complex conditional classes, use a " (code "computed") " or a CSSX " (code "defcomp") " that returns a class string.")) (p "React equivalent: " (code "className={danger ? 'red' : 'green'}") " and " (code "style={{fontSize: size}}") ". In SX the " (code "str") " + " (code "if") " + " (code "deref") " pattern handles it — no " (code "classnames") " library needed. For complex conditional classes, use a " (code "computed") " or a CSSX " (code "defcomp") " that returns a class string."))
(~docs/section :title "11. Resource + Suspense Pattern" :id "demo-resource" (~docs/section :title "11. Resource + Suspense Pattern" :id "demo-resource"
(p (code "resource") " wraps an async operation into a signal with " (code "loading") "/" (code "data") "/" (code "error") " states. Combined with " (code "cond") " + " (code "deref") ", this is the suspense pattern — no special form needed.") (p (code "resource") " wraps an async operation into a signal with " (code "loading") "/" (code "data") "/" (code "error") " states. Combined with " (code "cond") " + " (code "deref") ", this is the suspense pattern — no special form needed.")
(~reactive-islands/demo/resource) (~reactive-islands/index/demo-resource)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/resource ()\n (let ((data (resource (fn ()\n ;; Any promise-returning function\n (promise-delayed 1500\n (dict \"name\" \"Ada Lovelace\"\n \"role\" \"First Programmer\"))))))\n ;; This IS the suspense pattern:\n (let ((state (deref data)))\n (cond\n (get state \"loading\")\n (div \"Loading...\")\n (get state \"error\")\n (div \"Error: \" (get state \"error\"))\n :else\n (div (get (get state \"data\") \"name\"))))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/resource ()\n (let ((data (resource (fn ()\n ;; Any promise-returning function\n (promise-delayed 1500\n (dict \"name\" \"Ada Lovelace\"\n \"role\" \"First Programmer\"))))))\n ;; This IS the suspense pattern:\n (let ((state (deref data)))\n (cond\n (get state \"loading\")\n (div \"Loading...\")\n (get state \"error\")\n (div \"Error: \" (get state \"error\"))\n :else\n (div (get (get state \"data\") \"name\"))))))" "lisp"))
(p "React equivalent: " (code "Suspense") " + " (code "use()") " or " (code "useSWR") ". SX doesn't need a special " (code "suspense") " form because " (code "resource") " returns a signal and " (code "cond") " + " (code "deref") " creates reactive conditional rendering. When the promise resolves, the signal updates and the " (code "cond") " branch switches automatically.")) (p "React equivalent: " (code "Suspense") " + " (code "use()") " or " (code "useSWR") ". SX doesn't need a special " (code "suspense") " form because " (code "resource") " returns a signal and " (code "cond") " + " (code "deref") " creates reactive conditional rendering. When the promise resolves, the signal updates and the " (code "cond") " branch switches automatically."))
(~docs/section :title "12. Transition Pattern" :id "demo-transition" (~docs/section :title "12. Transition Pattern" :id "demo-transition"
(p "React's " (code "startTransition") " defers non-urgent updates so typing stays responsive. In SX: " (code "schedule-idle") " + " (code "batch") ". The filter runs during idle time, not blocking the input event.") (p "React's " (code "startTransition") " defers non-urgent updates so typing stays responsive. In SX: " (code "schedule-idle") " + " (code "batch") ". The filter runs during idle time, not blocking the input event.")
(~reactive-islands/demo/transition) (~reactive-islands/index/demo-transition)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/transition ()\n (let ((query (signal \"\"))\n (all-items (list \"Signals\" \"Effects\" ...))\n (filtered (signal (list)))\n (pending (signal false)))\n (reset! filtered all-items)\n ;; Filter effect — deferred via schedule-idle\n (effect (fn ()\n (let ((q (lower (deref query))))\n (if (= q \"\")\n (do (reset! pending false)\n (reset! filtered all-items))\n (do (reset! pending true)\n (schedule-idle (fn ()\n (batch (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower item) q))\n all-items))\n (reset! pending false))))))))))\n (div\n (input :bind query :placeholder \"Filter...\")\n (when (deref pending) (span \"Filtering...\"))\n (ul (map (fn (item) (li :key item item))\n (deref filtered))))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/transition ()\n (let ((query (signal \"\"))\n (all-items (list \"Signals\" \"Effects\" ...))\n (filtered (signal (list)))\n (pending (signal false)))\n (reset! filtered all-items)\n ;; Filter effect — deferred via schedule-idle\n (effect (fn ()\n (let ((q (lower (deref query))))\n (if (= q \"\")\n (do (reset! pending false)\n (reset! filtered all-items))\n (do (reset! pending true)\n (schedule-idle (fn ()\n (batch (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower item) q))\n all-items))\n (reset! pending false))))))))))\n (div\n (input :bind query :placeholder \"Filter...\")\n (when (deref pending) (span \"Filtering...\"))\n (ul (map (fn (item) (li :key item item))\n (deref filtered))))))" "lisp"))
(p "React equivalent: " (code "startTransition(() => setFiltered(...))") ". SX uses " (code "schedule-idle") " (" (code "requestIdleCallback") " under the hood) to defer the expensive " (code "filter") " operation, and " (code "batch") " to group the result into one update. Fine-grained signals already avoid the jank that makes transitions critical in React — this pattern is for truly expensive computations.")) (p "React equivalent: " (code "startTransition(() => setFiltered(...))") ". SX uses " (code "schedule-idle") " (" (code "requestIdleCallback") " under the hood) to defer the expensive " (code "filter") " operation, and " (code "batch") " to group the result into one update. Fine-grained signals already avoid the jank that makes transitions critical in React — this pattern is for truly expensive computations."))

194
sx/sx/scopes.sx Normal file
View File

@@ -0,0 +1,194 @@
;; ---------------------------------------------------------------------------
;; Scopes — the unified primitive beneath provide, collect!, and spreads
;; ---------------------------------------------------------------------------
;; ---- Demo components ----
(defcomp ~geography/demo-scope-basic ()
(div :class "space-y-2"
(scope "demo-theme" :value "violet"
(div :class "rounded-lg p-3 bg-violet-50 border border-violet-200"
(p :class "text-sm text-violet-800 font-semibold"
(str "Inside scope: theme = " (context "demo-theme")))
(p :class "text-xs text-stone-500" "scope creates a named scope. context reads it.")))
(div :class "rounded-lg p-3 bg-stone-50 border border-stone-200"
(p :class "text-sm text-stone-600" "Outside scope: no context available."))))
(defcomp ~geography/demo-scope-emit ()
(div :class "space-y-2"
(scope "demo-deps"
(div :class "rounded-lg p-3 bg-stone-50 border border-stone-200"
(p :class "text-sm text-stone-700"
(emit! "demo-deps" "lodash")
(emit! "demo-deps" "react")
"Components emit their dependencies upward."))
(div :class "rounded-lg p-3 bg-violet-50 border border-violet-200"
(p :class "text-sm text-violet-800 font-semibold" "Emitted:")
(ul :class "text-xs text-stone-600 list-disc pl-5"
(map (fn (d) (li (code d))) (emitted "demo-deps")))))))
(defcomp ~geography/demo-scope-dedup ()
(div :class "space-y-2"
(div :class "rounded-lg p-3 bg-stone-50 border border-stone-200"
(p :class "text-sm text-stone-700"
(collect! "demo-css-dedup" ".card { padding: 1rem }")
(collect! "demo-css-dedup" ".card { padding: 1rem }")
(collect! "demo-css-dedup" ".btn { color: blue }")
"Three collect! calls, two identical. Only unique values kept."))
(div :class "rounded-lg p-3 bg-violet-50 border border-violet-200"
(p :class "text-sm text-violet-800 font-semibold"
(str "Collected: " (len (collected "demo-css-dedup")) " rules"))
(ul :class "text-xs text-stone-600 list-disc pl-5"
(map (fn (r) (li (code r))) (collected "demo-css-dedup"))))))
;; ---- Layout helper ----
(defcomp ~geography/scopes-demo-example (&key demo code)
(div :class "grid grid-cols-1 lg:grid-cols-2 gap-4 my-6 items-start"
(div :class "border border-dashed border-stone-300 rounded-lg p-4 bg-stone-50 min-h-[80px]"
demo)
(div :class "not-prose bg-stone-100 rounded-lg p-4 overflow-x-auto"
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code code)))))
;; ---- Page content ----
(defcomp ~geography/scopes-content ()
(~docs/page :title "Scopes"
(p :class "text-stone-500 text-sm italic mb-8"
"The unified primitive. " (code "scope") " creates a named scope with an optional value "
"and an accumulator. " (code "provide") ", " (code "collect!") ", spreads, islands — "
"they all resolve to scope operations at the platform level.")
;; =====================================================================
;; I. The primitive
;; =====================================================================
(~docs/section :title "The primitive" :id "primitive"
(p (code "scope") " is a special form that pushes a named scope, evaluates its body, "
"then pops it. The scope has three properties: a name, a downward value, and an "
"upward accumulator.")
(~docs/code :code (highlight "(scope name body...) ;; scope with no value\n(scope name :value v body...) ;; scope with downward value" "lisp"))
(p "Within the body, " (code "context") " reads the value, " (code "emit!") " appends "
"to the accumulator, and " (code "emitted") " reads what was accumulated.")
(~geography/scopes-demo-example
:demo (~geography/demo-scope-basic)
:code (highlight "(scope \"theme\" :value \"violet\"\n (context \"theme\")) ;; → \"violet\"\n\n;; Nested scopes shadow:\n(scope \"x\" :value \"outer\"\n (scope \"x\" :value \"inner\"\n (context \"x\")) ;; → \"inner\"\n (context \"x\")) ;; → \"outer\"" "lisp")))
;; =====================================================================
;; II. Sugar forms
;; =====================================================================
(~docs/section :title "Sugar forms" :id "sugar"
(p "Nobody writes " (code "scope") " directly. The sugar forms are the API:")
(~docs/table
:headers (list "Sugar" "Expands to" "Used for")
:rows (list
(list "provide" "(scope name :value v body...)" "Downward context passing")
(list "collect!" "Lazy root scope + dedup emit" "CSS rule accumulation")
(list "Spreads" "(scope \"element-attrs\" ...)" "Child-to-parent attrs (implicit)")))
(~docs/subsection :title "provide — scope with a value"
(p (code "(provide name value body...)") " is exactly "
(code "(scope name :value value body...)") ". It exists because "
"the two-arg form is the common case.")
(~docs/code :code (highlight ";; These are equivalent:\n(provide \"theme\" {:primary \"violet\"}\n (h1 \"hello\"))\n\n(scope \"theme\" :value {:primary \"violet\"}\n (h1 \"hello\"))" "lisp")))
(~docs/subsection :title "collect! — lazy root scope with dedup"
(p (code "collect!") " is the most interesting sugar. When called, if no scope exists "
"for that name, it lazily creates a root scope with deduplication enabled. "
"Then it emits into it.")
(~geography/scopes-demo-example
:demo (~geography/demo-scope-dedup)
:code (highlight ";; collect! creates a lazy root scope:\n(collect! \"css\" \".card { pad: 1rem }\")\n(collect! \"css\" \".card { pad: 1rem }\") ;; deduped!\n(collect! \"css\" \".btn { color: blue }\")\n(collected \"css\") ;; → 2 rules\n\n;; Equivalent to:\n(scope \"css\" ;; with dedup\n (emit! \"css\" ...)\n (emitted \"css\"))" "lisp"))
(p (code "collected") " is an alias for " (code "emitted") ". "
(code "clear-collected!") " clears the accumulator."))
(~docs/subsection :title "Spreads — implicit element scope"
(p "Every element rendering function wraps its children in "
(code "(scope-push! \"element-attrs\" nil)") ". Spread children "
(code "emit!") " their attrs into this scope. After rendering, the element "
"merges the emitted attrs.")
(p "See the "
(a :href "/sx/(geography.(spreads))" :class "text-violet-600 hover:underline" "spreads article")
" for the full mechanism.")))
;; =====================================================================
;; III. Accumulator: upward data flow
;; =====================================================================
(~docs/section :title "Upward data flow" :id "upward"
(~geography/scopes-demo-example
:demo (~geography/demo-scope-emit)
:code (highlight "(scope \"deps\"\n (emit! \"deps\" \"lodash\")\n (emit! \"deps\" \"react\")\n (emitted \"deps\")) ;; → (\"lodash\" \"react\")" "lisp"))
(p "Accumulation always goes to the " (em "nearest") " enclosing scope with that name. "
"This is what makes nested elements work — a spread inside a nested "
(code "span") " emits to the " (code "span") "'s scope, not an outer "
(code "div") "'s scope."))
;; =====================================================================
;; IV. Platform implementation
;; =====================================================================
(~docs/section :title "Platform implementation" :id "platform"
(p "Each platform (Python, JavaScript) maintains a single data structure:")
(~docs/code :code (highlight "_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" "python"))
(p "Six operations on this structure:")
(~docs/table
:headers (list "Operation" "Purpose")
:rows (list
(list "scope-push!(name, value)" "Push {value, emitted: [], dedup: false}")
(list "scope-pop!(name)" "Pop the most recent scope")
(list "context(name, default?)" "Read value from nearest scope")
(list "emit!(name, value)" "Append to nearest scope's accumulator (respects dedup)")
(list "emitted(name)" "Read accumulated values from nearest scope")
(list "collect!(name, value)" "Lazy push root scope with dedup, then emit")))
(p (code "provide-push!") " and " (code "provide-pop!") " are aliases for "
(code "scope-push!") " and " (code "scope-pop!") ". "
"All adapter code uses " (code "scope-push!") "/" (code "scope-pop!") " directly."))
;; =====================================================================
;; V. Unification
;; =====================================================================
(~docs/section :title "What scope unifies" :id "unification"
(p "Before scopes, the platform had two separate mechanisms:")
(~docs/code :code (highlight ";; Before: two mechanisms\n_provide_stacks = {} ;; {name: [{value, emitted: []}]}\n_collect_buckets = {} ;; {name: [values...]}\n\n;; After: one mechanism\n_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" "python"))
(p "The unification is not just code cleanup. It means:")
(ul :class "space-y-1"
(li (code "collect!") " can be nested inside " (code "provide") " scopes — "
"they share the same stack.")
(li "A component can " (code "emit!") " and " (code "collect!") " into the same scope — "
"they use the same accumulator.")
(li "The dedup flag is per-scope, not per-mechanism — a " (code "provide") " scope "
"has no dedup, a " (code "collect!") " root scope has dedup."))
(p "See the "
(a :href "/sx/(etc.(plan.scoped-effects))" :class "text-violet-600 hover:underline" "scoped effects plan")
" for the full design rationale and future phases (reactive scopes, morph scopes).")
(~docs/note
(p (strong "Spec: ") "The " (code "scope") " special form is in "
(a :href "/sx/(language.(spec.(explore.evaluator)))" :class "font-mono text-violet-600 hover:underline text-sm" "eval.sx")
". Platform primitives are declared in "
(a :href "/sx/(language.(spec.(explore.boundary)))" :class "font-mono text-violet-600 hover:underline text-sm" "boundary.sx")
" (Tier 5: Scoped effects).")))))

View File

@@ -89,8 +89,9 @@
(p :class "text-stone-500 text-sm italic mb-8" (p :class "text-stone-500 text-sm italic mb-8"
"A spread is a value that, when returned as a child of an element, " "A spread is a value that, when returned as a child of an element, "
"injects attributes onto its parent instead of rendering as content. " "injects attributes onto its parent instead of rendering as content. "
"Internally, spreads work through " (code "provide") "/" (code "emit!") " — " "Internally, spreads work through "
"every element creates a provider scope, and spread children emit into it.") (a :href "/sx/(geography.(scopes))" :class "text-violet-600 hover:underline" "scopes")
" — every element creates a scope, and spread children emit into it.")
;; ===================================================================== ;; =====================================================================
;; I. How it works ;; I. How it works
@@ -125,9 +126,9 @@
;; ===================================================================== ;; =====================================================================
(~docs/section :title "collect! — the other upward channel" :id "collect" (~docs/section :title "collect! — the other upward channel" :id "collect"
(p "Spreads use " (code "provide") "/" (code "emit!") " (scoped, no dedup). " (p "Spreads use " (code "scope") "/" (code "emit!") " (scoped, no dedup). "
(code "collect!") "/" (code "collected") " is a separate upward channel — " (code "collect!") "/" (code "collected") " is also backed by scopes — "
"global, with automatic deduplication. Used for CSS rule accumulation.") "a lazy root scope with automatic deduplication. Used for CSS rule accumulation.")
(~docs/code :code (highlight ";; Deep inside a component tree:\n(collect! \"cssx\" \".sx-bg-red-500{background:red}\")\n\n;; At the flush point (once, in the layout):\n(let ((rules (collected \"cssx\")))\n (clear-collected! \"cssx\")\n (raw! (str \"<style>\" (join \"\" rules) \"</style>\")))" "lisp")) (~docs/code :code (highlight ";; Deep inside a component tree:\n(collect! \"cssx\" \".sx-bg-red-500{background:red}\")\n\n;; At the flush point (once, in the layout):\n(let ((rules (collected \"cssx\")))\n (clear-collected! \"cssx\")\n (raw! (str \"<style>\" (join \"\" rules) \"</style>\")))" "lisp"))
(p "Both are upward communication through the render tree, but with different " (p "Both are upward communication through the render tree, but with different "
"semantics — " (code "emit!") " is scoped to the nearest provider, " "semantics — " (code "emit!") " is scoped to the nearest provider, "

View File

@@ -617,6 +617,12 @@
;; Provide / Emit! section (under Geography) ;; Provide / Emit! section (under Geography)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defpage scopes-index
:path "/geography/scopes/"
:auth :public
:layout :sx-docs
:content (~layouts/doc :path "/sx/(geography.(scopes))" (~geography/scopes-content)))
(defpage provide-index (defpage provide-index
:path "/geography/provide/" :path "/geography/provide/"
:auth :public :auth :public