Add TCO to evaluator, update SX docs messaging
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m3s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m3s
Evaluator: add _Thunk + _trampoline for tail-call optimization in lambdas, components, if/when/cond/case/let/begin. All callers in html.py, resolver.py, handlers.py, pages.py, jinja_bridge.py, and query_registry.py unwrap thunks at non-tail positions. SX docs: update tagline to "s-expressions for the web", rewrite intro to reflect that SX replaces most JavaScript need, fix "What sx is not" to acknowledge macros and TCO exist. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,22 @@ class EvalError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class _Thunk:
|
||||||
|
"""Deferred evaluation — returned from tail positions for TCO."""
|
||||||
|
__slots__ = ("expr", "env")
|
||||||
|
|
||||||
|
def __init__(self, expr: Any, env: dict[str, Any]):
|
||||||
|
self.expr = expr
|
||||||
|
self.env = env
|
||||||
|
|
||||||
|
|
||||||
|
def _trampoline(val: Any) -> Any:
|
||||||
|
"""Unwrap thunks by re-entering the evaluator until we get an actual value."""
|
||||||
|
while isinstance(val, _Thunk):
|
||||||
|
val = _eval(val.expr, val.env)
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Public API
|
# Public API
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -50,7 +66,10 @@ def evaluate(expr: Any, env: dict[str, Any] | None = None) -> Any:
|
|||||||
"""Evaluate *expr* in *env* and return the result."""
|
"""Evaluate *expr* in *env* and return the result."""
|
||||||
if env is None:
|
if env is None:
|
||||||
env = {}
|
env = {}
|
||||||
return _eval(expr, env)
|
result = _eval(expr, env)
|
||||||
|
while isinstance(result, _Thunk):
|
||||||
|
result = _eval(result.expr, result.env)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def make_env(**kwargs: Any) -> dict[str, Any]:
|
def make_env(**kwargs: Any) -> dict[str, Any]:
|
||||||
@@ -90,7 +109,7 @@ def _eval(expr: Any, env: dict[str, Any]) -> Any:
|
|||||||
|
|
||||||
# --- dict literal -----------------------------------------------------
|
# --- dict literal -----------------------------------------------------
|
||||||
if isinstance(expr, dict):
|
if isinstance(expr, dict):
|
||||||
return {k: _eval(v, env) for k, v in expr.items()}
|
return {k: _trampoline(_eval(v, env)) for k, v in expr.items()}
|
||||||
|
|
||||||
# --- list = call or special form --------------------------------------
|
# --- list = call or special form --------------------------------------
|
||||||
if not isinstance(expr, list):
|
if not isinstance(expr, list):
|
||||||
@@ -103,7 +122,7 @@ def _eval(expr: Any, env: dict[str, Any]) -> Any:
|
|||||||
|
|
||||||
# If head is not a symbol/lambda/list, treat entire list as data
|
# If head is not a symbol/lambda/list, treat entire list as data
|
||||||
if not isinstance(head, (Symbol, Lambda, list)):
|
if not isinstance(head, (Symbol, Lambda, list)):
|
||||||
return [_eval(x, env) for x in expr]
|
return [_trampoline(_eval(x, env)) for x in expr]
|
||||||
|
|
||||||
# --- special forms ----------------------------------------------------
|
# --- special forms ----------------------------------------------------
|
||||||
if isinstance(head, Symbol):
|
if isinstance(head, Symbol):
|
||||||
@@ -122,11 +141,11 @@ def _eval(expr: Any, env: dict[str, Any]) -> Any:
|
|||||||
val = env[name]
|
val = env[name]
|
||||||
if isinstance(val, Macro):
|
if isinstance(val, Macro):
|
||||||
expanded = _expand_macro(val, expr[1:], env)
|
expanded = _expand_macro(val, expr[1:], env)
|
||||||
return _eval(expanded, env)
|
return _Thunk(expanded, env)
|
||||||
|
|
||||||
# --- function / lambda call -------------------------------------------
|
# --- function / lambda call -------------------------------------------
|
||||||
fn = _eval(head, env)
|
fn = _trampoline(_eval(head, env))
|
||||||
args = [_eval(a, env) for a in expr[1:]]
|
args = [_trampoline(_eval(a, env)) for a in expr[1:]]
|
||||||
|
|
||||||
if callable(fn) and not isinstance(fn, (Lambda, Component)):
|
if callable(fn) and not isinstance(fn, (Lambda, Component)):
|
||||||
return fn(*args)
|
return fn(*args)
|
||||||
@@ -151,7 +170,7 @@ def _call_lambda(fn: Lambda, args: list[Any], caller_env: dict[str, Any]) -> Any
|
|||||||
local.update(caller_env)
|
local.update(caller_env)
|
||||||
for p, v in zip(fn.params, args):
|
for p, v in zip(fn.params, args):
|
||||||
local[p] = v
|
local[p] = v
|
||||||
return _eval(fn.body, local)
|
return _Thunk(fn.body, local)
|
||||||
|
|
||||||
|
|
||||||
def _call_component(comp: Component, raw_args: list[Any], env: dict[str, Any]) -> Any:
|
def _call_component(comp: Component, raw_args: list[Any], env: dict[str, Any]) -> Any:
|
||||||
@@ -166,10 +185,10 @@ def _call_component(comp: Component, raw_args: list[Any], env: dict[str, Any]) -
|
|||||||
while i < len(raw_args):
|
while i < len(raw_args):
|
||||||
arg = raw_args[i]
|
arg = raw_args[i]
|
||||||
if isinstance(arg, Keyword) and i + 1 < len(raw_args):
|
if isinstance(arg, Keyword) and i + 1 < len(raw_args):
|
||||||
kwargs[arg.name] = _eval(raw_args[i + 1], env)
|
kwargs[arg.name] = _trampoline(_eval(raw_args[i + 1], env))
|
||||||
i += 2
|
i += 2
|
||||||
else:
|
else:
|
||||||
children.append(_eval(arg, env))
|
children.append(_trampoline(_eval(arg, env)))
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
local = dict(comp.closure)
|
local = dict(comp.closure)
|
||||||
@@ -181,7 +200,7 @@ def _call_component(comp: Component, raw_args: list[Any], env: dict[str, Any]) -
|
|||||||
local[p] = NIL
|
local[p] = NIL
|
||||||
if comp.has_children:
|
if comp.has_children:
|
||||||
local["children"] = children
|
local["children"] = children
|
||||||
return _eval(comp.body, local)
|
return _Thunk(comp.body, local)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -191,23 +210,22 @@ def _call_component(comp: Component, raw_args: list[Any], env: dict[str, Any]) -
|
|||||||
def _sf_if(expr: list, env: dict) -> Any:
|
def _sf_if(expr: list, env: dict) -> Any:
|
||||||
if len(expr) < 3:
|
if len(expr) < 3:
|
||||||
raise EvalError("if requires condition and then-branch")
|
raise EvalError("if requires condition and then-branch")
|
||||||
cond = _eval(expr[1], env)
|
cond = _trampoline(_eval(expr[1], env))
|
||||||
if cond and cond is not NIL:
|
if cond and cond is not NIL:
|
||||||
return _eval(expr[2], env)
|
return _Thunk(expr[2], env)
|
||||||
if len(expr) > 3:
|
if len(expr) > 3:
|
||||||
return _eval(expr[3], env)
|
return _Thunk(expr[3], env)
|
||||||
return NIL
|
return NIL
|
||||||
|
|
||||||
|
|
||||||
def _sf_when(expr: list, env: dict) -> Any:
|
def _sf_when(expr: list, env: dict) -> Any:
|
||||||
if len(expr) < 3:
|
if len(expr) < 3:
|
||||||
raise EvalError("when requires condition and body")
|
raise EvalError("when requires condition and body")
|
||||||
cond = _eval(expr[1], env)
|
cond = _trampoline(_eval(expr[1], env))
|
||||||
if cond and cond is not NIL:
|
if cond and cond is not NIL:
|
||||||
result = NIL
|
for body_expr in expr[2:-1]:
|
||||||
for body_expr in expr[2:]:
|
_trampoline(_eval(body_expr, env))
|
||||||
result = _eval(body_expr, env)
|
return _Thunk(expr[-1], env)
|
||||||
return result
|
|
||||||
return NIL
|
return NIL
|
||||||
|
|
||||||
|
|
||||||
@@ -228,22 +246,22 @@ def _sf_cond(expr: list, env: dict) -> Any:
|
|||||||
raise EvalError("cond clause must be (test result)")
|
raise EvalError("cond clause must be (test result)")
|
||||||
test = clause[0]
|
test = clause[0]
|
||||||
if isinstance(test, Symbol) and test.name in ("else", ":else"):
|
if isinstance(test, Symbol) and test.name in ("else", ":else"):
|
||||||
return _eval(clause[1], env)
|
return _Thunk(clause[1], env)
|
||||||
if isinstance(test, Keyword) and test.name == "else":
|
if isinstance(test, Keyword) and test.name == "else":
|
||||||
return _eval(clause[1], env)
|
return _Thunk(clause[1], env)
|
||||||
if _eval(test, env):
|
if _trampoline(_eval(test, env)):
|
||||||
return _eval(clause[1], env)
|
return _Thunk(clause[1], env)
|
||||||
else:
|
else:
|
||||||
i = 0
|
i = 0
|
||||||
while i < len(clauses) - 1:
|
while i < len(clauses) - 1:
|
||||||
test = clauses[i]
|
test = clauses[i]
|
||||||
result = clauses[i + 1]
|
result = clauses[i + 1]
|
||||||
if isinstance(test, Keyword) and test.name == "else":
|
if isinstance(test, Keyword) and test.name == "else":
|
||||||
return _eval(result, env)
|
return _Thunk(result, env)
|
||||||
if isinstance(test, Symbol) and test.name in (":else", "else"):
|
if isinstance(test, Symbol) and test.name in (":else", "else"):
|
||||||
return _eval(result, env)
|
return _Thunk(result, env)
|
||||||
if _eval(test, env):
|
if _trampoline(_eval(test, env)):
|
||||||
return _eval(result, env)
|
return _Thunk(result, env)
|
||||||
i += 2
|
i += 2
|
||||||
return NIL
|
return NIL
|
||||||
|
|
||||||
@@ -251,18 +269,18 @@ def _sf_cond(expr: list, env: dict) -> Any:
|
|||||||
def _sf_case(expr: list, env: dict) -> Any:
|
def _sf_case(expr: list, env: dict) -> Any:
|
||||||
if len(expr) < 2:
|
if len(expr) < 2:
|
||||||
raise EvalError("case requires expression to match")
|
raise EvalError("case requires expression to match")
|
||||||
match_val = _eval(expr[1], env)
|
match_val = _trampoline(_eval(expr[1], env))
|
||||||
clauses = expr[2:]
|
clauses = expr[2:]
|
||||||
i = 0
|
i = 0
|
||||||
while i < len(clauses) - 1:
|
while i < len(clauses) - 1:
|
||||||
test = clauses[i]
|
test = clauses[i]
|
||||||
result = clauses[i + 1]
|
result = clauses[i + 1]
|
||||||
if isinstance(test, Keyword) and test.name == "else":
|
if isinstance(test, Keyword) and test.name == "else":
|
||||||
return _eval(result, env)
|
return _Thunk(result, env)
|
||||||
if isinstance(test, Symbol) and test.name in (":else", "else"):
|
if isinstance(test, Symbol) and test.name in (":else", "else"):
|
||||||
return _eval(result, env)
|
return _Thunk(result, env)
|
||||||
if match_val == _eval(test, env):
|
if match_val == _trampoline(_eval(test, env)):
|
||||||
return _eval(result, env)
|
return _Thunk(result, env)
|
||||||
i += 2
|
i += 2
|
||||||
return NIL
|
return NIL
|
||||||
|
|
||||||
@@ -270,7 +288,7 @@ def _sf_case(expr: list, env: dict) -> Any:
|
|||||||
def _sf_and(expr: list, env: dict) -> Any:
|
def _sf_and(expr: list, env: dict) -> Any:
|
||||||
result: Any = True
|
result: Any = True
|
||||||
for arg in expr[1:]:
|
for arg in expr[1:]:
|
||||||
result = _eval(arg, env)
|
result = _trampoline(_eval(arg, env))
|
||||||
if not result:
|
if not result:
|
||||||
return result
|
return result
|
||||||
return result
|
return result
|
||||||
@@ -279,7 +297,7 @@ def _sf_and(expr: list, env: dict) -> Any:
|
|||||||
def _sf_or(expr: list, env: dict) -> Any:
|
def _sf_or(expr: list, env: dict) -> Any:
|
||||||
result: Any = False
|
result: Any = False
|
||||||
for arg in expr[1:]:
|
for arg in expr[1:]:
|
||||||
result = _eval(arg, env)
|
result = _trampoline(_eval(arg, env))
|
||||||
if result:
|
if result:
|
||||||
return result
|
return result
|
||||||
return result
|
return result
|
||||||
@@ -299,23 +317,23 @@ def _sf_let(expr: list, env: dict) -> Any:
|
|||||||
raise EvalError("let binding must be (name value)")
|
raise EvalError("let binding must be (name value)")
|
||||||
var = binding[0]
|
var = binding[0]
|
||||||
vname = var.name if isinstance(var, Symbol) else var
|
vname = var.name if isinstance(var, Symbol) else var
|
||||||
local[vname] = _eval(binding[1], local)
|
local[vname] = _trampoline(_eval(binding[1], local))
|
||||||
elif len(bindings) % 2 == 0:
|
elif len(bindings) % 2 == 0:
|
||||||
# Clojure-style: (name val name val ...)
|
# Clojure-style: (name val name val ...)
|
||||||
for i in range(0, len(bindings), 2):
|
for i in range(0, len(bindings), 2):
|
||||||
var = bindings[i]
|
var = bindings[i]
|
||||||
vname = var.name if isinstance(var, Symbol) else var
|
vname = var.name if isinstance(var, Symbol) else var
|
||||||
local[vname] = _eval(bindings[i + 1], local)
|
local[vname] = _trampoline(_eval(bindings[i + 1], local))
|
||||||
else:
|
else:
|
||||||
raise EvalError("let bindings must be (name val ...) pairs")
|
raise EvalError("let bindings must be (name val ...) pairs")
|
||||||
else:
|
else:
|
||||||
raise EvalError("let bindings must be a list")
|
raise EvalError("let bindings must be a list")
|
||||||
|
|
||||||
# Evaluate body expressions, return last
|
# Evaluate body expressions — all but last non-tail, last is tail
|
||||||
result: Any = NIL
|
body = expr[2:]
|
||||||
for body_expr in expr[2:]:
|
for body_expr in body[:-1]:
|
||||||
result = _eval(body_expr, local)
|
_trampoline(_eval(body_expr, local))
|
||||||
return result
|
return _Thunk(body[-1], local)
|
||||||
|
|
||||||
|
|
||||||
def _sf_lambda(expr: list, env: dict) -> Lambda:
|
def _sf_lambda(expr: list, env: dict) -> Lambda:
|
||||||
@@ -341,7 +359,7 @@ def _sf_define(expr: list, env: dict) -> Any:
|
|||||||
name_sym = expr[1]
|
name_sym = expr[1]
|
||||||
if not isinstance(name_sym, Symbol):
|
if not isinstance(name_sym, Symbol):
|
||||||
raise EvalError(f"define name must be symbol, got {type(name_sym).__name__}")
|
raise EvalError(f"define name must be symbol, got {type(name_sym).__name__}")
|
||||||
value = _eval(expr[2], env)
|
value = _trampoline(_eval(expr[2], env))
|
||||||
if isinstance(value, Lambda) and value.name is None:
|
if isinstance(value, Lambda) and value.name is None:
|
||||||
value.name = name_sym.name
|
value.name = name_sym.name
|
||||||
env[name_sym.name] = value
|
env[name_sym.name] = value
|
||||||
@@ -393,10 +411,11 @@ def _sf_defcomp(expr: list, env: dict) -> Component:
|
|||||||
|
|
||||||
|
|
||||||
def _sf_begin(expr: list, env: dict) -> Any:
|
def _sf_begin(expr: list, env: dict) -> Any:
|
||||||
result: Any = NIL
|
if len(expr) < 2:
|
||||||
for sub in expr[1:]:
|
return NIL
|
||||||
result = _eval(sub, env)
|
for sub in expr[1:-1]:
|
||||||
return result
|
_trampoline(_eval(sub, env))
|
||||||
|
return _Thunk(expr[-1], env)
|
||||||
|
|
||||||
|
|
||||||
def _sf_quote(expr: list, _env: dict) -> Any:
|
def _sf_quote(expr: list, _env: dict) -> Any:
|
||||||
@@ -407,18 +426,18 @@ def _sf_thread_first(expr: list, env: dict) -> Any:
|
|||||||
"""``(-> val (f a) (g b))`` → ``(g (f val a) b)``"""
|
"""``(-> val (f a) (g b))`` → ``(g (f val a) b)``"""
|
||||||
if len(expr) < 2:
|
if len(expr) < 2:
|
||||||
raise EvalError("-> requires at least a value")
|
raise EvalError("-> requires at least a value")
|
||||||
result = _eval(expr[1], env)
|
result = _trampoline(_eval(expr[1], env))
|
||||||
for form in expr[2:]:
|
for form in expr[2:]:
|
||||||
if isinstance(form, list):
|
if isinstance(form, list):
|
||||||
fn = _eval(form[0], env)
|
fn = _trampoline(_eval(form[0], env))
|
||||||
args = [result] + [_eval(a, env) for a in form[1:]]
|
args = [result] + [_trampoline(_eval(a, env)) for a in form[1:]]
|
||||||
else:
|
else:
|
||||||
fn = _eval(form, env)
|
fn = _trampoline(_eval(form, env))
|
||||||
args = [result]
|
args = [result]
|
||||||
if callable(fn) and not isinstance(fn, (Lambda, Component)):
|
if callable(fn) and not isinstance(fn, (Lambda, Component)):
|
||||||
result = fn(*args)
|
result = fn(*args)
|
||||||
elif isinstance(fn, Lambda):
|
elif isinstance(fn, Lambda):
|
||||||
result = _call_lambda(fn, args, env)
|
result = _trampoline(_call_lambda(fn, args, env))
|
||||||
else:
|
else:
|
||||||
raise EvalError(f"-> form not callable: {fn!r}")
|
raise EvalError(f"-> form not callable: {fn!r}")
|
||||||
return result
|
return result
|
||||||
@@ -482,14 +501,14 @@ def _qq_expand(template: Any, env: dict) -> Any:
|
|||||||
if head.name == "unquote":
|
if head.name == "unquote":
|
||||||
if len(template) < 2:
|
if len(template) < 2:
|
||||||
raise EvalError("unquote requires an expression")
|
raise EvalError("unquote requires an expression")
|
||||||
return _eval(template[1], env)
|
return _trampoline(_eval(template[1], env))
|
||||||
if head.name == "splice-unquote":
|
if head.name == "splice-unquote":
|
||||||
raise EvalError("splice-unquote not inside a list")
|
raise EvalError("splice-unquote not inside a list")
|
||||||
# Walk children, handling splice-unquote
|
# Walk children, handling splice-unquote
|
||||||
result: list[Any] = []
|
result: list[Any] = []
|
||||||
for item in template:
|
for item in template:
|
||||||
if isinstance(item, list) and len(item) == 2 and isinstance(item[0], Symbol) and item[0].name == "splice-unquote":
|
if isinstance(item, list) and len(item) == 2 and isinstance(item[0], Symbol) and item[0].name == "splice-unquote":
|
||||||
spliced = _eval(item[1], env)
|
spliced = _trampoline(_eval(item[1], env))
|
||||||
if isinstance(spliced, list):
|
if isinstance(spliced, list):
|
||||||
result.extend(spliced)
|
result.extend(spliced)
|
||||||
elif spliced is not None and spliced is not NIL:
|
elif spliced is not None and spliced is not NIL:
|
||||||
@@ -516,7 +535,7 @@ def _expand_macro(macro: Macro, raw_args: list[Any], env: dict) -> Any:
|
|||||||
rest_start = len(macro.params)
|
rest_start = len(macro.params)
|
||||||
local[macro.rest_param] = list(raw_args[rest_start:])
|
local[macro.rest_param] = list(raw_args[rest_start:])
|
||||||
|
|
||||||
return _eval(macro.body, local)
|
return _trampoline(_eval(macro.body, local))
|
||||||
|
|
||||||
|
|
||||||
def _sf_defhandler(expr: list, env: dict) -> HandlerDef:
|
def _sf_defhandler(expr: list, env: dict) -> HandlerDef:
|
||||||
@@ -629,7 +648,7 @@ def _sf_set_bang(expr: list, env: dict) -> Any:
|
|||||||
name_sym = expr[1]
|
name_sym = expr[1]
|
||||||
if not isinstance(name_sym, Symbol):
|
if not isinstance(name_sym, Symbol):
|
||||||
raise EvalError(f"set! name must be symbol, got {type(name_sym).__name__}")
|
raise EvalError(f"set! name must be symbol, got {type(name_sym).__name__}")
|
||||||
value = _eval(expr[2], env)
|
value = _trampoline(_eval(expr[2], env))
|
||||||
# Walk up scope if using Env objects; for plain dicts just overwrite
|
# Walk up scope if using Env objects; for plain dicts just overwrite
|
||||||
env[name_sym.name] = value
|
env[name_sym.name] = value
|
||||||
return value
|
return value
|
||||||
@@ -660,7 +679,7 @@ def _sf_defrelation(expr: list, env: dict) -> RelationDef:
|
|||||||
if isinstance(val, Keyword):
|
if isinstance(val, Keyword):
|
||||||
kwargs[key.name] = val.name
|
kwargs[key.name] = val.name
|
||||||
else:
|
else:
|
||||||
kwargs[key.name] = _eval(val, env) if not isinstance(val, str) else val
|
kwargs[key.name] = _trampoline(_eval(val, env)) if not isinstance(val, str) else val
|
||||||
i += 2
|
i += 2
|
||||||
else:
|
else:
|
||||||
kwargs[key.name] = None
|
kwargs[key.name] = None
|
||||||
@@ -746,7 +765,7 @@ def _sf_defpage(expr: list, env: dict) -> PageDef:
|
|||||||
elif isinstance(item, str):
|
elif isinstance(item, str):
|
||||||
auth.append(item)
|
auth.append(item)
|
||||||
else:
|
else:
|
||||||
auth.append(_eval(item, env))
|
auth.append(_trampoline(_eval(item, env)))
|
||||||
else:
|
else:
|
||||||
auth = str(auth_val) if auth_val else "public"
|
auth = str(auth_val) if auth_val else "public"
|
||||||
|
|
||||||
@@ -762,7 +781,7 @@ def _sf_defpage(expr: list, env: dict) -> PageDef:
|
|||||||
cache_val = slots.get("cache")
|
cache_val = slots.get("cache")
|
||||||
cache = None
|
cache = None
|
||||||
if cache_val is not None:
|
if cache_val is not None:
|
||||||
cache_result = _eval(cache_val, env)
|
cache_result = _trampoline(_eval(cache_val, env))
|
||||||
if isinstance(cache_result, dict):
|
if isinstance(cache_result, dict):
|
||||||
cache = cache_result
|
cache = cache_result
|
||||||
|
|
||||||
@@ -818,57 +837,57 @@ _SPECIAL_FORMS: dict[str, Any] = {
|
|||||||
def _ho_map(expr: list, env: dict) -> list:
|
def _ho_map(expr: list, env: dict) -> list:
|
||||||
if len(expr) != 3:
|
if len(expr) != 3:
|
||||||
raise EvalError("map requires fn and collection")
|
raise EvalError("map requires fn and collection")
|
||||||
fn = _eval(expr[1], env)
|
fn = _trampoline(_eval(expr[1], env))
|
||||||
coll = _eval(expr[2], env)
|
coll = _trampoline(_eval(expr[2], env))
|
||||||
if not isinstance(fn, Lambda):
|
if not isinstance(fn, Lambda):
|
||||||
raise EvalError(f"map requires lambda, got {type(fn).__name__}")
|
raise EvalError(f"map requires lambda, got {type(fn).__name__}")
|
||||||
return [_call_lambda(fn, [item], env) for item in coll]
|
return [_trampoline(_call_lambda(fn, [item], env)) for item in coll]
|
||||||
|
|
||||||
|
|
||||||
def _ho_map_indexed(expr: list, env: dict) -> list:
|
def _ho_map_indexed(expr: list, env: dict) -> list:
|
||||||
if len(expr) != 3:
|
if len(expr) != 3:
|
||||||
raise EvalError("map-indexed requires fn and collection")
|
raise EvalError("map-indexed requires fn and collection")
|
||||||
fn = _eval(expr[1], env)
|
fn = _trampoline(_eval(expr[1], env))
|
||||||
coll = _eval(expr[2], env)
|
coll = _trampoline(_eval(expr[2], env))
|
||||||
if not isinstance(fn, Lambda):
|
if not isinstance(fn, Lambda):
|
||||||
raise EvalError(f"map-indexed requires lambda, got {type(fn).__name__}")
|
raise EvalError(f"map-indexed requires lambda, got {type(fn).__name__}")
|
||||||
if len(fn.params) < 2:
|
if len(fn.params) < 2:
|
||||||
raise EvalError("map-indexed lambda needs (i item) params")
|
raise EvalError("map-indexed lambda needs (i item) params")
|
||||||
return [_call_lambda(fn, [i, item], env) for i, item in enumerate(coll)]
|
return [_trampoline(_call_lambda(fn, [i, item], env)) for i, item in enumerate(coll)]
|
||||||
|
|
||||||
|
|
||||||
def _ho_filter(expr: list, env: dict) -> list:
|
def _ho_filter(expr: list, env: dict) -> list:
|
||||||
if len(expr) != 3:
|
if len(expr) != 3:
|
||||||
raise EvalError("filter requires fn and collection")
|
raise EvalError("filter requires fn and collection")
|
||||||
fn = _eval(expr[1], env)
|
fn = _trampoline(_eval(expr[1], env))
|
||||||
coll = _eval(expr[2], env)
|
coll = _trampoline(_eval(expr[2], env))
|
||||||
if not isinstance(fn, Lambda):
|
if not isinstance(fn, Lambda):
|
||||||
raise EvalError(f"filter requires lambda, got {type(fn).__name__}")
|
raise EvalError(f"filter requires lambda, got {type(fn).__name__}")
|
||||||
return [item for item in coll if _call_lambda(fn, [item], env)]
|
return [item for item in coll if _trampoline(_call_lambda(fn, [item], env))]
|
||||||
|
|
||||||
|
|
||||||
def _ho_reduce(expr: list, env: dict) -> Any:
|
def _ho_reduce(expr: list, env: dict) -> Any:
|
||||||
if len(expr) != 4:
|
if len(expr) != 4:
|
||||||
raise EvalError("reduce requires fn, init, and collection")
|
raise EvalError("reduce requires fn, init, and collection")
|
||||||
fn = _eval(expr[1], env)
|
fn = _trampoline(_eval(expr[1], env))
|
||||||
acc = _eval(expr[2], env)
|
acc = _trampoline(_eval(expr[2], env))
|
||||||
coll = _eval(expr[3], env)
|
coll = _trampoline(_eval(expr[3], env))
|
||||||
if not isinstance(fn, Lambda):
|
if not isinstance(fn, Lambda):
|
||||||
raise EvalError(f"reduce requires lambda, got {type(fn).__name__}")
|
raise EvalError(f"reduce requires lambda, got {type(fn).__name__}")
|
||||||
for item in coll:
|
for item in coll:
|
||||||
acc = _call_lambda(fn, [acc, item], env)
|
acc = _trampoline(_call_lambda(fn, [acc, item], env))
|
||||||
return acc
|
return acc
|
||||||
|
|
||||||
|
|
||||||
def _ho_some(expr: list, env: dict) -> Any:
|
def _ho_some(expr: list, env: dict) -> Any:
|
||||||
if len(expr) != 3:
|
if len(expr) != 3:
|
||||||
raise EvalError("some requires fn and collection")
|
raise EvalError("some requires fn and collection")
|
||||||
fn = _eval(expr[1], env)
|
fn = _trampoline(_eval(expr[1], env))
|
||||||
coll = _eval(expr[2], env)
|
coll = _trampoline(_eval(expr[2], env))
|
||||||
if not isinstance(fn, Lambda):
|
if not isinstance(fn, Lambda):
|
||||||
raise EvalError(f"some requires lambda, got {type(fn).__name__}")
|
raise EvalError(f"some requires lambda, got {type(fn).__name__}")
|
||||||
for item in coll:
|
for item in coll:
|
||||||
result = _call_lambda(fn, [item], env)
|
result = _trampoline(_call_lambda(fn, [item], env))
|
||||||
if result:
|
if result:
|
||||||
return result
|
return result
|
||||||
return NIL
|
return NIL
|
||||||
@@ -877,12 +896,12 @@ def _ho_some(expr: list, env: dict) -> Any:
|
|||||||
def _ho_every(expr: list, env: dict) -> bool:
|
def _ho_every(expr: list, env: dict) -> bool:
|
||||||
if len(expr) != 3:
|
if len(expr) != 3:
|
||||||
raise EvalError("every? requires fn and collection")
|
raise EvalError("every? requires fn and collection")
|
||||||
fn = _eval(expr[1], env)
|
fn = _trampoline(_eval(expr[1], env))
|
||||||
coll = _eval(expr[2], env)
|
coll = _trampoline(_eval(expr[2], env))
|
||||||
if not isinstance(fn, Lambda):
|
if not isinstance(fn, Lambda):
|
||||||
raise EvalError(f"every? requires lambda, got {type(fn).__name__}")
|
raise EvalError(f"every? requires lambda, got {type(fn).__name__}")
|
||||||
for item in coll:
|
for item in coll:
|
||||||
if not _call_lambda(fn, [item], env):
|
if not _trampoline(_call_lambda(fn, [item], env)):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -890,12 +909,12 @@ def _ho_every(expr: list, env: dict) -> bool:
|
|||||||
def _ho_for_each(expr: list, env: dict) -> Any:
|
def _ho_for_each(expr: list, env: dict) -> Any:
|
||||||
if len(expr) != 3:
|
if len(expr) != 3:
|
||||||
raise EvalError("for-each requires fn and collection")
|
raise EvalError("for-each requires fn and collection")
|
||||||
fn = _eval(expr[1], env)
|
fn = _trampoline(_eval(expr[1], env))
|
||||||
coll = _eval(expr[2], env)
|
coll = _trampoline(_eval(expr[2], env))
|
||||||
if not isinstance(fn, Lambda):
|
if not isinstance(fn, Lambda):
|
||||||
raise EvalError(f"for-each requires lambda, got {type(fn).__name__}")
|
raise EvalError(f"for-each requires lambda, got {type(fn).__name__}")
|
||||||
for item in coll:
|
for item in coll:
|
||||||
_call_lambda(fn, [item], env)
|
_trampoline(_call_lambda(fn, [item], env))
|
||||||
return NIL
|
return NIL
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ def clear_handlers(service: str | None = None) -> None:
|
|||||||
def load_handler_file(filepath: str, service_name: str) -> list[HandlerDef]:
|
def load_handler_file(filepath: str, service_name: str) -> list[HandlerDef]:
|
||||||
"""Parse an .sx file, evaluate it, and register any HandlerDef values."""
|
"""Parse an .sx file, evaluate it, and register any HandlerDef values."""
|
||||||
from .parser import parse_all
|
from .parser import parse_all
|
||||||
from .evaluator import _eval
|
from .evaluator import _eval as _raw_eval, _trampoline
|
||||||
|
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
|
||||||
from .jinja_bridge import get_component_env
|
from .jinja_bridge import get_component_env
|
||||||
|
|
||||||
with open(filepath, encoding="utf-8") as f:
|
with open(filepath, encoding="utf-8") as f:
|
||||||
|
|||||||
@@ -28,7 +28,15 @@ import contextvars
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .types import Component, Keyword, Lambda, Macro, NIL, Symbol
|
from .types import Component, Keyword, Lambda, Macro, NIL, Symbol
|
||||||
from .evaluator import _eval, _call_component, _expand_macro
|
from .evaluator import _eval as _raw_eval, _call_component as _raw_call_component, _expand_macro, _trampoline
|
||||||
|
|
||||||
|
def _eval(expr, env):
|
||||||
|
"""Evaluate and unwrap thunks — all html.py _eval calls are non-tail."""
|
||||||
|
return _trampoline(_raw_eval(expr, env))
|
||||||
|
|
||||||
|
def _call_component(comp, raw_args, env):
|
||||||
|
"""Call component and unwrap thunks — non-tail in html.py."""
|
||||||
|
return _trampoline(_raw_call_component(comp, raw_args, env))
|
||||||
|
|
||||||
# ContextVar for collecting CSS class names during render.
|
# ContextVar for collecting CSS class names during render.
|
||||||
# Set to a set[str] to collect; None to skip.
|
# Set to a set[str] to collect; None to skip.
|
||||||
|
|||||||
@@ -169,7 +169,8 @@ def register_components(sx_source: str) -> None:
|
|||||||
(div :class "..." (div :class "..." title)))))
|
(div :class "..." (div :class "..." title)))))
|
||||||
''')
|
''')
|
||||||
"""
|
"""
|
||||||
from .evaluator import _eval
|
from .evaluator import _eval as _raw_eval, _trampoline
|
||||||
|
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
|
||||||
from .parser import parse_all
|
from .parser import parse_all
|
||||||
from .css_registry import scan_classes_from_sx
|
from .css_registry import scan_classes_from_sx
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,8 @@ def get_page_helpers(service: str) -> dict[str, Any]:
|
|||||||
def load_page_file(filepath: str, service_name: str) -> list[PageDef]:
|
def load_page_file(filepath: str, service_name: str) -> list[PageDef]:
|
||||||
"""Parse an .sx file, evaluate it, and register any PageDef values."""
|
"""Parse an .sx file, evaluate it, and register any PageDef values."""
|
||||||
from .parser import parse_all
|
from .parser import parse_all
|
||||||
from .evaluator import _eval
|
from .evaluator import _eval as _raw_eval, _trampoline
|
||||||
|
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
|
||||||
from .jinja_bridge import get_component_env
|
from .jinja_bridge import get_component_env
|
||||||
|
|
||||||
with open(filepath, encoding="utf-8") as f:
|
with open(filepath, encoding="utf-8") as f:
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ def clear(service: str | None = None) -> None:
|
|||||||
def load_query_file(filepath: str, service_name: str) -> list[QueryDef]:
|
def load_query_file(filepath: str, service_name: str) -> list[QueryDef]:
|
||||||
"""Parse an .sx file and register any defquery definitions."""
|
"""Parse an .sx file and register any defquery definitions."""
|
||||||
from .parser import parse_all
|
from .parser import parse_all
|
||||||
from .evaluator import _eval
|
from .evaluator import _eval as _raw_eval, _trampoline
|
||||||
|
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
|
||||||
from .jinja_bridge import get_component_env
|
from .jinja_bridge import get_component_env
|
||||||
|
|
||||||
with open(filepath, encoding="utf-8") as f:
|
with open(filepath, encoding="utf-8") as f:
|
||||||
@@ -102,7 +103,8 @@ def load_query_file(filepath: str, service_name: str) -> list[QueryDef]:
|
|||||||
def load_action_file(filepath: str, service_name: str) -> list[ActionDef]:
|
def load_action_file(filepath: str, service_name: str) -> list[ActionDef]:
|
||||||
"""Parse an .sx file and register any defaction definitions."""
|
"""Parse an .sx file and register any defaction definitions."""
|
||||||
from .parser import parse_all
|
from .parser import parse_all
|
||||||
from .evaluator import _eval
|
from .evaluator import _eval as _raw_eval, _trampoline
|
||||||
|
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
|
||||||
from .jinja_bridge import get_component_env
|
from .jinja_bridge import get_component_env
|
||||||
|
|
||||||
with open(filepath, encoding="utf-8") as f:
|
with open(filepath, encoding="utf-8") as f:
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ import asyncio
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .types import Component, Keyword, Lambda, NIL, Symbol
|
from .types import Component, Keyword, Lambda, NIL, Symbol
|
||||||
from .evaluator import _eval
|
from .evaluator import _eval as _raw_eval, _trampoline
|
||||||
|
|
||||||
|
def _eval(expr, env):
|
||||||
|
"""Evaluate and unwrap thunks — all resolver.py _eval calls are non-tail."""
|
||||||
|
return _trampoline(_raw_eval(expr, env))
|
||||||
from .html import render as html_render, _RawHTML
|
from .html import render as html_render, _RawHTML
|
||||||
from .primitives_io import (
|
from .primitives_io import (
|
||||||
IO_PRIMITIVES,
|
IO_PRIMITIVES,
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
(h1 :class "text-5xl font-bold text-stone-900 mb-4"
|
(h1 :class "text-5xl font-bold text-stone-900 mb-4"
|
||||||
(span :class "text-violet-600" "sx"))
|
(span :class "text-violet-600" "sx"))
|
||||||
(p :class "text-2xl text-stone-600 mb-8"
|
(p :class "text-2xl text-stone-600 mb-8"
|
||||||
"High power tools for HTML — with s-expressions")
|
"s-expressions for the web")
|
||||||
(p :class "text-lg text-stone-500 max-w-2xl mx-auto mb-12"
|
(p :class "text-lg text-stone-500 max-w-2xl mx-auto mb-12"
|
||||||
"A hypermedia-driven UI engine that combines htmx's server-first philosophy "
|
"A hypermedia-driven UI engine that combines htmx's server-first philosophy "
|
||||||
"with React's component model. All rendered via s-expressions over the wire.")
|
"with React's component model. S-expressions over the wire — no HTML, no JavaScript frameworks.")
|
||||||
(div :class "bg-stone-50 border border-stone-200 rounded-lg p-6 text-left font-mono text-sm overflow-x-auto"
|
(div :class "bg-stone-50 border border-stone-200 rounded-lg p-6 text-left font-mono text-sm overflow-x-auto"
|
||||||
(pre :class "leading-relaxed" children))))
|
(pre :class "leading-relaxed" children))))
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
(div :class "flex-shrink-0 w-8 h-8 rounded-full bg-violet-100 text-violet-700 flex items-center justify-center font-bold" "1")
|
(div :class "flex-shrink-0 w-8 h-8 rounded-full bg-violet-100 text-violet-700 flex items-center justify-center font-bold" "1")
|
||||||
(div
|
(div
|
||||||
(h3 :class "font-semibold text-stone-900" "Server renders sx")
|
(h3 :class "font-semibold text-stone-900" "Server renders sx")
|
||||||
(p :class "text-stone-600" "Python builds s-expression trees. Components, HTML elements, data — all in one format.")))
|
(p :class "text-stone-600" "Python builds s-expression trees. Components, elements, data — all in one format.")))
|
||||||
(div :class "flex items-start gap-4"
|
(div :class "flex items-start gap-4"
|
||||||
(div :class "flex-shrink-0 w-8 h-8 rounded-full bg-violet-100 text-violet-700 flex items-center justify-center font-bold" "2")
|
(div :class "flex-shrink-0 w-8 h-8 rounded-full bg-violet-100 text-violet-700 flex items-center justify-center font-bold" "2")
|
||||||
(div
|
(div
|
||||||
@@ -60,5 +60,5 @@
|
|||||||
(a :href "https://htmx.org" :class "text-violet-600 hover:underline" "htmx")
|
(a :href "https://htmx.org" :class "text-violet-600 hover:underline" "htmx")
|
||||||
" by Carson Gross. This documentation site is modelled on "
|
" by Carson Gross. This documentation site is modelled on "
|
||||||
(a :href "https://four.htmx.org" :class "text-violet-600 hover:underline" "four.htmx.org")
|
(a :href "https://four.htmx.org" :class "text-violet-600 hover:underline" "four.htmx.org")
|
||||||
". htmx showed that HTML is the right hypermedia format. "
|
". htmx showed that hypermedia belongs on the server. "
|
||||||
"sx takes that idea and wraps it in parentheses.")))
|
"sx takes that idea and wraps it in parentheses.")))
|
||||||
|
|||||||
@@ -279,14 +279,15 @@ def _docs_introduction_sx() -> str:
|
|||||||
'Components use defcomp with keyword parameters and optional children. '
|
'Components use defcomp with keyword parameters and optional children. '
|
||||||
'The evaluator supports let bindings, conditionals, lambda, map/filter/reduce, and ~80 primitives.")'
|
'The evaluator supports let bindings, conditionals, lambda, map/filter/reduce, and ~80 primitives.")'
|
||||||
' (p :class "text-stone-600"'
|
' (p :class "text-stone-600"'
|
||||||
' "sx is not trying to replace JavaScript. It\'s trying to replace the pattern of '
|
' "sx replaces the pattern of '
|
||||||
'shipping a JS framework + build step + client-side router + state management library '
|
'shipping a JS framework + build step + client-side router + state management library '
|
||||||
'just to render some server data into HTML."))'
|
'just to render some server data. For most applications, sx eliminates the need for '
|
||||||
|
'JavaScript entirely — htmx attributes handle interactivity, hyperscript handles small behaviours, '
|
||||||
|
'and the server handles everything else."))'
|
||||||
' (~doc-section :title "What sx is not" :id "not"'
|
' (~doc-section :title "What sx is not" :id "not"'
|
||||||
' (ul :class "space-y-2 text-stone-600"'
|
' (ul :class "space-y-2 text-stone-600"'
|
||||||
' (li "Not a general-purpose programming language — it\'s a UI rendering language")'
|
' (li "Not a general-purpose programming language — it\'s a UI rendering language")'
|
||||||
' (li "Not a Lisp implementation — no macros, no continuations, no tail-call optimization")'
|
' (li "Not a full Lisp — it has macros and TCO, but no continuations or call/cc")'
|
||||||
' (li "Not a replacement for JavaScript — it handles rendering, not arbitrary DOM manipulation")'
|
|
||||||
' (li "Not production-hardened at scale — it runs one website"))))'
|
' (li "Not production-hardened at scale — it runs one website"))))'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user