From ea2b71cfa3aa17930dffc3e071c666bfe96b2e4a Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 13 Mar 2026 02:58:21 +0000 Subject: [PATCH] =?UTF-8?q?Add=20provide/context/emit!/emitted=20=E2=80=94?= =?UTF-8?q?=20render-time=20dynamic=20scope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- shared/static/scripts/sx-browser.js | 98 +++++++++++++++++++++++--- shared/sx/ref/adapter-async.sx | 29 +++++++- shared/sx/ref/adapter-dom.sx | 15 +++- shared/sx/ref/adapter-html.sx | 17 ++++- shared/sx/ref/adapter-sx.sx | 13 +++- shared/sx/ref/bootstrap_py.py | 5 ++ shared/sx/ref/boundary.sx | 48 +++++++++++++ shared/sx/ref/eval.sx | 20 ++++++ shared/sx/ref/js.sx | 5 ++ shared/sx/ref/platform_js.py | 41 +++++++++++ shared/sx/ref/platform_py.py | 46 ++++++++++++ shared/sx/ref/py.sx | 5 ++ shared/sx/ref/render.sx | 7 ++ shared/sx/ref/sx_ref.py | 105 ++++++++++++++++++++++++++-- 14 files changed, 436 insertions(+), 18 deletions(-) diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 4e33d60..5d45290 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-13T02:18:19Z"; + var SX_VERSION = "2026-03-13T02:54:01Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -87,6 +87,7 @@ SxSpread.prototype._spread = true; var _collectBuckets = {}; + var _provideStacks = {}; function isSym(x) { return x != null && x._sym === true; } function isKw(x) { return x != null && x._kw === true; } @@ -162,6 +163,35 @@ if (_collectBuckets[bucket]) _collectBuckets[bucket] = []; } + 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 (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); + } else { + throw new Error("No provider for emit!: " + name); + } + return NIL; + } + function sxEmitted(name) { + if (_provideStacks[name] && _provideStacks[name].length) { + return _provideStacks[name][_provideStacks[name].length - 1].emitted.slice(); + } + return []; + } + function lambdaParams(f) { return f.params; } function lambdaBody(f) { return f.body; } function lambdaClosure(f) { return f.closure; } @@ -495,6 +525,12 @@ PRIMITIVES["collect!"] = sxCollect; PRIMITIVES["collected"] = sxCollected; PRIMITIVES["clear-collected!"] = sxClearCollected; + // provide/context/emit! — render-time dynamic scope + PRIMITIVES["provide-push!"] = providePush; + PRIMITIVES["provide-pop!"] = providePop; + PRIMITIVES["context"] = sxContext; + PRIMITIVES["emit!"] = sxEmit; + PRIMITIVES["emitted"] = sxEmitted; function isPrimitive(name) { return name in PRIMITIVES; } @@ -760,10 +796,10 @@ 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() { 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 == "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 == "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); 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))); })(); }; @@ -1170,6 +1206,18 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai })(); })(); }; + // sf-provide + var sfProvide = function(args, env) { return (function() { + var name = trampoline(evalExpr(first(args), env)); + var val = trampoline(evalExpr(nth(args, 1), env)); + var bodyExprs = slice(args, 2); + var result = NIL; + providePush(name, val); + { var _c = bodyExprs; for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; result = trampoline(evalExpr(e, env)); } } + providePop(name); + return result; +})(); }; + // expand-macro var expandMacro = function(mac, rawArgs, env) { return (function() { var local = envMerge(macroClosure(mac), env); @@ -1468,7 +1516,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 val; return escapeHtml((String(val))); })(); }; // 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"]; + 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"]; // render-html-form? var isRenderHtmlForm = function(name) { return contains(RENDER_HTML_FORMS, name); }; @@ -1517,7 +1565,18 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m = var f = trampoline(evalExpr(nth(expr, 1), env)); var coll = trampoline(evalExpr(nth(expr, 2), env)); return join("", filter(function(r) { return !isSxTruthy(isSpread(r)); }, map(function(item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [item], env) : renderToHtml(apply(f, [item]), env)); }, coll))); -})() : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))))))))); }; +})() : (isSxTruthy((name == "provide")) ? (function() { + var provName = trampoline(evalExpr(nth(expr, 1), env)); + var provVal = trampoline(evalExpr(nth(expr, 2), env)); + var bodyStart = 3; + var bodyCount = (len(expr) - 3); + providePush(provName, provVal); + return (function() { + var result = (isSxTruthy((bodyCount == 1)) ? renderToHtml(nth(expr, bodyStart), env) : join("", filter(function(r) { return !isSxTruthy(isSpread(r)); }, map(function(i) { return renderToHtml(nth(expr, i), env); }, range(bodyStart, (bodyStart + bodyCount)))))); + providePop(provName); + return result; +})(); +})() : renderValueToHtml(trampoline(evalExpr(expr, env)), env))))))))))))); }; // render-lambda-html var renderLambdaHtml = function(f, args, env) { return (function() { @@ -1722,7 +1781,7 @@ return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if })(); }; // 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"]; + 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"]; // HO_FORM_NAMES var HO_FORM_NAMES = ["map", "map-indexed", "filter", "reduce", "some", "every?", "for-each"]; @@ -1793,7 +1852,15 @@ return result; }, args); return append_b(results, aser(lambdaBody(f), local)); })() : invoke(f, item)); } } 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) : trampoline(evalExpr(expr, env))))))))))))))); +})() : (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() { + var provName = trampoline(evalExpr(first(args), env)); + var provVal = trampoline(evalExpr(nth(args, 1), env)); + var result = NIL; + providePush(provName, provVal); + { var _c = slice(args, 2); for (var _i = 0; _i < _c.length; _i++) { var body = _c[_i]; result = aser(body, env); } } + providePop(provName); + return result; +})() : trampoline(evalExpr(expr, env)))))))))))))))); })(); }; // eval-case-aser @@ -1926,7 +1993,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme var renderDomUnknownComponent = function(name) { return error((String("Unknown component: ") + String(name))); }; // 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"]; + 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"]; // render-dom-form? var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); }; @@ -2060,7 +2127,15 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme return domAppend(frag, val); })(); } } return frag; -})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns)))))))))))))); }; +})() : (isSxTruthy((name == "provide")) ? (function() { + var provName = trampoline(evalExpr(nth(expr, 1), env)); + var provVal = trampoline(evalExpr(nth(expr, 2), env)); + var frag = createFragment(); + providePush(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)); } } + providePop(provName); + return frag; +})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns))))))))))))))); }; // render-lambda-dom var renderLambdaDom = function(f, args, env, ns) { return (function() { @@ -6453,6 +6528,11 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { collect: sxCollect, collected: sxCollected, clearCollected: sxClearCollected, + providePush: providePush, + providePop: providePop, + context: sxContext, + emit: sxEmit, + emitted: sxEmitted, _version: "ref-2.0 (boot+dom+engine+html+orchestration+parser+sx, bootstrap-compiled)" }; diff --git a/shared/sx/ref/adapter-async.sx b/shared/sx/ref/adapter-async.sx index 2e56652..5b47d82 100644 --- a/shared/sx/ref/adapter-async.sx +++ b/shared/sx/ref/adapter-async.sx @@ -341,7 +341,7 @@ (list "if" "when" "cond" "case" "let" "let*" "begin" "do" "define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler" "deftype" "defeffect" - "map" "map-indexed" "filter" "for-each")) + "map" "map-indexed" "filter" "for-each" "provide")) (define async-render-form? :effects [] (fn ((name :as string)) @@ -434,6 +434,20 @@ (filter (fn (r) (not (spread? r))) (async-map-fn-render f coll env ctx)))) + ;; provide — render-time dynamic scope + (= name "provide") + (let ((prov-name (async-eval (nth expr 1) env ctx)) + (prov-val (async-eval (nth expr 2) env ctx)) + (body-start 3) + (body-count (- (len expr) 3))) + (provide-push! prov-name prov-val) + (let ((result (if (= body-count 1) + (async-render (nth expr body-start) env ctx) + (let ((results (async-map-render (slice expr body-start) env ctx))) + (join "" (filter (fn (r) (not (spread? r))) results)))))) + (provide-pop! prov-name) + result)) + ;; Fallback :else (async-render (async-eval expr env ctx) env ctx)))) @@ -894,7 +908,7 @@ "define" "defcomp" "defmacro" "defstyle" "defhandler" "defpage" "defquery" "defaction" "begin" "do" "quote" "->" "set!" "defisland" - "deftype" "defeffect")) + "deftype" "defeffect" "provide")) (define ASYNC_ASER_HO_NAMES (list "map" "map-indexed" "filter" "for-each")) @@ -1032,6 +1046,17 @@ (= name "deftype") (= name "defeffect")) (do (async-eval expr env ctx) nil) + ;; provide — render-time dynamic scope + (= name "provide") + (let ((prov-name (async-eval (first args) env ctx)) + (prov-val (async-eval (nth args 1) env ctx)) + (result nil)) + (provide-push! prov-name prov-val) + (for-each (fn (body) (set! result (async-aser body env ctx))) + (slice args 2)) + (provide-pop! prov-name) + result) + ;; Fallback :else (async-eval expr env ctx))))) diff --git a/shared/sx/ref/adapter-dom.sx b/shared/sx/ref/adapter-dom.sx index 5ac9533..5c956a8 100644 --- a/shared/sx/ref/adapter-dom.sx +++ b/shared/sx/ref/adapter-dom.sx @@ -359,7 +359,7 @@ (list "if" "when" "cond" "case" "let" "let*" "begin" "do" "define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler" "map" "map-indexed" "filter" "for-each" "portal" - "error-boundary")) + "error-boundary" "provide")) (define render-dom-form? :effects [] (fn ((name :as string)) @@ -598,6 +598,19 @@ coll) frag) + ;; provide — render-time dynamic scope + (= name "provide") + (let ((prov-name (trampoline (eval-expr (nth expr 1) env))) + (prov-val (trampoline (eval-expr (nth expr 2) env))) + (frag (create-fragment))) + (provide-push! prov-name prov-val) + (for-each + (fn (i) + (dom-append frag (render-to-dom (nth expr i) env ns))) + (range 3 (len expr))) + (provide-pop! prov-name) + frag) + ;; Fallback :else (render-to-dom (trampoline (eval-expr expr env)) env ns)))) diff --git a/shared/sx/ref/adapter-html.sx b/shared/sx/ref/adapter-html.sx index 64de4cd..3ad5f9b 100644 --- a/shared/sx/ref/adapter-html.sx +++ b/shared/sx/ref/adapter-html.sx @@ -56,7 +56,7 @@ (list "if" "when" "cond" "case" "let" "let*" "begin" "do" "define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler" "deftype" "defeffect" - "map" "map-indexed" "filter" "for-each")) + "map" "map-indexed" "filter" "for-each" "provide")) (define render-html-form? :effects [] (fn ((name :as string)) @@ -237,6 +237,21 @@ (render-to-html (apply f (list item)) env))) coll)))) + ;; provide — render-time dynamic scope + (= name "provide") + (let ((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) + (let ((result (if (= body-count 1) + (render-to-html (nth expr body-start) env) + (join "" (filter (fn (r) (not (spread? r))) + (map (fn (i) (render-to-html (nth expr i) env)) + (range body-start (+ body-start body-count)))))))) + (provide-pop! prov-name) + result)) + ;; Fallback :else (render-value-to-html (trampoline (eval-expr expr env)) env)))) diff --git a/shared/sx/ref/adapter-sx.sx b/shared/sx/ref/adapter-sx.sx index 677ed5a..fdc1190 100644 --- a/shared/sx/ref/adapter-sx.sx +++ b/shared/sx/ref/adapter-sx.sx @@ -174,7 +174,7 @@ "defhandler" "defpage" "defquery" "defaction" "defrelation" "begin" "do" "quote" "quasiquote" "->" "set!" "letrec" "dynamic-wind" "defisland" - "deftype" "defeffect")) + "deftype" "defeffect" "provide")) (define HO_FORM_NAMES (list "map" "map-indexed" "filter" "reduce" @@ -312,6 +312,17 @@ (= name "deftype") (= name "defeffect")) (do (trampoline (eval-expr expr env)) nil) + ;; provide — render-time dynamic scope + (= name "provide") + (let ((prov-name (trampoline (eval-expr (first args) env))) + (prov-val (trampoline (eval-expr (nth args 1) env))) + (result nil)) + (provide-push! prov-name prov-val) + (for-each (fn (body) (set! result (aser body env))) + (slice args 2)) + (provide-pop! prov-name) + result) + ;; Everything else — evaluate normally :else (trampoline (eval-expr expr env)))))) diff --git a/shared/sx/ref/bootstrap_py.py b/shared/sx/ref/bootstrap_py.py index afae46e..ea9f089 100644 --- a/shared/sx/ref/bootstrap_py.py +++ b/shared/sx/ref/bootstrap_py.py @@ -293,6 +293,11 @@ class PyEmitter: "collect!": "sx_collect", "collected": "sx_collected", "clear-collected!": "sx_clear_collected", + "provide-push!": "provide_push", + "provide-pop!": "provide_pop", + "context": "sx_context", + "emit!": "sx_emit", + "emitted": "sx_emitted", "is-raw-html?": "is_raw_html", "async-coroutine?": "is_async_coroutine", "async-await!": "async_await", diff --git a/shared/sx/ref/boundary.sx b/shared/sx/ref/boundary.sx index bc1d66d..49a7761 100644 --- a/shared/sx/ref/boundary.sx +++ b/shared/sx/ref/boundary.sx @@ -371,3 +371,51 @@ :effects [mutation] :doc "Clear a named render-time accumulator bucket. Used at flush points after emitting collected values (e.g. after writing a