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