From c2efa192c5239a1bda8b408d0308ef11b4092811 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 13 Mar 2026 01:37:35 +0000 Subject: [PATCH 1/3] Rewrite CSSX: unified Tailwind-style utility token system Replace the three-layer cssx system (macro + value functions + class components) with a single token resolver. Tokens like "bg-yellow-199", "hover:bg-rose-500", "md:text-xl" are parsed into CSS declarations. Two delivery mechanisms, same token format: - tw() function: returns inline style string for :style - ~cssx/tw macro: injects JIT class + "))))) From ea2b71cfa3aa17930dffc3e071c666bfe96b2e4a Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 13 Mar 2026 02:58:21 +0000 Subject: [PATCH 3/3] =?UTF-8?q?Add=20provide/context/emit!/emitted=20?= =?UTF-8?q?=E2=80=94=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 4e33d603..5d452905 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 2e566523..5b47d823 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 5ac95339..5c956a85 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 64de4cdb..3ad5f9b7 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 677ed5a4..fdc11901 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 afae46ed..ea9f089f 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 bc1d66d5..49a7761c 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