25 Commits

Author SHA1 Message Date
6aa2f3f6bd Add Special Forms docs page at /docs/special-forms
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m54s
Parses special-forms.sx spec into categorized form cards with syntax,
description, tail-position info, and highlighted examples. Follows the
same pattern as the Primitives page: Python helper returns structured
data, .sx components render it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:59:47 +00:00
6c27ebd3b4 Merge branch 'worktree-refactor-primitives' into macros
# Conflicts:
#	shared/sx/ref/bootstrap_js.py
#	shared/sx/ref/bootstrap_py.py
2026-03-06 01:50:29 +00:00
f77d7350dd Refactor SX primitives: modular, isomorphic, general-purpose
Spec modularization:
- Add (define-module :name) markers to primitives.sx creating 11 modules
  (7 core, 4 stdlib). Bootstrappers can now selectively include modules.
- Add parse_primitives_by_module() to boundary_parser.py.
- Remove split-ids primitive; inline at 4 call sites in blog/market queries.

Python file split:
- primitives.py: slimmed to registry + core primitives only (~350 lines)
- primitives_stdlib.py: NEW — stdlib primitives (format, text, style, debug)
- primitives_ctx.py: NEW — extracted 12 page context builders from IO
- primitives_io.py: add register_io_handler decorator, auto-derive
  IO_PRIMITIVES from registry, move sync IO bridges here

JS parity fixes:
- = uses === (strict equality), != uses !==
- round supports optional ndigits parameter
- concat uses nil-check not falsy-check (preserves 0, "", false)
- escape adds single quote entity (&#x27;) matching Python/markupsafe
- assert added (was missing from JS entirely)

Bootstrapper modularization:
- PRIMITIVES_JS_MODULES / PRIMITIVES_PY_MODULES dicts keyed by module
- --modules CLI flag for selective inclusion (core.* always included)
- Regenerated sx-ref.js and sx_ref.py with all fixes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:45:29 +00:00
ca8de3be1a Make continuations an optional extension, add special-forms.sx, ellipsis parsing
- Both bootstrappers (JS + Python) now gate shift/reset behind --extensions
  continuations flag. Without it, using reset/shift errors at runtime.
- JS bootstrapper: extracted Continuation/ShiftSignal types, sfReset/sfShift,
  continuation? primitive, and typeOf handling into CONTINUATIONS_JS constant.
  Extension wraps evalList, aserSpecial, and typeOf post-transpilation.
- Python bootstrapper: added special-forms.sx validation cross-check against
  eval.sx dispatch, warns on mismatches.
- Added shared/sx/ref/special-forms.sx: 36 declarative form specs with syntax,
  docs, tail-position, and examples. Used by bootstrappers for validation.
- Added ellipsis (...) support to both parser.py and parser.sx spec.
- Updated continuations essay to reflect optional extension architecture.
- Updated specs page and nav with special-forms.sx entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:44:50 +00:00
31ace8768e Merge scheme-forms into macros: named let, letrec, dynamic-wind, eq?/eqv?/equal?
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:16:16 +00:00
f34e55aa9b Add Scheme forms: named let, letrec, dynamic-wind, three-tier equality
Spec (eval.sx, primitives.sx):
- Named let: (let loop ((i 0)) body) — self-recursive lambda with TCO
- letrec: mutually recursive local bindings with closure patching
- dynamic-wind: entry/exit guards with wind stack for future continuations
- eq?/eqv?/equal?: identity, atom-value, and deep structural equality

Implementation (evaluator.py, async_eval.py, primitives.py):
- Both sync and async evaluators implement all four forms
- 33 new tests covering all forms including TCO at 10k depth

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:11:31 +00:00
102a27e845 Implement delimited continuations (shift/reset) across all evaluators
Bootstrap shift/reset to both Python and JS targets. The implementation
uses exception-based capture with re-evaluation: reset wraps in try/catch
for ShiftSignal, shift raises to the nearest reset, and continuation
invocation pushes a resume value and re-evaluates the body.

- Add Continuation type and _ShiftSignal to shared/sx/types.py
- Add sf_reset/sf_shift to hand-written evaluator.py
- Add async versions to async_eval.py
- Add shift/reset dispatch to eval.sx spec
- Bootstrap to Python: FIXUPS_PY with sf_reset/sf_shift, regenerate sx_ref.py
- Bootstrap to JS: Continuation/ShiftSignal types, sfReset/sfShift in fixups
- Add continuation? primitive to both bootstrappers and primitives.sx
- Allow callables (including Continuation) in hand-written HO map
- 44 unit tests (22 per evaluator) covering: passthrough, abort, invoke,
  double invoke, predicate, stored continuation, nested reset, practical patterns
- Update continuations essay to reflect implemented status with examples

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 00:58:50 +00:00
12fe93bb55 Add continuation specs: delimited (shift/reset) and full (call/cc)
Optional bolt-on extensions to the SX spec. continuations.sx defines
delimited continuations for all targets. callcc.sx defines full call/cc
for targets where it's native (Scheme, Haskell). Shared continuation
type if both are loaded. Wired into specs section of sx-docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 00:41:28 +00:00
0693586e6f Minor wording fixes in server architecture essay
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 00:20:32 +00:00
cfde5bc491 Fix sync IO primitives unreachable from sx_ref.py evaluator
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m49s
app-url, asset-url, config, jinja-global, relations-from are declared
as IO in boundary.sx but called inline in .sx code (inside let/filter).
async_eval_ref.py only intercepts IO at the top level — nested calls
fall through to sx_ref.eval_expr which couldn't find them.

Register sync bridge wrappers directly in _PRIMITIVES (bypassing
@register_primitive validation since they're boundary.sx, not
primitives.sx). Both async and sync eval paths now work.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 00:06:09 +00:00
abeb4551da Add server architecture essay to sx-docs
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m54s
Documents the boundary enforcement model: three tiers, boundary types,
runtime validation, the SX-in-Python rule, and the multi-language story.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:58:20 +00:00
04366990ec Enforce SX boundary contract via boundary.sx spec + runtime validation
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m33s
Add boundary.sx declaring all 34 I/O primitives, 32 page helpers, and 9
allowed boundary types. Runtime validation in boundary.py checks every
registration against the spec — undeclared primitives/helpers crash at
startup with SX_BOUNDARY_STRICT=1 (now set in both dev and prod).

Key changes:
- Move 5 I/O-in-disguise primitives (app-url, asset-url, config,
  jinja-global, relations-from) from primitives.py to primitives_io.py
- Remove duplicate url-for/route-prefix from primitives.py (already in IO)
- Fix parse-datetime to return ISO string instead of raw datetime
- Add datetime→isoformat conversion in _convert_result at the edge
- Wrap page helper return values with boundary type validation
- Replace all SxExpr(f"...") patterns with sx_call() or _sx_fragment()
- Add assert declaration to primitives.sx

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:50:02 +00:00
54adc9c216 Add TCO for parser loops in JS bootstrapper, enable SX_USE_REF
The JS parser transpiled from parser.sx used tail-recursive functions
(readStrLoop, skipWs, readListLoop, etc.) which overflow the stack on
large inputs — the bootstrapper page highlights 100KB of Python and
143KB of JavaScript, producing 7620 spans in a 907KB response.

The bootstrapper now detects zero-arg self-tail-recursive functions and
emits them as while(true) loops with continue instead of recursive
calls. Tested with 150K char strings and 8000 sibling elements.

Also enables SX_USE_REF=1 in dev via x-dev-env anchor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:09:02 +00:00
38f1f82988 Merge branch 'worktree-bootstrap-py' into macros 2026-03-05 22:48:14 +00:00
bb5c7e8444 Fall through to shared primitive registry for external primitives
is_primitive/get_primitive now check the shared registry
(shared.sx.primitives) when a name isn't in the transpiled PRIMITIVES
dict. Fixes Undefined symbol errors for register_primitive'd functions
like relations-from.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 22:48:14 +00:00
a40dd06811 Merge branch 'worktree-bootstrap-py' into macros 2026-03-05 22:45:05 +00:00
ef04beba00 Add call-fn dispatch for HO forms: handle both Lambda and native callable
HO forms (map, filter, reduce, etc.) now use call-fn which dispatches
Lambda → call-lambda, native callable → apply, else → clear EvalError.
Previously call-lambda crashed with AttributeError on native functions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 22:45:01 +00:00
4ed879bc84 Merge branch 'worktree-bootstrap-py' into macros 2026-03-05 22:36:59 +00:00
d076fc1465 Spec server definition forms (defhandler/defquery/defaction/defpage) in forms.sx
Previously defhandler routed to sf-define which tried to evaluate
(&key ...) params as expressions. Now each form has its own spec
with parse-key-params and platform constructors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 22:36:56 +00:00
17767ed8c4 Merge branch 'worktree-bootstrap-py' into macros 2026-03-05 22:17:29 +00:00
5aa13a99d1 Wire SX_USE_REF=1 switching through all route intermediaries
jinja_bridge, helpers, handlers, query_executor now conditionally
import from ref/sx_ref and ref/async_eval_ref when SX_USE_REF=1.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 22:17:26 +00:00
6328e3d680 Merge branch 'worktree-bootstrap-py' into macros
Bootstrap Python transpiler: reads .sx spec files and emits standalone
Python evaluator (sx_ref.py) with both HTML and SX wire format adapters.
Includes async wrapper and SX_USE_REF=1 switching mechanism.
2026-03-05 22:06:18 +00:00
7982a07f94 Add adapter-sx.sx transpilation, async wrapper, and SX_USE_REF switching
- Transpile adapter-sx.sx (aser) alongside adapter-html.sx for SX wire format
- Add platform functions: serialize, escape_string, is_special_form, is_ho_form,
  aser_special (with proper control-flow-through-aser dispatch)
- SxExpr wrapping prevents double-quoting in aser output
- async_eval_ref.py: async wrapper with I/O primitives, RequestContext,
  async_render, async_eval_to_sx, async_eval_slot_to_sx
- SX_USE_REF=1 env var switches shared.sx imports to transpiled backend
- 68 comparison tests (test_sx_ref.py), 289 total tests passing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 22:05:35 +00:00
4534fb9fee Add bootstrap_py.py: transpile SX spec to Python evaluator
Mirrors bootstrap_js.py pattern — reads the .sx reference spec files
(eval.sx, render.sx, adapter-html.sx) and emits a standalone Python
evaluator module (sx_ref.py) that can be compared against the
hand-written evaluator.py / html.py.

Key transpilation techniques:
- Nested IIFE lambdas for let bindings: (lambda a: body)(val)
- _sx_case helper for case/type dispatch
- Short-circuit and/or via Python ternaries
- Functions with set! emitted as def with _cells dict for mutation
- for-each with inline fn emitted as Python for loops
- Statement-level cond emitted as if/elif/else chains

Passes 27/27 comparison tests against hand-written evaluator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:32:01 +00:00
c43f774992 Skip event processor in standalone mode (no DB for LISTEN/NOTIFY)
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m28s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:02:05 +00:00
51 changed files with 10813 additions and 1980 deletions

View File

@@ -10,7 +10,8 @@
(defquery posts-by-ids (&key ids)
"Fetch multiple blog posts by comma-separated IDs."
(service "blog" "get-posts-by-ids" :ids (split-ids ids)))
(service "blog" "get-posts-by-ids"
:ids (map parse-int (filter (fn (s) (not (empty? s))) (split (str ids) ",")))))
(defquery search-posts (&key query page per-page)
"Search blog posts by text query, paginated."
@@ -35,4 +36,5 @@
(defquery page-configs-batch (&key container-type ids)
"Return PageConfigs for multiple container IDs (comma-separated)."
(service "page-config" "get-batch"
:container-type container-type :ids (split-ids ids)))
:container-type container-type
:ids (map parse-int (filter (fn (s) (not (empty? s))) (split (str ids) ",")))))

View File

@@ -10,6 +10,8 @@
x-dev-env: &dev-env
RELOAD: "true"
WORKERS: "1"
SX_USE_REF: "1"
SX_BOUNDARY_STRICT: "1"
x-sibling-models: &sibling-models
# Every app needs all sibling __init__.py + models/ for cross-domain SQLAlchemy imports

View File

@@ -56,6 +56,7 @@ x-app-env: &app-env
AP_DOMAIN_MARKET: market.rose-ash.com
AP_DOMAIN_EVENTS: events.rose-ash.com
EXTERNAL_INBOXES: "artdag|https://celery-artdag.rose-ash.com/inbox"
SX_BOUNDARY_STRICT: "1"
services:
blog:

View File

@@ -7,8 +7,10 @@
(defquery products-by-ids (&key ids)
"Return product details for comma-separated IDs."
(service "market-data" "products-by-ids" :ids (split-ids ids)))
(service "market-data" "products-by-ids"
:ids (map parse-int (filter (fn (s) (not (empty? s))) (split (str ids) ",")))))
(defquery marketplaces-by-ids (&key ids)
"Return marketplace data for comma-separated IDs."
(service "market-data" "marketplaces-by-ids" :ids (split-ids ids)))
(service "market-data" "marketplaces-by-ids"
:ids (map parse-int (filter (fn (s) (not (empty? s))) (split (str ids) ",")))))

View File

@@ -366,7 +366,7 @@ def create_base_app(
return await base_context()
# --- event processor ---
_event_processor = EventProcessor(app_name=name)
_event_processor = None if no_db else EventProcessor(app_name=name)
# --- startup ---
@app.before_serving
@@ -375,11 +375,13 @@ def create_base_app(
register_shared_handlers()
await init_config()
print(pretty())
await _event_processor.start()
if _event_processor:
await _event_processor.start()
@app.after_serving
async def _stop_event_processor():
await _event_processor.stop()
if _event_processor:
await _event_processor.stop()
from shared.infrastructure.auth_redis import close_auth_redis
await close_auth_redis()

View File

@@ -508,6 +508,25 @@
return NIL;
}
// =========================================================================
// Platform interface — Parser
// =========================================================================
// Character classification derived from the grammar:
// ident-start → [a-zA-Z_~*+\-><=/!?&]
// ident-char → ident-start + [0-9.:\/\[\]#,]
var _identStartRe = /[a-zA-Z_~*+\-><=/!?&]/;
var _identCharRe = /[a-zA-Z0-9_~*+\-><=/!?.:&/\[\]#,]/;
function isIdentStart(ch) { return _identStartRe.test(ch); }
function isIdentChar(ch) { return _identCharRe.test(ch); }
function parseNumber(s) { return Number(s); }
function escapeString(s) {
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t");
}
function sxExprSource(e) { return typeof e === "string" ? e : String(e); }
// === Transpiled from eval ===
// trampoline
@@ -528,10 +547,10 @@
var args = rest(expr);
return (isSxTruthy(!sxOr((typeOf(head) == "symbol"), (typeOf(head) == "lambda"), (typeOf(head) == "list"))) ? map(function(x) { return trampoline(evalExpr(x, env)); }, expr) : (isSxTruthy((typeOf(head) == "symbol")) ? (function() {
var name = symbolName(head);
return (isSxTruthy((name == "if")) ? sfIf(args, env) : (isSxTruthy((name == "when")) ? sfWhen(args, env) : (isSxTruthy((name == "cond")) ? sfCond(args, env) : (isSxTruthy((name == "case")) ? sfCase(args, env) : (isSxTruthy((name == "and")) ? sfAnd(args, env) : (isSxTruthy((name == "or")) ? sfOr(args, env) : (isSxTruthy((name == "let")) ? sfLet(args, env) : (isSxTruthy((name == "let*")) ? sfLet(args, env) : (isSxTruthy((name == "lambda")) ? sfLambda(args, env) : (isSxTruthy((name == "fn")) ? sfLambda(args, env) : (isSxTruthy((name == "define")) ? sfDefine(args, env) : (isSxTruthy((name == "defcomp")) ? sfDefcomp(args, env) : (isSxTruthy((name == "defmacro")) ? sfDefmacro(args, env) : (isSxTruthy((name == "defstyle")) ? sfDefstyle(args, env) : (isSxTruthy((name == "defkeyframes")) ? sfDefkeyframes(args, env) : (isSxTruthy((name == "defhandler")) ? sfDefine(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 == "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 == "lambda")) ? sfLambda(args, env) : (isSxTruthy((name == "fn")) ? sfLambda(args, env) : (isSxTruthy((name == "define")) ? sfDefine(args, env) : (isSxTruthy((name == "defcomp")) ? sfDefcomp(args, env) : (isSxTruthy((name == "defmacro")) ? sfDefmacro(args, env) : (isSxTruthy((name == "defstyle")) ? sfDefstyle(args, env) : (isSxTruthy((name == "defkeyframes")) ? sfDefkeyframes(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 == "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 == "map")) ? hoMap(args, env) : (isSxTruthy((name == "map-indexed")) ? hoMapIndexed(args, env) : (isSxTruthy((name == "filter")) ? hoFilter(args, env) : (isSxTruthy((name == "reduce")) ? hoReduce(args, env) : (isSxTruthy((name == "some")) ? hoSome(args, env) : (isSxTruthy((name == "every?")) ? hoEvery(args, env) : (isSxTruthy((name == "for-each")) ? hoForEach(args, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? (function() {
var mac = envGet(env, name);
return makeThunk(expandMacro(mac, args, env), env);
})() : (isSxTruthy(isRenderExpr(expr)) ? renderExpr(expr, env) : evalCall(head, args, env))))))))))))))))))))))))))))))));
})() : (isSxTruthy(isRenderExpr(expr)) ? renderExpr(expr, env) : evalCall(head, args, env)))))))))))))))))))))))))))))))))));
})() : evalCall(head, args, env)));
})(); };
@@ -788,25 +807,28 @@
return trampoline(evalExpr(macroBody(mac), local));
})(); };
// call-fn
var callFn = function(f, args, env) { return (isSxTruthy(isLambda(f)) ? trampoline(callLambda(f, args, env)) : (isSxTruthy(isCallable(f)) ? apply(f, args) : error((String("Not callable in HO form: ") + String(inspect(f)))))); };
// ho-map
var hoMap = function(args, env) { return (function() {
var f = trampoline(evalExpr(first(args), env));
var coll = trampoline(evalExpr(nth(args, 1), env));
return map(function(item) { return trampoline(callLambda(f, [item], env)); }, coll);
return map(function(item) { return callFn(f, [item], env); }, coll);
})(); };
// ho-map-indexed
var hoMapIndexed = function(args, env) { return (function() {
var f = trampoline(evalExpr(first(args), env));
var coll = trampoline(evalExpr(nth(args, 1), env));
return mapIndexed(function(i, item) { return trampoline(callLambda(f, [i, item], env)); }, coll);
return mapIndexed(function(i, item) { return callFn(f, [i, item], env); }, coll);
})(); };
// ho-filter
var hoFilter = function(args, env) { return (function() {
var f = trampoline(evalExpr(first(args), env));
var coll = trampoline(evalExpr(nth(args, 1), env));
return filter(function(item) { return trampoline(callLambda(f, [item], env)); }, coll);
return filter(function(item) { return callFn(f, [item], env); }, coll);
})(); };
// ho-reduce
@@ -814,28 +836,28 @@
var f = trampoline(evalExpr(first(args), env));
var init = trampoline(evalExpr(nth(args, 1), env));
var coll = trampoline(evalExpr(nth(args, 2), env));
return reduce(function(acc, item) { return trampoline(callLambda(f, [acc, item], env)); }, init, coll);
return reduce(function(acc, item) { return callFn(f, [acc, item], env); }, init, coll);
})(); };
// ho-some
var hoSome = function(args, env) { return (function() {
var f = trampoline(evalExpr(first(args), env));
var coll = trampoline(evalExpr(nth(args, 1), env));
return some(function(item) { return trampoline(callLambda(f, [item], env)); }, coll);
return some(function(item) { return callFn(f, [item], env); }, coll);
})(); };
// ho-every
var hoEvery = function(args, env) { return (function() {
var f = trampoline(evalExpr(first(args), env));
var coll = trampoline(evalExpr(nth(args, 1), env));
return isEvery(function(item) { return trampoline(callLambda(f, [item], env)); }, coll);
return isEvery(function(item) { return callFn(f, [item], env); }, coll);
})(); };
// ho-for-each
var hoForEach = function(args, env) { return (function() {
var f = trampoline(evalExpr(first(args), env));
var coll = trampoline(evalExpr(nth(args, 1), env));
return forEach(function(item) { return trampoline(callLambda(f, [item], env)); }, coll);
return forEach(function(item) { return callFn(f, [item], env); }, coll);
})(); };
@@ -875,6 +897,117 @@
})(); }, keys(attrs))); };
// === Transpiled from parser ===
// sx-parse
var sxParse = function(source) { return (function() {
var pos = 0;
var lenSrc = len(source);
var skipComment = function() { while(true) { if (isSxTruthy((isSxTruthy((pos < lenSrc)) && !(nth(source, pos) == "\n")))) { pos = (pos + 1);
continue; } else { return NIL; } } };
var skipWs = function() { while(true) { if (isSxTruthy((pos < lenSrc))) { { var ch = nth(source, pos);
if (isSxTruthy(sxOr((ch == " "), (ch == "\t"), (ch == "\n"), (ch == "\\r")))) { pos = (pos + 1);
continue; } else if (isSxTruthy((ch == ";"))) { pos = (pos + 1);
skipComment();
continue; } else { return NIL; } } } else { return NIL; } } };
var readString = function() { pos = (pos + 1);
return (function() {
var buf = "";
var readStrLoop = function() { while(true) { if (isSxTruthy((pos >= lenSrc))) { return error("Unterminated string"); } else { { var ch = nth(source, pos);
if (isSxTruthy((ch == "\""))) { pos = (pos + 1);
return NIL; } else if (isSxTruthy((ch == "\\"))) { pos = (pos + 1);
{ var esc = nth(source, pos);
buf = (String(buf) + String((isSxTruthy((esc == "n")) ? "\n" : (isSxTruthy((esc == "t")) ? "\t" : (isSxTruthy((esc == "r")) ? "\\r" : esc)))));
pos = (pos + 1);
continue; } } else { buf = (String(buf) + String(ch));
pos = (pos + 1);
continue; } } } } };
readStrLoop();
return buf;
})(); };
var readIdent = function() { return (function() {
var start = pos;
var readIdentLoop = function() { while(true) { if (isSxTruthy((isSxTruthy((pos < lenSrc)) && isIdentChar(nth(source, pos))))) { pos = (pos + 1);
continue; } else { return NIL; } } };
readIdentLoop();
return slice(source, start, pos);
})(); };
var readKeyword = function() { pos = (pos + 1);
return makeKeyword(readIdent()); };
var readNumber = function() { return (function() {
var start = pos;
if (isSxTruthy((isSxTruthy((pos < lenSrc)) && (nth(source, pos) == "-")))) {
pos = (pos + 1);
}
var readDigits = function() { while(true) { if (isSxTruthy((isSxTruthy((pos < lenSrc)) && (function() {
var c = nth(source, pos);
return (isSxTruthy((c >= "0")) && (c <= "9"));
})()))) { pos = (pos + 1);
continue; } else { return NIL; } } };
readDigits();
if (isSxTruthy((isSxTruthy((pos < lenSrc)) && (nth(source, pos) == ".")))) {
pos = (pos + 1);
readDigits();
}
if (isSxTruthy((isSxTruthy((pos < lenSrc)) && sxOr((nth(source, pos) == "e"), (nth(source, pos) == "E"))))) {
pos = (pos + 1);
if (isSxTruthy((isSxTruthy((pos < lenSrc)) && sxOr((nth(source, pos) == "+"), (nth(source, pos) == "-"))))) {
pos = (pos + 1);
}
readDigits();
}
return parseNumber(slice(source, start, pos));
})(); };
var readSymbol = function() { return (function() {
var name = readIdent();
return (isSxTruthy((name == "true")) ? true : (isSxTruthy((name == "false")) ? false : (isSxTruthy((name == "nil")) ? NIL : makeSymbol(name))));
})(); };
var readList = function(closeCh) { return (function() {
var items = [];
var readListLoop = function() { while(true) { skipWs();
if (isSxTruthy((pos >= lenSrc))) { return error("Unterminated list"); } else { if (isSxTruthy((nth(source, pos) == closeCh))) { pos = (pos + 1);
return NIL; } else { items.push(readExpr());
continue; } } } };
readListLoop();
return items;
})(); };
var readMap = function() { return (function() {
var result = {};
var readMapLoop = function() { while(true) { skipWs();
if (isSxTruthy((pos >= lenSrc))) { return error("Unterminated map"); } else { if (isSxTruthy((nth(source, pos) == "}"))) { pos = (pos + 1);
return NIL; } else { { var keyExpr = readExpr();
var keyStr = (isSxTruthy((typeOf(keyExpr) == "keyword")) ? keywordName(keyExpr) : (String(keyExpr)));
var valExpr = readExpr();
result[keyStr] = valExpr;
continue; } } } } };
readMapLoop();
return result;
})(); };
var readExpr = function() { skipWs();
return (isSxTruthy((pos >= lenSrc)) ? error("Unexpected end of input") : (function() {
var ch = nth(source, pos);
return (isSxTruthy((ch == "(")) ? ((pos = (pos + 1)), readList(")")) : (isSxTruthy((ch == "[")) ? ((pos = (pos + 1)), readList("]")) : (isSxTruthy((ch == "{")) ? ((pos = (pos + 1)), readMap()) : (isSxTruthy((ch == "\"")) ? readString() : (isSxTruthy((ch == ":")) ? readKeyword() : (isSxTruthy((ch == "`")) ? ((pos = (pos + 1)), [makeSymbol("quasiquote"), readExpr()]) : (isSxTruthy((ch == ",")) ? ((pos = (pos + 1)), (isSxTruthy((isSxTruthy((pos < lenSrc)) && (nth(source, pos) == "@"))) ? ((pos = (pos + 1)), [makeSymbol("splice-unquote"), readExpr()]) : [makeSymbol("unquote"), readExpr()])) : (isSxTruthy(sxOr((isSxTruthy((ch >= "0")) && (ch <= "9")), (isSxTruthy((ch == "-")) && isSxTruthy(((pos + 1) < lenSrc)) && (function() {
var nextCh = nth(source, (pos + 1));
return (isSxTruthy((nextCh >= "0")) && (nextCh <= "9"));
})()))) ? readNumber() : (isSxTruthy(isIdentStart(ch)) ? readSymbol() : error((String("Unexpected character: ") + String(ch))))))))))));
})()); };
return (function() {
var exprs = [];
var parseLoop = function() { while(true) { skipWs();
if (isSxTruthy((pos < lenSrc))) { exprs.push(readExpr());
continue; } else { return NIL; } } };
parseLoop();
return exprs;
})();
})(); };
// sx-serialize
var sxSerialize = function(val) { return (function() { var _m = typeOf(val); if (_m == "nil") return "nil"; if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "number") return (String(val)); if (_m == "string") return (String("\"") + String(escapeString(val)) + String("\"")); if (_m == "symbol") return symbolName(val); if (_m == "keyword") return (String(":") + String(keywordName(val))); if (_m == "list") return (String("(") + String(join(" ", map(sxSerialize, val))) + String(")")); if (_m == "dict") return sxSerializeDict(val); if (_m == "sx-expr") return sxExprSource(val); return (String(val)); })(); };
// sx-serialize-dict
var sxSerializeDict = function(d) { return (String("{") + String(join(" ", reduce(function(acc, key) { return concat(acc, [(String(":") + String(key)), sxSerialize(dictGet(d, key))]); }, [], keys(d)))) + String("}")); };
// === Transpiled from adapter-dom ===
// SVG_NS
@@ -1284,7 +1417,7 @@ return forEach(function(attr) { return (isSxTruthy(!domHasAttr(newEl, first(attr
var _preloadCache = {};
// _css-hash
var _cssHash = "";
var _cssHash = NIL;
// dispatch-trigger-events
var dispatchTriggerEvents = function(el, headerVal) { return (isSxTruthy(headerVal) ? (function() {
@@ -1747,7 +1880,7 @@ return (_styleCache = {}); };
return (isSxTruthy(decls) ? ((isSxTruthy(startsWith(base, "animate-")) ? (function() {
var kfName = slice(base, 8);
return (isSxTruthy(dictHas(_styleKeyframes, kfName)) ? append_b(kfNeeded, [kfName, dictGet(_styleKeyframes, kfName)]) : NIL);
})() : NIL), (isSxTruthy(isNil(variant)) ? append_b(baseDecls, decls) : (isSxTruthy(dictHas(_responsiveBreakpoints, variant)) ? append_b(mediaRules, [dictGet(_responsiveBreakpoints, variant), decls]) : (isSxTruthy(dictHas(_pseudoVariants, variant)) ? append_b(pseudoRules, [dictGet(_pseudoVariants, variant), decls]) : (function() {
})() : NIL), (isSxTruthy(isNil(variant)) ? (isSxTruthy(isChildSelectorAtom(base)) ? append_b(pseudoRules, [">:not(:first-child)", decls]) : append_b(baseDecls, decls)) : (isSxTruthy(dictHas(_responsiveBreakpoints, variant)) ? append_b(mediaRules, [dictGet(_responsiveBreakpoints, variant), decls]) : (isSxTruthy(dictHas(_pseudoVariants, variant)) ? append_b(pseudoRules, [dictGet(_pseudoVariants, variant), decls]) : (function() {
var vparts = split(variant, ":");
var mediaPart = NIL;
var pseudoPart = NIL;
@@ -1766,12 +1899,12 @@ return (_styleCache = {}); };
} } }
return (function() {
var hashInput = join(";", baseDecls);
{ var _c = chunkEvery(mediaRules, 2); for (var _i = 0; _i < _c.length; _i++) { var mr = _c[_i]; hashInput = (String(hashInput) + String("@") + String(first(mr)) + String("{") + String(nth(mr, 1)) + String("}")); } }
{ var _c = chunkEvery(pseudoRules, 2); for (var _i = 0; _i < _c.length; _i++) { var pr = _c[_i]; hashInput = (String(hashInput) + String(first(pr)) + String("{") + String(nth(pr, 1)) + String("}")); } }
{ var _c = chunkEvery(kfNeeded, 2); for (var _i = 0; _i < _c.length; _i++) { var kf = _c[_i]; hashInput = (String(hashInput) + String(nth(kf, 1))); } }
{ var _c = mediaRules; for (var _i = 0; _i < _c.length; _i++) { var mr = _c[_i]; hashInput = (String(hashInput) + String("@") + String(first(mr)) + String("{") + String(nth(mr, 1)) + String("}")); } }
{ var _c = pseudoRules; for (var _i = 0; _i < _c.length; _i++) { var pr = _c[_i]; hashInput = (String(hashInput) + String(first(pr)) + String("{") + String(nth(pr, 1)) + String("}")); } }
{ var _c = kfNeeded; for (var _i = 0; _i < _c.length; _i++) { var kf = _c[_i]; hashInput = (String(hashInput) + String(nth(kf, 1))); } }
return (function() {
var cn = (String("sx-") + String(hashStyle(hashInput)));
var sv = makeStyleValue_(cn, join(";", baseDecls), chunkEvery(mediaRules, 2), chunkEvery(pseudoRules, 2), chunkEvery(kfNeeded, 2));
var sv = makeStyleValue_(cn, join(";", baseDecls), mediaRules, pseudoRules, kfNeeded);
_styleCache[key] = sv;
injectStyleValue(sv, atoms);
return sv;
@@ -2740,18 +2873,11 @@ callExpr.push(dictGet(kwargs, k)); } }
if (!cssTarget) return;
var rules = [];
// Child-selector atoms are now routed to pseudoRules by the resolver
// with selector ">:not(:first-child)", so base declarations are always
// applied directly to the class.
if (sv.declarations) {
var hasChild = false;
if (atoms) {
for (var ai = 0; ai < atoms.length; ai++) {
if (isChildSelectorAtom(atoms[ai])) { hasChild = true; break; }
}
}
if (hasChild) {
rules.push("." + sv.className + ">:not(:first-child){" + sv.declarations + "}");
} else {
rules.push("." + sv.className + "{" + sv.declarations + "}");
}
rules.push("." + sv.className + "{" + sv.declarations + "}");
}
for (var pi = 0; pi < sv.pseudoRules.length; pi++) {
var sel = sv.pseudoRules[pi][0], decls = sv.pseudoRules[pi][1];
@@ -2952,107 +3078,8 @@ callExpr.push(dictGet(kwargs, k)); } }
// Expose render functions as primitives so SX code can call them
if (typeof renderToDom === "function") PRIMITIVES["render-to-dom"] = renderToDom;
// =========================================================================
// Parser
// =========================================================================
function parse(text) {
var pos = 0;
function skipWs() {
while (pos < text.length) {
var ch = text[pos];
if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") { pos++; continue; }
if (ch === ";") { while (pos < text.length && text[pos] !== "\n") pos++; continue; }
break;
}
}
function readExpr() {
skipWs();
if (pos >= text.length) return undefined;
var ch = text[pos];
if (ch === "(") { pos++; return readList(")"); }
if (ch === "[") { pos++; return readList("]"); }
if (ch === "{") { pos++; return readMap(); }
if (ch === '"') return readString();
if (ch === ":") return readKeyword();
if (ch === "`") { pos++; return [new Symbol("quasiquote"), readExpr()]; }
if (ch === ",") {
pos++;
if (pos < text.length && text[pos] === "@") { pos++; return [new Symbol("splice-unquote"), readExpr()]; }
return [new Symbol("unquote"), readExpr()];
}
if (ch === "-" && pos + 1 < text.length && text[pos + 1] >= "0" && text[pos + 1] <= "9") return readNumber();
if (ch >= "0" && ch <= "9") return readNumber();
return readSymbol();
}
function readList(close) {
var items = [];
while (true) {
skipWs();
if (pos >= text.length) throw new Error("Unterminated list");
if (text[pos] === close) { pos++; return items; }
items.push(readExpr());
}
}
function readMap() {
var result = {};
while (true) {
skipWs();
if (pos >= text.length) throw new Error("Unterminated map");
if (text[pos] === "}") { pos++; return result; }
var key = readExpr();
var keyStr = (key && key._kw) ? key.name : String(key);
result[keyStr] = readExpr();
}
}
function readString() {
pos++; // skip "
var s = "";
while (pos < text.length) {
var ch = text[pos];
if (ch === '"') { pos++; return s; }
if (ch === "\\") { pos++; var esc = text[pos]; s += esc === "n" ? "\n" : esc === "t" ? "\t" : esc === "r" ? "\r" : esc; pos++; continue; }
s += ch; pos++;
}
throw new Error("Unterminated string");
}
function readKeyword() {
pos++; // skip :
var name = readIdent();
return new Keyword(name);
}
function readNumber() {
var start = pos;
if (text[pos] === "-") pos++;
while (pos < text.length && text[pos] >= "0" && text[pos] <= "9") pos++;
if (pos < text.length && text[pos] === ".") { pos++; while (pos < text.length && text[pos] >= "0" && text[pos] <= "9") pos++; }
if (pos < text.length && (text[pos] === "e" || text[pos] === "E")) {
pos++;
if (pos < text.length && (text[pos] === "+" || text[pos] === "-")) pos++;
while (pos < text.length && text[pos] >= "0" && text[pos] <= "9") pos++;
}
return Number(text.slice(start, pos));
}
function readIdent() {
var start = pos;
while (pos < text.length && /[a-zA-Z0-9_~*+\-><=/!?.:&]/.test(text[pos])) pos++;
return text.slice(start, pos);
}
function readSymbol() {
var name = readIdent();
if (name === "true") return true;
if (name === "false") return false;
if (name === "nil") return NIL;
return new Symbol(name);
}
var exprs = [];
while (true) {
skipWs();
if (pos >= text.length) break;
exprs.push(readExpr());
}
return exprs;
}
// Parser — compiled from parser.sx (see PLATFORM_PARSER_JS for ident char classes)
var parse = sxParse;
// =========================================================================
// Public API
@@ -3104,7 +3131,7 @@ callExpr.push(dictGet(kwargs, k)); } }
renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null,
getEnv: function() { return componentEnv; },
init: typeof bootInit === "function" ? bootInit : null,
_version: "ref-2.0 (boot+cssx+dom+engine+orchestration, bootstrap-compiled)"
_version: "ref-2.0 (boot+cssx+dom+engine+orchestration+parser, bootstrap-compiled)"
};

File diff suppressed because it is too large Load Diff

View File

@@ -87,7 +87,7 @@
var RE_COMMENT = /;[^\n]*/y;
var RE_STRING = /"(?:[^"\\]|\\[\s\S])*"/y;
var RE_NUMBER = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/y;
var RE_KEYWORD = /:[a-zA-Z_][a-zA-Z0-9_>:\-]*/y;
var RE_KEYWORD = /:[a-zA-Z_~*+\-><=/!?&\[][a-zA-Z0-9_~*+\-><=/!?.:&/\[\]#,]*/y;
var RE_SYMBOL = /[a-zA-Z_~*+\-><=/!?&][a-zA-Z0-9_~*+\-><=/!?.:&]*/y;
function Tokenizer(text) {

View File

@@ -31,16 +31,27 @@ from .parser import (
parse_all,
serialize,
)
from .evaluator import (
EvalError,
evaluate,
make_env,
)
import os as _os
if _os.environ.get("SX_USE_REF") == "1":
from .ref.sx_ref import (
EvalError,
evaluate,
make_env,
)
else:
from .evaluator import (
EvalError,
evaluate,
make_env,
)
from .primitives import (
all_primitives,
get_primitive,
register_primitive,
)
from . import primitives_stdlib # noqa: F401 — registers stdlib primitives
from .env import Env
__all__ = [

View File

@@ -279,6 +279,10 @@ async def _asf_or(expr, env, ctx):
async def _asf_let(expr, env, ctx):
# Named let: (let name ((x 0) ...) body)
if isinstance(expr[1], Symbol):
return await _asf_named_let(expr, env, ctx)
bindings = expr[1]
local = dict(env)
if isinstance(bindings, list):
@@ -299,6 +303,98 @@ async def _asf_let(expr, env, ctx):
return NIL
async def _asf_named_let(expr, env, ctx):
"""Async named let: (let name ((x 0) ...) body)"""
loop_name = expr[1].name
bindings = expr[2]
body = expr[3:]
params: list[str] = []
inits: list = []
if isinstance(bindings, list):
if bindings and isinstance(bindings[0], list):
for binding in bindings:
var = binding[0]
params.append(var.name if isinstance(var, Symbol) else var)
inits.append(binding[1])
elif len(bindings) % 2 == 0:
for i in range(0, len(bindings), 2):
var = bindings[i]
params.append(var.name if isinstance(var, Symbol) else var)
inits.append(bindings[i + 1])
loop_body = body[0] if len(body) == 1 else [Symbol("begin")] + list(body)
loop_fn = Lambda(params, loop_body, dict(env), name=loop_name)
loop_fn.closure[loop_name] = loop_fn
init_vals = [await _async_trampoline(await _async_eval(init, env, ctx)) for init in inits]
return await _async_call_lambda(loop_fn, init_vals, env, ctx)
async def _asf_letrec(expr, env, ctx):
"""Async letrec: (letrec ((name1 val1) ...) body)"""
bindings = expr[1]
local = dict(env)
names: list[str] = []
val_exprs: list = []
if isinstance(bindings, list):
if bindings and isinstance(bindings[0], list):
for binding in bindings:
var = binding[0]
vname = var.name if isinstance(var, Symbol) else var
names.append(vname)
val_exprs.append(binding[1])
local[vname] = NIL
elif len(bindings) % 2 == 0:
for i in range(0, len(bindings), 2):
var = bindings[i]
vname = var.name if isinstance(var, Symbol) else var
names.append(vname)
val_exprs.append(bindings[i + 1])
local[vname] = NIL
values = [await _async_trampoline(await _async_eval(ve, local, ctx)) for ve in val_exprs]
for name, val in zip(names, values):
local[name] = val
for val in values:
if isinstance(val, Lambda):
for name in names:
val.closure[name] = local[name]
for body_expr in expr[2:-1]:
await _async_trampoline(await _async_eval(body_expr, local, ctx))
if len(expr) > 2:
return _AsyncThunk(expr[-1], local, ctx)
return NIL
async def _asf_dynamic_wind(expr, env, ctx):
"""Async dynamic-wind: (dynamic-wind before body after)"""
before = await _async_trampoline(await _async_eval(expr[1], env, ctx))
body_fn = await _async_trampoline(await _async_eval(expr[2], env, ctx))
after = await _async_trampoline(await _async_eval(expr[3], env, ctx))
async def _call_thunk(fn):
if isinstance(fn, Lambda):
return await _async_trampoline(await _async_call_lambda(fn, [], env, ctx))
if callable(fn):
r = fn()
if inspect.iscoroutine(r):
return await r
return r
raise EvalError(f"dynamic-wind: expected thunk, got {type(fn).__name__}")
await _call_thunk(before)
try:
result = await _call_thunk(body_fn)
finally:
await _call_thunk(after)
return result
async def _asf_lambda(expr, env, ctx):
params_expr = expr[1]
param_names = []
@@ -467,6 +563,7 @@ _ASYNC_SPECIAL_FORMS: dict[str, Any] = {
"or": _asf_or,
"let": _asf_let,
"let*": _asf_let,
"letrec": _asf_letrec,
"lambda": _asf_lambda,
"fn": _asf_lambda,
"define": _asf_define,
@@ -481,9 +578,52 @@ _ASYNC_SPECIAL_FORMS: dict[str, Any] = {
"quasiquote": _asf_quasiquote,
"->": _asf_thread_first,
"set!": _asf_set_bang,
"dynamic-wind": _asf_dynamic_wind,
}
# ---------------------------------------------------------------------------
# Async delimited continuations — shift / reset
# ---------------------------------------------------------------------------
_ASYNC_RESET_RESUME: list = []
async def _asf_reset(expr, env, ctx):
"""(reset body) — async version."""
from .types import Continuation, _ShiftSignal
body = expr[1]
try:
return await async_eval(body, env, ctx)
except _ShiftSignal as sig:
def cont_fn(value=None):
from .types import NIL
_ASYNC_RESET_RESUME.append(value if value is not None else NIL)
try:
# Sync re-evaluation; the async caller will trampoline
from .evaluator import _eval as sync_eval, _trampoline
return _trampoline(sync_eval(body, env))
finally:
_ASYNC_RESET_RESUME.pop()
k = Continuation(cont_fn)
sig_env = dict(sig.env)
sig_env[sig.k_name] = k
return await async_eval(sig.body, sig_env, ctx)
async def _asf_shift(expr, env, ctx):
"""(shift k body) — async version."""
from .types import _ShiftSignal
if _ASYNC_RESET_RESUME:
return _ASYNC_RESET_RESUME[-1]
k_name = expr[1].name
body = expr[2]
raise _ShiftSignal(k_name, body, env)
_ASYNC_SPECIAL_FORMS["reset"] = _asf_reset
_ASYNC_SPECIAL_FORMS["shift"] = _asf_shift
# ---------------------------------------------------------------------------
# Async higher-order forms
# ---------------------------------------------------------------------------

144
shared/sx/boundary.py Normal file
View File

@@ -0,0 +1,144 @@
"""
SX Boundary Enforcement — runtime validation.
Reads declarations from boundary.sx + primitives.sx and validates
that all registered primitives, I/O handlers, and page helpers
are declared in the spec.
Controlled by SX_BOUNDARY_STRICT env var:
- "1": validation raises errors (fail fast)
- anything else: validation logs warnings
"""
from __future__ import annotations
import logging
import os
from typing import Any
logger = logging.getLogger("sx.boundary")
# ---------------------------------------------------------------------------
# Lazy-loaded declaration sets (populated on first use)
# ---------------------------------------------------------------------------
_DECLARED_PURE: frozenset[str] | None = None
_DECLARED_IO: frozenset[str] | None = None
_DECLARED_HELPERS: dict[str, frozenset[str]] | None = None
def _load_declarations() -> None:
global _DECLARED_PURE, _DECLARED_IO, _DECLARED_HELPERS
if _DECLARED_PURE is not None:
return
try:
from .ref.boundary_parser import parse_primitives_sx, parse_boundary_sx
_DECLARED_PURE = parse_primitives_sx()
_DECLARED_IO, _DECLARED_HELPERS = parse_boundary_sx()
logger.debug(
"Boundary loaded: %d pure, %d io, %d services",
len(_DECLARED_PURE), len(_DECLARED_IO), len(_DECLARED_HELPERS),
)
except Exception as e:
logger.warning("Failed to load boundary declarations: %s", e)
_DECLARED_PURE = frozenset()
_DECLARED_IO = frozenset()
_DECLARED_HELPERS = {}
def _is_strict() -> bool:
return os.environ.get("SX_BOUNDARY_STRICT") == "1"
def _report(message: str) -> None:
if _is_strict():
raise RuntimeError(f"SX boundary violation: {message}")
else:
logger.warning("SX boundary: %s", message)
# ---------------------------------------------------------------------------
# Validation functions
# ---------------------------------------------------------------------------
def validate_primitive(name: str) -> None:
"""Validate that a pure primitive is declared in primitives.sx."""
_load_declarations()
assert _DECLARED_PURE is not None
if name not in _DECLARED_PURE:
_report(f"Undeclared pure primitive: {name!r}. Add to primitives.sx.")
def validate_io(name: str) -> None:
"""Validate that an I/O primitive is declared in boundary.sx."""
_load_declarations()
assert _DECLARED_IO is not None
if name not in _DECLARED_IO:
_report(f"Undeclared I/O primitive: {name!r}. Add to boundary.sx.")
def validate_helper(service: str, name: str) -> None:
"""Validate that a page helper is declared in boundary.sx."""
_load_declarations()
assert _DECLARED_HELPERS is not None
svc_helpers = _DECLARED_HELPERS.get(service, frozenset())
if name not in svc_helpers:
_report(
f"Undeclared page helper: {name!r} for service {service!r}. "
f"Add to boundary.sx."
)
def validate_boundary_value(value: Any, context: str = "") -> None:
"""Validate that a value is an allowed SX boundary type.
Allowed: int, float, str, bool, None/NIL, list, dict, SxExpr, StyleValue.
NOT allowed: datetime, ORM models, Quart objects, raw callables.
"""
from .types import NIL, StyleValue
from .parser import SxExpr
if value is None or value is NIL:
return
if isinstance(value, (int, float, str, bool)):
return
if isinstance(value, SxExpr):
return
if isinstance(value, StyleValue):
return
if isinstance(value, list):
for item in value:
validate_boundary_value(item, context)
return
if isinstance(value, dict):
for k, v in value.items():
validate_boundary_value(v, context)
return
type_name = type(value).__name__
ctx_msg = f" (in {context})" if context else ""
_report(
f"Non-SX type crossing boundary{ctx_msg}: {type_name}. "
f"Convert to dict/string at the edge."
)
# ---------------------------------------------------------------------------
# Declaration accessors (for introspection / bootstrapper use)
# ---------------------------------------------------------------------------
def declared_pure() -> frozenset[str]:
_load_declarations()
assert _DECLARED_PURE is not None
return _DECLARED_PURE
def declared_io() -> frozenset[str]:
_load_declarations()
assert _DECLARED_IO is not None
return _DECLARED_IO
def declared_helpers() -> dict[str, frozenset[str]]:
_load_declarations()
assert _DECLARED_HELPERS is not None
return dict(_DECLARED_HELPERS)

View File

@@ -33,7 +33,7 @@ from __future__ import annotations
from typing import Any
from .types import Component, HandlerDef, Keyword, Lambda, Macro, NIL, PageDef, RelationDef, Symbol
from .types import Component, Continuation, HandlerDef, Keyword, Lambda, Macro, NIL, PageDef, RelationDef, Symbol, _ShiftSignal
from .primitives import _PRIMITIVES
@@ -306,6 +306,11 @@ def _sf_or(expr: list, env: dict) -> Any:
def _sf_let(expr: list, env: dict) -> Any:
if len(expr) < 3:
raise EvalError("let requires bindings and body")
# Named let: (let name ((x 0) ...) body)
if isinstance(expr[1], Symbol):
return _sf_named_let(expr, env)
bindings = expr[1]
local = dict(env)
@@ -336,6 +341,127 @@ def _sf_let(expr: list, env: dict) -> Any:
return _Thunk(body[-1], local)
def _sf_named_let(expr: list, env: dict) -> Any:
"""``(let name ((x 0) (y 1)) body...)`` — self-recursive loop.
Desugars to a lambda bound to *name* whose closure includes itself,
called with the initial values. Tail calls to *name* produce TCO thunks.
"""
loop_name = expr[1].name
bindings = expr[2]
body = expr[3:]
params: list[str] = []
inits: list[Any] = []
if isinstance(bindings, list):
if bindings and isinstance(bindings[0], list):
for binding in bindings:
var = binding[0]
params.append(var.name if isinstance(var, Symbol) else var)
inits.append(binding[1])
elif len(bindings) % 2 == 0:
for i in range(0, len(bindings), 2):
var = bindings[i]
params.append(var.name if isinstance(var, Symbol) else var)
inits.append(bindings[i + 1])
# Build loop body (wrap in begin if multiple expressions)
loop_body = body[0] if len(body) == 1 else [Symbol("begin")] + list(body)
# Create self-recursive lambda
loop_fn = Lambda(params, loop_body, dict(env), name=loop_name)
loop_fn.closure[loop_name] = loop_fn
# Evaluate initial values in enclosing env, then call
init_vals = [_trampoline(_eval(init, env)) for init in inits]
return _call_lambda(loop_fn, init_vals, env)
def _sf_letrec(expr: list, env: dict) -> Any:
"""``(letrec ((name1 val1) ...) body)`` — mutually recursive bindings.
All names are bound to NIL first, then values are evaluated (so they
can reference each other), then lambda closures are patched.
"""
if len(expr) < 3:
raise EvalError("letrec requires bindings and body")
bindings = expr[1]
local = dict(env)
names: list[str] = []
val_exprs: list[Any] = []
if isinstance(bindings, list):
if bindings and isinstance(bindings[0], list):
for binding in bindings:
var = binding[0]
vname = var.name if isinstance(var, Symbol) else var
names.append(vname)
val_exprs.append(binding[1])
local[vname] = NIL
elif len(bindings) % 2 == 0:
for i in range(0, len(bindings), 2):
var = bindings[i]
vname = var.name if isinstance(var, Symbol) else var
names.append(vname)
val_exprs.append(bindings[i + 1])
local[vname] = NIL
# Evaluate all values — they can see each other's names (initially NIL)
values = [_trampoline(_eval(ve, local)) for ve in val_exprs]
# Bind final values
for name, val in zip(names, values):
local[name] = val
# Patch lambda closures so they see the final bindings
for val in values:
if isinstance(val, Lambda):
for name in names:
val.closure[name] = local[name]
body = expr[2:]
for body_expr in body[:-1]:
_trampoline(_eval(body_expr, local))
return _Thunk(body[-1], local)
def _sf_dynamic_wind(expr: list, env: dict) -> Any:
"""``(dynamic-wind before body after)`` — entry/exit guards.
All three arguments are thunks (zero-arg functions).
*before* is called on entry, *after* is always called on exit (even on
error). The wind stack is maintained for future continuation support.
"""
if len(expr) != 4:
raise EvalError("dynamic-wind requires 3 arguments (before, body, after)")
before = _trampoline(_eval(expr[1], env))
body_fn = _trampoline(_eval(expr[2], env))
after = _trampoline(_eval(expr[3], env))
def _call_thunk(fn: Any) -> Any:
if isinstance(fn, Lambda):
return _trampoline(_call_lambda(fn, [], env))
if callable(fn):
return fn()
raise EvalError(f"dynamic-wind: expected thunk, got {type(fn).__name__}")
# Entry
_call_thunk(before)
_WIND_STACK.append((before, after))
try:
result = _call_thunk(body_fn)
finally:
_WIND_STACK.pop()
_call_thunk(after)
return result
# Wind stack for dynamic-wind (thread-safe enough for sync evaluator)
_WIND_STACK: list[tuple] = []
def _sf_lambda(expr: list, env: dict) -> Lambda:
if len(expr) < 3:
raise EvalError("lambda requires params and body")
@@ -874,6 +1000,42 @@ def _sf_defpage(expr: list, env: dict) -> PageDef:
return page
# ---------------------------------------------------------------------------
# Delimited continuations — shift / reset
# ---------------------------------------------------------------------------
_RESET_RESUME = [] # stack of resume values; empty = not resuming
_RESET_SENTINEL = object()
def _sf_reset(expr, env):
"""(reset body) — establish a continuation delimiter."""
body = expr[1]
try:
return _trampoline(_eval(body, env))
except _ShiftSignal as sig:
def cont_fn(value=NIL):
_RESET_RESUME.append(value)
try:
return _trampoline(_eval(body, env))
finally:
_RESET_RESUME.pop()
k = Continuation(cont_fn)
sig_env = dict(sig.env)
sig_env[sig.k_name] = k
return _trampoline(_eval(sig.body, sig_env))
def _sf_shift(expr, env):
"""(shift k body) — capture continuation to nearest reset."""
if _RESET_RESUME:
return _RESET_RESUME[-1]
k_name = expr[1].name # symbol
body = expr[2]
raise _ShiftSignal(k_name, body, env)
_SPECIAL_FORMS: dict[str, Any] = {
"if": _sf_if,
"when": _sf_when,
@@ -883,6 +1045,7 @@ _SPECIAL_FORMS: dict[str, Any] = {
"or": _sf_or,
"let": _sf_let,
"let*": _sf_let,
"letrec": _sf_letrec,
"lambda": _sf_lambda,
"fn": _sf_lambda,
"define": _sf_define,
@@ -895,12 +1058,15 @@ _SPECIAL_FORMS: dict[str, Any] = {
"quote": _sf_quote,
"->": _sf_thread_first,
"set!": _sf_set_bang,
"dynamic-wind": _sf_dynamic_wind,
"defmacro": _sf_defmacro,
"quasiquote": _sf_quasiquote,
"defhandler": _sf_defhandler,
"defpage": _sf_defpage,
"defquery": _sf_defquery,
"defaction": _sf_defaction,
"reset": _sf_reset,
"shift": _sf_shift,
}
@@ -913,9 +1079,11 @@ def _ho_map(expr: list, env: dict) -> list:
raise EvalError("map requires fn and collection")
fn = _trampoline(_eval(expr[1], env))
coll = _trampoline(_eval(expr[2], env))
if not isinstance(fn, Lambda):
raise EvalError(f"map requires lambda, got {type(fn).__name__}")
return [_trampoline(_call_lambda(fn, [item], env)) for item in coll]
if isinstance(fn, Lambda):
return [_trampoline(_call_lambda(fn, [item], env)) for item in coll]
if callable(fn):
return [fn(item) for item in coll]
raise EvalError(f"map requires lambda, got {type(fn).__name__}")
def _ho_map_indexed(expr: list, env: dict) -> list:

View File

@@ -69,7 +69,11 @@ def clear_handlers(service: str | None = None) -> None:
def load_handler_file(filepath: str, service_name: str) -> list[HandlerDef]:
"""Parse an .sx file, evaluate it, and register any HandlerDef values."""
from .parser import parse_all
from .evaluator import _eval as _raw_eval, _trampoline
import os
if os.environ.get("SX_USE_REF") == "1":
from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
else:
from .evaluator import _eval as _raw_eval, _trampoline
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
from .jinja_bridge import get_component_env
@@ -126,7 +130,11 @@ async def execute_handler(
4. Return ``SxExpr`` wire format
"""
from .jinja_bridge import get_component_env, _get_request_context
from .async_eval import async_eval_to_sx
import os
if os.environ.get("SX_USE_REF") == "1":
from .ref.async_eval_ref import async_eval_to_sx
else:
from .async_eval import async_eval_to_sx
from .types import NIL
if args is None:

View File

@@ -16,6 +16,16 @@ from .page import SEARCH_HEADERS_MOBILE, SEARCH_HEADERS_DESKTOP
from .parser import SxExpr
def _sx_fragment(*parts: str) -> SxExpr:
"""Wrap pre-rendered SX wire format strings in a fragment.
Infrastructure utility for composing already-serialized SX strings.
NOT for building SX from Python data — use sx_call() or _render_to_sx().
"""
joined = " ".join(p for p in parts if p)
return SxExpr(f"(<> {joined})") if joined else SxExpr("")
def call_url(ctx: dict, key: str, path: str = "/") -> str:
"""Call a URL helper from context (e.g., blog_url, account_url)."""
fn = ctx.get(key)
@@ -51,8 +61,7 @@ def _as_sx(val: Any) -> SxExpr | None:
if isinstance(val, SxExpr):
return val if val.source else None
html = str(val)
escaped = html.replace("\\", "\\\\").replace('"', '\\"')
return SxExpr(f'(~rich-text :html "{escaped}")')
return sx_call("rich-text", html=html)
async def root_header_sx(ctx: dict, *, oob: bool = False) -> str:
@@ -77,7 +86,7 @@ async def root_header_sx(ctx: dict, *, oob: bool = False) -> str:
def mobile_menu_sx(*sections: str) -> SxExpr:
"""Assemble mobile menu from pre-built sections (deepest first)."""
parts = [s for s in sections if s]
return SxExpr("(<> " + " ".join(parts) + ")") if parts else SxExpr("")
return _sx_fragment(*parts) if parts else SxExpr("")
async def mobile_root_nav_sx(ctx: dict) -> str:
@@ -130,7 +139,7 @@ async def _post_nav_items_sx(ctx: dict) -> SxExpr:
is_admin_page=is_admin_page or None)
if admin_nav:
parts.append(admin_nav)
return SxExpr("(<> " + " ".join(parts) + ")") if parts else SxExpr("")
return _sx_fragment(*parts) if parts else SxExpr("")
async def _post_admin_nav_items_sx(ctx: dict, slug: str,
@@ -158,7 +167,7 @@ async def _post_admin_nav_items_sx(ctx: dict, slug: str,
parts.append(await _render_to_sx("nav-link", href=href, label=label,
select_colours=select_colours,
is_selected=is_sel or None))
return SxExpr("(<> " + " ".join(parts) + ")") if parts else SxExpr("")
return _sx_fragment(*parts) if parts else SxExpr("")
# ---------------------------------------------------------------------------
@@ -265,7 +274,7 @@ async def oob_header_sx(parent_id: str, child_id: str, row_sx: str) -> str:
async def header_child_sx(inner_sx: str, *, id: str = "root-header-child") -> str:
"""Wrap inner sx in a header-child div."""
return await _render_to_sx("header-child-sx",
id=id, inner=SxExpr(f"(<> {inner_sx})"),
id=id, inner=_sx_fragment(inner_sx),
)
@@ -273,7 +282,7 @@ async def oob_page_sx(*, oobs: str = "", filter: str = "", aside: str = "",
content: str = "", menu: str = "") -> str:
"""Build OOB response as sx wire format."""
return await _render_to_sx("oob-sx",
oobs=SxExpr(f"(<> {oobs})") if oobs else None,
oobs=_sx_fragment(oobs) if oobs else None,
filter=SxExpr(filter) if filter else None,
aside=SxExpr(aside) if aside else None,
menu=SxExpr(menu) if menu else None,
@@ -294,7 +303,7 @@ async def full_page_sx(ctx: dict, *, header_rows: str,
if not menu:
menu = await mobile_root_nav_sx(ctx)
body_sx = await _render_to_sx("app-body",
header_rows=SxExpr(f"(<> {header_rows})") if header_rows else None,
header_rows=_sx_fragment(header_rows) if header_rows else None,
filter=SxExpr(filter) if filter else None,
aside=SxExpr(aside) if aside else None,
menu=SxExpr(menu) if menu else None,
@@ -303,7 +312,7 @@ async def full_page_sx(ctx: dict, *, header_rows: str,
if meta:
# Wrap body + meta in a fragment so sx.js renders both;
# auto-hoist moves meta/title/link elements to <head>.
body_sx = "(<> " + meta + " " + body_sx + ")"
body_sx = _sx_fragment(meta, body_sx)
return sx_page(ctx, body_sx, meta_html=meta_html)
@@ -346,7 +355,11 @@ async def _render_to_sx_with_env(__name: str, extra_env: dict, **kwargs: Any) ->
**Private** — service code should use ``sx_call()`` or defmacros instead.
"""
from .jinja_bridge import get_component_env, _get_request_context
from .async_eval import async_eval_slot_to_sx
import os
if os.environ.get("SX_USE_REF") == "1":
from .ref.async_eval_ref import async_eval_slot_to_sx
else:
from .async_eval import async_eval_slot_to_sx
from .types import Symbol, Keyword, NIL as _NIL
# Build AST with extra_env entries as keyword args so _aser_component
@@ -377,7 +390,11 @@ async def _render_to_sx(__name: str, **kwargs: Any) -> str:
Only infrastructure code (helpers.py, layouts.py) should call this.
"""
from .jinja_bridge import get_component_env, _get_request_context
from .async_eval import async_eval_to_sx
import os
if os.environ.get("SX_USE_REF") == "1":
from .ref.async_eval_ref import async_eval_to_sx
else:
from .async_eval import async_eval_to_sx
ast = _build_component_ast(__name, **kwargs)
env = dict(get_component_env())
@@ -397,7 +414,11 @@ async def render_to_html(__name: str, **kwargs: Any) -> str:
format. Used by route renders that need HTML (full pages, fragments).
"""
from .jinja_bridge import get_component_env, _get_request_context
from .async_eval import async_render
import os
if os.environ.get("SX_USE_REF") == "1":
from .ref.async_eval_ref import async_render
else:
from .async_eval import async_render
ast = _build_component_ast(__name, **kwargs)
env = dict(get_component_env())

View File

@@ -27,7 +27,11 @@ from typing import Any
from .types import NIL, Component, Keyword, Macro, Symbol
from .parser import parse
from .html import render as html_render, _render_component
import os as _os
if _os.environ.get("SX_USE_REF") == "1":
from .ref.sx_ref import render as html_render, render_html_component as _render_component
else:
from .html import render as html_render, _render_component
# ---------------------------------------------------------------------------
@@ -169,7 +173,10 @@ def register_components(sx_source: str) -> None:
(div :class "..." (div :class "..." title)))))
''')
"""
from .evaluator import _eval as _raw_eval, _trampoline
if _os.environ.get("SX_USE_REF") == "1":
from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
else:
from .evaluator import _eval as _raw_eval, _trampoline
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
from .parser import parse_all
from .css_registry import scan_classes_from_sx

View File

@@ -79,9 +79,34 @@ def register_page_helpers(service: str, helpers: dict[str, Any]) -> None:
:auth :public
:content (docs-content slug))
"""
from .boundary import validate_helper, validate_boundary_value
import asyncio
import functools
for name in helpers:
validate_helper(service, name)
# Wrap helpers to validate return values at the boundary
wrapped: dict[str, Any] = {}
for name, fn in helpers.items():
if asyncio.iscoroutinefunction(fn):
@functools.wraps(fn)
async def _async_wrap(*a, _fn=fn, _name=name, **kw):
result = await _fn(*a, **kw)
validate_boundary_value(result, context=f"helper {_name!r}")
return result
wrapped[name] = _async_wrap
else:
@functools.wraps(fn)
def _sync_wrap(*a, _fn=fn, _name=name, **kw):
result = _fn(*a, **kw)
validate_boundary_value(result, context=f"helper {_name!r}")
return result
wrapped[name] = _sync_wrap
if service not in _PAGE_HELPERS:
_PAGE_HELPERS[service] = {}
_PAGE_HELPERS[service].update(helpers)
_PAGE_HELPERS[service].update(wrapped)
def get_page_helpers(service: str) -> dict[str, Any]:

View File

@@ -83,7 +83,7 @@ class Tokenizer:
COMMENT = re.compile(r";[^\n]*")
STRING = re.compile(r'"(?:[^"\\]|\\.)*"')
NUMBER = re.compile(r"-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?")
KEYWORD = re.compile(r":[a-zA-Z_][a-zA-Z0-9_>:-]*")
KEYWORD = re.compile(r":[a-zA-Z_~*+\-><=/!?&\[]{1}[a-zA-Z0-9_~*+\-><=/!?.:&/\[\]#,]*")
# Symbols may start with alpha, _, or common operator chars, plus ~ for components,
# <> for the fragment symbol, and & for &key/&rest.
SYMBOL = re.compile(r"[a-zA-Z_~*+\-><=/!?&][a-zA-Z0-9_~*+\-><=/!?.:&]*")
@@ -170,6 +170,11 @@ class Tokenizer:
return float(num_str)
return int(num_str)
# Ellipsis (... as a symbol, used in spec declarations)
if char == "." and self.text[self.pos:self.pos + 3] == "...":
self._advance(3)
return Symbol("...")
# Symbol
m = self.SYMBOL.match(self.text, self.pos)
if m:

View File

@@ -10,7 +10,7 @@ from __future__ import annotations
import math
from typing import Any, Callable
from .types import Keyword, Lambda, NIL
from .types import Keyword, NIL
# ---------------------------------------------------------------------------
@@ -30,6 +30,8 @@ def register_primitive(name: str):
return "".join(str(a) for a in args)
"""
def decorator(fn: Callable) -> Callable:
from .boundary import validate_primitive
validate_primitive(name)
_PRIMITIVES[name] = fn
return fn
return decorator
@@ -132,6 +134,27 @@ def prim_eq(a: Any, b: Any) -> bool:
def prim_neq(a: Any, b: Any) -> bool:
return a != b
@register_primitive("eq?")
def prim_eq_identity(a: Any, b: Any) -> bool:
"""Identity equality — true only if a and b are the same object."""
return a is b
@register_primitive("eqv?")
def prim_eqv(a: Any, b: Any) -> bool:
"""Equivalent: identity for compound types, value for atoms."""
if a is b:
return True
if isinstance(a, (int, float, str, bool)) and isinstance(b, type(a)):
return a == b
if (a is None or a is NIL) and (b is None or b is NIL):
return True
return False
@register_primitive("equal?")
def prim_equal(a: Any, b: Any) -> bool:
"""Deep structural equality (same as =)."""
return a == b
@register_primitive("<")
def prim_lt(a: Any, b: Any) -> bool:
return a < b
@@ -185,6 +208,11 @@ def prim_is_list(x: Any) -> bool:
def prim_is_dict(x: Any) -> bool:
return isinstance(x, dict)
@register_primitive("continuation?")
def prim_is_continuation(x: Any) -> bool:
from .types import Continuation
return isinstance(x, Continuation)
@register_primitive("empty?")
def prim_is_empty(coll: Any) -> bool:
if coll is None or coll is NIL:
@@ -263,12 +291,6 @@ def prim_join(sep: str, coll: list) -> str:
def prim_replace(s: str, old: str, new: str) -> str:
return s.replace(old, new)
@register_primitive("strip-tags")
def prim_strip_tags(s: str) -> str:
"""Strip HTML tags from a string."""
import re
return re.sub(r"<[^>]+>", "", s)
@register_primitive("slice")
def prim_slice(coll: Any, start: int, end: Any = None) -> Any:
"""Slice a string or list: (slice coll start end?)."""
@@ -430,186 +452,3 @@ def prim_into(target: Any, coll: Any) -> Any:
return result
raise ValueError(f"into: unsupported target type {type(target).__name__}")
# ---------------------------------------------------------------------------
# URL helpers
# ---------------------------------------------------------------------------
@register_primitive("app-url")
def prim_app_url(service: str, path: str = "/") -> str:
"""``(app-url "blog" "/my-post/")`` → full URL for service."""
from shared.infrastructure.urls import app_url
return app_url(service, path)
@register_primitive("url-for")
def prim_url_for(endpoint: str, **kwargs: Any) -> str:
"""``(url-for "endpoint")`` → quart.url_for."""
from quart import url_for
return url_for(endpoint, **kwargs)
@register_primitive("asset-url")
def prim_asset_url(path: str = "") -> str:
"""``(asset-url "/img/logo.png")`` → versioned static URL."""
from shared.infrastructure.urls import asset_url
return asset_url(path)
@register_primitive("config")
def prim_config(key: str) -> Any:
"""``(config "key")`` → shared.config.config()[key]."""
from shared.config import config
cfg = config()
return cfg.get(key)
@register_primitive("jinja-global")
def prim_jinja_global(key: str, default: Any = None) -> Any:
"""``(jinja-global "key")`` → current_app.jinja_env.globals[key]."""
from quart import current_app
return current_app.jinja_env.globals.get(key, default)
@register_primitive("relations-from")
def prim_relations_from(entity_type: str) -> list[dict]:
"""``(relations-from "page")`` → list of RelationDef dicts."""
from shared.sx.relations import relations_from
return [
{
"name": d.name, "from_type": d.from_type, "to_type": d.to_type,
"cardinality": d.cardinality, "nav": d.nav,
"nav_icon": d.nav_icon, "nav_label": d.nav_label,
}
for d in relations_from(entity_type)
]
# ---------------------------------------------------------------------------
# Format helpers
# ---------------------------------------------------------------------------
@register_primitive("format-date")
def prim_format_date(date_str: Any, fmt: str) -> str:
"""``(format-date date-str fmt)`` → formatted date string."""
from datetime import datetime
try:
dt = datetime.fromisoformat(str(date_str))
return dt.strftime(fmt)
except (ValueError, TypeError):
return str(date_str) if date_str else ""
@register_primitive("format-decimal")
def prim_format_decimal(val: Any, places: Any = 2) -> str:
"""``(format-decimal val places)`` → formatted decimal string."""
try:
return f"{float(val):.{int(places)}f}"
except (ValueError, TypeError):
return "0." + "0" * int(places)
@register_primitive("parse-int")
def prim_parse_int(val: Any, default: Any = 0) -> int | Any:
"""``(parse-int val default?)`` → int(val) with fallback."""
try:
return int(val)
except (ValueError, TypeError):
return default
@register_primitive("parse-datetime")
def prim_parse_datetime(val: Any) -> Any:
"""``(parse-datetime "2024-01-15T10:00:00")`` → datetime object."""
from datetime import datetime
if not val or val is NIL:
return NIL
return datetime.fromisoformat(str(val))
@register_primitive("split-ids")
def prim_split_ids(val: Any) -> list[int]:
"""``(split-ids "1,2,3")`` → [1, 2, 3]. Parse comma-separated int IDs."""
if not val or val is NIL:
return []
return [int(x.strip()) for x in str(val).split(",") if x.strip()]
# ---------------------------------------------------------------------------
# Assertions
# ---------------------------------------------------------------------------
@register_primitive("assert")
def prim_assert(condition: Any, message: str = "Assertion failed") -> bool:
if not condition:
raise RuntimeError(f"Assertion error: {message}")
return True
# ---------------------------------------------------------------------------
# Text helpers
# ---------------------------------------------------------------------------
@register_primitive("pluralize")
def prim_pluralize(count: Any, singular: str = "", plural: str = "s") -> str:
"""``(pluralize count)`` → "s" if count != 1, else "".
``(pluralize count "item" "items")`` → "item" or "items"."""
try:
n = int(count)
except (ValueError, TypeError):
n = 0
if singular or plural != "s":
return singular if n == 1 else plural
return "" if n == 1 else "s"
@register_primitive("escape")
def prim_escape(s: Any) -> str:
"""``(escape val)`` → HTML-escaped string."""
from markupsafe import escape as _escape
return str(_escape(str(s) if s is not None and s is not NIL else ""))
@register_primitive("route-prefix")
def prim_route_prefix() -> str:
"""``(route-prefix)`` → service URL prefix for dev/prod routing."""
from shared.utils import route_prefix
return route_prefix()
# ---------------------------------------------------------------------------
# Style primitives
# ---------------------------------------------------------------------------
@register_primitive("css")
def prim_css(*args: Any) -> Any:
"""``(css :flex :gap-4 :hover:bg-sky-200)`` → StyleValue.
Accepts keyword atoms (strings without colon prefix) and runtime
strings. Returns a StyleValue with a content-addressed class name
and all resolved CSS declarations.
"""
from .style_resolver import resolve_style
atoms = tuple(
(a.name if isinstance(a, Keyword) else str(a))
for a in args if a is not None and a is not NIL and a is not False
)
if not atoms:
return NIL
return resolve_style(atoms)
@register_primitive("merge-styles")
def prim_merge_styles(*styles: Any) -> Any:
"""``(merge-styles style1 style2)`` → merged StyleValue.
Merges multiple StyleValues; later declarations win.
"""
from .types import StyleValue
from .style_resolver import merge_styles
valid = [s for s in styles if isinstance(s, StyleValue)]
if not valid:
return NIL
if len(valid) == 1:
return valid[0]
return merge_styles(valid)

544
shared/sx/primitives_ctx.py Normal file
View File

@@ -0,0 +1,544 @@
"""
Service-specific page context IO handlers.
These are application-specific (rose-ash), not part of the generic SX
framework. Each handler builds a dict of template data from Quart request
context for use by .sx page components.
"""
from __future__ import annotations
from typing import Any
from .primitives_io import register_io_handler
# ---------------------------------------------------------------------------
# Root / post headers
# ---------------------------------------------------------------------------
@register_io_handler("root-header-ctx")
async def _io_root_header_ctx(
args: list[Any], kwargs: dict[str, Any], ctx: Any
) -> dict[str, Any]:
"""``(root-header-ctx)`` → dict with all root header values.
Fetches cart-mini, auth-menu, nav-tree fragments and computes
settings-url / is-admin from rights. Result is cached on ``g``
per request so multiple calls (e.g. header + mobile) are free.
"""
from quart import g, current_app, request
cached = getattr(g, "_root_header_ctx", None)
if cached is not None:
return cached
from shared.infrastructure.fragments import fetch_fragments
from shared.infrastructure.cart_identity import current_cart_identity
from shared.infrastructure.urls import app_url
from shared.config import config
from .types import NIL
user = getattr(g, "user", None)
ident = current_cart_identity()
cart_params: dict[str, Any] = {}
if ident["user_id"] is not None:
cart_params["user_id"] = ident["user_id"]
if ident["session_id"] is not None:
cart_params["session_id"] = ident["session_id"]
auth_params: dict[str, Any] = {}
if user and getattr(user, "email", None):
auth_params["email"] = user.email
nav_params = {"app_name": current_app.name, "path": request.path}
cart_mini, auth_menu, nav_tree = await fetch_fragments([
("cart", "cart-mini", cart_params or None),
("account", "auth-menu", auth_params or None),
("blog", "nav-tree", nav_params),
])
rights = getattr(g, "rights", None) or {}
is_admin = (
rights.get("admin", False)
if isinstance(rights, dict)
else getattr(rights, "admin", False)
)
result = {
"cart-mini": cart_mini or NIL,
"blog-url": app_url("blog", ""),
"site-title": config()["title"],
"app-label": current_app.name,
"nav-tree": nav_tree or NIL,
"auth-menu": auth_menu or NIL,
"nav-panel": NIL,
"settings-url": app_url("blog", "/settings/") if is_admin else "",
"is-admin": is_admin,
}
g._root_header_ctx = result
return result
@register_io_handler("post-header-ctx")
async def _io_post_header_ctx(
args: list[Any], kwargs: dict[str, Any], ctx: Any
) -> dict[str, Any]:
"""``(post-header-ctx)`` → dict with post-level header values."""
from quart import g, request
cached = getattr(g, "_post_header_ctx", None)
if cached is not None:
return cached
from shared.infrastructure.urls import app_url
from .types import NIL
from .parser import SxExpr
dctx = getattr(g, "_defpage_ctx", None) or {}
post = dctx.get("post") or {}
slug = post.get("slug", "")
if not slug:
result: dict[str, Any] = {"slug": ""}
g._post_header_ctx = result
return result
title = (post.get("title") or "")[:160]
feature_image = post.get("feature_image") or NIL
# Container nav (pre-fetched by page helper into defpage ctx)
raw_nav = dctx.get("container_nav") or ""
container_nav: Any = NIL
nav_str = str(raw_nav).strip()
if nav_str and nav_str.replace("(<>", "").replace(")", "").strip():
if isinstance(raw_nav, SxExpr):
container_nav = raw_nav
else:
container_nav = SxExpr(nav_str)
page_cart_count = dctx.get("page_cart_count", 0) or 0
rights = getattr(g, "rights", None) or {}
is_admin = (
rights.get("admin", False)
if isinstance(rights, dict)
else getattr(rights, "admin", False)
)
is_admin_page = dctx.get("is_admin_section") or "/admin" in request.path
from quart import current_app
select_colours = current_app.jinja_env.globals.get("select_colours", "")
result = {
"slug": slug,
"title": title,
"feature-image": feature_image,
"link-href": app_url("blog", f"/{slug}/"),
"container-nav": container_nav,
"page-cart-count": page_cart_count,
"cart-href": app_url("cart", f"/{slug}/") if page_cart_count else "",
"admin-href": app_url("blog", f"/{slug}/admin/"),
"is-admin": is_admin,
"is-admin-page": is_admin_page or NIL,
"select-colours": select_colours,
}
g._post_header_ctx = result
return result
# ---------------------------------------------------------------------------
# Cart
# ---------------------------------------------------------------------------
@register_io_handler("cart-page-ctx")
async def _io_cart_page_ctx(
args: list[Any], kwargs: dict[str, Any], ctx: Any
) -> dict[str, Any]:
"""``(cart-page-ctx)`` → dict with cart page header values."""
from quart import g
from .types import NIL
from shared.infrastructure.urls import app_url
page_post = getattr(g, "page_post", None)
if not page_post:
return {"slug": "", "title": "", "feature-image": NIL, "cart-url": "/"}
slug = getattr(page_post, "slug", "") or ""
title = (getattr(page_post, "title", "") or "")[:160]
feature_image = getattr(page_post, "feature_image", None) or NIL
return {
"slug": slug,
"title": title,
"feature-image": feature_image,
"page-cart-url": app_url("cart", f"/{slug}/"),
"cart-url": app_url("cart", "/"),
}
# ---------------------------------------------------------------------------
# Events
# ---------------------------------------------------------------------------
@register_io_handler("events-calendar-ctx")
async def _io_events_calendar_ctx(
args: list[Any], kwargs: dict[str, Any], ctx: Any
) -> dict[str, Any]:
"""``(events-calendar-ctx)`` → dict with events calendar header values."""
from quart import g
cal = getattr(g, "calendar", None)
if not cal:
dctx = getattr(g, "_defpage_ctx", None) or {}
cal = dctx.get("calendar")
if not cal:
return {"slug": ""}
return {
"slug": getattr(cal, "slug", "") or "",
"name": getattr(cal, "name", "") or "",
"description": getattr(cal, "description", "") or "",
}
@register_io_handler("events-day-ctx")
async def _io_events_day_ctx(
args: list[Any], kwargs: dict[str, Any], ctx: Any
) -> dict[str, Any]:
"""``(events-day-ctx)`` → dict with events day header values."""
from quart import g, url_for
from .types import NIL
from .parser import SxExpr
dctx = getattr(g, "_defpage_ctx", None) or {}
cal = getattr(g, "calendar", None) or dctx.get("calendar")
day_date = dctx.get("day_date") or getattr(g, "day_date", None)
if not cal or not day_date:
return {"date-str": ""}
cal_slug = getattr(cal, "slug", "") or ""
# Build confirmed entries nav
confirmed = dctx.get("confirmed_entries") or []
rights = getattr(g, "rights", None) or {}
is_admin = (
rights.get("admin", False)
if isinstance(rights, dict)
else getattr(rights, "admin", False)
)
from .helpers import sx_call
nav_parts: list[str] = []
if confirmed:
entry_links = []
for entry in confirmed:
href = url_for(
"calendar.day.calendar_entries.calendar_entry.get",
calendar_slug=cal_slug,
year=day_date.year, month=day_date.month, day=day_date.day,
entry_id=entry.id,
)
start = entry.start_at.strftime("%H:%M") if entry.start_at else ""
end = (
f" \u2013 {entry.end_at.strftime('%H:%M')}"
if entry.end_at else ""
)
entry_links.append(sx_call(
"events-day-entry-link",
href=href, name=entry.name, time_str=f"{start}{end}",
))
inner = "".join(entry_links)
nav_parts.append(sx_call(
"events-day-entries-nav", inner=SxExpr(inner),
))
if is_admin and day_date:
admin_href = url_for(
"defpage_day_admin", calendar_slug=cal_slug,
year=day_date.year, month=day_date.month, day=day_date.day,
)
nav_parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog"))
return {
"date-str": day_date.strftime("%A %d %B %Y"),
"year": day_date.year,
"month": day_date.month,
"day": day_date.day,
"nav": SxExpr("".join(nav_parts)) if nav_parts else NIL,
}
@register_io_handler("events-entry-ctx")
async def _io_events_entry_ctx(
args: list[Any], kwargs: dict[str, Any], ctx: Any
) -> dict[str, Any]:
"""``(events-entry-ctx)`` → dict with events entry header values."""
from quart import g, url_for
from .types import NIL
from .parser import SxExpr
dctx = getattr(g, "_defpage_ctx", None) or {}
cal = getattr(g, "calendar", None) or dctx.get("calendar")
entry = getattr(g, "entry", None) or dctx.get("entry")
if not cal or not entry:
return {"id": ""}
cal_slug = getattr(cal, "slug", "") or ""
day = dctx.get("day")
month = dctx.get("month")
year = dctx.get("year")
# Times
start = entry.start_at
end = entry.end_at
time_str = ""
if start:
time_str = start.strftime("%H:%M")
if end:
time_str += f" \u2192 {end.strftime('%H:%M')}"
link_href = url_for(
"calendar.day.calendar_entries.calendar_entry.get",
calendar_slug=cal_slug,
year=year, month=month, day=day, entry_id=entry.id,
)
# Build nav: associated posts + admin link
entry_posts = dctx.get("entry_posts") or []
rights = getattr(g, "rights", None) or {}
is_admin = (
rights.get("admin", False)
if isinstance(rights, dict)
else getattr(rights, "admin", False)
)
from .helpers import sx_call
from shared.infrastructure.urls import app_url
nav_parts: list[str] = []
if entry_posts:
post_links = ""
for ep in entry_posts:
ep_slug = getattr(ep, "slug", "")
ep_title = getattr(ep, "title", "")
feat = getattr(ep, "feature_image", None)
href = app_url("blog", f"/{ep_slug}/")
if feat:
img_html = sx_call("events-post-img", src=feat, alt=ep_title)
else:
img_html = sx_call("events-post-img-placeholder")
post_links += sx_call(
"events-entry-nav-post-link",
href=href, img=SxExpr(img_html), title=ep_title,
)
nav_parts.append(
sx_call("events-entry-posts-nav-oob", items=SxExpr(post_links))
.replace(' :hx-swap-oob "true"', '')
)
if is_admin:
admin_url = url_for(
"calendar.day.calendar_entries.calendar_entry.admin.admin",
calendar_slug=cal_slug,
day=day, month=month, year=year, entry_id=entry.id,
)
nav_parts.append(sx_call("events-entry-admin-link", href=admin_url))
# Entry admin nav (ticket_types link)
admin_href = url_for(
"calendar.day.calendar_entries.calendar_entry.admin.admin",
calendar_slug=cal_slug,
day=day, month=month, year=year, entry_id=entry.id,
) if is_admin else ""
ticket_types_href = url_for(
"calendar.day.calendar_entries.calendar_entry.ticket_types.get",
calendar_slug=cal_slug, entry_id=entry.id,
year=year, month=month, day=day,
)
from quart import current_app
select_colours = current_app.jinja_env.globals.get("select_colours", "")
return {
"id": str(entry.id),
"name": entry.name or "",
"time-str": time_str,
"link-href": link_href,
"nav": SxExpr("".join(nav_parts)) if nav_parts else NIL,
"admin-href": admin_href,
"ticket-types-href": ticket_types_href,
"is-admin": is_admin,
"select-colours": select_colours,
}
@register_io_handler("events-slot-ctx")
async def _io_events_slot_ctx(
args: list[Any], kwargs: dict[str, Any], ctx: Any
) -> dict[str, Any]:
"""``(events-slot-ctx)`` → dict with events slot header values."""
from quart import g
dctx = getattr(g, "_defpage_ctx", None) or {}
slot = getattr(g, "slot", None) or dctx.get("slot")
if not slot:
return {"name": ""}
return {
"name": getattr(slot, "name", "") or "",
"description": getattr(slot, "description", "") or "",
}
@register_io_handler("events-ticket-type-ctx")
async def _io_events_ticket_type_ctx(
args: list[Any], kwargs: dict[str, Any], ctx: Any
) -> dict[str, Any]:
"""``(events-ticket-type-ctx)`` → dict with ticket type header values."""
from quart import g, url_for
dctx = getattr(g, "_defpage_ctx", None) or {}
cal = getattr(g, "calendar", None) or dctx.get("calendar")
entry = getattr(g, "entry", None) or dctx.get("entry")
ticket_type = getattr(g, "ticket_type", None) or dctx.get("ticket_type")
if not cal or not entry or not ticket_type:
return {"id": ""}
cal_slug = getattr(cal, "slug", "") or ""
day = dctx.get("day")
month = dctx.get("month")
year = dctx.get("year")
link_href = url_for(
"calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get",
calendar_slug=cal_slug, year=year, month=month, day=day,
entry_id=entry.id, ticket_type_id=ticket_type.id,
)
return {
"id": str(ticket_type.id),
"name": getattr(ticket_type, "name", "") or "",
"link-href": link_href,
}
# ---------------------------------------------------------------------------
# Market
# ---------------------------------------------------------------------------
@register_io_handler("market-header-ctx")
async def _io_market_header_ctx(
args: list[Any], kwargs: dict[str, Any], ctx: Any
) -> dict[str, Any]:
"""``(market-header-ctx)`` → dict with market header data."""
from quart import g, url_for
from shared.config import config as get_config
from .parser import SxExpr
cfg = get_config()
market_title = cfg.get("market_title", "")
link_href = url_for("defpage_market_home")
# Get categories if market is loaded
market = getattr(g, "market", None)
categories = {}
if market:
from bp.browse.services.nav import get_nav
nav_data = await get_nav(g.s, market_id=market.id)
categories = nav_data.get("cats", {})
# Build minimal ctx for existing helper functions
select_colours = getattr(g, "select_colours", "")
if not select_colours:
from quart import current_app
select_colours = current_app.jinja_env.globals.get("select_colours", "")
rights = getattr(g, "rights", None) or {}
mini_ctx: dict[str, Any] = {
"market_title": market_title,
"top_slug": "",
"sub_slug": "",
"categories": categories,
"qs": "",
"hx_select_search": "#main-panel",
"select_colours": select_colours,
"rights": rights,
"category_label": "",
}
# Build header + mobile nav data via new data-driven helpers
from sxc.pages.layouts import _market_header_data, _mobile_nav_panel_sx
header_data = _market_header_data(mini_ctx)
mobile_nav = _mobile_nav_panel_sx(mini_ctx)
return {
"market-title": market_title,
"link-href": link_href,
"top-slug": "",
"sub-slug": "",
"categories": header_data.get("categories", []),
"hx-select": header_data.get("hx-select", "#main-panel"),
"select-colours": header_data.get("select-colours", ""),
"all-href": header_data.get("all-href", ""),
"all-active": header_data.get("all-active", False),
"admin-href": header_data.get("admin-href", ""),
"mobile-nav": SxExpr(mobile_nav) if mobile_nav else "",
}
# ---------------------------------------------------------------------------
# Federation
# ---------------------------------------------------------------------------
@register_io_handler("federation-actor-ctx")
async def _io_federation_actor_ctx(
args: list[Any], kwargs: dict[str, Any], ctx: Any
) -> dict[str, Any] | None:
"""``(federation-actor-ctx)`` → serialized actor dict or None."""
from quart import g
actor = getattr(g, "_social_actor", None)
if not actor:
return None
return {
"id": actor.id,
"preferred_username": actor.preferred_username,
"display_name": getattr(actor, "display_name", None),
"icon_url": getattr(actor, "icon_url", None),
"actor_url": getattr(actor, "actor_url", ""),
}
# ---------------------------------------------------------------------------
# Misc UI contexts
# ---------------------------------------------------------------------------
@register_io_handler("select-colours")
async def _io_select_colours(
args: list[Any], kwargs: dict[str, Any], ctx: Any
) -> str:
"""``(select-colours)`` → the shared select/hover CSS class string."""
from quart import current_app
return current_app.jinja_env.globals.get("select_colours", "")
@register_io_handler("account-nav-ctx")
async def _io_account_nav_ctx(
args: list[Any], kwargs: dict[str, Any], ctx: Any
) -> Any:
"""``(account-nav-ctx)`` → account nav fragments as SxExpr, or NIL."""
from quart import g
from .types import NIL
from .parser import SxExpr
from .helpers import sx_call
val = getattr(g, "account_nav", None)
if not val:
return NIL
if isinstance(val, SxExpr):
return val
return sx_call("rich-text", html=str(val))
@register_io_handler("app-rights")
async def _io_app_rights(
args: list[Any], kwargs: dict[str, Any], ctx: Any
) -> dict[str, Any]:
"""``(app-rights)`` → user rights dict from ``g.rights``."""
from quart import g
return getattr(g, "rights", None) or {}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,131 @@
"""
Standard library primitives — isomorphic, opt-in modules.
Augment core with format, text, style, and debug primitives.
These are registered into the same _PRIMITIVES registry as core.
"""
from __future__ import annotations
from typing import Any
from .primitives import register_primitive
from .types import NIL
# ---------------------------------------------------------------------------
# stdlib.format
# ---------------------------------------------------------------------------
@register_primitive("format-date")
def prim_format_date(date_str: Any, fmt: str) -> str:
"""``(format-date date-str fmt)`` → formatted date string."""
from datetime import datetime
try:
dt = datetime.fromisoformat(str(date_str))
return dt.strftime(fmt)
except (ValueError, TypeError):
return str(date_str) if date_str else ""
@register_primitive("format-decimal")
def prim_format_decimal(val: Any, places: Any = 2) -> str:
"""``(format-decimal val places)`` → formatted decimal string."""
try:
return f"{float(val):.{int(places)}f}"
except (ValueError, TypeError):
return "0." + "0" * int(places)
@register_primitive("parse-int")
def prim_parse_int(val: Any, default: Any = 0) -> int | Any:
"""``(parse-int val default?)`` → int(val) with fallback."""
try:
return int(val)
except (ValueError, TypeError):
return default
@register_primitive("parse-datetime")
def prim_parse_datetime(val: Any) -> Any:
"""``(parse-datetime "2024-01-15T10:00:00")`` → ISO string or nil."""
from datetime import datetime
if not val or val is NIL:
return NIL
try:
dt = datetime.fromisoformat(str(val))
return dt.isoformat()
except (ValueError, TypeError):
return NIL
# ---------------------------------------------------------------------------
# stdlib.text
# ---------------------------------------------------------------------------
@register_primitive("pluralize")
def prim_pluralize(count: Any, singular: str = "", plural: str = "s") -> str:
"""``(pluralize count)`` → "s" if count != 1, else "".
``(pluralize count "item" "items")`` → "item" or "items"."""
try:
n = int(count)
except (ValueError, TypeError):
n = 0
if singular or plural != "s":
return singular if n == 1 else plural
return "" if n == 1 else "s"
@register_primitive("escape")
def prim_escape(s: Any) -> str:
"""``(escape val)`` → HTML-escaped string."""
from markupsafe import escape as _escape
return str(_escape(str(s) if s is not None and s is not NIL else ""))
@register_primitive("strip-tags")
def prim_strip_tags(s: str) -> str:
"""Strip HTML tags from a string."""
import re
return re.sub(r"<[^>]+>", "", s)
# ---------------------------------------------------------------------------
# stdlib.style
# ---------------------------------------------------------------------------
@register_primitive("css")
def prim_css(*args: Any) -> Any:
"""``(css :flex :gap-4 :hover:bg-sky-200)`` → StyleValue."""
from .types import Keyword
from .style_resolver import resolve_style
atoms = tuple(
(a.name if isinstance(a, Keyword) else str(a))
for a in args if a is not None and a is not NIL and a is not False
)
if not atoms:
return NIL
return resolve_style(atoms)
@register_primitive("merge-styles")
def prim_merge_styles(*styles: Any) -> Any:
"""``(merge-styles style1 style2)`` → merged StyleValue."""
from .types import StyleValue
from .style_resolver import merge_styles
valid = [s for s in styles if isinstance(s, StyleValue)]
if not valid:
return NIL
if len(valid) == 1:
return valid[0]
return merge_styles(valid)
# ---------------------------------------------------------------------------
# stdlib.debug
# ---------------------------------------------------------------------------
@register_primitive("assert")
def prim_assert(condition: Any, message: str = "Assertion failed") -> bool:
if not condition:
raise RuntimeError(f"Assertion error: {message}")
return True

View File

@@ -20,7 +20,11 @@ async def execute_query(query_def: QueryDef, params: dict[str, str]) -> Any:
Parameters are bound from request query string args.
"""
from .jinja_bridge import get_component_env, _get_request_context
from .async_eval import async_eval
import os
if os.environ.get("SX_USE_REF") == "1":
from .ref.async_eval_ref import async_eval
else:
from .async_eval import async_eval
env = dict(get_component_env())
env.update(query_def.closure)
@@ -45,7 +49,11 @@ async def execute_action(action_def: ActionDef, payload: dict[str, Any]) -> Any:
Parameters are bound from the JSON request body.
"""
from .jinja_bridge import get_component_env, _get_request_context
from .async_eval import async_eval
import os
if os.environ.get("SX_USE_REF") == "1":
from .ref.async_eval_ref import async_eval
else:
from .async_eval import async_eval
env = dict(get_component_env())
env.update(action_def.closure)

85
shared/sx/ref/BOUNDARY.md Normal file
View File

@@ -0,0 +1,85 @@
# SX Boundary Enforcement
## Principle
SX is an uninterrupted island of pure evaluation. Host code (Python, JavaScript, Rust, etc.) interacts with it only through declared entry points. The specification enforces this — violations are errors, not style suggestions.
## The Three Tiers
### Tier 1: Pure Primitives
Declared in `primitives.sx`. Stateless, synchronous, no side effects. Available in every SX environment on every target.
Examples: `+`, `str`, `map`, `get`, `concat`, `merge`
### Tier 2: I/O Primitives
Declared in `boundary.sx`. Async, side-effectful, require host context (request, config, services). Server-side only.
Examples: `frag`, `query`, `current-user`, `csrf-token`, `request-arg`
### Tier 3: Page Helpers
Declared in `boundary.sx` with a `:service` scope. Registered per-app, return data that `.sx` components render. Server-side only.
Examples: `highlight` (sx), `editor-data` (blog), `all-markets-data` (market)
## Boundary Types
Only these types may cross the host-SX boundary:
| Type | Python | JavaScript | Rust (future) |
|------|--------|-----------|----------------|
| number | `int`, `float` | `number` | `f64` |
| string | `str` | `string` | `String` |
| boolean | `bool` | `boolean` | `bool` |
| nil | `NIL` sentinel | `NIL` sentinel | `SxValue::Nil` |
| keyword | `str` (colon-prefixed) | `string` | `String` |
| list | `list` | `Array` | `Vec<SxValue>` |
| dict | `dict` | `Object` / `Map` | `HashMap<String, SxValue>` |
| sx-source | `SxExpr` wrapper | `string` | `String` |
| style-value | `StyleValue` | `StyleValue` | `StyleValue` |
**NOT allowed:** ORM models, datetime objects, request objects, raw callables, framework types. Convert at the edge before crossing.
## Enforcement Mechanism
The bootstrappers (`bootstrap_js.py`, `bootstrap_py.py`, future `bootstrap_rs.py`, etc.) read `boundary.sx` and emit target-native validation:
- **Typed targets (Rust, Haskell, TypeScript):** Boundary types become an enum/ADT/discriminated union. Registration functions have type signatures that reject non-SX values at compile time. You literally cannot register a primitive that returns a `datetime` — it won't typecheck.
- **Python + mypy:** Boundary types become a `Protocol`/`Union` type. `validate_boundary_value()` checks at runtime; mypy catches most violations statically.
- **JavaScript:** Runtime validation only. `registerPrimitive()` checks the name against the declared set. Boundary type checking is runtime.
## The Contract
1. **Spec-first.** Every primitive, I/O function, and page helper must be declared in `primitives.sx` or `boundary.sx` before it can be registered. Undeclared registration = error.
2. **SX types only.** Values crossing the boundary must be SX-typed. Host-native types (datetime, ORM models, request objects) must be converted to dicts/strings at the edge.
3. **Data in, markup out.** Python returns data (dicts, lists, strings). `.sx` files compose markup. No SX source construction in Python — no f-strings, no string concatenation, no `SxExpr(f"...")`.
4. **Closed island.** SX code can only call symbols in its env + declared primitives. There is no FFI, no `eval-python`, no escape hatch from inside SX.
5. **Fail fast.** Violations are runtime errors (startup crash), not warnings. For typed targets, they're compile errors.
## Adding a New Primitive
1. Add declaration to `primitives.sx` (pure) or `boundary.sx` (I/O / page helper)
2. Implement in the target language's primitive file
3. The bootstrapper-emitted validator will accept it on next rebuild/restart
4. If you skip step 1, the app crashes on startup telling you exactly what's missing
## File Map
```
shared/sx/ref/
primitives.sx — Pure primitive declarations
boundary.sx — I/O primitive + page helper + boundary type declarations
bootstrap_js.py — JS bootstrapper (reads both, emits validation)
bootstrap_py.py — Python bootstrapper (reads both, emits validation)
eval.sx — Evaluator spec (symbol resolution, env model)
parser.sx — Parser spec
render.sx — Renderer spec (shared registries)
```

View File

@@ -0,0 +1,7 @@
"""Reference SX evaluator — transpiled from canonical .sx spec files.
This package provides the bootstrap-compiled evaluator as an alternative
backend to the hand-written evaluator.py / html.py / async_eval.py.
Enable by setting SX_USE_REF=1 environment variable.
"""

View File

@@ -0,0 +1,999 @@
"""Async evaluation wrapper for the transpiled reference evaluator.
Wraps the sync sx_ref.py evaluator with async I/O support, mirroring
the hand-written async_eval.py. Provides the same public API:
async_eval() — evaluate with I/O primitives
async_render() — render to HTML with I/O
async_eval_to_sx() — evaluate to SX wire format with I/O
async_eval_slot_to_sx() — expand components server-side, then serialize
The sync transpiled evaluator handles all control flow, special forms,
and lambda/component dispatch. This wrapper adds:
- RequestContext threading
- I/O primitive interception (query, service, request-arg, etc.)
- Async trampoline for thunks
- SxExpr wrapping for wire format output
DO NOT EDIT by hand — this is a thin wrapper; the actual eval logic
lives in sx_ref.py (generated) and the I/O primitives in primitives_io.py.
"""
from __future__ import annotations
import contextvars
import inspect
from typing import Any
from ..types import Component, Keyword, Lambda, Macro, NIL, StyleValue, Symbol
from ..parser import SxExpr, serialize
from ..primitives_io import IO_PRIMITIVES, RequestContext, execute_io
from ..html import (
HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS,
escape_text, escape_attr, _RawHTML, css_class_collector, _svg_context,
)
from . import sx_ref
# Re-export EvalError from sx_ref
EvalError = sx_ref.EvalError
# When True, _aser expands known components server-side
_expand_components: contextvars.ContextVar[bool] = contextvars.ContextVar(
"_expand_components_ref", default=False
)
# ---------------------------------------------------------------------------
# Async TCO
# ---------------------------------------------------------------------------
class _AsyncThunk:
__slots__ = ("expr", "env", "ctx")
def __init__(self, expr, env, ctx):
self.expr = expr
self.env = env
self.ctx = ctx
async def _async_trampoline(val):
while isinstance(val, _AsyncThunk):
val = await _async_eval(val.expr, val.env, val.ctx)
return val
# ---------------------------------------------------------------------------
# Async evaluate — wraps transpiled sync eval with I/O support
# ---------------------------------------------------------------------------
async def async_eval(expr, env, ctx=None):
"""Public entry point: evaluate with I/O primitives."""
if ctx is None:
ctx = RequestContext()
result = await _async_eval(expr, env, ctx)
while isinstance(result, _AsyncThunk):
result = await _async_eval(result.expr, result.env, result.ctx)
return result
async def _async_eval(expr, env, ctx):
"""Internal async evaluator. Intercepts I/O primitives,
delegates everything else to the sync transpiled evaluator."""
# Intercept I/O primitive calls
if isinstance(expr, list) and expr:
head = expr[0]
if isinstance(head, Symbol) and head.name in IO_PRIMITIVES:
args, kwargs = await _parse_io_args(expr[1:], env, ctx)
return await execute_io(head.name, args, kwargs, ctx)
# For everything else, use the sync transpiled evaluator
result = sx_ref.eval_expr(expr, env)
return sx_ref.trampoline(result)
async def _parse_io_args(exprs, env, ctx):
"""Parse and evaluate I/O node args (keyword + positional)."""
args = []
kwargs = {}
i = 0
while i < len(exprs):
item = exprs[i]
if isinstance(item, Keyword) and i + 1 < len(exprs):
kwargs[item.name] = await async_eval(exprs[i + 1], env, ctx)
i += 2
else:
args.append(await async_eval(item, env, ctx))
i += 1
return args, kwargs
# ---------------------------------------------------------------------------
# Async HTML renderer
# ---------------------------------------------------------------------------
async def async_render(expr, env, ctx=None):
"""Render to HTML, awaiting I/O primitives inline."""
if ctx is None:
ctx = RequestContext()
return await _arender(expr, env, ctx)
async def _arender(expr, env, ctx):
if expr is None or expr is NIL or expr is False or expr is True:
return ""
if isinstance(expr, _RawHTML):
return expr.html
if isinstance(expr, str):
return escape_text(expr)
if isinstance(expr, (int, float)):
return escape_text(str(expr))
if isinstance(expr, Symbol):
val = await async_eval(expr, env, ctx)
return await _arender(val, env, ctx)
if isinstance(expr, Keyword):
return escape_text(expr.name)
if isinstance(expr, list):
if not expr:
return ""
return await _arender_list(expr, env, ctx)
if isinstance(expr, dict):
return ""
return escape_text(str(expr))
async def _arender_list(expr, env, ctx):
head = expr[0]
if isinstance(head, Symbol):
name = head.name
# I/O primitive
if name in IO_PRIMITIVES:
result = await async_eval(expr, env, ctx)
return await _arender(result, env, ctx)
# raw!
if name == "raw!":
parts = []
for arg in expr[1:]:
val = await async_eval(arg, env, ctx)
if isinstance(val, _RawHTML):
parts.append(val.html)
elif isinstance(val, str):
parts.append(val)
elif val is not None and val is not NIL:
parts.append(str(val))
return "".join(parts)
# Fragment
if name == "<>":
parts = [await _arender(c, env, ctx) for c in expr[1:]]
return "".join(parts)
# html: prefix
if name.startswith("html:"):
return await _arender_element(name[5:], expr[1:], env, ctx)
# Render-aware special forms
arsf = _ASYNC_RENDER_FORMS.get(name)
if arsf is not None:
if name in HTML_TAGS and (
(len(expr) > 1 and isinstance(expr[1], Keyword))
or _svg_context.get(False)
):
return await _arender_element(name, expr[1:], env, ctx)
return await arsf(expr, env, ctx)
# Macro expansion
if name in env:
val = env[name]
if isinstance(val, Macro):
expanded = sx_ref.trampoline(
sx_ref.expand_macro(val, expr[1:], env)
)
return await _arender(expanded, env, ctx)
# HTML tag
if name in HTML_TAGS:
return await _arender_element(name, expr[1:], env, ctx)
# Component
if name.startswith("~"):
val = env.get(name)
if isinstance(val, Component):
return await _arender_component(val, expr[1:], env, ctx)
# Custom element
if "-" in name and len(expr) > 1 and isinstance(expr[1], Keyword):
return await _arender_element(name, expr[1:], env, ctx)
# SVG context
if _svg_context.get(False):
return await _arender_element(name, expr[1:], env, ctx)
# Fallback — evaluate then render
result = await async_eval(expr, env, ctx)
return await _arender(result, env, ctx)
if isinstance(head, (Lambda, list)):
result = await async_eval(expr, env, ctx)
return await _arender(result, env, ctx)
# Data list
parts = [await _arender(item, env, ctx) for item in expr]
return "".join(parts)
async def _arender_element(tag, args, env, ctx):
attrs = {}
children = []
i = 0
while i < len(args):
arg = args[i]
if isinstance(arg, Keyword) and i + 1 < len(args):
attrs[arg.name] = await async_eval(args[i + 1], env, ctx)
i += 2
else:
children.append(arg)
i += 1
# StyleValue → class
style_val = attrs.get("style")
if isinstance(style_val, StyleValue):
from ..css_registry import register_generated_rule
register_generated_rule(style_val)
existing = attrs.get("class")
if existing and existing is not NIL and existing is not False:
attrs["class"] = f"{existing} {style_val.class_name}"
else:
attrs["class"] = style_val.class_name
del attrs["style"]
class_val = attrs.get("class")
if class_val is not None and class_val is not NIL and class_val is not False:
collector = css_class_collector.get(None)
if collector is not None:
collector.update(str(class_val).split())
parts = [f"<{tag}"]
for attr_name, attr_val in attrs.items():
if attr_val is None or attr_val is NIL or attr_val is False:
continue
if attr_name in BOOLEAN_ATTRS:
if attr_val:
parts.append(f" {attr_name}")
elif attr_val is True:
parts.append(f" {attr_name}")
else:
parts.append(f' {attr_name}="{escape_attr(str(attr_val))}"')
parts.append(">")
opening = "".join(parts)
if tag in VOID_ELEMENTS:
return opening
token = None
if tag in ("svg", "math"):
token = _svg_context.set(True)
try:
child_parts = [await _arender(c, env, ctx) for c in children]
finally:
if token is not None:
_svg_context.reset(token)
return f"{opening}{''.join(child_parts)}</{tag}>"
async def _arender_component(comp, args, env, ctx):
kwargs = {}
children = []
i = 0
while i < len(args):
arg = args[i]
if isinstance(arg, Keyword) and i + 1 < len(args):
kwargs[arg.name] = await async_eval(args[i + 1], env, ctx)
i += 2
else:
children.append(arg)
i += 1
local = dict(comp.closure)
local.update(env)
for p in comp.params:
local[p] = kwargs.get(p, NIL)
if comp.has_children:
child_html = [await _arender(c, env, ctx) for c in children]
local["children"] = _RawHTML("".join(child_html))
return await _arender(comp.body, local, ctx)
async def _arender_lambda(fn, args, env, ctx):
local = dict(fn.closure)
local.update(env)
for p, v in zip(fn.params, args):
local[p] = v
return await _arender(fn.body, local, ctx)
# ---------------------------------------------------------------------------
# Render-aware special forms
# ---------------------------------------------------------------------------
async def _arsf_if(expr, env, ctx):
cond = await async_eval(expr[1], env, ctx)
if cond and cond is not NIL:
return await _arender(expr[2], env, ctx)
return await _arender(expr[3], env, ctx) if len(expr) > 3 else ""
async def _arsf_when(expr, env, ctx):
cond = await async_eval(expr[1], env, ctx)
if cond and cond is not NIL:
return "".join([await _arender(b, env, ctx) for b in expr[2:]])
return ""
async def _arsf_cond(expr, env, ctx):
clauses = expr[1:]
if not clauses:
return ""
if isinstance(clauses[0], list) and len(clauses[0]) == 2:
for clause in clauses:
test = clause[0]
if isinstance(test, Symbol) and test.name in ("else", ":else"):
return await _arender(clause[1], env, ctx)
if isinstance(test, Keyword) and test.name == "else":
return await _arender(clause[1], env, ctx)
if await async_eval(test, env, ctx):
return await _arender(clause[1], env, ctx)
else:
i = 0
while i < len(clauses) - 1:
test, result = clauses[i], clauses[i + 1]
if isinstance(test, Keyword) and test.name == "else":
return await _arender(result, env, ctx)
if isinstance(test, Symbol) and test.name in (":else", "else"):
return await _arender(result, env, ctx)
if await async_eval(test, env, ctx):
return await _arender(result, env, ctx)
i += 2
return ""
async def _arsf_let(expr, env, ctx):
bindings = expr[1]
local = dict(env)
if isinstance(bindings, list):
if bindings and isinstance(bindings[0], list):
for b in bindings:
var = b[0]
vname = var.name if isinstance(var, Symbol) else var
local[vname] = await async_eval(b[1], local, ctx)
elif len(bindings) % 2 == 0:
for i in range(0, len(bindings), 2):
var = bindings[i]
vname = var.name if isinstance(var, Symbol) else var
local[vname] = await async_eval(bindings[i + 1], local, ctx)
return "".join([await _arender(b, local, ctx) for b in expr[2:]])
async def _arsf_begin(expr, env, ctx):
return "".join([await _arender(sub, env, ctx) for sub in expr[1:]])
async def _arsf_define(expr, env, ctx):
await async_eval(expr, env, ctx)
return ""
async def _arsf_map(expr, env, ctx):
fn = await async_eval(expr[1], env, ctx)
coll = await async_eval(expr[2], env, ctx)
parts = []
for item in coll:
if isinstance(fn, Lambda):
parts.append(await _arender_lambda(fn, (item,), env, ctx))
elif callable(fn):
r = fn(item)
if inspect.iscoroutine(r):
r = await r
parts.append(await _arender(r, env, ctx))
else:
parts.append(await _arender(item, env, ctx))
return "".join(parts)
async def _arsf_map_indexed(expr, env, ctx):
fn = await async_eval(expr[1], env, ctx)
coll = await async_eval(expr[2], env, ctx)
parts = []
for i, item in enumerate(coll):
if isinstance(fn, Lambda):
parts.append(await _arender_lambda(fn, (i, item), env, ctx))
elif callable(fn):
r = fn(i, item)
if inspect.iscoroutine(r):
r = await r
parts.append(await _arender(r, env, ctx))
else:
parts.append(await _arender(item, env, ctx))
return "".join(parts)
async def _arsf_filter(expr, env, ctx):
result = await async_eval(expr, env, ctx)
return await _arender(result, env, ctx)
async def _arsf_for_each(expr, env, ctx):
fn = await async_eval(expr[1], env, ctx)
coll = await async_eval(expr[2], env, ctx)
parts = []
for item in coll:
if isinstance(fn, Lambda):
parts.append(await _arender_lambda(fn, (item,), env, ctx))
elif callable(fn):
r = fn(item)
if inspect.iscoroutine(r):
r = await r
parts.append(await _arender(r, env, ctx))
else:
parts.append(await _arender(item, env, ctx))
return "".join(parts)
_ASYNC_RENDER_FORMS = {
"if": _arsf_if,
"when": _arsf_when,
"cond": _arsf_cond,
"let": _arsf_let,
"let*": _arsf_let,
"begin": _arsf_begin,
"do": _arsf_begin,
"define": _arsf_define,
"defstyle": _arsf_define,
"defkeyframes": _arsf_define,
"defcomp": _arsf_define,
"defmacro": _arsf_define,
"defhandler": _arsf_define,
"map": _arsf_map,
"map-indexed": _arsf_map_indexed,
"filter": _arsf_filter,
"for-each": _arsf_for_each,
}
# ---------------------------------------------------------------------------
# Async SX wire format (aser)
# ---------------------------------------------------------------------------
async def async_eval_to_sx(expr, env, ctx=None):
"""Evaluate and produce SX source string (wire format)."""
if ctx is None:
ctx = RequestContext()
result = await _aser(expr, env, ctx)
if isinstance(result, SxExpr):
return result
if result is None or result is NIL:
return SxExpr("")
if isinstance(result, str):
return SxExpr(result)
return SxExpr(serialize(result))
async def async_eval_slot_to_sx(expr, env, ctx=None):
"""Like async_eval_to_sx but expands component calls server-side."""
if ctx is None:
ctx = RequestContext()
token = _expand_components.set(True)
try:
return await _eval_slot_inner(expr, env, ctx)
finally:
_expand_components.reset(token)
async def _eval_slot_inner(expr, env, ctx):
if isinstance(expr, list) and expr:
head = expr[0]
if isinstance(head, Symbol) and head.name.startswith("~"):
comp = env.get(head.name)
if isinstance(comp, Component):
result = await _aser_component(comp, expr[1:], env, ctx)
if isinstance(result, SxExpr):
return result
if result is None or result is NIL:
return SxExpr("")
if isinstance(result, str):
return SxExpr(result)
return SxExpr(serialize(result))
result = await _aser(expr, env, ctx)
result = await _maybe_expand_component_result(result, env, ctx)
if isinstance(result, SxExpr):
return result
if result is None or result is NIL:
return SxExpr("")
if isinstance(result, str):
return SxExpr(result)
return SxExpr(serialize(result))
async def _maybe_expand_component_result(result, env, ctx):
raw = None
if isinstance(result, SxExpr):
raw = str(result).strip()
elif isinstance(result, str):
raw = result.strip()
if raw and raw.startswith("(~"):
from ..parser import parse_all
parsed = parse_all(raw)
if parsed:
return await async_eval_slot_to_sx(parsed[0], env, ctx)
return result
async def _aser(expr, env, ctx):
"""Evaluate for SX wire format — serialize rendering forms, evaluate control flow."""
if isinstance(expr, (int, float, bool)):
return expr
if isinstance(expr, SxExpr):
return expr
if isinstance(expr, str):
return expr
if expr is None or expr is NIL:
return NIL
if isinstance(expr, Symbol):
name = expr.name
if name in env:
return env[name]
if sx_ref.is_primitive(name):
return sx_ref.get_primitive(name)
if name == "true":
return True
if name == "false":
return False
if name == "nil":
return NIL
raise EvalError(f"Undefined symbol: {name}")
if isinstance(expr, Keyword):
return expr.name
if isinstance(expr, dict):
return {k: await _aser(v, env, ctx) for k, v in expr.items()}
if not isinstance(expr, list):
return expr
if not expr:
return []
head = expr[0]
if not isinstance(head, (Symbol, Lambda, list)):
return [await _aser(x, env, ctx) for x in expr]
if isinstance(head, Symbol):
name = head.name
# I/O primitives
if name in IO_PRIMITIVES:
args, kwargs = await _parse_io_args(expr[1:], env, ctx)
return await execute_io(name, args, kwargs, ctx)
# Fragment
if name == "<>":
return await _aser_fragment(expr[1:], env, ctx)
# raw!
if name == "raw!":
return await _aser_call("raw!", expr[1:], env, ctx)
# html: prefix
if name.startswith("html:"):
return await _aser_call(name[5:], expr[1:], env, ctx)
# Component call
if name.startswith("~"):
val = env.get(name)
if isinstance(val, Macro):
expanded = sx_ref.trampoline(
sx_ref.expand_macro(val, expr[1:], env)
)
return await _aser(expanded, env, ctx)
if isinstance(val, Component) and _expand_components.get():
return await _aser_component(val, expr[1:], env, ctx)
return await _aser_call(name, expr[1:], env, ctx)
# Serialize-mode special/HO forms
sf = _ASER_FORMS.get(name)
if sf is not None:
if name in HTML_TAGS and (
(len(expr) > 1 and isinstance(expr[1], Keyword))
or _svg_context.get(False)
):
return await _aser_call(name, expr[1:], env, ctx)
return await sf(expr, env, ctx)
# HTML tag
if name in HTML_TAGS:
return await _aser_call(name, expr[1:], env, ctx)
# Macro
if name in env:
val = env[name]
if isinstance(val, Macro):
expanded = sx_ref.trampoline(
sx_ref.expand_macro(val, expr[1:], env)
)
return await _aser(expanded, env, ctx)
# Custom element
if "-" in name and len(expr) > 1 and isinstance(expr[1], Keyword):
return await _aser_call(name, expr[1:], env, ctx)
# SVG context
if _svg_context.get(False):
return await _aser_call(name, expr[1:], env, ctx)
# Function/lambda call
fn = await async_eval(head, env, ctx)
args = [await async_eval(a, env, ctx) for a in expr[1:]]
if callable(fn) and not isinstance(fn, (Lambda, Component)):
result = fn(*args)
if inspect.iscoroutine(result):
return await result
return result
if isinstance(fn, Lambda):
local = dict(fn.closure)
local.update(env)
for p, v in zip(fn.params, args):
local[p] = v
return await _aser(fn.body, local, ctx)
if isinstance(fn, Component):
return await _aser_call(f"~{fn.name}", expr[1:], env, ctx)
raise EvalError(f"Not callable: {fn!r}")
async def _aser_fragment(children, env, ctx):
parts = []
for child in children:
result = await _aser(child, env, ctx)
if isinstance(result, list):
for item in result:
if item is not NIL and item is not None:
parts.append(serialize(item))
elif result is not NIL and result is not None:
parts.append(serialize(result))
if not parts:
return SxExpr("")
return SxExpr("(<> " + " ".join(parts) + ")")
async def _aser_component(comp, args, env, ctx):
kwargs = {}
children = []
i = 0
while i < len(args):
arg = args[i]
if isinstance(arg, Keyword) and i + 1 < len(args):
kwargs[arg.name] = await _aser(args[i + 1], env, ctx)
i += 2
else:
children.append(arg)
i += 1
local = dict(comp.closure)
local.update(env)
for p in comp.params:
local[p] = kwargs.get(p, NIL)
if comp.has_children:
child_parts = [serialize(await _aser(c, env, ctx)) for c in children]
local["children"] = SxExpr("(<> " + " ".join(child_parts) + ")")
return await _aser(comp.body, local, ctx)
async def _aser_call(name, args, env, ctx):
token = None
if name in ("svg", "math"):
token = _svg_context.set(True)
try:
parts = [name]
extra_class = None
i = 0
while i < len(args):
arg = args[i]
if isinstance(arg, Keyword) and i + 1 < len(args):
val = await _aser(args[i + 1], env, ctx)
if val is not NIL and val is not None:
if arg.name == "style" and isinstance(val, StyleValue):
from ..css_registry import register_generated_rule
register_generated_rule(val)
extra_class = val.class_name
else:
parts.append(f":{arg.name}")
if isinstance(val, list):
live = [v for v in val if v is not NIL and v is not None]
items = [serialize(v) for v in live]
if not items:
parts.append("nil")
elif any(isinstance(v, SxExpr) for v in live):
parts.append("(<> " + " ".join(items) + ")")
else:
parts.append("(list " + " ".join(items) + ")")
else:
parts.append(serialize(val))
i += 2
else:
result = await _aser(arg, env, ctx)
if result is not NIL and result is not None:
if isinstance(result, list):
for item in result:
if item is not NIL and item is not None:
parts.append(serialize(item))
else:
parts.append(serialize(result))
i += 1
if extra_class:
_merge_class_into_parts(parts, extra_class)
return SxExpr("(" + " ".join(parts) + ")")
finally:
if token is not None:
_svg_context.reset(token)
def _merge_class_into_parts(parts, class_name):
for i, p in enumerate(parts):
if p == ":class" and i + 1 < len(parts):
existing = parts[i + 1]
if existing.startswith('"') and existing.endswith('"'):
parts[i + 1] = existing[:-1] + " " + class_name + '"'
else:
parts[i + 1] = f'(str {existing} " {class_name}")'
return
parts.insert(1, f'"{class_name}"')
parts.insert(1, ":class")
# ---------------------------------------------------------------------------
# Aser-mode special forms
# ---------------------------------------------------------------------------
async def _assf_if(expr, env, ctx):
cond = await async_eval(expr[1], env, ctx)
if cond and cond is not NIL:
return await _aser(expr[2], env, ctx)
return await _aser(expr[3], env, ctx) if len(expr) > 3 else NIL
async def _assf_when(expr, env, ctx):
cond = await async_eval(expr[1], env, ctx)
if cond and cond is not NIL:
result = NIL
for body_expr in expr[2:]:
result = await _aser(body_expr, env, ctx)
return result
return NIL
async def _assf_let(expr, env, ctx):
bindings = expr[1]
local = dict(env)
if isinstance(bindings, list):
if bindings and isinstance(bindings[0], list):
for b in bindings:
var = b[0]
vname = var.name if isinstance(var, Symbol) else var
local[vname] = await _aser(b[1], local, ctx)
elif len(bindings) % 2 == 0:
for i in range(0, len(bindings), 2):
var = bindings[i]
vname = var.name if isinstance(var, Symbol) else var
local[vname] = await _aser(bindings[i + 1], local, ctx)
result = NIL
for body_expr in expr[2:]:
result = await _aser(body_expr, local, ctx)
return result
async def _assf_cond(expr, env, ctx):
clauses = expr[1:]
if not clauses:
return NIL
if isinstance(clauses[0], list) and len(clauses[0]) == 2:
for clause in clauses:
test = clause[0]
if isinstance(test, Symbol) and test.name in ("else", ":else"):
return await _aser(clause[1], env, ctx)
if isinstance(test, Keyword) and test.name == "else":
return await _aser(clause[1], env, ctx)
if await async_eval(test, env, ctx):
return await _aser(clause[1], env, ctx)
else:
i = 0
while i < len(clauses) - 1:
test, result = clauses[i], clauses[i + 1]
if isinstance(test, Keyword) and test.name == "else":
return await _aser(result, env, ctx)
if isinstance(test, Symbol) and test.name in (":else", "else"):
return await _aser(result, env, ctx)
if await async_eval(test, env, ctx):
return await _aser(result, env, ctx)
i += 2
return NIL
async def _assf_case(expr, env, ctx):
match_val = await async_eval(expr[1], env, ctx)
clauses = expr[2:]
i = 0
while i < len(clauses) - 1:
test, result = clauses[i], clauses[i + 1]
if isinstance(test, Keyword) and test.name == "else":
return await _aser(result, env, ctx)
if isinstance(test, Symbol) and test.name in (":else", "else"):
return await _aser(result, env, ctx)
if match_val == await async_eval(test, env, ctx):
return await _aser(result, env, ctx)
i += 2
return NIL
async def _assf_begin(expr, env, ctx):
result = NIL
for sub in expr[1:]:
result = await _aser(sub, env, ctx)
return result
async def _assf_define(expr, env, ctx):
await async_eval(expr, env, ctx)
return NIL
async def _assf_and(expr, env, ctx):
result = True
for arg in expr[1:]:
result = await async_eval(arg, env, ctx)
if not result:
return result
return result
async def _assf_or(expr, env, ctx):
result = False
for arg in expr[1:]:
result = await async_eval(arg, env, ctx)
if result:
return result
return result
async def _assf_lambda(expr, env, ctx):
params_expr = expr[1]
param_names = []
for p in params_expr:
if isinstance(p, Symbol):
param_names.append(p.name)
elif isinstance(p, str):
param_names.append(p)
return Lambda(param_names, expr[2], dict(env))
async def _assf_quote(expr, env, ctx):
return expr[1] if len(expr) > 1 else NIL
async def _assf_thread_first(expr, env, ctx):
result = await async_eval(expr[1], env, ctx)
for form in expr[2:]:
if isinstance(form, list):
fn = await async_eval(form[0], env, ctx)
fn_args = [result] + [await async_eval(a, env, ctx) for a in form[1:]]
else:
fn = await async_eval(form, env, ctx)
fn_args = [result]
if callable(fn) and not isinstance(fn, (Lambda, Component)):
result = fn(*fn_args)
if inspect.iscoroutine(result):
result = await result
elif isinstance(fn, Lambda):
local = dict(fn.closure)
local.update(env)
for p, v in zip(fn.params, fn_args):
local[p] = v
result = await async_eval(fn.body, local, ctx)
else:
raise EvalError(f"-> form not callable: {fn!r}")
return result
async def _assf_set_bang(expr, env, ctx):
value = await async_eval(expr[2], env, ctx)
env[expr[1].name] = value
return value
# Aser-mode HO forms
async def _asho_map(expr, env, ctx):
fn = await async_eval(expr[1], env, ctx)
coll = await async_eval(expr[2], env, ctx)
results = []
for item in coll:
if isinstance(fn, Lambda):
local = dict(fn.closure)
local.update(env)
local[fn.params[0]] = item
results.append(await _aser(fn.body, local, ctx))
elif callable(fn):
r = fn(item)
results.append(await r if inspect.iscoroutine(r) else r)
else:
raise EvalError(f"map requires callable, got {type(fn).__name__}")
return results
async def _asho_map_indexed(expr, env, ctx):
fn = await async_eval(expr[1], env, ctx)
coll = await async_eval(expr[2], env, ctx)
results = []
for i, item in enumerate(coll):
if isinstance(fn, Lambda):
local = dict(fn.closure)
local.update(env)
local[fn.params[0]] = i
local[fn.params[1]] = item
results.append(await _aser(fn.body, local, ctx))
elif callable(fn):
r = fn(i, item)
results.append(await r if inspect.iscoroutine(r) else r)
else:
raise EvalError(f"map-indexed requires callable, got {type(fn).__name__}")
return results
async def _asho_filter(expr, env, ctx):
return await async_eval(expr, env, ctx)
async def _asho_for_each(expr, env, ctx):
fn = await async_eval(expr[1], env, ctx)
coll = await async_eval(expr[2], env, ctx)
results = []
for item in coll:
if isinstance(fn, Lambda):
local = dict(fn.closure)
local.update(env)
local[fn.params[0]] = item
results.append(await _aser(fn.body, local, ctx))
elif callable(fn):
r = fn(item)
results.append(await r if inspect.iscoroutine(r) else r)
return results
_ASER_FORMS = {
"if": _assf_if,
"when": _assf_when,
"cond": _assf_cond,
"case": _assf_case,
"and": _assf_and,
"or": _assf_or,
"let": _assf_let,
"let*": _assf_let,
"lambda": _assf_lambda,
"fn": _assf_lambda,
"define": _assf_define,
"defstyle": _assf_define,
"defkeyframes": _assf_define,
"defcomp": _assf_define,
"defmacro": _assf_define,
"defhandler": _assf_define,
"begin": _assf_begin,
"do": _assf_begin,
"quote": _assf_quote,
"->": _assf_thread_first,
"set!": _assf_set_bang,
"map": _asho_map,
"map-indexed": _asho_map_indexed,
"filter": _asho_filter,
"for-each": _asho_for_each,
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

461
shared/sx/ref/boundary.sx Normal file
View File

@@ -0,0 +1,461 @@
;; ==========================================================================
;; boundary.sx — SX boundary contract
;;
;; Declares everything allowed to cross the host-SX boundary:
;; I/O primitives (Tier 2) and page helpers (Tier 3).
;;
;; Pure primitives (Tier 1) are declared in primitives.sx.
;; This file declares what primitives.sx does NOT cover:
;; async/side-effectful host functions that need request context.
;;
;; Format:
;; (define-io-primitive "name"
;; :params (param1 param2 &key ...)
;; :returns "type"
;; :async true
;; :doc "description"
;; :context :request)
;;
;; (define-page-helper "name"
;; :params (param1 param2)
;; :returns "type"
;; :service "service-name")
;;
;; Bootstrappers read this file and emit frozen sets + validation
;; functions for the target language.
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Tier 1: Pure primitives — declared in primitives.sx
;; --------------------------------------------------------------------------
(declare-tier :pure :source "primitives.sx")
;; --------------------------------------------------------------------------
;; Tier 2: I/O primitives — async, side-effectful, need host context
;; --------------------------------------------------------------------------
(define-io-primitive "frag"
:params (service frag-type &key)
:returns "string"
:async true
:doc "Fetch cross-service HTML fragment."
:context :request)
(define-io-primitive "query"
:params (service query-name &key)
:returns "any"
:async true
:doc "Fetch data from another service via internal HTTP."
:context :request)
(define-io-primitive "action"
:params (service action-name &key)
:returns "any"
:async true
:doc "Call an action on another service via internal HTTP."
:context :request)
(define-io-primitive "current-user"
:params ()
:returns "dict?"
:async true
:doc "Current authenticated user dict, or nil."
:context :request)
(define-io-primitive "htmx-request?"
:params ()
:returns "boolean"
:async true
:doc "True if current request has HX-Request header."
:context :request)
(define-io-primitive "service"
:params (service-or-method &rest args &key)
:returns "any"
:async true
:doc "Call a domain service method. Two-arg: (service svc method). One-arg: (service method) uses bound handler service."
:context :request)
(define-io-primitive "request-arg"
:params (name &rest default)
:returns "any"
:async true
:doc "Read a query string argument from the current request."
:context :request)
(define-io-primitive "request-path"
:params ()
:returns "string"
:async true
:doc "Current request path."
:context :request)
(define-io-primitive "nav-tree"
:params ()
:returns "list"
:async true
:doc "Navigation tree as list of node dicts."
:context :request)
(define-io-primitive "get-children"
:params (&key parent-type parent-id)
:returns "list"
:async true
:doc "Fetch child entities for a parent."
:context :request)
(define-io-primitive "g"
:params (key)
:returns "any"
:async true
:doc "Read a value from the Quart request-local g object."
:context :request)
(define-io-primitive "csrf-token"
:params ()
:returns "string"
:async true
:doc "Current CSRF token string."
:context :request)
(define-io-primitive "abort"
:params (status &rest message)
:returns "nil"
:async true
:doc "Raise HTTP error from SX."
:context :request)
(define-io-primitive "url-for"
:params (endpoint &key)
:returns "string"
:async true
:doc "Generate URL for a named endpoint."
:context :request)
(define-io-primitive "route-prefix"
:params ()
:returns "string"
:async true
:doc "Service URL prefix for dev/prod routing."
:context :request)
(define-io-primitive "root-header-ctx"
:params ()
:returns "dict"
:async true
:doc "Dict with root header values (cart-mini, auth-menu, nav-tree, etc.)."
:context :request)
(define-io-primitive "post-header-ctx"
:params ()
:returns "dict"
:async true
:doc "Dict with post-level header values."
:context :request)
(define-io-primitive "select-colours"
:params ()
:returns "string"
:async true
:doc "Shared select/hover CSS class string."
:context :request)
(define-io-primitive "account-nav-ctx"
:params ()
:returns "any"
:async true
:doc "Account nav fragments, or nil."
:context :request)
(define-io-primitive "app-rights"
:params ()
:returns "dict"
:async true
:doc "User rights dict from g.rights."
:context :request)
(define-io-primitive "federation-actor-ctx"
:params ()
:returns "dict?"
:async true
:doc "Serialized ActivityPub actor dict or nil."
:context :request)
(define-io-primitive "request-view-args"
:params (key)
:returns "any"
:async true
:doc "Read a URL view argument from the current request."
:context :request)
(define-io-primitive "cart-page-ctx"
:params ()
:returns "dict"
:async true
:doc "Dict with cart page header values."
:context :request)
(define-io-primitive "events-calendar-ctx"
:params ()
:returns "dict"
:async true
:doc "Dict with events calendar header values."
:context :request)
(define-io-primitive "events-day-ctx"
:params ()
:returns "dict"
:async true
:doc "Dict with events day header values."
:context :request)
(define-io-primitive "events-entry-ctx"
:params ()
:returns "dict"
:async true
:doc "Dict with events entry header values."
:context :request)
(define-io-primitive "events-slot-ctx"
:params ()
:returns "dict"
:async true
:doc "Dict with events slot header values."
:context :request)
(define-io-primitive "events-ticket-type-ctx"
:params ()
:returns "dict"
:async true
:doc "Dict with ticket type header values."
:context :request)
(define-io-primitive "market-header-ctx"
:params ()
:returns "dict"
:async true
:doc "Dict with market header data."
:context :request)
;; Moved from primitives.py — these need host context (infra/config/Quart)
(define-io-primitive "app-url"
:params (service &rest path)
:returns "string"
:async false
:doc "Full URL for a service: (app-url \"blog\" \"/my-post/\")."
:context :config)
(define-io-primitive "asset-url"
:params (&rest path)
:returns "string"
:async false
:doc "Versioned static asset URL."
:context :config)
(define-io-primitive "config"
:params (key)
:returns "any"
:async false
:doc "Read a value from app-config.yaml."
:context :config)
(define-io-primitive "jinja-global"
:params (key &rest default)
:returns "any"
:async false
:doc "Read a Jinja environment global."
:context :request)
(define-io-primitive "relations-from"
:params (entity-type)
:returns "list"
:async false
:doc "List of RelationDef dicts for an entity type."
:context :config)
;; --------------------------------------------------------------------------
;; Tier 3: Page helpers — service-scoped, registered per app
;; --------------------------------------------------------------------------
;; SX docs service
(define-page-helper "highlight"
:params (code lang)
:returns "sx-source"
:service "sx")
(define-page-helper "primitives-data"
:params ()
:returns "dict"
:service "sx")
(define-page-helper "special-forms-data"
:params ()
:returns "dict"
:service "sx")
(define-page-helper "reference-data"
:params (slug)
:returns "dict"
:service "sx")
(define-page-helper "attr-detail-data"
:params (slug)
:returns "dict"
:service "sx")
(define-page-helper "header-detail-data"
:params (slug)
:returns "dict"
:service "sx")
(define-page-helper "event-detail-data"
:params (slug)
:returns "dict"
:service "sx")
(define-page-helper "read-spec-file"
:params (filename)
:returns "string"
:service "sx")
(define-page-helper "bootstrapper-data"
:params (target)
:returns "dict"
:service "sx")
;; Blog service
(define-page-helper "editor-data"
:params (&key)
:returns "dict"
:service "blog")
(define-page-helper "editor-page-data"
:params (&key)
:returns "dict"
:service "blog")
(define-page-helper "post-admin-data"
:params (&key slug)
:returns "dict"
:service "blog")
(define-page-helper "post-data-data"
:params (&key slug)
:returns "dict"
:service "blog")
(define-page-helper "post-preview-data"
:params (&key slug)
:returns "dict"
:service "blog")
(define-page-helper "post-entries-data"
:params (&key slug)
:returns "dict"
:service "blog")
(define-page-helper "post-settings-data"
:params (&key slug)
:returns "dict"
:service "blog")
(define-page-helper "post-edit-data"
:params (&key slug)
:returns "dict"
:service "blog")
;; Events service
(define-page-helper "calendar-admin-data"
:params (&key calendar-slug)
:returns "dict"
:service "events")
(define-page-helper "day-admin-data"
:params (&key calendar-slug year month day)
:returns "dict"
:service "events")
(define-page-helper "slots-data"
:params (&key calendar-slug)
:returns "dict"
:service "events")
(define-page-helper "slot-data"
:params (&key calendar-slug slot-id)
:returns "dict"
:service "events")
(define-page-helper "entry-data"
:params (&key calendar-slug entry-id)
:returns "dict"
:service "events")
(define-page-helper "entry-admin-data"
:params (&key calendar-slug entry-id year month day)
:returns "dict"
:service "events")
(define-page-helper "ticket-types-data"
:params (&key calendar-slug entry-id year month day)
:returns "dict"
:service "events")
(define-page-helper "ticket-type-data"
:params (&key calendar-slug entry-id ticket-type-id year month day)
:returns "dict"
:service "events")
(define-page-helper "tickets-data"
:params (&key)
:returns "dict"
:service "events")
(define-page-helper "ticket-detail-data"
:params (&key code)
:returns "dict"
:service "events")
(define-page-helper "ticket-admin-data"
:params (&key)
:returns "dict"
:service "events")
(define-page-helper "markets-data"
:params (&key)
:returns "dict"
:service "events")
;; Market service
(define-page-helper "all-markets-data"
:params (&key)
:returns "dict"
:service "market")
(define-page-helper "page-markets-data"
:params (&key slug)
:returns "dict"
:service "market")
(define-page-helper "page-admin-data"
:params (&key slug)
:returns "dict"
:service "market")
(define-page-helper "market-home-data"
:params (&key page-slug market-slug)
:returns "dict"
:service "market")
;; --------------------------------------------------------------------------
;; Boundary types — what's allowed to cross the host-SX boundary
;; --------------------------------------------------------------------------
(define-boundary-types
(list "number" "string" "boolean" "nil" "keyword"
"list" "dict" "sx-source" "style-value"))

View File

@@ -0,0 +1,134 @@
"""
Parse boundary.sx and primitives.sx to extract declared names.
Shared by both bootstrap_py.py and bootstrap_js.py, and used at runtime
by the validation module.
"""
from __future__ import annotations
import os
from typing import Any
# Allow standalone use (from bootstrappers) or in-project imports
try:
from shared.sx.parser import parse_all
from shared.sx.types import Symbol, Keyword, NIL as SX_NIL
except ImportError:
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
sys.path.insert(0, _PROJECT)
from shared.sx.parser import parse_all
from shared.sx.types import Symbol, Keyword, NIL as SX_NIL
def _ref_dir() -> str:
return os.path.dirname(os.path.abspath(__file__))
def _read_file(filename: str) -> str:
filepath = os.path.join(_ref_dir(), filename)
with open(filepath, encoding="utf-8") as f:
return f.read()
def _extract_keyword_arg(expr: list, key: str) -> Any:
"""Extract :key value from a flat keyword-arg list."""
for i, item in enumerate(expr):
if isinstance(item, Keyword) and item.name == key and i + 1 < len(expr):
return expr[i + 1]
return None
def parse_primitives_sx() -> frozenset[str]:
"""Parse primitives.sx and return frozenset of declared pure primitive names."""
by_module = parse_primitives_by_module()
all_names: set[str] = set()
for names in by_module.values():
all_names.update(names)
return frozenset(all_names)
def parse_primitives_by_module() -> dict[str, frozenset[str]]:
"""Parse primitives.sx and return primitives grouped by module.
Returns:
Dict mapping module name (e.g. "core.arithmetic") to frozenset of
primitive names declared under that module.
"""
source = _read_file("primitives.sx")
exprs = parse_all(source)
modules: dict[str, set[str]] = {}
current_module = "_unscoped"
for expr in exprs:
if not isinstance(expr, list) or len(expr) < 2:
continue
if not isinstance(expr[0], Symbol):
continue
if expr[0].name == "define-module":
mod_name = expr[1]
if isinstance(mod_name, Keyword):
current_module = mod_name.name
elif isinstance(mod_name, str):
current_module = mod_name
elif expr[0].name == "define-primitive":
name = expr[1]
if isinstance(name, str):
modules.setdefault(current_module, set()).add(name)
return {mod: frozenset(names) for mod, names in modules.items()}
def parse_boundary_sx() -> tuple[frozenset[str], dict[str, frozenset[str]]]:
"""Parse boundary.sx and return (io_names, {service: helper_names}).
Returns:
io_names: frozenset of declared I/O primitive names
helpers: dict mapping service name to frozenset of helper names
"""
source = _read_file("boundary.sx")
exprs = parse_all(source)
io_names: set[str] = set()
helpers: dict[str, set[str]] = {}
for expr in exprs:
if not isinstance(expr, list) or not expr:
continue
head = expr[0]
if not isinstance(head, Symbol):
continue
if head.name == "define-io-primitive":
name = expr[1]
if isinstance(name, str):
io_names.add(name)
elif head.name == "define-page-helper":
name = expr[1]
service = _extract_keyword_arg(expr, "service")
if isinstance(name, str) and isinstance(service, str):
helpers.setdefault(service, set()).add(name)
frozen_helpers = {svc: frozenset(names) for svc, names in helpers.items()}
return frozenset(io_names), frozen_helpers
def parse_boundary_types() -> frozenset[str]:
"""Parse boundary.sx and return the declared boundary type names."""
source = _read_file("boundary.sx")
exprs = parse_all(source)
for expr in exprs:
if (isinstance(expr, list) and len(expr) >= 2
and isinstance(expr[0], Symbol)
and expr[0].name == "define-boundary-types"):
type_list = expr[1]
if isinstance(type_list, list):
# (list "number" "string" ...)
return frozenset(
item for item in type_list
if isinstance(item, str)
)
return frozenset()

245
shared/sx/ref/callcc.sx Normal file
View File

@@ -0,0 +1,245 @@
;; ==========================================================================
;; callcc.sx — Full first-class continuations (call/cc)
;;
;; OPTIONAL EXTENSION — not required by the core evaluator.
;; Bootstrappers include this only when the target supports it naturally.
;;
;; Full call/cc (call-with-current-continuation) captures the ENTIRE
;; remaining computation as a first-class function — not just up to a
;; delimiter, but all the way to the top level. Invoking a continuation
;; captured by call/cc abandons the current computation entirely and
;; resumes from where the continuation was captured.
;;
;; This is strictly more powerful than delimited continuations (shift/reset)
;; but harder to implement in targets that don't support it natively.
;; Recommended only for targets where it's natural:
;; - Scheme/Racket (native call/cc)
;; - Haskell (ContT monad transformer)
;;
;; For targets like Python, JavaScript, and Rust, delimited continuations
;; (continuations.sx) are more practical and cover the same use cases
;; without requiring a global CPS transform.
;;
;; One new special form:
;; (call/cc f) — call f with the current continuation
;;
;; One new type:
;; continuation — same type as in continuations.sx
;;
;; If both extensions are loaded, the continuation type is shared.
;; Delimited and undelimited continuations are the same type —
;; the difference is in how they are captured, not what they are.
;;
;; Platform requirements:
;; (make-continuation fn) — wrap a function as a continuation value
;; (continuation? x) — type predicate
;; (type-of continuation) → "continuation"
;; (call-with-cc f env) — target-specific call/cc implementation
;; ==========================================================================
;; --------------------------------------------------------------------------
;; 1. Semantics
;; --------------------------------------------------------------------------
;;
;; (call/cc f)
;;
;; Evaluates f (which must be a function of one argument), passing it the
;; current continuation as a continuation value. f can:
;;
;; a) Return normally — call/cc returns whatever f returns
;; b) Invoke the continuation — abandons f's computation, call/cc
;; "returns" the value passed to the continuation
;; c) Store the continuation — invoke it later, possibly multiple times
;;
;; Key difference from shift/reset: invoking an undelimited continuation
;; NEVER RETURNS to the caller. It abandons the current computation and
;; jumps back to where call/cc was originally called.
;;
;; ;; Delimited (shift/reset) — k returns a value:
;; (reset (+ 1 (shift k (+ (k 10) (k 20)))))
;; ;; (k 10) → 11, returns to the (+ ... (k 20)) expression
;; ;; (k 20) → 21, returns to the (+ 11 ...) expression
;; ;; result: 32
;;
;; ;; Undelimited (call/cc) — k does NOT return:
;; (+ 1 (call/cc (fn (k)
;; (+ (k 10) (k 20)))))
;; ;; (k 10) abandons (+ (k 10) (k 20)) entirely
;; ;; jumps back to (+ 1 _) with 10
;; ;; result: 11
;; ;; (k 20) is never reached
;;
;; --------------------------------------------------------------------------
;; --------------------------------------------------------------------------
;; 2. call/cc — call with current continuation
;; --------------------------------------------------------------------------
(define sf-callcc
(fn (args env)
;; Single argument: a function to call with the current continuation.
(let ((f-expr (first args))
(f (trampoline (eval-expr f-expr env))))
(call-with-cc f env))))
;; --------------------------------------------------------------------------
;; 3. Derived forms
;; --------------------------------------------------------------------------
;;
;; With call/cc available, several patterns become expressible:
;;
;; --- Early return ---
;;
;; (define find-first
;; (fn (pred items)
;; (call/cc (fn (return)
;; (for-each (fn (item)
;; (when (pred item)
;; (return item)))
;; items)
;; nil))))
;;
;; --- Exception-like flow ---
;;
;; (define try-catch
;; (fn (body handler)
;; (call/cc (fn (throw)
;; (body throw)))))
;;
;; (try-catch
;; (fn (throw)
;; (let ((result (dangerous-operation)))
;; (when (not result) (throw "failed"))
;; result))
;; (fn (error) (str "Caught: " error)))
;;
;; --- Coroutines ---
;;
;; Two call/cc captures that alternate control between two
;; computations. Each captures its own continuation, then invokes
;; the other's. This gives cooperative multitasking without threads.
;;
;; --- Undo ---
;;
;; (define with-undo
;; (fn (action)
;; (call/cc (fn (restore)
;; (action)
;; restore))))
;;
;; ;; (let ((undo (with-undo (fn () (delete-item 42)))))
;; ;; (undo "anything")) → item 42 is back
;;
;; --------------------------------------------------------------------------
;; --------------------------------------------------------------------------
;; 4. Interaction with delimited continuations
;; --------------------------------------------------------------------------
;;
;; If both callcc.sx and continuations.sx are loaded:
;;
;; - The continuation type is shared. (continuation? k) returns true
;; for both delimited and undelimited continuations.
;;
;; - shift inside a call/cc body captures up to the nearest reset,
;; not up to the call/cc. The two mechanisms compose.
;;
;; - call/cc inside a reset body captures the entire continuation
;; (past the reset). This is the expected behavior — call/cc is
;; undelimited by definition.
;;
;; - A delimited continuation (from shift) returns a value when invoked.
;; An undelimited continuation (from call/cc) does not return.
;; Both are callable with the same syntax: (k value).
;; The caller cannot distinguish them by type — only by behavior.
;;
;; --------------------------------------------------------------------------
;; --------------------------------------------------------------------------
;; 5. Interaction with I/O and state
;; --------------------------------------------------------------------------
;;
;; Full call/cc has well-known interactions with side effects:
;;
;; Re-entry:
;; Invoking a saved continuation re-enters a completed computation.
;; If that computation mutated state (set!, I/O writes), the mutations
;; are NOT undone. The continuation resumes in the current state,
;; not the state at the time of capture.
;;
;; I/O:
;; Same as delimited continuations — I/O executes at invocation time.
;; A continuation containing (current-user) will call current-user
;; when invoked, in whatever request context exists then.
;;
;; Dynamic extent:
;; call/cc captures the continuation, not the dynamic environment.
;; Host-language context (Python's Quart request context, JavaScript's
;; async context) may not be valid when a saved continuation is invoked
;; later. Typed targets can enforce this; dynamic targets fail at runtime.
;;
;; Recommendation:
;; Use call/cc for pure control flow (early return, coroutines,
;; backtracking). Use delimited continuations for effectful patterns
;; (suspense, cooperative scheduling) where the delimiter provides
;; a natural boundary.
;;
;; --------------------------------------------------------------------------
;; --------------------------------------------------------------------------
;; 6. Implementation notes per target
;; --------------------------------------------------------------------------
;;
;; Scheme / Racket:
;; Native call/cc. Zero implementation effort.
;;
;; Haskell:
;; ContT monad transformer. The evaluator runs in ContT, and call/cc
;; is callCC from Control.Monad.Cont. Natural and type-safe.
;;
;; Python:
;; Requires full CPS transform of the evaluator, or greenlet-based
;; stack capture. Significantly more invasive than delimited
;; continuations. NOT RECOMMENDED — use continuations.sx instead.
;;
;; JavaScript:
;; Requires full CPS transform. Cannot be implemented with generators
;; alone (generators only support delimited yield, not full escape).
;; NOT RECOMMENDED — use continuations.sx instead.
;;
;; Rust:
;; Full CPS transform at compile time. Possible but adds significant
;; complexity. Delimited continuations are more natural (enum-based).
;; Consider only if the target genuinely needs undelimited escape.
;;
;; --------------------------------------------------------------------------
;; --------------------------------------------------------------------------
;; 7. Platform interface — what each target must provide
;; --------------------------------------------------------------------------
;;
;; (call-with-cc f env)
;; Call f with the current continuation. f is a function of one
;; argument (the continuation). If f returns normally, call-with-cc
;; returns f's result. If f invokes the continuation, the computation
;; jumps to the call-with-cc call site with the provided value.
;;
;; (make-continuation fn)
;; Wrap a native function as a continuation value.
;; (Shared with continuations.sx if both are loaded.)
;;
;; (continuation? x)
;; Type predicate.
;; (Shared with continuations.sx if both are loaded.)
;;
;; Continuations must be callable via the standard function-call
;; dispatch in eval-list (same path as lambda calls).
;;
;; --------------------------------------------------------------------------

View File

@@ -0,0 +1,248 @@
;; ==========================================================================
;; continuations.sx — Delimited continuations (shift/reset)
;;
;; OPTIONAL EXTENSION — not required by the core evaluator.
;; Bootstrappers include this only when the target requests it.
;;
;; Delimited continuations capture "the rest of the computation up to
;; a delimiter." They are strictly less powerful than full call/cc but
;; cover the practical use cases: suspendable rendering, cooperative
;; scheduling, linear async flows, wizard forms, and undo.
;;
;; Two new special forms:
;; (reset body) — establish a delimiter
;; (shift k body) — capture the continuation to the nearest reset
;;
;; One new type:
;; continuation — a captured delimited continuation, callable
;;
;; The captured continuation is a function of one argument. Invoking it
;; provides the value that the shift expression "returns" within the
;; delimited context, then completes the rest of the reset body.
;;
;; Continuations are composable — invoking a continuation returns a
;; value (the result of the reset body), which can be used normally.
;; This is the key difference from undelimited call/cc, where invoking
;; a continuation never returns.
;;
;; Platform requirements:
;; (make-continuation fn) — wrap a function as a continuation value
;; (continuation? x) — type predicate
;; (type-of continuation) → "continuation"
;; Continuations are callable (same dispatch as lambda).
;; ==========================================================================
;; --------------------------------------------------------------------------
;; 1. Type
;; --------------------------------------------------------------------------
;;
;; A continuation is a callable value of one argument.
;;
;; (continuation? k) → true if k is a captured continuation
;; (type-of k) → "continuation"
;; (k value) → invoke: resume the captured computation with value
;;
;; Continuations are first-class: they can be stored in variables, passed
;; as arguments, returned from functions, and put in data structures.
;;
;; Invoking a delimited continuation RETURNS a value — the result of the
;; reset body. This makes them composable:
;;
;; (+ 1 (reset (+ 10 (shift k (k 5)))))
;; ;; k is "add 10 to _ and return from reset"
;; ;; (k 5) → 15, which is returned from reset
;; ;; (+ 1 15) → 16
;;
;; --------------------------------------------------------------------------
;; --------------------------------------------------------------------------
;; 2. reset — establish a continuation delimiter
;; --------------------------------------------------------------------------
;;
;; (reset body)
;;
;; Evaluates body in the current environment. If no shift occurs during
;; evaluation of body, reset simply returns the value of body.
;;
;; If shift occurs, reset is the boundary — the continuation captured by
;; shift extends from the shift point back to (and including) this reset.
;;
;; reset is the "prompt" — it marks where the continuation stops.
;;
;; Semantics:
;; (reset expr) where expr contains no shift
;; → (eval expr env) ;; just evaluates normally
;;
;; (reset ... (shift k body) ...)
;; → captures continuation, evaluates shift's body
;; → the result of the shift body is the result of the reset
;;
;; --------------------------------------------------------------------------
(define sf-reset
(fn (args env)
;; Single argument: the body expression.
;; Install a continuation delimiter, then evaluate body.
;; The implementation is target-specific:
;; - In Scheme: native reset/shift
;; - In Haskell: Control.Monad.CC or delimited continuations library
;; - In Python: coroutine/generator-based (see implementation notes)
;; - In JavaScript: generator-based or CPS transform
;; - In Rust: CPS transform at compile time
(let ((body (first args)))
(eval-with-delimiter body env))))
;; --------------------------------------------------------------------------
;; 3. shift — capture the continuation to the nearest reset
;; --------------------------------------------------------------------------
;;
;; (shift k body)
;;
;; Captures the continuation from this point back to the nearest enclosing
;; reset and binds it to k. Then evaluates body in the current environment
;; extended with k. The result of body becomes the result of the enclosing
;; reset.
;;
;; k is a function of one argument. Calling (k value) resumes the captured
;; computation with value standing in for the shift expression.
;;
;; The continuation k is composable: (k value) returns a value (the result
;; of the reset body when resumed with value). This means k can be called
;; multiple times, and its result can be used in further computation.
;;
;; Examples:
;;
;; ;; Basic: shift provides a value to the surrounding computation
;; (reset (+ 1 (shift k (k 41))))
;; ;; k = "add 1 to _", (k 41) → 42, reset returns 42
;;
;; ;; Abort: shift can discard the continuation entirely
;; (reset (+ 1 (shift k "aborted")))
;; ;; k is never called, reset returns "aborted"
;;
;; ;; Multiple invocations: k can be called more than once
;; (reset (+ 1 (shift k (list (k 10) (k 20)))))
;; ;; (k 10) → 11, (k 20) → 21, reset returns (11 21)
;;
;; ;; Stored for later: k can be saved and invoked outside reset
;; (define saved nil)
;; (reset (+ 1 (shift k (set! saved k) 0)))
;; ;; reset returns 0, saved holds the continuation
;; (saved 99) ;; → 100
;;
;; --------------------------------------------------------------------------
(define sf-shift
(fn (args env)
;; Two arguments: the continuation variable name, and the body.
(let ((k-name (symbol-name (first args)))
(body (second args)))
;; Capture the current continuation up to the nearest reset.
;; Bind it to k-name in the environment, then evaluate body.
;; The result of body is returned to the reset.
(capture-continuation k-name body env))))
;; --------------------------------------------------------------------------
;; 4. Interaction with other features
;; --------------------------------------------------------------------------
;;
;; TCO (trampoline):
;; Continuations interact naturally with the trampoline. A shift inside
;; a tail-call position captures the continuation including the pending
;; return. The trampoline resolves thunks before the continuation is
;; delimited.
;;
;; Macros:
;; shift/reset are special forms, not macros. Macros expand before
;; evaluation, so shift inside a macro-expanded form works correctly —
;; it captures the continuation of the expanded code.
;;
;; Components:
;; shift inside a component body captures the continuation of that
;; component's render. The enclosing reset determines the delimiter.
;; This is the foundation for suspendable rendering — a component can
;; shift to suspend, and the server resumes it when data arrives.
;;
;; I/O primitives:
;; I/O primitives execute at invocation time, in whatever context
;; exists then. A continuation that captures a computation containing
;; I/O will re-execute that I/O when invoked. If the I/O requires
;; request context (e.g. current-user), invoking the continuation
;; outside a request will fail — same as calling the I/O directly.
;; This is consistent, not a restriction.
;;
;; In typed targets (Haskell, Rust), the type system can enforce that
;; continuations containing I/O are only invoked in appropriate contexts.
;; In dynamic targets (Python, JS), it fails at runtime.
;;
;; Lexical scope:
;; Continuations capture the dynamic extent (what happens next) but
;; close over the lexical environment at the point of capture. Variable
;; bindings in the continuation refer to the same environment — mutations
;; via set! are visible.
;;
;; --------------------------------------------------------------------------
;; --------------------------------------------------------------------------
;; 5. Implementation notes per target
;; --------------------------------------------------------------------------
;;
;; The bootstrapper emits target-specific continuation machinery.
;; The spec defines semantics; each target chooses representation.
;;
;; Scheme / Racket:
;; Native shift/reset. No transformation needed. The bootstrapper
;; emits (require racket/control) or equivalent.
;;
;; Haskell:
;; Control.Monad.CC provides delimited continuations in the CC monad.
;; Alternatively, the evaluator can be CPS-transformed at compile time.
;; Continuations become first-class functions naturally.
;;
;; Python:
;; Generator-based: reset creates a generator, shift yields from it.
;; The trampoline loop drives the generator. Each yield is a shift
;; point, and send() provides the resume value.
;; Alternative: greenlet-based (stackful coroutines).
;;
;; JavaScript:
;; Generator-based (function* / yield). Similar to Python.
;; Alternative: CPS transform at bootstrap time — the bootstrapper
;; rewrites the evaluator into continuation-passing style, making
;; shift/reset explicit function arguments.
;;
;; Rust:
;; CPS transform at compile time. Continuations become enum variants
;; or boxed closures. The type system ensures continuations are used
;; linearly if desired (affine types via ownership).
;;
;; --------------------------------------------------------------------------
;; --------------------------------------------------------------------------
;; 6. Platform interface — what each target must provide
;; --------------------------------------------------------------------------
;;
;; (eval-with-delimiter expr env)
;; Install a reset delimiter, evaluate expr, return result.
;; If expr calls shift, the continuation is captured up to here.
;;
;; (capture-continuation k-name body env)
;; Capture the current continuation up to the nearest delimiter.
;; Bind it to k-name in env, evaluate body, return result to delimiter.
;;
;; (make-continuation fn)
;; Wrap a native function as a continuation value.
;;
;; (continuation? x)
;; Type predicate.
;;
;; Continuations must be callable via the standard function-call
;; dispatch in eval-list (same path as lambda calls).
;;
;; --------------------------------------------------------------------------

View File

@@ -185,7 +185,10 @@
(cond
(nil? variant)
(append! base-decls decls)
(if (is-child-selector-atom? base)
(append! pseudo-rules
(list ">:not(:first-child)" decls))
(append! base-decls decls))
(dict-has? _responsive-breakpoints variant)
(append! media-rules
@@ -222,23 +225,23 @@
(fn (mr)
(set! hash-input
(str hash-input "@" (first mr) "{" (nth mr 1) "}")))
(chunk-every media-rules 2))
media-rules)
(for-each
(fn (pr)
(set! hash-input
(str hash-input (first pr) "{" (nth pr 1) "}")))
(chunk-every pseudo-rules 2))
pseudo-rules)
(for-each
(fn (kf)
(set! hash-input (str hash-input (nth kf 1))))
(chunk-every kf-needed 2))
kf-needed)
(let ((cn (str "sx-" (hash-style hash-input)))
(sv (make-style-value cn
(join ";" base-decls)
(chunk-every media-rules 2)
(chunk-every pseudo-rules 2)
(chunk-every kf-needed 2))))
media-rules
pseudo-rules
kf-needed)))
(dict-set! _style-cache key sv)
;; Inject CSS rules
(inject-style-value sv atoms)

View File

@@ -136,6 +136,7 @@
(= name "or") (sf-or args env)
(= name "let") (sf-let args env)
(= name "let*") (sf-let args env)
(= name "letrec") (sf-letrec args env)
(= name "lambda") (sf-lambda args env)
(= name "fn") (sf-lambda args env)
(= name "define") (sf-define args env)
@@ -143,13 +144,19 @@
(= name "defmacro") (sf-defmacro args env)
(= name "defstyle") (sf-defstyle args env)
(= name "defkeyframes") (sf-defkeyframes args env)
(= name "defhandler") (sf-define args env)
(= name "defhandler") (sf-defhandler args env)
(= name "defpage") (sf-defpage args env)
(= name "defquery") (sf-defquery args env)
(= name "defaction") (sf-defaction args env)
(= name "begin") (sf-begin args env)
(= name "do") (sf-begin args env)
(= name "quote") (sf-quote args env)
(= name "quasiquote") (sf-quasiquote args env)
(= name "->") (sf-thread-first args env)
(= name "set!") (sf-set! args env)
(= name "reset") (sf-reset args env)
(= name "shift") (sf-shift args env)
(= name "dynamic-wind") (sf-dynamic-wind args env)
;; Higher-order forms
(= name "map") (ho-map args env)
@@ -381,36 +388,83 @@
(define sf-let
(fn (args env)
(let ((bindings (first args))
(body (rest args))
(local (env-extend env)))
;; Parse bindings — support both ((name val) ...) and (name val name val ...)
;; Detect named let: (let name ((x 0) ...) body)
;; If first arg is a symbol, delegate to sf-named-let.
(if (= (type-of (first args)) "symbol")
(sf-named-let args env)
(let ((bindings (first args))
(body (rest args))
(local (env-extend env)))
;; Parse bindings — support both ((name val) ...) and (name val name val ...)
(if (and (= (type-of (first bindings)) "list")
(= (len (first bindings)) 2))
;; Scheme-style
(for-each
(fn (binding)
(let ((vname (if (= (type-of (first binding)) "symbol")
(symbol-name (first binding))
(first binding))))
(env-set! local vname (trampoline (eval-expr (nth binding 1) local)))))
bindings)
;; Clojure-style
(let ((i 0))
(reduce
(fn (acc pair-idx)
(let ((vname (if (= (type-of (nth bindings (* pair-idx 2))) "symbol")
(symbol-name (nth bindings (* pair-idx 2)))
(nth bindings (* pair-idx 2))))
(val-expr (nth bindings (inc (* pair-idx 2)))))
(env-set! local vname (trampoline (eval-expr val-expr local)))))
nil
(range 0 (/ (len bindings) 2)))))
;; Evaluate body — last expression in tail position
(for-each
(fn (e) (trampoline (eval-expr e local)))
(slice body 0 (dec (len body))))
(make-thunk (last body) local)))))
;; Named let: (let name ((x 0) (y 1)) body...)
;; Desugars to a self-recursive lambda called with initial values.
;; The loop name is bound in the body so recursive calls produce TCO thunks.
(define sf-named-let
(fn (args env)
(let ((loop-name (symbol-name (first args)))
(bindings (nth args 1))
(body (slice args 2))
(params (list))
(inits (list)))
;; Extract param names and init expressions
(if (and (= (type-of (first bindings)) "list")
(= (len (first bindings)) 2))
;; Scheme-style
;; Scheme-style: ((x 0) (y 1))
(for-each
(fn (binding)
(let ((vname (if (= (type-of (first binding)) "symbol")
(symbol-name (first binding))
(first binding))))
(env-set! local vname (trampoline (eval-expr (nth binding 1) local)))))
(append! params (if (= (type-of (first binding)) "symbol")
(symbol-name (first binding))
(first binding)))
(append! inits (nth binding 1)))
bindings)
;; Clojure-style
(let ((i 0))
(reduce
(fn (acc pair-idx)
(let ((vname (if (= (type-of (nth bindings (* pair-idx 2))) "symbol")
(symbol-name (nth bindings (* pair-idx 2)))
(nth bindings (* pair-idx 2))))
(val-expr (nth bindings (inc (* pair-idx 2)))))
(env-set! local vname (trampoline (eval-expr val-expr local)))))
nil
(range 0 (/ (len bindings) 2)))))
;; Evaluate body — last expression in tail position
(for-each
(fn (e) (trampoline (eval-expr e local)))
(slice body 0 (dec (len body))))
(make-thunk (last body) local))))
;; Clojure-style: (x 0 y 1)
(reduce
(fn (acc pair-idx)
(do
(append! params (if (= (type-of (nth bindings (* pair-idx 2))) "symbol")
(symbol-name (nth bindings (* pair-idx 2)))
(nth bindings (* pair-idx 2))))
(append! inits (nth bindings (inc (* pair-idx 2))))))
nil
(range 0 (/ (len bindings) 2))))
;; Build loop body (wrap in begin if multiple exprs)
(let ((loop-body (if (= (len body) 1) (first body)
(cons (make-symbol "begin") body)))
(loop-fn (make-lambda params loop-body env)))
;; Self-reference: loop can call itself by name
(set-lambda-name! loop-fn loop-name)
(env-set! (lambda-closure loop-fn) loop-name loop-fn)
;; Evaluate initial values in enclosing env, then call
(let ((init-vals (map (fn (e) (trampoline (eval-expr e env))) inits)))
(call-lambda loop-fn init-vals env))))))
(define sf-lambda
@@ -602,6 +656,109 @@
value)))
;; --------------------------------------------------------------------------
;; 6c. letrec — mutually recursive local bindings
;; --------------------------------------------------------------------------
;;
;; (letrec ((even? (fn (n) (if (= n 0) true (odd? (- n 1)))))
;; (odd? (fn (n) (if (= n 0) false (even? (- n 1))))))
;; (even? 10))
;;
;; All bindings are first set to nil in the local env, then all values
;; are evaluated (so they can see each other's names), then lambda
;; closures are patched to include the final bindings.
;; --------------------------------------------------------------------------
(define sf-letrec
(fn (args env)
(let ((bindings (first args))
(body (rest args))
(local (env-extend env))
(names (list))
(val-exprs (list)))
;; First pass: bind all names to nil
(if (and (= (type-of (first bindings)) "list")
(= (len (first bindings)) 2))
;; Scheme-style
(for-each
(fn (binding)
(let ((vname (if (= (type-of (first binding)) "symbol")
(symbol-name (first binding))
(first binding))))
(append! names vname)
(append! val-exprs (nth binding 1))
(env-set! local vname nil)))
bindings)
;; Clojure-style
(reduce
(fn (acc pair-idx)
(let ((vname (if (= (type-of (nth bindings (* pair-idx 2))) "symbol")
(symbol-name (nth bindings (* pair-idx 2)))
(nth bindings (* pair-idx 2))))
(val-expr (nth bindings (inc (* pair-idx 2)))))
(append! names vname)
(append! val-exprs val-expr)
(env-set! local vname nil)))
nil
(range 0 (/ (len bindings) 2))))
;; Second pass: evaluate values (they can see each other's names)
(let ((values (map (fn (e) (trampoline (eval-expr e local))) val-exprs)))
;; Bind final values
(for-each
(fn (pair) (env-set! local (first pair) (nth pair 1)))
(zip names values))
;; Patch lambda closures so they see the final bindings
(for-each
(fn (val)
(when (lambda? val)
(for-each
(fn (n) (env-set! (lambda-closure val) n (env-get local n)))
names)))
values))
;; Evaluate body
(for-each
(fn (e) (trampoline (eval-expr e local)))
(slice body 0 (dec (len body))))
(make-thunk (last body) local))))
;; --------------------------------------------------------------------------
;; 6d. dynamic-wind — entry/exit guards
;; --------------------------------------------------------------------------
;;
;; (dynamic-wind before-thunk body-thunk after-thunk)
;;
;; All three are zero-argument functions (thunks):
;; 1. Call before-thunk
;; 2. Call body-thunk, capture result
;; 3. Call after-thunk (always, even on error)
;; 4. Return body result
;;
;; The wind stack is maintained so that when continuations jump across
;; dynamic-wind boundaries, the correct before/after thunks fire.
;; Without active continuations, this is equivalent to try/finally.
;;
;; Platform requirements:
;; (push-wind! before after) — push wind record onto stack
;; (pop-wind!) — pop wind record from stack
;; (call-thunk f env) — call a zero-arg function
;; --------------------------------------------------------------------------
(define sf-dynamic-wind
(fn (args env)
(let ((before (trampoline (eval-expr (first args) env)))
(body (trampoline (eval-expr (nth args 1) env)))
(after (trampoline (eval-expr (nth args 2) env))))
;; Call entry thunk
(call-thunk before env)
;; Push wind record, run body, pop, call exit
(push-wind! before after)
(let ((result (call-thunk body env)))
(pop-wind!)
(call-thunk after env)
result))))
;; --------------------------------------------------------------------------
;; 6b. Macro expansion
;; --------------------------------------------------------------------------
@@ -629,18 +786,26 @@
;; 7. Higher-order forms
;; --------------------------------------------------------------------------
;; call-fn: unified caller for HO forms — handles both Lambda and native callable
(define call-fn
(fn (f args env)
(cond
(lambda? f) (trampoline (call-lambda f args env))
(callable? f) (apply f args)
:else (error (str "Not callable in HO form: " (inspect f))))))
(define ho-map
(fn (args env)
(let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(map (fn (item) (trampoline (call-lambda f (list item) env))) coll))))
(map (fn (item) (call-fn f (list item) env)) coll))))
(define ho-map-indexed
(fn (args env)
(let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(map-indexed
(fn (i item) (trampoline (call-lambda f (list i item) env)))
(fn (i item) (call-fn f (list i item) env))
coll))))
(define ho-filter
@@ -648,7 +813,7 @@
(let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(filter
(fn (item) (trampoline (call-lambda f (list item) env)))
(fn (item) (call-fn f (list item) env))
coll))))
(define ho-reduce
@@ -657,7 +822,7 @@
(init (trampoline (eval-expr (nth args 1) env)))
(coll (trampoline (eval-expr (nth args 2) env))))
(reduce
(fn (acc item) (trampoline (call-lambda f (list acc item) env)))
(fn (acc item) (call-fn f (list acc item) env))
init
coll))))
@@ -666,7 +831,7 @@
(let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(some
(fn (item) (trampoline (call-lambda f (list item) env)))
(fn (item) (call-fn f (list item) env))
coll))))
(define ho-every
@@ -674,7 +839,7 @@
(let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(every?
(fn (item) (trampoline (call-lambda f (list item) env)))
(fn (item) (call-fn f (list item) env))
coll))))
@@ -683,7 +848,7 @@
(let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(for-each
(fn (item) (trampoline (call-lambda f (list item) env)))
(fn (item) (call-fn f (list item) env))
coll))))
@@ -765,6 +930,12 @@
;; (apply f args) → call f with args list
;; (zip lists...) → list of tuples
;;
;;
;; CSSX (style system):
;; (build-keyframes name steps env) → StyleValue (platform builds @keyframes)
;;
;; Dynamic wind (for dynamic-wind):
;; (push-wind! before after) → void (push wind record onto stack)
;; (pop-wind!) → void (pop wind record from stack)
;; (call-thunk f env) → value (call a zero-arg function)
;; --------------------------------------------------------------------------

118
shared/sx/ref/forms.sx Normal file
View File

@@ -0,0 +1,118 @@
;; ==========================================================================
;; forms.sx — Server-side definition forms
;;
;; Platform-specific special forms for declaring handlers, pages, queries,
;; and actions. These parse &key parameter lists and create typed definition
;; objects that the server runtime uses for routing and execution.
;;
;; When SX moves to isomorphic execution, these forms will have different
;; platform bindings on client vs server. The spec stays the same — only
;; the constructors (make-handler-def, make-query-def, etc.) change.
;;
;; Platform functions required:
;; make-handler-def(name, params, body, env) → HandlerDef
;; make-query-def(name, params, doc, body, env) → QueryDef
;; make-action-def(name, params, doc, body, env) → ActionDef
;; make-page-def(name, slots, env) → PageDef
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Shared: parse (&key param1 param2 ...) → list of param name strings
;; --------------------------------------------------------------------------
(define parse-key-params
(fn (params-expr)
(let ((params (list))
(in-key false))
(for-each
(fn (p)
(when (= (type-of p) "symbol")
(let ((name (symbol-name p)))
(cond
(= name "&key") (set! in-key true)
in-key (append! params name)
:else (append! params name)))))
params-expr)
params)))
;; --------------------------------------------------------------------------
;; defhandler — (defhandler name (&key param...) body)
;; --------------------------------------------------------------------------
(define sf-defhandler
(fn (args env)
(let ((name-sym (first args))
(params-raw (nth args 1))
(body (nth args 2))
(name (symbol-name name-sym))
(params (parse-key-params params-raw)))
(let ((hdef (make-handler-def name params body env)))
(env-set! env (str "handler:" name) hdef)
hdef))))
;; --------------------------------------------------------------------------
;; defquery — (defquery name (&key param...) "docstring" body)
;; --------------------------------------------------------------------------
(define sf-defquery
(fn (args env)
(let ((name-sym (first args))
(params-raw (nth args 1))
(name (symbol-name name-sym))
(params (parse-key-params params-raw))
;; Optional docstring before body
(has-doc (and (>= (len args) 4) (= (type-of (nth args 2)) "string")))
(doc (if has-doc (nth args 2) ""))
(body (if has-doc (nth args 3) (nth args 2))))
(let ((qdef (make-query-def name params doc body env)))
(env-set! env (str "query:" name) qdef)
qdef))))
;; --------------------------------------------------------------------------
;; defaction — (defaction name (&key param...) "docstring" body)
;; --------------------------------------------------------------------------
(define sf-defaction
(fn (args env)
(let ((name-sym (first args))
(params-raw (nth args 1))
(name (symbol-name name-sym))
(params (parse-key-params params-raw))
(has-doc (and (>= (len args) 4) (= (type-of (nth args 2)) "string")))
(doc (if has-doc (nth args 2) ""))
(body (if has-doc (nth args 3) (nth args 2))))
(let ((adef (make-action-def name params doc body env)))
(env-set! env (str "action:" name) adef)
adef))))
;; --------------------------------------------------------------------------
;; defpage — (defpage name :path "/..." :auth :public :content expr ...)
;;
;; Keyword-slot form: all values after the name are :key value pairs.
;; Values are stored as unevaluated AST — resolved at request time.
;; --------------------------------------------------------------------------
(define sf-defpage
(fn (args env)
(let ((name-sym (first args))
(name (symbol-name name-sym))
(slots {}))
;; Parse keyword slots from remaining args
(let ((i 1)
(max-i (len args)))
(for-each
(fn (idx)
(when (and (< idx max-i)
(= (type-of (nth args idx)) "keyword"))
(when (< (+ idx 1) max-i)
(dict-set! slots (keyword-name (nth args idx))
(nth args (+ idx 1))))))
(range 1 max-i 2)))
(let ((pdef (make-page-def name slots env)))
(env-set! env (str "page:" name) pdef)
pdef))))

View File

@@ -4,10 +4,16 @@
;; Defines how SX source text is tokenized and parsed into AST.
;; The parser is intentionally simple — s-expressions need minimal parsing.
;;
;; Single-pass recursive descent: reads source text directly into AST,
;; no separate tokenization phase. All mutable cursor state lives inside
;; the parse closure.
;;
;; Grammar:
;; program → expr*
;; expr → atom | list | quote-sugar
;; expr → atom | list | vector | map | quote-sugar
;; list → '(' expr* ')'
;; vector → '[' expr* ']' (sugar for list)
;; map → '{' (key expr)* '}'
;; atom → string | number | keyword | symbol | boolean | nil
;; string → '"' (char | escape)* '"'
;; number → '-'? digit+ ('.' digit+)? ([eE] [+-]? digit+)?
@@ -15,316 +21,264 @@
;; symbol → ident
;; boolean → 'true' | 'false'
;; nil → 'nil'
;; ident → [a-zA-Z_~*+\-><=/!?&] [a-zA-Z0-9_~*+\-><=/!?.:&]*
;; ident → ident-start ident-char*
;; comment → ';' to end of line (discarded)
;;
;; Dict literal:
;; {key val ...} → dict object (keys are keywords or expressions)
;;
;; Quote sugar:
;; `(expr) → (quasiquote expr)
;; ,(expr) → (unquote expr)
;; ,@(expr) → (splice-unquote expr)
;; `expr → (quasiquote expr)
;; ,expr → (unquote expr)
;; ,@expr → (splice-unquote expr)
;;
;; Platform interface (each target implements natively):
;; (ident-start? ch) → boolean
;; (ident-char? ch) → boolean
;; (make-symbol name) → Symbol value
;; (make-keyword name) → Keyword value
;; (escape-string s) → string with " and \ escaped for serialization
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Tokenizer
;; Parser — single-pass recursive descent
;; --------------------------------------------------------------------------
;; Produces a flat stream of tokens from source text.
;; Each token is a (type value line col) tuple.
;; Returns a list of top-level AST expressions.
(define tokenize
(define sx-parse
(fn (source)
(let ((pos 0)
(line 1)
(col 1)
(tokens (list))
(len-src (len source)))
;; Main loop — bootstrap compilers convert to while
(define scan-next
;; -- Cursor helpers (closure over pos, source, len-src) --
(define skip-comment
(fn ()
(when (and (< pos len-src) (not (= (nth source pos) "\n")))
(set! pos (inc pos))
(skip-comment))))
(define skip-ws
(fn ()
(when (< pos len-src)
(let ((ch (nth source pos)))
(cond
;; Whitespace — skip
(whitespace? ch)
(do (advance-pos!) (scan-next))
;; Whitespace
(or (= ch " ") (= ch "\t") (= ch "\n") (= ch "\r"))
(do (set! pos (inc pos)) (skip-ws))
;; Comment — skip to end of line
(= ch ";")
(do (skip-to-eol!) (scan-next))
(do (set! pos (inc pos))
(skip-comment)
(skip-ws))
;; Not whitespace or comment — stop
:else nil)))))
;; -- Atom readers --
(define read-string
(fn ()
(set! pos (inc pos)) ;; skip opening "
(let ((buf ""))
(define read-str-loop
(fn ()
(if (>= pos len-src)
(error "Unterminated string")
(let ((ch (nth source pos)))
(cond
(= ch "\"")
(do (set! pos (inc pos)) nil) ;; done
(= ch "\\")
(do (set! pos (inc pos))
(let ((esc (nth source pos)))
(set! buf (str buf
(cond
(= esc "n") "\n"
(= esc "t") "\t"
(= esc "r") "\r"
:else esc)))
(set! pos (inc pos))
(read-str-loop)))
:else
(do (set! buf (str buf ch))
(set! pos (inc pos))
(read-str-loop)))))))
(read-str-loop)
buf)))
(define read-ident
(fn ()
(let ((start pos))
(define read-ident-loop
(fn ()
(when (and (< pos len-src)
(ident-char? (nth source pos)))
(set! pos (inc pos))
(read-ident-loop))))
(read-ident-loop)
(slice source start pos))))
(define read-keyword
(fn ()
(set! pos (inc pos)) ;; skip :
(make-keyword (read-ident))))
(define read-number
(fn ()
(let ((start pos))
;; Optional leading minus
(when (and (< pos len-src) (= (nth source pos) "-"))
(set! pos (inc pos)))
;; Integer digits
(define read-digits
(fn ()
(when (and (< pos len-src)
(let ((c (nth source pos)))
(and (>= c "0") (<= c "9"))))
(set! pos (inc pos))
(read-digits))))
(read-digits)
;; Decimal part
(when (and (< pos len-src) (= (nth source pos) "."))
(set! pos (inc pos))
(read-digits))
;; Exponent
(when (and (< pos len-src)
(or (= (nth source pos) "e")
(= (nth source pos) "E")))
(set! pos (inc pos))
(when (and (< pos len-src)
(or (= (nth source pos) "+")
(= (nth source pos) "-")))
(set! pos (inc pos)))
(read-digits))
(parse-number (slice source start pos)))))
(define read-symbol
(fn ()
(let ((name (read-ident)))
(cond
(= name "true") true
(= name "false") false
(= name "nil") nil
:else (make-symbol name)))))
;; -- Composite readers --
(define read-list
(fn (close-ch)
(let ((items (list)))
(define read-list-loop
(fn ()
(skip-ws)
(if (>= pos len-src)
(error "Unterminated list")
(if (= (nth source pos) close-ch)
(do (set! pos (inc pos)) nil) ;; done
(do (append! items (read-expr))
(read-list-loop))))))
(read-list-loop)
items)))
(define read-map
(fn ()
(let ((result (dict)))
(define read-map-loop
(fn ()
(skip-ws)
(if (>= pos len-src)
(error "Unterminated map")
(if (= (nth source pos) "}")
(do (set! pos (inc pos)) nil) ;; done
(let ((key-expr (read-expr))
(key-str (if (= (type-of key-expr) "keyword")
(keyword-name key-expr)
(str key-expr)))
(val-expr (read-expr)))
(dict-set! result key-str val-expr)
(read-map-loop))))))
(read-map-loop)
result)))
;; -- Main expression reader --
(define read-expr
(fn ()
(skip-ws)
(if (>= pos len-src)
(error "Unexpected end of input")
(let ((ch (nth source pos)))
(cond
;; Lists
(= ch "(")
(do (set! pos (inc pos)) (read-list ")"))
(= ch "[")
(do (set! pos (inc pos)) (read-list "]"))
;; Map
(= ch "{")
(do (set! pos (inc pos)) (read-map))
;; String
(= ch "\"")
(do (append! tokens (scan-string)) (scan-next))
;; Open paren
(= ch "(")
(do (append! tokens (list "lparen" "(" line col))
(advance-pos!)
(scan-next))
;; Close paren
(= ch ")")
(do (append! tokens (list "rparen" ")" line col))
(advance-pos!)
(scan-next))
;; Open bracket (list sugar)
(= ch "[")
(do (append! tokens (list "lbracket" "[" line col))
(advance-pos!)
(scan-next))
;; Close bracket
(= ch "]")
(do (append! tokens (list "rbracket" "]" line col))
(advance-pos!)
(scan-next))
;; Open brace (dict literal)
(= ch "{")
(do (append! tokens (list "lbrace" "{" line col))
(advance-pos!)
(scan-next))
;; Close brace
(= ch "}")
(do (append! tokens (list "rbrace" "}" line col))
(advance-pos!)
(scan-next))
;; Quasiquote sugar
(= ch "`")
(do (advance-pos!)
(let ((inner (scan-next-expr)))
(append! tokens (list "quasiquote" inner line col))
(scan-next)))
;; Unquote / splice-unquote
(= ch ",")
(do (advance-pos!)
(if (and (< pos len-src) (= (nth source pos) "@"))
(do (advance-pos!)
(let ((inner (scan-next-expr)))
(append! tokens (list "splice-unquote" inner line col))
(scan-next)))
(let ((inner (scan-next-expr)))
(append! tokens (list "unquote" inner line col))
(scan-next))))
(read-string)
;; Keyword
(= ch ":")
(do (append! tokens (scan-keyword)) (scan-next))
(read-keyword)
;; Quasiquote sugar
(= ch "`")
(do (set! pos (inc pos))
(list (make-symbol "quasiquote") (read-expr)))
;; Unquote / splice-unquote
(= ch ",")
(do (set! pos (inc pos))
(if (and (< pos len-src) (= (nth source pos) "@"))
(do (set! pos (inc pos))
(list (make-symbol "splice-unquote") (read-expr)))
(list (make-symbol "unquote") (read-expr))))
;; Number (or negative number)
(or (digit? ch)
(and (= ch "-") (< (inc pos) len-src)
(digit? (nth source (inc pos)))))
(do (append! tokens (scan-number)) (scan-next))
(or (and (>= ch "0") (<= ch "9"))
(and (= ch "-")
(< (inc pos) len-src)
(let ((next-ch (nth source (inc pos))))
(and (>= next-ch "0") (<= next-ch "9")))))
(read-number)
;; Symbol
;; Ellipsis (... as a symbol)
(and (= ch ".")
(< (+ pos 2) len-src)
(= (nth source (+ pos 1)) ".")
(= (nth source (+ pos 2)) "."))
(do (set! pos (+ pos 3))
(make-symbol "..."))
;; Symbol (must be ident-start char)
(ident-start? ch)
(do (append! tokens (scan-symbol)) (scan-next))
(read-symbol)
;; Unknown — skip
;; Unexpected
:else
(do (advance-pos!) (scan-next)))))))
(scan-next)
tokens)))
(error (str "Unexpected character: " ch)))))))
;; --------------------------------------------------------------------------
;; Token scanners (pseudo-code — each target implements natively)
;; --------------------------------------------------------------------------
(define scan-string
(fn ()
;; Scan from opening " to closing ", handling escape sequences.
;; Returns ("string" value line col).
;; Escape sequences: \" \\ \n \t \r
(let ((start-line line)
(start-col col)
(result ""))
(advance-pos!) ;; skip opening "
(define scan-str-loop
(fn ()
(if (>= pos (len source))
(error "Unterminated string")
(let ((ch (nth source pos)))
(cond
(= ch "\"")
(do (advance-pos!) nil) ;; done
(= ch "\\")
(do (advance-pos!)
(let ((esc (nth source pos)))
(set! result (str result
(case esc
"n" "\n"
"t" "\t"
"r" "\r"
:else esc)))
(advance-pos!)
(scan-str-loop)))
:else
(do (set! result (str result ch))
(advance-pos!)
(scan-str-loop)))))))
(scan-str-loop)
(list "string" result start-line start-col))))
(define scan-keyword
(fn ()
;; Scan :identifier
(let ((start-line line) (start-col col))
(advance-pos!) ;; skip :
(let ((name (scan-ident-chars)))
(list "keyword" name start-line start-col)))))
(define scan-number
(fn ()
;; Scan integer or float literal
(let ((start-line line) (start-col col) (buf ""))
(when (= (nth source pos) "-")
(set! buf "-")
(advance-pos!))
;; Integer part
(define scan-digits
(fn ()
(when (and (< pos (len source)) (digit? (nth source pos)))
(set! buf (str buf (nth source pos)))
(advance-pos!)
(scan-digits))))
(scan-digits)
;; Decimal part
(when (and (< pos (len source)) (= (nth source pos) "."))
(set! buf (str buf "."))
(advance-pos!)
(scan-digits))
;; Exponent
(when (and (< pos (len source))
(or (= (nth source pos) "e") (= (nth source pos) "E")))
(set! buf (str buf (nth source pos)))
(advance-pos!)
(when (and (< pos (len source))
(or (= (nth source pos) "+") (= (nth source pos) "-")))
(set! buf (str buf (nth source pos)))
(advance-pos!))
(scan-digits))
(list "number" (parse-number buf) start-line start-col))))
(define scan-symbol
(fn ()
;; Scan identifier, check for true/false/nil
(let ((start-line line)
(start-col col)
(name (scan-ident-chars)))
(cond
(= name "true") (list "boolean" true start-line start-col)
(= name "false") (list "boolean" false start-line start-col)
(= name "nil") (list "nil" nil start-line start-col)
:else (list "symbol" name start-line start-col)))))
;; --------------------------------------------------------------------------
;; Parser — tokens → AST
;; --------------------------------------------------------------------------
(define parse
(fn (tokens)
;; Parse all top-level expressions from token stream.
(let ((pos 0)
(exprs (list)))
(define parse-loop
(fn ()
(when (< pos (len tokens))
(let ((result (parse-expr tokens)))
(append! exprs result)
(parse-loop)))))
(parse-loop)
exprs)))
(define parse-expr
(fn (tokens)
;; Parse a single expression.
(let ((tok (nth tokens pos)))
(case (first tok) ;; token type
"lparen"
(do (set! pos (inc pos))
(parse-list tokens "rparen"))
"lbracket"
(do (set! pos (inc pos))
(parse-list tokens "rbracket"))
"lbrace"
(do (set! pos (inc pos))
(parse-dict tokens))
"string" (do (set! pos (inc pos)) (nth tok 1))
"number" (do (set! pos (inc pos)) (nth tok 1))
"boolean" (do (set! pos (inc pos)) (nth tok 1))
"nil" (do (set! pos (inc pos)) nil)
"keyword"
(do (set! pos (inc pos))
(make-keyword (nth tok 1)))
"symbol"
(do (set! pos (inc pos))
(make-symbol (nth tok 1)))
:else (error (str "Unexpected token: " (inspect tok)))))))
(define parse-list
(fn (tokens close-type)
;; Parse expressions until close-type token.
(let ((items (list)))
(define parse-list-loop
(fn ()
(if (>= pos (len tokens))
(error "Unterminated list")
(if (= (first (nth tokens pos)) close-type)
(do (set! pos (inc pos)) nil) ;; done
(do (append! items (parse-expr tokens))
(parse-list-loop))))))
(parse-list-loop)
items)))
(define parse-dict
(fn (tokens)
;; Parse {key val key val ...} until "rbrace" token.
;; Returns a dict (plain object).
(let ((result (dict)))
(define parse-dict-loop
(fn ()
(if (>= pos (len tokens))
(error "Unterminated dict")
(if (= (first (nth tokens pos)) "rbrace")
(do (set! pos (inc pos)) nil) ;; done
(let ((key-expr (parse-expr tokens))
(key-str (if (= (type-of key-expr) "keyword")
(keyword-name key-expr)
(str key-expr)))
(val-expr (parse-expr tokens)))
(dict-set! result key-str val-expr)
(parse-dict-loop))))))
(parse-dict-loop)
result)))
;; -- Entry point: parse all top-level expressions --
(let ((exprs (list)))
(define parse-loop
(fn ()
(skip-ws)
(when (< pos len-src)
(append! exprs (read-expr))
(parse-loop))))
(parse-loop)
exprs))))
;; --------------------------------------------------------------------------
;; Serializer — AST → SX source text
;; --------------------------------------------------------------------------
(define serialize
(define sx-serialize
(fn (val)
(case (type-of val)
"nil" "nil"
@@ -333,46 +287,41 @@
"string" (str "\"" (escape-string val) "\"")
"symbol" (symbol-name val)
"keyword" (str ":" (keyword-name val))
"list" (str "(" (join " " (map serialize val)) ")")
"dict" (serialize-dict val)
"list" (str "(" (join " " (map sx-serialize val)) ")")
"dict" (sx-serialize-dict val)
"sx-expr" (sx-expr-source val)
:else (str val))))
(define serialize-dict
(define sx-serialize-dict
(fn (d)
(str "(dict "
(str "{"
(join " "
(reduce
(fn (acc key)
(concat acc (list (str ":" key) (serialize (dict-get d key)))))
(concat acc (list (str ":" key) (sx-serialize (dict-get d key)))))
(list)
(keys d)))
")")))
"}")))
;; --------------------------------------------------------------------------
;; Platform parser interface
;; --------------------------------------------------------------------------
;;
;; Character classification:
;; (whitespace? ch) → boolean
;; (digit? ch) → boolean
;; (ident-start? ch) → boolean (letter, _, ~, *, +, -, etc.)
;; (ident-char? ch) → boolean (ident-start + digits, ., :)
;; Character classification (implemented natively per target):
;; (ident-start? ch) → boolean
;; True for: a-z A-Z _ ~ * + - > < = / ! ? &
;;
;; Constructors:
;; (make-symbol name) → Symbol value
;; (make-keyword name) → Keyword value
;; (parse-number s) → number (int or float from string)
;; (ident-char? ch) → boolean
;; True for: ident-start chars plus: 0-9 . : / [ ] # ,
;;
;; Constructors (provided by the SX runtime):
;; (make-symbol name) → Symbol value
;; (make-keyword name) → Keyword value
;; (parse-number s) → number (int or float from string)
;;
;; String utilities:
;; (escape-string s) → string with " and \ escaped
;; (sx-expr-source e) → unwrap SxExpr to its source string
;;
;; Cursor state (mutable — each target manages its own way):
;; pos, line, col — current position in source
;; (advance-pos!) → increment pos, update line/col
;; (skip-to-eol!) → advance past end of line
;; (scan-ident-chars) → consume and return identifier string
;; (escape-string s) → string with " and \ escaped
;; (sx-expr-source e) → unwrap SxExpr to its source string
;; --------------------------------------------------------------------------

View File

@@ -18,13 +18,19 @@
;; The :body is optional — when provided, it gives a reference
;; implementation in SX that bootstrap compilers MAY use for testing
;; or as a fallback. Most targets will implement natively for performance.
;;
;; Modules: (define-module :name) scopes subsequent define-primitive
;; entries until the next define-module. Bootstrappers use this to
;; selectively include primitive groups.
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Arithmetic
;; Core — Arithmetic
;; --------------------------------------------------------------------------
(define-module :core.arithmetic)
(define-primitive "+"
:params (&rest args)
:returns "number"
@@ -115,13 +121,15 @@
;; --------------------------------------------------------------------------
;; Comparison
;; Core — Comparison
;; --------------------------------------------------------------------------
(define-module :core.comparison)
(define-primitive "="
:params (a b)
:returns "boolean"
:doc "Equality (value equality, not identity).")
:doc "Deep structural equality. Alias for equal?.")
(define-primitive "!="
:params (a b)
@@ -129,6 +137,27 @@
:doc "Inequality."
:body (not (= a b)))
(define-primitive "eq?"
:params (a b)
:returns "boolean"
:doc "Identity equality. True only if a and b are the exact same object.
For immutable atoms (numbers, strings, booleans, nil) this may or
may not match — use eqv? for reliable atom comparison.")
(define-primitive "eqv?"
:params (a b)
:returns "boolean"
:doc "Equivalent value for atoms, identity for compound objects.
Returns true for identical objects (eq?), and also for numbers,
strings, booleans, and nil with the same value. For lists, dicts,
lambdas, and components, only true if same identity.")
(define-primitive "equal?"
:params (a b)
:returns "boolean"
:doc "Deep structural equality. Recursively compares lists and dicts.
Same semantics as = but explicit Scheme name.")
(define-primitive "<"
:params (a b)
:returns "boolean"
@@ -151,9 +180,11 @@
;; --------------------------------------------------------------------------
;; Predicates
;; Core — Predicates
;; --------------------------------------------------------------------------
(define-module :core.predicates)
(define-primitive "odd?"
:params (n)
:returns "boolean"
@@ -197,6 +228,11 @@
:returns "boolean"
:doc "True if x is a dict/map.")
(define-primitive "continuation?"
:params (x)
:returns "boolean"
:doc "True if x is a captured continuation.")
(define-primitive "empty?"
:params (coll)
:returns "boolean"
@@ -209,9 +245,11 @@
;; --------------------------------------------------------------------------
;; Logic
;; Core — Logic
;; --------------------------------------------------------------------------
(define-module :core.logic)
(define-primitive "not"
:params (x)
:returns "boolean"
@@ -219,9 +257,11 @@
;; --------------------------------------------------------------------------
;; Strings
;; Core — Strings
;; --------------------------------------------------------------------------
(define-module :core.strings)
(define-primitive "str"
:params (&rest args)
:returns "string"
@@ -279,9 +319,11 @@
;; --------------------------------------------------------------------------
;; Collections — construction
;; Core — Collections
;; --------------------------------------------------------------------------
(define-module :core.collections)
(define-primitive "list"
:params (&rest args)
:returns "list"
@@ -297,11 +339,6 @@
:returns "list"
:doc "Integer range [start, end) with optional step.")
;; --------------------------------------------------------------------------
;; Collections — access
;; --------------------------------------------------------------------------
(define-primitive "get"
:params (coll key &rest default)
:returns "any"
@@ -354,9 +391,11 @@
;; --------------------------------------------------------------------------
;; Collectionsdict operations
;; CoreDict operations
;; --------------------------------------------------------------------------
(define-module :core.dict)
(define-primitive "keys"
:params (d)
:returns "list"
@@ -389,9 +428,11 @@
;; --------------------------------------------------------------------------
;; Format helpers
;; Stdlib — Format
;; --------------------------------------------------------------------------
(define-module :stdlib.format)
(define-primitive "format-date"
:params (date-str fmt)
:returns "string"
@@ -407,11 +448,18 @@
:returns "number"
:doc "Parse string to integer with optional default on failure.")
(define-primitive "parse-datetime"
:params (s)
:returns "string"
:doc "Parse datetime string — identity passthrough (returns string or nil).")
;; --------------------------------------------------------------------------
;; Text helpers
;; Stdlib — Text
;; --------------------------------------------------------------------------
(define-module :stdlib.text)
(define-primitive "pluralize"
:params (count &rest forms)
:returns "string"
@@ -429,23 +477,10 @@
;; --------------------------------------------------------------------------
;; Date & parsing helpers
;; Stdlib — Style
;; --------------------------------------------------------------------------
(define-primitive "parse-datetime"
:params (s)
:returns "string"
:doc "Parse datetime string — identity passthrough (returns string or nil).")
(define-primitive "split-ids"
:params (s)
:returns "list"
:doc "Split comma-separated ID string into list of trimmed non-empty strings.")
;; --------------------------------------------------------------------------
;; CSSX — style system primitives
;; --------------------------------------------------------------------------
(define-module :stdlib.style)
(define-primitive "css"
:params (&rest atoms)
@@ -457,3 +492,15 @@
:params (&rest styles)
:returns "style-value"
:doc "Merge multiple StyleValues into one combined StyleValue.")
;; --------------------------------------------------------------------------
;; Stdlib — Debug
;; --------------------------------------------------------------------------
(define-module :stdlib.debug)
(define-primitive "assert"
:params (condition &rest message)
:returns "boolean"
:doc "Assert condition is truthy; raise error with message if not.")

View File

@@ -0,0 +1,412 @@
;; ==========================================================================
;; special-forms.sx — Specification of all SX special forms
;;
;; Special forms are syntactic constructs whose arguments are NOT evaluated
;; before dispatch. Each form has its own evaluation rules — unlike primitives,
;; which receive pre-evaluated values.
;;
;; This file is a SPECIFICATION, not executable code. Bootstrap compilers
;; consume these declarations but implement special forms natively.
;;
;; Format:
;; (define-special-form "name"
;; :syntax (name arg1 arg2 ...)
;; :doc "description"
;; :tail-position "which subexpressions are in tail position"
;; :example "(name ...)")
;;
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Control flow
;; --------------------------------------------------------------------------
(define-special-form "if"
:syntax (if condition then-expr else-expr)
:doc "If condition is truthy, evaluate then-expr; otherwise evaluate else-expr.
Both branches are in tail position. The else branch is optional and
defaults to nil."
:tail-position "then-expr, else-expr"
:example "(if (> x 10) \"big\" \"small\")")
(define-special-form "when"
:syntax (when condition body ...)
:doc "If condition is truthy, evaluate all body expressions sequentially.
Returns the value of the last body expression, or nil if condition
is falsy. Only the last body expression is in tail position."
:tail-position "last body expression"
:example "(when (logged-in? user)
(render-dashboard user))")
(define-special-form "cond"
:syntax (cond test1 result1 test2 result2 ... :else default)
:doc "Multi-way conditional. Tests are evaluated in order; the result
paired with the first truthy test is returned. The :else keyword
(or the symbol else) matches unconditionally. Supports both
Clojure-style flat pairs and Scheme-style nested pairs:
Clojure: (cond test1 result1 test2 result2 :else default)
Scheme: (cond (test1 result1) (test2 result2) (else default))"
:tail-position "all result expressions"
:example "(cond
(= status \"active\") (render-active item)
(= status \"draft\") (render-draft item)
:else (render-unknown item))")
(define-special-form "case"
:syntax (case expr val1 result1 val2 result2 ... :else default)
:doc "Match expr against values using equality. Like cond but tests
a single expression against multiple values. The :else keyword
matches if no values match."
:tail-position "all result expressions"
:example "(case (get request \"method\")
\"GET\" (handle-get request)
\"POST\" (handle-post request)
:else (method-not-allowed))")
(define-special-form "and"
:syntax (and expr ...)
:doc "Short-circuit logical AND. Evaluates expressions left to right.
Returns the first falsy value, or the last value if all are truthy.
Returns true if given no arguments."
:tail-position "last expression"
:example "(and (valid? input) (authorized? user) (process input))")
(define-special-form "or"
:syntax (or expr ...)
:doc "Short-circuit logical OR. Evaluates expressions left to right.
Returns the first truthy value, or the last value if all are falsy.
Returns false if given no arguments."
:tail-position "last expression"
:example "(or (get cache key) (fetch-from-db key) \"default\")")
;; --------------------------------------------------------------------------
;; Binding
;; --------------------------------------------------------------------------
(define-special-form "let"
:syntax (let bindings body ...)
:doc "Create local bindings and evaluate body in the extended environment.
Bindings can be Scheme-style ((name val) ...) or Clojure-style
(name val name val ...). Each binding can see previous bindings.
Only the last body expression is in tail position.
Named let: (let name ((x init) ...) body) creates a loop. The name
is bound to a function that takes the same params and recurses with
tail-call optimization."
:tail-position "last body expression; recursive call in named let"
:example ";; Basic let
(let ((x 10) (y 20))
(+ x y))
;; Clojure-style
(let (x 10 y 20)
(+ x y))
;; Named let (loop)
(let loop ((i 0) (acc 0))
(if (= i 100)
acc
(loop (+ i 1) (+ acc i))))")
(define-special-form "let*"
:syntax (let* bindings body ...)
:doc "Alias for let. In SX, let is already sequential (each binding
sees previous ones), so let* is identical to let."
:tail-position "last body expression"
:example "(let* ((x 10) (y (* x 2)))
(+ x y)) ;; → 30")
(define-special-form "letrec"
:syntax (letrec bindings body ...)
:doc "Mutually recursive local bindings. All names are bound to nil first,
then all values are evaluated (so they can reference each other),
then lambda closures are patched to include the final bindings.
Used for defining mutually recursive local functions."
:tail-position "last body expression"
:example "(letrec ((even? (fn (n) (if (= n 0) true (odd? (- n 1)))))
(odd? (fn (n) (if (= n 0) false (even? (- n 1))))))
(even? 10)) ;; → true")
(define-special-form "define"
:syntax (define name value)
:doc "Bind name to value in the current environment. If value is a lambda
and has no name, the lambda's name is set to the symbol name.
Returns the value."
:tail-position "none (value is eagerly evaluated)"
:example "(define greeting \"hello\")
(define double (fn (x) (* x 2)))")
(define-special-form "set!"
:syntax (set! name value)
:doc "Mutate an existing binding. The name must already be bound in the
current environment. Returns the new value."
:tail-position "none (value is eagerly evaluated)"
:example "(let (count 0)
(set! count (+ count 1)))")
;; --------------------------------------------------------------------------
;; Functions and components
;; --------------------------------------------------------------------------
(define-special-form "lambda"
:syntax (lambda params body)
:doc "Create a function. Params is a list of parameter names. Body is
a single expression (the return value). The lambda captures the
current environment as its closure."
:tail-position "body"
:example "(lambda (x y) (+ x y))")
(define-special-form "fn"
:syntax (fn params body)
:doc "Alias for lambda."
:tail-position "body"
:example "(fn (x) (* x x))")
(define-special-form "defcomp"
:syntax (defcomp ~name (&key param1 param2 &rest children) body)
:doc "Define a component. Components are called with keyword arguments
and optional positional children. The &key marker introduces
keyword parameters. The &rest (or &children) marker captures
remaining positional arguments as a list.
Component names conventionally start with ~ to distinguish them
from HTML elements. Components are evaluated with a merged
environment: closure + caller-env + bound-params."
:tail-position "body"
:example "(defcomp ~card (&key title subtitle &rest children)
(div :class \"card\"
(h2 title)
(when subtitle (p subtitle))
children))")
(define-special-form "defmacro"
:syntax (defmacro name (params ...) body)
:doc "Define a macro. Macros receive their arguments unevaluated (as raw
AST) and return a new expression that is then evaluated. The
returned expression replaces the macro call. Use quasiquote for
template construction."
:tail-position "none (expansion is evaluated separately)"
:example "(defmacro unless (condition &rest body)
`(when (not ~condition) ~@body))")
;; --------------------------------------------------------------------------
;; Sequencing and threading
;; --------------------------------------------------------------------------
(define-special-form "begin"
:syntax (begin expr ...)
:doc "Evaluate expressions sequentially. Returns the value of the last
expression. Used when multiple side-effecting expressions need
to be grouped."
:tail-position "last expression"
:example "(begin
(log \"starting\")
(process data)
(log \"done\"))")
(define-special-form "do"
:syntax (do expr ...)
:doc "Alias for begin."
:tail-position "last expression"
:example "(do (set! x 1) (set! y 2) (+ x y))")
(define-special-form "->"
:syntax (-> value form1 form2 ...)
:doc "Thread-first macro. Threads value through a series of function calls,
inserting it as the first argument of each form. Nested lists are
treated as function calls; bare symbols become unary calls."
:tail-position "last form"
:example "(-> user
(get \"name\")
upper
(str \" says hello\"))
;; Expands to: (str (upper (get user \"name\")) \" says hello\")")
;; --------------------------------------------------------------------------
;; Quoting
;; --------------------------------------------------------------------------
(define-special-form "quote"
:syntax (quote expr)
:doc "Return expr as data, without evaluating it. Symbols remain symbols,
lists remain lists. The reader shorthand is the ' prefix."
:tail-position "none (not evaluated)"
:example "'(+ 1 2) ;; → the list (+ 1 2), not the number 3")
(define-special-form "quasiquote"
:syntax (quasiquote expr)
:doc "Template construction. Like quote, but allows unquoting with ~ and
splicing with ~@. The reader shorthand is the ` prefix.
`(a ~b ~@c)
Quotes everything except: ~expr evaluates expr and inserts the
result; ~@expr evaluates to a list and splices its elements."
:tail-position "none (template is constructed, not evaluated)"
:example "`(div :class \"card\" ~title ~@children)")
;; --------------------------------------------------------------------------
;; Continuations
;; --------------------------------------------------------------------------
(define-special-form "reset"
:syntax (reset body)
:doc "Establish a continuation delimiter. Evaluates body normally unless
a shift is encountered, in which case the continuation (the rest
of the computation up to this reset) is captured and passed to
the shift's body. Without shift, reset is a no-op wrapper."
:tail-position "body"
:example "(reset (+ 1 (shift k (k 10)))) ;; → 11")
(define-special-form "shift"
:syntax (shift k body)
:doc "Capture the continuation to the nearest reset as k, then evaluate
body with k bound. If k is never called, the value of body is
returned from the reset (abort). If k is called with a value,
the reset body is re-evaluated with shift returning that value.
k can be called multiple times."
:tail-position "body"
:example ";; Abort: shift body becomes the reset result
(reset (+ 1 (shift k 42))) ;; → 42
;; Resume: k re-enters the computation
(reset (+ 1 (shift k (k 10)))) ;; → 11
;; Multiple invocations
(reset (* 2 (shift k (+ (k 1) (k 10))))) ;; → 24")
;; --------------------------------------------------------------------------
;; Guards
;; --------------------------------------------------------------------------
(define-special-form "dynamic-wind"
:syntax (dynamic-wind before-thunk body-thunk after-thunk)
:doc "Entry/exit guards. All three arguments are zero-argument functions
(thunks). before-thunk is called on entry, body-thunk is called
for the result, and after-thunk is always called on exit (even on
error). The wind stack is maintained so that when continuations
jump across dynamic-wind boundaries, the correct before/after
thunks fire."
:tail-position "none (all thunks are eagerly called)"
:example "(dynamic-wind
(fn () (log \"entering\"))
(fn () (do-work))
(fn () (log \"exiting\")))")
;; --------------------------------------------------------------------------
;; Higher-order forms
;;
;; These are syntactic forms (not primitives) because the evaluator
;; handles them directly for performance — avoiding the overhead of
;; constructing argument lists and doing generic dispatch. They could
;; be implemented as primitives but are special-cased in eval-list.
;; --------------------------------------------------------------------------
(define-special-form "map"
:syntax (map fn coll)
:doc "Apply fn to each element of coll, returning a list of results."
:tail-position "none"
:example "(map (fn (x) (* x x)) (list 1 2 3 4)) ;; → (1 4 9 16)")
(define-special-form "map-indexed"
:syntax (map-indexed fn coll)
:doc "Like map, but fn receives two arguments: (index element)."
:tail-position "none"
:example "(map-indexed (fn (i x) (str i \": \" x)) (list \"a\" \"b\" \"c\"))")
(define-special-form "filter"
:syntax (filter fn coll)
:doc "Return elements of coll for which fn returns truthy."
:tail-position "none"
:example "(filter (fn (x) (> x 3)) (list 1 5 2 8 3)) ;; → (5 8)")
(define-special-form "reduce"
:syntax (reduce fn init coll)
:doc "Reduce coll to a single value. fn receives (accumulator element)
and returns the new accumulator. init is the initial value."
:tail-position "none"
:example "(reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3 4)) ;; → 10")
(define-special-form "some"
:syntax (some fn coll)
:doc "Return the first truthy result of applying fn to elements of coll,
or nil if none match. Short-circuits on first truthy result."
:tail-position "none"
:example "(some (fn (x) (> x 3)) (list 1 2 5 3)) ;; → true")
(define-special-form "every?"
:syntax (every? fn coll)
:doc "Return true if fn returns truthy for every element of coll.
Short-circuits on first falsy result."
:tail-position "none"
:example "(every? (fn (x) (> x 0)) (list 1 2 3)) ;; → true")
(define-special-form "for-each"
:syntax (for-each fn coll)
:doc "Apply fn to each element of coll for side effects. Returns nil."
:tail-position "none"
:example "(for-each (fn (x) (log x)) (list 1 2 3))")
;; --------------------------------------------------------------------------
;; Definition forms (domain-specific)
;;
;; These define named entities in the environment. They are special forms
;; because their arguments have domain-specific structure that the
;; evaluator parses directly.
;; --------------------------------------------------------------------------
(define-special-form "defstyle"
:syntax (defstyle name atoms ...)
:doc "Define a named style. Evaluates atoms to a StyleValue and binds
it to name in the environment."
:tail-position "none"
:example "(defstyle card-style :rounded-lg :shadow-md :p-4 :bg-white)")
(define-special-form "defkeyframes"
:syntax (defkeyframes name steps ...)
:doc "Define a CSS @keyframes animation. Steps are (percentage properties ...)
pairs. Produces a StyleValue with the animation name and keyframe rules."
:tail-position "none"
:example "(defkeyframes fade-in
(0 :opacity-0)
(100 :opacity-100))")
(define-special-form "defhandler"
:syntax (defhandler name (&key params ...) body)
:doc "Define an event handler function. Used by the SxEngine for
client-side event handling."
:tail-position "body"
:example "(defhandler toggle-menu (&key target)
(toggle-class target \"hidden\"))")
(define-special-form "defpage"
:syntax (defpage name &key route method content ...)
:doc "Define a page route. Declares the URL pattern, HTTP method, and
content component for server-side page routing."
:tail-position "none"
:example "(defpage dashboard-page
:route \"/dashboard\"
:content (~dashboard-content))")
(define-special-form "defquery"
:syntax (defquery name (&key params ...) body)
:doc "Define a named query for data fetching. Used by the resolver
system to declare data dependencies."
:tail-position "body"
:example "(defquery user-profile (&key user-id)
(fetch-user user-id))")
(define-special-form "defaction"
:syntax (defaction name (&key params ...) body)
:doc "Define a named action for mutations. Like defquery but for
write operations."
:tail-position "body"
:example "(defaction update-profile (&key user-id name email)
(save-user user-id name email))")

1259
shared/sx/ref/sx_ref.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -33,6 +33,7 @@ STYLE_ATOMS: dict[str, str] = {
"flex": "display:flex",
"inline-flex": "display:inline-flex",
"table": "display:table",
"table-row": "display:table-row",
"grid": "display:grid",
"contents": "display:contents",
"hidden": "display:none",
@@ -84,6 +85,7 @@ STYLE_ATOMS: dict[str, str] = {
"flex-row": "flex-direction:row",
"flex-col": "flex-direction:column",
"flex-wrap": "flex-wrap:wrap",
"flex-0": "flex:0",
"flex-1": "flex:1 1 0%",
"flex-shrink-0": "flex-shrink:0",
"shrink-0": "flex-shrink:0",
@@ -149,6 +151,7 @@ STYLE_ATOMS: dict[str, str] = {
"mt-2": "margin-top:.5rem",
"mt-3": "margin-top:.75rem",
"mt-4": "margin-top:1rem",
"mt-5": "margin-top:1.25rem",
"mt-6": "margin-top:1.5rem",
"mt-8": "margin-top:2rem",
"mt-[8px]": "margin-top:8px",
@@ -196,6 +199,7 @@ STYLE_ATOMS: dict[str, str] = {
"pb-8": "padding-bottom:2rem",
"pb-[48px]": "padding-bottom:48px",
"pl-2": "padding-left:.5rem",
"pl-3": "padding-left:.75rem",
"pl-5": "padding-left:1.25rem",
"pl-6": "padding-left:1.5rem",
"pr-1": "padding-right:.25rem",
@@ -216,11 +220,15 @@ STYLE_ATOMS: dict[str, str] = {
"w-10": "width:2.5rem",
"w-11": "width:2.75rem",
"w-12": "width:3rem",
"w-14": "width:3.5rem",
"w-16": "width:4rem",
"w-20": "width:5rem",
"w-24": "width:6rem",
"w-28": "width:7rem",
"w-32": "width:8rem",
"w-40": "width:10rem",
"w-48": "width:12rem",
"w-56": "width:14rem",
"w-1/2": "width:50%",
"w-1/3": "width:33.333333%",
"w-1/4": "width:25%",
@@ -241,6 +249,7 @@ STYLE_ATOMS: dict[str, str] = {
"h-10": "height:2.5rem",
"h-12": "height:3rem",
"h-14": "height:3.5rem",
"h-14": "height:3.5rem",
"h-16": "height:4rem",
"h-24": "height:6rem",
"h-28": "height:7rem",
@@ -268,11 +277,15 @@ STYLE_ATOMS: dict[str, str] = {
"max-w-3xl": "max-width:48rem",
"max-w-4xl": "max-width:56rem",
"max-w-full": "max-width:100%",
"max-w-0": "max-width:0",
"max-w-none": "max-width:none",
"max-w-screen-2xl": "max-width:1536px",
"max-w-[360px]": "max-width:360px",
"max-w-[768px]": "max-width:768px",
"max-w-[640px]": "max-width:640px",
"max-h-32": "max-height:8rem",
"max-h-64": "max-height:16rem",
"max-h-72": "max-height:18rem",
"max-h-96": "max-height:24rem",
"max-h-none": "max-height:none",
"max-h-[448px]": "max-height:448px",
@@ -282,6 +295,7 @@ STYLE_ATOMS: dict[str, str] = {
"text-xs": "font-size:.75rem;line-height:1rem",
"text-sm": "font-size:.875rem;line-height:1.25rem",
"text-base": "font-size:1rem;line-height:1.5rem",
"text-md": "font-size:1rem;line-height:1.5rem", # alias for text-base
"text-lg": "font-size:1.125rem;line-height:1.75rem",
"text-xl": "font-size:1.25rem;line-height:1.75rem",
"text-2xl": "font-size:1.5rem;line-height:2rem",
@@ -345,6 +359,7 @@ STYLE_ATOMS: dict[str, str] = {
"text-rose-500": "color:rgb(244 63 94)",
"text-rose-600": "color:rgb(225 29 72)",
"text-rose-700": "color:rgb(190 18 60)",
"text-rose-800": "color:rgb(159 18 57)",
"text-rose-800/80": "color:rgba(159,18,57,.8)",
"text-rose-900": "color:rgb(136 19 55)",
"text-orange-600": "color:rgb(234 88 12)",
@@ -355,6 +370,10 @@ STYLE_ATOMS: dict[str, str] = {
"text-yellow-700": "color:rgb(161 98 7)",
"text-green-600": "color:rgb(22 163 74)",
"text-green-800": "color:rgb(22 101 52)",
"text-green-900": "color:rgb(20 83 45)",
"text-neutral-400": "color:rgb(163 163 163)",
"text-neutral-500": "color:rgb(115 115 115)",
"text-neutral-600": "color:rgb(82 82 82)",
"text-emerald-500": "color:rgb(16 185 129)",
"text-emerald-600": "color:rgb(5 150 105)",
"text-emerald-700": "color:rgb(4 120 87)",
@@ -371,6 +390,7 @@ STYLE_ATOMS: dict[str, str] = {
"text-violet-600": "color:rgb(124 58 237)",
"text-violet-700": "color:rgb(109 40 217)",
"text-violet-800": "color:rgb(91 33 182)",
"text-violet-900": "color:rgb(76 29 149)",
# ── Background Colors ────────────────────────────────────────────────
"bg-transparent": "background-color:transparent",
@@ -413,6 +433,9 @@ STYLE_ATOMS: dict[str, str] = {
"bg-yellow-300": "background-color:rgb(253 224 71)",
"bg-green-50": "background-color:rgb(240 253 244)",
"bg-green-100": "background-color:rgb(220 252 231)",
"bg-green-200": "background-color:rgb(187 247 208)",
"bg-neutral-50/70": "background-color:rgba(250,250,250,.7)",
"bg-black/70": "background-color:rgba(0,0,0,.7)",
"bg-emerald-50": "background-color:rgb(236 253 245)",
"bg-emerald-50/80": "background-color:rgba(236,253,245,.8)",
"bg-emerald-100": "background-color:rgb(209 250 229)",
@@ -435,6 +458,12 @@ STYLE_ATOMS: dict[str, str] = {
"bg-violet-400": "background-color:rgb(167 139 250)",
"bg-violet-500": "background-color:rgb(139 92 246)",
"bg-violet-600": "background-color:rgb(124 58 237)",
"bg-violet-700": "background-color:rgb(109 40 217)",
"bg-amber-200": "background-color:rgb(253 230 138)",
"bg-blue-700": "background-color:rgb(29 78 216)",
"bg-emerald-700": "background-color:rgb(4 120 87)",
"bg-purple-700": "background-color:rgb(126 34 206)",
"bg-stone-50/60": "background-color:rgba(250,250,249,.6)",
# ── Border ───────────────────────────────────────────────────────────
"border": "border-width:1px",
@@ -445,6 +474,7 @@ STYLE_ATOMS: dict[str, str] = {
"border-b": "border-bottom-width:1px",
"border-b-2": "border-bottom-width:2px",
"border-r": "border-right-width:1px",
"border-l": "border-left-width:1px",
"border-l-4": "border-left-width:4px",
"border-dashed": "border-style:dashed",
"border-none": "border-style:none",
@@ -472,6 +502,9 @@ STYLE_ATOMS: dict[str, str] = {
"border-violet-200": "border-color:rgb(221 214 254)",
"border-violet-300": "border-color:rgb(196 181 253)",
"border-violet-400": "border-color:rgb(167 139 250)",
"border-neutral-200": "border-color:rgb(229 229 229)",
"border-red-400": "border-color:rgb(248 113 113)",
"border-stone-400": "border-color:rgb(168 162 158)",
"border-t-white": "border-top-color:rgb(255 255 255)",
"border-t-stone-600": "border-top-color:rgb(87 83 78)",
"border-l-stone-400": "border-left-color:rgb(168 162 158)",
@@ -499,17 +532,26 @@ STYLE_ATOMS: dict[str, str] = {
"opacity-0": "opacity:0",
"opacity-40": "opacity:.4",
"opacity-50": "opacity:.5",
"opacity-90": "opacity:.9",
"opacity-100": "opacity:1",
# ── Ring / Outline ───────────────────────────────────────────────────
"outline-none": "outline:2px solid transparent;outline-offset:2px",
"ring-2": "box-shadow:0 0 0 2px var(--tw-ring-color,rgb(59 130 246))",
"ring-offset-2": "box-shadow:0 0 0 2px rgb(255 255 255),0 0 0 4px var(--tw-ring-color,rgb(59 130 246))",
"ring-stone-300": "--tw-ring-color:rgb(214 211 209)",
"ring-stone-500": "--tw-ring-color:rgb(120 113 108)",
"ring-violet-500": "--tw-ring-color:rgb(139 92 246)",
"ring-blue-500": "--tw-ring-color:rgb(59 130 246)",
"ring-green-500": "--tw-ring-color:rgb(22 163 74)",
"ring-purple-500": "--tw-ring-color:rgb(147 51 234)",
# ── Overflow ─────────────────────────────────────────────────────────
"overflow-hidden": "overflow:hidden",
"overflow-x-auto": "overflow-x:auto",
"overflow-y-auto": "overflow-y:auto",
"overflow-visible": "overflow:visible",
"overflow-y-visible": "overflow-y:visible",
"overscroll-contain": "overscroll-behavior:contain",
# ── Text Decoration ──────────────────────────────────────────────────
@@ -655,8 +697,13 @@ PSEUDO_VARIANTS: dict[str, str] = {
"placeholder": "::placeholder",
"file": "::file-selector-button",
"aria-selected": "[aria-selected=true]",
"invalid": ":invalid",
"placeholder-shown": ":placeholder-shown",
"group-hover": ":is(.group:hover) &",
"group-open": ":is(.group[open]) &",
"group-open/cat": ":is(.group\\/cat[open]) &",
"group-open/filter": ":is(.group\\/filter[open]) &",
"group-open/root": ":is(.group\\/root[open]) &",
}

View File

@@ -0,0 +1,201 @@
"""Tests for delimited continuations (shift/reset).
Tests run against both the hand-written evaluator and the transpiled
sx_ref evaluator to verify both implementations match.
"""
import pytest
from shared.sx import parse, evaluate, EvalError, NIL
from shared.sx.types import Continuation
from shared.sx.ref import sx_ref
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def ev(text, env=None):
"""Parse and evaluate via hand-written evaluator."""
return evaluate(parse(text), env)
def ev_ref(text, env=None):
"""Parse and evaluate via transpiled sx_ref."""
return sx_ref.evaluate(parse(text), env)
EVALUATORS = [
pytest.param(ev, id="hand-written"),
pytest.param(ev_ref, id="sx_ref"),
]
# ---------------------------------------------------------------------------
# Basic shift/reset
# ---------------------------------------------------------------------------
class TestBasicReset:
"""Reset without shift is a no-op wrapper."""
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_reset_passthrough(self, evaluate):
assert evaluate("(reset 42)") == 42
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_reset_expression(self, evaluate):
assert evaluate("(reset (+ 1 2))") == 3
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_reset_with_let(self, evaluate):
assert evaluate("(reset (let (x 10) (+ x 5)))") == 15
class TestShiftAbort:
"""Shift without invoking k aborts to the reset boundary."""
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_abort_returns_shift_body(self, evaluate):
# (reset (+ 1 (shift k 42))) → shift body 42 is returned, + 1 is abandoned
assert evaluate("(reset (+ 1 (shift k 42)))") == 42
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_abort_string(self, evaluate):
assert evaluate('(reset (+ 1 (shift k "aborted")))') == "aborted"
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_abort_with_computation(self, evaluate):
assert evaluate("(reset (+ 1 (shift k (* 6 7))))") == 42
class TestContinuationInvoke:
"""Invoking the captured continuation re-enters the reset body."""
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_invoke_once(self, evaluate):
# (reset (+ 1 (shift k (k 10)))) → k resumes with 10, so + 1 10 = 11
assert evaluate("(reset (+ 1 (shift k (k 10))))") == 11
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_invoke_with_zero(self, evaluate):
assert evaluate("(reset (+ 1 (shift k (k 0))))") == 1
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_invoke_twice(self, evaluate):
# k invoked twice: (+ (k 1) (k 10)) → (+ 1 1) + ... → (+ 2 11) = 13
# Actually: (k 1) re-evaluates (+ 1 <shift>) where shift returns 1 → 2
# Then (k 10) re-evaluates (+ 1 <shift>) where shift returns 10 → 11
# Then (+ 2 11) = 13
assert evaluate("(reset (+ 1 (shift k (+ (k 1) (k 10)))))") == 13
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_invoke_transforms_value(self, evaluate):
# k wraps: (reset (* 2 (shift k (k (k 3)))))
# k(3) → (* 2 3) = 6, k(6) → (* 2 6) = 12
assert evaluate("(reset (* 2 (shift k (k (k 3)))))") == 12
class TestContinuationPredicate:
"""The continuation? predicate identifies captured continuations."""
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_continuation_is_true(self, evaluate):
result = evaluate("(reset (shift k (continuation? k)))")
assert result is True
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_non_continuation(self, evaluate):
assert evaluate("(continuation? 42)") is False
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_nil_not_continuation(self, evaluate):
assert evaluate("(continuation? nil)") is False
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_lambda_not_continuation(self, evaluate):
assert evaluate("(continuation? (fn (x) x))") is False
class TestStoredContinuation:
"""Continuations can be stored and invoked later."""
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_stored_in_variable(self, evaluate):
code = """
(let (saved nil)
(reset (+ 1 (shift k (do (set! saved k) "captured"))))
)
"""
# The reset returns "captured" (abort path)
assert evaluate(code) == "captured"
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_continuation_type(self, evaluate):
"""Verify that a captured continuation is identified by continuation?."""
code = '(reset (shift k (continuation? k)))'
result = evaluate(code)
assert result is True
class TestNestedReset:
"""Nested reset blocks delimit independently."""
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_inner_reset(self, evaluate):
code = "(reset (+ 1 (reset (+ 2 (shift k (k 10))))))"
# Inner reset: (+ 2 (shift k (k 10))) → k(10) → (+ 2 10) = 12
# Outer reset: (+ 1 12) = 13
assert evaluate(code) == 13
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_inner_abort_outer_continues(self, evaluate):
code = "(reset (+ 1 (reset (shift k 99))))"
# Inner reset aborts with 99
# Outer reset: (+ 1 99) = 100
assert evaluate(code) == 100
class TestPracticalPatterns:
"""Practical uses of delimited continuations."""
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_early_return(self, evaluate):
"""Shift without invoking k acts as early return."""
code = """
(reset
(let (x 5)
(if (> x 3)
(shift k "too big")
(* x x))))
"""
assert evaluate(code) == "too big"
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_normal_path(self, evaluate):
"""When condition doesn't trigger shift, normal result."""
code = """
(reset
(let (x 2)
(if (> x 3)
(shift k "too big")
(* x x))))
"""
assert evaluate(code) == 4
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_continuation_as_function(self, evaluate):
"""Map over a continuation to apply it to multiple values."""
code = """
(reset
(+ 10 (shift k
(map k (list 1 2 3)))))
"""
result = evaluate(code)
assert result == [11, 12, 13]
@pytest.mark.parametrize("evaluate", EVALUATORS)
def test_default_value(self, evaluate):
"""Calling k with no args passes NIL."""
code = '(reset (shift k (nil? (k))))'
# k() passes NIL, reset body re-evals: (shift k ...) returns NIL
# Then the outer shift body checks: (nil? NIL) = true
assert evaluate(code) is True

View File

@@ -0,0 +1,327 @@
"""Tests for Scheme-inspired forms: named let, letrec, dynamic-wind, eq?/eqv?/equal?."""
import pytest
from shared.sx import parse, evaluate, EvalError, Symbol, NIL
from shared.sx.types import Lambda
def ev(text, env=None):
"""Parse and evaluate a single expression."""
return evaluate(parse(text), env)
# ---------------------------------------------------------------------------
# Named let
# ---------------------------------------------------------------------------
class TestNamedLet:
def test_basic_loop(self):
"""Named let as a simple counter loop."""
result = ev("""
(let loop ((i 0) (acc 0))
(if (> i 5)
acc
(loop (+ i 1) (+ acc i))))
""")
assert result == 15 # 0+1+2+3+4+5
def test_factorial(self):
result = ev("""
(let fact ((n 10) (acc 1))
(if (<= n 1)
acc
(fact (- n 1) (* acc n))))
""")
assert result == 3628800
def test_tco_deep_recursion(self):
"""Named let should use TCO — no stack overflow on deep loops."""
result = ev("""
(let loop ((i 0))
(if (>= i 10000)
i
(loop (+ i 1))))
""")
assert result == 10000
def test_clojure_style_bindings(self):
"""Named let with Clojure-style flat bindings."""
result = ev("""
(let loop (i 0 acc (list))
(if (>= i 3)
acc
(loop (+ i 1) (append acc i))))
""")
assert result == [0, 1, 2]
def test_scheme_style_bindings(self):
"""Named let with Scheme-style paired bindings."""
result = ev("""
(let loop ((i 3) (result (list)))
(if (= i 0)
result
(loop (- i 1) (cons i result))))
""")
assert result == [1, 2, 3]
def test_accumulator_pattern(self):
"""Named let accumulating a result — pure functional, no set! needed."""
result = ev("""
(let loop ((i 0) (acc (list)))
(if (>= i 3)
acc
(loop (+ i 1) (append acc (* i i)))))
""")
assert result == [0, 1, 4]
def test_init_evaluated_in_outer_env(self):
"""Initial values are evaluated in the enclosing environment."""
result = ev("""
(let ((x 10))
(let loop ((a x) (b (* x 2)))
(+ a b)))
""")
assert result == 30 # 10 + 20
def test_build_list_with_named_let(self):
"""Idiomatic Scheme pattern: build a list with named let."""
result = ev("""
(let collect ((items (list 1 2 3 4 5)) (acc (list)))
(if (empty? items)
acc
(collect (rest items)
(if (even? (first items))
(append acc (first items))
acc))))
""")
assert result == [2, 4]
def test_fibonacci(self):
"""Fibonacci via named let."""
result = ev("""
(let fib ((n 10) (a 0) (b 1))
(if (= n 0) a
(fib (- n 1) b (+ a b))))
""")
assert result == 55
def test_string_building(self):
"""Named let for building strings."""
result = ev("""
(let build ((items (list "a" "b" "c")) (acc ""))
(if (empty? items)
acc
(build (rest items)
(str acc (if (= acc "") "" ",") (first items)))))
""")
assert result == "a,b,c"
# ---------------------------------------------------------------------------
# letrec
# ---------------------------------------------------------------------------
class TestLetrec:
def test_basic(self):
"""Simple letrec with a self-referencing lambda."""
result = ev("""
(letrec ((double (fn (x) (* x 2))))
(double 21))
""")
assert result == 42
def test_mutual_recursion(self):
"""Classic even?/odd? mutual recursion."""
result = ev("""
(letrec ((my-even? (fn (n)
(if (= n 0) true (my-odd? (- n 1)))))
(my-odd? (fn (n)
(if (= n 0) false (my-even? (- n 1))))))
(list (my-even? 10) (my-odd? 10)
(my-even? 7) (my-odd? 7)))
""")
assert result == [True, False, False, True]
def test_clojure_style(self):
"""letrec with flat bindings."""
result = ev("""
(letrec (f (fn (x) (if (= x 0) 1 (* x (f (- x 1))))))
(f 5))
""")
assert result == 120
def test_closures_see_each_other(self):
"""Lambdas in letrec see each other's final values."""
result = ev("""
(letrec ((a (fn () (b)))
(b (fn () 42)))
(a))
""")
assert result == 42
def test_non_forward_ref(self):
"""letrec with non-lambda values that don't reference each other."""
result = ev("""
(letrec ((x 10) (y 20))
(+ x y))
""")
assert result == 30
def test_three_way_mutual(self):
"""Three mutually recursive functions."""
result = ev("""
(letrec ((f (fn (n) (if (= n 0) 1 (g (- n 1)))))
(g (fn (n) (if (= n 0) 2 (h (- n 1)))))
(h (fn (n) (if (= n 0) 3 (f (- n 1))))))
(list (f 0) (f 1) (f 2) (f 3)
(g 0) (g 1) (g 2)))
""")
# f(0)=1, f(1)=g(0)=2, f(2)=g(1)=h(0)=3, f(3)=g(2)=h(1)=f(0)=1
# g(0)=2, g(1)=h(0)=3, g(2)=h(1)=f(0)=1
assert result == [1, 2, 3, 1, 2, 3, 1]
# ---------------------------------------------------------------------------
# dynamic-wind
# ---------------------------------------------------------------------------
class TestDynamicWind:
def _make_log_env(self):
"""Create env with a log! function that appends to a Python list."""
log = []
env = {"log!": lambda msg: log.append(msg) or NIL}
return env, log
def test_basic_flow(self):
"""Entry and exit thunks called around body."""
env, log = self._make_log_env()
result = ev("""
(dynamic-wind
(fn () (log! "enter"))
(fn () (do (log! "body") 42))
(fn () (log! "exit")))
""", env)
assert result == 42
assert log == ["enter", "body", "exit"]
def test_after_called_on_error(self):
"""Exit thunk is called even when body raises an error."""
env, log = self._make_log_env()
with pytest.raises(Exception):
ev("""
(dynamic-wind
(fn () (log! "enter"))
(fn () (do (log! "body") (error "boom")))
(fn () (log! "exit")))
""", env)
assert log == ["enter", "body", "exit"]
def test_nested(self):
"""Nested dynamic-wind calls entry/exit in correct order."""
env, log = self._make_log_env()
ev("""
(dynamic-wind
(fn () (log! "outer-in"))
(fn ()
(dynamic-wind
(fn () (log! "inner-in"))
(fn () (log! "body"))
(fn () (log! "inner-out"))))
(fn () (log! "outer-out")))
""", env)
assert log == [
"outer-in", "inner-in", "body", "inner-out", "outer-out"
]
def test_return_value(self):
"""Body return value is propagated."""
result = ev("""
(dynamic-wind
(fn () nil)
(fn () (+ 20 22))
(fn () nil))
""")
assert result == 42
def test_before_after_are_thunks(self):
"""Before and after must be zero-arg functions."""
env, log = self._make_log_env()
# Verify it works with native Python callables too
env["enter"] = lambda: log.append("in") or NIL
env["leave"] = lambda: log.append("out") or NIL
result = ev("""
(dynamic-wind enter (fn () 99) leave)
""", env)
assert result == 99
assert log == ["in", "out"]
# ---------------------------------------------------------------------------
# Three-tier equality
# ---------------------------------------------------------------------------
class TestEquality:
def test_eq_identity_same_object(self):
"""eq? is true for the same object."""
env = {}
ev("(define x (list 1 2 3))", env)
assert ev("(eq? x x)", env) is True
def test_eq_identity_different_objects(self):
"""eq? is false for different objects with same value."""
assert ev("(eq? (list 1 2) (list 1 2))") is False
def test_eq_numbers(self):
"""eq? on small ints — Python interns them, so identity holds."""
assert ev("(eq? 42 42)") is True
def test_eqv_numbers(self):
"""eqv? compares numbers by value."""
assert ev("(eqv? 42 42)") is True
assert ev("(eqv? 42 43)") is False
def test_eqv_strings(self):
"""eqv? compares strings by value."""
assert ev('(eqv? "hello" "hello")') is True
assert ev('(eqv? "hello" "world")') is False
def test_eqv_nil(self):
"""eqv? on nil values."""
assert ev("(eqv? nil nil)") is True
def test_eqv_booleans(self):
assert ev("(eqv? true true)") is True
assert ev("(eqv? true false)") is False
def test_eqv_different_lists(self):
"""eqv? is false for different list objects."""
assert ev("(eqv? (list 1 2) (list 1 2))") is False
def test_equal_deep(self):
"""equal? does deep structural comparison."""
assert ev("(equal? (list 1 2 3) (list 1 2 3))") is True
assert ev("(equal? (list 1 2) (list 1 2 3))") is False
def test_equal_nested(self):
"""equal? recursively compares nested structures."""
assert ev("(equal? {:a (list 1 2)} {:a (list 1 2)})") is True
def test_equal_is_same_as_equals(self):
"""equal? and = have the same semantics."""
assert ev("(equal? 42 42)") is True
assert ev("(= 42 42)") is True
assert ev("(equal? (list 1) (list 1))") is True
assert ev("(= (list 1) (list 1))") is True
def test_eq_eqv_equal_hierarchy(self):
"""eq? ⊂ eqv? ⊂ equal? — each is progressively looser."""
env = {}
ev("(define x (list 1 2 3))", env)
# Same object: all three true
assert ev("(eq? x x)", env) is True
assert ev("(eqv? x x)", env) is True
assert ev("(equal? x x)", env) is True
# Different objects, same value: eq? false, eqv? false, equal? true
assert ev("(eq? (list 1 2) (list 1 2))", env) is False
assert ev("(eqv? (list 1 2) (list 1 2))", env) is False
assert ev("(equal? (list 1 2) (list 1 2))", env) is True

View File

@@ -0,0 +1,415 @@
"""Tests for the transpiled sx_ref.py evaluator.
Runs the same test cases as test_evaluator.py and test_html.py but
against the bootstrap-compiled evaluator to verify correctness.
"""
import pytest
from shared.sx.parser import parse
from shared.sx.types import Symbol, Keyword, NIL, Lambda, Component, Macro
from shared.sx.ref import sx_ref
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def ev(text, env=None):
"""Parse and evaluate a single expression via sx_ref."""
return sx_ref.evaluate(parse(text), env)
def render(text, env=None):
"""Parse and render via sx_ref."""
return sx_ref.render(parse(text), env)
# ---------------------------------------------------------------------------
# Literals and lookups
# ---------------------------------------------------------------------------
class TestLiterals:
def test_int(self):
assert ev("42") == 42
def test_string(self):
assert ev('"hello"') == "hello"
def test_true(self):
assert ev("true") is True
def test_nil(self):
assert ev("nil") is NIL
def test_symbol_lookup(self):
assert ev("x", {"x": 10}) == 10
def test_undefined_symbol(self):
with pytest.raises(sx_ref.EvalError, match="Undefined symbol"):
ev("xyz")
def test_keyword_evaluates_to_name(self):
assert ev(":foo") == "foo"
# ---------------------------------------------------------------------------
# Arithmetic
# ---------------------------------------------------------------------------
class TestArithmetic:
def test_add(self):
assert ev("(+ 1 2 3)") == 6
def test_sub(self):
assert ev("(- 10 3)") == 7
def test_mul(self):
assert ev("(* 2 3 4)") == 24
def test_div(self):
assert ev("(/ 10 4)") == 2.5
def test_mod(self):
assert ev("(mod 7 3)") == 1
# ---------------------------------------------------------------------------
# Special forms
# ---------------------------------------------------------------------------
class TestSpecialForms:
def test_if_true(self):
assert ev("(if true 1 2)") == 1
def test_if_false(self):
assert ev("(if false 1 2)") == 2
def test_if_no_else(self):
assert ev("(if false 1)") is NIL
def test_when_true(self):
assert ev("(when true 42)") == 42
def test_when_false(self):
assert ev("(when false 42)") is NIL
def test_and_short_circuit(self):
assert ev("(and true true 3)") == 3
assert ev("(and true false 3)") is False
def test_or_short_circuit(self):
assert ev("(or false false 3)") == 3
assert ev("(or false 2 3)") == 2
def test_let_scheme_style(self):
assert ev("(let ((x 10) (y 20)) (+ x y))") == 30
def test_let_clojure_style(self):
assert ev("(let (x 10 y 20) (+ x y))") == 30
def test_let_sequential(self):
assert ev("(let ((x 1) (y (+ x 1))) y)") == 2
def test_begin(self):
assert ev("(begin 1 2 3)") == 3
def test_quote(self):
result = ev("(quote (a b c))")
assert result == [Symbol("a"), Symbol("b"), Symbol("c")]
def test_cond_clojure(self):
assert ev("(cond false 1 true 2 :else 3)") == 2
def test_cond_else(self):
assert ev("(cond false 1 false 2 :else 99)") == 99
def test_case(self):
assert ev('(case 2 1 "one" 2 "two" :else "other")') == "two"
def test_thread_first(self):
assert ev("(-> 5 (+ 3) (* 2))") == 16
def test_define(self):
env = {}
ev("(define x 42)", env)
assert env["x"] == 42
# ---------------------------------------------------------------------------
# Lambda
# ---------------------------------------------------------------------------
class TestLambda:
def test_create_and_call(self):
assert ev("((fn (x) (* x x)) 5)") == 25
def test_closure(self):
result = ev("(let ((a 10)) ((fn (x) (+ x a)) 5))")
assert result == 15
def test_higher_order(self):
result = ev("(let ((double (fn (x) (* x 2)))) (double 7))")
assert result == 14
# ---------------------------------------------------------------------------
# Collections
# ---------------------------------------------------------------------------
class TestCollections:
def test_list_constructor(self):
assert ev("(list 1 2 3)") == [1, 2, 3]
def test_dict_constructor(self):
assert ev("(dict :a 1 :b 2)") == {"a": 1, "b": 2}
def test_get_dict(self):
assert ev('(get {:a 1 :b 2} "a")') == 1
def test_get_list(self):
assert ev("(get (list 10 20 30) 1)") == 20
def test_first_last_rest(self):
assert ev("(first (list 1 2 3))") == 1
assert ev("(last (list 1 2 3))") == 3
assert ev("(rest (list 1 2 3))") == [2, 3]
def test_len(self):
assert ev("(len (list 1 2 3))") == 3
def test_concat(self):
assert ev("(concat (list 1 2) (list 3 4))") == [1, 2, 3, 4]
def test_cons(self):
assert ev("(cons 0 (list 1 2))") == [0, 1, 2]
def test_merge(self):
assert ev("(merge {:a 1} {:b 2} {:a 3})") == {"a": 3, "b": 2}
def test_empty(self):
assert ev("(empty? (list))") is True
assert ev("(empty? (list 1))") is False
assert ev("(empty? nil)") is True
# ---------------------------------------------------------------------------
# Higher-order forms
# ---------------------------------------------------------------------------
class TestHigherOrder:
def test_map(self):
assert ev("(map (fn (x) (* x x)) (list 1 2 3 4))") == [1, 4, 9, 16]
def test_filter(self):
assert ev("(filter (fn (x) (> x 2)) (list 1 2 3 4))") == [3, 4]
def test_reduce(self):
assert ev("(reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3))") == 6
def test_some(self):
assert ev("(some (fn (x) (> x 3)) (list 1 2 3 4 5))") is True
def test_for_each(self):
result = ev("(for-each (fn (x) x) (list 1 2 3))")
assert result is NIL
# ---------------------------------------------------------------------------
# String ops
# ---------------------------------------------------------------------------
class TestStrings:
def test_str(self):
assert ev('(str "hello" " " "world")') == "hello world"
def test_upper_lower(self):
assert ev('(upper "hello")') == "HELLO"
assert ev('(lower "HELLO")') == "hello"
def test_split_join(self):
assert ev('(split "a,b,c" ",")') == ["a", "b", "c"]
assert ev('(join "-" (list "a" "b"))') == "a-b"
def test_starts_ends(self):
assert ev('(starts-with? "hello" "hel")') is True
assert ev('(ends-with? "hello" "llo")') is True
# ---------------------------------------------------------------------------
# Components
# ---------------------------------------------------------------------------
class TestComponents:
def test_defcomp_and_render(self):
env = {}
ev("(defcomp ~box (&key title) (div :class \"box\" title))", env)
result = render("(~box :title \"hi\")", env)
assert result == '<div class="box">hi</div>'
def test_defcomp_with_children(self):
env = {}
ev("(defcomp ~wrap (&rest children) (div children))", env)
result = render('(~wrap (span "a") (span "b"))', env)
assert result == '<div><span>a</span><span>b</span></div>'
# ---------------------------------------------------------------------------
# HTML rendering
# ---------------------------------------------------------------------------
class TestHTMLRendering:
def test_basic_element(self):
assert render("(div)") == "<div></div>"
def test_text_content(self):
assert render('(p "hello")') == "<p>hello</p>"
def test_attributes(self):
result = render('(a :href "/about" "link")')
assert result == '<a href="/about">link</a>'
def test_void_element(self):
result = render('(br)')
assert result == "<br />"
def test_nested(self):
result = render('(div (p "a") (p "b"))')
assert result == "<div><p>a</p><p>b</p></div>"
def test_fragment(self):
result = render('(<> (span "a") (span "b"))')
assert result == "<span>a</span><span>b</span>"
def test_conditional_rendering(self):
result = render('(if true (span "yes") (span "no"))')
assert result == "<span>yes</span>"
def test_map_rendering(self):
result = render('(map (fn (x) (li x)) (list "a" "b"))')
assert result == "<li>a</li><li>b</li>"
def test_html_escaping(self):
result = render('(span "<b>bold</b>")')
assert result == "<span>&lt;b&gt;bold&lt;/b&gt;</span>"
# ---------------------------------------------------------------------------
# Aser (SX wire format)
# ---------------------------------------------------------------------------
class TestAser:
def test_render_to_sx_basic(self):
expr = parse("(div :class \"foo\" \"hello\")")
result = sx_ref.render_to_sx(expr, {})
assert result == '(div :class "foo" "hello")'
def test_component_not_expanded(self):
expr = parse('(~card :title "hi")')
result = sx_ref.render_to_sx(expr, {})
assert result == '(~card :title "hi")'
def test_fragment(self):
expr = parse('(<> "a" "b")')
result = sx_ref.render_to_sx(expr, {})
assert result == '(<> "a" "b")'
def test_let_evaluates(self):
expr = parse('(let ((x 5)) x)')
result = sx_ref.render_to_sx(expr, {})
assert result == "5"
def test_if_evaluates(self):
expr = parse('(if true "yes" "no")')
result = sx_ref.render_to_sx(expr, {})
assert result == 'yes' # strings pass through unserialized
# ---------------------------------------------------------------------------
# Macros
# ---------------------------------------------------------------------------
class TestDefcomp:
def test_defcomp_basic(self):
"""defcomp should parse &key params without trying to eval &key as a symbol."""
env = {}
ev('(defcomp ~card (&key title) (div title))', env)
assert isinstance(env.get("~card"), Component)
def test_defcomp_render(self):
env = {}
ev('(defcomp ~card (&key title) (div title))', env)
result = render('(~card :title "hello")', env)
assert result == '<div>hello</div>'
def test_defcomp_with_children(self):
env = {}
ev('(defcomp ~wrap (&key title &rest children) (div title children))', env)
comp = env["~wrap"]
assert comp.has_children is True
assert "title" in comp.params
def test_defcomp_multiple_params(self):
env = {}
ev('(defcomp ~box (&key a b c) (div a b c))', env)
result = render('(~box :a "1" :b "2" :c "3")', env)
assert result == '<div>123</div>'
class TestDefhandler:
def test_defhandler_basic(self):
"""defhandler should parse &key params and create a HandlerDef."""
from shared.sx.types import HandlerDef
env = {}
ev('(defhandler link-card (&key slug keys) (div slug))', env)
hdef = env.get("handler:link-card")
assert isinstance(hdef, HandlerDef)
assert hdef.name == "link-card"
assert hdef.params == ["slug", "keys"]
def test_defquery_basic(self):
from shared.sx.types import QueryDef
env = {}
ev('(defquery get-post (&key slug) "Fetch a post" (list slug))', env)
qdef = env.get("query:get-post")
assert isinstance(qdef, QueryDef)
assert qdef.params == ["slug"]
assert qdef.doc == "Fetch a post"
def test_defaction_basic(self):
from shared.sx.types import ActionDef
env = {}
ev('(defaction save-post (&key title body) (list title body))', env)
adef = env.get("action:save-post")
assert isinstance(adef, ActionDef)
assert adef.params == ["title", "body"]
class TestHOWithNativeCallable:
def test_map_with_native_fn(self):
"""map should work with native callables (primitives), not just Lambda."""
result = ev('(map str (list 1 2 3))')
assert result == ["1", "2", "3"]
def test_filter_with_native_fn(self):
result = ev('(filter number? (list 1 "a" 2 "b" 3))')
assert result == [1, 2, 3]
def test_map_with_env_fn(self):
"""map should work with Python functions registered in env."""
env = {"double": lambda x: x * 2}
result = ev('(map double (list 1 2 3))', env)
assert result == [2, 4, 6]
def test_ho_non_callable_fails_fast(self):
"""Passing a non-callable to map should error clearly."""
import pytest
with pytest.raises(sx_ref.EvalError, match="Not callable"):
ev('(map 42 (list 1 2 3))')
class TestMacros:
def test_defmacro_and_expand(self):
env = {}
ev("(defmacro unless (test body) (list (quote if) (list (quote not) test) body))", env)
result = ev('(unless false "ran")', env)
assert result == "ran"

View File

@@ -302,9 +302,45 @@ class StyleValue:
return self.class_name
# ---------------------------------------------------------------------------
# Continuation
# ---------------------------------------------------------------------------
class Continuation:
"""A captured delimited continuation (shift/reset).
Callable with one argument — provides the value that the shift
expression "returns" within the delimited context.
"""
__slots__ = ("fn",)
def __init__(self, fn):
self.fn = fn
def __call__(self, value=NIL):
return self.fn(value)
def __repr__(self):
return "<continuation>"
class _ShiftSignal(BaseException):
"""Raised by shift to unwind to the nearest reset.
Inherits from BaseException (not Exception) to avoid being caught
by generic except clauses in user code.
"""
__slots__ = ("k_name", "body", "env")
def __init__(self, k_name, body, env):
self.k_name = k_name
self.body = body
self.env = env
# ---------------------------------------------------------------------------
# Type alias
# ---------------------------------------------------------------------------
# An s-expression value after evaluation
SExp = int | float | str | bool | Symbol | Keyword | Lambda | Macro | Component | HandlerDef | RelationDef | PageDef | QueryDef | ActionDef | StyleValue | list | dict | _Nil | None
SExp = int | float | str | bool | Symbol | Keyword | Lambda | Macro | Component | Continuation | HandlerDef | RelationDef | PageDef | QueryDef | ActionDef | StyleValue | list | dict | _Nil | None

View File

@@ -238,7 +238,7 @@ def _tokenize_bash(code: str) -> list[tuple[str, str]]:
def highlight(code: str, language: str = "lisp"):
"""Highlight code in the given language. Returns SxExpr for wire format."""
from shared.sx.parser import SxExpr
from shared.sx.parser import SxExpr, serialize
if language in ("lisp", "sx", "sexp"):
return SxExpr(highlight_sx(code))
elif language in ("python", "py"):
@@ -246,5 +246,4 @@ def highlight(code: str, language: str = "lisp"):
elif language in ("bash", "sh", "shell"):
return SxExpr(highlight_bash(code))
# Fallback: no highlighting, just escaped text
escaped = code.replace("\\", "\\\\").replace('"', '\\"')
return SxExpr(f'(span "{escaped}")')
return SxExpr("(span " + serialize(code) + ")")

View File

@@ -22,7 +22,7 @@
(~doc-section :title "What sx is not" :id "not"
(ul :class "space-y-2 text-stone-600"
(li "Not a general-purpose programming language — it's a UI rendering language")
(li "Not a full Lisp — it has macros and TCO, but no continuations or call/cc")
(li "Not a full Lisp — it has macros, TCO, and delimited continuations, but no full call/cc")
(li "Not production-hardened at scale — it runs one website")))))
(defcomp ~docs-getting-started-content ()
@@ -76,6 +76,15 @@
"sx provides ~80 built-in pure functions. They work identically on server (Python) and client (JavaScript).")
(div :class "space-y-6" prims))))
(defcomp ~docs-special-forms-content (&key forms)
(~doc-page :title "Special Forms"
(~doc-section :title "Syntactic constructs" :id "special-forms"
(p :class "text-stone-600"
"Special forms are syntactic constructs whose arguments are NOT evaluated before dispatch. Each form has its own evaluation rules — unlike primitives, which receive pre-evaluated values. Together with primitives, special forms define the complete language surface.")
(p :class "text-stone-600"
"Forms marked with a tail position enable " (a :href "/essays/tco" :class "text-violet-600 hover:underline" "tail-call optimization") " — recursive calls in tail position use constant stack space.")
(div :class "space-y-10" forms))))
(defcomp ~docs-css-content ()
(~doc-page :title "On-Demand CSS"
(~doc-section :title "How it works" :id "how"

View File

@@ -123,3 +123,40 @@
:category cat
:primitives (get primitives cat)))
(keys primitives))))
;; Build all special form category sections from a {category: [form, ...]} dict.
(defcomp ~doc-special-forms-tables (&key forms)
(<> (map (fn (cat)
(~doc-special-forms-category
:category cat
:forms (get forms cat)))
(keys forms))))
(defcomp ~doc-special-forms-category (&key category forms)
(div :class "space-y-4"
(h3 :class "text-xl font-semibold text-stone-800 border-b border-stone-200 pb-2" category)
(div :class "space-y-4"
(map (fn (f)
(~doc-special-form-card
:name (get f "name")
:syntax (get f "syntax")
:doc (get f "doc")
:tail-position (get f "tail-position")
:example (get f "example")))
forms))))
(defcomp ~doc-special-form-card (&key name syntax doc tail-position example)
(div :class "border border-stone-200 rounded-lg p-4 space-y-3"
(div :class "flex items-baseline gap-3"
(code :class "text-lg font-bold text-violet-700" name)
(when (not (= tail-position "none"))
(span :class "text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-700" "TCO")))
(when (not (= syntax ""))
(pre :class "bg-stone-50 rounded px-3 py-2 text-sm font-mono text-stone-700 overflow-x-auto"
syntax))
(p :class "text-stone-600 text-sm whitespace-pre-line" doc)
(when (not (= tail-position ""))
(p :class "text-xs text-stone-500"
(span :class "font-semibold" "Tail position: ") tail-position))
(when (not (= example ""))
(~doc-code :code (highlight example "lisp")))))

File diff suppressed because one or more lines are too long

View File

@@ -8,6 +8,7 @@
(dict :label "Components" :href "/docs/components")
(dict :label "Evaluator" :href "/docs/evaluator")
(dict :label "Primitives" :href "/docs/primitives")
(dict :label "Special Forms" :href "/docs/special-forms")
(dict :label "CSS" :href "/docs/css")
(dict :label "Server Rendering" :href "/docs/server-rendering")))
@@ -75,6 +76,8 @@
:summary "Self-reference, and the tangled hierarchy of a language that defines itself.")
(dict :label "The Reflexive Web" :href "/essays/reflexive-web"
:summary "A web where pages can inspect, modify, and extend their own rendering pipeline.")
(dict :label "Server Architecture" :href "/essays/server-architecture"
:summary "How SX enforces the boundary between host and embedded language, and what it looks like across targets.")
(dict :label "sx sucks" :href "/essays/sx-sucks"
:summary "An honest accounting of everything wrong with SX and why you probably shouldn't use it.")))
@@ -84,6 +87,7 @@
(dict :label "Parser" :href "/specs/parser")
(dict :label "Evaluator" :href "/specs/evaluator")
(dict :label "Primitives" :href "/specs/primitives")
(dict :label "Special Forms" :href "/specs/special-forms")
(dict :label "Renderer" :href "/specs/renderer")
(dict :label "Adapters" :href "/specs/adapters")
(dict :label "DOM Adapter" :href "/specs/adapter-dom")
@@ -93,11 +97,14 @@
(dict :label "SxEngine" :href "/specs/engine")
(dict :label "Orchestration" :href "/specs/orchestration")
(dict :label "Boot" :href "/specs/boot")
(dict :label "CSSX" :href "/specs/cssx")))
(dict :label "CSSX" :href "/specs/cssx")
(dict :label "Continuations" :href "/specs/continuations")
(dict :label "call/cc" :href "/specs/callcc")))
(define bootstrappers-nav-items (list
(dict :label "Overview" :href "/bootstrappers/")
(dict :label "JavaScript" :href "/bootstrappers/javascript")))
(dict :label "JavaScript" :href "/bootstrappers/javascript")
(dict :label "Python" :href "/bootstrappers/python")))
;; Spec file registry — canonical metadata for spec viewer pages.
;; Python only handles file I/O (read-spec-file); all metadata lives here.
@@ -114,6 +121,9 @@
(dict :slug "primitives" :filename "primitives.sx" :title "Primitives"
:desc "All built-in pure functions and their signatures."
:prose "Primitives are the built-in functions available in every SX environment. Each entry declares a name, parameter signature, and semantics. Bootstrap compilers implement these natively per target (JavaScript, Python, etc.). The registry covers arithmetic, comparison, string manipulation, list operations, dict operations, type predicates, and control flow helpers. All primitives are pure — they take values and return values with no side effects. Platform-specific operations (DOM access, HTTP, file I/O) are provided separately via platform bridge functions, not primitives.")
(dict :slug "special-forms" :filename "special-forms.sx" :title "Special Forms"
:desc "All special forms — syntactic constructs with custom evaluation rules."
:prose "Special forms are the syntactic constructs whose arguments are NOT evaluated before dispatch. Each form has its own evaluation rules — unlike primitives, which receive pre-evaluated values. Together with primitives, special forms define the complete language surface. The registry covers control flow (if, when, cond, case, and, or), binding (let, letrec, define, set!), functions (lambda, defcomp, defmacro), sequencing (begin, do, thread-first), quoting (quote, quasiquote), continuations (reset, shift), guards (dynamic-wind), higher-order forms (map, filter, reduce), and domain-specific definitions (defstyle, defhandler, defpage, defquery, defaction).")
(dict :slug "renderer" :filename "render.sx" :title "Renderer"
:desc "Shared rendering registries and utilities used by all adapters."
:prose "The renderer defines what is renderable and how arguments are parsed, but not the output format. It maintains registries of known HTML tags, SVG tags, void elements, and boolean attributes. It specifies how keyword arguments on elements become HTML attributes, how children are collected, and how special attributes (class, style, data-*) are handled. All three adapters (DOM, HTML, SX wire) share these definitions so they agree on what constitutes valid markup. The renderer also defines the StyleValue type used by the CSSX on-demand CSS system.")))
@@ -143,7 +153,15 @@
:desc "On-demand CSS: style dictionary, keyword resolution, rule injection."
:prose "CSSX is the on-demand CSS system. It resolves keyword atoms (:flex, :gap-4, :hover:bg-sky-200) into StyleValue objects with content-addressed class names, injecting CSS rules into the document on first use. The style dictionary is a JSON blob containing: atoms (keyword to CSS declarations), pseudo-variants (hover:, focus:, etc.), responsive breakpoints (md:, lg:, etc.), keyframe animations, arbitrary value patterns, and child selector prefixes (space-x-, space-y-). Classes are only emitted when used, keeping the CSS payload minimal. The dictionary is typically served inline in a <script type=\"text/sx-styles\"> tag.")))
(define all-spec-items (concat core-spec-items (concat adapter-spec-items browser-spec-items)))
(define extension-spec-items (list
(dict :slug "continuations" :filename "continuations.sx" :title "Continuations"
:desc "Delimited continuations — shift/reset for suspendable rendering and cooperative scheduling."
:prose "Delimited continuations capture the rest of a computation up to a delimiter. shift captures the continuation to the nearest reset as a first-class callable value. Unlike full call/cc, delimited continuations are composable — invoking one returns a value. This covers the practical use cases: suspendable server rendering, cooperative scheduling, linear async flows, wizard-style multi-step UIs, and undo. Each bootstrapper target implements the mechanism differently — generators in Python/JS, native shift/reset in Scheme, ContT in Haskell, CPS transform in Rust — but the semantics are identical. Optional extension: code that doesn't use continuations pays zero cost.")
(dict :slug "callcc" :filename "callcc.sx" :title "call/cc"
:desc "Full first-class continuations — call-with-current-continuation."
:prose "Full call/cc captures the entire remaining computation as a first-class function — not just up to a delimiter, but all the way to the top level. Invoking the continuation abandons the current computation entirely and resumes from where it was captured. Strictly more powerful than delimited continuations, but harder to implement in targets that don't support it natively. Recommended for Scheme and Haskell targets where it's natural. Python, JavaScript, and Rust targets should prefer delimited continuations (continuations.sx) unless full escape semantics are genuinely needed. Optional extension: the continuation type is shared with continuations.sx if both are loaded.")))
(define all-spec-items (concat core-spec-items (concat adapter-spec-items (concat browser-spec-items extension-spec-items))))
(define find-spec
(fn (slug)

View File

@@ -47,6 +47,13 @@
:sx-swap "outerHTML" :sx-push-url "true"
"primitives.sx"))
(td :class "px-3 py-2 text-stone-700" "All built-in pure functions and their signatures"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
(a :href "/specs/special-forms" :class "hover:underline"
:sx-get "/specs/special-forms" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
"special-forms.sx"))
(td :class "px-3 py-2 text-stone-700" "All special forms — syntactic constructs with custom evaluation rules"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
(a :href "/specs/renderer" :class "hover:underline"
@@ -157,7 +164,8 @@
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words font-mono text-stone-700"
"parser.sx (standalone — no dependencies)
primitives.sx (standalone — declarative registry)
eval.sx depends on: parser, primitives
special-forms.sx (standalone — declarative registry)
eval.sx depends on: parser, primitives, special-forms
render.sx (standalone — shared registries)
adapter-dom.sx depends on: render, eval
@@ -167,7 +175,39 @@ adapter-sx.sx depends on: render, eval
engine.sx depends on: eval, adapter-dom
orchestration.sx depends on: engine, adapter-dom
cssx.sx depends on: render
boot.sx depends on: cssx, orchestration, adapter-dom, render")))
boot.sx depends on: cssx, orchestration, adapter-dom, render
;; Extensions (optional — loaded only when target requests them)
continuations.sx depends on: eval (optional)
callcc.sx depends on: eval (optional)")))
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Extensions")
(p :class "text-stone-600"
"Optional bolt-on specifications that extend the core language. Bootstrappers include them only when the target requests them. Code that doesn't use extensions pays zero cost.")
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" "File")
(th :class "px-3 py-2 font-medium text-stone-600" "Role")
(th :class "px-3 py-2 font-medium text-stone-600" "Recommended targets")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
(a :href "/specs/continuations" :class "hover:underline"
:sx-get "/specs/continuations" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
"continuations.sx"))
(td :class "px-3 py-2 text-stone-700" "Delimited continuations — shift/reset")
(td :class "px-3 py-2 text-stone-500" "All targets"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
(a :href "/specs/callcc" :class "hover:underline"
:sx-get "/specs/callcc" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
"callcc.sx"))
(td :class "px-3 py-2 text-stone-700" "Full first-class continuations — call/cc")
(td :class "px-3 py-2 text-stone-500" "Scheme, Haskell"))))))
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Self-hosting")
@@ -268,9 +308,11 @@ boot.sx depends on: cssx, orchestration, adapter-dom, render")))
(td :class "px-3 py-2 text-green-600" "Live"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Python")
(td :class "px-3 py-2 font-mono text-sm text-stone-400" "bootstrap_py.py")
(td :class "px-3 py-2 font-mono text-sm text-stone-400" "shared/sx/")
(td :class "px-3 py-2 text-stone-400" "Planned"))
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
(a :href "/bootstrappers/python" :class "hover:underline"
"bootstrap_py.py"))
(td :class "px-3 py-2 font-mono text-sm text-stone-500" "sx_ref.py")
(td :class "px-3 py-2 text-green-600" "Live"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Rust")
(td :class "px-3 py-2 font-mono text-sm text-stone-400" "bootstrap_rs.py")
@@ -320,6 +362,47 @@ boot.sx depends on: cssx, orchestration, adapter-dom, render")))
(pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words"
(code (highlight bootstrapped-output "javascript"))))))))
;; ---------------------------------------------------------------------------
;; Python bootstrapper detail
;; ---------------------------------------------------------------------------
(defcomp ~bootstrapper-py-content (&key bootstrapper-source bootstrapped-output)
(~doc-page :title "Python Bootstrapper"
(div :class "space-y-8"
(div :class "space-y-3"
(p :class "text-stone-600"
"This page reads the canonical " (code :class "text-violet-700 text-sm" ".sx")
" spec files, runs the Python bootstrapper, and displays both the compiler source and its generated Python output. "
"The generated code below is live — it was produced by the bootstrapper at page load time.")
(p :class "text-xs text-stone-400 italic"
"With SX_USE_REF=1, the server-side SX evaluator running this page IS the bootstrapped output. "
"This page re-runs the bootstrapper to display the source and result."))
(div :class "space-y-3"
(div :class "flex items-baseline gap-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Bootstrapper")
(span :class "text-sm text-stone-400 font-mono" "bootstrap_py.py"))
(p :class "text-sm text-stone-500"
"The compiler reads " (code :class "text-violet-700 text-sm" ".sx")
" spec files (eval, primitives, render, adapter-html) "
"and emits a standalone Python module. Platform bridge functions (type constructors, environment ops) "
"are emitted as native Python implementations.")
(div :class "bg-stone-100 rounded-lg p-5 max-h-96 overflow-y-auto border border-stone-200"
(pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words"
(code (highlight bootstrapper-source "python")))))
(div :class "space-y-3"
(div :class "flex items-baseline gap-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Generated Output")
(span :class "text-sm text-stone-400 font-mono" "sx_ref.py"))
(p :class "text-sm text-stone-500"
"The Python below was generated by running the bootstrapper against the current spec files. "
"It is a complete server-side SX evaluator — eval, primitives, and HTML renderer.")
(div :class "bg-stone-100 rounded-lg p-5 max-h-96 overflow-y-auto border border-violet-300"
(pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words"
(code (highlight bootstrapped-output "python"))))))))
;; ---------------------------------------------------------------------------
;; Not found
;; ---------------------------------------------------------------------------

View File

@@ -44,6 +44,8 @@
"evaluator" (~docs-evaluator-content)
"primitives" (~docs-primitives-content
:prims (~doc-primitives-tables :primitives (primitives-data)))
"special-forms" (~docs-special-forms-content
:forms (~doc-special-forms-tables :forms (special-forms-data)))
"css" (~docs-css-content)
"server-rendering" (~docs-server-rendering-content)
:else (~docs-introduction-content)))
@@ -278,6 +280,7 @@
"continuations" (~essay-continuations)
"godel-escher-bach" (~essay-godel-escher-bach)
"reflexive-web" (~essay-reflexive-web)
"server-architecture" (~essay-server-architecture)
:else (~essays-index-content)))
;; ---------------------------------------------------------------------------
@@ -330,6 +333,14 @@
:filename (get item "filename") :href (str "/specs/" (get item "slug"))
:source (read-spec-file (get item "filename"))))
browser-spec-items))
"extensions" (~spec-overview-content
:spec-title "Extensions"
:spec-files (map (fn (item)
(dict :title (get item "title") :desc (get item "desc")
:prose (get item "prose")
:filename (get item "filename") :href (str "/specs/" (get item "slug"))
:source (read-spec-file (get item "filename"))))
extension-spec-items))
:else (let ((spec (find-spec slug)))
(if spec
(~spec-detail-content
@@ -368,6 +379,10 @@
:data (bootstrapper-data slug)
:content (if bootstrapper-not-found
(~spec-not-found :slug slug)
(~bootstrapper-js-content
:bootstrapper-source bootstrapper-source
:bootstrapped-output bootstrapped-output)))
(if (= slug "python")
(~bootstrapper-py-content
:bootstrapper-source bootstrapper-source
:bootstrapped-output bootstrapped-output)
(~bootstrapper-js-content
:bootstrapper-source bootstrapper-source
:bootstrapped-output bootstrapped-output))))

View File

@@ -14,6 +14,7 @@ def _register_sx_helpers() -> None:
register_page_helpers("sx", {
"highlight": _highlight,
"primitives-data": _primitives_data,
"special-forms-data": _special_forms_data,
"reference-data": _reference_data,
"attr-detail-data": _attr_detail_data,
"header-detail-data": _header_detail_data,
@@ -29,6 +30,89 @@ def _primitives_data() -> dict:
return PRIMITIVES
def _special_forms_data() -> dict:
"""Parse special-forms.sx and return categorized form data.
Returns a dict of category → list of form dicts, each with:
name, syntax, doc, tail_position, example
"""
import os
from shared.sx.parser import parse_all, serialize
from shared.sx.types import Symbol, Keyword
spec_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"..", "..", "..", "shared", "sx", "ref", "special-forms.sx",
)
with open(spec_path) as f:
exprs = parse_all(f.read())
# Categories inferred from comment sections in the file.
# We assign forms to categories based on their order in the spec.
categories: dict[str, list[dict]] = {}
current_category = "Other"
# Map form names to categories
category_map = {
"if": "Control Flow", "when": "Control Flow", "cond": "Control Flow",
"case": "Control Flow", "and": "Control Flow", "or": "Control Flow",
"let": "Binding", "let*": "Binding", "letrec": "Binding",
"define": "Binding", "set!": "Binding",
"lambda": "Functions & Components", "fn": "Functions & Components",
"defcomp": "Functions & Components", "defmacro": "Functions & Components",
"begin": "Sequencing & Threading", "do": "Sequencing & Threading",
"->": "Sequencing & Threading",
"quote": "Quoting", "quasiquote": "Quoting",
"reset": "Continuations", "shift": "Continuations",
"dynamic-wind": "Guards",
"map": "Higher-Order Forms", "map-indexed": "Higher-Order Forms",
"filter": "Higher-Order Forms", "reduce": "Higher-Order Forms",
"some": "Higher-Order Forms", "every?": "Higher-Order Forms",
"for-each": "Higher-Order Forms",
"defstyle": "Domain Definitions", "defkeyframes": "Domain Definitions",
"defhandler": "Domain Definitions", "defpage": "Domain Definitions",
"defquery": "Domain Definitions", "defaction": "Domain Definitions",
}
for expr in exprs:
if not isinstance(expr, list) or len(expr) < 2:
continue
head = expr[0]
if not isinstance(head, Symbol) or head.name != "define-special-form":
continue
name = expr[1]
# Extract keyword args
kwargs: dict[str, str] = {}
i = 2
while i < len(expr) - 1:
if isinstance(expr[i], Keyword):
key = expr[i].name
val = expr[i + 1]
if isinstance(val, list):
# For :syntax, avoid quote sugar (quasiquote → `x)
items = [serialize(item) for item in val]
kwargs[key] = "(" + " ".join(items) + ")"
else:
kwargs[key] = str(val)
i += 2
else:
i += 1
category = category_map.get(name, "Other")
if category not in categories:
categories[category] = []
categories[category].append({
"name": name,
"syntax": kwargs.get("syntax", ""),
"doc": kwargs.get("doc", ""),
"tail-position": kwargs.get("tail-position", ""),
"example": kwargs.get("example", ""),
})
return categories
def _reference_data(slug: str) -> dict:
"""Return reference table data for a given slug.
@@ -135,29 +219,44 @@ def _bootstrapper_data(target: str) -> dict:
"""
import os
if target != "javascript":
if target not in ("javascript", "python"):
return {"bootstrapper-not-found": True}
ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref")
if not os.path.isdir(ref_dir):
ref_dir = "/app/shared/sx/ref"
# Read bootstrapper source
bs_path = os.path.join(ref_dir, "bootstrap_js.py")
try:
with open(bs_path, encoding="utf-8") as f:
bootstrapper_source = f.read()
except FileNotFoundError:
bootstrapper_source = "# bootstrapper source not found"
if target == "javascript":
# Read bootstrapper source
bs_path = os.path.join(ref_dir, "bootstrap_js.py")
try:
with open(bs_path, encoding="utf-8") as f:
bootstrapper_source = f.read()
except FileNotFoundError:
bootstrapper_source = "# bootstrapper source not found"
# Run the bootstrap to generate JS
from shared.sx.ref.bootstrap_js import compile_ref_to_js
try:
bootstrapped_output = compile_ref_to_js(
adapters=["dom", "engine", "orchestration", "boot", "cssx"]
)
except Exception as e:
bootstrapped_output = f"// bootstrap error: {e}"
# Run the bootstrap to generate JS
from shared.sx.ref.bootstrap_js import compile_ref_to_js
try:
bootstrapped_output = compile_ref_to_js(
adapters=["dom", "engine", "orchestration", "boot", "cssx"]
)
except Exception as e:
bootstrapped_output = f"// bootstrap error: {e}"
elif target == "python":
bs_path = os.path.join(ref_dir, "bootstrap_py.py")
try:
with open(bs_path, encoding="utf-8") as f:
bootstrapper_source = f.read()
except FileNotFoundError:
bootstrapper_source = "# bootstrapper source not found"
from shared.sx.ref.bootstrap_py import compile_ref_to_py
try:
bootstrapped_output = compile_ref_to_py()
except Exception as e:
bootstrapped_output = f"# bootstrap error: {e}"
return {
"bootstrapper-not-found": None,
@@ -176,7 +275,7 @@ def _attr_detail_data(slug: str) -> dict:
- attr-not-found (truthy if not found)
"""
from content.pages import ATTR_DETAILS
from shared.sx.helpers import SxExpr
from shared.sx.helpers import sx_call
detail = ATTR_DETAILS.get(slug)
if not detail:
@@ -193,7 +292,7 @@ def _attr_detail_data(slug: str) -> dict:
"attr-description": detail["description"],
"attr-example": detail["example"],
"attr-handler": detail.get("handler"),
"attr-demo": SxExpr(f"(~{demo_name})") if demo_name else None,
"attr-demo": sx_call(demo_name) if demo_name else None,
"attr-wire-id": wire_id,
}
@@ -201,7 +300,7 @@ def _attr_detail_data(slug: str) -> dict:
def _header_detail_data(slug: str) -> dict:
"""Return header detail data for a specific header slug."""
from content.pages import HEADER_DETAILS
from shared.sx.helpers import SxExpr
from shared.sx.helpers import sx_call
detail = HEADER_DETAILS.get(slug)
if not detail:
@@ -214,14 +313,14 @@ def _header_detail_data(slug: str) -> dict:
"header-direction": detail["direction"],
"header-description": detail["description"],
"header-example": detail.get("example"),
"header-demo": SxExpr(f"(~{demo_name})") if demo_name else None,
"header-demo": sx_call(demo_name) if demo_name else None,
}
def _event_detail_data(slug: str) -> dict:
"""Return event detail data for a specific event slug."""
from content.pages import EVENT_DETAILS
from shared.sx.helpers import SxExpr
from shared.sx.helpers import sx_call
detail = EVENT_DETAILS.get(slug)
if not detail:
@@ -233,5 +332,5 @@ def _event_detail_data(slug: str) -> dict:
"event-title": slug,
"event-description": detail["description"],
"event-example": detail.get("example"),
"event-demo": SxExpr(f"(~{demo_name})") if demo_name else None,
"event-demo": sx_call(demo_name) if demo_name else None,
}