Compare commits
9 Commits
cfde5bc491
...
6aa2f3f6bd
| Author | SHA1 | Date | |
|---|---|---|---|
| 6aa2f3f6bd | |||
| 6c27ebd3b4 | |||
| f77d7350dd | |||
| ca8de3be1a | |||
| 31ace8768e | |||
| f34e55aa9b | |||
| 102a27e845 | |||
| 12fe93bb55 | |||
| 0693586e6f |
@@ -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) ",")))))
|
||||
|
||||
@@ -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
@@ -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__ = [
|
||||
|
||||
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
544
shared/sx/primitives_ctx.py
Normal 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
131
shared/sx/primitives_stdlib.py
Normal file
131
shared/sx/primitives_stdlib.py
Normal 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
|
||||
@@ -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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'");
|
||||
};
|
||||
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
||||
};
|
||||
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)
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
245
shared/sx/ref/callcc.sx
Normal 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).
|
||||
;;
|
||||
;; --------------------------------------------------------------------------
|
||||
248
shared/sx/ref/continuations.sx
Normal file
248
shared/sx/ref/continuations.sx
Normal 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).
|
||||
;;
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -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)
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Collections — dict operations
|
||||
;; Core — Dict 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.")
|
||||
|
||||
412
shared/sx/ref/special-forms.sx
Normal file
412
shared/sx/ref/special-forms.sx
Normal 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))")
|
||||
@@ -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
|
||||
# =========================================================================
|
||||
|
||||
201
shared/sx/tests/test_continuations.py
Normal file
201
shared/sx/tests/test_continuations.py
Normal 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
|
||||
327
shared/sx/tests/test_scheme_forms.py
Normal file
327
shared/sx/tests/test_scheme_forms.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user