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

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