7 Commits

Author SHA1 Message Date
11fdd1a840 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>
2026-03-13 17:30:34 +00:00
6ca46bb295 Exclude reader-macro-demo.sx from component loader
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Rename to .sx.future — the file uses #z3 reader macros that aren't
implemented yet, causing a ParseError that blocks ALL component loading
and breaks the provide docs page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:28:26 +00:00
e1a5e3eb89 Reframe spreads article around provide/emit! as the mechanism
Lead with provide/emit! from the first sentence. make-spread/spread?/spread-attrs
are now presented as user-facing API on top of the provide/emit! substrate,
not as independent primitives. Restructured sections, removed redundant
"deeper primitive" content that duplicated the new section I.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:12:47 +00:00
aef990735f Add provide/emit! geography article, update spreads article, fix foundations rendering
- New geography article (provide.sx): four primitives, demos, nested scoping,
  adapter comparison, spec explorer links
- Updated spreads article section VI: provide/emit! is now implemented, not planned
- Fixed foundations.sx: ~docs/code-block → ~docs/code (undefined component
  was causing the page to silently fail to render)
- Added nav entry and defpage route for provide/emit! article

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:04:52 +00:00
04d3b2ecaf Use separate CI build directory to avoid clobbering dev working tree
CI was doing git reset --hard on /root/rose-ash (the dev directory),
flipping the checked-out branch and causing empty diffs when merging.
Now builds in /root/rose-ash-ci and uses push event SHAs for diffing.
Also adds --resolve-image always to stack deploys.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 15:42:00 +00:00
c4a999d0d0 Merge branch 'worktree-api-urls' into macros 2026-03-13 15:41:40 +00:00
2de4ba8c57 Refactor spread to use provide/emit! internally
Spreads now emit their attrs into the nearest element's provide scope
instead of requiring per-child spread? checks at every intermediate
layer. emit! is tolerant (no-op when no provider), so spreads in
non-element contexts silently vanish.

- adapter-html: element/lake/marsh wrap children in provide, collect
  emitted; removed 14 spread filters from fragment, forms, components
- adapter-sx: aser wraps result to catch spread values from fn calls;
  aser-call uses provide with attr-parts/child-parts ordering
- adapter-async: same pattern for both render and aser paths
- adapter-dom: added emit! in spread dispatch + provide in element
  rendering; kept spread? checks for reactive/island and DOM safety
- platform: emit! returns NIL when no provider instead of erroring
- 3 new aser tests: stored spread, nested element, silent drop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 15:41:32 +00:00
26 changed files with 1622 additions and 849 deletions

View File

@@ -7,6 +7,7 @@ on:
env: env:
REGISTRY: registry.rose-ash.com:5000 REGISTRY: registry.rose-ash.com:5000
APP_DIR: /root/rose-ash APP_DIR: /root/rose-ash
BUILD_DIR: /root/rose-ash-ci
jobs: jobs:
build-and-deploy: build-and-deploy:
@@ -33,23 +34,26 @@ jobs:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
run: | run: |
ssh "root@$DEPLOY_HOST" " ssh "root@$DEPLOY_HOST" "
cd ${{ env.APP_DIR }} # --- Build in isolated CI directory (never touch dev working tree) ---
BUILD=${{ env.BUILD_DIR }}
# Save current HEAD before updating ORIGIN=\$(git -C ${{ env.APP_DIR }} remote get-url origin)
OLD_HEAD=\$(git rev-parse HEAD 2>/dev/null || echo none) if [ ! -d \"\$BUILD/.git\" ]; then
git clone \"\$ORIGIN\" \"\$BUILD\"
git fetch origin ${{ github.ref_name }} fi
cd \"\$BUILD\"
git fetch origin
git reset --hard origin/${{ github.ref_name }} git reset --hard origin/${{ github.ref_name }}
NEW_HEAD=\$(git rev-parse HEAD) # Detect changes using push event SHAs (not local checkout state)
BEFORE='${{ github.event.before }}'
AFTER='${{ github.sha }}'
# Detect what changed
REBUILD_ALL=false REBUILD_ALL=false
if [ \"\$OLD_HEAD\" = \"none\" ] || [ \"\$OLD_HEAD\" = \"\$NEW_HEAD\" ]; then if [ -z \"\$BEFORE\" ] || [ \"\$BEFORE\" = '0000000000000000000000000000000000000000' ] || ! git cat-file -e \"\$BEFORE\" 2>/dev/null; then
# First deploy or CI re-run on same commit — rebuild all # New branch, force push, or unreachable parent — rebuild all
REBUILD_ALL=true REBUILD_ALL=true
else else
CHANGED=\$(git diff --name-only \$OLD_HEAD \$NEW_HEAD) CHANGED=\$(git diff --name-only \$BEFORE \$AFTER)
if echo \"\$CHANGED\" | grep -q '^shared/'; then if echo \"\$CHANGED\" | grep -q '^shared/'; then
REBUILD_ALL=true REBUILD_ALL=true
fi fi
@@ -86,8 +90,8 @@ jobs:
# Deploy swarm stacks only on main branch # Deploy swarm stacks only on main branch
if [ '${{ github.ref_name }}' = 'main' ]; then if [ '${{ github.ref_name }}' = 'main' ]; then
source .env source ${{ env.APP_DIR }}/.env
docker stack deploy -c docker-compose.yml rose-ash docker stack deploy --resolve-image always -c docker-compose.yml rose-ash
echo 'Waiting for swarm services to update...' echo 'Waiting for swarm services to update...'
sleep 10 sleep 10
docker stack services rose-ash docker stack services rose-ash
@@ -99,17 +103,17 @@ jobs:
fi fi
if [ \"\$SX_REBUILT\" = true ]; then if [ \"\$SX_REBUILT\" = true ]; then
echo 'Deploying sx-web stack (sx-web.org)...' echo 'Deploying sx-web stack (sx-web.org)...'
docker stack deploy -c /root/sx-web/docker-compose.yml sx-web docker stack deploy --resolve-image always -c /root/sx-web/docker-compose.yml sx-web
sleep 5 sleep 5
docker stack services sx-web docker stack services sx-web
# Reload Caddy to pick up any Caddyfile changes
docker service update --force caddy_caddy 2>/dev/null || true docker service update --force caddy_caddy 2>/dev/null || true
fi fi
else else
echo 'Skipping swarm deploy (branch: ${{ github.ref_name }})' echo 'Skipping swarm deploy (branch: ${{ github.ref_name }})'
fi fi
# Dev stack always deployed (bind-mounted source + auto-reload) # Dev stack uses working tree (bind-mounted source + auto-reload)
cd ${{ env.APP_DIR }}
echo 'Deploying dev stack...' echo 'Deploying dev stack...'
docker compose -p rose-ash-dev -f docker-compose.yml -f docker-compose.dev.yml up -d docker compose -p rose-ash-dev -f docker-compose.yml -f docker-compose.dev.yml up -d
echo 'Dev stack deployed' echo 'Dev stack deployed'

View File

@@ -14,7 +14,7 @@
// ========================================================================= // =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-13T12:16:43Z"; var SX_VERSION = "2026-03-13T16:48:03Z";
function isNil(x) { return x === NIL || x === null || x === undefined; } function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); } function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -86,8 +86,7 @@
function SxSpread(attrs) { this.attrs = attrs || {}; } function SxSpread(attrs) { this.attrs = attrs || {}; }
SxSpread.prototype._spread = true; SxSpread.prototype._spread = true;
var _collectBuckets = {}; var _scopeStacks = {};
var _provideStacks = {};
function isSym(x) { return x != null && x._sym === true; } function isSym(x) { return x != null && x._sym === true; }
function isKw(x) { return x != null && x._kw === true; } function isKw(x) { return x != null && x._kw === true; }
@@ -151,46 +150,54 @@
function isSpread(x) { return x != null && x._spread === true; } function isSpread(x) { return x != null && x._spread === true; }
function spreadAttrs(s) { return s && s._spread ? s.attrs : {}; } function spreadAttrs(s) { return s && s._spread ? s.attrs : {}; }
function sxCollect(bucket, value) { function scopePush(name, value) {
if (!_collectBuckets[bucket]) _collectBuckets[bucket] = []; if (!_scopeStacks[name]) _scopeStacks[name] = [];
var items = _collectBuckets[bucket]; _scopeStacks[name].push({value: value !== undefined ? value : NIL, emitted: [], dedup: false});
if (items.indexOf(value) === -1) items.push(value);
} }
function sxCollected(bucket) { function scopePop(name) {
return _collectBuckets[bucket] ? _collectBuckets[bucket].slice() : []; if (_scopeStacks[name] && _scopeStacks[name].length) _scopeStacks[name].pop();
}
function sxClearCollected(bucket) {
if (_collectBuckets[bucket]) _collectBuckets[bucket] = [];
} }
// Aliases — provide-push!/provide-pop! map to scope-push!/scope-pop!
var providePush = scopePush;
var providePop = scopePop;
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) { function sxContext(name) {
if (_provideStacks[name] && _provideStacks[name].length) { if (_scopeStacks[name] && _scopeStacks[name].length) {
return _provideStacks[name][_provideStacks[name].length - 1].value; return _scopeStacks[name][_scopeStacks[name].length - 1].value;
} }
if (arguments.length > 1) return arguments[1]; if (arguments.length > 1) return arguments[1];
throw new Error("No provider for: " + name); throw new Error("No provider for: " + name);
} }
function sxEmit(name, value) { function sxEmit(name, value) {
if (_provideStacks[name] && _provideStacks[name].length) { if (_scopeStacks[name] && _scopeStacks[name].length) {
_provideStacks[name][_provideStacks[name].length - 1].emitted.push(value); var entry = _scopeStacks[name][_scopeStacks[name].length - 1];
} else { if (entry.dedup && entry.emitted.indexOf(value) !== -1) return NIL;
throw new Error("No provider for emit!: " + name); entry.emitted.push(value);
} }
return NIL; return NIL;
} }
function sxEmitted(name) { function sxEmitted(name) {
if (_provideStacks[name] && _provideStacks[name].length) { if (_scopeStacks[name] && _scopeStacks[name].length) {
return _provideStacks[name][_provideStacks[name].length - 1].emitted.slice(); return _scopeStacks[name][_scopeStacks[name].length - 1].emitted.slice();
} }
return []; return [];
} }
function sxCollect(bucket, value) {
if (!_scopeStacks[bucket] || !_scopeStacks[bucket].length) {
if (!_scopeStacks[bucket]) _scopeStacks[bucket] = [];
_scopeStacks[bucket].push({value: NIL, emitted: [], dedup: true});
}
var entry = _scopeStacks[bucket][_scopeStacks[bucket].length - 1];
if (entry.emitted.indexOf(value) === -1) entry.emitted.push(value);
}
function sxCollected(bucket) {
return sxEmitted(bucket);
}
function sxClearCollected(bucket) {
if (_scopeStacks[bucket] && _scopeStacks[bucket].length) {
_scopeStacks[bucket][_scopeStacks[bucket].length - 1].emitted = [];
}
}
function lambdaParams(f) { return f.params; } function lambdaParams(f) { return f.params; }
function lambdaBody(f) { return f.body; } function lambdaBody(f) { return f.body; }
@@ -519,14 +526,17 @@
}; };
// stdlib.spread — spread + collect primitives // stdlib.spread — spread + collect + scope primitives
PRIMITIVES["make-spread"] = makeSpread; PRIMITIVES["make-spread"] = makeSpread;
PRIMITIVES["spread?"] = isSpread; PRIMITIVES["spread?"] = isSpread;
PRIMITIVES["spread-attrs"] = spreadAttrs; PRIMITIVES["spread-attrs"] = spreadAttrs;
PRIMITIVES["collect!"] = sxCollect; PRIMITIVES["collect!"] = sxCollect;
PRIMITIVES["collected"] = sxCollected; PRIMITIVES["collected"] = sxCollected;
PRIMITIVES["clear-collected!"] = sxClearCollected; PRIMITIVES["clear-collected!"] = sxClearCollected;
// provide/context/emit! — render-time dynamic scope // scope — unified render-time dynamic scope
PRIMITIVES["scope-push!"] = scopePush;
PRIMITIVES["scope-pop!"] = scopePop;
// provide-push!/provide-pop! — aliases for scope-push!/scope-pop!
PRIMITIVES["provide-push!"] = providePush; PRIMITIVES["provide-push!"] = providePush;
PRIMITIVES["provide-pop!"] = providePop; PRIMITIVES["provide-pop!"] = providePop;
PRIMITIVES["context"] = sxContext; PRIMITIVES["context"] = sxContext;
@@ -798,10 +808,10 @@
var args = rest(expr); 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() { 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); 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 == "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() { 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 == "scope")) ? sfScope(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); var mac = envGet(env, name);
return makeThunk(expandMacro(mac, args, env), env); 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))); })() : evalCall(head, args, env)));
})(); }; })(); };
@@ -1206,6 +1216,22 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
callThunk(after, env); callThunk(after, env);
return result; return result;
})(); })();
})(); };
// sf-scope
var sfScope = function(args, env) { return (function() {
var name = trampoline(evalExpr(first(args), env));
var rest = slice(args, 1);
var val = NIL;
var bodyExprs = NIL;
(isSxTruthy((isSxTruthy((len(rest) >= 2)) && isSxTruthy((typeOf(first(rest)) == "keyword")) && (keywordName(first(rest)) == "value"))) ? ((val = trampoline(evalExpr(nth(rest, 1), env))), (bodyExprs = slice(rest, 2))) : (bodyExprs = rest));
scopePush(name, val);
return (function() {
var result = NIL;
{ var _c = bodyExprs; for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; result = trampoline(evalExpr(e, env)); } }
scopePop(name);
return result;
})();
})(); }; })(); };
// sf-provide // sf-provide
@@ -1214,9 +1240,9 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var val = trampoline(evalExpr(nth(args, 1), env)); var val = trampoline(evalExpr(nth(args, 1), env));
var bodyExprs = slice(args, 2); var bodyExprs = slice(args, 2);
var result = NIL; var result = NIL;
providePush(name, val); scopePush(name, val);
{ var _c = bodyExprs; for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; result = trampoline(evalExpr(e, env)); } } { var _c = bodyExprs; for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; result = trampoline(evalExpr(e, env)); } }
providePop(name); scopePop(name);
return result; return result;
})(); }; })(); };
@@ -1523,13 +1549,13 @@ continue; } else { return NIL; } } };
// render-to-html // render-to-html
var renderToHtml = function(expr, env) { setRenderActiveB(true); var renderToHtml = function(expr, env) { setRenderActiveB(true);
return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(expr); if (_m == "number") return (String(expr)); if (_m == "boolean") return (isSxTruthy(expr) ? "true" : "false"); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? "" : renderListToHtml(expr, env)); if (_m == "symbol") return renderValueToHtml(trampoline(evalExpr(expr, env)), env); if (_m == "keyword") return escapeHtml(keywordName(expr)); if (_m == "raw-html") return rawHtmlContent(expr); if (_m == "spread") return expr; return renderValueToHtml(trampoline(evalExpr(expr, env)), env); })(); }; return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(expr); if (_m == "number") return (String(expr)); if (_m == "boolean") return (isSxTruthy(expr) ? "true" : "false"); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? "" : renderListToHtml(expr, env)); if (_m == "symbol") return renderValueToHtml(trampoline(evalExpr(expr, env)), env); if (_m == "keyword") return escapeHtml(keywordName(expr)); if (_m == "raw-html") return rawHtmlContent(expr); if (_m == "spread") return (sxEmit("element-attrs", spreadAttrs(expr)), ""); return renderValueToHtml(trampoline(evalExpr(expr, env)), env); })(); };
// render-value-to-html // render-value-to-html
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))); })(); }; 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 (sxEmit("element-attrs", spreadAttrs(val)), ""); return escapeHtml((String(val))); })(); };
// RENDER_HTML_FORMS // 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", "provide"]; 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", "scope", "provide"];
// render-html-form? // render-html-form?
var isRenderHtmlForm = function(name) { return contains(RENDER_HTML_FORMS, name); }; var isRenderHtmlForm = function(name) { return contains(RENDER_HTML_FORMS, name); };
@@ -1537,10 +1563,10 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
// render-list-to-html // render-list-to-html
var renderListToHtml = function(expr, env) { return (isSxTruthy(isEmpty(expr)) ? "" : (function() { var renderListToHtml = function(expr, env) { return (isSxTruthy(isEmpty(expr)) ? "" : (function() {
var head = first(expr); var head = first(expr);
return (isSxTruthy(!isSxTruthy((typeOf(head) == "symbol"))) ? join("", filter(function(x) { return !isSxTruthy(isSpread(x)); }, map(function(x) { return renderValueToHtml(x, env); }, expr))) : (function() { return (isSxTruthy(!isSxTruthy((typeOf(head) == "symbol"))) ? join("", map(function(x) { return renderValueToHtml(x, env); }, expr)) : (function() {
var name = symbolName(head); var name = symbolName(head);
var args = rest(expr); var args = rest(expr);
return (isSxTruthy((name == "<>")) ? join("", filter(function(x) { return !isSxTruthy(isSpread(x)); }, map(function(x) { return renderToHtml(x, env); }, args))) : (isSxTruthy((name == "raw!")) ? join("", map(function(x) { return (String(trampoline(evalExpr(x, env)))); }, args)) : (isSxTruthy((name == "lake")) ? renderHtmlLake(args, env) : (isSxTruthy((name == "marsh")) ? renderHtmlMarsh(args, env) : (isSxTruthy(contains(HTML_TAGS, name)) ? renderHtmlElement(name, args, env) : (isSxTruthy((isSxTruthy(startsWith(name, "~")) && isSxTruthy(envHas(env, name)) && isIsland(envGet(env, name)))) ? renderHtmlIsland(envGet(env, name), args, env) : (isSxTruthy(startsWith(name, "~")) ? (function() { return (isSxTruthy((name == "<>")) ? join("", map(function(x) { return renderToHtml(x, env); }, args)) : (isSxTruthy((name == "raw!")) ? join("", map(function(x) { return (String(trampoline(evalExpr(x, env)))); }, args)) : (isSxTruthy((name == "lake")) ? renderHtmlLake(args, env) : (isSxTruthy((name == "marsh")) ? renderHtmlMarsh(args, env) : (isSxTruthy(contains(HTML_TAGS, name)) ? renderHtmlElement(name, args, env) : (isSxTruthy((isSxTruthy(startsWith(name, "~")) && isSxTruthy(envHas(env, name)) && isIsland(envGet(env, name)))) ? renderHtmlIsland(envGet(env, name), args, env) : (isSxTruthy(startsWith(name, "~")) ? (function() {
var val = envGet(env, name); var val = envGet(env, name);
return (isSxTruthy(isComponent(val)) ? renderHtmlComponent(val, args, env) : (isSxTruthy(isMacro(val)) ? renderToHtml(expandMacro(val, args, env), env) : error((String("Unknown component: ") + String(name))))); return (isSxTruthy(isComponent(val)) ? renderHtmlComponent(val, args, env) : (isSxTruthy(isMacro(val)) ? renderToHtml(expandMacro(val, args, env), env) : error((String("Unknown component: ") + String(name)))));
})() : (isSxTruthy(isRenderHtmlForm(name)) ? dispatchHtmlForm(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? renderToHtml(expandMacro(envGet(env, name), args, env), env) : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))))))); })() : (isSxTruthy(isRenderHtmlForm(name)) ? dispatchHtmlForm(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? renderToHtml(expandMacro(envGet(env, name), args, env), env) : renderValueToHtml(trampoline(evalExpr(expr, env)), env))))))))));
@@ -1551,45 +1577,48 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
var dispatchHtmlForm = function(name, expr, env) { return (isSxTruthy((name == "if")) ? (function() { var dispatchHtmlForm = function(name, expr, env) { return (isSxTruthy((name == "if")) ? (function() {
var condVal = trampoline(evalExpr(nth(expr, 1), env)); var condVal = trampoline(evalExpr(nth(expr, 1), env));
return (isSxTruthy(condVal) ? renderToHtml(nth(expr, 2), env) : (isSxTruthy((len(expr) > 3)) ? renderToHtml(nth(expr, 3), env) : "")); return (isSxTruthy(condVal) ? renderToHtml(nth(expr, 2), env) : (isSxTruthy((len(expr) > 3)) ? renderToHtml(nth(expr, 3), env) : ""));
})() : (isSxTruthy((name == "when")) ? (isSxTruthy(!isSxTruthy(trampoline(evalExpr(nth(expr, 1), env)))) ? "" : (isSxTruthy((len(expr) == 3)) ? renderToHtml(nth(expr, 2), env) : (function() { })() : (isSxTruthy((name == "when")) ? (isSxTruthy(!isSxTruthy(trampoline(evalExpr(nth(expr, 1), env)))) ? "" : (isSxTruthy((len(expr) == 3)) ? renderToHtml(nth(expr, 2), env) : join("", map(function(i) { return renderToHtml(nth(expr, i), env); }, range(2, len(expr)))))) : (isSxTruthy((name == "cond")) ? (function() {
var results = map(function(i) { return renderToHtml(nth(expr, i), env); }, range(2, len(expr)));
return join("", filter(function(r) { return !isSxTruthy(isSpread(r)); }, results));
})())) : (isSxTruthy((name == "cond")) ? (function() {
var branch = evalCond(rest(expr), env); var branch = evalCond(rest(expr), env);
return (isSxTruthy(branch) ? renderToHtml(branch, env) : ""); return (isSxTruthy(branch) ? renderToHtml(branch, env) : "");
})() : (isSxTruthy((name == "case")) ? renderToHtml(trampoline(evalExpr(expr, env)), env) : (isSxTruthy(sxOr((name == "let"), (name == "let*"))) ? (function() { })() : (isSxTruthy((name == "case")) ? renderToHtml(trampoline(evalExpr(expr, env)), env) : (isSxTruthy(sxOr((name == "let"), (name == "let*"))) ? (function() {
var local = processBindings(nth(expr, 1), env); var local = processBindings(nth(expr, 1), env);
return (isSxTruthy((len(expr) == 3)) ? renderToHtml(nth(expr, 2), local) : (function() { return (isSxTruthy((len(expr) == 3)) ? renderToHtml(nth(expr, 2), local) : join("", map(function(i) { return renderToHtml(nth(expr, i), local); }, range(2, len(expr)))));
var results = map(function(i) { return renderToHtml(nth(expr, i), local); }, range(2, len(expr))); })() : (isSxTruthy(sxOr((name == "begin"), (name == "do"))) ? (isSxTruthy((len(expr) == 2)) ? renderToHtml(nth(expr, 1), env) : join("", map(function(i) { return renderToHtml(nth(expr, i), env); }, range(1, len(expr))))) : (isSxTruthy(isDefinitionForm(name)) ? (trampoline(evalExpr(expr, env)), "") : (isSxTruthy((name == "map")) ? (function() {
return join("", filter(function(r) { return !isSxTruthy(isSpread(r)); }, results));
})());
})() : (isSxTruthy(sxOr((name == "begin"), (name == "do"))) ? (isSxTruthy((len(expr) == 2)) ? renderToHtml(nth(expr, 1), env) : (function() {
var results = map(function(i) { return renderToHtml(nth(expr, i), env); }, range(1, len(expr)));
return join("", filter(function(r) { return !isSxTruthy(isSpread(r)); }, results));
})()) : (isSxTruthy(isDefinitionForm(name)) ? (trampoline(evalExpr(expr, env)), "") : (isSxTruthy((name == "map")) ? (function() {
var f = trampoline(evalExpr(nth(expr, 1), env)); var f = trampoline(evalExpr(nth(expr, 1), env));
var coll = trampoline(evalExpr(nth(expr, 2), 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))); return join("", map(function(item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [item], env) : renderToHtml(apply(f, [item]), env)); }, coll));
})() : (isSxTruthy((name == "map-indexed")) ? (function() { })() : (isSxTruthy((name == "map-indexed")) ? (function() {
var f = trampoline(evalExpr(nth(expr, 1), env)); var f = trampoline(evalExpr(nth(expr, 1), env));
var coll = trampoline(evalExpr(nth(expr, 2), env)); var coll = trampoline(evalExpr(nth(expr, 2), env));
return join("", filter(function(r) { return !isSxTruthy(isSpread(r)); }, mapIndexed(function(i, item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [i, item], env) : renderToHtml(apply(f, [i, item]), env)); }, coll))); return join("", mapIndexed(function(i, item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [i, item], env) : renderToHtml(apply(f, [i, item]), env)); }, coll));
})() : (isSxTruthy((name == "filter")) ? renderToHtml(trampoline(evalExpr(expr, env)), env) : (isSxTruthy((name == "for-each")) ? (function() { })() : (isSxTruthy((name == "filter")) ? renderToHtml(trampoline(evalExpr(expr, env)), env) : (isSxTruthy((name == "for-each")) ? (function() {
var f = trampoline(evalExpr(nth(expr, 1), env)); var f = trampoline(evalExpr(nth(expr, 1), env));
var coll = trampoline(evalExpr(nth(expr, 2), 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))); return join("", map(function(item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [item], env) : renderToHtml(apply(f, [item]), env)); }, coll));
})() : (isSxTruthy((name == "scope")) ? (function() {
var scopeName = trampoline(evalExpr(nth(expr, 1), env));
var restArgs = slice(expr, 2);
var scopeVal = NIL;
var bodyExprs = NIL;
(isSxTruthy((isSxTruthy((len(restArgs) >= 2)) && isSxTruthy((typeOf(first(restArgs)) == "keyword")) && (keywordName(first(restArgs)) == "value"))) ? ((scopeVal = trampoline(evalExpr(nth(restArgs, 1), env))), (bodyExprs = slice(restArgs, 2))) : (bodyExprs = restArgs));
scopePush(scopeName, scopeVal);
return (function() {
var result = (isSxTruthy((len(bodyExprs) == 1)) ? renderToHtml(first(bodyExprs), env) : join("", map(function(e) { return renderToHtml(e, env); }, bodyExprs)));
scopePop(scopeName);
return result;
})();
})() : (isSxTruthy((name == "provide")) ? (function() { })() : (isSxTruthy((name == "provide")) ? (function() {
var provName = trampoline(evalExpr(nth(expr, 1), env)); var provName = trampoline(evalExpr(nth(expr, 1), env));
var provVal = trampoline(evalExpr(nth(expr, 2), env)); var provVal = trampoline(evalExpr(nth(expr, 2), env));
var bodyStart = 3; var bodyStart = 3;
var bodyCount = (len(expr) - 3); var bodyCount = (len(expr) - 3);
providePush(provName, provVal); scopePush(provName, provVal);
return (function() { 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)))))); var result = (isSxTruthy((bodyCount == 1)) ? renderToHtml(nth(expr, bodyStart), env) : join("", map(function(i) { return renderToHtml(nth(expr, i), env); }, range(bodyStart, (bodyStart + bodyCount)))));
providePop(provName); scopePop(provName);
return result; return result;
})(); })();
})() : renderValueToHtml(trampoline(evalExpr(expr, env)), env))))))))))))); }; })() : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))))))))))); };
// render-lambda-html // render-lambda-html
var renderLambdaHtml = function(f, args, env) { return (function() { var renderLambdaHtml = function(f, args, env) { return (function() {
@@ -1614,14 +1643,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
var local = envMerge(componentClosure(comp), env); var local = envMerge(componentClosure(comp), env);
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } } { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
if (isSxTruthy(componentHasChildren(comp))) { if (isSxTruthy(componentHasChildren(comp))) {
(function() { envSet(local, "children", makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children))));
var parts = [];
{ var _c = children; for (var _i = 0; _i < _c.length; _i++) { var c = _c[_i]; (function() {
var r = renderToHtml(c, env);
return (isSxTruthy(!isSxTruthy(isSpread(r))) ? append_b(parts, r) : NIL);
})(); } }
return envSet(local, "children", makeRawHtml(join("", parts)));
})();
} }
return renderToHtml(componentBody(comp), local); return renderToHtml(componentBody(comp), local);
})(); })();
@@ -1633,14 +1655,12 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
var attrs = first(parsed); var attrs = first(parsed);
var children = nth(parsed, 1); var children = nth(parsed, 1);
var isVoid = contains(VOID_ELEMENTS, tag); var isVoid = contains(VOID_ELEMENTS, tag);
return (isSxTruthy(isVoid) ? (String("<") + String(tag) + String(renderAttrs(attrs)) + String(" />")) : (function() { return (isSxTruthy(isVoid) ? (String("<") + String(tag) + String(renderAttrs(attrs)) + String(" />")) : (scopePush("element-attrs", NIL), (function() {
var contentParts = []; var content = join("", map(function(c) { return renderToHtml(c, env); }, children));
{ var _c = children; for (var _i = 0; _i < _c.length; _i++) { var c = _c[_i]; (function() { { var _c = sxEmitted("element-attrs"); for (var _i = 0; _i < _c.length; _i++) { var spreadDict = _c[_i]; mergeSpreadAttrs(attrs, spreadDict); } }
var result = renderToHtml(c, env); scopePop("element-attrs");
return (isSxTruthy(isSpread(result)) ? mergeSpreadAttrs(attrs, spreadAttrs(result)) : append_b(contentParts, result)); return (String("<") + String(tag) + String(renderAttrs(attrs)) + String(">") + String(content) + String("</") + String(tag) + String(">"));
})(); } } })()));
return (String("<") + String(tag) + String(renderAttrs(attrs)) + String(">") + String(join("", contentParts)) + String("</") + String(tag) + String(">"));
})());
})(); }; })(); };
// render-html-lake // render-html-lake
@@ -1659,12 +1679,13 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
})(); }, {["i"]: 0, ["skip"]: false}, args); })(); }, {["i"]: 0, ["skip"]: false}, args);
return (function() { return (function() {
var lakeAttrs = {["data-sx-lake"]: sxOr(lakeId, "")}; var lakeAttrs = {["data-sx-lake"]: sxOr(lakeId, "")};
var contentParts = []; scopePush("element-attrs", NIL);
{ var _c = children; for (var _i = 0; _i < _c.length; _i++) { var c = _c[_i]; (function() { return (function() {
var result = renderToHtml(c, env); var content = join("", map(function(c) { return renderToHtml(c, env); }, children));
return (isSxTruthy(isSpread(result)) ? mergeSpreadAttrs(lakeAttrs, spreadAttrs(result)) : append_b(contentParts, result)); { var _c = sxEmitted("element-attrs"); for (var _i = 0; _i < _c.length; _i++) { var spreadDict = _c[_i]; mergeSpreadAttrs(lakeAttrs, spreadDict); } }
})(); } } scopePop("element-attrs");
return (String("<") + String(lakeTag) + String(renderAttrs(lakeAttrs)) + String(">") + String(join("", contentParts)) + String("</") + String(lakeTag) + String(">")); return (String("<") + String(lakeTag) + String(renderAttrs(lakeAttrs)) + String(">") + String(content) + String("</") + String(lakeTag) + String(">"));
})();
})(); })();
})(); }; })(); };
@@ -1684,12 +1705,13 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
})(); }, {["i"]: 0, ["skip"]: false}, args); })(); }, {["i"]: 0, ["skip"]: false}, args);
return (function() { return (function() {
var marshAttrs = {["data-sx-marsh"]: sxOr(marshId, "")}; var marshAttrs = {["data-sx-marsh"]: sxOr(marshId, "")};
var contentParts = []; scopePush("element-attrs", NIL);
{ var _c = children; for (var _i = 0; _i < _c.length; _i++) { var c = _c[_i]; (function() { return (function() {
var result = renderToHtml(c, env); var content = join("", map(function(c) { return renderToHtml(c, env); }, children));
return (isSxTruthy(isSpread(result)) ? mergeSpreadAttrs(marshAttrs, spreadAttrs(result)) : append_b(contentParts, result)); { var _c = sxEmitted("element-attrs"); for (var _i = 0; _i < _c.length; _i++) { var spreadDict = _c[_i]; mergeSpreadAttrs(marshAttrs, spreadDict); } }
})(); } } scopePop("element-attrs");
return (String("<") + String(marshTag) + String(renderAttrs(marshAttrs)) + String(">") + String(join("", contentParts)) + String("</") + String(marshTag) + String(">")); return (String("<") + String(marshTag) + String(renderAttrs(marshAttrs)) + String(">") + String(content) + String("</") + String(marshTag) + String(">"));
})();
})(); })();
})(); }; })(); };
@@ -1710,14 +1732,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
var islandName = componentName(island); var islandName = componentName(island);
{ var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } } { var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
if (isSxTruthy(componentHasChildren(island))) { if (isSxTruthy(componentHasChildren(island))) {
(function() { envSet(local, "children", makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children))));
var parts = [];
{ var _c = children; for (var _i = 0; _i < _c.length; _i++) { var c = _c[_i]; (function() {
var r = renderToHtml(c, env);
return (isSxTruthy(!isSxTruthy(isSpread(r))) ? append_b(parts, r) : NIL);
})(); } }
return envSet(local, "children", makeRawHtml(join("", parts)));
})();
} }
return (function() { return (function() {
var bodyHtml = renderToHtml(componentBody(island), local); var bodyHtml = renderToHtml(componentBody(island), local);
@@ -1741,10 +1756,13 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
// aser // aser
var aser = function(expr, env) { setRenderActiveB(true); var aser = function(expr, env) { setRenderActiveB(true);
return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if (_m == "string") return expr; if (_m == "boolean") return expr; if (_m == "nil") return NIL; if (_m == "symbol") return (function() { return (function() {
var result = (function() { var _m = typeOf(expr); if (_m == "number") return expr; if (_m == "string") return expr; if (_m == "boolean") return expr; if (_m == "nil") return NIL; if (_m == "symbol") return (function() {
var name = symbolName(expr); var name = symbolName(expr);
return (isSxTruthy(envHas(env, name)) ? envGet(env, name) : (isSxTruthy(isPrimitive(name)) ? getPrimitive(name) : (isSxTruthy((name == "true")) ? true : (isSxTruthy((name == "false")) ? false : (isSxTruthy((name == "nil")) ? NIL : error((String("Undefined symbol: ") + String(name)))))))); return (isSxTruthy(envHas(env, name)) ? envGet(env, name) : (isSxTruthy(isPrimitive(name)) ? getPrimitive(name) : (isSxTruthy((name == "true")) ? true : (isSxTruthy((name == "false")) ? false : (isSxTruthy((name == "nil")) ? NIL : error((String("Undefined symbol: ") + String(name))))))));
})(); if (_m == "keyword") return keywordName(expr); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? [] : aserList(expr, env)); if (_m == "spread") return expr; return expr; })(); }; })(); if (_m == "keyword") return keywordName(expr); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? [] : aserList(expr, env)); if (_m == "spread") return (sxEmit("element-attrs", spreadAttrs(expr)), NIL); return expr; })();
return (isSxTruthy(isSpread(result)) ? (sxEmit("element-attrs", spreadAttrs(result)), NIL) : result);
})(); };
// aser-list // aser-list
var aserList = function(expr, env) { return (function() { var aserList = function(expr, env) { return (function() {
@@ -1772,33 +1790,40 @@ return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if
// aser-call // aser-call
var aserCall = function(name, args, env) { return (function() { var aserCall = function(name, args, env) { return (function() {
var parts = [name]; var attrParts = [];
var childParts = [];
var skip = false; var skip = false;
var i = 0; var i = 0;
scopePush("element-attrs", NIL);
{ var _c = args; for (var _i = 0; _i < _c.length; _i++) { var arg = _c[_i]; (isSxTruthy(skip) ? ((skip = false), (i = (i + 1))) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((i + 1) < len(args)))) ? (function() { { var _c = args; for (var _i = 0; _i < _c.length; _i++) { var arg = _c[_i]; (isSxTruthy(skip) ? ((skip = false), (i = (i + 1))) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((i + 1) < len(args)))) ? (function() {
var val = aser(nth(args, (i + 1)), env); var val = aser(nth(args, (i + 1)), env);
if (isSxTruthy(!isSxTruthy(isNil(val)))) { if (isSxTruthy(!isSxTruthy(isNil(val)))) {
parts.push((String(":") + String(keywordName(arg)))); attrParts.push((String(":") + String(keywordName(arg))));
parts.push(serialize(val)); attrParts.push(serialize(val));
} }
skip = true; skip = true;
return (i = (i + 1)); return (i = (i + 1));
})() : (function() { })() : (function() {
var val = aser(arg, env); var val = aser(arg, env);
if (isSxTruthy(!isSxTruthy(isNil(val)))) { if (isSxTruthy(!isSxTruthy(isNil(val)))) {
(isSxTruthy(isSpread(val)) ? forEach(function(k) { return (function() { (isSxTruthy((typeOf(val) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? append_b(childParts, serialize(item)) : NIL); }, val) : append_b(childParts, serialize(val)));
var v = dictGet(spreadAttrs(val), k);
parts.push((String(":") + String(k)));
return append_b(parts, serialize(v));
})(); }, keys(spreadAttrs(val))) : (isSxTruthy((typeOf(val) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? append_b(parts, serialize(item)) : NIL); }, val) : append_b(parts, serialize(val))));
} }
return (i = (i + 1)); return (i = (i + 1));
})())); } } })())); } }
{ var _c = sxEmitted("element-attrs"); for (var _i = 0; _i < _c.length; _i++) { var spreadDict = _c[_i]; { var _c = keys(spreadDict); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; (function() {
var v = dictGet(spreadDict, k);
attrParts.push((String(":") + String(k)));
return append_b(attrParts, serialize(v));
})(); } } } }
scopePop("element-attrs");
return (function() {
var parts = concat([name], attrParts, childParts);
return (String("(") + String(join(" ", parts)) + String(")")); return (String("(") + String(join(" ", parts)) + String(")"));
})();
})(); }; })(); };
// SPECIAL_FORM_NAMES // 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", "provide"]; 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", "scope", "provide"];
// HO_FORM_NAMES // HO_FORM_NAMES
var HO_FORM_NAMES = ["map", "map-indexed", "filter", "reduce", "some", "every?", "for-each"]; var HO_FORM_NAMES = ["map", "map-indexed", "filter", "reduce", "some", "every?", "for-each"];
@@ -1869,15 +1894,28 @@ return result; }, args);
return append_b(results, aser(lambdaBody(f), local)); return append_b(results, aser(lambdaBody(f), local));
})() : invoke(f, item)); } } })() : invoke(f, item)); } }
return (isSxTruthy(isEmpty(results)) ? NIL : results); 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) : (isSxTruthy((name == "provide")) ? (function() { })() : (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 == "scope")) ? (function() {
var scopeName = trampoline(evalExpr(first(args), env));
var restArgs = rest(args);
var scopeVal = NIL;
var bodyArgs = NIL;
(isSxTruthy((isSxTruthy((len(restArgs) >= 2)) && isSxTruthy((typeOf(first(restArgs)) == "keyword")) && (keywordName(first(restArgs)) == "value"))) ? ((scopeVal = trampoline(evalExpr(nth(restArgs, 1), env))), (bodyArgs = slice(restArgs, 2))) : (bodyArgs = restArgs));
scopePush(scopeName, scopeVal);
return (function() {
var result = NIL;
{ var _c = bodyArgs; for (var _i = 0; _i < _c.length; _i++) { var body = _c[_i]; result = aser(body, env); } }
scopePop(scopeName);
return result;
})();
})() : (isSxTruthy((name == "provide")) ? (function() {
var provName = trampoline(evalExpr(first(args), env)); var provName = trampoline(evalExpr(first(args), env));
var provVal = trampoline(evalExpr(nth(args, 1), env)); var provVal = trampoline(evalExpr(nth(args, 1), env));
var result = NIL; var result = NIL;
providePush(provName, provVal); scopePush(provName, provVal);
{ var _c = slice(args, 2); for (var _i = 0; _i < _c.length; _i++) { var body = _c[_i]; result = aser(body, env); } } { var _c = slice(args, 2); for (var _i = 0; _i < _c.length; _i++) { var body = _c[_i]; result = aser(body, env); } }
providePop(provName); scopePop(provName);
return result; return result;
})() : trampoline(evalExpr(expr, env)))))))))))))))); })() : trampoline(evalExpr(expr, env)))))))))))))))));
})(); }; })(); };
// eval-case-aser // eval-case-aser
@@ -1898,7 +1936,7 @@ return result; }, args);
// render-to-dom // render-to-dom
var renderToDom = function(expr, env, ns) { setRenderActiveB(true); var renderToDom = function(expr, env, ns) { setRenderActiveB(true);
return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragment(); if (_m == "boolean") return createFragment(); if (_m == "raw-html") return domParseHtml(rawHtmlContent(expr)); if (_m == "string") return createTextNode(expr); if (_m == "number") return createTextNode((String(expr))); if (_m == "symbol") return renderToDom(trampoline(evalExpr(expr, env)), env, ns); if (_m == "keyword") return createTextNode(keywordName(expr)); if (_m == "dom-node") return expr; if (_m == "spread") return expr; if (_m == "dict") return createFragment(); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? createFragment() : renderDomList(expr, env, ns)); return (isSxTruthy(isSignal(expr)) ? (isSxTruthy(_islandScope) ? reactiveText(expr) : createTextNode((String(deref(expr))))) : createTextNode((String(expr)))); })(); }; return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragment(); if (_m == "boolean") return createFragment(); if (_m == "raw-html") return domParseHtml(rawHtmlContent(expr)); if (_m == "string") return createTextNode(expr); if (_m == "number") return createTextNode((String(expr))); if (_m == "symbol") return renderToDom(trampoline(evalExpr(expr, env)), env, ns); if (_m == "keyword") return createTextNode(keywordName(expr)); if (_m == "dom-node") return expr; if (_m == "spread") return (sxEmit("element-attrs", spreadAttrs(expr)), expr); if (_m == "dict") return createFragment(); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? createFragment() : renderDomList(expr, env, ns)); return (isSxTruthy(isSignal(expr)) ? (isSxTruthy(_islandScope) ? reactiveText(expr) : createTextNode((String(deref(expr))))) : createTextNode((String(expr)))); })(); };
// render-dom-list // render-dom-list
var renderDomList = function(expr, env, ns) { return (function() { var renderDomList = function(expr, env, ns) { return (function() {
@@ -1927,6 +1965,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
var renderDomElement = function(tag, args, env, ns) { return (function() { var renderDomElement = function(tag, args, env, ns) { return (function() {
var newNs = (isSxTruthy((tag == "svg")) ? SVG_NS : (isSxTruthy((tag == "math")) ? MATH_NS : ns)); var newNs = (isSxTruthy((tag == "svg")) ? SVG_NS : (isSxTruthy((tag == "math")) ? MATH_NS : ns));
var el = domCreateElement(tag, newNs); var el = domCreateElement(tag, newNs);
scopePush("element-attrs", NIL);
reduce(function(state, arg) { return (function() { reduce(function(state, arg) { return (function() {
var skip = get(state, "skip"); var skip = get(state, "skip");
return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() { return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() {
@@ -1951,8 +1990,11 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
return assoc(state, "skip", true, "i", (get(state, "i") + 1)); return assoc(state, "skip", true, "i", (get(state, "i") + 1));
})() : ((isSxTruthy(!isSxTruthy(contains(VOID_ELEMENTS, tag))) ? (function() { })() : ((isSxTruthy(!isSxTruthy(contains(VOID_ELEMENTS, tag))) ? (function() {
var child = renderToDom(arg, env, newNs); var child = renderToDom(arg, env, newNs);
return (isSxTruthy((isSxTruthy(isSpread(child)) && _islandScope)) ? reactiveSpread(el, function() { return renderToDom(arg, env, newNs); }) : (isSxTruthy(isSpread(child)) ? forEach(function(key) { return (function() { return (isSxTruthy((isSxTruthy(isSpread(child)) && _islandScope)) ? reactiveSpread(el, function() { return renderToDom(arg, env, newNs); }) : (isSxTruthy(isSpread(child)) ? NIL : domAppend(el, child)));
var val = dictGet(spreadAttrs(child), key); })() : NIL), assoc(state, "i", (get(state, "i") + 1)))));
})(); }, {["i"]: 0, ["skip"]: false}, args);
{ var _c = sxEmitted("element-attrs"); for (var _i = 0; _i < _c.length; _i++) { var spreadDict = _c[_i]; { var _c = keys(spreadDict); for (var _i = 0; _i < _c.length; _i++) { var key = _c[_i]; (function() {
var val = dictGet(spreadDict, key);
return (isSxTruthy((key == "class")) ? (function() { return (isSxTruthy((key == "class")) ? (function() {
var existing = domGetAttr(el, "class"); var existing = domGetAttr(el, "class");
return domSetAttr(el, "class", (isSxTruthy((isSxTruthy(existing) && !isSxTruthy((existing == "")))) ? (String(existing) + String(" ") + String(val)) : val)); return domSetAttr(el, "class", (isSxTruthy((isSxTruthy(existing) && !isSxTruthy((existing == "")))) ? (String(existing) + String(" ") + String(val)) : val));
@@ -1960,9 +2002,8 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
var existing = domGetAttr(el, "style"); var existing = domGetAttr(el, "style");
return domSetAttr(el, "style", (isSxTruthy((isSxTruthy(existing) && !isSxTruthy((existing == "")))) ? (String(existing) + String(";") + String(val)) : val)); return domSetAttr(el, "style", (isSxTruthy((isSxTruthy(existing) && !isSxTruthy((existing == "")))) ? (String(existing) + String(";") + String(val)) : val));
})() : domSetAttr(el, key, (String(val))))); })() : domSetAttr(el, key, (String(val)))));
})(); }, keys(spreadAttrs(child))) : domAppend(el, child))); })(); } } } }
})() : NIL), assoc(state, "i", (get(state, "i") + 1))))); scopePop("element-attrs");
})(); }, {["i"]: 0, ["skip"]: false}, args);
return el; return el;
})(); }; })(); };
@@ -2019,7 +2060,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
var renderDomUnknownComponent = function(name) { return error((String("Unknown component: ") + String(name))); }; var renderDomUnknownComponent = function(name) { return error((String("Unknown component: ") + String(name))); };
// RENDER_DOM_FORMS // 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", "provide"]; 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", "scope", "provide"];
// render-dom-form? // render-dom-form?
var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); }; var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); };
@@ -2161,15 +2202,26 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
return domAppend(frag, val); return domAppend(frag, val);
})(); } } })(); } }
return frag; return frag;
})() : (isSxTruthy((name == "scope")) ? (function() {
var scopeName = trampoline(evalExpr(nth(expr, 1), env));
var restArgs = slice(expr, 2);
var scopeVal = NIL;
var bodyExprs = NIL;
var frag = createFragment();
(isSxTruthy((isSxTruthy((len(restArgs) >= 2)) && isSxTruthy((typeOf(first(restArgs)) == "keyword")) && (keywordName(first(restArgs)) == "value"))) ? ((scopeVal = trampoline(evalExpr(nth(restArgs, 1), env))), (bodyExprs = slice(restArgs, 2))) : (bodyExprs = restArgs));
scopePush(scopeName, scopeVal);
{ var _c = bodyExprs; for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; domAppend(frag, renderToDom(e, env, ns)); } }
scopePop(scopeName);
return frag;
})() : (isSxTruthy((name == "provide")) ? (function() { })() : (isSxTruthy((name == "provide")) ? (function() {
var provName = trampoline(evalExpr(nth(expr, 1), env)); var provName = trampoline(evalExpr(nth(expr, 1), env));
var provVal = trampoline(evalExpr(nth(expr, 2), env)); var provVal = trampoline(evalExpr(nth(expr, 2), env));
var frag = createFragment(); var frag = createFragment();
providePush(provName, provVal); scopePush(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)); } } { 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); scopePop(provName);
return frag; return frag;
})() : renderToDom(trampoline(evalExpr(expr, env)), env, ns))))))))))))))); }; })() : renderToDom(trampoline(evalExpr(expr, env)), env, ns)))))))))))))))); };
// render-lambda-dom // render-lambda-dom
var renderLambdaDom = function(f, args, env, ns) { return (function() { var renderLambdaDom = function(f, args, env, ns) { return (function() {
@@ -6634,6 +6686,8 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
collect: sxCollect, collect: sxCollect,
collected: sxCollected, collected: sxCollected,
clearCollected: sxClearCollected, clearCollected: sxClearCollected,
scopePush: scopePush,
scopePop: scopePop,
providePush: providePush, providePush: providePush,
providePop: providePop, providePop: providePop,
context: sxContext, context: sxContext,

View File

@@ -48,7 +48,7 @@
"string" (escape-html expr) "string" (escape-html expr)
"number" (escape-html (str expr)) "number" (escape-html (str expr))
"raw-html" (raw-html-content expr) "raw-html" (raw-html-content expr)
"spread" expr "spread" (do (emit! "element-attrs" (spread-attrs expr)) "")
"symbol" (let ((val (async-eval expr env ctx))) "symbol" (let ((val (async-eval expr env ctx)))
(async-render val env ctx)) (async-render val env ctx))
"keyword" (escape-html (keyword-name expr)) "keyword" (escape-html (keyword-name expr))
@@ -80,10 +80,9 @@
(= name "raw!") (= name "raw!")
(async-render-raw args env ctx) (async-render-raw args env ctx)
;; Fragment (spreads filtered — no parent element) ;; Fragment
(= name "<>") (= name "<>")
(join "" (filter (fn (r) (not (spread? r))) (join "" (async-map-render args env ctx))
(async-map-render args env ctx)))
;; html: prefix ;; html: prefix
(starts-with? name "html:") (starts-with? name "html:")
@@ -171,18 +170,19 @@
(css-class-collect! (str class-val)))) (css-class-collect! (str class-val))))
(if (contains? VOID_ELEMENTS tag) (if (contains? VOID_ELEMENTS tag)
(str "<" tag (render-attrs attrs) ">") (str "<" tag (render-attrs attrs) ">")
;; Render children, collecting spreads and content separately ;; Provide scope for spread emit!
(let ((token (if (or (= tag "svg") (= tag "math")) (let ((token (if (or (= tag "svg") (= tag "math"))
(svg-context-set! true) (svg-context-set! true)
nil)) nil))
(content-parts (list))) (content-parts (list)))
(scope-push! "element-attrs" nil)
(for-each (for-each
(fn (c) (fn (c) (append! content-parts (async-render c env ctx)))
(let ((result (async-render c env ctx)))
(if (spread? result)
(merge-spread-attrs attrs (spread-attrs result))
(append! content-parts result))))
children) children)
(for-each
(fn (spread-dict) (merge-spread-attrs attrs spread-dict))
(emitted "element-attrs"))
(scope-pop! "element-attrs")
(when token (svg-context-reset! token)) (when token (svg-context-reset! token))
(str "<" tag (render-attrs attrs) ">" (str "<" tag (render-attrs attrs) ">"
(join "" content-parts) (join "" content-parts)
@@ -231,14 +231,11 @@
(for-each (for-each
(fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil))) (fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
(component-params comp)) (component-params comp))
;; Pre-render children to raw HTML (filter spreads — no parent element) ;; Pre-render children to raw HTML
(when (component-has-children? comp) (when (component-has-children? comp)
(let ((parts (list))) (let ((parts (list)))
(for-each (for-each
(fn (c) (fn (c) (append! parts (async-render c env ctx)))
(let ((r (async-render c env ctx)))
(when (not (spread? r))
(append! parts r))))
children) children)
(env-set! local "children" (env-set! local "children"
(make-raw-html (join "" parts))))) (make-raw-html (join "" parts)))))
@@ -259,14 +256,11 @@
(for-each (for-each
(fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil))) (fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
(component-params island)) (component-params island))
;; Pre-render children (filter spreads — no parent element) ;; Pre-render children
(when (component-has-children? island) (when (component-has-children? island)
(let ((parts (list))) (let ((parts (list)))
(for-each (for-each
(fn (c) (fn (c) (append! parts (async-render c env ctx)))
(let ((r (async-render c env ctx)))
(when (not (spread? r))
(append! parts r))))
children) children)
(env-set! local "children" (env-set! local "children"
(make-raw-html (join "" parts))))) (make-raw-html (join "" parts)))))
@@ -341,7 +335,7 @@
(list "if" "when" "cond" "case" "let" "let*" "begin" "do" (list "if" "when" "cond" "case" "let" "let*" "begin" "do"
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler" "define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
"deftype" "defeffect" "deftype" "defeffect"
"map" "map-indexed" "filter" "for-each" "provide")) "map" "map-indexed" "filter" "for-each" "scope" "provide"))
(define async-render-form? :effects [] (define async-render-form? :effects []
(fn ((name :as string)) (fn ((name :as string))
@@ -367,14 +361,13 @@
(async-render (nth expr 3) env ctx) (async-render (nth expr 3) env ctx)
""))) "")))
;; when — single body: pass through (spread propagates). Multi: join strings. ;; when — single body: pass through. Multi: join strings.
(= name "when") (= name "when")
(if (not (async-eval (nth expr 1) env ctx)) (if (not (async-eval (nth expr 1) env ctx))
"" ""
(if (= (len expr) 3) (if (= (len expr) 3)
(async-render (nth expr 2) env ctx) (async-render (nth expr 2) env ctx)
(let ((results (async-map-render (slice expr 2) env ctx))) (join "" (async-map-render (slice expr 2) env ctx))))
(join "" (filter (fn (r) (not (spread? r))) results)))))
;; cond — uses cond-scheme? (every? check) from eval.sx ;; cond — uses cond-scheme? (every? check) from eval.sx
(= name "cond") (= name "cond")
@@ -392,60 +385,71 @@
(let ((local (async-process-bindings (nth expr 1) env ctx))) (let ((local (async-process-bindings (nth expr 1) env ctx)))
(if (= (len expr) 3) (if (= (len expr) 3)
(async-render (nth expr 2) local ctx) (async-render (nth expr 2) local ctx)
(let ((results (async-map-render (slice expr 2) local ctx))) (join "" (async-map-render (slice expr 2) local ctx))))
(join "" (filter (fn (r) (not (spread? r))) results)))))
;; begin / do — single body: pass through. Multi: join strings. ;; begin / do — single body: pass through. Multi: join strings.
(or (= name "begin") (= name "do")) (or (= name "begin") (= name "do"))
(if (= (len expr) 2) (if (= (len expr) 2)
(async-render (nth expr 1) env ctx) (async-render (nth expr 1) env ctx)
(let ((results (async-map-render (rest expr) env ctx))) (join "" (async-map-render (rest expr) env ctx)))
(join "" (filter (fn (r) (not (spread? r))) results))))
;; Definition forms ;; Definition forms
(definition-form? name) (definition-form? name)
(do (async-eval expr env ctx) "") (do (async-eval expr env ctx) "")
;; map — spreads filtered ;; map
(= name "map") (= name "map")
(let ((f (async-eval (nth expr 1) env ctx)) (let ((f (async-eval (nth expr 1) env ctx))
(coll (async-eval (nth expr 2) env ctx))) (coll (async-eval (nth expr 2) env ctx)))
(join "" (join "" (async-map-fn-render f coll env ctx)))
(filter (fn (r) (not (spread? r)))
(async-map-fn-render f coll env ctx))))
;; map-indexed — spreads filtered ;; map-indexed
(= name "map-indexed") (= name "map-indexed")
(let ((f (async-eval (nth expr 1) env ctx)) (let ((f (async-eval (nth expr 1) env ctx))
(coll (async-eval (nth expr 2) env ctx))) (coll (async-eval (nth expr 2) env ctx)))
(join "" (join "" (async-map-indexed-fn-render f coll env ctx)))
(filter (fn (r) (not (spread? r)))
(async-map-indexed-fn-render f coll env ctx))))
;; filter — eval fully then render ;; filter — eval fully then render
(= name "filter") (= name "filter")
(async-render (async-eval expr env ctx) env ctx) (async-render (async-eval expr env ctx) env ctx)
;; for-each (render variant) — spreads filtered ;; for-each (render variant)
(= name "for-each") (= name "for-each")
(let ((f (async-eval (nth expr 1) env ctx)) (let ((f (async-eval (nth expr 1) env ctx))
(coll (async-eval (nth expr 2) env ctx))) (coll (async-eval (nth expr 2) env ctx)))
(join "" (join "" (async-map-fn-render f coll env ctx)))
(filter (fn (r) (not (spread? r)))
(async-map-fn-render f coll env ctx))))
;; provide — render-time dynamic scope ;; scope — unified render-time dynamic scope
(= name "scope")
(let ((scope-name (async-eval (nth expr 1) env ctx))
(rest-args (slice expr 2))
(scope-val nil)
(body-exprs nil))
;; Check for :value keyword
(if (and (>= (len rest-args) 2)
(= (type-of (first rest-args)) "keyword")
(= (keyword-name (first rest-args)) "value"))
(do (set! scope-val (async-eval (nth rest-args 1) env ctx))
(set! body-exprs (slice rest-args 2)))
(set! body-exprs rest-args))
(scope-push! scope-name scope-val)
(let ((result (if (= (len body-exprs) 1)
(async-render (first body-exprs) env ctx)
(join "" (async-map-render body-exprs env ctx)))))
(scope-pop! scope-name)
result))
;; provide — sugar for scope with value
(= name "provide") (= name "provide")
(let ((prov-name (async-eval (nth expr 1) env ctx)) (let ((prov-name (async-eval (nth expr 1) env ctx))
(prov-val (async-eval (nth expr 2) env ctx)) (prov-val (async-eval (nth expr 2) env ctx))
(body-start 3) (body-start 3)
(body-count (- (len expr) 3))) (body-count (- (len expr) 3)))
(provide-push! prov-name prov-val) (scope-push! prov-name prov-val)
(let ((result (if (= body-count 1) (let ((result (if (= body-count 1)
(async-render (nth expr body-start) env ctx) (async-render (nth expr body-start) env ctx)
(let ((results (async-map-render (slice expr body-start) env ctx))) (join "" (async-map-render (slice expr body-start) env ctx)))))
(join "" (filter (fn (r) (not (spread? r))) results)))))) (scope-pop! prov-name)
(provide-pop! prov-name)
result)) result))
;; Fallback ;; Fallback
@@ -595,35 +599,34 @@
(define-async async-aser :effects [render io] (define-async async-aser :effects [render io]
(fn (expr (env :as dict) ctx) (fn (expr (env :as dict) ctx)
(case (type-of expr) (let ((t (type-of expr))
"number" expr (result nil))
"string" expr (cond
"boolean" expr (= t "number") (set! result expr)
"nil" nil (= t "string") (set! result expr)
(= t "boolean") (set! result expr)
"symbol" (= t "nil") (set! result nil)
(= t "symbol")
(let ((name (symbol-name expr))) (let ((name (symbol-name expr)))
(set! result
(cond (cond
(env-has? env name) (env-get env name) (env-has? env name) (env-get env name)
(primitive? name) (get-primitive name) (primitive? name) (get-primitive name)
(= name "true") true (= name "true") true
(= name "false") false (= name "false") false
(= name "nil") nil (= name "nil") nil
:else (error (str "Undefined symbol: " name)))) :else (error (str "Undefined symbol: " name)))))
(= t "keyword") (set! result (keyword-name expr))
"keyword" (keyword-name expr) (= t "dict") (set! result (async-aser-dict expr env ctx))
;; Spread — emit attrs to nearest element provider
"dict" (async-aser-dict expr env ctx) (= t "spread") (do (emit! "element-attrs" (spread-attrs expr))
(set! result nil))
;; Spread — pass through for client rendering (= t "list") (set! result (if (empty? expr) (list) (async-aser-list expr env ctx)))
"spread" expr :else (set! result expr))
;; Catch spread values from function calls and symbol lookups
"list" (if (spread? result)
(if (empty? expr) (do (emit! "element-attrs" (spread-attrs result)) nil)
(list) result))))
(async-aser-list expr env ctx))
:else expr)))
(define-async async-aser-dict :effects [render io] (define-async async-aser-dict :effects [render io]
@@ -775,7 +778,6 @@
(define-async async-aser-fragment :effects [render io] (define-async async-aser-fragment :effects [render io]
(fn ((children :as list) (env :as dict) ctx) (fn ((children :as list) (env :as dict) ctx)
;; Spreads are filtered — fragments have no parent element to merge into
(let ((parts (list))) (let ((parts (list)))
(for-each (for-each
(fn (c) (fn (c)
@@ -783,10 +785,10 @@
(if (= (type-of result) "list") (if (= (type-of result) "list")
(for-each (for-each
(fn (item) (fn (item)
(when (and (not (nil? item)) (not (spread? item))) (when (not (nil? item))
(append! parts (serialize item)))) (append! parts (serialize item))))
result) result)
(when (and (not (nil? result)) (not (spread? result))) (when (not (nil? result))
(append! parts (serialize result)))))) (append! parts (serialize result))))))
children) children)
(if (empty? parts) (if (empty? parts)
@@ -860,9 +862,12 @@
(let ((token (if (or (= name "svg") (= name "math")) (let ((token (if (or (= name "svg") (= name "math"))
(svg-context-set! true) (svg-context-set! true)
nil)) nil))
(parts (list name)) (attr-parts (list))
(child-parts (list))
(skip false) (skip false)
(i 0)) (i 0))
;; Provide scope for spread emit!
(scope-push! "element-attrs" nil)
(for-each (for-each
(fn (arg) (fn (arg)
(if skip (if skip
@@ -872,39 +877,43 @@
(< (inc i) (len args))) (< (inc i) (len args)))
(let ((val (async-aser (nth args (inc i)) env ctx))) (let ((val (async-aser (nth args (inc i)) env ctx)))
(when (not (nil? val)) (when (not (nil? val))
(append! parts (str ":" (keyword-name arg))) (append! attr-parts (str ":" (keyword-name arg)))
(if (= (type-of val) "list") (if (= (type-of val) "list")
(let ((live (filter (fn (v) (not (nil? v))) val))) (let ((live (filter (fn (v) (not (nil? v))) val)))
(if (empty? live) (if (empty? live)
(append! parts "nil") (append! attr-parts "nil")
(let ((items (map serialize live))) (let ((items (map serialize live)))
(if (some (fn (v) (sx-expr? v)) live) (if (some (fn (v) (sx-expr? v)) live)
(append! parts (str "(<> " (join " " items) ")")) (append! attr-parts (str "(<> " (join " " items) ")"))
(append! parts (str "(list " (join " " items) ")")))))) (append! attr-parts (str "(list " (join " " items) ")"))))))
(append! parts (serialize val)))) (append! attr-parts (serialize val))))
(set! skip true) (set! skip true)
(set! i (inc i))) (set! i (inc i)))
(let ((result (async-aser arg env ctx))) (let ((result (async-aser arg env ctx)))
(when (not (nil? result)) (when (not (nil? result))
(if (spread? result)
;; Spread child — merge attrs as keyword args into parent element
(for-each
(fn (k)
(let ((v (dict-get (spread-attrs result) k)))
(append! parts (str ":" k))
(append! parts (serialize v))))
(keys (spread-attrs result)))
(if (= (type-of result) "list") (if (= (type-of result) "list")
(for-each (for-each
(fn (item) (fn (item)
(when (not (nil? item)) (when (not (nil? item))
(append! parts (serialize item)))) (append! child-parts (serialize item))))
result) result)
(append! parts (serialize result))))) (append! child-parts (serialize result))))
(set! i (inc i)))))) (set! i (inc i))))))
args) args)
;; Collect emitted spread attrs — after explicit attrs, before children
(for-each
(fn (spread-dict)
(for-each
(fn (k)
(let ((v (dict-get spread-dict k)))
(append! attr-parts (str ":" k))
(append! attr-parts (serialize v))))
(keys spread-dict)))
(emitted "element-attrs"))
(scope-pop! "element-attrs")
(when token (svg-context-reset! token)) (when token (svg-context-reset! token))
(make-sx-expr (str "(" (join " " parts) ")"))))) (let ((parts (concat (list name) attr-parts child-parts)))
(make-sx-expr (str "(" (join " " parts) ")"))))))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -917,7 +926,7 @@
"define" "defcomp" "defmacro" "defstyle" "define" "defcomp" "defmacro" "defstyle"
"defhandler" "defpage" "defquery" "defaction" "defhandler" "defpage" "defquery" "defaction"
"begin" "do" "quote" "->" "set!" "defisland" "begin" "do" "quote" "->" "set!" "defisland"
"deftype" "defeffect" "provide")) "deftype" "defeffect" "scope" "provide"))
(define ASYNC_ASER_HO_NAMES (define ASYNC_ASER_HO_NAMES
(list "map" "map-indexed" "filter" "for-each")) (list "map" "map-indexed" "filter" "for-each"))
@@ -1055,15 +1064,35 @@
(= name "deftype") (= name "defeffect")) (= name "deftype") (= name "defeffect"))
(do (async-eval expr env ctx) nil) (do (async-eval expr env ctx) nil)
;; provide — render-time dynamic scope ;; scope — unified render-time dynamic scope
(= name "scope")
(let ((scope-name (async-eval (first args) env ctx))
(rest-args (rest args))
(scope-val nil)
(body-args nil))
;; Check for :value keyword
(if (and (>= (len rest-args) 2)
(= (type-of (first rest-args)) "keyword")
(= (keyword-name (first rest-args)) "value"))
(do (set! scope-val (async-eval (nth rest-args 1) env ctx))
(set! body-args (slice rest-args 2)))
(set! body-args rest-args))
(scope-push! scope-name scope-val)
(let ((result nil))
(for-each (fn (body) (set! result (async-aser body env ctx)))
body-args)
(scope-pop! scope-name)
result))
;; provide — sugar for scope with value
(= name "provide") (= name "provide")
(let ((prov-name (async-eval (first args) env ctx)) (let ((prov-name (async-eval (first args) env ctx))
(prov-val (async-eval (nth args 1) env ctx)) (prov-val (async-eval (nth args 1) env ctx))
(result nil)) (result nil))
(provide-push! prov-name prov-val) (scope-push! prov-name prov-val)
(for-each (fn (body) (set! result (async-aser body env ctx))) (for-each (fn (body) (set! result (async-aser body env ctx)))
(slice args 2)) (slice args 2))
(provide-pop! prov-name) (scope-pop! prov-name)
result) result)
;; Fallback ;; Fallback

View File

@@ -44,8 +44,8 @@
;; Pre-rendered DOM node → pass through ;; Pre-rendered DOM node → pass through
"dom-node" expr "dom-node" expr
;; Spread → pass through (parent element handles it) ;; Spread → emit attrs to nearest element provider, pass through for reactive-spread
"spread" expr "spread" (do (emit! "element-attrs" (spread-attrs expr)) expr)
;; Dict → empty ;; Dict → empty
"dict" (create-fragment) "dict" (create-fragment)
@@ -180,6 +180,9 @@
:else ns)) :else ns))
(el (dom-create-element tag new-ns))) (el (dom-create-element tag new-ns)))
;; Provide scope for spread emit! — deeply nested spreads emit here
(scope-push! "element-attrs" nil)
;; Process args: keywords → attrs, others → children ;; Process args: keywords → attrs, others → children
(reduce (reduce
(fn (state arg) (fn (state arg)
@@ -236,28 +239,8 @@
;; Reactive spread: track signal deps, update attrs on change ;; Reactive spread: track signal deps, update attrs on change
(and (spread? child) *island-scope*) (and (spread? child) *island-scope*)
(reactive-spread el (fn () (render-to-dom arg env new-ns))) (reactive-spread el (fn () (render-to-dom arg env new-ns)))
;; Static spread: one-shot merge attrs onto parent element ;; Static spread: already emitted via provide, skip
(spread? child) (spread? child) nil
(for-each
(fn ((key :as string))
(let ((val (dict-get (spread-attrs child) key)))
(if (= key "class")
;; Class: append to existing
(let ((existing (dom-get-attr el "class")))
(dom-set-attr el "class"
(if (and existing (not (= existing "")))
(str existing " " val)
val)))
(if (= key "style")
;; Style: append with semicolon
(let ((existing (dom-get-attr el "style")))
(dom-set-attr el "style"
(if (and existing (not (= existing "")))
(str existing ";" val)
val)))
;; Other attrs: overwrite
(dom-set-attr el key (str val))))))
(keys (spread-attrs child)))
;; Normal child: append to element ;; Normal child: append to element
:else :else
(dom-append el child)))) (dom-append el child))))
@@ -265,6 +248,29 @@
(dict "i" 0 "skip" false) (dict "i" 0 "skip" false)
args) args)
;; Collect emitted spread attrs and merge onto DOM element
(for-each
(fn (spread-dict)
(for-each
(fn ((key :as string))
(let ((val (dict-get spread-dict key)))
(if (= key "class")
(let ((existing (dom-get-attr el "class")))
(dom-set-attr el "class"
(if (and existing (not (= existing "")))
(str existing " " val)
val)))
(if (= key "style")
(let ((existing (dom-get-attr el "style")))
(dom-set-attr el "style"
(if (and existing (not (= existing "")))
(str existing ";" val)
val)))
(dom-set-attr el key (str val))))))
(keys spread-dict)))
(emitted "element-attrs"))
(scope-pop! "element-attrs")
el))) el)))
@@ -375,7 +381,7 @@
(list "if" "when" "cond" "case" "let" "let*" "begin" "do" (list "if" "when" "cond" "case" "let" "let*" "begin" "do"
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler" "define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
"map" "map-indexed" "filter" "for-each" "portal" "map" "map-indexed" "filter" "for-each" "portal"
"error-boundary" "provide")) "error-boundary" "scope" "provide"))
(define render-dom-form? :effects [] (define render-dom-form? :effects []
(fn ((name :as string)) (fn ((name :as string))
@@ -631,17 +637,39 @@
coll) coll)
frag) frag)
;; provide — render-time dynamic scope ;; scope — unified render-time dynamic scope
(= name "scope")
(let ((scope-name (trampoline (eval-expr (nth expr 1) env)))
(rest-args (slice expr 2))
(scope-val nil)
(body-exprs nil)
(frag (create-fragment)))
;; Check for :value keyword
(if (and (>= (len rest-args) 2)
(= (type-of (first rest-args)) "keyword")
(= (keyword-name (first rest-args)) "value"))
(do (set! scope-val (trampoline (eval-expr (nth rest-args 1) env)))
(set! body-exprs (slice rest-args 2)))
(set! body-exprs rest-args))
(scope-push! scope-name scope-val)
(for-each
(fn (e)
(dom-append frag (render-to-dom e env ns)))
body-exprs)
(scope-pop! scope-name)
frag)
;; provide — sugar for scope with value
(= name "provide") (= name "provide")
(let ((prov-name (trampoline (eval-expr (nth expr 1) env))) (let ((prov-name (trampoline (eval-expr (nth expr 1) env)))
(prov-val (trampoline (eval-expr (nth expr 2) env))) (prov-val (trampoline (eval-expr (nth expr 2) env)))
(frag (create-fragment))) (frag (create-fragment)))
(provide-push! prov-name prov-val) (scope-push! prov-name prov-val)
(for-each (for-each
(fn (i) (fn (i)
(dom-append frag (render-to-dom (nth expr i) env ns))) (dom-append frag (render-to-dom (nth expr i) env ns)))
(range 3 (len expr))) (range 3 (len expr)))
(provide-pop! prov-name) (scope-pop! prov-name)
frag) frag)
;; Fallback ;; Fallback

View File

@@ -30,8 +30,8 @@
"keyword" (escape-html (keyword-name expr)) "keyword" (escape-html (keyword-name expr))
;; Raw HTML passthrough ;; Raw HTML passthrough
"raw-html" (raw-html-content expr) "raw-html" (raw-html-content expr)
;; Spread — pass through as-is (parent element will merge attrs) ;; Spread — emit attrs to nearest element provider
"spread" expr "spread" (do (emit! "element-attrs" (spread-attrs expr)) "")
;; Everything else — evaluate first ;; Everything else — evaluate first
:else (render-value-to-html (trampoline (eval-expr expr env)) env)))) :else (render-value-to-html (trampoline (eval-expr expr env)) env))))
@@ -44,7 +44,7 @@
"boolean" (if val "true" "false") "boolean" (if val "true" "false")
"list" (render-list-to-html val env) "list" (render-list-to-html val env)
"raw-html" (raw-html-content val) "raw-html" (raw-html-content val)
"spread" val "spread" (do (emit! "element-attrs" (spread-attrs val)) "")
:else (escape-html (str val))))) :else (escape-html (str val)))))
@@ -56,7 +56,7 @@
(list "if" "when" "cond" "case" "let" "let*" "begin" "do" (list "if" "when" "cond" "case" "let" "let*" "begin" "do"
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler" "define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
"deftype" "defeffect" "deftype" "defeffect"
"map" "map-indexed" "filter" "for-each" "provide")) "map" "map-indexed" "filter" "for-each" "scope" "provide"))
(define render-html-form? :effects [] (define render-html-form? :effects []
(fn ((name :as string)) (fn ((name :as string))
@@ -73,16 +73,14 @@
"" ""
(let ((head (first expr))) (let ((head (first expr)))
(if (not (= (type-of head) "symbol")) (if (not (= (type-of head) "symbol"))
;; Data list — render each item (spreads filtered — no parent element) ;; Data list — render each item
(join "" (filter (fn (x) (not (spread? x))) (join "" (map (fn (x) (render-value-to-html x env)) expr))
(map (fn (x) (render-value-to-html x env)) expr)))
(let ((name (symbol-name head)) (let ((name (symbol-name head))
(args (rest expr))) (args (rest expr)))
(cond (cond
;; Fragment (spreads filtered — no parent element) ;; Fragment
(= name "<>") (= name "<>")
(join "" (filter (fn (x) (not (spread? x))) (join "" (map (fn (x) (render-to-html x env)) args))
(map (fn (x) (render-to-html x env)) args)))
;; Raw HTML passthrough ;; Raw HTML passthrough
(= name "raw!") (= name "raw!")
@@ -152,15 +150,14 @@
(render-to-html (nth expr 3) env) (render-to-html (nth expr 3) env)
""))) "")))
;; when — single body: pass through (spread propagates). Multi: join strings. ;; when — single body: pass through. Multi: join strings.
(= name "when") (= name "when")
(if (not (trampoline (eval-expr (nth expr 1) env))) (if (not (trampoline (eval-expr (nth expr 1) env)))
"" ""
(if (= (len expr) 3) (if (= (len expr) 3)
(render-to-html (nth expr 2) env) (render-to-html (nth expr 2) env)
(let ((results (map (fn (i) (render-to-html (nth expr i) env)) (join "" (map (fn (i) (render-to-html (nth expr i) env))
(range 2 (len expr))))) (range 2 (len expr))))))
(join "" (filter (fn (r) (not (spread? r))) results)))))
;; cond ;; cond
(= name "cond") (= name "cond")
@@ -178,78 +175,92 @@
(let ((local (process-bindings (nth expr 1) env))) (let ((local (process-bindings (nth expr 1) env)))
(if (= (len expr) 3) (if (= (len expr) 3)
(render-to-html (nth expr 2) local) (render-to-html (nth expr 2) local)
(let ((results (map (fn (i) (render-to-html (nth expr i) local)) (join "" (map (fn (i) (render-to-html (nth expr i) local))
(range 2 (len expr))))) (range 2 (len expr))))))
(join "" (filter (fn (r) (not (spread? r))) results)))))
;; begin / do — single body: pass through. Multi: join strings. ;; begin / do — single body: pass through. Multi: join strings.
(or (= name "begin") (= name "do")) (or (= name "begin") (= name "do"))
(if (= (len expr) 2) (if (= (len expr) 2)
(render-to-html (nth expr 1) env) (render-to-html (nth expr 1) env)
(let ((results (map (fn (i) (render-to-html (nth expr i) env)) (join "" (map (fn (i) (render-to-html (nth expr i) env))
(range 1 (len expr))))) (range 1 (len expr)))))
(join "" (filter (fn (r) (not (spread? r))) results))))
;; Definition forms — eval for side effects ;; Definition forms — eval for side effects
(definition-form? name) (definition-form? name)
(do (trampoline (eval-expr expr env)) "") (do (trampoline (eval-expr expr env)) "")
;; map — spreads filtered (no parent element in list context) ;; map
(= name "map") (= name "map")
(let ((f (trampoline (eval-expr (nth expr 1) env))) (let ((f (trampoline (eval-expr (nth expr 1) env)))
(coll (trampoline (eval-expr (nth expr 2) env)))) (coll (trampoline (eval-expr (nth expr 2) env))))
(join "" (join ""
(filter (fn (r) (not (spread? r)))
(map (map
(fn (item) (fn (item)
(if (lambda? f) (if (lambda? f)
(render-lambda-html f (list item) env) (render-lambda-html f (list item) env)
(render-to-html (apply f (list item)) env))) (render-to-html (apply f (list item)) env)))
coll)))) coll)))
;; map-indexed — spreads filtered ;; map-indexed
(= name "map-indexed") (= name "map-indexed")
(let ((f (trampoline (eval-expr (nth expr 1) env))) (let ((f (trampoline (eval-expr (nth expr 1) env)))
(coll (trampoline (eval-expr (nth expr 2) env)))) (coll (trampoline (eval-expr (nth expr 2) env))))
(join "" (join ""
(filter (fn (r) (not (spread? r)))
(map-indexed (map-indexed
(fn (i item) (fn (i item)
(if (lambda? f) (if (lambda? f)
(render-lambda-html f (list i item) env) (render-lambda-html f (list i item) env)
(render-to-html (apply f (list i item)) env))) (render-to-html (apply f (list i item)) env)))
coll)))) coll)))
;; filter — evaluate fully then render ;; filter — evaluate fully then render
(= name "filter") (= name "filter")
(render-to-html (trampoline (eval-expr expr env)) env) (render-to-html (trampoline (eval-expr expr env)) env)
;; for-each (render variant) — spreads filtered ;; for-each (render variant)
(= name "for-each") (= name "for-each")
(let ((f (trampoline (eval-expr (nth expr 1) env))) (let ((f (trampoline (eval-expr (nth expr 1) env)))
(coll (trampoline (eval-expr (nth expr 2) env)))) (coll (trampoline (eval-expr (nth expr 2) env))))
(join "" (join ""
(filter (fn (r) (not (spread? r)))
(map (map
(fn (item) (fn (item)
(if (lambda? f) (if (lambda? f)
(render-lambda-html f (list item) env) (render-lambda-html f (list item) env)
(render-to-html (apply f (list item)) env))) (render-to-html (apply f (list item)) env)))
coll)))) coll)))
;; provide — render-time dynamic scope ;; scope — unified render-time dynamic scope
(= name "scope")
(let ((scope-name (trampoline (eval-expr (nth expr 1) env)))
(rest-args (slice expr 2))
(scope-val nil)
(body-exprs nil))
;; Check for :value keyword
(if (and (>= (len rest-args) 2)
(= (type-of (first rest-args)) "keyword")
(= (keyword-name (first rest-args)) "value"))
(do (set! scope-val (trampoline (eval-expr (nth rest-args 1) env)))
(set! body-exprs (slice rest-args 2)))
(set! body-exprs rest-args))
(scope-push! scope-name scope-val)
(let ((result (if (= (len body-exprs) 1)
(render-to-html (first body-exprs) env)
(join "" (map (fn (e) (render-to-html e env)) body-exprs)))))
(scope-pop! scope-name)
result))
;; provide — sugar for scope with value
(= name "provide") (= name "provide")
(let ((prov-name (trampoline (eval-expr (nth expr 1) env))) (let ((prov-name (trampoline (eval-expr (nth expr 1) env)))
(prov-val (trampoline (eval-expr (nth expr 2) env))) (prov-val (trampoline (eval-expr (nth expr 2) env)))
(body-start 3) (body-start 3)
(body-count (- (len expr) 3))) (body-count (- (len expr) 3)))
(provide-push! prov-name prov-val) (scope-push! prov-name prov-val)
(let ((result (if (= body-count 1) (let ((result (if (= body-count 1)
(render-to-html (nth expr body-start) env) (render-to-html (nth expr body-start) env)
(join "" (filter (fn (r) (not (spread? r))) (join "" (map (fn (i) (render-to-html (nth expr i) env))
(map (fn (i) (render-to-html (nth expr i) env)) (range body-start (+ body-start body-count)))))))
(range body-start (+ body-start body-count)))))))) (scope-pop! prov-name)
(provide-pop! prov-name)
result)) result))
;; Fallback ;; Fallback
@@ -307,17 +318,9 @@
(env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil))) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
(component-params comp)) (component-params comp))
;; If component accepts children, pre-render them to raw HTML ;; If component accepts children, pre-render them to raw HTML
;; Spread values are filtered out (no parent element to merge onto)
(when (component-has-children? comp) (when (component-has-children? comp)
(let ((parts (list)))
(for-each
(fn (c)
(let ((r (render-to-html c env)))
(when (not (spread? r))
(append! parts r))))
children)
(env-set! local "children" (env-set! local "children"
(make-raw-html (join "" parts))))) (make-raw-html (join "" (map (fn (c) (render-to-html c env)) children)))))
(render-to-html (component-body comp) local))))) (render-to-html (component-body comp) local)))))
@@ -329,18 +332,17 @@
(is-void (contains? VOID_ELEMENTS tag))) (is-void (contains? VOID_ELEMENTS tag)))
(if is-void (if is-void
(str "<" tag (render-attrs attrs) " />") (str "<" tag (render-attrs attrs) " />")
;; Render children, collecting spreads and content separately ;; Provide scope for spread emit!
(let ((content-parts (list))) (do
(scope-push! "element-attrs" nil)
(let ((content (join "" (map (fn (c) (render-to-html c env)) children))))
(for-each (for-each
(fn (c) (fn (spread-dict) (merge-spread-attrs attrs spread-dict))
(let ((result (render-to-html c env))) (emitted "element-attrs"))
(if (spread? result) (scope-pop! "element-attrs")
(merge-spread-attrs attrs (spread-attrs result))
(append! content-parts result))))
children)
(str "<" tag (render-attrs attrs) ">" (str "<" tag (render-attrs attrs) ">"
(join "" content-parts) content
"</" tag ">")))))) "</" tag ">")))))))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -375,19 +377,17 @@
(assoc state "i" (inc (get state "i")))))))) (assoc state "i" (inc (get state "i"))))))))
(dict "i" 0 "skip" false) (dict "i" 0 "skip" false)
args) args)
;; Render children, handling spreads ;; Provide scope for spread emit!
(let ((lake-attrs (dict "data-sx-lake" (or lake-id ""))) (let ((lake-attrs (dict "data-sx-lake" (or lake-id ""))))
(content-parts (list))) (scope-push! "element-attrs" nil)
(let ((content (join "" (map (fn (c) (render-to-html c env)) children))))
(for-each (for-each
(fn (c) (fn (spread-dict) (merge-spread-attrs lake-attrs spread-dict))
(let ((result (render-to-html c env))) (emitted "element-attrs"))
(if (spread? result) (scope-pop! "element-attrs")
(merge-spread-attrs lake-attrs (spread-attrs result))
(append! content-parts result))))
children)
(str "<" lake-tag (render-attrs lake-attrs) ">" (str "<" lake-tag (render-attrs lake-attrs) ">"
(join "" content-parts) content
"</" lake-tag ">"))))) "</" lake-tag ">"))))))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -425,19 +425,17 @@
(assoc state "i" (inc (get state "i")))))))) (assoc state "i" (inc (get state "i"))))))))
(dict "i" 0 "skip" false) (dict "i" 0 "skip" false)
args) args)
;; Render children, handling spreads ;; Provide scope for spread emit!
(let ((marsh-attrs (dict "data-sx-marsh" (or marsh-id ""))) (let ((marsh-attrs (dict "data-sx-marsh" (or marsh-id ""))))
(content-parts (list))) (scope-push! "element-attrs" nil)
(let ((content (join "" (map (fn (c) (render-to-html c env)) children))))
(for-each (for-each
(fn (c) (fn (spread-dict) (merge-spread-attrs marsh-attrs spread-dict))
(let ((result (render-to-html c env))) (emitted "element-attrs"))
(if (spread? result) (scope-pop! "element-attrs")
(merge-spread-attrs marsh-attrs (spread-attrs result))
(append! content-parts result))))
children)
(str "<" marsh-tag (render-attrs marsh-attrs) ">" (str "<" marsh-tag (render-attrs marsh-attrs) ">"
(join "" content-parts) content
"</" marsh-tag ">"))))) "</" marsh-tag ">"))))))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -487,17 +485,9 @@
(component-params island)) (component-params island))
;; If island accepts children, pre-render them to raw HTML ;; If island accepts children, pre-render them to raw HTML
;; Spread values filtered out (no parent element)
(when (component-has-children? island) (when (component-has-children? island)
(let ((parts (list)))
(for-each
(fn (c)
(let ((r (render-to-html c env)))
(when (not (spread? r))
(append! parts r))))
children)
(env-set! local "children" (env-set! local "children"
(make-raw-html (join "" parts))))) (make-raw-html (join "" (map (fn (c) (render-to-html c env)) children)))))
;; Render the island body as HTML ;; Render the island body as HTML
(let ((body-html (render-to-html (component-body island) local)) (let ((body-html (render-to-html (component-body island) local))

View File

@@ -25,6 +25,7 @@
;; Evaluate for SX wire format — serialize rendering forms, ;; Evaluate for SX wire format — serialize rendering forms,
;; evaluate control flow and function calls. ;; evaluate control flow and function calls.
(set-render-active! true) (set-render-active! true)
(let ((result
(case (type-of expr) (case (type-of expr)
"number" expr "number" expr
"string" expr "string" expr
@@ -48,10 +49,14 @@
(list) (list)
(aser-list expr env)) (aser-list expr env))
;; Spread — pass through for client rendering ;; Spread — emit attrs to nearest element provider
"spread" expr "spread" (do (emit! "element-attrs" (spread-attrs expr)) nil)
:else expr))) :else expr)))
;; Catch spread values from function calls and symbol lookups
(if (spread? result)
(do (emit! "element-attrs" (spread-attrs result)) nil)
result))))
(define aser-list :effects [render] (define aser-list :effects [render]
@@ -110,7 +115,6 @@
(fn ((children :as list) (env :as dict)) (fn ((children :as list) (env :as dict))
;; Serialize (<> child1 child2 ...) to sx source string ;; Serialize (<> child1 child2 ...) to sx source string
;; Must flatten list results (e.g. from map/filter) to avoid nested parens ;; Must flatten list results (e.g. from map/filter) to avoid nested parens
;; Spreads are filtered — fragments have no parent element to merge into
(let ((parts (list))) (let ((parts (list)))
(for-each (for-each
(fn (c) (fn (c)
@@ -118,10 +122,10 @@
(if (= (type-of result) "list") (if (= (type-of result) "list")
(for-each (for-each
(fn (item) (fn (item)
(when (and (not (nil? item)) (not (spread? item))) (when (not (nil? item))
(append! parts (serialize item)))) (append! parts (serialize item))))
result) result)
(when (and (not (nil? result)) (not (spread? result))) (when (not (nil? result))
(append! parts (serialize result)))))) (append! parts (serialize result))))))
children) children)
(if (empty? parts) (if (empty? parts)
@@ -134,9 +138,13 @@
;; Serialize (name :key val child ...) — evaluate args but keep as sx ;; Serialize (name :key val child ...) — evaluate args but keep as sx
;; Uses for-each + mutable state (not reduce) so bootstrapper emits for-loops ;; Uses for-each + mutable state (not reduce) so bootstrapper emits for-loops
;; that can contain nested for-each for list flattening. ;; that can contain nested for-each for list flattening.
(let ((parts (list name)) ;; Separate attrs and children so emitted spread attrs go before children.
(let ((attr-parts (list))
(child-parts (list))
(skip false) (skip false)
(i 0)) (i 0))
;; Provide scope for spread emit!
(scope-push! "element-attrs" nil)
(for-each (for-each
(fn (arg) (fn (arg)
(if skip (if skip
@@ -146,30 +154,34 @@
(< (inc i) (len args))) (< (inc i) (len args)))
(let ((val (aser (nth args (inc i)) env))) (let ((val (aser (nth args (inc i)) env)))
(when (not (nil? val)) (when (not (nil? val))
(append! parts (str ":" (keyword-name arg))) (append! attr-parts (str ":" (keyword-name arg)))
(append! parts (serialize val))) (append! attr-parts (serialize val)))
(set! skip true) (set! skip true)
(set! i (inc i))) (set! i (inc i)))
(let ((val (aser arg env))) (let ((val (aser arg env)))
(when (not (nil? val)) (when (not (nil? val))
(if (spread? val)
;; Spread child — merge attrs as keyword args into parent element
(for-each
(fn (k)
(let ((v (dict-get (spread-attrs val) k)))
(append! parts (str ":" k))
(append! parts (serialize v))))
(keys (spread-attrs val)))
(if (= (type-of val) "list") (if (= (type-of val) "list")
(for-each (for-each
(fn (item) (fn (item)
(when (not (nil? item)) (when (not (nil? item))
(append! parts (serialize item)))) (append! child-parts (serialize item))))
val) val)
(append! parts (serialize val))))) (append! child-parts (serialize val))))
(set! i (inc i)))))) (set! i (inc i))))))
args) args)
(str "(" (join " " parts) ")")))) ;; Collect emitted spread attrs — goes after explicit attrs, before children
(for-each
(fn (spread-dict)
(for-each
(fn (k)
(let ((v (dict-get spread-dict k)))
(append! attr-parts (str ":" k))
(append! attr-parts (serialize v))))
(keys spread-dict)))
(emitted "element-attrs"))
(scope-pop! "element-attrs")
(let ((parts (concat (list name) attr-parts child-parts)))
(str "(" (join " " parts) ")")))))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -183,7 +195,7 @@
"defhandler" "defpage" "defquery" "defaction" "defrelation" "defhandler" "defpage" "defquery" "defaction" "defrelation"
"begin" "do" "quote" "quasiquote" "begin" "do" "quote" "quasiquote"
"->" "set!" "letrec" "dynamic-wind" "defisland" "->" "set!" "letrec" "dynamic-wind" "defisland"
"deftype" "defeffect" "provide")) "deftype" "defeffect" "scope" "provide"))
(define HO_FORM_NAMES (define HO_FORM_NAMES
(list "map" "map-indexed" "filter" "reduce" (list "map" "map-indexed" "filter" "reduce"
@@ -321,15 +333,35 @@
(= name "deftype") (= name "defeffect")) (= name "deftype") (= name "defeffect"))
(do (trampoline (eval-expr expr env)) nil) (do (trampoline (eval-expr expr env)) nil)
;; provide — render-time dynamic scope ;; scope — unified render-time dynamic scope
(= name "scope")
(let ((scope-name (trampoline (eval-expr (first args) env)))
(rest-args (rest args))
(scope-val nil)
(body-args nil))
;; Check for :value keyword
(if (and (>= (len rest-args) 2)
(= (type-of (first rest-args)) "keyword")
(= (keyword-name (first rest-args)) "value"))
(do (set! scope-val (trampoline (eval-expr (nth rest-args 1) env)))
(set! body-args (slice rest-args 2)))
(set! body-args rest-args))
(scope-push! scope-name scope-val)
(let ((result nil))
(for-each (fn (body) (set! result (aser body env)))
body-args)
(scope-pop! scope-name)
result))
;; provide — sugar for scope with value
(= name "provide") (= name "provide")
(let ((prov-name (trampoline (eval-expr (first args) env))) (let ((prov-name (trampoline (eval-expr (first args) env)))
(prov-val (trampoline (eval-expr (nth args 1) env))) (prov-val (trampoline (eval-expr (nth args 1) env)))
(result nil)) (result nil))
(provide-push! prov-name prov-val) (scope-push! prov-name prov-val)
(for-each (fn (body) (set! result (aser body env))) (for-each (fn (body) (set! result (aser body env)))
(slice args 2)) (slice args 2))
(provide-pop! prov-name) (scope-pop! prov-name)
result) result)
;; Everything else — evaluate normally ;; Everything else — evaluate normally

View File

@@ -293,6 +293,8 @@ class PyEmitter:
"collect!": "sx_collect", "collect!": "sx_collect",
"collected": "sx_collected", "collected": "sx_collected",
"clear-collected!": "sx_clear_collected", "clear-collected!": "sx_clear_collected",
"scope-push!": "scope_push",
"scope-pop!": "scope_pop",
"provide-push!": "provide_push", "provide-push!": "provide_push",
"provide-pop!": "provide_pop", "provide-pop!": "provide_pop",
"context": "sx_context", "context": "sx_context",

View File

@@ -374,30 +374,44 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
;; Tier 5: Dynamic scope — render-time provide/context/emit! ;; Tier 5: Scoped effects — unified render-time dynamic scope
;; ;;
;; `provide` is a special form (not a primitive) that creates a named scope ;; `scope` is the general primitive. `provide` is sugar for scope-with-value.
;; with a value and an empty accumulator. `context` reads the value from the ;; Both `provide` and `scope` are special forms in the evaluator.
;; nearest enclosing provider. `emit!` appends to the accumulator, `emitted`
;; reads the accumulated values.
;; ;;
;; The platform must implement per-name stacks. Each entry has a value and ;; The platform must implement per-name stacks. Each entry has a value,
;; an emitted list. `provide-push!`/`provide-pop!` manage the stack. ;; an emitted list, and a dedup flag. `scope-push!`/`scope-pop!` manage
;; the stack. `provide-push!`/`provide-pop!` are aliases.
;;
;; `collect!`/`collected`/`clear-collected!` (Tier 4) are backed by scopes:
;; collect! lazily creates a root scope with dedup=true, then emits into it.
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(declare-tier :dynamic-scope :source "eval.sx") (declare-tier :scoped-effects :source "eval.sx")
(declare-spread-primitive "scope-push!"
:params (name value)
:returns "nil"
:effects [mutation]
:doc "Push a scope with name and value. General form — provide-push! is an alias.")
(declare-spread-primitive "scope-pop!"
:params (name)
:returns "nil"
:effects [mutation]
:doc "Pop the most recent scope for name. General form — provide-pop! is an alias.")
(declare-spread-primitive "provide-push!" (declare-spread-primitive "provide-push!"
:params (name value) :params (name value)
:returns "nil" :returns "nil"
:effects [mutation] :effects [mutation]
:doc "Push a provider scope with name and value (platform internal).") :doc "Alias for scope-push!. Push a scope with name and value.")
(declare-spread-primitive "provide-pop!" (declare-spread-primitive "provide-pop!"
:params (name) :params (name)
:returns "nil" :returns "nil"
:effects [mutation] :effects [mutation]
:doc "Pop the most recent provider scope for name (platform internal).") :doc "Alias for scope-pop!. Pop the most recent scope for name.")
(declare-spread-primitive "context" (declare-spread-primitive "context"
:params (name &rest default) :params (name &rest default)

View File

@@ -162,6 +162,7 @@
(= name "reset") (sf-reset args env) (= name "reset") (sf-reset args env)
(= name "shift") (sf-shift args env) (= name "shift") (sf-shift args env)
(= name "dynamic-wind") (sf-dynamic-wind args env) (= name "dynamic-wind") (sf-dynamic-wind args env)
(= name "scope") (sf-scope args env)
(= name "provide") (sf-provide args env) (= name "provide") (sf-provide args env)
;; Higher-order forms ;; Higher-order forms
@@ -951,11 +952,35 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
;; 6a2. provide — render-time dynamic scope ;; 6a2. scope — unified render-time dynamic scope primitive
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
;; ;;
;; (provide name value body...) — push a named scope with value and empty ;; (scope name body...) or (scope name :value v body...)
;; accumulator, evaluate body, pop scope. Returns last body result. ;; Push a named scope with optional value and empty accumulator,
;; evaluate body, pop scope. Returns last body result.
;;
;; `provide` is sugar: (provide name value body...) = (scope name :value value body...)
(define sf-scope
(fn ((args :as list) (env :as dict))
(let ((name (trampoline (eval-expr (first args) env)))
(rest (slice args 1))
(val nil)
(body-exprs nil))
;; Check for :value keyword
(if (and (>= (len rest) 2) (= (type-of (first rest)) "keyword") (= (keyword-name (first rest)) "value"))
(do (set! val (trampoline (eval-expr (nth rest 1) env)))
(set! body-exprs (slice rest 2)))
(set! body-exprs rest))
(scope-push! name val)
(let ((result nil))
(for-each (fn (e) (set! result (trampoline (eval-expr e env)))) body-exprs)
(scope-pop! name)
result))))
;; provide — sugar for scope with a value
;; (provide name value body...) → (scope name :value value body...)
(define sf-provide (define sf-provide
(fn ((args :as list) (env :as dict)) (fn ((args :as list) (env :as dict))
@@ -963,9 +988,9 @@
(val (trampoline (eval-expr (nth args 1) env))) (val (trampoline (eval-expr (nth args 1) env)))
(body-exprs (slice args 2)) (body-exprs (slice args 2))
(result nil)) (result nil))
(provide-push! name val) (scope-push! name val)
(for-each (fn (e) (set! result (trampoline (eval-expr e env)))) body-exprs) (for-each (fn (e) (set! result (trampoline (eval-expr e env)))) body-exprs)
(provide-pop! name) (scope-pop! name)
result))) result)))

View File

@@ -527,6 +527,8 @@
"collect!" "sxCollect" "collect!" "sxCollect"
"collected" "sxCollected" "collected" "sxCollected"
"clear-collected!" "sxClearCollected" "clear-collected!" "sxClearCollected"
"scope-push!" "scopePush"
"scope-pop!" "scopePop"
"provide-push!" "providePush" "provide-push!" "providePush"
"provide-pop!" "providePop" "provide-pop!" "providePop"
"context" "sxContext" "context" "sxContext"

View File

@@ -883,8 +883,7 @@ PREAMBLE = '''\
function SxSpread(attrs) { this.attrs = attrs || {}; } function SxSpread(attrs) { this.attrs = attrs || {}; }
SxSpread.prototype._spread = true; SxSpread.prototype._spread = true;
var _collectBuckets = {}; var _scopeStacks = {};
var _provideStacks = {};
function isSym(x) { return x != null && x._sym === true; } function isSym(x) { return x != null && x._sym === true; }
function isKw(x) { return x != null && x._kw === true; } function isKw(x) { return x != null && x._kw === true; }
@@ -1098,14 +1097,17 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
''', ''',
"stdlib.spread": ''' "stdlib.spread": '''
// stdlib.spread — spread + collect primitives // stdlib.spread — spread + collect + scope primitives
PRIMITIVES["make-spread"] = makeSpread; PRIMITIVES["make-spread"] = makeSpread;
PRIMITIVES["spread?"] = isSpread; PRIMITIVES["spread?"] = isSpread;
PRIMITIVES["spread-attrs"] = spreadAttrs; PRIMITIVES["spread-attrs"] = spreadAttrs;
PRIMITIVES["collect!"] = sxCollect; PRIMITIVES["collect!"] = sxCollect;
PRIMITIVES["collected"] = sxCollected; PRIMITIVES["collected"] = sxCollected;
PRIMITIVES["clear-collected!"] = sxClearCollected; PRIMITIVES["clear-collected!"] = sxClearCollected;
// provide/context/emit! — render-time dynamic scope // scope — unified render-time dynamic scope
PRIMITIVES["scope-push!"] = scopePush;
PRIMITIVES["scope-pop!"] = scopePop;
// provide-push!/provide-pop! — aliases for scope-push!/scope-pop!
PRIMITIVES["provide-push!"] = providePush; PRIMITIVES["provide-push!"] = providePush;
PRIMITIVES["provide-pop!"] = providePop; PRIMITIVES["provide-pop!"] = providePop;
PRIMITIVES["context"] = sxContext; PRIMITIVES["context"] = sxContext;
@@ -1174,46 +1176,54 @@ PLATFORM_JS_PRE = '''
function isSpread(x) { return x != null && x._spread === true; } function isSpread(x) { return x != null && x._spread === true; }
function spreadAttrs(s) { return s && s._spread ? s.attrs : {}; } function spreadAttrs(s) { return s && s._spread ? s.attrs : {}; }
function sxCollect(bucket, value) { function scopePush(name, value) {
if (!_collectBuckets[bucket]) _collectBuckets[bucket] = []; if (!_scopeStacks[name]) _scopeStacks[name] = [];
var items = _collectBuckets[bucket]; _scopeStacks[name].push({value: value !== undefined ? value : NIL, emitted: [], dedup: false});
if (items.indexOf(value) === -1) items.push(value);
} }
function sxCollected(bucket) { function scopePop(name) {
return _collectBuckets[bucket] ? _collectBuckets[bucket].slice() : []; if (_scopeStacks[name] && _scopeStacks[name].length) _scopeStacks[name].pop();
}
function sxClearCollected(bucket) {
if (_collectBuckets[bucket]) _collectBuckets[bucket] = [];
} }
// Aliases — provide-push!/provide-pop! map to scope-push!/scope-pop!
var providePush = scopePush;
var providePop = scopePop;
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) { function sxContext(name) {
if (_provideStacks[name] && _provideStacks[name].length) { if (_scopeStacks[name] && _scopeStacks[name].length) {
return _provideStacks[name][_provideStacks[name].length - 1].value; return _scopeStacks[name][_scopeStacks[name].length - 1].value;
} }
if (arguments.length > 1) return arguments[1]; if (arguments.length > 1) return arguments[1];
throw new Error("No provider for: " + name); throw new Error("No provider for: " + name);
} }
function sxEmit(name, value) { function sxEmit(name, value) {
if (_provideStacks[name] && _provideStacks[name].length) { if (_scopeStacks[name] && _scopeStacks[name].length) {
_provideStacks[name][_provideStacks[name].length - 1].emitted.push(value); var entry = _scopeStacks[name][_scopeStacks[name].length - 1];
} else { if (entry.dedup && entry.emitted.indexOf(value) !== -1) return NIL;
throw new Error("No provider for emit!: " + name); entry.emitted.push(value);
} }
return NIL; return NIL;
} }
function sxEmitted(name) { function sxEmitted(name) {
if (_provideStacks[name] && _provideStacks[name].length) { if (_scopeStacks[name] && _scopeStacks[name].length) {
return _provideStacks[name][_provideStacks[name].length - 1].emitted.slice(); return _scopeStacks[name][_scopeStacks[name].length - 1].emitted.slice();
} }
return []; return [];
} }
function sxCollect(bucket, value) {
if (!_scopeStacks[bucket] || !_scopeStacks[bucket].length) {
if (!_scopeStacks[bucket]) _scopeStacks[bucket] = [];
_scopeStacks[bucket].push({value: NIL, emitted: [], dedup: true});
}
var entry = _scopeStacks[bucket][_scopeStacks[bucket].length - 1];
if (entry.emitted.indexOf(value) === -1) entry.emitted.push(value);
}
function sxCollected(bucket) {
return sxEmitted(bucket);
}
function sxClearCollected(bucket) {
if (_scopeStacks[bucket] && _scopeStacks[bucket].length) {
_scopeStacks[bucket][_scopeStacks[bucket].length - 1].emitted = [];
}
}
function lambdaParams(f) { return f.params; } function lambdaParams(f) { return f.params; }
function lambdaBody(f) { return f.body; } function lambdaBody(f) { return f.body; }
@@ -3246,6 +3256,8 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has
api_lines.append(' collect: sxCollect,') api_lines.append(' collect: sxCollect,')
api_lines.append(' collected: sxCollected,') api_lines.append(' collected: sxCollected,')
api_lines.append(' clearCollected: sxClearCollected,') api_lines.append(' clearCollected: sxClearCollected,')
api_lines.append(' scopePush: scopePush,')
api_lines.append(' scopePop: scopePop,')
api_lines.append(' providePush: providePush,') api_lines.append(' providePush: providePush,')
api_lines.append(' providePop: providePop,') api_lines.append(' providePop: providePop,')
api_lines.append(' context: sxContext,') api_lines.append(' context: sxContext,')

View File

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

View File

@@ -252,6 +252,8 @@
"collect!" "sx_collect" "collect!" "sx_collect"
"collected" "sx_collected" "collected" "sx_collected"
"clear-collected!" "sx_clear_collected" "clear-collected!" "sx_clear_collected"
"scope-push!" "scope_push"
"scope-pop!" "scope_pop"
"provide-push!" "provide_push" "provide-push!" "provide_push"
"provide-pop!" "provide_pop" "provide-pop!" "provide_pop"
"context" "sx_context" "context" "sx_context"

View File

@@ -269,11 +269,13 @@
;; (collected bucket) → list ;; (collected bucket) → list
;; (clear-collected! bucket) → void ;; (clear-collected! bucket) → void
;; ;;
;; Dynamic scope (provide/context/emit!): ;; Scoped effects (scope/provide/context/emit!):
;; (provide-push! name val) → void ;; (scope-push! name val) → void (general form)
;; (provide-pop! name) → void ;; (scope-pop! name) → void (general form)
;; (context name &rest def) → value from nearest provider ;; (provide-push! name val) → alias for scope-push!
;; (emit! name value) → void (append to provider accumulator) ;; (provide-pop! name) → alias for scope-pop!
;; (context name &rest def) → value from nearest scope
;; (emit! name value) → void (append to scope accumulator)
;; (emitted name) → list of emitted values ;; (emitted name) → list of emitted values
;; ;;
;; From parser.sx: ;; From parser.sx:

View File

@@ -1,5 +1,3 @@
# WARNING: special-forms.sx declares forms not in eval.sx: reset, shift
# WARNING: eval.sx dispatches forms not in special-forms.sx: form?, provide
""" """
sx_ref.py -- Generated from reference SX evaluator specification. sx_ref.py -- Generated from reference SX evaluator specification.
@@ -52,53 +50,56 @@ class _Spread:
self.attrs = dict(attrs) if attrs else {} self.attrs = dict(attrs) if attrs else {}
# Render-time accumulator buckets (per render pass) # Unified scope stacks — backing store for provide/context/emit!/collect!
_collect_buckets: dict[str, list] = {} # Each entry: {"value": v, "emitted": [], "dedup": bool}
_scope_stacks: dict[str, list[dict]] = {}
def _collect_reset(): def _collect_reset():
"""Reset all collect buckets (call at start of each render pass).""" """Reset all scope stacks (call at start of each render pass)."""
global _collect_buckets global _scope_stacks
_collect_buckets = {} _scope_stacks = {}
# Render-time dynamic scope stacks (provide/context/emit!) def scope_push(name, value=None):
_provide_stacks: dict[str, list[dict]] = {} """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): def scope_pop(name):
"""Push a provider scope with name, value, and empty emitted list.""" """Pop the most recent scope for name."""
_provide_stacks.setdefault(name, []).append({"value": value, "emitted": []}) if name in _scope_stacks and _scope_stacks[name]:
_scope_stacks[name].pop()
def provide_pop(name): # Aliases — provide-push!/provide-pop! map to scope-push!/scope-pop!
"""Pop the most recent provider scope for name.""" provide_push = scope_push
if name in _provide_stacks and _provide_stacks[name]: provide_pop = scope_pop
_provide_stacks[name].pop()
def sx_context(name, *default): def sx_context(name, *default):
"""Read value from nearest enclosing provider. Error if no provider and no default.""" """Read value from nearest enclosing scope. Error if no scope and no default."""
if name in _provide_stacks and _provide_stacks[name]: if name in _scope_stacks and _scope_stacks[name]:
return _provide_stacks[name][-1]["value"] return _scope_stacks[name][-1]["value"]
if default: if default:
return default[0] return default[0]
raise RuntimeError(f"No provider for: {name}") raise RuntimeError(f"No provider for: {name}")
def sx_emit(name, value): def sx_emit(name, value):
"""Append value to nearest enclosing provider's accumulator. Error if no provider.""" """Append value to nearest enclosing scope's accumulator. Respects dedup flag."""
if name in _provide_stacks and _provide_stacks[name]: if name in _scope_stacks and _scope_stacks[name]:
_provide_stacks[name][-1]["emitted"].append(value) entry = _scope_stacks[name][-1]
else: if entry["dedup"] and value in entry["emitted"]:
raise RuntimeError(f"No provider for emit!: {name}") return NIL
entry["emitted"].append(value)
return NIL return NIL
def sx_emitted(name): def sx_emitted(name):
"""Return list of values emitted into nearest matching provider.""" """Return list of values emitted into nearest matching scope."""
if name in _provide_stacks and _provide_stacks[name]: if name in _scope_stacks and _scope_stacks[name]:
return list(_provide_stacks[name][-1]["emitted"]) return list(_scope_stacks[name][-1]["emitted"])
return [] return []
@@ -303,23 +304,23 @@ def spread_attrs(s):
def sx_collect(bucket, value): def sx_collect(bucket, value):
"""Add value to named render-time accumulator (deduplicated).""" """Add value to named scope accumulator (deduplicated). Lazily creates root scope."""
if bucket not in _collect_buckets: if bucket not in _scope_stacks or not _scope_stacks[bucket]:
_collect_buckets[bucket] = [] _scope_stacks.setdefault(bucket, []).append({"value": None, "emitted": [], "dedup": True})
items = _collect_buckets[bucket] entry = _scope_stacks[bucket][-1]
if value not in items: if value not in entry["emitted"]:
items.append(value) entry["emitted"].append(value)
def sx_collected(bucket): def sx_collected(bucket):
"""Return all values in named render-time accumulator.""" """Return all values collected in named scope accumulator."""
return list(_collect_buckets.get(bucket, [])) return sx_emitted(bucket)
def sx_clear_collected(bucket): def sx_clear_collected(bucket):
"""Clear a named render-time accumulator bucket.""" """Clear nearest scope's accumulator for name."""
if bucket in _collect_buckets: if bucket in _scope_stacks and _scope_stacks[bucket]:
_collect_buckets[bucket] = [] _scope_stacks[bucket][-1]["emitted"] = []
def lambda_params(f): def lambda_params(f):
@@ -941,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 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["make-spread"] = make_spread
PRIMITIVES["spread?"] = is_spread PRIMITIVES["spread?"] = is_spread
PRIMITIVES["spread-attrs"] = spread_attrs PRIMITIVES["spread-attrs"] = spread_attrs
PRIMITIVES["collect!"] = sx_collect PRIMITIVES["collect!"] = sx_collect
PRIMITIVES["collected"] = sx_collected PRIMITIVES["collected"] = sx_collected
PRIMITIVES["clear-collected!"] = sx_clear_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-push!"] = provide_push
PRIMITIVES["provide-pop!"] = provide_pop PRIMITIVES["provide-pop!"] = provide_pop
PRIMITIVES["context"] = sx_context PRIMITIVES["context"] = sx_context
@@ -1398,6 +1402,8 @@ def eval_list(expr, env):
return sf_shift(args, env) return sf_shift(args, env)
elif sx_truthy((name == 'dynamic-wind')): elif sx_truthy((name == 'dynamic-wind')):
return sf_dynamic_wind(args, env) return sf_dynamic_wind(args, env)
elif sx_truthy((name == 'scope')):
return sf_scope(args, env)
elif sx_truthy((name == 'provide')): elif sx_truthy((name == 'provide')):
return sf_provide(args, env) return sf_provide(args, env)
elif sx_truthy((name == 'map')): elif sx_truthy((name == 'map')):
@@ -1891,6 +1897,25 @@ def sf_dynamic_wind(args, env):
call_thunk(after, env) call_thunk(after, env)
return result 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 # sf-provide
def sf_provide(args, env): def sf_provide(args, env):
_cells = {} _cells = {}
@@ -1898,10 +1923,10 @@ def sf_provide(args, env):
val = trampoline(eval_expr(nth(args, 1), env)) val = trampoline(eval_expr(nth(args, 1), env))
body_exprs = slice(args, 2) body_exprs = slice(args, 2)
_cells['result'] = NIL _cells['result'] = NIL
provide_push(name, val) scope_push(name, val)
for e in body_exprs: for e in body_exprs:
_cells['result'] = trampoline(eval_expr(e, env)) _cells['result'] = trampoline(eval_expr(e, env))
provide_pop(name) scope_pop(name)
return _cells['result'] return _cells['result']
# expand-macro # expand-macro
@@ -2225,7 +2250,8 @@ def render_to_html(expr, env):
elif _match == 'raw-html': elif _match == 'raw-html':
return raw_html_content(expr) return raw_html_content(expr)
elif _match == 'spread': elif _match == 'spread':
return expr sx_emit('element-attrs', spread_attrs(expr))
return ''
else: else:
return render_value_to_html(trampoline(eval_expr(expr, env)), env) return render_value_to_html(trampoline(eval_expr(expr, env)), env)
@@ -2248,12 +2274,13 @@ def render_value_to_html(val, env):
elif _match == 'raw-html': elif _match == 'raw-html':
return raw_html_content(val) return raw_html_content(val)
elif _match == 'spread': elif _match == 'spread':
return val sx_emit('element-attrs', spread_attrs(val))
return ''
else: else:
return escape_html(sx_str(val)) return escape_html(sx_str(val))
# RENDER_HTML_FORMS # 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? # render-html-form?
def is_render_html_form(name): def is_render_html_form(name):
@@ -2266,12 +2293,12 @@ def render_list_to_html(expr, env):
else: else:
head = first(expr) head = first(expr)
if sx_truthy((not sx_truthy((type_of(head) == 'symbol')))): if sx_truthy((not sx_truthy((type_of(head) == 'symbol')))):
return join('', filter(lambda x: (not sx_truthy(is_spread(x))), map(lambda x: render_value_to_html(x, env), expr))) return join('', map(lambda x: render_value_to_html(x, env), expr))
else: else:
name = symbol_name(head) name = symbol_name(head)
args = rest(expr) args = rest(expr)
if sx_truthy((name == '<>')): if sx_truthy((name == '<>')):
return join('', filter(lambda x: (not sx_truthy(is_spread(x))), map(lambda x: render_to_html(x, env), args))) return join('', map(lambda x: render_to_html(x, env), args))
elif sx_truthy((name == 'raw!')): elif sx_truthy((name == 'raw!')):
return join('', map(lambda x: sx_str(trampoline(eval_expr(x, env))), args)) return join('', map(lambda x: sx_str(trampoline(eval_expr(x, env))), args))
elif sx_truthy((name == 'lake')): elif sx_truthy((name == 'lake')):
@@ -2315,8 +2342,7 @@ def dispatch_html_form(name, expr, env):
if sx_truthy((len(expr) == 3)): if sx_truthy((len(expr) == 3)):
return render_to_html(nth(expr, 2), env) return render_to_html(nth(expr, 2), env)
else: else:
results = map(lambda i: render_to_html(nth(expr, i), env), range(2, len(expr))) return join('', map(lambda i: render_to_html(nth(expr, i), env), range(2, len(expr))))
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), results))
elif sx_truthy((name == 'cond')): elif sx_truthy((name == 'cond')):
branch = eval_cond(rest(expr), env) branch = eval_cond(rest(expr), env)
if sx_truthy(branch): if sx_truthy(branch):
@@ -2330,39 +2356,51 @@ def dispatch_html_form(name, expr, env):
if sx_truthy((len(expr) == 3)): if sx_truthy((len(expr) == 3)):
return render_to_html(nth(expr, 2), local) return render_to_html(nth(expr, 2), local)
else: else:
results = map(lambda i: render_to_html(nth(expr, i), local), range(2, len(expr))) return join('', map(lambda i: render_to_html(nth(expr, i), local), range(2, len(expr))))
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), results))
elif sx_truthy(((name == 'begin') if sx_truthy((name == 'begin')) else (name == 'do'))): elif sx_truthy(((name == 'begin') if sx_truthy((name == 'begin')) else (name == 'do'))):
if sx_truthy((len(expr) == 2)): if sx_truthy((len(expr) == 2)):
return render_to_html(nth(expr, 1), env) return render_to_html(nth(expr, 1), env)
else: else:
results = map(lambda i: render_to_html(nth(expr, i), env), range(1, len(expr))) return join('', map(lambda i: render_to_html(nth(expr, i), env), range(1, len(expr))))
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), results))
elif sx_truthy(is_definition_form(name)): elif sx_truthy(is_definition_form(name)):
trampoline(eval_expr(expr, env)) trampoline(eval_expr(expr, env))
return '' return ''
elif sx_truthy((name == 'map')): elif sx_truthy((name == 'map')):
f = trampoline(eval_expr(nth(expr, 1), env)) f = trampoline(eval_expr(nth(expr, 1), env))
coll = trampoline(eval_expr(nth(expr, 2), 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))) 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 == 'map-indexed')): elif sx_truthy((name == 'map-indexed')):
f = trampoline(eval_expr(nth(expr, 1), env)) f = trampoline(eval_expr(nth(expr, 1), env))
coll = trampoline(eval_expr(nth(expr, 2), env)) coll = trampoline(eval_expr(nth(expr, 2), env))
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), map_indexed(lambda i, item: (render_lambda_html(f, [i, item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [i, item]), env)), coll))) return join('', map_indexed(lambda i, item: (render_lambda_html(f, [i, item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [i, item]), env)), coll))
elif sx_truthy((name == 'filter')): elif sx_truthy((name == 'filter')):
return render_to_html(trampoline(eval_expr(expr, env)), env) return render_to_html(trampoline(eval_expr(expr, env)), env)
elif sx_truthy((name == 'for-each')): elif sx_truthy((name == 'for-each')):
f = trampoline(eval_expr(nth(expr, 1), env)) f = trampoline(eval_expr(nth(expr, 1), env))
coll = trampoline(eval_expr(nth(expr, 2), 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))) 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')): elif sx_truthy((name == 'provide')):
prov_name = trampoline(eval_expr(nth(expr, 1), env)) prov_name = trampoline(eval_expr(nth(expr, 1), env))
prov_val = trampoline(eval_expr(nth(expr, 2), env)) prov_val = trampoline(eval_expr(nth(expr, 2), env))
body_start = 3 body_start = 3
body_count = (len(expr) - 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('', 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)))))) 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 return result
else: else:
return render_value_to_html(trampoline(eval_expr(expr, env)), env) return render_value_to_html(trampoline(eval_expr(expr, env)), env)
@@ -2382,12 +2420,7 @@ def render_html_component(comp, args, env):
for p in component_params(comp): for p in component_params(comp):
local[p] = (dict_get(kwargs, p) if sx_truthy(dict_has(kwargs, p)) else NIL) local[p] = (dict_get(kwargs, p) if sx_truthy(dict_has(kwargs, p)) else NIL)
if sx_truthy(component_has_children(comp)): if sx_truthy(component_has_children(comp)):
parts = [] local['children'] = make_raw_html(join('', map(lambda c: render_to_html(c, env), children)))
for c in children:
r = render_to_html(c, env)
if sx_truthy((not sx_truthy(is_spread(r)))):
parts.append(r)
local['children'] = make_raw_html(join('', parts))
return render_to_html(component_body(comp), local) return render_to_html(component_body(comp), local)
# render-html-element # render-html-element
@@ -2399,14 +2432,12 @@ def render_html_element(tag, args, env):
if sx_truthy(is_void): if sx_truthy(is_void):
return sx_str('<', tag, render_attrs(attrs), ' />') return sx_str('<', tag, render_attrs(attrs), ' />')
else: else:
content_parts = [] scope_push('element-attrs', NIL)
for c in children: content = join('', map(lambda c: render_to_html(c, env), children))
result = render_to_html(c, env) for spread_dict in sx_emitted('element-attrs'):
if sx_truthy(is_spread(result)): merge_spread_attrs(attrs, spread_dict)
merge_spread_attrs(attrs, spread_attrs(result)) scope_pop('element-attrs')
else: return sx_str('<', tag, render_attrs(attrs), '>', content, '</', tag, '>')
content_parts.append(result)
return sx_str('<', tag, render_attrs(attrs), '>', join('', content_parts), '</', tag, '>')
# render-html-lake # render-html-lake
def render_html_lake(args, env): def render_html_lake(args, env):
@@ -2416,14 +2447,12 @@ def render_html_lake(args, env):
children = [] 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) 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 '')} lake_attrs = {'data-sx-lake': (_cells['lake_id'] if sx_truthy(_cells['lake_id']) else '')}
content_parts = [] scope_push('element-attrs', NIL)
for c in children: content = join('', map(lambda c: render_to_html(c, env), children))
result = render_to_html(c, env) for spread_dict in sx_emitted('element-attrs'):
if sx_truthy(is_spread(result)): merge_spread_attrs(lake_attrs, spread_dict)
merge_spread_attrs(lake_attrs, spread_attrs(result)) scope_pop('element-attrs')
else: return sx_str('<', _cells['lake_tag'], render_attrs(lake_attrs), '>', content, '</', _cells['lake_tag'], '>')
content_parts.append(result)
return sx_str('<', _cells['lake_tag'], render_attrs(lake_attrs), '>', join('', content_parts), '</', _cells['lake_tag'], '>')
# render-html-marsh # render-html-marsh
def render_html_marsh(args, env): def render_html_marsh(args, env):
@@ -2433,14 +2462,12 @@ def render_html_marsh(args, env):
children = [] 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) 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 '')} marsh_attrs = {'data-sx-marsh': (_cells['marsh_id'] if sx_truthy(_cells['marsh_id']) else '')}
content_parts = [] scope_push('element-attrs', NIL)
for c in children: content = join('', map(lambda c: render_to_html(c, env), children))
result = render_to_html(c, env) for spread_dict in sx_emitted('element-attrs'):
if sx_truthy(is_spread(result)): merge_spread_attrs(marsh_attrs, spread_dict)
merge_spread_attrs(marsh_attrs, spread_attrs(result)) scope_pop('element-attrs')
else: return sx_str('<', _cells['marsh_tag'], render_attrs(marsh_attrs), '>', content, '</', _cells['marsh_tag'], '>')
content_parts.append(result)
return sx_str('<', _cells['marsh_tag'], render_attrs(marsh_attrs), '>', join('', content_parts), '</', _cells['marsh_tag'], '>')
# render-html-island # render-html-island
def render_html_island(island, args, env): def render_html_island(island, args, env):
@@ -2452,12 +2479,7 @@ def render_html_island(island, args, env):
for p in component_params(island): for p in component_params(island):
local[p] = (dict_get(kwargs, p) if sx_truthy(dict_has(kwargs, p)) else NIL) local[p] = (dict_get(kwargs, p) if sx_truthy(dict_has(kwargs, p)) else NIL)
if sx_truthy(component_has_children(island)): if sx_truthy(component_has_children(island)):
parts = [] local['children'] = make_raw_html(join('', map(lambda c: render_to_html(c, env), children)))
for c in children:
r = render_to_html(c, env)
if sx_truthy((not sx_truthy(is_spread(r)))):
parts.append(r)
local['children'] = make_raw_html(join('', parts))
body_html = render_to_html(component_body(island), local) body_html = render_to_html(component_body(island), local)
state_sx = serialize_island_state(kwargs) state_sx = serialize_island_state(kwargs)
return sx_str('<span data-sx-island="', escape_attr(island_name), '"', (sx_str(' data-sx-state="', escape_attr(state_sx), '"') if sx_truthy(state_sx) else ''), '>', body_html, '</span>') return sx_str('<span data-sx-island="', escape_attr(island_name), '"', (sx_str(' data-sx-state="', escape_attr(state_sx), '"') if sx_truthy(state_sx) else ''), '>', body_html, '</span>')
@@ -2483,40 +2505,12 @@ def render_to_sx(expr, env):
# aser # aser
def aser(expr, env): def aser(expr, env):
set_render_active_b(True) set_render_active_b(True)
_match = type_of(expr) result = _sx_case(type_of(expr), [('number', lambda: expr), ('string', lambda: expr), ('boolean', lambda: expr), ('nil', lambda: NIL), ('symbol', lambda: (lambda name: (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))))(symbol_name(expr))), ('keyword', lambda: keyword_name(expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else aser_list(expr, env))), ('spread', lambda: _sx_begin(sx_emit('element-attrs', spread_attrs(expr)), NIL)), (None, lambda: expr)])
if _match == 'number': if sx_truthy(is_spread(result)):
return expr sx_emit('element-attrs', spread_attrs(result))
elif _match == 'string':
return expr
elif _match == 'boolean':
return expr
elif _match == 'nil':
return NIL
elif _match == 'symbol':
name = symbol_name(expr)
if sx_truthy(env_has(env, name)):
return env_get(env, name)
elif sx_truthy(is_primitive(name)):
return get_primitive(name)
elif sx_truthy((name == 'true')):
return True
elif sx_truthy((name == 'false')):
return False
elif sx_truthy((name == 'nil')):
return NIL return NIL
else: else:
return error(sx_str('Undefined symbol: ', name)) return result
elif _match == 'keyword':
return keyword_name(expr)
elif _match == 'list':
if sx_truthy(empty_p(expr)):
return []
else:
return aser_list(expr, env)
elif _match == 'spread':
return expr
else:
return expr
# aser-list # aser-list
def aser_list(expr, env): def aser_list(expr, env):
@@ -2561,10 +2555,10 @@ def aser_fragment(children, env):
result = aser(c, env) result = aser(c, env)
if sx_truthy((type_of(result) == 'list')): if sx_truthy((type_of(result) == 'list')):
for item in result: for item in result:
if sx_truthy(((not sx_truthy(is_nil(item))) if not sx_truthy((not sx_truthy(is_nil(item)))) else (not sx_truthy(is_spread(item))))): if sx_truthy((not sx_truthy(is_nil(item)))):
parts.append(serialize(item)) parts.append(serialize(item))
else: else:
if sx_truthy(((not sx_truthy(is_nil(result))) if not sx_truthy((not sx_truthy(is_nil(result)))) else (not sx_truthy(is_spread(result))))): if sx_truthy((not sx_truthy(is_nil(result)))):
parts.append(serialize(result)) parts.append(serialize(result))
if sx_truthy(empty_p(parts)): if sx_truthy(empty_p(parts)):
return '' return ''
@@ -2574,9 +2568,11 @@ def aser_fragment(children, env):
# aser-call # aser-call
def aser_call(name, args, env): def aser_call(name, args, env):
_cells = {} _cells = {}
parts = [name] attr_parts = []
child_parts = []
_cells['skip'] = False _cells['skip'] = False
_cells['i'] = 0 _cells['i'] = 0
scope_push('element-attrs', NIL)
for arg in args: for arg in args:
if sx_truthy(_cells['skip']): if sx_truthy(_cells['skip']):
_cells['skip'] = False _cells['skip'] = False
@@ -2585,30 +2581,31 @@ def aser_call(name, args, env):
if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((_cells['i'] + 1) < len(args)))): if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((_cells['i'] + 1) < len(args)))):
val = aser(nth(args, (_cells['i'] + 1)), env) val = aser(nth(args, (_cells['i'] + 1)), env)
if sx_truthy((not sx_truthy(is_nil(val)))): if sx_truthy((not sx_truthy(is_nil(val)))):
parts.append(sx_str(':', keyword_name(arg))) attr_parts.append(sx_str(':', keyword_name(arg)))
parts.append(serialize(val)) attr_parts.append(serialize(val))
_cells['skip'] = True _cells['skip'] = True
_cells['i'] = (_cells['i'] + 1) _cells['i'] = (_cells['i'] + 1)
else: else:
val = aser(arg, env) val = aser(arg, env)
if sx_truthy((not sx_truthy(is_nil(val)))): if sx_truthy((not sx_truthy(is_nil(val)))):
if sx_truthy(is_spread(val)):
for k in keys(spread_attrs(val)):
v = dict_get(spread_attrs(val), k)
parts.append(sx_str(':', k))
parts.append(serialize(v))
else:
if sx_truthy((type_of(val) == 'list')): if sx_truthy((type_of(val) == 'list')):
for item in val: for item in val:
if sx_truthy((not sx_truthy(is_nil(item)))): if sx_truthy((not sx_truthy(is_nil(item)))):
parts.append(serialize(item)) child_parts.append(serialize(item))
else: else:
parts.append(serialize(val)) child_parts.append(serialize(val))
_cells['i'] = (_cells['i'] + 1) _cells['i'] = (_cells['i'] + 1)
for spread_dict in sx_emitted('element-attrs'):
for k in keys(spread_dict):
v = dict_get(spread_dict, k)
attr_parts.append(sx_str(':', k))
attr_parts.append(serialize(v))
scope_pop('element-attrs')
parts = concat([name], attr_parts, child_parts)
return sx_str('(', join(' ', parts), ')') return sx_str('(', join(' ', parts), ')')
# SPECIAL_FORM_NAMES # 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
HO_FORM_NAMES = ['map', 'map-indexed', 'filter', 'reduce', 'some', 'every?', 'for-each'] HO_FORM_NAMES = ['map', 'map-indexed', 'filter', 'reduce', 'some', 'every?', 'for-each']
@@ -2705,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')))))))))))): 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)) trampoline(eval_expr(expr, env))
return NIL 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')): elif sx_truthy((name == 'provide')):
prov_name = trampoline(eval_expr(first(args), env)) prov_name = trampoline(eval_expr(first(args), env))
prov_val = trampoline(eval_expr(nth(args, 1), env)) prov_val = trampoline(eval_expr(nth(args, 1), env))
_cells['result'] = NIL _cells['result'] = NIL
provide_push(prov_name, prov_val) scope_push(prov_name, prov_val)
for body in slice(args, 2): for body in slice(args, 2):
_cells['result'] = aser(body, env) _cells['result'] = aser(body, env)
provide_pop(prov_name) scope_pop(prov_name)
return _cells['result'] return _cells['result']
else: else:
return trampoline(eval_expr(expr, env)) return trampoline(eval_expr(expr, env))
@@ -3659,7 +3672,8 @@ async def async_render(expr, env, ctx):
elif _match == 'raw-html': elif _match == 'raw-html':
return raw_html_content(expr) return raw_html_content(expr)
elif _match == 'spread': elif _match == 'spread':
return expr sx_emit('element-attrs', spread_attrs(expr))
return ''
elif _match == 'symbol': elif _match == 'symbol':
val = (await async_eval(expr, env, ctx)) val = (await async_eval(expr, env, ctx))
return (await async_render(val, env, ctx)) return (await async_render(val, env, ctx))
@@ -3691,7 +3705,7 @@ async def async_render_list(expr, env, ctx):
elif sx_truthy((name == 'raw!')): elif sx_truthy((name == 'raw!')):
return (await async_render_raw(args, env, ctx)) return (await async_render_raw(args, env, ctx))
elif sx_truthy((name == '<>')): elif sx_truthy((name == '<>')):
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), (await async_map_render(args, env, ctx)))) return join('', (await async_map_render(args, env, ctx)))
elif sx_truthy(starts_with_p(name, 'html:')): elif sx_truthy(starts_with_p(name, 'html:')):
return (await async_render_element(slice(name, 5), args, env, ctx)) return (await async_render_element(slice(name, 5), args, env, ctx))
elif sx_truthy(async_render_form_p(name)): elif sx_truthy(async_render_form_p(name)):
@@ -3746,12 +3760,12 @@ async def async_render_element(tag, args, env, ctx):
else: else:
token = (svg_context_set(True) if sx_truthy(((tag == 'svg') if sx_truthy((tag == 'svg')) else (tag == 'math'))) else NIL) token = (svg_context_set(True) if sx_truthy(((tag == 'svg') if sx_truthy((tag == 'svg')) else (tag == 'math'))) else NIL)
content_parts = [] content_parts = []
scope_push('element-attrs', NIL)
for c in children: for c in children:
result = (await async_render(c, env, ctx)) content_parts.append((await async_render(c, env, ctx)))
if sx_truthy(is_spread(result)): for spread_dict in sx_emitted('element-attrs'):
merge_spread_attrs(attrs, spread_attrs(result)) merge_spread_attrs(attrs, spread_dict)
else: scope_pop('element-attrs')
content_parts.append(result)
if sx_truthy(token): if sx_truthy(token):
svg_context_reset(token) svg_context_reset(token)
return sx_str('<', tag, render_attrs(attrs), '>', join('', content_parts), '</', tag, '>') return sx_str('<', tag, render_attrs(attrs), '>', join('', content_parts), '</', tag, '>')
@@ -3787,9 +3801,7 @@ async def async_render_component(comp, args, env, ctx):
if sx_truthy(component_has_children(comp)): if sx_truthy(component_has_children(comp)):
parts = [] parts = []
for c in children: for c in children:
r = (await async_render(c, env, ctx)) parts.append((await async_render(c, env, ctx)))
if sx_truthy((not sx_truthy(is_spread(r)))):
parts.append(r)
local['children'] = make_raw_html(join('', parts)) local['children'] = make_raw_html(join('', parts))
return (await async_render(component_body(comp), local, ctx)) return (await async_render(component_body(comp), local, ctx))
@@ -3805,9 +3817,7 @@ async def async_render_island(island, args, env, ctx):
if sx_truthy(component_has_children(island)): if sx_truthy(component_has_children(island)):
parts = [] parts = []
for c in children: for c in children:
r = (await async_render(c, env, ctx)) parts.append((await async_render(c, env, ctx)))
if sx_truthy((not sx_truthy(is_spread(r)))):
parts.append(r)
local['children'] = make_raw_html(join('', parts)) local['children'] = make_raw_html(join('', parts))
body_html = (await async_render(component_body(island), local, ctx)) body_html = (await async_render(component_body(island), local, ctx))
state_json = serialize_island_state(kwargs) state_json = serialize_island_state(kwargs)
@@ -3847,7 +3857,7 @@ async def async_map_render(exprs, env, ctx):
return results return results
# ASYNC_RENDER_FORMS # 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? # async-render-form?
def async_render_form_p(name): def async_render_form_p(name):
@@ -3871,8 +3881,7 @@ async def dispatch_async_render_form(name, expr, env, ctx):
if sx_truthy((len(expr) == 3)): if sx_truthy((len(expr) == 3)):
return (await async_render(nth(expr, 2), env, ctx)) return (await async_render(nth(expr, 2), env, ctx))
else: else:
results = (await async_map_render(slice(expr, 2), env, ctx)) return join('', (await async_map_render(slice(expr, 2), env, ctx)))
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), results))
elif sx_truthy((name == 'cond')): elif sx_truthy((name == 'cond')):
clauses = rest(expr) clauses = rest(expr)
if sx_truthy(cond_scheme_p(clauses)): if sx_truthy(cond_scheme_p(clauses)):
@@ -3886,39 +3895,51 @@ async def dispatch_async_render_form(name, expr, env, ctx):
if sx_truthy((len(expr) == 3)): if sx_truthy((len(expr) == 3)):
return (await async_render(nth(expr, 2), local, ctx)) return (await async_render(nth(expr, 2), local, ctx))
else: else:
results = (await async_map_render(slice(expr, 2), local, ctx)) return join('', (await async_map_render(slice(expr, 2), local, ctx)))
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), results))
elif sx_truthy(((name == 'begin') if sx_truthy((name == 'begin')) else (name == 'do'))): elif sx_truthy(((name == 'begin') if sx_truthy((name == 'begin')) else (name == 'do'))):
if sx_truthy((len(expr) == 2)): if sx_truthy((len(expr) == 2)):
return (await async_render(nth(expr, 1), env, ctx)) return (await async_render(nth(expr, 1), env, ctx))
else: else:
results = (await async_map_render(rest(expr), env, ctx)) return join('', (await async_map_render(rest(expr), env, ctx)))
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), results))
elif sx_truthy(is_definition_form(name)): elif sx_truthy(is_definition_form(name)):
(await async_eval(expr, env, ctx)) (await async_eval(expr, env, ctx))
return '' return ''
elif sx_truthy((name == 'map')): elif sx_truthy((name == 'map')):
f = (await async_eval(nth(expr, 1), env, ctx)) f = (await async_eval(nth(expr, 1), env, ctx))
coll = (await async_eval(nth(expr, 2), 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)))) return join('', (await async_map_fn_render(f, coll, env, ctx)))
elif sx_truthy((name == 'map-indexed')): elif sx_truthy((name == 'map-indexed')):
f = (await async_eval(nth(expr, 1), env, ctx)) f = (await async_eval(nth(expr, 1), env, ctx))
coll = (await async_eval(nth(expr, 2), 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_indexed_fn_render(f, coll, env, ctx)))) return join('', (await async_map_indexed_fn_render(f, coll, env, ctx)))
elif sx_truthy((name == 'filter')): elif sx_truthy((name == 'filter')):
return (await async_render((await async_eval(expr, env, ctx)), env, ctx)) return (await async_render((await async_eval(expr, env, ctx)), env, ctx))
elif sx_truthy((name == 'for-each')): elif sx_truthy((name == 'for-each')):
f = (await async_eval(nth(expr, 1), env, ctx)) f = (await async_eval(nth(expr, 1), env, ctx))
coll = (await async_eval(nth(expr, 2), 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)))) 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')): elif sx_truthy((name == 'provide')):
prov_name = (await async_eval(nth(expr, 1), env, ctx)) prov_name = (await async_eval(nth(expr, 1), env, ctx))
prov_val = (await async_eval(nth(expr, 2), env, ctx)) prov_val = (await async_eval(nth(expr, 2), env, ctx))
body_start = 3 body_start = 3
body_count = (len(expr) - 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 (lambda results: join('', filter(lambda r: (not sx_truthy(is_spread(r))), results)))((await async_map_render(slice(expr, body_start), env, ctx)))) 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 return result
else: else:
return (await async_render((await async_eval(expr, env, ctx)), env, ctx)) return (await async_render((await async_eval(expr, env, ctx)), env, ctx))
@@ -4019,42 +4040,35 @@ async def async_invoke(f, *args):
# async-aser # async-aser
async def async_aser(expr, env, ctx): async def async_aser(expr, env, ctx):
_match = type_of(expr) t = type_of(expr)
if _match == 'number': result = NIL
return expr if sx_truthy((t == 'number')):
elif _match == 'string': result = expr
return expr elif sx_truthy((t == 'string')):
elif _match == 'boolean': result = expr
return expr elif sx_truthy((t == 'boolean')):
elif _match == 'nil': result = expr
return NIL elif sx_truthy((t == 'nil')):
elif _match == 'symbol': result = NIL
elif sx_truthy((t == 'symbol')):
name = symbol_name(expr) name = symbol_name(expr)
if sx_truthy(env_has(env, name)): result = (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name)))))))
return env_get(env, name) elif sx_truthy((t == 'keyword')):
elif sx_truthy(is_primitive(name)): result = keyword_name(expr)
return get_primitive(name) elif sx_truthy((t == 'dict')):
elif sx_truthy((name == 'true')): result = (await async_aser_dict(expr, env, ctx))
return True elif sx_truthy((t == 'spread')):
elif sx_truthy((name == 'false')): sx_emit('element-attrs', spread_attrs(expr))
return False result = NIL
elif sx_truthy((name == 'nil')): elif sx_truthy((t == 'list')):
result = ([] if sx_truthy(empty_p(expr)) else (await async_aser_list(expr, env, ctx)))
else:
result = expr
if sx_truthy(is_spread(result)):
sx_emit('element-attrs', spread_attrs(result))
return NIL return NIL
else: else:
return error(sx_str('Undefined symbol: ', name)) return result
elif _match == 'keyword':
return keyword_name(expr)
elif _match == 'dict':
return (await async_aser_dict(expr, env, ctx))
elif _match == 'spread':
return expr
elif _match == 'list':
if sx_truthy(empty_p(expr)):
return []
else:
return (await async_aser_list(expr, env, ctx))
else:
return expr
# async-aser-dict # async-aser-dict
async def async_aser_dict(expr, env, ctx): async def async_aser_dict(expr, env, ctx):
@@ -4148,10 +4162,10 @@ async def async_aser_fragment(children, env, ctx):
result = (await async_aser(c, env, ctx)) result = (await async_aser(c, env, ctx))
if sx_truthy((type_of(result) == 'list')): if sx_truthy((type_of(result) == 'list')):
for item in result: for item in result:
if sx_truthy(((not sx_truthy(is_nil(item))) if not sx_truthy((not sx_truthy(is_nil(item)))) else (not sx_truthy(is_spread(item))))): if sx_truthy((not sx_truthy(is_nil(item)))):
parts.append(serialize(item)) parts.append(serialize(item))
else: else:
if sx_truthy(((not sx_truthy(is_nil(result))) if not sx_truthy((not sx_truthy(is_nil(result)))) else (not sx_truthy(is_spread(result))))): if sx_truthy((not sx_truthy(is_nil(result)))):
parts.append(serialize(result)) parts.append(serialize(result))
if sx_truthy(empty_p(parts)): if sx_truthy(empty_p(parts)):
return make_sx_expr('') return make_sx_expr('')
@@ -4204,9 +4218,11 @@ async def async_parse_aser_kw_args(args, kwargs, children, env, ctx):
async def async_aser_call(name, args, env, ctx): async def async_aser_call(name, args, env, ctx):
_cells = {} _cells = {}
token = (svg_context_set(True) if sx_truthy(((name == 'svg') if sx_truthy((name == 'svg')) else (name == 'math'))) else NIL) token = (svg_context_set(True) if sx_truthy(((name == 'svg') if sx_truthy((name == 'svg')) else (name == 'math'))) else NIL)
parts = [name] attr_parts = []
child_parts = []
_cells['skip'] = False _cells['skip'] = False
_cells['i'] = 0 _cells['i'] = 0
scope_push('element-attrs', NIL)
for arg in args: for arg in args:
if sx_truthy(_cells['skip']): if sx_truthy(_cells['skip']):
_cells['skip'] = False _cells['skip'] = False
@@ -4215,43 +4231,44 @@ async def async_aser_call(name, args, env, ctx):
if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((_cells['i'] + 1) < len(args)))): if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((_cells['i'] + 1) < len(args)))):
val = (await async_aser(nth(args, (_cells['i'] + 1)), env, ctx)) val = (await async_aser(nth(args, (_cells['i'] + 1)), env, ctx))
if sx_truthy((not sx_truthy(is_nil(val)))): if sx_truthy((not sx_truthy(is_nil(val)))):
parts.append(sx_str(':', keyword_name(arg))) attr_parts.append(sx_str(':', keyword_name(arg)))
if sx_truthy((type_of(val) == 'list')): if sx_truthy((type_of(val) == 'list')):
live = filter(lambda v: (not sx_truthy(is_nil(v))), val) live = filter(lambda v: (not sx_truthy(is_nil(v))), val)
if sx_truthy(empty_p(live)): if sx_truthy(empty_p(live)):
parts.append('nil') attr_parts.append('nil')
else: else:
items = map(serialize, live) items = map(serialize, live)
if sx_truthy(some(lambda v: is_sx_expr(v), live)): if sx_truthy(some(lambda v: is_sx_expr(v), live)):
parts.append(sx_str('(<> ', join(' ', items), ')')) attr_parts.append(sx_str('(<> ', join(' ', items), ')'))
else: else:
parts.append(sx_str('(list ', join(' ', items), ')')) attr_parts.append(sx_str('(list ', join(' ', items), ')'))
else: else:
parts.append(serialize(val)) attr_parts.append(serialize(val))
_cells['skip'] = True _cells['skip'] = True
_cells['i'] = (_cells['i'] + 1) _cells['i'] = (_cells['i'] + 1)
else: else:
result = (await async_aser(arg, env, ctx)) result = (await async_aser(arg, env, ctx))
if sx_truthy((not sx_truthy(is_nil(result)))): if sx_truthy((not sx_truthy(is_nil(result)))):
if sx_truthy(is_spread(result)):
for k in keys(spread_attrs(result)):
v = dict_get(spread_attrs(result), k)
parts.append(sx_str(':', k))
parts.append(serialize(v))
else:
if sx_truthy((type_of(result) == 'list')): if sx_truthy((type_of(result) == 'list')):
for item in result: for item in result:
if sx_truthy((not sx_truthy(is_nil(item)))): if sx_truthy((not sx_truthy(is_nil(item)))):
parts.append(serialize(item)) child_parts.append(serialize(item))
else: else:
parts.append(serialize(result)) child_parts.append(serialize(result))
_cells['i'] = (_cells['i'] + 1) _cells['i'] = (_cells['i'] + 1)
for spread_dict in sx_emitted('element-attrs'):
for k in keys(spread_dict):
v = dict_get(spread_dict, k)
attr_parts.append(sx_str(':', k))
attr_parts.append(serialize(v))
scope_pop('element-attrs')
if sx_truthy(token): if sx_truthy(token):
svg_context_reset(token) svg_context_reset(token)
parts = concat([name], attr_parts, child_parts)
return make_sx_expr(sx_str('(', join(' ', parts), ')')) return make_sx_expr(sx_str('(', join(' ', parts), ')'))
# ASYNC_ASER_FORM_NAMES # 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
ASYNC_ASER_HO_NAMES = ['map', 'map-indexed', 'filter', 'for-each'] ASYNC_ASER_HO_NAMES = ['map', 'map-indexed', 'filter', 'for-each']
@@ -4345,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'))))))))))): 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)) (await async_eval(expr, env, ctx))
return NIL 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')): elif sx_truthy((name == 'provide')):
prov_name = (await async_eval(first(args), env, ctx)) prov_name = (await async_eval(first(args), env, ctx))
prov_val = (await async_eval(nth(args, 1), env, ctx)) prov_val = (await async_eval(nth(args, 1), env, ctx))
_cells['result'] = NIL _cells['result'] = NIL
provide_push(prov_name, prov_val) scope_push(prov_name, prov_val)
for body in slice(args, 2): for body in slice(args, 2):
_cells['result'] = (await async_aser(body, env, ctx)) _cells['result'] = (await async_aser(body, env, ctx))
provide_pop(prov_name) scope_pop(prov_name)
return _cells['result'] return _cells['result']
else: else:
return (await async_eval(expr, env, ctx)) return (await async_eval(expr, env, ctx))

View File

@@ -285,6 +285,62 @@
(assert-equal "(div :class \"card\" :style \"color:red\" \"hello\")" (assert-equal "(div :class \"card\" :style \"color:red\" \"hello\")"
(render-sx "(div (make-spread {:class \"card\"}) (make-spread {:style \"color:red\"}) \"hello\")"))) (render-sx "(div (make-spread {:class \"card\"}) (make-spread {:style \"color:red\"}) \"hello\")")))
(deftest "spread in fragment is filtered" (deftest "spread in fragment is silently dropped"
(assert-equal "(<> \"hello\")" (assert-equal "(<> \"hello\")"
(render-sx "(<> (make-spread {:class \"card\"}) \"hello\")")))) (render-sx "(<> (make-spread {:class \"card\"}) \"hello\")")))
(deftest "stored spread in let binding"
(assert-equal "(div :class \"card\" \"hello\")"
(render-sx "(let ((card (make-spread {:class \"card\"})))
(div card \"hello\"))")))
(deftest "spread in nested element"
(assert-equal "(div (span :class \"inner\" \"hi\"))"
(render-sx "(div (span (make-spread {:class \"inner\"}) \"hi\"))")))
(deftest "spread in non-element context silently drops"
(assert-equal "hello"
(render-sx "(do (make-spread {:class \"card\"}) \"hello\")"))))
;; --------------------------------------------------------------------------
;; Scope tests — unified scope primitive
;; --------------------------------------------------------------------------
(defsuite "scope"
(deftest "scope with value and context"
(assert-equal "dark"
(render-sx "(scope \"sc-theme\" :value \"dark\" (context \"sc-theme\"))")))
(deftest "scope without value defaults to nil"
(assert-equal ""
(render-sx "(scope \"sc-nil\" (str (context \"sc-nil\")))")))
(deftest "scope with emit!/emitted"
(assert-equal "a,b"
(render-sx "(scope \"sc-emit\" (emit! \"sc-emit\" \"a\") (emit! \"sc-emit\" \"b\") (join \",\" (emitted \"sc-emit\")))")))
(deftest "provide is equivalent to scope with value"
(assert-equal "42"
(render-sx "(provide \"sc-prov\" 42 (str (context \"sc-prov\")))")))
(deftest "collect! works via scope (lazy root scope)"
(assert-equal "x,y"
(render-sx "(do (collect! \"sc-coll\" \"x\") (collect! \"sc-coll\" \"y\") (join \",\" (collected \"sc-coll\")))")))
(deftest "collect! deduplicates"
(assert-equal "a"
(render-sx "(do (collect! \"sc-dedup\" \"a\") (collect! \"sc-dedup\" \"a\") (join \",\" (collected \"sc-dedup\")))")))
(deftest "clear-collected! clears scope accumulator"
(assert-equal ""
(render-sx "(do (collect! \"sc-clear\" \"x\") (clear-collected! \"sc-clear\") (join \",\" (collected \"sc-clear\")))")))
(deftest "nested scope shadows outer"
(assert-equal "inner"
(render-sx "(scope \"sc-nest\" :value \"outer\" (scope \"sc-nest\" :value \"inner\" (context \"sc-nest\")))")))
(deftest "scope pops correctly after body"
(assert-equal "outer"
(render-sx "(scope \"sc-pop\" :value \"outer\" (scope \"sc-pop\" :value \"inner\" \"ignore\") (context \"sc-pop\"))"))))

View File

@@ -374,8 +374,12 @@
:children (list :children (list
{:label "Reference" :href "/sx/(geography.(hypermedia.(reference)))" :children reference-nav-items} {:label "Reference" :href "/sx/(geography.(hypermedia.(reference)))" :children reference-nav-items}
{:label "Examples" :href "/sx/(geography.(hypermedia.(example)))" :children examples-nav-items})} {:label "Examples" :href "/sx/(geography.(hypermedia.(example)))" :children examples-nav-items})}
{:label "Scopes" :href "/sx/(geography.(scopes))"
:summary "The unified primitive beneath provide, collect!, spreads, and islands. Named scope with downward value, upward accumulation, and a dedup flag."}
{:label "Provide / Emit!" :href "/sx/(geography.(provide))"
:summary "Sugar for scope-with-value. Render-time dynamic scope — the substrate beneath spreads, CSSX, and script collection."}
{:label "Spreads" :href "/sx/(geography.(spreads))" {:label "Spreads" :href "/sx/(geography.(spreads))"
:summary "Child-to-parent communication across render boundaries — spread, collect!, reactive-spread, and the path to provide/context/emit!."} :summary "Child-to-parent communication across render boundaries — spread, collect!, reactive-spread, built on scopes."}
{:label "Marshes" :href "/sx/(geography.(marshes))" {:label "Marshes" :href "/sx/(geography.(marshes))"
:summary "Where reactivity and hypermedia interpenetrate — server writes to signals, reactive transforms reshape server content, client state modifies how hypermedia is interpreted."} :summary "Where reactivity and hypermedia interpenetrate — server writes to signals, reactive transforms reshape server content, client state modifies how hypermedia is interpreted."}
{:label "Isomorphism" :href "/sx/(geography.(isomorphism))" :children isomorphism-nav-items})} {:label "Isomorphism" :href "/sx/(geography.(isomorphism))" :children isomorphism-nav-items})}

View File

@@ -60,6 +60,10 @@
"phase2" '(~reactive-islands/phase2/reactive-islands-phase2-content) "phase2" '(~reactive-islands/phase2/reactive-islands-phase2-content)
:else '(~reactive-islands/index/reactive-islands-index-content))))) :else '(~reactive-islands/index/reactive-islands-index-content)))))
(define scopes
(fn (content)
(if (nil? content) '(~geography/scopes-content) content)))
(define spreads (define spreads
(fn (content) (fn (content)
(if (nil? content) '(~geography/spreads-content) content))) (if (nil? content) '(~geography/spreads-content) content)))

View File

@@ -20,7 +20,7 @@
(p "Every layer is definable in terms of the one below. " (p "Every layer is definable in terms of the one below. "
"No layer can be decomposed without the layer beneath it.") "No layer can be decomposed without the layer beneath it.")
(~docs/code-block :code (~docs/code :code
(str "Layer 0: CEK machine (expression + environment + continuation)\n" (str "Layer 0: CEK machine (expression + environment + continuation)\n"
"Layer 1: Continuations (shift / reset \u2014 delimited capture)\n" "Layer 1: Continuations (shift / reset \u2014 delimited capture)\n"
"Layer 2: Algebraic effects (operations + handlers)\n" "Layer 2: Algebraic effects (operations + handlers)\n"
@@ -69,7 +69,7 @@
(p "SX already implements CEK. It just doesn't name it:") (p "SX already implements CEK. It just doesn't name it:")
(~docs/code-block :code (~docs/code :code
(str ";; eval-expr IS the CEK transition function\n" (str ";; eval-expr IS the CEK transition function\n"
";; C = expr, E = env, K = implicit (call stack / trampoline)\n" ";; C = expr, E = env, K = implicit (call stack / trampoline)\n"
"(define eval-expr\n" "(define eval-expr\n"
@@ -105,7 +105,7 @@
(p "Delimited continuations (Felleisen 1988, Danvy & Filinski 1990) " (p "Delimited continuations (Felleisen 1988, Danvy & Filinski 1990) "
"expose the K register as a first-class value:") "expose the K register as a first-class value:")
(~docs/code-block :code (~docs/code :code
(str ";; reset marks a point in the continuation\n" (str ";; reset marks a point in the continuation\n"
"(reset\n" "(reset\n"
" (+ 1 (shift k ;; k = \"the rest up to reset\"\n" " (+ 1 (shift k ;; k = \"the rest up to reset\"\n"
@@ -148,7 +148,7 @@
"an operation (\"perform this effect\") and a handler (\"here's what that effect means\"). " "an operation (\"perform this effect\") and a handler (\"here's what that effect means\"). "
"The handler receives the operation's argument and a continuation to resume the program.") "The handler receives the operation's argument and a continuation to resume the program.")
(~docs/code-block :code (~docs/code :code
(str ";; Pseudocode \u2014 algebraic effect style\n" (str ";; Pseudocode \u2014 algebraic effect style\n"
"(handle\n" "(handle\n"
" (fn () (+ 1 (perform :ask \"what number?\")))\n" " (fn () (+ 1 (perform :ask \"what number?\")))\n"
@@ -317,7 +317,7 @@
(p "The deepest primitive is not a single thing. " (p "The deepest primitive is not a single thing. "
"It's a point in a three-dimensional space:") "It's a point in a three-dimensional space:")
(~docs/code-block :code (~docs/code :code
(str "depth: CEK \u2192 continuations \u2192 algebraic effects \u2192 scoped effects\n" (str "depth: CEK \u2192 continuations \u2192 algebraic effects \u2192 scoped effects\n"
"topology: sequential \u2192 concurrent \u2192 distributed\n" "topology: sequential \u2192 concurrent \u2192 distributed\n"
"linearity: unrestricted \u2192 affine \u2192 linear")) "linearity: unrestricted \u2192 affine \u2192 linear"))
@@ -378,7 +378,7 @@
(p "If C, E, and K are all data structures (not host stack frames), " (p "If C, E, and K are all data structures (not host stack frames), "
"the entire computation state is serializable:") "the entire computation state is serializable:")
(~docs/code-block :code (~docs/code :code
(str ";; Freeze a computation mid-flight\n" (str ";; Freeze a computation mid-flight\n"
"(let ((state (capture-cek)))\n" "(let ((state (capture-cek)))\n"
" (send-to-worker state) ;; ship to another machine\n" " (send-to-worker state) ;; ship to another machine\n"
@@ -454,7 +454,7 @@
(p "Add optional effect annotations to function definitions:") (p "Add optional effect annotations to function definitions:")
(~docs/code-block :code (~docs/code :code
(str ";; Declare what effects a function uses\n" (str ";; Declare what effects a function uses\n"
"(define fetch-user :effects [io auth]\n" "(define fetch-user :effects [io auth]\n"
" (fn (id) ...))\n" " (fn (id) ...))\n"
@@ -477,7 +477,7 @@
(p "Refactor eval.sx to expose the CEK registers as data:") (p "Refactor eval.sx to expose the CEK registers as data:")
(~docs/code-block :code (~docs/code :code
(str ";; The CEK state is a value\n" (str ";; The CEK state is a value\n"
"(define-record CEK\n" "(define-record CEK\n"
" :control expr ;; the expression\n" " :control expr ;; the expression\n"
@@ -514,7 +514,7 @@
(p "Extend the CEK machine to support multiple concurrent computations:") (p "Extend the CEK machine to support multiple concurrent computations:")
(~docs/code-block :code (~docs/code :code
(str ";; Fork: create two CEK states from one\n" (str ";; Fork: create two CEK states from one\n"
"(define fork :effects [concurrency]\n" "(define fork :effects [concurrency]\n"
" (fn (cek)\n" " (fn (cek)\n"
@@ -539,7 +539,7 @@
(p "Add resource-safety constraints:") (p "Add resource-safety constraints:")
(~docs/code-block :code (~docs/code :code
(str ";; Linear scope: must be entered, must complete\n" (str ";; Linear scope: must be entered, must complete\n"
"(define-linear open-file :effects [io linear]\n" "(define-linear open-file :effects [io linear]\n"
" (fn (path)\n" " (fn (path)\n"

View File

@@ -340,24 +340,27 @@
(p "The path from current SX to the scope primitive follows the existing plan " (p "The path from current SX to the scope primitive follows the existing plan "
"and adds two phases:") "and adds two phases:")
(~docs/subsection :title "Phase 1: provide/context/emit! (immediate)" (~docs/subsection :title "Phase 1: provide/context/emit! "
(p "Already planned. Implement render-time dynamic scope. Four primitives: " (p (strong "Complete. ") "Render-time dynamic scope. Four primitives: "
(code "provide") " (special form), " (code "context") ", " (code "emit!") ", " (code "provide") " (special form), " (code "context") ", " (code "emit!") ", "
(code "emitted") ". Platform provides " (code "provide-push!/provide-pop!") ".") (code "emitted") ". Platform provides " (code "scope-push!/scope-pop!") ". "
(p "This is " (code "scope") " with " (code ":propagation :render") " only. " "Spreads reimplemented on provide/emit!.")
"No change to islands or lakes. Pure addition.") (p "See "
(p (strong "Delivers: ") "render-time context, scoped accumulation, " (a :href "/sx/(geography.(provide))" :class "text-violet-600 hover:underline" "provide article")
"spread and collect reimplemented as sugar over provide/emit.")) " and "
(a :href "/sx/(geography.(spreads))" :class "text-violet-600 hover:underline" "spreads article")
"."))
(~docs/subsection :title "Phase 2: scope as the common form (next)" (~docs/subsection :title "Phase 2: scope as the common form "
(p "Introduce " (code "scope") " as the general form. " (p (strong "Complete. ") (code "scope") " is now the general form. "
(code "provide") " becomes sugar for " (code "(scope ... :propagation :render)") ". " (code "provide") " is sugar for " (code "(scope name :value v body...)") ". "
(code "defisland") " becomes sugar for " (code "(scope ... :propagation :reactive)") ". " (code "collect!") " creates a lazy root scope with deduplication. "
(code "lake") " becomes sugar for " (code "(scope ... :propagation :morph)") ".") "All adapters use " (code "scope-push!/scope-pop!") " directly.")
(p "The sugar forms remain — nobody writes " (code "scope") " directly in page code. " (p "The unified platform structure:")
"But the evaluator, adapters, and bootstrappers all dispatch through one mechanism.") (~docs/code :code (highlight "_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" "python"))
(p (strong "Delivers: ") "unified internal representation, reactive context (the new cell), " (p "See "
"simplified adapter code (one scope handler instead of three separate paths).")) (a :href "/sx/(geography.(scopes))" :class "text-violet-600 hover:underline" "scopes article")
"."))
(~docs/subsection :title "Phase 3: effect handlers (future)" (~docs/subsection :title "Phase 3: effect handlers (future)"
(p "Make propagation modes extensible. A " (code ":propagation") " value is a " (p "Make propagation modes extensible. A " (code ":propagation") " value is a "
@@ -437,8 +440,10 @@
"and composable. It's the last primitive SX needs.") "and composable. It's the last primitive SX needs.")
(~docs/note (~docs/note
(p (strong "Status: ") "Phase 1 (" (code "provide/context/emit!") ") is specced and " (p (strong "Status: ") "Phase 1 (" (code "provide/context/emit!") ") and "
"ready to build. Phase 2 (" (code "scope") " unification) follows naturally once " "Phase 2 (" (code "scope") " unification) are complete. "
"provide is working. Phase 3 (extensible handlers) is the research frontier — " "574 tests pass. All four adapters use " (code "scope-push!/scope-pop!") ". "
(code "collect!") " is backed by lazy scopes with dedup. "
"Phase 3 (extensible handlers) is the research frontier — "
"it may turn out that three modes are sufficient, or it may turn out that " "it may turn out that three modes are sufficient, or it may turn out that "
"user-defined modes unlock something unexpected."))))) "user-defined modes unlock something unexpected.")))))

250
sx/sx/provide.sx Normal file
View File

@@ -0,0 +1,250 @@
;; ---------------------------------------------------------------------------
;; Provide / Context / Emit! — render-time dynamic scope
;; ---------------------------------------------------------------------------
;; ---- Demo components ----
(defcomp ~geography/demo-provide-basic ()
(div :class "space-y-2"
(provide "theme" {:primary "violet" :accent "rose"}
(div :class "rounded-lg p-3 bg-violet-50 border border-violet-200"
(p :class "text-sm text-violet-800 font-semibold" "Inside provider: theme.primary = violet")
(p :class "text-xs text-stone-500" "Child reads context value without prop threading.")))
(div :class "rounded-lg p-3 bg-stone-50 border border-stone-200"
(p :class "text-sm text-stone-600" "Outside provider: no theme context."))))
(defcomp ~geography/demo-emit-collect ()
(div :class "space-y-2"
(provide "scripts" nil
(div :class "rounded-lg p-3 bg-stone-50 border border-stone-200"
(p :class "text-sm text-stone-700"
(emit! "scripts" "analytics.js")
(emit! "scripts" "charts.js")
"Page content renders here. Scripts emitted silently."))
(div :class "rounded-lg p-3 bg-violet-50 border border-violet-200"
(p :class "text-sm text-violet-800 font-semibold" "Emitted scripts:")
(ul :class "text-xs text-stone-600 list-disc pl-5"
(map (fn (s) (li (code s))) (emitted "scripts")))))))
(defcomp ~geography/demo-spread-mechanism ()
(div :class "space-y-2"
(div (make-spread {:class "rounded-lg p-3 bg-rose-50 border border-rose-200"})
(p :class "text-sm text-rose-800 font-semibold" "Spread child styled this div")
(p :class "text-xs text-stone-500" "The spread emitted into the element-attrs provider."))
(let ((card (make-spread {:class "rounded-lg p-3 bg-amber-50 border border-amber-200"})))
(div card
(p :class "text-sm text-amber-800 font-semibold" "Stored spread, same mechanism")
(p :class "text-xs text-stone-500" "Bound to a let variable, applied when rendered as child.")))))
(defcomp ~geography/demo-nested-provide ()
(div :class "space-y-2"
(provide "level" "outer"
(div :class "rounded-lg p-3 bg-stone-50 border border-stone-200"
(p :class "text-sm text-stone-700"
(str "Level: " (context "level")))
(provide "level" "inner"
(div :class "rounded-lg p-3 bg-violet-50 border border-violet-200 ml-4"
(p :class "text-sm text-violet-700"
(str "Level: " (context "level")))))
(p :class "text-sm text-stone-500 mt-1"
(str "Back to: " (context "level")))))))
;; ---- Layout helper (reuse from spreads article) ----
(defcomp ~geography/provide-demo-example (&key demo code)
(div :class "grid grid-cols-1 lg:grid-cols-2 gap-4 my-6 items-start"
(div :class "border border-dashed border-stone-300 rounded-lg p-4 bg-stone-50 min-h-[80px]"
demo)
(div :class "not-prose bg-stone-100 rounded-lg p-4 overflow-x-auto"
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code code)))))
;; ---- Page content ----
(defcomp ~geography/provide-content ()
(~docs/page :title "Provide / Context / Emit!"
(p :class "text-stone-500 text-sm italic mb-8"
"Sugar for " (code "scope") " with a value. " (code "provide") " creates a named scope "
"with a value and an accumulator. " (code "context") " reads the value downward. "
(code "emit!") " appends to the accumulator upward. " (code "emitted") " retrieves what was emitted. "
"See "
(a :href "/sx/(geography.(scopes))" :class "text-violet-600 hover:underline" "scopes")
" for the unified primitive.")
;; =====================================================================
;; I. The four primitives
;; =====================================================================
(~docs/section :title "Four primitives" :id "primitives"
(~docs/subsection :title "provide (special form)"
(p (code "provide") " creates a named scope with a value and an empty accumulator. "
"The body expressions execute with the scope active. When the body completes, "
"the scope is popped.")
(~docs/code :code (highlight "(provide name value\n body...)\n\n;; Example: theme context\n(provide \"theme\" {:primary \"violet\"}\n (h1 \"Title\") ;; can read (context \"theme\")\n (p \"Body\")) ;; scope active for all children" "lisp"))
(p (code "provide") " is a special form, not a function — the body is evaluated "
"inside the scope, not before it."))
(~docs/subsection :title "context"
(p "Reads the value from the nearest enclosing " (code "provide") " with the given name. "
"Errors if no provider and no default given.")
(~docs/code :code (highlight "(provide \"theme\" {:primary \"violet\" :font \"serif\"}\n (get (context \"theme\") :primary)) ;; → \"violet\"\n\n;; With default (no error when missing):\n(context \"theme\" {:primary \"stone\"}) ;; → {:primary \"stone\"}" "lisp")))
(~docs/subsection :title "emit!"
(p "Appends a value to the nearest enclosing provider's accumulator. "
"Tolerant: returns nil silently when no provider exists.")
(~docs/code :code (highlight "(provide \"scripts\" nil\n (emit! \"scripts\" \"analytics.js\")\n (emit! \"scripts\" \"charts.js\")\n ;; accumulator now has both scripts\n )\n\n;; Outside any provider — silently does nothing:\n(emit! \"scripts\" \"orphan.js\") ;; → nil, no error" "lisp"))
(p "Tolerance is critical. Spreads emit into " (code "\"element-attrs\"")
" — but a spread might be evaluated in a fragment, a " (code "begin") " block, or a "
(code "map") " call where no element provider exists. "
"Tolerant " (code "emit!") " means these cases silently vanish instead of crashing."))
(~docs/subsection :title "emitted"
(p "Returns the list of values emitted into the nearest provider with the given name. "
"Empty list if no provider.")
(~docs/code :code (highlight "(provide \"scripts\" nil\n (emit! \"scripts\" \"a.js\")\n (emit! \"scripts\" \"b.js\")\n (emitted \"scripts\")) ;; → (\"a.js\" \"b.js\")" "lisp"))))
;; =====================================================================
;; II. Two directions, one mechanism
;; =====================================================================
(~docs/section :title "Two directions, one mechanism" :id "directions"
(p (code "provide") " serves both downward and upward communication through a single scope.")
(~docs/table
:headers (list "Direction" "Read with" "Write with" "Example")
:rows (list
(list "Downward (scope → child)" "context" "provide value" "Theme, config, locale")
(list "Upward (child → scope)" "emitted" "emit!" "Script collection, spread attrs")))
(~geography/provide-demo-example
:demo (~geography/demo-provide-basic)
:code (highlight ";; Downward: theme context\n(provide \"theme\"\n {:primary \"violet\" :accent \"rose\"}\n (h1 :style (str \"color:\"\n (get (context \"theme\") :primary))\n \"Themed heading\")\n (p \"inherits theme context\"))" "lisp"))
(~geography/provide-demo-example
:demo (~geography/demo-emit-collect)
:code (highlight ";; Upward: script accumulation\n(provide \"scripts\" nil\n (div\n (emit! \"scripts\" \"analytics.js\")\n (div\n (emit! \"scripts\" \"charts.js\")\n \"chart\"))\n ;; Collect at the boundary:\n (for-each (fn (s)\n (script :src s))\n (emitted \"scripts\")))" "lisp")))
;; =====================================================================
;; III. How spreads use it
;; =====================================================================
(~docs/section :title "How spreads use provide/emit!" :id "spreads"
(p "Every element rendering function wraps its children in a provider scope "
"named " (code "\"element-attrs\"") ". When the adapter encounters a spread child, "
"it emits the spread's attrs into this scope. After all children render, the "
"element collects and merges the emitted attrs.")
(~geography/provide-demo-example
:demo (~geography/demo-spread-mechanism)
:code (highlight ";; Spread = emit! into element-attrs\n(div (make-spread {:class \"card\"})\n \"hello\")\n\n;; Internally:\n;; 1. div opens provider:\n;; (provide-push! \"element-attrs\" nil)\n;; 2. spread child emits:\n;; (emit! \"element-attrs\"\n;; {:class \"card\"})\n;; 3. div collects + merges:\n;; (emitted \"element-attrs\")\n;; → ({:class \"card\"})\n;; 4. (provide-pop! \"element-attrs\")\n;; Result: <div class=\"card\">hello</div>" "lisp"))
(~docs/subsection :title "Why this matters"
(p "Before the refactor, every intermediate form in the render pipeline — "
"fragments, " (code "let") ", " (code "begin") ", " (code "map") ", "
(code "for-each") ", " (code "when") ", " (code "cond") ", component children — "
"needed an explicit " (code "(filter (fn (r) (not (spread? r))) ...)") " to strip "
"spread values from rendered output. Over 25 such filters existed across the four adapters.")
(p "With provide/emit!, all of these disappear. Spreads emit into the nearest element's "
"scope regardless of how many layers of control flow they pass through. Non-element "
"contexts have no provider, so " (code "emit!") " is a silent no-op.")))
;; =====================================================================
;; IV. Nested scoping
;; =====================================================================
(~docs/section :title "Nested scoping" :id "nesting"
(p "Providers stack. Each " (code "provide") " pushes onto a per-name stack; "
"the closest one wins. This gives lexical-style scoping at render time.")
(~geography/provide-demo-example
:demo (~geography/demo-nested-provide)
:code (highlight "(provide \"level\" \"outer\"\n (context \"level\") ;; → \"outer\"\n (provide \"level\" \"inner\"\n (context \"level\")) ;; → \"inner\"\n (context \"level\")) ;; → \"outer\" again" "lisp"))
(p "For " (code "emit!") ", this means emissions go to the " (em "nearest") " provider. "
"A spread inside a nested element emits to that element, not an ancestor.")
(~docs/code :code (highlight ";; Nested elements = nested providers\n(div ;; provider A\n (span ;; provider B\n (make-spread {:class \"inner\"})) ;; emits to B\n (make-spread {:class \"outer\"})) ;; emits to A\n;; → <div class=\"outer\"><span class=\"inner\"></span></div>" "lisp")))
;; =====================================================================
;; V. Across all adapters
;; =====================================================================
(~docs/section :title "Across all adapters" :id "adapters"
(p "The provide/emit! mechanism works identically across all four rendering adapters. "
"The element rendering pattern is the same; only the output format differs.")
(~docs/table
:headers (list "Adapter" "Element render" "Spread dispatch")
:rows (list
(list "HTML (server)" "provide-push! → render children → merge emitted → provide-pop!" "(emit! \"element-attrs\" (spread-attrs expr)) → \"\"")
(list "Async (server)" "Same pattern, with await on child rendering" "Same dispatch")
(list "SX wire (aser)" "provide-push! → serialize children → merge emitted as :key attrs → provide-pop!" "(emit! \"element-attrs\" (spread-attrs expr)) → nil")
(list "DOM (browser)" "provide-push! → reduce children → merge emitted onto DOM element → provide-pop!" "emit! + keep value for reactive-spread detection")))
(~docs/subsection :title "DOM adapter: reactive-spread preserved"
(p "In the DOM adapter, spread children inside islands are still checked individually "
"for signal dependencies. " (code "reactive-spread") " tracks signal deps and "
"surgically updates attributes when signals change. The static path uses provide/emit!; "
"the reactive path wraps it in an effect.")
(p "See the "
(a :href "/sx/(geography.(spreads))" :class "text-violet-600 hover:underline" "spreads article")
" for reactive-spread details.")))
;; =====================================================================
;; VI. Comparison with collect!
;; =====================================================================
(~docs/section :title "Comparison with collect! / collected" :id "comparison"
(~docs/table
:headers (list "" "provide / emit!" "collect! / collected")
:rows (list
(list "Scope" "Lexical (nearest enclosing provide)" "Global (render-wide)")
(list "Deduplication" "None — every emit! appends" "Automatic (same value skipped)")
(list "Multiple scopes" "Yes — nested provides shadow" "No — single global bucket per name")
(list "Downward data" "Yes (context)" "No")
(list "Used by" "Spreads (element-attrs)" "CSSX rule accumulation")))
(p (code "collect!") " remains the right tool for CSS rule accumulation — deduplication "
"matters there, and rules need to reach the layout root regardless of nesting depth. "
(code "emit!") " is right for spread attrs — no dedup needed, and each element only "
"wants attrs from its direct children."))
;; =====================================================================
;; VII. Platform implementation
;; =====================================================================
(~docs/section :title "Platform implementation" :id "platform"
(p (code "provide") " is sugar for " (code "scope") ". At the platform level, "
(code "provide-push!") " and " (code "provide-pop!") " are aliases for "
(code "scope-push!") " and " (code "scope-pop!") ". All operations work on a unified "
(code "_scope_stacks") " data structure.")
(~docs/table
:headers (list "Platform primitive" "Purpose")
:rows (list
(list "scope-push!(name, value)" "Push a new scope with value and empty accumulator")
(list "scope-pop!(name)" "Pop the most recent scope")
(list "context(name, ...default)" "Read value from nearest scope (error if missing and no default)")
(list "emit!(name, value)" "Append to nearest scope's accumulator (tolerant: no-op if missing)")
(list "emitted(name)" "Return accumulated values from nearest scope")))
(p (code "provide") " is a special form in "
(a :href "/sx/(language.(spec.(explore.evaluator)))" :class "font-mono text-violet-600 hover:underline text-sm" "eval.sx")
" — it calls " (code "scope-push!") ", evaluates the body, "
"then calls " (code "scope-pop!") ". See "
(a :href "/sx/(geography.(scopes))" :class "text-violet-600 hover:underline" "scopes")
" for the full unified platform.")
(~docs/note
(p (strong "Spec explorer: ") "See the provide/emit! primitives in "
(a :href "/sx/(language.(spec.(explore.boundary)))" :class "font-mono text-violet-600 hover:underline text-sm" "boundary.sx explorer")
". The " (code "provide") " special form is in "
(a :href "/sx/(language.(spec.(explore.evaluator)))" :class "font-mono text-violet-600 hover:underline text-sm" "eval.sx explorer")
". Element rendering with provide/emit! is visible in "
(a :href "/sx/(language.(spec.(explore.adapter-html)))" :class "font-mono text-violet-600 hover:underline text-sm" "adapter-html")
" and "
(a :href "/sx/(language.(spec.(explore.adapter-async)))" :class "font-mono text-violet-600 hover:underline text-sm" "adapter-async")
".")))))

View File

@@ -11,73 +11,73 @@
(~docs/section :title "1. Signal + Computed + Effect" :id "demo-counter" (~docs/section :title "1. Signal + Computed + Effect" :id "demo-counter"
(p "A signal holds a value. A computed derives from it. Click the buttons — the counter and doubled value update instantly, no server round-trip.") (p "A signal holds a value. A computed derives from it. Click the buttons — the counter and doubled value update instantly, no server round-trip.")
(~reactive-islands/demo/counter :initial 0) (~reactive-islands/index/demo-counter :initial 0)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0)))\n (doubled (computed (fn () (* 2 (deref count))))))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! count dec)) \"\")\n (span (deref count))\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (p \"doubled: \" (deref doubled)))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0)))\n (doubled (computed (fn () (* 2 (deref count))))))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! count dec)) \"\")\n (span (deref count))\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (p \"doubled: \" (deref doubled)))))" "lisp"))
(p (code "(deref count)") " in a text position creates a reactive text node. When " (code "count") " changes, " (em "only that text node") " updates. " (code "doubled") " recomputes automatically. No diffing.")) (p (code "(deref count)") " in a text position creates a reactive text node. When " (code "count") " changes, " (em "only that text node") " updates. " (code "doubled") " recomputes automatically. No diffing."))
(~docs/section :title "2. Temperature Converter" :id "demo-temperature" (~docs/section :title "2. Temperature Converter" :id "demo-temperature"
(p "Two derived values from one signal. Click to change Celsius — Fahrenheit updates reactively.") (p "Two derived values from one signal. Click to change Celsius — Fahrenheit updates reactively.")
(~reactive-islands/demo/temperature) (~reactive-islands/index/demo-temperature)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/temperature ()\n (let ((celsius (signal 20)))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! celsius (fn (c) (- c 5)))) \"5\")\n (span (deref celsius))\n (button :on-click (fn (e) (swap! celsius (fn (c) (+ c 5)))) \"+5\")\n (span \"°C = \")\n (span (+ (* (deref celsius) 1.8) 32))\n (span \"°F\"))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/temperature ()\n (let ((celsius (signal 20)))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! celsius (fn (c) (- c 5)))) \"5\")\n (span (deref celsius))\n (button :on-click (fn (e) (swap! celsius (fn (c) (+ c 5)))) \"+5\")\n (span \"°C = \")\n (span (+ (* (deref celsius) 1.8) 32))\n (span \"°F\"))))" "lisp"))
(p "The actual implementation uses " (code "computed") " for Fahrenheit: " (code "(computed (fn () (+ (* (deref celsius) 1.8) 32)))") ". The " (code "(deref fahrenheit)") " in the span creates a reactive text node that updates when celsius changes.")) (p "The actual implementation uses " (code "computed") " for Fahrenheit: " (code "(computed (fn () (+ (* (deref celsius) 1.8) 32)))") ". The " (code "(deref fahrenheit)") " in the span creates a reactive text node that updates when celsius changes."))
(~docs/section :title "3. Effect + Cleanup: Stopwatch" :id "demo-stopwatch" (~docs/section :title "3. Effect + Cleanup: Stopwatch" :id "demo-stopwatch"
(p "Effects can return cleanup functions. This stopwatch starts a " (code "set-interval") " — the cleanup clears it when the running signal toggles off.") (p "Effects can return cleanup functions. This stopwatch starts a " (code "set-interval") " — the cleanup clears it when the running signal toggles off.")
(~reactive-islands/demo/stopwatch) (~reactive-islands/index/demo-stopwatch)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/stopwatch ()\n (let ((running (signal false))\n (elapsed (signal 0))\n (time-text (create-text-node \"0.0s\"))\n (btn-text (create-text-node \"Start\")))\n ;; Timer: effect creates interval, cleanup clears it\n (effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! elapsed inc)) 100)))\n (fn () (clear-interval id))))))\n ;; Display: updates text node when elapsed changes\n (effect (fn ()\n (let ((e (deref elapsed)))\n (dom-set-text-content time-text\n (str (floor (/ e 10)) \".\" (mod e 10) \"s\")))))\n ;; Button label\n (effect (fn ()\n (dom-set-text-content btn-text\n (if (deref running) \"Stop\" \"Start\"))))\n (div :class \"...\"\n (span time-text)\n (button :on-click (fn (e) (swap! running not)) btn-text)\n (button :on-click (fn (e)\n (reset! running false) (reset! elapsed 0)) \"Reset\"))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/stopwatch ()\n (let ((running (signal false))\n (elapsed (signal 0))\n (time-text (create-text-node \"0.0s\"))\n (btn-text (create-text-node \"Start\")))\n ;; Timer: effect creates interval, cleanup clears it\n (effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! elapsed inc)) 100)))\n (fn () (clear-interval id))))))\n ;; Display: updates text node when elapsed changes\n (effect (fn ()\n (let ((e (deref elapsed)))\n (dom-set-text-content time-text\n (str (floor (/ e 10)) \".\" (mod e 10) \"s\")))))\n ;; Button label\n (effect (fn ()\n (dom-set-text-content btn-text\n (if (deref running) \"Stop\" \"Start\"))))\n (div :class \"...\"\n (span time-text)\n (button :on-click (fn (e) (swap! running not)) btn-text)\n (button :on-click (fn (e)\n (reset! running false) (reset! elapsed 0)) \"Reset\"))))" "lisp"))
(p "Three effects, each tracking different signals. The timer effect's cleanup fires before each re-run — toggling " (code "running") " off clears the interval. No hook rules: effects can appear anywhere, in any order.")) (p "Three effects, each tracking different signals. The timer effect's cleanup fires before each re-run — toggling " (code "running") " off clears the interval. No hook rules: effects can appear anywhere, in any order."))
(~docs/section :title "4. Imperative Pattern" :id "demo-imperative" (~docs/section :title "4. Imperative Pattern" :id "demo-imperative"
(p "For complex reactivity (dynamic classes, conditional text), use the imperative pattern: " (code "create-text-node") " + " (code "effect") " + " (code "dom-set-text-content") ".") (p "For complex reactivity (dynamic classes, conditional text), use the imperative pattern: " (code "create-text-node") " + " (code "effect") " + " (code "dom-set-text-content") ".")
(~reactive-islands/demo/imperative) (~reactive-islands/index/demo-imperative)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/imperative ()\n (let ((count (signal 0))\n (text-node (create-text-node \"0\")))\n ;; Explicit effect: re-runs when count changes\n (effect (fn ()\n (dom-set-text-content text-node (str (deref count)))))\n (div :class \"...\"\n (span text-node)\n (button :on-click (fn (e) (swap! count inc)) \"+\"))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/imperative ()\n (let ((count (signal 0))\n (text-node (create-text-node \"0\")))\n ;; Explicit effect: re-runs when count changes\n (effect (fn ()\n (dom-set-text-content text-node (str (deref count)))))\n (div :class \"...\"\n (span text-node)\n (button :on-click (fn (e) (swap! count inc)) \"+\"))))" "lisp"))
(p "Two patterns exist: " (strong "declarative") " (" (code "(span (deref sig))") " — auto-reactive via " (code "reactive-text") ") and " (strong "imperative") " (" (code "create-text-node") " + " (code "effect") " — explicit, full control). Use declarative for simple text, imperative for dynamic classes, conditional DOM, or complex updates.")) (p "Two patterns exist: " (strong "declarative") " (" (code "(span (deref sig))") " — auto-reactive via " (code "reactive-text") ") and " (strong "imperative") " (" (code "create-text-node") " + " (code "effect") " — explicit, full control). Use declarative for simple text, imperative for dynamic classes, conditional DOM, or complex updates."))
(~docs/section :title "5. Reactive List" :id "demo-reactive-list" (~docs/section :title "5. Reactive List" :id "demo-reactive-list"
(p "When " (code "map") " is used with " (code "(deref signal)") " inside an island, it auto-upgrades to a reactive list. With " (code ":key") " attributes, existing DOM nodes are reused across updates — only additions, removals, and reorderings touch the DOM.") (p "When " (code "map") " is used with " (code "(deref signal)") " inside an island, it auto-upgrades to a reactive list. With " (code ":key") " attributes, existing DOM nodes are reused across updates — only additions, removals, and reorderings touch the DOM.")
(~reactive-islands/demo/reactive-list) (~reactive-islands/index/demo-reactive-list)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/reactive-list ()\n (let ((next-id (signal 1))\n (items (signal (list)))\n (add-item (fn (e)\n (batch (fn ()\n (swap! items (fn (old)\n (append old (dict \"id\" (deref next-id)\n \"text\" (str \"Item \" (deref next-id))))))\n (swap! next-id inc)))))\n (remove-item (fn (id)\n (swap! items (fn (old)\n (filter (fn (item) (not (= (get item \"id\") id))) old))))))\n (div\n (button :on-click add-item \"Add Item\")\n (span (deref (computed (fn () (len (deref items))))) \" items\")\n (ul\n (map (fn (item)\n (li :key (str (get item \"id\"))\n (span (get item \"text\"))\n (button :on-click (fn (e) (remove-item (get item \"id\"))) \"✕\")))\n (deref items))))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/reactive-list ()\n (let ((next-id (signal 1))\n (items (signal (list)))\n (add-item (fn (e)\n (batch (fn ()\n (swap! items (fn (old)\n (append old (dict \"id\" (deref next-id)\n \"text\" (str \"Item \" (deref next-id))))))\n (swap! next-id inc)))))\n (remove-item (fn (id)\n (swap! items (fn (old)\n (filter (fn (item) (not (= (get item \"id\") id))) old))))))\n (div\n (button :on-click add-item \"Add Item\")\n (span (deref (computed (fn () (len (deref items))))) \" items\")\n (ul\n (map (fn (item)\n (li :key (str (get item \"id\"))\n (span (get item \"text\"))\n (button :on-click (fn (e) (remove-item (get item \"id\"))) \"✕\")))\n (deref items))))))" "lisp"))
(p (code ":key") " identifies each list item. When items change, the reconciler matches old and new keys — reusing existing DOM nodes, inserting new ones, and removing stale ones. Without keys, the list falls back to clear-and-rerender. " (code "batch") " groups the two signal writes into one update pass.")) (p (code ":key") " identifies each list item. When items change, the reconciler matches old and new keys — reusing existing DOM nodes, inserting new ones, and removing stale ones. Without keys, the list falls back to clear-and-rerender. " (code "batch") " groups the two signal writes into one update pass."))
(~docs/section :title "6. Input Binding" :id "demo-input-binding" (~docs/section :title "6. Input Binding" :id "demo-input-binding"
(p "The " (code ":bind") " attribute creates a two-way link between a signal and a form element. Type in the input — the signal updates. Change the signal — the input updates. Works with text inputs, checkboxes, radios, textareas, and selects.") (p "The " (code ":bind") " attribute creates a two-way link between a signal and a form element. Type in the input — the signal updates. Change the signal — the input updates. Works with text inputs, checkboxes, radios, textareas, and selects.")
(~reactive-islands/demo/input-binding) (~reactive-islands/index/demo-input-binding)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/input-binding ()\n (let ((name (signal \"\"))\n (agreed (signal false)))\n (div\n (input :type \"text\" :bind name\n :placeholder \"Type your name...\")\n (span \"Hello, \" (strong (deref name)) \"!\")\n (input :type \"checkbox\" :bind agreed)\n (when (deref agreed)\n (p \"Thanks for agreeing!\")))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/input-binding ()\n (let ((name (signal \"\"))\n (agreed (signal false)))\n (div\n (input :type \"text\" :bind name\n :placeholder \"Type your name...\")\n (span \"Hello, \" (strong (deref name)) \"!\")\n (input :type \"checkbox\" :bind agreed)\n (when (deref agreed)\n (p \"Thanks for agreeing!\")))))" "lisp"))
(p (code ":bind") " detects the element type automatically — text inputs use " (code "value") " + " (code "input") " event, checkboxes use " (code "checked") " + " (code "change") " event. The effect only updates the DOM when the value actually changed, preventing cursor jump.")) (p (code ":bind") " detects the element type automatically — text inputs use " (code "value") " + " (code "input") " event, checkboxes use " (code "checked") " + " (code "change") " event. The effect only updates the DOM when the value actually changed, preventing cursor jump."))
(~docs/section :title "7. Portals" :id "demo-portal" (~docs/section :title "7. Portals" :id "demo-portal"
(p "A " (code "portal") " renders children into a DOM node " (em "outside") " the island's subtree. Essential for modals, tooltips, and toasts — anything that must escape " (code "overflow:hidden") " or z-index stacking.") (p "A " (code "portal") " renders children into a DOM node " (em "outside") " the island's subtree. Essential for modals, tooltips, and toasts — anything that must escape " (code "overflow:hidden") " or z-index stacking.")
(~reactive-islands/demo/portal) (~reactive-islands/index/demo-portal)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/portal ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n (if (deref open?) \"Close Modal\" \"Open Modal\"))\n (portal \"#portal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 ...\"\n :on-click (fn (e) (reset! open? false))\n (div :class \"bg-white rounded-lg p-6 ...\"\n :on-click (fn (e) (stop-propagation e))\n (h2 \"Portal Modal\")\n (p \"Rendered outside the island's DOM.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/portal ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n (if (deref open?) \"Close Modal\" \"Open Modal\"))\n (portal \"#portal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 ...\"\n :on-click (fn (e) (reset! open? false))\n (div :class \"bg-white rounded-lg p-6 ...\"\n :on-click (fn (e) (stop-propagation e))\n (h2 \"Portal Modal\")\n (p \"Rendered outside the island's DOM.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp"))
(p "The portal content lives in " (code "#portal-root") " (typically at the page body level), not inside the island. On island disposal, portal content is automatically removed from its target — the " (code "register-in-scope") " mechanism handles cleanup.")) (p "The portal content lives in " (code "#portal-root") " (typically at the page body level), not inside the island. On island disposal, portal content is automatically removed from its target — the " (code "register-in-scope") " mechanism handles cleanup."))
(~docs/section :title "8. Error Boundaries" :id "demo-error-boundary" (~docs/section :title "8. Error Boundaries" :id "demo-error-boundary"
(p "When an island's rendering or effect throws, " (code "error-boundary") " catches the error and renders a fallback. The fallback receives the error and a retry function. Partial effects created before the error are disposed automatically.") (p "When an island's rendering or effect throws, " (code "error-boundary") " catches the error and renders a fallback. The fallback receives the error and a retry function. Partial effects created before the error are disposed automatically.")
(~reactive-islands/demo/error-boundary) (~reactive-islands/index/demo-error-boundary)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/error-boundary ()\n (let ((throw? (signal false)))\n (error-boundary\n ;; Fallback: receives (err retry-fn)\n (fn (err retry-fn)\n (div :class \"p-3 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700\" (error-message err))\n (button :on-click (fn (e)\n (reset! throw? false) (invoke retry-fn))\n \"Retry\")))\n ;; Children: the happy path\n (do\n (when (deref throw?) (error \"Intentional explosion!\"))\n (p \"Everything is fine.\")))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/error-boundary ()\n (let ((throw? (signal false)))\n (error-boundary\n ;; Fallback: receives (err retry-fn)\n (fn (err retry-fn)\n (div :class \"p-3 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700\" (error-message err))\n (button :on-click (fn (e)\n (reset! throw? false) (invoke retry-fn))\n \"Retry\")))\n ;; Children: the happy path\n (do\n (when (deref throw?) (error \"Intentional explosion!\"))\n (p \"Everything is fine.\")))))" "lisp"))
(p "React equivalent: " (code "componentDidCatch") " / " (code "ErrorBoundary") ". SX's version is simpler — one form, not a class. The " (code "error-boundary") " form is a render-dom special form in " (code "adapter-dom.sx") ".")) (p "React equivalent: " (code "componentDidCatch") " / " (code "ErrorBoundary") ". SX's version is simpler — one form, not a class. The " (code "error-boundary") " form is a render-dom special form in " (code "adapter-dom.sx") "."))
(~docs/section :title "9. Refs — Imperative DOM Access" :id "demo-refs" (~docs/section :title "9. Refs — Imperative DOM Access" :id "demo-refs"
(p "The " (code ":ref") " attribute captures a DOM element handle into a dict. Use it for imperative operations: focusing, measuring, reading values.") (p "The " (code ":ref") " attribute captures a DOM element handle into a dict. Use it for imperative operations: focusing, measuring, reading values.")
(~reactive-islands/demo/refs) (~reactive-islands/index/demo-refs)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/refs ()\n (let ((my-ref (dict \"current\" nil))\n (msg (signal \"\")))\n (input :ref my-ref :type \"text\"\n :placeholder \"I can be focused programmatically\")\n (button :on-click (fn (e)\n (dom-focus (get my-ref \"current\")))\n \"Focus Input\")\n (button :on-click (fn (e)\n (let ((el (get my-ref \"current\")))\n (reset! msg (str \"value: \" (dom-get-prop el \"value\")))))\n \"Read Input\")\n (when (not (= (deref msg) \"\"))\n (p (deref msg)))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/refs ()\n (let ((my-ref (dict \"current\" nil))\n (msg (signal \"\")))\n (input :ref my-ref :type \"text\"\n :placeholder \"I can be focused programmatically\")\n (button :on-click (fn (e)\n (dom-focus (get my-ref \"current\")))\n \"Focus Input\")\n (button :on-click (fn (e)\n (let ((el (get my-ref \"current\")))\n (reset! msg (str \"value: \" (dom-get-prop el \"value\")))))\n \"Read Input\")\n (when (not (= (deref msg) \"\"))\n (p (deref msg)))))" "lisp"))
(p "React equivalent: " (code "useRef") ". In SX, a ref is just " (code "(dict \"current\" nil)") " — no special API. The " (code ":ref") " attribute sets " (code "(dict-set! ref \"current\" el)") " when the element is created. Read it with " (code "(get ref \"current\")") ".")) (p "React equivalent: " (code "useRef") ". In SX, a ref is just " (code "(dict \"current\" nil)") " — no special API. The " (code ":ref") " attribute sets " (code "(dict-set! ref \"current\" el)") " when the element is created. Read it with " (code "(get ref \"current\")") "."))
(~docs/section :title "10. Dynamic Class and Style" :id "demo-dynamic-class" (~docs/section :title "10. Dynamic Class and Style" :id "demo-dynamic-class"
(p "React uses " (code "className") " and " (code "style") " props with state. SX does the same — " (code "(deref signal)") " inside a " (code ":class") " or " (code ":style") " attribute creates a reactive binding. The attribute updates when the signal changes.") (p "React uses " (code "className") " and " (code "style") " props with state. SX does the same — " (code "(deref signal)") " inside a " (code ":class") " or " (code ":style") " attribute creates a reactive binding. The attribute updates when the signal changes.")
(~reactive-islands/demo/dynamic-class) (~reactive-islands/index/demo-dynamic-class)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/dynamic-class ()\n (let ((danger (signal false))\n (size (signal 16)))\n (div\n (button :on-click (fn (e) (swap! danger not))\n (if (deref danger) \"Safe mode\" \"Danger mode\"))\n (button :on-click (fn (e) (swap! size (fn (s) (+ s 2))))\n \"Bigger\")\n ;; Reactive class — recomputed when danger changes\n (div :class (str \"p-3 rounded font-medium \"\n (if (deref danger)\n \"bg-red-100 text-red-800\"\n \"bg-green-100 text-green-800\"))\n ;; Reactive style — recomputed when size changes\n :style (str \"font-size:\" (deref size) \"px\")\n \"This element's class and style are reactive.\"))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/dynamic-class ()\n (let ((danger (signal false))\n (size (signal 16)))\n (div\n (button :on-click (fn (e) (swap! danger not))\n (if (deref danger) \"Safe mode\" \"Danger mode\"))\n (button :on-click (fn (e) (swap! size (fn (s) (+ s 2))))\n \"Bigger\")\n ;; Reactive class — recomputed when danger changes\n (div :class (str \"p-3 rounded font-medium \"\n (if (deref danger)\n \"bg-red-100 text-red-800\"\n \"bg-green-100 text-green-800\"))\n ;; Reactive style — recomputed when size changes\n :style (str \"font-size:\" (deref size) \"px\")\n \"This element's class and style are reactive.\"))))" "lisp"))
(p "React equivalent: " (code "className={danger ? 'red' : 'green'}") " and " (code "style={{fontSize: size}}") ". In SX the " (code "str") " + " (code "if") " + " (code "deref") " pattern handles it — no " (code "classnames") " library needed. For complex conditional classes, use a " (code "computed") " or a CSSX " (code "defcomp") " that returns a class string.")) (p "React equivalent: " (code "className={danger ? 'red' : 'green'}") " and " (code "style={{fontSize: size}}") ". In SX the " (code "str") " + " (code "if") " + " (code "deref") " pattern handles it — no " (code "classnames") " library needed. For complex conditional classes, use a " (code "computed") " or a CSSX " (code "defcomp") " that returns a class string."))
(~docs/section :title "11. Resource + Suspense Pattern" :id "demo-resource" (~docs/section :title "11. Resource + Suspense Pattern" :id "demo-resource"
(p (code "resource") " wraps an async operation into a signal with " (code "loading") "/" (code "data") "/" (code "error") " states. Combined with " (code "cond") " + " (code "deref") ", this is the suspense pattern — no special form needed.") (p (code "resource") " wraps an async operation into a signal with " (code "loading") "/" (code "data") "/" (code "error") " states. Combined with " (code "cond") " + " (code "deref") ", this is the suspense pattern — no special form needed.")
(~reactive-islands/demo/resource) (~reactive-islands/index/demo-resource)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/resource ()\n (let ((data (resource (fn ()\n ;; Any promise-returning function\n (promise-delayed 1500\n (dict \"name\" \"Ada Lovelace\"\n \"role\" \"First Programmer\"))))))\n ;; This IS the suspense pattern:\n (let ((state (deref data)))\n (cond\n (get state \"loading\")\n (div \"Loading...\")\n (get state \"error\")\n (div \"Error: \" (get state \"error\"))\n :else\n (div (get (get state \"data\") \"name\"))))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/resource ()\n (let ((data (resource (fn ()\n ;; Any promise-returning function\n (promise-delayed 1500\n (dict \"name\" \"Ada Lovelace\"\n \"role\" \"First Programmer\"))))))\n ;; This IS the suspense pattern:\n (let ((state (deref data)))\n (cond\n (get state \"loading\")\n (div \"Loading...\")\n (get state \"error\")\n (div \"Error: \" (get state \"error\"))\n :else\n (div (get (get state \"data\") \"name\"))))))" "lisp"))
(p "React equivalent: " (code "Suspense") " + " (code "use()") " or " (code "useSWR") ". SX doesn't need a special " (code "suspense") " form because " (code "resource") " returns a signal and " (code "cond") " + " (code "deref") " creates reactive conditional rendering. When the promise resolves, the signal updates and the " (code "cond") " branch switches automatically.")) (p "React equivalent: " (code "Suspense") " + " (code "use()") " or " (code "useSWR") ". SX doesn't need a special " (code "suspense") " form because " (code "resource") " returns a signal and " (code "cond") " + " (code "deref") " creates reactive conditional rendering. When the promise resolves, the signal updates and the " (code "cond") " branch switches automatically."))
(~docs/section :title "12. Transition Pattern" :id "demo-transition" (~docs/section :title "12. Transition Pattern" :id "demo-transition"
(p "React's " (code "startTransition") " defers non-urgent updates so typing stays responsive. In SX: " (code "schedule-idle") " + " (code "batch") ". The filter runs during idle time, not blocking the input event.") (p "React's " (code "startTransition") " defers non-urgent updates so typing stays responsive. In SX: " (code "schedule-idle") " + " (code "batch") ". The filter runs during idle time, not blocking the input event.")
(~reactive-islands/demo/transition) (~reactive-islands/index/demo-transition)
(~docs/code :code (highlight "(defisland ~reactive-islands/demo/transition ()\n (let ((query (signal \"\"))\n (all-items (list \"Signals\" \"Effects\" ...))\n (filtered (signal (list)))\n (pending (signal false)))\n (reset! filtered all-items)\n ;; Filter effect — deferred via schedule-idle\n (effect (fn ()\n (let ((q (lower (deref query))))\n (if (= q \"\")\n (do (reset! pending false)\n (reset! filtered all-items))\n (do (reset! pending true)\n (schedule-idle (fn ()\n (batch (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower item) q))\n all-items))\n (reset! pending false))))))))))\n (div\n (input :bind query :placeholder \"Filter...\")\n (when (deref pending) (span \"Filtering...\"))\n (ul (map (fn (item) (li :key item item))\n (deref filtered))))))" "lisp")) (~docs/code :code (highlight "(defisland ~reactive-islands/demo/transition ()\n (let ((query (signal \"\"))\n (all-items (list \"Signals\" \"Effects\" ...))\n (filtered (signal (list)))\n (pending (signal false)))\n (reset! filtered all-items)\n ;; Filter effect — deferred via schedule-idle\n (effect (fn ()\n (let ((q (lower (deref query))))\n (if (= q \"\")\n (do (reset! pending false)\n (reset! filtered all-items))\n (do (reset! pending true)\n (schedule-idle (fn ()\n (batch (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower item) q))\n all-items))\n (reset! pending false))))))))))\n (div\n (input :bind query :placeholder \"Filter...\")\n (when (deref pending) (span \"Filtering...\"))\n (ul (map (fn (item) (li :key item item))\n (deref filtered))))))" "lisp"))
(p "React equivalent: " (code "startTransition(() => setFiltered(...))") ". SX uses " (code "schedule-idle") " (" (code "requestIdleCallback") " under the hood) to defer the expensive " (code "filter") " operation, and " (code "batch") " to group the result into one update. Fine-grained signals already avoid the jank that makes transitions critical in React — this pattern is for truly expensive computations.")) (p "React equivalent: " (code "startTransition(() => setFiltered(...))") ". SX uses " (code "schedule-idle") " (" (code "requestIdleCallback") " under the hood) to defer the expensive " (code "filter") " operation, and " (code "batch") " to group the result into one update. Fine-grained signals already avoid the jank that makes transitions critical in React — this pattern is for truly expensive computations."))

194
sx/sx/scopes.sx Normal file
View File

@@ -0,0 +1,194 @@
;; ---------------------------------------------------------------------------
;; Scopes — the unified primitive beneath provide, collect!, and spreads
;; ---------------------------------------------------------------------------
;; ---- Demo components ----
(defcomp ~geography/demo-scope-basic ()
(div :class "space-y-2"
(scope "demo-theme" :value "violet"
(div :class "rounded-lg p-3 bg-violet-50 border border-violet-200"
(p :class "text-sm text-violet-800 font-semibold"
(str "Inside scope: theme = " (context "demo-theme")))
(p :class "text-xs text-stone-500" "scope creates a named scope. context reads it.")))
(div :class "rounded-lg p-3 bg-stone-50 border border-stone-200"
(p :class "text-sm text-stone-600" "Outside scope: no context available."))))
(defcomp ~geography/demo-scope-emit ()
(div :class "space-y-2"
(scope "demo-deps"
(div :class "rounded-lg p-3 bg-stone-50 border border-stone-200"
(p :class "text-sm text-stone-700"
(emit! "demo-deps" "lodash")
(emit! "demo-deps" "react")
"Components emit their dependencies upward."))
(div :class "rounded-lg p-3 bg-violet-50 border border-violet-200"
(p :class "text-sm text-violet-800 font-semibold" "Emitted:")
(ul :class "text-xs text-stone-600 list-disc pl-5"
(map (fn (d) (li (code d))) (emitted "demo-deps")))))))
(defcomp ~geography/demo-scope-dedup ()
(div :class "space-y-2"
(div :class "rounded-lg p-3 bg-stone-50 border border-stone-200"
(p :class "text-sm text-stone-700"
(collect! "demo-css-dedup" ".card { padding: 1rem }")
(collect! "demo-css-dedup" ".card { padding: 1rem }")
(collect! "demo-css-dedup" ".btn { color: blue }")
"Three collect! calls, two identical. Only unique values kept."))
(div :class "rounded-lg p-3 bg-violet-50 border border-violet-200"
(p :class "text-sm text-violet-800 font-semibold"
(str "Collected: " (len (collected "demo-css-dedup")) " rules"))
(ul :class "text-xs text-stone-600 list-disc pl-5"
(map (fn (r) (li (code r))) (collected "demo-css-dedup"))))))
;; ---- Layout helper ----
(defcomp ~geography/scopes-demo-example (&key demo code)
(div :class "grid grid-cols-1 lg:grid-cols-2 gap-4 my-6 items-start"
(div :class "border border-dashed border-stone-300 rounded-lg p-4 bg-stone-50 min-h-[80px]"
demo)
(div :class "not-prose bg-stone-100 rounded-lg p-4 overflow-x-auto"
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code code)))))
;; ---- Page content ----
(defcomp ~geography/scopes-content ()
(~docs/page :title "Scopes"
(p :class "text-stone-500 text-sm italic mb-8"
"The unified primitive. " (code "scope") " creates a named scope with an optional value "
"and an accumulator. " (code "provide") ", " (code "collect!") ", spreads, islands — "
"they all resolve to scope operations at the platform level.")
;; =====================================================================
;; I. The primitive
;; =====================================================================
(~docs/section :title "The primitive" :id "primitive"
(p (code "scope") " is a special form that pushes a named scope, evaluates its body, "
"then pops it. The scope has three properties: a name, a downward value, and an "
"upward accumulator.")
(~docs/code :code (highlight "(scope name body...) ;; scope with no value\n(scope name :value v body...) ;; scope with downward value" "lisp"))
(p "Within the body, " (code "context") " reads the value, " (code "emit!") " appends "
"to the accumulator, and " (code "emitted") " reads what was accumulated.")
(~geography/scopes-demo-example
:demo (~geography/demo-scope-basic)
:code (highlight "(scope \"theme\" :value \"violet\"\n (context \"theme\")) ;; → \"violet\"\n\n;; Nested scopes shadow:\n(scope \"x\" :value \"outer\"\n (scope \"x\" :value \"inner\"\n (context \"x\")) ;; → \"inner\"\n (context \"x\")) ;; → \"outer\"" "lisp")))
;; =====================================================================
;; II. Sugar forms
;; =====================================================================
(~docs/section :title "Sugar forms" :id "sugar"
(p "Nobody writes " (code "scope") " directly. The sugar forms are the API:")
(~docs/table
:headers (list "Sugar" "Expands to" "Used for")
:rows (list
(list "provide" "(scope name :value v body...)" "Downward context passing")
(list "collect!" "Lazy root scope + dedup emit" "CSS rule accumulation")
(list "Spreads" "(scope \"element-attrs\" ...)" "Child-to-parent attrs (implicit)")))
(~docs/subsection :title "provide — scope with a value"
(p (code "(provide name value body...)") " is exactly "
(code "(scope name :value value body...)") ". It exists because "
"the two-arg form is the common case.")
(~docs/code :code (highlight ";; These are equivalent:\n(provide \"theme\" {:primary \"violet\"}\n (h1 \"hello\"))\n\n(scope \"theme\" :value {:primary \"violet\"}\n (h1 \"hello\"))" "lisp")))
(~docs/subsection :title "collect! — lazy root scope with dedup"
(p (code "collect!") " is the most interesting sugar. When called, if no scope exists "
"for that name, it lazily creates a root scope with deduplication enabled. "
"Then it emits into it.")
(~geography/scopes-demo-example
:demo (~geography/demo-scope-dedup)
:code (highlight ";; collect! creates a lazy root scope:\n(collect! \"css\" \".card { pad: 1rem }\")\n(collect! \"css\" \".card { pad: 1rem }\") ;; deduped!\n(collect! \"css\" \".btn { color: blue }\")\n(collected \"css\") ;; → 2 rules\n\n;; Equivalent to:\n(scope \"css\" ;; with dedup\n (emit! \"css\" ...)\n (emitted \"css\"))" "lisp"))
(p (code "collected") " is an alias for " (code "emitted") ". "
(code "clear-collected!") " clears the accumulator."))
(~docs/subsection :title "Spreads — implicit element scope"
(p "Every element rendering function wraps its children in "
(code "(scope-push! \"element-attrs\" nil)") ". Spread children "
(code "emit!") " their attrs into this scope. After rendering, the element "
"merges the emitted attrs.")
(p "See the "
(a :href "/sx/(geography.(spreads))" :class "text-violet-600 hover:underline" "spreads article")
" for the full mechanism.")))
;; =====================================================================
;; III. Accumulator: upward data flow
;; =====================================================================
(~docs/section :title "Upward data flow" :id "upward"
(~geography/scopes-demo-example
:demo (~geography/demo-scope-emit)
:code (highlight "(scope \"deps\"\n (emit! \"deps\" \"lodash\")\n (emit! \"deps\" \"react\")\n (emitted \"deps\")) ;; → (\"lodash\" \"react\")" "lisp"))
(p "Accumulation always goes to the " (em "nearest") " enclosing scope with that name. "
"This is what makes nested elements work — a spread inside a nested "
(code "span") " emits to the " (code "span") "'s scope, not an outer "
(code "div") "'s scope."))
;; =====================================================================
;; IV. Platform implementation
;; =====================================================================
(~docs/section :title "Platform implementation" :id "platform"
(p "Each platform (Python, JavaScript) maintains a single data structure:")
(~docs/code :code (highlight "_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" "python"))
(p "Six operations on this structure:")
(~docs/table
:headers (list "Operation" "Purpose")
:rows (list
(list "scope-push!(name, value)" "Push {value, emitted: [], dedup: false}")
(list "scope-pop!(name)" "Pop the most recent scope")
(list "context(name, default?)" "Read value from nearest scope")
(list "emit!(name, value)" "Append to nearest scope's accumulator (respects dedup)")
(list "emitted(name)" "Read accumulated values from nearest scope")
(list "collect!(name, value)" "Lazy push root scope with dedup, then emit")))
(p (code "provide-push!") " and " (code "provide-pop!") " are aliases for "
(code "scope-push!") " and " (code "scope-pop!") ". "
"All adapter code uses " (code "scope-push!") "/" (code "scope-pop!") " directly."))
;; =====================================================================
;; V. Unification
;; =====================================================================
(~docs/section :title "What scope unifies" :id "unification"
(p "Before scopes, the platform had two separate mechanisms:")
(~docs/code :code (highlight ";; Before: two mechanisms\n_provide_stacks = {} ;; {name: [{value, emitted: []}]}\n_collect_buckets = {} ;; {name: [values...]}\n\n;; After: one mechanism\n_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" "python"))
(p "The unification is not just code cleanup. It means:")
(ul :class "space-y-1"
(li (code "collect!") " can be nested inside " (code "provide") " scopes — "
"they share the same stack.")
(li "A component can " (code "emit!") " and " (code "collect!") " into the same scope — "
"they use the same accumulator.")
(li "The dedup flag is per-scope, not per-mechanism — a " (code "provide") " scope "
"has no dedup, a " (code "collect!") " root scope has dedup."))
(p "See the "
(a :href "/sx/(etc.(plan.scoped-effects))" :class "text-violet-600 hover:underline" "scoped effects plan")
" for the full design rationale and future phases (reactive scopes, morph scopes).")
(~docs/note
(p (strong "Spec: ") "The " (code "scope") " special form is in "
(a :href "/sx/(language.(spec.(explore.evaluator)))" :class "font-mono text-violet-600 hover:underline text-sm" "eval.sx")
". Platform primitives are declared in "
(a :href "/sx/(language.(spec.(explore.boundary)))" :class "font-mono text-violet-600 hover:underline text-sm" "boundary.sx")
" (Tier 5: Scoped effects).")))))

View File

@@ -89,20 +89,25 @@
(p :class "text-stone-500 text-sm italic mb-8" (p :class "text-stone-500 text-sm italic mb-8"
"A spread is a value that, when returned as a child of an element, " "A spread is a value that, when returned as a child of an element, "
"injects attributes onto its parent instead of rendering as content. " "injects attributes onto its parent instead of rendering as content. "
"This inverts the normal direction of data flow: children tell parents how to look.") "Internally, spreads work through "
(a :href "/sx/(geography.(scopes))" :class "text-violet-600 hover:underline" "scopes")
" — every element creates a scope, and spread children emit into it.")
;; ===================================================================== ;; =====================================================================
;; I. The primitives ;; I. How it works
;; ===================================================================== ;; =====================================================================
(~docs/section :title "Three primitives" :id "primitives" (~docs/section :title "How it works" :id "mechanism"
(p "The spread system has three orthogonal primitives. Each operates at a " (p "Every element wraps its children in a " (code "provide") " scope named "
"different level of the render pipeline.") (code "\"element-attrs\"") ". When the renderer encounters a spread child, "
"it calls " (code "emit!") " to push the spread's attrs into that scope. "
(~docs/subsection :title "1. make-spread / spread? / spread-attrs" "After all children render, the element collects the emitted attrs and merges them. "
(p "A spread is a value type. " (code "make-spread") " creates one from a dict of " "This is the " (a :href "/sx/(geography.(provide))" :class "text-violet-600 hover:underline"
"attributes. When the renderer encounters a spread as a child of an element, " "provide/emit!") " mechanism — spreads are one instance of it.")
"it merges the attrs onto the parent element instead of appending a DOM node.") (p "The user-facing API is " (code "make-spread") " — it creates a spread value from a dict. "
(code "spread?") " tests for one. " (code "spread-attrs") " extracts the dict. "
"But the actual delivery from child to parent goes through provide/emit!, not through "
"return-value inspection.")
(~geography/demo-example (~geography/demo-example
:demo (~geography/demo-spread-basic) :demo (~geography/demo-spread-basic)
@@ -110,18 +115,31 @@
(p (code "class") " values are appended (space-joined). " (p (code "class") " values are appended (space-joined). "
(code "style") " values are appended (semicolon-joined). " (code "style") " values are appended (semicolon-joined). "
"All other attributes overwrite.")) "All other attributes overwrite.")
(~docs/subsection :title "2. collect! / collected / clear-collected!" (p "Tolerant " (code "emit!") " means a spread outside any element context "
(p "Render-time accumulators. Values are collected into named buckets " "(in a fragment, " (code "begin") ", or bare " (code "map") ") silently vanishes "
"during rendering and retrieved at flush points. Deduplication is automatic.") "instead of crashing — there's no provider to emit into, so it's a no-op."))
;; =====================================================================
;; II. collect! — the other upward channel
;; =====================================================================
(~docs/section :title "collect! — the other upward channel" :id "collect"
(p "Spreads use " (code "scope") "/" (code "emit!") " (scoped, no dedup). "
(code "collect!") "/" (code "collected") " is also backed by scopes — "
"a lazy root scope with automatic deduplication. Used for CSS rule accumulation.")
(~docs/code :code (highlight ";; Deep inside a component tree:\n(collect! \"cssx\" \".sx-bg-red-500{background:red}\")\n\n;; At the flush point (once, in the layout):\n(let ((rules (collected \"cssx\")))\n (clear-collected! \"cssx\")\n (raw! (str \"<style>\" (join \"\" rules) \"</style>\")))" "lisp")) (~docs/code :code (highlight ";; Deep inside a component tree:\n(collect! \"cssx\" \".sx-bg-red-500{background:red}\")\n\n;; At the flush point (once, in the layout):\n(let ((rules (collected \"cssx\")))\n (clear-collected! \"cssx\")\n (raw! (str \"<style>\" (join \"\" rules) \"</style>\")))" "lisp"))
(p "This is upward communication through the render tree: " (p "Both are upward communication through the render tree, but with different "
"a deeply nested component contributes a CSS rule, and the layout " "semantics — " (code "emit!") " is scoped to the nearest provider, "
"emits all accumulated rules as a single " (code "<style>") " tag. " (code "collect!") " is global and deduplicates."))
"No prop threading, no context providers, no global state."))
(~docs/subsection :title "3. reactive-spread (islands)" ;; =====================================================================
;; III. Reactive spreads (islands)
;; =====================================================================
(~docs/section :title "Reactive spreads" :id "reactive"
(~docs/subsection :title "reactive-spread (islands)"
(p "Inside an island, when a spread's value depends on signals, " (p "Inside an island, when a spread's value depends on signals, "
(code "reactive-spread") " tracks signal dependencies and surgically " (code "reactive-spread") " tracks signal dependencies and surgically "
"updates the parent element's attributes when signals change.") "updates the parent element's attributes when signals change.")
@@ -138,12 +156,12 @@
(li "No re-render. No VDOM. No diffing. Just attr surgery.")))) (li "No re-render. No VDOM. No diffing. Just attr surgery."))))
;; ===================================================================== ;; =====================================================================
;; II. Orthogonality ;; IV. Orthogonality
;; ===================================================================== ;; =====================================================================
(~docs/section :title "Orthogonality across boundaries" :id "orthogonality" (~docs/section :title "Orthogonality across boundaries" :id "orthogonality"
(p "The three primitives operate at different levels of the render pipeline. " (p "Spread (via provide/emit!), collect!, and reactive-spread operate at different "
"They compose without knowing about each other.") "levels of the render pipeline. They compose without knowing about each other.")
(~docs/table (~docs/table
:headers (list "Primitive" "Direction" "Boundary" "When") :headers (list "Primitive" "Direction" "Boundary" "When")
@@ -179,11 +197,11 @@
(list "Client rendering" "spread in wire format (aser)" "reactive-spread (live)"))))) (list "Client rendering" "spread in wire format (aser)" "reactive-spread (live)")))))
;; ===================================================================== ;; =====================================================================
;; III. CSSX as use case ;; V. CSSX as use case
;; ===================================================================== ;; =====================================================================
(~docs/section :title "CSSX: the first application" :id "cssx" (~docs/section :title "CSSX: the first application" :id "cssx"
(p (code "~cssx/tw") " is a component that uses all three primitives:") (p (code "~cssx/tw") " is the primary consumer of spreads:")
(~geography/demo-example (~geography/demo-example
:demo (~geography/demo-cssx-tw) :demo (~geography/demo-cssx-tw)
@@ -193,7 +211,7 @@
(code "collect!") " to accumulate CSS rules for batch flushing, and " (code "collect!") " to accumulate CSS rules for batch flushing, and "
"when called inside an island with signal-dependent tokens, " "when called inside an island with signal-dependent tokens, "
(code "reactive-spread") " makes it live.") (code "reactive-spread") " makes it live.")
(p "But " (code "~cssx/tw") " is just one instance. The same primitives enable:") (p "But " (code "~cssx/tw") " is just one instance. The same mechanism enables:")
(ul :class "list-disc pl-5 space-y-1 text-stone-600" (ul :class "list-disc pl-5 space-y-1 text-stone-600"
(li (code "~aria") " — reactive accessibility attributes driven by UI state") (li (code "~aria") " — reactive accessibility attributes driven by UI state")
(li (code "~data-attrs") " — signal-driven data attributes for coordination") (li (code "~data-attrs") " — signal-driven data attributes for coordination")
@@ -201,7 +219,7 @@
(li "Any component that needs to inject attributes onto its parent"))) (li "Any component that needs to inject attributes onto its parent")))
;; ===================================================================== ;; =====================================================================
;; IV. Semantic variables ;; VI. Semantic variables
;; ===================================================================== ;; =====================================================================
(~docs/section :title "Semantic style variables" :id "variables" (~docs/section :title "Semantic style variables" :id "variables"
@@ -218,7 +236,7 @@
(code "~admin/heading") " are different components in different namespaces.")) (code "~admin/heading") " are different components in different namespaces."))
;; ===================================================================== ;; =====================================================================
;; V. What nothing else does ;; VII. What nothing else does
;; ===================================================================== ;; =====================================================================
(~docs/section :title "What nothing else does" :id "unique" (~docs/section :title "What nothing else does" :id "unique"
@@ -229,52 +247,43 @@
(p "CSS-in-JS libraries (styled-components, Emotion) create " (em "wrapper elements") (p "CSS-in-JS libraries (styled-components, Emotion) create " (em "wrapper elements")
". They don't inject attrs onto an existing element from a child position. " ". They don't inject attrs onto an existing element from a child position. "
"And they need a build step, a runtime, a theme provider.") "And they need a build step, a runtime, a theme provider.")
(p "SX does it with three orthogonal primitives that already existed for other reasons:") (p "SX does it with " (code "provide") "/" (code "emit!") " — render-time dynamic scope "
"that already existed for other reasons:")
(ul :class "list-disc pl-5 space-y-1 text-stone-600" (ul :class "list-disc pl-5 space-y-1 text-stone-600"
(li (strong "spread") " — child-to-parent attr injection (existed for component composition)") (li (strong "provide/emit!") " — scoped upward communication (the general mechanism)")
(li (strong "collect!") " — render-time accumulation (existed for CSS rule batching)") (li (strong "make-spread") " — creates spread values that emit into element-attrs providers")
(li (strong "reactive-spread") " — just the obvious combination of spread + effect")) (li (strong "reactive-spread") " — wraps emit! in a signal effect for live updates"))
(p "No new concepts. No new runtime. No new API surface.")) (p "No new concepts. No new runtime. No new API surface."))
;; ===================================================================== ;; =====================================================================
;; VI. The deeper primitive ;; VIII. The general primitive
;; ===================================================================== ;; =====================================================================
(~docs/section :title "The deeper primitive: provide / context / emit!" :id "provide" (~docs/section :title "The general primitive" :id "provide"
(p "Spread and collect are both instances of the same pattern: " (p "Spreads, collect!, and context are all instances of one mechanism: "
(strong "child communicates upward through the render tree") ". " (code "provide") "/" (code "emit!") " — render-time dynamic scope. "
"The general form is " (code "provide") "/" (code "context") "/" (code "emit!") (a :href "/sx/(geography.(provide))" :class "text-violet-600 hover:underline"
" — render-time dynamic scope.") "See the full provide/context/emit! article")
" for the general primitive, nested scoping, and its other uses.")
(~docs/subsection :title "The unification"
(~docs/table (~docs/table
:headers (list "Current" "General form" "Direction") :headers (list "Mechanism" "General form" "Direction")
:rows (list :rows (list
(list "collect! / collected" "emit! / emitted" "upward (child → scope)") (list "spread" "emit! into element-attrs provider" "upward (child → parent)")
(list "make-spread" "emit! into implicit parent-attrs provider" "upward (child → parent)") (list "collect! / collected" "emit! / emitted (global, deduped)" "upward (child → scope)")
(list "(nothing yet)" "context" "downward (scope → child)"))) (list "theme / config" "context" "downward (scope → child)")))
(p (code "provide") " creates a named scope with a value (downward) and an accumulator (upward). "
(code "context") " reads the value. " (code "emit!") " appends to the accumulator. "
(code "emitted") " retrieves accumulated values.")
(~docs/code :code (highlight ";; Downward: theme context\n(provide \"theme\" {:primary \"violet\" :font \"serif\"}\n (h1 :style (str \"color:\" (get (context \"theme\") :primary))\n \"Themed heading\"))\n\n;; Upward: script accumulation (like collect!)\n(provide \"scripts\" nil\n (div\n (emit! \"scripts\" \"analytics.js\")\n (div (emit! \"scripts\" \"charts.js\") \"chart\"))\n (for-each (fn (s) (script :src s)) (emitted \"scripts\")))\n\n;; Both at once\n(provide \"page\" {:title \"Home\"}\n (h1 (context \"page\" :title))\n (emit! \"page\" {:meta \"og:title\" :content \"Home\"})\n (for-each (fn (m) (meta :name (get m :meta) :content (get m :content)))\n (emitted \"page\")))" "lisp")))
(~docs/subsection :title "What this means"
(p "Three mechanisms collapse into one:")
(ul :class "list-disc pl-5 space-y-1 text-stone-600"
(li (code "collect!") " = " (code "emit!") " with deduplication")
(li (code "spread") " = " (code "emit!") " into implicit parent-attrs provider")
(li (code "collected") " = " (code "emitted"))
(li (code "context") " = downward data flow (new capability — no current equivalent)"))
(p "The reactive-spread we just built would naturally follow — the effect tracks "
"signal deps in the " (code "emit!") " call, the provider accumulates, the element applies.")
(p :class "text-stone-500 italic"
"This is the planned next step. The current primitives (spread, collect, reactive-spread) "
"work and are fully orthogonal. " (code "provide/context/emit!") " will be the deeper "
"foundation they are reimplemented on top of."))
(~docs/note (~docs/note
(p (strong "Plan: ") (code "provide") "/" (code "context") "/" (code "emit!") " is specced " (p (strong "Spec: ") "The provide/emit! primitives are declared in "
"and ready to implement. Per-name stacks. Each entry has a value and an emitted list. " (a :href "/sx/(language.(spec.(explore.boundary)))" :class "font-mono text-violet-600 hover:underline text-sm" "boundary.sx")
"Four primitives: " (code "provide") " (special form), " (code "context") ", " " (Tier 5: Dynamic scope). The " (code "provide") " special form is in "
(code "emit!") ", " (code "emitted") ". Platform provides " (code "provide-push!") (a :href "/sx/(language.(spec.(explore.evaluator)))" :class "font-mono text-violet-600 hover:underline text-sm" "eval.sx")
"/" (code "provide-pop!") ". See the implementation plan for details."))))) ". Element rendering with provide/emit! is visible in all four adapter specs: "
(a :href "/sx/(language.(spec.(explore.adapter-html)))" :class "font-mono text-violet-600 hover:underline text-sm" "adapter-html")
", "
(a :href "/sx/(language.(spec.(explore.adapter-async)))" :class "font-mono text-violet-600 hover:underline text-sm" "adapter-async")
", "
(a :href "/sx/(language.(spec.(explore.adapter-sx)))" :class "font-mono text-violet-600 hover:underline text-sm" "adapter-sx")
", "
(a :href "/sx/(language.(spec.(explore.adapter-dom)))" :class "font-mono text-violet-600 hover:underline text-sm" "adapter-dom")
".")))))

View File

@@ -613,6 +613,22 @@
"phase2" (~reactive-islands/phase2/reactive-islands-phase2-content) "phase2" (~reactive-islands/phase2/reactive-islands-phase2-content)
:else (~reactive-islands/index/reactive-islands-index-content)))) :else (~reactive-islands/index/reactive-islands-index-content))))
;; ---------------------------------------------------------------------------
;; Provide / Emit! section (under Geography)
;; ---------------------------------------------------------------------------
(defpage scopes-index
:path "/geography/scopes/"
:auth :public
:layout :sx-docs
:content (~layouts/doc :path "/sx/(geography.(scopes))" (~geography/scopes-content)))
(defpage provide-index
:path "/geography/provide/"
:auth :public
:layout :sx-docs
:content (~layouts/doc :path "/sx/(geography.(provide))" (~geography/provide-content)))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Spreads section (under Geography) ;; Spreads section (under Geography)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------