Add provide/context/emit!/emitted — render-time dynamic scope
Four new primitives for scoped downward value passing and upward accumulation through the render tree. Specced in .sx, bootstrapped to Python and JS across all adapters (eval, html, sx, dom, async). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,46 @@ def _collect_reset():
|
||||
_collect_buckets = {}
|
||||
|
||||
|
||||
# Render-time dynamic scope stacks (provide/context/emit!)
|
||||
_provide_stacks: dict[str, list[dict]] = {}
|
||||
|
||||
|
||||
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 provide_pop(name):
|
||||
"""Pop the most recent provider scope for name."""
|
||||
if name in _provide_stacks and _provide_stacks[name]:
|
||||
_provide_stacks[name].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"]
|
||||
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. Error if no provider."""
|
||||
if name in _provide_stacks and _provide_stacks[name]:
|
||||
_provide_stacks[name][-1]["emitted"].append(value)
|
||||
else:
|
||||
raise RuntimeError(f"No provider for emit!: {name}")
|
||||
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 []
|
||||
|
||||
|
||||
def sx_truthy(x):
|
||||
"""SX truthiness: everything is truthy except False, None, and NIL."""
|
||||
if x is False:
|
||||
@@ -905,6 +945,12 @@ 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
|
||||
PRIMITIVES["provide-push!"] = provide_push
|
||||
PRIMITIVES["provide-pop!"] = provide_pop
|
||||
PRIMITIVES["context"] = sx_context
|
||||
PRIMITIVES["emit!"] = sx_emit
|
||||
PRIMITIVES["emitted"] = sx_emitted
|
||||
|
||||
|
||||
def is_primitive(name):
|
||||
@@ -1349,6 +1395,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 == 'provide')):
|
||||
return sf_provide(args, env)
|
||||
elif sx_truthy((name == 'map')):
|
||||
return ho_map(args, env)
|
||||
elif sx_truthy((name == 'map-indexed')):
|
||||
@@ -1840,6 +1888,19 @@ def sf_dynamic_wind(args, env):
|
||||
call_thunk(after, env)
|
||||
return result
|
||||
|
||||
# sf-provide
|
||||
def sf_provide(args, env):
|
||||
_cells = {}
|
||||
name = trampoline(eval_expr(first(args), env))
|
||||
val = trampoline(eval_expr(nth(args, 1), env))
|
||||
body_exprs = slice(args, 2)
|
||||
_cells['result'] = NIL
|
||||
provide_push(name, val)
|
||||
for e in body_exprs:
|
||||
_cells['result'] = trampoline(eval_expr(e, env))
|
||||
provide_pop(name)
|
||||
return _cells['result']
|
||||
|
||||
# expand-macro
|
||||
def expand_macro(mac, raw_args, env):
|
||||
local = env_merge(macro_closure(mac), env)
|
||||
@@ -2189,7 +2250,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']
|
||||
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-form?
|
||||
def is_render_html_form(name):
|
||||
@@ -2291,6 +2352,15 @@ 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('', filter(lambda r: (not sx_truthy(is_spread(r))), 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 == '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)
|
||||
result = (render_to_html(nth(expr, body_start), env) if sx_truthy((body_count == 1)) else join('', filter(lambda r: (not sx_truthy(is_spread(r))), map(lambda i: render_to_html(nth(expr, i), env), range(body_start, (body_start + body_count))))))
|
||||
provide_pop(prov_name)
|
||||
return result
|
||||
else:
|
||||
return render_value_to_html(trampoline(eval_expr(expr, env)), env)
|
||||
|
||||
@@ -2529,7 +2599,7 @@ def aser_call(name, args, env):
|
||||
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']
|
||||
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']
|
||||
|
||||
# HO_FORM_NAMES
|
||||
HO_FORM_NAMES = ['map', 'map-indexed', 'filter', 'reduce', 'some', 'every?', 'for-each']
|
||||
@@ -2626,6 +2696,15 @@ 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 == '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)
|
||||
for body in slice(args, 2):
|
||||
_cells['result'] = aser(body, env)
|
||||
provide_pop(prov_name)
|
||||
return _cells['result']
|
||||
else:
|
||||
return trampoline(eval_expr(expr, env))
|
||||
|
||||
@@ -3759,7 +3838,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']
|
||||
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-form?
|
||||
def async_render_form_p(name):
|
||||
@@ -3823,6 +3902,15 @@ 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('', filter(lambda r: (not sx_truthy(is_spread(r))), (await async_map_fn_render(f, coll, env, ctx))))
|
||||
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)
|
||||
result = ((await async_render(nth(expr, body_start), env, ctx)) if sx_truthy((body_count == 1)) else (lambda results: join('', filter(lambda r: (not sx_truthy(is_spread(r))), results)))((await async_map_render(slice(expr, body_start), env, ctx))))
|
||||
provide_pop(prov_name)
|
||||
return result
|
||||
else:
|
||||
return (await async_render((await async_eval(expr, env, ctx)), env, ctx))
|
||||
|
||||
@@ -4148,7 +4236,7 @@ async def async_aser_call(name, args, env, ctx):
|
||||
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']
|
||||
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_HO_NAMES
|
||||
ASYNC_ASER_HO_NAMES = ['map', 'map-indexed', 'filter', 'for-each']
|
||||
@@ -4242,6 +4330,15 @@ 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 == '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)
|
||||
for body in slice(args, 2):
|
||||
_cells['result'] = (await async_aser(body, env, ctx))
|
||||
provide_pop(prov_name)
|
||||
return _cells['result']
|
||||
else:
|
||||
return (await async_eval(expr, env, ctx))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user