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

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