- Add page-helpers-demo page with defisland ~demo-client-runner (pure SX, zero JS files) showing spec functions running on both server and client - Fix _aser_component children serialization: flatten list results from map instead of serialize(list) which wraps in parens creating ((div ...) ...) that re-parses as invalid function call. Fixed in adapter-async.sx spec and async_eval_ref.py - Switch _eval_slot to use async_eval_ref.py when SX_USE_REF=1 (was hardcoded to async_eval.py) - Add Island type support to async_eval_ref.py: import, SSR rendering, aser dispatch, thread-first, defisland in _ASER_FORMS - Add server affinity check: components with :affinity :server expand even when _expand_components is False - Add diagnostic _aser_stack context to EvalError messages - New spec files: adapter-async.sx, page-helpers.sx, platform_js.py - Bootstrappers: page-helpers module support, performance.now() timing - 0-arity lambda event handler fix in adapter-dom.sx Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1023 lines
33 KiB
Python
1023 lines
33 KiB
Python
"""Async evaluation wrapper for the transpiled reference evaluator.
|
|
|
|
Wraps the sync sx_ref.py evaluator with async I/O support, mirroring
|
|
the hand-written async_eval.py. Provides the same public API:
|
|
|
|
async_eval() — evaluate with I/O primitives
|
|
async_render() — render to HTML with I/O
|
|
async_eval_to_sx() — evaluate to SX wire format with I/O
|
|
async_eval_slot_to_sx() — expand components server-side, then serialize
|
|
|
|
The sync transpiled evaluator handles all control flow, special forms,
|
|
and lambda/component dispatch. This wrapper adds:
|
|
|
|
- RequestContext threading
|
|
- I/O primitive interception (query, service, request-arg, etc.)
|
|
- Async trampoline for thunks
|
|
- SxExpr wrapping for wire format output
|
|
|
|
DO NOT EDIT by hand — this is a thin wrapper; the actual eval logic
|
|
lives in sx_ref.py (generated) and the I/O primitives in primitives_io.py.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextvars
|
|
import inspect
|
|
from typing import Any
|
|
|
|
from ..types import Component, Island, Keyword, Lambda, Macro, NIL, Symbol
|
|
from ..parser import SxExpr, serialize
|
|
from ..primitives_io import IO_PRIMITIVES, RequestContext, execute_io
|
|
from ..html import (
|
|
HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS,
|
|
escape_text, escape_attr, _RawHTML, css_class_collector, _svg_context,
|
|
)
|
|
|
|
from . import sx_ref
|
|
|
|
# Re-export EvalError from sx_ref
|
|
EvalError = sx_ref.EvalError
|
|
|
|
# When True, _aser expands known components server-side
|
|
_expand_components: contextvars.ContextVar[bool] = contextvars.ContextVar(
|
|
"_expand_components_ref", default=False
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Async TCO
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class _AsyncThunk:
|
|
__slots__ = ("expr", "env", "ctx")
|
|
def __init__(self, expr, env, ctx):
|
|
self.expr = expr
|
|
self.env = env
|
|
self.ctx = ctx
|
|
|
|
|
|
async def _async_trampoline(val):
|
|
while isinstance(val, _AsyncThunk):
|
|
val = await _async_eval(val.expr, val.env, val.ctx)
|
|
return val
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Async evaluate — wraps transpiled sync eval with I/O support
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def async_eval(expr, env, ctx=None):
|
|
"""Public entry point: evaluate with I/O primitives."""
|
|
if ctx is None:
|
|
ctx = RequestContext()
|
|
result = await _async_eval(expr, env, ctx)
|
|
while isinstance(result, _AsyncThunk):
|
|
result = await _async_eval(result.expr, result.env, result.ctx)
|
|
return result
|
|
|
|
|
|
async def _async_eval(expr, env, ctx):
|
|
"""Internal async evaluator. Intercepts I/O primitives,
|
|
delegates everything else to the sync transpiled evaluator."""
|
|
# Intercept I/O primitive calls
|
|
if isinstance(expr, list) and expr:
|
|
head = expr[0]
|
|
if isinstance(head, Symbol) and head.name in IO_PRIMITIVES:
|
|
args, kwargs = await _parse_io_args(expr[1:], env, ctx)
|
|
return await execute_io(head.name, args, kwargs, ctx)
|
|
|
|
# Check if this is a render expression (HTML tag, component, fragment)
|
|
# so we can wrap the result in _RawHTML to prevent double-escaping.
|
|
# The sync evaluator returns plain strings from render_list_to_html;
|
|
# the async renderer would HTML-escape those without this wrapper.
|
|
is_render = isinstance(expr, list) and sx_ref.is_render_expr(expr)
|
|
|
|
# For everything else, use the sync transpiled evaluator
|
|
result = sx_ref.eval_expr(expr, env)
|
|
result = sx_ref.trampoline(result)
|
|
|
|
if is_render and isinstance(result, str):
|
|
return _RawHTML(result)
|
|
return result
|
|
|
|
|
|
async def _parse_io_args(exprs, env, ctx):
|
|
"""Parse and evaluate I/O node args (keyword + positional)."""
|
|
args = []
|
|
kwargs = {}
|
|
i = 0
|
|
while i < len(exprs):
|
|
item = exprs[i]
|
|
if isinstance(item, Keyword) and i + 1 < len(exprs):
|
|
kwargs[item.name] = await async_eval(exprs[i + 1], env, ctx)
|
|
i += 2
|
|
else:
|
|
args.append(await async_eval(item, env, ctx))
|
|
i += 1
|
|
return args, kwargs
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Async HTML renderer
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def async_render(expr, env, ctx=None):
|
|
"""Render to HTML, awaiting I/O primitives inline."""
|
|
if ctx is None:
|
|
ctx = RequestContext()
|
|
return await _arender(expr, env, ctx)
|
|
|
|
|
|
async def _arender(expr, env, ctx):
|
|
if expr is None or expr is NIL or expr is False or expr is True:
|
|
return ""
|
|
if isinstance(expr, _RawHTML):
|
|
return expr.html
|
|
# Also handle sx_ref._RawHTML from the sync evaluator
|
|
if isinstance(expr, sx_ref._RawHTML):
|
|
return expr.html
|
|
if isinstance(expr, str):
|
|
return escape_text(expr)
|
|
if isinstance(expr, (int, float)):
|
|
return escape_text(str(expr))
|
|
if isinstance(expr, Symbol):
|
|
val = await async_eval(expr, env, ctx)
|
|
return await _arender(val, env, ctx)
|
|
if isinstance(expr, Keyword):
|
|
return escape_text(expr.name)
|
|
if isinstance(expr, list):
|
|
if not expr:
|
|
return ""
|
|
return await _arender_list(expr, env, ctx)
|
|
if isinstance(expr, dict):
|
|
return ""
|
|
return escape_text(str(expr))
|
|
|
|
|
|
async def _arender_list(expr, env, ctx):
|
|
head = expr[0]
|
|
if isinstance(head, Symbol):
|
|
name = head.name
|
|
|
|
# I/O primitive
|
|
if name in IO_PRIMITIVES:
|
|
result = await async_eval(expr, env, ctx)
|
|
return await _arender(result, env, ctx)
|
|
|
|
# raw!
|
|
if name == "raw!":
|
|
parts = []
|
|
for arg in expr[1:]:
|
|
val = await async_eval(arg, env, ctx)
|
|
if isinstance(val, _RawHTML):
|
|
parts.append(val.html)
|
|
elif isinstance(val, str):
|
|
parts.append(val)
|
|
elif val is not None and val is not NIL:
|
|
parts.append(str(val))
|
|
return "".join(parts)
|
|
|
|
# Fragment
|
|
if name == "<>":
|
|
parts = [await _arender(c, env, ctx) for c in expr[1:]]
|
|
return "".join(parts)
|
|
|
|
# html: prefix
|
|
if name.startswith("html:"):
|
|
return await _arender_element(name[5:], expr[1:], env, ctx)
|
|
|
|
# Render-aware special forms
|
|
arsf = _ASYNC_RENDER_FORMS.get(name)
|
|
if arsf is not None:
|
|
if name in HTML_TAGS and (
|
|
(len(expr) > 1 and isinstance(expr[1], Keyword))
|
|
or _svg_context.get(False)
|
|
):
|
|
return await _arender_element(name, expr[1:], env, ctx)
|
|
return await arsf(expr, env, ctx)
|
|
|
|
# Macro expansion
|
|
if name in env:
|
|
val = env[name]
|
|
if isinstance(val, Macro):
|
|
expanded = sx_ref.trampoline(
|
|
sx_ref.expand_macro(val, expr[1:], env)
|
|
)
|
|
return await _arender(expanded, env, ctx)
|
|
|
|
# HTML tag
|
|
if name in HTML_TAGS:
|
|
return await _arender_element(name, expr[1:], env, ctx)
|
|
|
|
# Component / Island
|
|
if name.startswith("~"):
|
|
val = env.get(name)
|
|
if isinstance(val, Island):
|
|
return sx_ref.render_html_island(val, expr[1:], env)
|
|
if isinstance(val, Component):
|
|
return await _arender_component(val, expr[1:], env, ctx)
|
|
|
|
# Custom element
|
|
if "-" in name and len(expr) > 1 and isinstance(expr[1], Keyword):
|
|
return await _arender_element(name, expr[1:], env, ctx)
|
|
|
|
# SVG context
|
|
if _svg_context.get(False):
|
|
return await _arender_element(name, expr[1:], env, ctx)
|
|
|
|
# Fallback — evaluate then render
|
|
result = await async_eval(expr, env, ctx)
|
|
return await _arender(result, env, ctx)
|
|
|
|
if isinstance(head, (Lambda, list)):
|
|
result = await async_eval(expr, env, ctx)
|
|
return await _arender(result, env, ctx)
|
|
|
|
# Data list
|
|
parts = [await _arender(item, env, ctx) for item in expr]
|
|
return "".join(parts)
|
|
|
|
|
|
async def _arender_element(tag, args, env, ctx):
|
|
attrs = {}
|
|
children = []
|
|
i = 0
|
|
while i < len(args):
|
|
arg = args[i]
|
|
if isinstance(arg, Keyword) and i + 1 < len(args):
|
|
attrs[arg.name] = await async_eval(args[i + 1], env, ctx)
|
|
i += 2
|
|
else:
|
|
children.append(arg)
|
|
i += 1
|
|
|
|
class_val = attrs.get("class")
|
|
if class_val is not None and class_val is not NIL and class_val is not False:
|
|
collector = css_class_collector.get(None)
|
|
if collector is not None:
|
|
collector.update(str(class_val).split())
|
|
|
|
parts = [f"<{tag}"]
|
|
for attr_name, attr_val in attrs.items():
|
|
if attr_val is None or attr_val is NIL or attr_val is False:
|
|
continue
|
|
if attr_name in BOOLEAN_ATTRS:
|
|
if attr_val:
|
|
parts.append(f" {attr_name}")
|
|
elif attr_val is True:
|
|
parts.append(f" {attr_name}")
|
|
else:
|
|
parts.append(f' {attr_name}="{escape_attr(str(attr_val))}"')
|
|
parts.append(">")
|
|
opening = "".join(parts)
|
|
|
|
if tag in VOID_ELEMENTS:
|
|
return opening
|
|
|
|
token = None
|
|
if tag in ("svg", "math"):
|
|
token = _svg_context.set(True)
|
|
try:
|
|
child_parts = [await _arender(c, env, ctx) for c in children]
|
|
finally:
|
|
if token is not None:
|
|
_svg_context.reset(token)
|
|
|
|
return f"{opening}{''.join(child_parts)}</{tag}>"
|
|
|
|
|
|
async def _arender_component(comp, args, env, ctx):
|
|
kwargs = {}
|
|
children = []
|
|
i = 0
|
|
while i < len(args):
|
|
arg = args[i]
|
|
if isinstance(arg, Keyword) and i + 1 < len(args):
|
|
kwargs[arg.name] = await async_eval(args[i + 1], env, ctx)
|
|
i += 2
|
|
else:
|
|
children.append(arg)
|
|
i += 1
|
|
local = dict(comp.closure)
|
|
local.update(env)
|
|
for p in comp.params:
|
|
local[p] = kwargs.get(p, NIL)
|
|
if comp.has_children:
|
|
child_html = [await _arender(c, env, ctx) for c in children]
|
|
local["children"] = _RawHTML("".join(child_html))
|
|
return await _arender(comp.body, local, ctx)
|
|
|
|
|
|
async def _arender_lambda(fn, args, env, ctx):
|
|
local = dict(fn.closure)
|
|
local.update(env)
|
|
for p, v in zip(fn.params, args):
|
|
local[p] = v
|
|
return await _arender(fn.body, local, ctx)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Render-aware special forms
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def _arsf_if(expr, env, ctx):
|
|
cond = await async_eval(expr[1], env, ctx)
|
|
if cond and cond is not NIL:
|
|
return await _arender(expr[2], env, ctx)
|
|
return await _arender(expr[3], env, ctx) if len(expr) > 3 else ""
|
|
|
|
|
|
async def _arsf_when(expr, env, ctx):
|
|
cond = await async_eval(expr[1], env, ctx)
|
|
if cond and cond is not NIL:
|
|
return "".join([await _arender(b, env, ctx) for b in expr[2:]])
|
|
return ""
|
|
|
|
|
|
async def _arsf_cond(expr, env, ctx):
|
|
clauses = expr[1:]
|
|
if not clauses:
|
|
return ""
|
|
if isinstance(clauses[0], list) and len(clauses[0]) == 2:
|
|
for clause in clauses:
|
|
test = clause[0]
|
|
if isinstance(test, Symbol) and test.name in ("else", ":else"):
|
|
return await _arender(clause[1], env, ctx)
|
|
if isinstance(test, Keyword) and test.name == "else":
|
|
return await _arender(clause[1], env, ctx)
|
|
if await async_eval(test, env, ctx):
|
|
return await _arender(clause[1], env, ctx)
|
|
else:
|
|
i = 0
|
|
while i < len(clauses) - 1:
|
|
test, result = clauses[i], clauses[i + 1]
|
|
if isinstance(test, Keyword) and test.name == "else":
|
|
return await _arender(result, env, ctx)
|
|
if isinstance(test, Symbol) and test.name in (":else", "else"):
|
|
return await _arender(result, env, ctx)
|
|
if await async_eval(test, env, ctx):
|
|
return await _arender(result, env, ctx)
|
|
i += 2
|
|
return ""
|
|
|
|
|
|
async def _arsf_let(expr, env, ctx):
|
|
bindings = expr[1]
|
|
local = dict(env)
|
|
if isinstance(bindings, list):
|
|
if bindings and isinstance(bindings[0], list):
|
|
for b in bindings:
|
|
var = b[0]
|
|
vname = var.name if isinstance(var, Symbol) else var
|
|
local[vname] = await async_eval(b[1], local, ctx)
|
|
elif len(bindings) % 2 == 0:
|
|
for i in range(0, len(bindings), 2):
|
|
var = bindings[i]
|
|
vname = var.name if isinstance(var, Symbol) else var
|
|
local[vname] = await async_eval(bindings[i + 1], local, ctx)
|
|
return "".join([await _arender(b, local, ctx) for b in expr[2:]])
|
|
|
|
|
|
async def _arsf_begin(expr, env, ctx):
|
|
return "".join([await _arender(sub, env, ctx) for sub in expr[1:]])
|
|
|
|
|
|
async def _arsf_define(expr, env, ctx):
|
|
await async_eval(expr, env, ctx)
|
|
return ""
|
|
|
|
|
|
async def _arsf_map(expr, env, ctx):
|
|
fn = await async_eval(expr[1], env, ctx)
|
|
coll = await async_eval(expr[2], env, ctx)
|
|
parts = []
|
|
for item in coll:
|
|
if isinstance(fn, Lambda):
|
|
parts.append(await _arender_lambda(fn, (item,), env, ctx))
|
|
elif callable(fn):
|
|
r = fn(item)
|
|
if inspect.iscoroutine(r):
|
|
r = await r
|
|
parts.append(await _arender(r, env, ctx))
|
|
else:
|
|
parts.append(await _arender(item, env, ctx))
|
|
return "".join(parts)
|
|
|
|
|
|
async def _arsf_map_indexed(expr, env, ctx):
|
|
fn = await async_eval(expr[1], env, ctx)
|
|
coll = await async_eval(expr[2], env, ctx)
|
|
parts = []
|
|
for i, item in enumerate(coll):
|
|
if isinstance(fn, Lambda):
|
|
parts.append(await _arender_lambda(fn, (i, item), env, ctx))
|
|
elif callable(fn):
|
|
r = fn(i, item)
|
|
if inspect.iscoroutine(r):
|
|
r = await r
|
|
parts.append(await _arender(r, env, ctx))
|
|
else:
|
|
parts.append(await _arender(item, env, ctx))
|
|
return "".join(parts)
|
|
|
|
|
|
async def _arsf_filter(expr, env, ctx):
|
|
result = await async_eval(expr, env, ctx)
|
|
return await _arender(result, env, ctx)
|
|
|
|
|
|
async def _arsf_for_each(expr, env, ctx):
|
|
fn = await async_eval(expr[1], env, ctx)
|
|
coll = await async_eval(expr[2], env, ctx)
|
|
parts = []
|
|
for item in coll:
|
|
if isinstance(fn, Lambda):
|
|
parts.append(await _arender_lambda(fn, (item,), env, ctx))
|
|
elif callable(fn):
|
|
r = fn(item)
|
|
if inspect.iscoroutine(r):
|
|
r = await r
|
|
parts.append(await _arender(r, env, ctx))
|
|
else:
|
|
parts.append(await _arender(item, env, ctx))
|
|
return "".join(parts)
|
|
|
|
|
|
_ASYNC_RENDER_FORMS = {
|
|
"if": _arsf_if,
|
|
"when": _arsf_when,
|
|
"cond": _arsf_cond,
|
|
"let": _arsf_let,
|
|
"let*": _arsf_let,
|
|
"begin": _arsf_begin,
|
|
"do": _arsf_begin,
|
|
"define": _arsf_define,
|
|
"defstyle": _arsf_define,
|
|
"defcomp": _arsf_define,
|
|
"defmacro": _arsf_define,
|
|
"defhandler": _arsf_define,
|
|
"defisland": _arsf_define,
|
|
"map": _arsf_map,
|
|
"map-indexed": _arsf_map_indexed,
|
|
"filter": _arsf_filter,
|
|
"for-each": _arsf_for_each,
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Async SX wire format (aser)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def async_eval_to_sx(expr, env, ctx=None):
|
|
"""Evaluate and produce SX source string (wire format)."""
|
|
if ctx is None:
|
|
ctx = RequestContext()
|
|
result = await _aser(expr, env, ctx)
|
|
if isinstance(result, SxExpr):
|
|
return result
|
|
if result is None or result is NIL:
|
|
return SxExpr("")
|
|
if isinstance(result, str):
|
|
return SxExpr(result)
|
|
return SxExpr(serialize(result))
|
|
|
|
|
|
async def async_eval_slot_to_sx(expr, env, ctx=None):
|
|
"""Like async_eval_to_sx but expands component calls server-side."""
|
|
if ctx is None:
|
|
ctx = RequestContext()
|
|
token = _expand_components.set(True)
|
|
try:
|
|
return await _eval_slot_inner(expr, env, ctx)
|
|
finally:
|
|
_expand_components.reset(token)
|
|
|
|
|
|
async def _eval_slot_inner(expr, env, ctx):
|
|
if isinstance(expr, list) and expr:
|
|
head = expr[0]
|
|
if isinstance(head, Symbol) and head.name.startswith("~"):
|
|
comp = env.get(head.name)
|
|
if isinstance(comp, Component):
|
|
result = await _aser_component(comp, expr[1:], env, ctx)
|
|
if isinstance(result, SxExpr):
|
|
return result
|
|
if result is None or result is NIL:
|
|
return SxExpr("")
|
|
if isinstance(result, str):
|
|
return SxExpr(result)
|
|
return SxExpr(serialize(result))
|
|
elif isinstance(comp, Island):
|
|
pass # Islands serialize as SX for client hydration
|
|
result = await _aser(expr, env, ctx)
|
|
result = await _maybe_expand_component_result(result, env, ctx)
|
|
if isinstance(result, SxExpr):
|
|
return result
|
|
if result is None or result is NIL:
|
|
return SxExpr("")
|
|
if isinstance(result, str):
|
|
return SxExpr(result)
|
|
return SxExpr(serialize(result))
|
|
|
|
|
|
async def _maybe_expand_component_result(result, env, ctx):
|
|
raw = None
|
|
if isinstance(result, SxExpr):
|
|
raw = str(result).strip()
|
|
elif isinstance(result, str):
|
|
raw = result.strip()
|
|
if raw and raw.startswith("(~"):
|
|
from ..parser import parse_all
|
|
parsed = parse_all(raw)
|
|
if parsed:
|
|
return await async_eval_slot_to_sx(parsed[0], env, ctx)
|
|
return result
|
|
|
|
|
|
_aser_stack: list[str] = [] # diagnostic: track expression context
|
|
|
|
|
|
async def _aser(expr, env, ctx):
|
|
"""Evaluate for SX wire format — serialize rendering forms, evaluate control flow."""
|
|
if isinstance(expr, (int, float, bool)):
|
|
return expr
|
|
if isinstance(expr, SxExpr):
|
|
return expr
|
|
if isinstance(expr, str):
|
|
return expr
|
|
if expr is None or expr is NIL:
|
|
return NIL
|
|
|
|
if isinstance(expr, Symbol):
|
|
name = expr.name
|
|
if name in env:
|
|
return env[name]
|
|
if sx_ref.is_primitive(name):
|
|
return sx_ref.get_primitive(name)
|
|
if name == "true":
|
|
return True
|
|
if name == "false":
|
|
return False
|
|
if name == "nil":
|
|
return NIL
|
|
ctx_info = " → ".join(_aser_stack[-5:]) if _aser_stack else "(top)"
|
|
raise EvalError(f"Undefined symbol: {name} [aser context: {ctx_info}]")
|
|
|
|
if isinstance(expr, Keyword):
|
|
return expr.name
|
|
|
|
if isinstance(expr, dict):
|
|
return {k: await _aser(v, env, ctx) for k, v in expr.items()}
|
|
|
|
if not isinstance(expr, list):
|
|
return expr
|
|
if not expr:
|
|
return []
|
|
|
|
head = expr[0]
|
|
if not isinstance(head, (Symbol, Lambda, list)):
|
|
return [await _aser(x, env, ctx) for x in expr]
|
|
|
|
if isinstance(head, Symbol):
|
|
name = head.name
|
|
|
|
# I/O primitives
|
|
if name in IO_PRIMITIVES:
|
|
args, kwargs = await _parse_io_args(expr[1:], env, ctx)
|
|
return await execute_io(name, args, kwargs, ctx)
|
|
|
|
# Fragment
|
|
if name == "<>":
|
|
return await _aser_fragment(expr[1:], env, ctx)
|
|
|
|
# raw!
|
|
if name == "raw!":
|
|
return await _aser_call("raw!", expr[1:], env, ctx)
|
|
|
|
# html: prefix
|
|
if name.startswith("html:"):
|
|
return await _aser_call(name[5:], expr[1:], env, ctx)
|
|
|
|
# Component / Island call
|
|
if name.startswith("~"):
|
|
val = env.get(name)
|
|
if isinstance(val, Macro):
|
|
expanded = sx_ref.trampoline(
|
|
sx_ref.expand_macro(val, expr[1:], env)
|
|
)
|
|
return await _aser(expanded, env, ctx)
|
|
if isinstance(val, Component) and (
|
|
_expand_components.get()
|
|
or getattr(val, "render_target", None) == "server"
|
|
):
|
|
return await _aser_component(val, expr[1:], env, ctx)
|
|
return await _aser_call(name, expr[1:], env, ctx)
|
|
|
|
# Serialize-mode special/HO forms
|
|
sf = _ASER_FORMS.get(name)
|
|
if sf is not None:
|
|
if name in HTML_TAGS and (
|
|
(len(expr) > 1 and isinstance(expr[1], Keyword))
|
|
or _svg_context.get(False)
|
|
):
|
|
return await _aser_call(name, expr[1:], env, ctx)
|
|
return await sf(expr, env, ctx)
|
|
|
|
# HTML tag
|
|
if name in HTML_TAGS:
|
|
return await _aser_call(name, expr[1:], env, ctx)
|
|
|
|
# Macro
|
|
if name in env:
|
|
val = env[name]
|
|
if isinstance(val, Macro):
|
|
expanded = sx_ref.trampoline(
|
|
sx_ref.expand_macro(val, expr[1:], env)
|
|
)
|
|
return await _aser(expanded, env, ctx)
|
|
|
|
# Custom element
|
|
if "-" in name and len(expr) > 1 and isinstance(expr[1], Keyword):
|
|
return await _aser_call(name, expr[1:], env, ctx)
|
|
|
|
# SVG context
|
|
if _svg_context.get(False):
|
|
return await _aser_call(name, expr[1:], env, ctx)
|
|
|
|
# Function/lambda call — fallback: evaluate head as callable
|
|
fn = await async_eval(head, env, ctx)
|
|
args = [await async_eval(a, env, ctx) for a in expr[1:]]
|
|
|
|
if callable(fn) and not isinstance(fn, (Lambda, Component, Island)):
|
|
result = fn(*args)
|
|
if inspect.iscoroutine(result):
|
|
return await result
|
|
return result
|
|
if isinstance(fn, Lambda):
|
|
local = dict(fn.closure)
|
|
local.update(env)
|
|
for p, v in zip(fn.params, args):
|
|
local[p] = v
|
|
return await _aser(fn.body, local, ctx)
|
|
if isinstance(fn, Component):
|
|
return await _aser_call(f"~{fn.name}", expr[1:], env, ctx)
|
|
if isinstance(fn, Island):
|
|
return await _aser_call(f"~{fn.name}", expr[1:], env, ctx)
|
|
raise EvalError(f"Not callable in aser: {fn!r} (expr head: {head!r})")
|
|
|
|
|
|
async def _aser_fragment(children, env, ctx):
|
|
parts = []
|
|
for child in children:
|
|
result = await _aser(child, env, ctx)
|
|
if isinstance(result, list):
|
|
for item in result:
|
|
if item is not NIL and item is not None:
|
|
parts.append(serialize(item))
|
|
elif result is not NIL and result is not None:
|
|
parts.append(serialize(result))
|
|
if not parts:
|
|
return SxExpr("")
|
|
return SxExpr("(<> " + " ".join(parts) + ")")
|
|
|
|
|
|
async def _aser_component(comp, args, env, ctx):
|
|
_aser_stack.append(f"~{comp.name}")
|
|
try:
|
|
kwargs = {}
|
|
children = []
|
|
i = 0
|
|
while i < len(args):
|
|
arg = args[i]
|
|
if isinstance(arg, Keyword) and i + 1 < len(args):
|
|
kwargs[arg.name] = await _aser(args[i + 1], env, ctx)
|
|
i += 2
|
|
else:
|
|
children.append(arg)
|
|
i += 1
|
|
local = dict(comp.closure)
|
|
local.update(env)
|
|
for p in comp.params:
|
|
local[p] = kwargs.get(p, NIL)
|
|
if comp.has_children:
|
|
child_parts = []
|
|
for c in children:
|
|
result = await _aser(c, env, ctx)
|
|
if isinstance(result, list):
|
|
for item in result:
|
|
if item is not NIL and item is not None:
|
|
child_parts.append(serialize(item))
|
|
elif result is not NIL and result is not None:
|
|
child_parts.append(serialize(result))
|
|
local["children"] = SxExpr("(<> " + " ".join(child_parts) + ")")
|
|
return await _aser(comp.body, local, ctx)
|
|
finally:
|
|
_aser_stack.pop()
|
|
|
|
|
|
async def _aser_call(name, args, env, ctx):
|
|
_aser_stack.append(name)
|
|
token = None
|
|
if name in ("svg", "math"):
|
|
token = _svg_context.set(True)
|
|
try:
|
|
parts = [name]
|
|
extra_class = None
|
|
i = 0
|
|
while i < len(args):
|
|
arg = args[i]
|
|
if isinstance(arg, Keyword) and i + 1 < len(args):
|
|
val = await _aser(args[i + 1], env, ctx)
|
|
if val is not NIL and val is not None:
|
|
parts.append(f":{arg.name}")
|
|
if isinstance(val, list):
|
|
live = [v for v in val if v is not NIL and v is not None]
|
|
items = [serialize(v) for v in live]
|
|
if not items:
|
|
parts.append("nil")
|
|
elif any(isinstance(v, SxExpr) for v in live):
|
|
parts.append("(<> " + " ".join(items) + ")")
|
|
else:
|
|
parts.append("(list " + " ".join(items) + ")")
|
|
else:
|
|
parts.append(serialize(val))
|
|
i += 2
|
|
else:
|
|
result = await _aser(arg, env, ctx)
|
|
if result is not NIL and result is not None:
|
|
if isinstance(result, list):
|
|
for item in result:
|
|
if item is not NIL and item is not None:
|
|
parts.append(serialize(item))
|
|
else:
|
|
parts.append(serialize(result))
|
|
i += 1
|
|
if extra_class:
|
|
_merge_class_into_parts(parts, extra_class)
|
|
return SxExpr("(" + " ".join(parts) + ")")
|
|
finally:
|
|
_aser_stack.pop()
|
|
if token is not None:
|
|
_svg_context.reset(token)
|
|
|
|
|
|
def _merge_class_into_parts(parts, class_name):
|
|
for i, p in enumerate(parts):
|
|
if p == ":class" and i + 1 < len(parts):
|
|
existing = parts[i + 1]
|
|
if existing.startswith('"') and existing.endswith('"'):
|
|
parts[i + 1] = existing[:-1] + " " + class_name + '"'
|
|
else:
|
|
parts[i + 1] = f'(str {existing} " {class_name}")'
|
|
return
|
|
parts.insert(1, f'"{class_name}"')
|
|
parts.insert(1, ":class")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Aser-mode special forms
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def _assf_if(expr, env, ctx):
|
|
cond = await async_eval(expr[1], env, ctx)
|
|
if cond and cond is not NIL:
|
|
return await _aser(expr[2], env, ctx)
|
|
return await _aser(expr[3], env, ctx) if len(expr) > 3 else NIL
|
|
|
|
|
|
async def _assf_when(expr, env, ctx):
|
|
cond = await async_eval(expr[1], env, ctx)
|
|
if cond and cond is not NIL:
|
|
result = NIL
|
|
for body_expr in expr[2:]:
|
|
result = await _aser(body_expr, env, ctx)
|
|
return result
|
|
return NIL
|
|
|
|
|
|
async def _assf_let(expr, env, ctx):
|
|
bindings = expr[1]
|
|
local = dict(env)
|
|
if isinstance(bindings, list):
|
|
if bindings and isinstance(bindings[0], list):
|
|
for b in bindings:
|
|
var = b[0]
|
|
vname = var.name if isinstance(var, Symbol) else var
|
|
local[vname] = await _aser(b[1], local, ctx)
|
|
elif len(bindings) % 2 == 0:
|
|
for i in range(0, len(bindings), 2):
|
|
var = bindings[i]
|
|
vname = var.name if isinstance(var, Symbol) else var
|
|
local[vname] = await _aser(bindings[i + 1], local, ctx)
|
|
result = NIL
|
|
for body_expr in expr[2:]:
|
|
result = await _aser(body_expr, local, ctx)
|
|
return result
|
|
|
|
|
|
async def _assf_cond(expr, env, ctx):
|
|
clauses = expr[1:]
|
|
if not clauses:
|
|
return NIL
|
|
if isinstance(clauses[0], list) and len(clauses[0]) == 2:
|
|
for clause in clauses:
|
|
test = clause[0]
|
|
if isinstance(test, Symbol) and test.name in ("else", ":else"):
|
|
return await _aser(clause[1], env, ctx)
|
|
if isinstance(test, Keyword) and test.name == "else":
|
|
return await _aser(clause[1], env, ctx)
|
|
if await async_eval(test, env, ctx):
|
|
return await _aser(clause[1], env, ctx)
|
|
else:
|
|
i = 0
|
|
while i < len(clauses) - 1:
|
|
test, result = clauses[i], clauses[i + 1]
|
|
if isinstance(test, Keyword) and test.name == "else":
|
|
return await _aser(result, env, ctx)
|
|
if isinstance(test, Symbol) and test.name in (":else", "else"):
|
|
return await _aser(result, env, ctx)
|
|
if await async_eval(test, env, ctx):
|
|
return await _aser(result, env, ctx)
|
|
i += 2
|
|
return NIL
|
|
|
|
|
|
async def _assf_case(expr, env, ctx):
|
|
match_val = await async_eval(expr[1], env, ctx)
|
|
clauses = expr[2:]
|
|
i = 0
|
|
while i < len(clauses) - 1:
|
|
test, result = clauses[i], clauses[i + 1]
|
|
if isinstance(test, Keyword) and test.name == "else":
|
|
return await _aser(result, env, ctx)
|
|
if isinstance(test, Symbol) and test.name in (":else", "else"):
|
|
return await _aser(result, env, ctx)
|
|
if match_val == await async_eval(test, env, ctx):
|
|
return await _aser(result, env, ctx)
|
|
i += 2
|
|
return NIL
|
|
|
|
|
|
async def _assf_begin(expr, env, ctx):
|
|
result = NIL
|
|
for sub in expr[1:]:
|
|
result = await _aser(sub, env, ctx)
|
|
return result
|
|
|
|
|
|
async def _assf_define(expr, env, ctx):
|
|
await async_eval(expr, env, ctx)
|
|
return NIL
|
|
|
|
|
|
async def _assf_and(expr, env, ctx):
|
|
result = True
|
|
for arg in expr[1:]:
|
|
result = await async_eval(arg, env, ctx)
|
|
if not result:
|
|
return result
|
|
return result
|
|
|
|
|
|
async def _assf_or(expr, env, ctx):
|
|
result = False
|
|
for arg in expr[1:]:
|
|
result = await async_eval(arg, env, ctx)
|
|
if result:
|
|
return result
|
|
return result
|
|
|
|
|
|
async def _assf_lambda(expr, env, ctx):
|
|
params_expr = expr[1]
|
|
param_names = []
|
|
for p in params_expr:
|
|
if isinstance(p, Symbol):
|
|
param_names.append(p.name)
|
|
elif isinstance(p, str):
|
|
param_names.append(p)
|
|
return Lambda(param_names, expr[2], dict(env))
|
|
|
|
|
|
async def _assf_quote(expr, env, ctx):
|
|
return expr[1] if len(expr) > 1 else NIL
|
|
|
|
|
|
async def _assf_thread_first(expr, env, ctx):
|
|
result = await async_eval(expr[1], env, ctx)
|
|
for form in expr[2:]:
|
|
if isinstance(form, list):
|
|
fn = await async_eval(form[0], env, ctx)
|
|
fn_args = [result] + [await async_eval(a, env, ctx) for a in form[1:]]
|
|
else:
|
|
fn = await async_eval(form, env, ctx)
|
|
fn_args = [result]
|
|
if callable(fn) and not isinstance(fn, (Lambda, Component, Island)):
|
|
result = fn(*fn_args)
|
|
if inspect.iscoroutine(result):
|
|
result = await result
|
|
elif isinstance(fn, Lambda):
|
|
local = dict(fn.closure)
|
|
local.update(env)
|
|
for p, v in zip(fn.params, fn_args):
|
|
local[p] = v
|
|
result = await async_eval(fn.body, local, ctx)
|
|
else:
|
|
raise EvalError(f"-> form not callable: {fn!r}")
|
|
return result
|
|
|
|
|
|
async def _assf_set_bang(expr, env, ctx):
|
|
value = await async_eval(expr[2], env, ctx)
|
|
env[expr[1].name] = value
|
|
return value
|
|
|
|
|
|
# Aser-mode HO forms
|
|
|
|
async def _asho_map(expr, env, ctx):
|
|
fn = await async_eval(expr[1], env, ctx)
|
|
coll = await async_eval(expr[2], env, ctx)
|
|
results = []
|
|
for item in coll:
|
|
if isinstance(fn, Lambda):
|
|
local = dict(fn.closure)
|
|
local.update(env)
|
|
local[fn.params[0]] = item
|
|
results.append(await _aser(fn.body, local, ctx))
|
|
elif callable(fn):
|
|
r = fn(item)
|
|
results.append(await r if inspect.iscoroutine(r) else r)
|
|
else:
|
|
raise EvalError(f"map requires callable, got {type(fn).__name__}")
|
|
return results
|
|
|
|
|
|
async def _asho_map_indexed(expr, env, ctx):
|
|
fn = await async_eval(expr[1], env, ctx)
|
|
coll = await async_eval(expr[2], env, ctx)
|
|
results = []
|
|
for i, item in enumerate(coll):
|
|
if isinstance(fn, Lambda):
|
|
local = dict(fn.closure)
|
|
local.update(env)
|
|
local[fn.params[0]] = i
|
|
local[fn.params[1]] = item
|
|
results.append(await _aser(fn.body, local, ctx))
|
|
elif callable(fn):
|
|
r = fn(i, item)
|
|
results.append(await r if inspect.iscoroutine(r) else r)
|
|
else:
|
|
raise EvalError(f"map-indexed requires callable, got {type(fn).__name__}")
|
|
return results
|
|
|
|
|
|
async def _asho_filter(expr, env, ctx):
|
|
return await async_eval(expr, env, ctx)
|
|
|
|
|
|
async def _asho_for_each(expr, env, ctx):
|
|
fn = await async_eval(expr[1], env, ctx)
|
|
coll = await async_eval(expr[2], env, ctx)
|
|
results = []
|
|
for item in coll:
|
|
if isinstance(fn, Lambda):
|
|
local = dict(fn.closure)
|
|
local.update(env)
|
|
local[fn.params[0]] = item
|
|
results.append(await _aser(fn.body, local, ctx))
|
|
elif callable(fn):
|
|
r = fn(item)
|
|
results.append(await r if inspect.iscoroutine(r) else r)
|
|
return results
|
|
|
|
|
|
_ASER_FORMS = {
|
|
"if": _assf_if,
|
|
"when": _assf_when,
|
|
"cond": _assf_cond,
|
|
"case": _assf_case,
|
|
"and": _assf_and,
|
|
"or": _assf_or,
|
|
"let": _assf_let,
|
|
"let*": _assf_let,
|
|
"lambda": _assf_lambda,
|
|
"fn": _assf_lambda,
|
|
"define": _assf_define,
|
|
"defstyle": _assf_define,
|
|
"defcomp": _assf_define,
|
|
"defmacro": _assf_define,
|
|
"defhandler": _assf_define,
|
|
"defisland": _assf_define,
|
|
"begin": _assf_begin,
|
|
"do": _assf_begin,
|
|
"quote": _assf_quote,
|
|
"->": _assf_thread_first,
|
|
"set!": _assf_set_bang,
|
|
"map": _asho_map,
|
|
"map-indexed": _asho_map_indexed,
|
|
"filter": _asho_filter,
|
|
"for-each": _asho_for_each,
|
|
}
|