9 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
32 changed files with 6292 additions and 1350 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

@@ -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) ",")))))

File diff suppressed because it is too large Load Diff

View File

@@ -51,6 +51,7 @@ from .primitives import (
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
# ---------------------------------------------------------------------------

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

@@ -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
# ---------------------------------------------------------------------------
@@ -134,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
@@ -187,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:
@@ -265,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?)."""
@@ -432,181 +452,3 @@ def prim_into(target: Any, coll: Any) -> Any:
return result
raise ValueError(f"into: unsupported target type {type(target).__name__}")
# ---------------------------------------------------------------------------
# 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")`` → 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
@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 ""))
# ---------------------------------------------------------------------------
# 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)
# ---------------------------------------------------------------------------
# Sync IO bridge primitives
#
# These are declared in boundary.sx (I/O tier), NOT primitives.sx.
# They bypass @register_primitive validation because they aren't pure.
# But they must be evaluator-visible because they're called inline in .sx
# code (inside let, filter, etc.) where the async IO interceptor can't
# reach them — particularly in async_eval_ref.py which only intercepts
# IO at the top level.
#
# The async evaluators also intercept these via IO_PRIMITIVES, so the
# async path works too. This registration ensures the sync fallback works.
# ---------------------------------------------------------------------------
def _bridge_app_url(service, *path_parts):
from shared.infrastructure.urls import app_url
path = str(path_parts[0]) if path_parts else "/"
return app_url(str(service), path)
def _bridge_asset_url(*path_parts):
from shared.infrastructure.urls import asset_url
path = str(path_parts[0]) if path_parts else ""
return asset_url(path)
def _bridge_config(key):
from shared.config import config
cfg = config()
return cfg.get(str(key))
def _bridge_jinja_global(key, *default):
from quart import current_app
d = default[0] if default else None
return current_app.jinja_env.globals.get(str(key), d)
def _bridge_relations_from(entity_type):
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(str(entity_type))
]
_PRIMITIVES["app-url"] = _bridge_app_url
_PRIMITIVES["asset-url"] = _bridge_asset_url
_PRIMITIVES["config"] = _bridge_config
_PRIMITIVES["jinja-global"] = _bridge_jinja_global
_PRIMITIVES["relations-from"] = _bridge_relations_from

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

@@ -165,6 +165,12 @@ class JSEmitter:
"sf-and": "sfAnd",
"sf-or": "sfOr",
"sf-let": "sfLet",
"sf-named-let": "sfNamedLet",
"sf-letrec": "sfLetrec",
"sf-dynamic-wind": "sfDynamicWind",
"push-wind!": "pushWind",
"pop-wind!": "popWind",
"call-thunk": "callThunk",
"sf-lambda": "sfLambda",
"sf-define": "sfDefine",
"sf-defcomp": "sfDefcomp",
@@ -996,13 +1002,108 @@ ADAPTER_DEPS = {
}
def compile_ref_to_js(adapters: list[str] | None = None) -> str:
EXTENSION_NAMES = {"continuations"}
CONTINUATIONS_JS = '''
// =========================================================================
// Extension: Delimited continuations (shift/reset)
// =========================================================================
function Continuation(fn) { this.fn = fn; }
Continuation.prototype._continuation = true;
Continuation.prototype.call = function(value) { return this.fn(value !== undefined ? value : NIL); };
function ShiftSignal(kName, body, env) {
this.kName = kName;
this.body = body;
this.env = env;
}
PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; };
var _resetResume = [];
function sfReset(args, env) {
var body = args[0];
try {
return trampoline(evalExpr(body, env));
} catch (e) {
if (e instanceof ShiftSignal) {
var sig = e;
var cont = new Continuation(function(value) {
if (value === undefined) value = NIL;
_resetResume.push(value);
try {
return trampoline(evalExpr(body, env));
} finally {
_resetResume.pop();
}
});
var sigEnv = merge(sig.env);
sigEnv[sig.kName] = cont;
return trampoline(evalExpr(sig.body, sigEnv));
}
throw e;
}
}
function sfShift(args, env) {
if (_resetResume.length > 0) {
return _resetResume[_resetResume.length - 1];
}
var kName = symbolName(args[0]);
var body = args[1];
throw new ShiftSignal(kName, body, env);
}
// Wrap evalList to intercept reset/shift
var _baseEvalList = evalList;
evalList = function(expr, env) {
var head = expr[0];
if (isSym(head)) {
var name = head.name;
if (name === "reset") return sfReset(expr.slice(1), env);
if (name === "shift") return sfShift(expr.slice(1), env);
}
return _baseEvalList(expr, env);
};
// Wrap aserSpecial to handle reset/shift in SX wire mode
if (typeof aserSpecial === "function") {
var _baseAserSpecial = aserSpecial;
aserSpecial = function(name, expr, env) {
if (name === "reset") return sfReset(expr.slice(1), env);
if (name === "shift") return sfShift(expr.slice(1), env);
return _baseAserSpecial(name, expr, env);
};
}
// Wrap typeOf to recognize continuations
var _baseTypeOf = typeOf;
typeOf = function(x) {
if (x != null && x._continuation) return "continuation";
return _baseTypeOf(x);
};
'''
def compile_ref_to_js(
adapters: list[str] | None = None,
modules: list[str] | None = None,
extensions: list[str] | None = None,
) -> str:
"""Read reference .sx files and emit JavaScript.
Args:
adapters: List of adapter names to include.
Valid names: html, sx, dom, engine.
None = include all adapters.
modules: List of primitive module names to include.
core.* are always included. stdlib.* are opt-in.
None = include all modules (backward compatible).
extensions: List of optional extensions to include.
Valid names: continuations.
None = no extensions.
"""
ref_dir = os.path.dirname(os.path.abspath(__file__))
emitter = JSEmitter()
@@ -1049,6 +1150,15 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
defines = extract_defines(src)
all_sections.append((label, defines))
# Resolve extensions
ext_set = set()
if extensions:
for e in extensions:
if e not in EXTENSION_NAMES:
raise ValueError(f"Unknown extension: {e!r}. Valid: {', '.join(EXTENSION_NAMES)}")
ext_set.add(e)
has_continuations = "continuations" in ext_set
# Build output
has_html = "html" in adapter_set
has_sx = "sx" in adapter_set
@@ -1060,9 +1170,25 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
has_parser = "parser" in adapter_set
adapter_label = "+".join(sorted(adapter_set)) if adapter_set else "core-only"
# Determine which primitive modules to include
prim_modules = None # None = all
if modules is not None:
prim_modules = [m for m in _ALL_JS_MODULES if m.startswith("core.")]
for m in modules:
if m not in prim_modules:
if m not in PRIMITIVES_JS_MODULES:
raise ValueError(f"Unknown module: {m!r}. Valid: {', '.join(PRIMITIVES_JS_MODULES)}")
prim_modules.append(m)
parts = []
parts.append(PREAMBLE)
parts.append(PLATFORM_JS)
parts.append(PLATFORM_JS_PRE)
parts.append('\n // =========================================================================')
parts.append(' // Primitives')
parts.append(' // =========================================================================\n')
parts.append(' var PRIMITIVES = {};')
parts.append(_assemble_primitives_js(prim_modules))
parts.append(PLATFORM_JS_POST)
# Parser platform must come before compiled parser.sx
if has_parser:
@@ -1083,6 +1209,8 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
parts.append(adapter_platform[name])
parts.append(fixups_js(has_html, has_sx, has_dom))
if has_continuations:
parts.append(CONTINUATIONS_JS)
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label))
parts.append(EPILOGUE)
return "\n".join(parts)
@@ -1181,7 +1309,235 @@ PREAMBLE = '''\
return arguments.length ? arguments[arguments.length - 1] : false;
}'''
PLATFORM_JS = '''
# ---------------------------------------------------------------------------
# Primitive modules — JS implementations keyed by spec module name.
# core.* modules are always included; stdlib.* are opt-in.
# ---------------------------------------------------------------------------
PRIMITIVES_JS_MODULES: dict[str, str] = {
"core.arithmetic": '''
// core.arithmetic
PRIMITIVES["+"] = function() { var s = 0; for (var i = 0; i < arguments.length; i++) s += arguments[i]; return s; };
PRIMITIVES["-"] = function(a, b) { return arguments.length === 1 ? -a : a - b; };
PRIMITIVES["*"] = function() { var s = 1; for (var i = 0; i < arguments.length; i++) s *= arguments[i]; return s; };
PRIMITIVES["/"] = function(a, b) { return a / b; };
PRIMITIVES["mod"] = function(a, b) { return a % b; };
PRIMITIVES["inc"] = function(n) { return n + 1; };
PRIMITIVES["dec"] = function(n) { return n - 1; };
PRIMITIVES["abs"] = Math.abs;
PRIMITIVES["floor"] = Math.floor;
PRIMITIVES["ceil"] = Math.ceil;
PRIMITIVES["round"] = function(x, n) {
if (n === undefined || n === 0) return Math.round(x);
var f = Math.pow(10, n); return Math.round(x * f) / f;
};
PRIMITIVES["min"] = Math.min;
PRIMITIVES["max"] = Math.max;
PRIMITIVES["sqrt"] = Math.sqrt;
PRIMITIVES["pow"] = Math.pow;
PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); };
''',
"core.comparison": '''
// core.comparison
PRIMITIVES["="] = function(a, b) { return a === b; };
PRIMITIVES["!="] = function(a, b) { return a !== b; };
PRIMITIVES["<"] = function(a, b) { return a < b; };
PRIMITIVES[">"] = function(a, b) { return a > b; };
PRIMITIVES["<="] = function(a, b) { return a <= b; };
PRIMITIVES[">="] = function(a, b) { return a >= b; };
''',
"core.logic": '''
// core.logic
PRIMITIVES["not"] = function(x) { return !isSxTruthy(x); };
''',
"core.predicates": '''
// core.predicates
PRIMITIVES["nil?"] = isNil;
PRIMITIVES["number?"] = function(x) { return typeof x === "number"; };
PRIMITIVES["string?"] = function(x) { return typeof x === "string"; };
PRIMITIVES["list?"] = Array.isArray;
PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; };
PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); };
PRIMITIVES["contains?"] = function(c, k) {
if (typeof c === "string") return c.indexOf(String(k)) !== -1;
if (Array.isArray(c)) return c.indexOf(k) !== -1;
return k in c;
};
PRIMITIVES["odd?"] = function(n) { return n % 2 !== 0; };
PRIMITIVES["even?"] = function(n) { return n % 2 === 0; };
PRIMITIVES["zero?"] = function(n) { return n === 0; };
''',
"core.strings": '''
// core.strings
PRIMITIVES["str"] = function() {
var p = [];
for (var i = 0; i < arguments.length; i++) {
var v = arguments[i]; if (isNil(v)) continue; p.push(String(v));
}
return p.join("");
};
PRIMITIVES["upper"] = function(s) { return String(s).toUpperCase(); };
PRIMITIVES["lower"] = function(s) { return String(s).toLowerCase(); };
PRIMITIVES["trim"] = function(s) { return String(s).trim(); };
PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); };
PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); };
PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); };
PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; };
PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; };
PRIMITIVES["slice"] = function(c, a, b) { return b !== undefined ? c.slice(a, b) : c.slice(a); };
PRIMITIVES["concat"] = function() {
var out = [];
for (var i = 0; i < arguments.length; i++) if (!isNil(arguments[i])) out = out.concat(arguments[i]);
return out;
};
''',
"core.collections": '''
// core.collections
PRIMITIVES["list"] = function() { return Array.prototype.slice.call(arguments); };
PRIMITIVES["dict"] = function() {
var d = {};
for (var i = 0; i < arguments.length - 1; i += 2) d[arguments[i]] = arguments[i + 1];
return d;
};
PRIMITIVES["range"] = function(a, b, step) {
var r = []; step = step || 1;
for (var i = a; step > 0 ? i < b : i > b; i += step) r.push(i);
return r;
};
PRIMITIVES["get"] = function(c, k, def) { var v = (c && c[k]); return v !== undefined ? v : (def !== undefined ? def : NIL); };
PRIMITIVES["len"] = function(c) { return Array.isArray(c) ? c.length : typeof c === "string" ? c.length : Object.keys(c).length; };
PRIMITIVES["first"] = function(c) { return c && c.length > 0 ? c[0] : NIL; };
PRIMITIVES["last"] = function(c) { return c && c.length > 0 ? c[c.length - 1] : NIL; };
PRIMITIVES["rest"] = function(c) { return c ? c.slice(1) : []; };
PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; };
PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); };
PRIMITIVES["append"] = function(c, x) { return (c || []).concat([x]); };
PRIMITIVES["chunk-every"] = function(c, n) {
var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r;
};
PRIMITIVES["zip-pairs"] = function(c) {
var r = []; for (var i = 0; i < c.length - 1; i++) r.push([c[i], c[i + 1]]); return r;
};
''',
"core.dict": '''
// core.dict
PRIMITIVES["keys"] = function(d) { return Object.keys(d || {}); };
PRIMITIVES["vals"] = function(d) { var r = []; for (var k in d) r.push(d[k]); return r; };
PRIMITIVES["merge"] = function() {
var out = {};
for (var i = 0; i < arguments.length; i++) { var d = arguments[i]; if (d && !isNil(d)) for (var k in d) out[k] = d[k]; }
return out;
};
PRIMITIVES["assoc"] = function(d) {
var out = {}; if (d && !isNil(d)) for (var k in d) out[k] = d[k];
for (var i = 1; i < arguments.length - 1; i += 2) out[arguments[i]] = arguments[i + 1];
return out;
};
PRIMITIVES["dissoc"] = function(d) {
var out = {}; for (var k in d) out[k] = d[k];
for (var i = 1; i < arguments.length; i++) delete out[arguments[i]];
return out;
};
PRIMITIVES["into"] = function(target, coll) {
if (Array.isArray(target)) return Array.isArray(coll) ? coll.slice() : Object.entries(coll);
var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; }
return r;
};
''',
"stdlib.format": '''
// stdlib.format
PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); };
PRIMITIVES["parse-int"] = function(v, d) { var n = parseInt(v, 10); return isNaN(n) ? (d || 0) : n; };
PRIMITIVES["format-date"] = function(s, fmt) {
if (!s) return "";
try {
var d = new Date(s);
if (isNaN(d.getTime())) return String(s);
var months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
var short_months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
return fmt.replace(/%-d/g, d.getDate()).replace(/%d/g, ("0"+d.getDate()).slice(-2))
.replace(/%B/g, months[d.getMonth()]).replace(/%b/g, short_months[d.getMonth()])
.replace(/%Y/g, d.getFullYear()).replace(/%m/g, ("0"+(d.getMonth()+1)).slice(-2))
.replace(/%H/g, ("0"+d.getHours()).slice(-2)).replace(/%M/g, ("0"+d.getMinutes()).slice(-2));
} catch (e) { return String(s); }
};
PRIMITIVES["parse-datetime"] = function(s) { return s ? String(s) : NIL; };
''',
"stdlib.text": '''
// stdlib.text
PRIMITIVES["pluralize"] = function(n, s, p) {
if (s || (p && p !== "s")) return n == 1 ? (s || "") : (p || "s");
return n == 1 ? "" : "s";
};
PRIMITIVES["escape"] = function(s) {
return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;");
};
PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); };
''',
"stdlib.style": '''
// stdlib.style
PRIMITIVES["css"] = function() {
var atoms = [];
for (var i = 0; i < arguments.length; i++) {
var a = arguments[i];
if (isNil(a) || a === false) continue;
atoms.push(isKw(a) ? a.name : String(a));
}
if (!atoms.length) return NIL;
return new StyleValue("sx-" + atoms.join("-"), atoms.join(";"), [], [], []);
};
PRIMITIVES["merge-styles"] = function() {
var valid = [];
for (var i = 0; i < arguments.length; i++) {
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
}
if (!valid.length) return NIL;
if (valid.length === 1) return valid[0];
var allDecls = valid.map(function(v) { return v.declarations; }).join(";");
return new StyleValue("sx-merged", allDecls, [], [], []);
};
''',
"stdlib.debug": '''
// stdlib.debug
PRIMITIVES["assert"] = function(cond, msg) {
if (!isSxTruthy(cond)) throw new Error("Assertion error: " + (msg || "Assertion failed"));
return true;
};
''',
}
# Modules to include by default (all)
_ALL_JS_MODULES = list(PRIMITIVES_JS_MODULES.keys())
# Selected primitive modules for current compilation (None = all)
def _assemble_primitives_js(modules: list[str] | None = None) -> str:
"""Assemble JS primitive code from selected modules.
If modules is None, all modules are included.
Core modules are always included regardless of the list.
"""
if modules is None:
modules = _ALL_JS_MODULES
parts = []
for mod in modules:
if mod in PRIMITIVES_JS_MODULES:
parts.append(PRIMITIVES_JS_MODULES[mod])
return "\n".join(parts)
PLATFORM_JS_PRE = '''
// =========================================================================
// Platform interface — JS implementation
// =========================================================================
@@ -1305,180 +1661,9 @@ PLATFORM_JS = '''
function error(msg) { throw new Error(msg); }
function inspect(x) { return JSON.stringify(x); }
// =========================================================================
// Primitives
// =========================================================================
var PRIMITIVES = {};
// Arithmetic
PRIMITIVES["+"] = function() { var s = 0; for (var i = 0; i < arguments.length; i++) s += arguments[i]; return s; };
PRIMITIVES["-"] = function(a, b) { return arguments.length === 1 ? -a : a - b; };
PRIMITIVES["*"] = function() { var s = 1; for (var i = 0; i < arguments.length; i++) s *= arguments[i]; return s; };
PRIMITIVES["/"] = function(a, b) { return a / b; };
PRIMITIVES["mod"] = function(a, b) { return a % b; };
PRIMITIVES["inc"] = function(n) { return n + 1; };
PRIMITIVES["dec"] = function(n) { return n - 1; };
PRIMITIVES["abs"] = Math.abs;
PRIMITIVES["floor"] = Math.floor;
PRIMITIVES["ceil"] = Math.ceil;
PRIMITIVES["round"] = Math.round;
PRIMITIVES["min"] = Math.min;
PRIMITIVES["max"] = Math.max;
PRIMITIVES["sqrt"] = Math.sqrt;
PRIMITIVES["pow"] = Math.pow;
PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); };
// Comparison
PRIMITIVES["="] = function(a, b) { return a == b; };
PRIMITIVES["!="] = function(a, b) { return a != b; };
PRIMITIVES["<"] = function(a, b) { return a < b; };
PRIMITIVES[">"] = function(a, b) { return a > b; };
PRIMITIVES["<="] = function(a, b) { return a <= b; };
PRIMITIVES[">="] = function(a, b) { return a >= b; };
// Logic
PRIMITIVES["not"] = function(x) { return !isSxTruthy(x); };
// String
PRIMITIVES["str"] = function() {
var p = [];
for (var i = 0; i < arguments.length; i++) {
var v = arguments[i]; if (isNil(v)) continue; p.push(String(v));
}
return p.join("");
};
PRIMITIVES["upper"] = function(s) { return String(s).toUpperCase(); };
PRIMITIVES["lower"] = function(s) { return String(s).toLowerCase(); };
PRIMITIVES["trim"] = function(s) { return String(s).trim(); };
PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); };
PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); };
PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); };
PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; };
PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; };
PRIMITIVES["slice"] = function(c, a, b) { return b !== undefined ? c.slice(a, b) : c.slice(a); };
PRIMITIVES["concat"] = function() {
var out = [];
for (var i = 0; i < arguments.length; i++) if (arguments[i]) out = out.concat(arguments[i]);
return out;
};
PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); };
// Predicates
PRIMITIVES["nil?"] = isNil;
PRIMITIVES["number?"] = function(x) { return typeof x === "number"; };
PRIMITIVES["string?"] = function(x) { return typeof x === "string"; };
PRIMITIVES["list?"] = Array.isArray;
PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; };
PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); };
PRIMITIVES["contains?"] = function(c, k) {
if (typeof c === "string") return c.indexOf(String(k)) !== -1;
if (Array.isArray(c)) return c.indexOf(k) !== -1;
return k in c;
};
PRIMITIVES["odd?"] = function(n) { return n % 2 !== 0; };
PRIMITIVES["even?"] = function(n) { return n % 2 === 0; };
PRIMITIVES["zero?"] = function(n) { return n === 0; };
// Collections
PRIMITIVES["list"] = function() { return Array.prototype.slice.call(arguments); };
PRIMITIVES["dict"] = function() {
var d = {};
for (var i = 0; i < arguments.length - 1; i += 2) d[arguments[i]] = arguments[i + 1];
return d;
};
PRIMITIVES["range"] = function(a, b, step) {
var r = []; step = step || 1;
for (var i = a; step > 0 ? i < b : i > b; i += step) r.push(i);
return r;
};
PRIMITIVES["get"] = function(c, k, def) { var v = (c && c[k]); return v !== undefined ? v : (def !== undefined ? def : NIL); };
PRIMITIVES["len"] = function(c) { return Array.isArray(c) ? c.length : typeof c === "string" ? c.length : Object.keys(c).length; };
PRIMITIVES["first"] = function(c) { return c && c.length > 0 ? c[0] : NIL; };
PRIMITIVES["last"] = function(c) { return c && c.length > 0 ? c[c.length - 1] : NIL; };
PRIMITIVES["rest"] = function(c) { return c ? c.slice(1) : []; };
PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; };
PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); };
PRIMITIVES["append"] = function(c, x) { return (c || []).concat([x]); };
PRIMITIVES["keys"] = function(d) { return Object.keys(d || {}); };
PRIMITIVES["vals"] = function(d) { var r = []; for (var k in d) r.push(d[k]); return r; };
PRIMITIVES["merge"] = function() {
var out = {};
for (var i = 0; i < arguments.length; i++) { var d = arguments[i]; if (d && !isNil(d)) for (var k in d) out[k] = d[k]; }
return out;
};
PRIMITIVES["assoc"] = function(d) {
var out = {}; if (d && !isNil(d)) for (var k in d) out[k] = d[k];
for (var i = 1; i < arguments.length - 1; i += 2) out[arguments[i]] = arguments[i + 1];
return out;
};
PRIMITIVES["dissoc"] = function(d) {
var out = {}; for (var k in d) out[k] = d[k];
for (var i = 1; i < arguments.length; i++) delete out[arguments[i]];
return out;
};
PRIMITIVES["chunk-every"] = function(c, n) {
var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r;
};
PRIMITIVES["zip-pairs"] = function(c) {
var r = []; for (var i = 0; i < c.length - 1; i++) r.push([c[i], c[i + 1]]); return r;
};
PRIMITIVES["into"] = function(target, coll) {
if (Array.isArray(target)) return Array.isArray(coll) ? coll.slice() : Object.entries(coll);
var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; }
return r;
};
// Format
PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); };
PRIMITIVES["parse-int"] = function(v, d) { var n = parseInt(v, 10); return isNaN(n) ? (d || 0) : n; };
PRIMITIVES["pluralize"] = function(n, s, p) {
if (s || (p && p !== "s")) return n == 1 ? (s || "") : (p || "s");
return n == 1 ? "" : "s";
};
PRIMITIVES["escape"] = function(s) {
return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
};
PRIMITIVES["format-date"] = function(s, fmt) {
if (!s) return "";
try {
var d = new Date(s);
if (isNaN(d.getTime())) return String(s);
var months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
var short_months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
return fmt.replace(/%-d/g, d.getDate()).replace(/%d/g, ("0"+d.getDate()).slice(-2))
.replace(/%B/g, months[d.getMonth()]).replace(/%b/g, short_months[d.getMonth()])
.replace(/%Y/g, d.getFullYear()).replace(/%m/g, ("0"+(d.getMonth()+1)).slice(-2))
.replace(/%H/g, ("0"+d.getHours()).slice(-2)).replace(/%M/g, ("0"+d.getMinutes()).slice(-2));
} catch (e) { return String(s); }
};
PRIMITIVES["parse-datetime"] = function(s) { return s ? String(s) : NIL; };
PRIMITIVES["split-ids"] = function(s) {
if (!s) return [];
return String(s).split(",").map(function(x) { return x.trim(); }).filter(function(x) { return x; });
};
PRIMITIVES["css"] = function() {
// Stub — CSSX requires style dictionary which is browser-only
var atoms = [];
for (var i = 0; i < arguments.length; i++) {
var a = arguments[i];
if (isNil(a) || a === false) continue;
atoms.push(isKw(a) ? a.name : String(a));
}
if (!atoms.length) return NIL;
return new StyleValue("sx-" + atoms.join("-"), atoms.join(";"), [], [], []);
};
PRIMITIVES["merge-styles"] = function() {
var valid = [];
for (var i = 0; i < arguments.length; i++) {
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
}
if (!valid.length) return NIL;
if (valid.length === 1) return valid[0];
var allDecls = valid.map(function(v) { return v.declarations; }).join(";");
return new StyleValue("sx-merged", allDecls, [], [], []);
};
'''
PLATFORM_JS_POST = '''
function isPrimitive(name) { return name in PRIMITIVES; }
function getPrimitive(name) { return PRIMITIVES[name]; }
@@ -2825,18 +3010,26 @@ if __name__ == "__main__":
p = argparse.ArgumentParser(description="Bootstrap-compile SX reference spec to JavaScript")
p.add_argument("--adapters", "-a",
help="Comma-separated adapter list (html,sx,dom,engine). Default: all")
p.add_argument("--modules", "-m",
help="Comma-separated primitive modules (core.* always included). Default: all")
p.add_argument("--extensions",
help="Comma-separated extensions (continuations). Default: none.")
p.add_argument("--output", "-o",
help="Output file (default: stdout)")
args = p.parse_args()
adapters = args.adapters.split(",") if args.adapters else None
js = compile_ref_to_js(adapters)
modules = args.modules.split(",") if args.modules else None
extensions = args.extensions.split(",") if args.extensions else None
js = compile_ref_to_js(adapters, modules, extensions)
if args.output:
with open(args.output, "w") as f:
f.write(js)
included = ", ".join(adapters) if adapters else "all"
print(f"Wrote {args.output} ({len(js)} bytes, adapters: {included})",
mods = ", ".join(modules) if modules else "all"
ext_label = ", ".join(extensions) if extensions else "none"
print(f"Wrote {args.output} ({len(js)} bytes, adapters: {included}, modules: {mods}, extensions: {ext_label})",
file=sys.stderr)
else:
print(js)

View File

@@ -186,6 +186,8 @@ class PyEmitter:
"sf-quasiquote": "sf_quasiquote",
"sf-thread-first": "sf_thread_first",
"sf-set!": "sf_set_bang",
"sf-reset": "sf_reset",
"sf-shift": "sf_shift",
"qq-expand": "qq_expand",
"ho-map": "ho_map",
"ho-map-indexed": "ho_map_indexed",
@@ -801,14 +803,116 @@ ADAPTER_FILES = {
}
def compile_ref_to_py(adapters: list[str] | None = None) -> str:
EXTENSION_NAMES = {"continuations"}
# Extension-provided special forms (not in eval.sx core)
EXTENSION_FORMS = {
"continuations": {"reset", "shift"},
}
def _parse_special_forms_spec(ref_dir: str) -> set[str]:
"""Parse special-forms.sx to extract declared form names."""
filepath = os.path.join(ref_dir, "special-forms.sx")
if not os.path.exists(filepath):
return set()
with open(filepath) as f:
src = f.read()
names = set()
for expr in parse_all(src):
if (isinstance(expr, list) and len(expr) >= 2
and isinstance(expr[0], Symbol)
and expr[0].name == "define-special-form"
and isinstance(expr[1], str)):
names.add(expr[1])
return names
def _extract_eval_dispatch_names(all_sections: list) -> set[str]:
"""Extract special form names dispatched in eval-list from transpiled sections."""
names = set()
for _label, defines in all_sections:
for name, _expr in defines:
if name.startswith("sf-"):
form = name[3:]
if form in ("cond-scheme", "cond-clojure", "case-loop"):
continue
names.add(form)
if name.startswith("ho-"):
form = name[3:]
names.add(form)
return names
def _validate_special_forms(ref_dir: str, all_sections: list,
has_continuations: bool) -> None:
"""Cross-check special-forms.sx against eval.sx dispatch. Warn on mismatches."""
spec_names = _parse_special_forms_spec(ref_dir)
if not spec_names:
return
dispatch_names = _extract_eval_dispatch_names(all_sections)
if has_continuations:
dispatch_names |= EXTENSION_FORMS["continuations"]
name_aliases = {
"thread-first": "->",
"every": "every?",
"set-bang": "set!",
}
normalized_dispatch = set()
for n in dispatch_names:
normalized_dispatch.add(name_aliases.get(n, n))
internal = {"named-let"}
normalized_dispatch -= internal
undispatched = spec_names - normalized_dispatch
ignore = {"fn", "let*", "do", "defrelation"}
undispatched -= ignore
unspecced = normalized_dispatch - spec_names
unspecced -= ignore
if undispatched:
import sys
print(f"# WARNING: special-forms.sx declares forms not in eval.sx: "
f"{', '.join(sorted(undispatched))}", file=sys.stderr)
if unspecced:
import sys
print(f"# WARNING: eval.sx dispatches forms not in special-forms.sx: "
f"{', '.join(sorted(unspecced))}", file=sys.stderr)
def compile_ref_to_py(
adapters: list[str] | None = None,
modules: list[str] | None = None,
extensions: list[str] | None = None,
) -> str:
"""Read reference .sx files and emit Python.
Args:
adapters: List of adapter names to include.
Valid names: html, sx.
None = include all server-side adapters.
modules: List of primitive module names to include.
core.* are always included. stdlib.* are opt-in.
None = include all modules (backward compatible).
extensions: List of optional extensions to include.
Valid names: continuations.
None = no extensions.
"""
# Determine which primitive modules to include
prim_modules = None # None = all
if modules is not None:
prim_modules = [m for m in _ALL_PY_MODULES if m.startswith("core.")]
for m in modules:
if m not in prim_modules:
if m not in PRIMITIVES_PY_MODULES:
raise ValueError(f"Unknown module: {m!r}. Valid: {', '.join(PRIMITIVES_PY_MODULES)}")
prim_modules.append(m)
ref_dir = os.path.dirname(os.path.abspath(__file__))
emitter = PyEmitter()
@@ -842,6 +946,18 @@ def compile_ref_to_py(adapters: list[str] | None = None) -> str:
defines = extract_defines(src)
all_sections.append((label, defines))
# Resolve extensions
ext_set = set()
if extensions:
for e in extensions:
if e not in EXTENSION_NAMES:
raise ValueError(f"Unknown extension: {e!r}. Valid: {', '.join(EXTENSION_NAMES)}")
ext_set.add(e)
has_continuations = "continuations" in ext_set
# Validate special forms
_validate_special_forms(ref_dir, all_sections, has_continuations)
# Build output
has_html = "html" in adapter_set
has_sx = "sx" in adapter_set
@@ -849,7 +965,9 @@ def compile_ref_to_py(adapters: list[str] | None = None) -> str:
parts = []
parts.append(PREAMBLE)
parts.append(PLATFORM_PY)
parts.append(PRIMITIVES_PY)
parts.append(PRIMITIVES_PY_PRE)
parts.append(_assemble_primitives_py(prim_modules))
parts.append(PRIMITIVES_PY_POST)
for label, defines in all_sections:
parts.append(f"\n# === Transpiled from {label} ===\n")
@@ -859,6 +977,8 @@ def compile_ref_to_py(adapters: list[str] | None = None) -> str:
parts.append("")
parts.append(FIXUPS_PY)
if has_continuations:
parts.append(CONTINUATIONS_PY)
parts.append(public_api_py(has_html, has_sx))
return "\n".join(parts)
@@ -887,8 +1007,8 @@ from typing import Any
# =========================================================================
from shared.sx.types import (
NIL, Symbol, Keyword, Lambda, Component, Macro, StyleValue,
HandlerDef, QueryDef, ActionDef, PageDef,
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro, StyleValue,
HandlerDef, QueryDef, ActionDef, PageDef, _ShiftSignal,
)
from shared.sx.parser import SxExpr
'''
@@ -998,6 +1118,8 @@ def type_of(x):
return "raw-html"
if isinstance(x, StyleValue):
return "style-value"
if isinstance(x, Continuation):
return "continuation"
if isinstance(x, list):
return "list"
if isinstance(x, dict):
@@ -1506,29 +1628,14 @@ def aser_special(name, expr, env):
return trampoline(result)
'''
PRIMITIVES_PY = '''
# =========================================================================
# Primitives
# =========================================================================
# ---------------------------------------------------------------------------
# Primitive modules — Python implementations keyed by spec module name.
# core.* modules are always included; stdlib.* are opt-in.
# ---------------------------------------------------------------------------
# Save builtins before shadowing
_b_len = len
_b_map = map
_b_filter = filter
_b_range = range
_b_list = list
_b_dict = dict
_b_max = max
_b_min = min
_b_round = round
_b_abs = abs
_b_sum = sum
_b_zip = zip
_b_int = int
PRIMITIVES = {}
# Arithmetic
PRIMITIVES_PY_MODULES: dict[str, str] = {
"core.arithmetic": '''
# core.arithmetic
PRIMITIVES["+"] = lambda *args: _b_sum(args)
PRIMITIVES["-"] = lambda a, b=None: -a if b is None else a - b
PRIMITIVES["*"] = lambda *args: _sx_mul(*args)
@@ -1551,42 +1658,31 @@ def _sx_mul(*args):
for a in args:
r *= a
return r
''',
# Comparison
"core.comparison": '''
# core.comparison
PRIMITIVES["="] = lambda a, b: a == b
PRIMITIVES["!="] = lambda a, b: a != b
PRIMITIVES["<"] = lambda a, b: a < b
PRIMITIVES[">"] = lambda a, b: a > b
PRIMITIVES["<="] = lambda a, b: a <= b
PRIMITIVES[">="] = lambda a, b: a >= b
''',
# Logic
"core.logic": '''
# core.logic
PRIMITIVES["not"] = lambda x: not sx_truthy(x)
''',
# String
PRIMITIVES["str"] = sx_str
PRIMITIVES["upper"] = lambda s: str(s).upper()
PRIMITIVES["lower"] = lambda s: str(s).lower()
PRIMITIVES["trim"] = lambda s: str(s).strip()
PRIMITIVES["split"] = lambda s, sep=" ": str(s).split(sep)
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new)
PRIMITIVES["starts-with?"] = lambda s, p: str(s).startswith(p)
PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(p)
PRIMITIVES["slice"] = lambda c, a, b=None: c[a:b] if b is not None else c[a:]
PRIMITIVES["concat"] = lambda *args: _b_sum((a for a in args if a), [])
PRIMITIVES["strip-tags"] = lambda s: _strip_tags(str(s))
import re as _re
def _strip_tags(s):
return _re.sub(r"<[^>]+>", "", s)
# Predicates
"core.predicates": '''
# core.predicates
PRIMITIVES["nil?"] = lambda x: x is None or x is NIL
PRIMITIVES["number?"] = lambda x: isinstance(x, (int, float)) and not isinstance(x, bool)
PRIMITIVES["string?"] = lambda x: isinstance(x, str)
PRIMITIVES["list?"] = lambda x: isinstance(x, _b_list)
PRIMITIVES["dict?"] = lambda x: isinstance(x, _b_dict)
PRIMITIVES["continuation?"] = lambda x: isinstance(x, Continuation)
PRIMITIVES["empty?"] = lambda c: (
c is None or c is NIL or
(isinstance(c, (_b_list, str, _b_dict)) and _b_len(c) == 0)
@@ -1598,8 +1694,25 @@ PRIMITIVES["contains?"] = lambda c, k: (
PRIMITIVES["odd?"] = lambda n: n % 2 != 0
PRIMITIVES["even?"] = lambda n: n % 2 == 0
PRIMITIVES["zero?"] = lambda n: n == 0
''',
# Collections
"core.strings": '''
# core.strings
PRIMITIVES["str"] = sx_str
PRIMITIVES["upper"] = lambda s: str(s).upper()
PRIMITIVES["lower"] = lambda s: str(s).lower()
PRIMITIVES["trim"] = lambda s: str(s).strip()
PRIMITIVES["split"] = lambda s, sep=" ": str(s).split(sep)
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new)
PRIMITIVES["starts-with?"] = lambda s, p: str(s).startswith(p)
PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(p)
PRIMITIVES["slice"] = lambda c, a, b=None: c[a:b] if b is not None else c[a:]
PRIMITIVES["concat"] = lambda *args: _b_sum((a for a in args if a), [])
''',
"core.collections": '''
# core.collections
PRIMITIVES["list"] = lambda *args: _b_list(args)
PRIMITIVES["dict"] = lambda *args: {args[i]: args[i+1] for i in _b_range(0, _b_len(args)-1, 2)}
PRIMITIVES["range"] = lambda a, b, step=1: _b_list(_b_range(_b_int(a), _b_int(b), _b_int(step)))
@@ -1611,22 +1724,20 @@ PRIMITIVES["rest"] = lambda c: c[1:] if c else []
PRIMITIVES["nth"] = lambda c, n: c[n] if c and 0 <= n < _b_len(c) else NIL
PRIMITIVES["cons"] = lambda x, c: [x] + (c or [])
PRIMITIVES["append"] = lambda c, x: (c or []) + [x]
PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)]
PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
''',
"core.dict": '''
# core.dict
PRIMITIVES["keys"] = lambda d: _b_list((d or {}).keys())
PRIMITIVES["vals"] = lambda d: _b_list((d or {}).values())
PRIMITIVES["merge"] = lambda *args: _sx_merge_dicts(*args)
PRIMITIVES["assoc"] = lambda d, *kvs: _sx_assoc(d, *kvs)
PRIMITIVES["dissoc"] = lambda d, *ks: {k: v for k, v in d.items() if k not in ks}
PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)]
PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
PRIMITIVES["into"] = lambda target, coll: (_b_list(coll) if isinstance(target, _b_list) else {p[0]: p[1] for p in coll if isinstance(p, _b_list) and _b_len(p) >= 2})
PRIMITIVES["zip"] = lambda *colls: [_b_list(t) for t in _b_zip(*colls)]
# Format
PRIMITIVES["format-decimal"] = lambda v, p=2: f"{float(v):.{p}f}"
PRIMITIVES["parse-int"] = lambda v, d=0: _sx_parse_int(v, d)
PRIMITIVES["pluralize"] = lambda n, s="", p="s": s if n == 1 else p
PRIMITIVES["escape"] = escape_html
def _sx_merge_dicts(*args):
out = {}
for d in args:
@@ -1639,13 +1750,80 @@ def _sx_assoc(d, *kvs):
for i in _b_range(0, _b_len(kvs) - 1, 2):
out[kvs[i]] = kvs[i + 1]
return out
''',
"stdlib.format": '''
# stdlib.format
PRIMITIVES["format-decimal"] = lambda v, p=2: f"{float(v):.{p}f}"
PRIMITIVES["parse-int"] = lambda v, d=0: _sx_parse_int(v, d)
PRIMITIVES["parse-datetime"] = lambda s: str(s) if s else NIL
def _sx_parse_int(v, default=0):
try:
return _b_int(v)
except (ValueError, TypeError):
return default
''',
"stdlib.text": '''
# stdlib.text
PRIMITIVES["pluralize"] = lambda n, s="", p="s": s if n == 1 else p
PRIMITIVES["escape"] = escape_html
PRIMITIVES["strip-tags"] = lambda s: _strip_tags(str(s))
import re as _re
def _strip_tags(s):
return _re.sub(r"<[^>]+>", "", s)
''',
"stdlib.style": '''
# stdlib.style — stubs (CSSX needs full runtime)
''',
"stdlib.debug": '''
# stdlib.debug
PRIMITIVES["assert"] = lambda cond, msg="Assertion failed": (_ for _ in ()).throw(RuntimeError(f"Assertion error: {msg}")) if not sx_truthy(cond) else True
''',
}
_ALL_PY_MODULES = list(PRIMITIVES_PY_MODULES.keys())
def _assemble_primitives_py(modules: list[str] | None = None) -> str:
"""Assemble Python primitive code from selected modules."""
if modules is None:
modules = _ALL_PY_MODULES
parts = []
for mod in modules:
if mod in PRIMITIVES_PY_MODULES:
parts.append(PRIMITIVES_PY_MODULES[mod])
return "\n".join(parts)
PRIMITIVES_PY_PRE = '''
# =========================================================================
# Primitives
# =========================================================================
# Save builtins before shadowing
_b_len = len
_b_map = map
_b_filter = filter
_b_range = range
_b_list = list
_b_dict = dict
_b_max = max
_b_min = min
_b_round = round
_b_abs = abs
_b_sum = sum
_b_zip = zip
_b_int = int
PRIMITIVES = {}
'''
PRIMITIVES_PY_POST = '''
def is_primitive(name):
if name in PRIMITIVES:
return True
@@ -1757,6 +1935,65 @@ def _wrap_aser_outputs():
aser_fragment = _aser_fragment_wrapped
'''
CONTINUATIONS_PY = '''
# =========================================================================
# Extension: delimited continuations (shift/reset)
# =========================================================================
_RESET_RESUME = [] # stack of resume values; empty = not resuming
_SPECIAL_FORM_NAMES = _SPECIAL_FORM_NAMES | frozenset(["reset", "shift"])
def sf_reset(args, env):
"""(reset body) -- establish a continuation delimiter."""
body = first(args)
try:
return trampoline(eval_expr(body, env))
except _ShiftSignal as sig:
def cont_fn(value=NIL):
_RESET_RESUME.append(value)
try:
return trampoline(eval_expr(body, env))
finally:
_RESET_RESUME.pop()
k = Continuation(cont_fn)
sig_env = dict(sig.env)
sig_env[sig.k_name] = k
return trampoline(eval_expr(sig.body, sig_env))
def sf_shift(args, env):
"""(shift k body) -- capture continuation to nearest reset."""
if _RESET_RESUME:
return _RESET_RESUME[-1]
k_name = symbol_name(first(args))
body = nth(args, 1)
raise _ShiftSignal(k_name, body, env)
# Wrap eval_list to inject shift/reset dispatch
_base_eval_list = eval_list
def _eval_list_with_continuations(expr, env):
head = first(expr)
if type_of(head) == "symbol":
name = symbol_name(head)
args = rest(expr)
if name == "reset":
return sf_reset(args, env)
if name == "shift":
return sf_shift(args, env)
return _base_eval_list(expr, env)
eval_list = _eval_list_with_continuations
# Inject into aser_special
_base_aser_special = aser_special
def _aser_special_with_continuations(name, expr, env):
if name == "reset":
return sf_reset(expr[1:], env)
if name == "shift":
return sf_shift(expr[1:], env)
return _base_aser_special(name, expr, env)
aser_special = _aser_special_with_continuations
'''
def public_api_py(has_html: bool, has_sx: bool) -> str:
lines = [
@@ -1811,9 +2048,21 @@ def main():
default=None,
help="Comma-separated adapter names (html,sx). Default: all server-side.",
)
parser.add_argument(
"--modules",
default=None,
help="Comma-separated primitive modules (core.* always included). Default: all.",
)
parser.add_argument(
"--extensions",
default=None,
help="Comma-separated extensions (continuations). Default: none.",
)
args = parser.parse_args()
adapters = args.adapters.split(",") if args.adapters else None
print(compile_ref_to_py(adapters))
modules = args.modules.split(",") if args.modules else None
extensions = args.extensions.split(",") if args.extensions else None
print(compile_ref_to_py(adapters, modules, extensions))
if __name__ == "__main__":

View File

@@ -293,6 +293,11 @@
:returns "dict"
:service "sx")
(define-page-helper "special-forms-data"
:params ()
:returns "dict"
:service "sx")
(define-page-helper "reference-data"
:params (slug)
:returns "dict"

View File

@@ -42,17 +42,44 @@ def _extract_keyword_arg(expr: list, key: str) -> Any:
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)
names: set[str] = set()
modules: dict[str, set[str]] = {}
current_module = "_unscoped"
for expr in exprs:
if (isinstance(expr, list) and len(expr) >= 2
and isinstance(expr[0], Symbol)
and expr[0].name == "define-primitive"):
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):
names.add(name)
return frozenset(names)
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]]]:

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

@@ -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)
@@ -153,6 +154,9 @@
(= 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)
@@ -384,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
@@ -605,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
;; --------------------------------------------------------------------------
@@ -776,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)
;; --------------------------------------------------------------------------

View File

@@ -246,6 +246,14 @@
(and (>= next-ch "0") (<= next-ch "9")))))
(read-number)
;; 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)
(read-symbol)

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,33 +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.")
;; --------------------------------------------------------------------------
;; Assertions
;; --------------------------------------------------------------------------
(define-primitive "assert"
:params (condition &rest message)
:returns "boolean"
:doc "Assert condition is truthy; raise error with message if not.")
;; --------------------------------------------------------------------------
;; CSSX — style system primitives
;; --------------------------------------------------------------------------
(define-module :stdlib.style)
(define-primitive "css"
:params (&rest atoms)
@@ -467,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))")

View File

@@ -17,8 +17,8 @@ from typing import Any
# =========================================================================
from shared.sx.types import (
NIL, Symbol, Keyword, Lambda, Component, Macro, StyleValue,
HandlerDef, QueryDef, ActionDef, PageDef,
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro, StyleValue,
HandlerDef, QueryDef, ActionDef, PageDef, _ShiftSignal,
)
from shared.sx.parser import SxExpr
@@ -127,6 +127,8 @@ def type_of(x):
return "raw-html"
if isinstance(x, StyleValue):
return "style-value"
if isinstance(x, Continuation):
return "continuation"
if isinstance(x, list):
return "list"
if isinstance(x, dict):
@@ -656,7 +658,8 @@ _b_int = int
PRIMITIVES = {}
# Arithmetic
# core.arithmetic
PRIMITIVES["+"] = lambda *args: _b_sum(args)
PRIMITIVES["-"] = lambda a, b=None: -a if b is None else a - b
PRIMITIVES["*"] = lambda *args: _sx_mul(*args)
@@ -680,7 +683,8 @@ def _sx_mul(*args):
r *= a
return r
# Comparison
# core.comparison
PRIMITIVES["="] = lambda a, b: a == b
PRIMITIVES["!="] = lambda a, b: a != b
PRIMITIVES["<"] = lambda a, b: a < b
@@ -688,33 +692,18 @@ PRIMITIVES[">"] = lambda a, b: a > b
PRIMITIVES["<="] = lambda a, b: a <= b
PRIMITIVES[">="] = lambda a, b: a >= b
# Logic
# core.logic
PRIMITIVES["not"] = lambda x: not sx_truthy(x)
# String
PRIMITIVES["str"] = sx_str
PRIMITIVES["upper"] = lambda s: str(s).upper()
PRIMITIVES["lower"] = lambda s: str(s).lower()
PRIMITIVES["trim"] = lambda s: str(s).strip()
PRIMITIVES["split"] = lambda s, sep=" ": str(s).split(sep)
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new)
PRIMITIVES["starts-with?"] = lambda s, p: str(s).startswith(p)
PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(p)
PRIMITIVES["slice"] = lambda c, a, b=None: c[a:b] if b is not None else c[a:]
PRIMITIVES["concat"] = lambda *args: _b_sum((a for a in args if a), [])
PRIMITIVES["strip-tags"] = lambda s: _strip_tags(str(s))
import re as _re
def _strip_tags(s):
return _re.sub(r"<[^>]+>", "", s)
# Predicates
# core.predicates
PRIMITIVES["nil?"] = lambda x: x is None or x is NIL
PRIMITIVES["number?"] = lambda x: isinstance(x, (int, float)) and not isinstance(x, bool)
PRIMITIVES["string?"] = lambda x: isinstance(x, str)
PRIMITIVES["list?"] = lambda x: isinstance(x, _b_list)
PRIMITIVES["dict?"] = lambda x: isinstance(x, _b_dict)
PRIMITIVES["continuation?"] = lambda x: isinstance(x, Continuation)
PRIMITIVES["empty?"] = lambda c: (
c is None or c is NIL or
(isinstance(c, (_b_list, str, _b_dict)) and _b_len(c) == 0)
@@ -727,7 +716,22 @@ PRIMITIVES["odd?"] = lambda n: n % 2 != 0
PRIMITIVES["even?"] = lambda n: n % 2 == 0
PRIMITIVES["zero?"] = lambda n: n == 0
# Collections
# core.strings
PRIMITIVES["str"] = sx_str
PRIMITIVES["upper"] = lambda s: str(s).upper()
PRIMITIVES["lower"] = lambda s: str(s).lower()
PRIMITIVES["trim"] = lambda s: str(s).strip()
PRIMITIVES["split"] = lambda s, sep=" ": str(s).split(sep)
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new)
PRIMITIVES["starts-with?"] = lambda s, p: str(s).startswith(p)
PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(p)
PRIMITIVES["slice"] = lambda c, a, b=None: c[a:b] if b is not None else c[a:]
PRIMITIVES["concat"] = lambda *args: _b_sum((a for a in args if a), [])
# core.collections
PRIMITIVES["list"] = lambda *args: _b_list(args)
PRIMITIVES["dict"] = lambda *args: {args[i]: args[i+1] for i in _b_range(0, _b_len(args)-1, 2)}
PRIMITIVES["range"] = lambda a, b, step=1: _b_list(_b_range(_b_int(a), _b_int(b), _b_int(step)))
@@ -739,22 +743,19 @@ PRIMITIVES["rest"] = lambda c: c[1:] if c else []
PRIMITIVES["nth"] = lambda c, n: c[n] if c and 0 <= n < _b_len(c) else NIL
PRIMITIVES["cons"] = lambda x, c: [x] + (c or [])
PRIMITIVES["append"] = lambda c, x: (c or []) + [x]
PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)]
PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
# core.dict
PRIMITIVES["keys"] = lambda d: _b_list((d or {}).keys())
PRIMITIVES["vals"] = lambda d: _b_list((d or {}).values())
PRIMITIVES["merge"] = lambda *args: _sx_merge_dicts(*args)
PRIMITIVES["assoc"] = lambda d, *kvs: _sx_assoc(d, *kvs)
PRIMITIVES["dissoc"] = lambda d, *ks: {k: v for k, v in d.items() if k not in ks}
PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)]
PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
PRIMITIVES["into"] = lambda target, coll: (_b_list(coll) if isinstance(target, _b_list) else {p[0]: p[1] for p in coll if isinstance(p, _b_list) and _b_len(p) >= 2})
PRIMITIVES["zip"] = lambda *colls: [_b_list(t) for t in _b_zip(*colls)]
# Format
PRIMITIVES["format-decimal"] = lambda v, p=2: f"{float(v):.{p}f}"
PRIMITIVES["parse-int"] = lambda v, d=0: _sx_parse_int(v, d)
PRIMITIVES["pluralize"] = lambda n, s="", p="s": s if n == 1 else p
PRIMITIVES["escape"] = escape_html
def _sx_merge_dicts(*args):
out = {}
for d in args:
@@ -768,12 +769,36 @@ def _sx_assoc(d, *kvs):
out[kvs[i]] = kvs[i + 1]
return out
# stdlib.format
PRIMITIVES["format-decimal"] = lambda v, p=2: f"{float(v):.{p}f}"
PRIMITIVES["parse-int"] = lambda v, d=0: _sx_parse_int(v, d)
PRIMITIVES["parse-datetime"] = lambda s: str(s) if s else NIL
def _sx_parse_int(v, default=0):
try:
return _b_int(v)
except (ValueError, TypeError):
return default
# stdlib.text
PRIMITIVES["pluralize"] = lambda n, s="", p="s": s if n == 1 else p
PRIMITIVES["escape"] = escape_html
PRIMITIVES["strip-tags"] = lambda s: _strip_tags(str(s))
import re as _re
def _strip_tags(s):
return _re.sub(r"<[^>]+>", "", s)
# stdlib.style — stubs (CSSX needs full runtime)
# stdlib.debug
PRIMITIVES["assert"] = lambda cond, msg="Assertion failed": (_ for _ in ()).throw(RuntimeError(f"Assertion error: {msg}")) if not sx_truthy(cond) else True
def is_primitive(name):
if name in PRIMITIVES:
return True
@@ -861,7 +886,7 @@ trampoline = lambda val: (lambda result: (trampoline(eval_expr(thunk_expr(result
eval_expr = lambda expr, env: _sx_case(type_of(expr), [('number', lambda: expr), ('string', lambda: expr), ('boolean', lambda: expr), ('nil', lambda: NIL), ('symbol', lambda: (lambda name: (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))))(symbol_name(expr))), ('keyword', lambda: keyword_name(expr)), ('dict', lambda: map_dict(lambda k, v: trampoline(eval_expr(v, env)), expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else eval_list(expr, env))), (None, lambda: expr)])
# eval-list
eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defkeyframes(args, env) if sx_truthy((name == 'defkeyframes')) else (sf_defhandler(args, env) if sx_truthy((name == 'defhandler')) else (sf_defpage(args, env) if sx_truthy((name == 'defpage')) else (sf_defquery(args, env) if sx_truthy((name == 'defquery')) else (sf_defaction(args, env) if sx_truthy((name == 'defaction')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy(is_render_expr(expr)) else eval_call(head, args, env))))))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr))
eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_letrec(args, env) if sx_truthy((name == 'letrec')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defkeyframes(args, env) if sx_truthy((name == 'defkeyframes')) else (sf_defhandler(args, env) if sx_truthy((name == 'defhandler')) else (sf_defpage(args, env) if sx_truthy((name == 'defpage')) else (sf_defquery(args, env) if sx_truthy((name == 'defquery')) else (sf_defaction(args, env) if sx_truthy((name == 'defaction')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (sf_reset(args, env) if sx_truthy((name == 'reset')) else (sf_shift(args, env) if sx_truthy((name == 'shift')) else (sf_dynamic_wind(args, env) if sx_truthy((name == 'dynamic-wind')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy(is_render_expr(expr)) else eval_call(head, args, env))))))))))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr))
# eval-call
eval_call = lambda head, args, env: (lambda f: (lambda evaluated_args: (apply(f, evaluated_args) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else (not sx_truthy(is_component(f)))))) else (call_lambda(f, evaluated_args, env) if sx_truthy(is_lambda(f)) else (call_component(f, args, env) if sx_truthy(is_component(f)) else error(sx_str('Not callable: ', inspect(f)))))))(map(lambda a: trampoline(eval_expr(a, env)), args)))(trampoline(eval_expr(head, env)))
@@ -903,7 +928,13 @@ sf_and = lambda args, env: (True if sx_truthy(empty_p(args)) else (lambda val: (
sf_or = lambda args, env: (False if sx_truthy(empty_p(args)) else (lambda val: (val if sx_truthy(val) else sf_or(rest(args), env)))(trampoline(eval_expr(first(args), env))))
# sf-let
sf_let = lambda args, env: (lambda bindings: (lambda body: (lambda local: _sx_begin((for_each(lambda binding: (lambda vname: _sx_dict_set(local, vname, trampoline(eval_expr(nth(binding, 1), local))))((symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding))), bindings) if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))) else (lambda i: reduce(lambda acc, pair_idx: (lambda vname: (lambda val_expr: _sx_dict_set(local, vname, trampoline(eval_expr(val_expr, local))))(nth(bindings, ((pair_idx * 2) + 1))))((symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), NIL, range(0, (len(bindings) / 2))))(0)), for_each(lambda e: trampoline(eval_expr(e, local)), slice(body, 0, (len(body) - 1))), make_thunk(last(body), local)))(env_extend(env)))(rest(args)))(first(args))
sf_let = lambda args, env: (sf_named_let(args, env) if sx_truthy((type_of(first(args)) == 'symbol')) else (lambda bindings: (lambda body: (lambda local: _sx_begin((for_each(lambda binding: (lambda vname: _sx_dict_set(local, vname, trampoline(eval_expr(nth(binding, 1), local))))((symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding))), bindings) if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))) else (lambda i: reduce(lambda acc, pair_idx: (lambda vname: (lambda val_expr: _sx_dict_set(local, vname, trampoline(eval_expr(val_expr, local))))(nth(bindings, ((pair_idx * 2) + 1))))((symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), NIL, range(0, (len(bindings) / 2))))(0)), for_each(lambda e: trampoline(eval_expr(e, local)), slice(body, 0, (len(body) - 1))), make_thunk(last(body), local)))(env_extend(env)))(rest(args)))(first(args)))
# sf-named-let
sf_named_let = lambda args, env: (lambda loop_name: (lambda bindings: (lambda body: (lambda params: (lambda inits: _sx_begin((for_each(_sx_fn(lambda binding: (
_sx_append(params, (symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding))),
_sx_append(inits, nth(binding, 1))
)[-1]), bindings) if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))) else reduce(lambda acc, pair_idx: _sx_begin(_sx_append(params, (symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), _sx_append(inits, nth(bindings, ((pair_idx * 2) + 1)))), NIL, range(0, (len(bindings) / 2)))), (lambda loop_body: (lambda loop_fn: _sx_begin(_sx_set_attr(loop_fn, 'name', loop_name), _sx_dict_set(lambda_closure(loop_fn), loop_name, loop_fn), (lambda init_vals: call_lambda(loop_fn, init_vals, env))(map(lambda e: trampoline(eval_expr(e, env)), inits))))(make_lambda(params, loop_body, env)))((first(body) if sx_truthy((len(body) == 1)) else cons(make_symbol('begin'), body)))))([]))([]))(slice(args, 2)))(nth(args, 1)))(symbol_name(first(args)))
# sf-lambda
sf_lambda = lambda args, env: (lambda params_expr: (lambda body: (lambda param_names: make_lambda(param_names, body, env))(map(lambda p: (symbol_name(p) if sx_truthy((type_of(p) == 'symbol')) else p), params_expr)))(nth(args, 1)))(first(args))
@@ -972,6 +1003,12 @@ sf_thread_first = lambda args, env: (lambda val: reduce(lambda result, form: ((l
# sf-set!
sf_set_bang = lambda args, env: (lambda name: (lambda value: _sx_begin(_sx_dict_set(env, name, value), value))(trampoline(eval_expr(nth(args, 1), env))))(symbol_name(first(args)))
# sf-letrec
sf_letrec = lambda args, env: (lambda bindings: (lambda body: (lambda local: (lambda names: (lambda val_exprs: _sx_begin((for_each(lambda binding: (lambda vname: _sx_begin(_sx_append(names, vname), _sx_append(val_exprs, nth(binding, 1)), _sx_dict_set(local, vname, NIL)))((symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding))), bindings) if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))) else reduce(lambda acc, pair_idx: (lambda vname: (lambda val_expr: _sx_begin(_sx_append(names, vname), _sx_append(val_exprs, val_expr), _sx_dict_set(local, vname, NIL)))(nth(bindings, ((pair_idx * 2) + 1))))((symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), NIL, range(0, (len(bindings) / 2)))), (lambda values: _sx_begin(for_each(lambda pair: _sx_dict_set(local, first(pair), nth(pair, 1)), zip(names, values)), for_each(lambda val: (for_each(lambda n: _sx_dict_set(lambda_closure(val), n, env_get(local, n)), names) if sx_truthy(is_lambda(val)) else NIL), values)))(map(lambda e: trampoline(eval_expr(e, local)), val_exprs)), for_each(lambda e: trampoline(eval_expr(e, local)), slice(body, 0, (len(body) - 1))), make_thunk(last(body), local)))([]))([]))(env_extend(env)))(rest(args)))(first(args))
# sf-dynamic-wind
sf_dynamic_wind = lambda args, env: (lambda before: (lambda body: (lambda after: _sx_begin(call_thunk(before, env), push_wind_b(before, after), (lambda result: _sx_begin(pop_wind_b(), call_thunk(after, env), result))(call_thunk(body, env))))(trampoline(eval_expr(nth(args, 2), env))))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env)))
# expand-macro
expand_macro = lambda mac, raw_args, env: (lambda local: _sx_begin(for_each(lambda pair: _sx_dict_set(local, first(pair), (nth(raw_args, nth(pair, 1)) if sx_truthy((nth(pair, 1) < len(raw_args))) else NIL)), map_indexed(lambda i, p: [p, i], macro_params(mac))), (_sx_dict_set(local, macro_rest_param(mac), slice(raw_args, len(macro_params(mac)))) if sx_truthy(macro_rest_param(mac)) else NIL), trampoline(eval_expr(macro_body(mac), local))))(env_merge(macro_closure(mac), env))
@@ -1132,6 +1169,64 @@ def _wrap_aser_outputs():
aser_fragment = _aser_fragment_wrapped
# =========================================================================
# Extension: delimited continuations (shift/reset)
# =========================================================================
_RESET_RESUME = [] # stack of resume values; empty = not resuming
_SPECIAL_FORM_NAMES = _SPECIAL_FORM_NAMES | frozenset(["reset", "shift"])
def sf_reset(args, env):
"""(reset body) -- establish a continuation delimiter."""
body = first(args)
try:
return trampoline(eval_expr(body, env))
except _ShiftSignal as sig:
def cont_fn(value=NIL):
_RESET_RESUME.append(value)
try:
return trampoline(eval_expr(body, env))
finally:
_RESET_RESUME.pop()
k = Continuation(cont_fn)
sig_env = dict(sig.env)
sig_env[sig.k_name] = k
return trampoline(eval_expr(sig.body, sig_env))
def sf_shift(args, env):
"""(shift k body) -- capture continuation to nearest reset."""
if _RESET_RESUME:
return _RESET_RESUME[-1]
k_name = symbol_name(first(args))
body = nth(args, 1)
raise _ShiftSignal(k_name, body, env)
# Wrap eval_list to inject shift/reset dispatch
_base_eval_list = eval_list
def _eval_list_with_continuations(expr, env):
head = first(expr)
if type_of(head) == "symbol":
name = symbol_name(head)
args = rest(expr)
if name == "reset":
return sf_reset(args, env)
if name == "shift":
return sf_shift(args, env)
return _base_eval_list(expr, env)
eval_list = _eval_list_with_continuations
# Inject into aser_special
_base_aser_special = aser_special
def _aser_special_with_continuations(name, expr, env):
if name == "reset":
return sf_reset(expr[1:], env)
if name == "shift":
return sf_shift(expr[1:], env)
return _base_aser_special(name, expr, env)
aser_special = _aser_special_with_continuations
# =========================================================================
# Public API
# =========================================================================

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

@@ -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

@@ -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")))
@@ -86,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")
@@ -95,7 +97,9 @@
(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/")
@@ -117,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.")))
@@ -146,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")

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)))
@@ -331,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

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.