Page helpers demo: defisland, map-in-children fix, _eval_slot ref evaluator
- 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>
This commit is contained in:
@@ -26,7 +26,7 @@ import contextvars
|
||||
import inspect
|
||||
from typing import Any
|
||||
|
||||
from ..types import Component, Keyword, Lambda, Macro, NIL, Symbol
|
||||
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 (
|
||||
@@ -210,9 +210,11 @@ async def _arender_list(expr, env, ctx):
|
||||
if name in HTML_TAGS:
|
||||
return await _arender_element(name, expr[1:], env, ctx)
|
||||
|
||||
# Component
|
||||
# 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)
|
||||
|
||||
@@ -455,6 +457,7 @@ _ASYNC_RENDER_FORMS = {
|
||||
"defcomp": _arsf_define,
|
||||
"defmacro": _arsf_define,
|
||||
"defhandler": _arsf_define,
|
||||
"defisland": _arsf_define,
|
||||
"map": _arsf_map,
|
||||
"map-indexed": _arsf_map_indexed,
|
||||
"filter": _arsf_filter,
|
||||
@@ -505,6 +508,8 @@ async def _eval_slot_inner(expr, env, ctx):
|
||||
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):
|
||||
@@ -530,6 +535,9 @@ async def _maybe_expand_component_result(result, 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)):
|
||||
@@ -553,7 +561,8 @@ async def _aser(expr, env, ctx):
|
||||
return False
|
||||
if name == "nil":
|
||||
return NIL
|
||||
raise EvalError(f"Undefined symbol: {name}")
|
||||
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
|
||||
@@ -590,7 +599,7 @@ async def _aser(expr, env, ctx):
|
||||
if name.startswith("html:"):
|
||||
return await _aser_call(name[5:], expr[1:], env, ctx)
|
||||
|
||||
# Component call
|
||||
# Component / Island call
|
||||
if name.startswith("~"):
|
||||
val = env.get(name)
|
||||
if isinstance(val, Macro):
|
||||
@@ -598,7 +607,10 @@ async def _aser(expr, env, ctx):
|
||||
sx_ref.expand_macro(val, expr[1:], env)
|
||||
)
|
||||
return await _aser(expanded, env, ctx)
|
||||
if isinstance(val, Component) and _expand_components.get():
|
||||
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)
|
||||
|
||||
@@ -633,11 +645,11 @@ async def _aser(expr, env, ctx):
|
||||
if _svg_context.get(False):
|
||||
return await _aser_call(name, expr[1:], env, ctx)
|
||||
|
||||
# Function/lambda call
|
||||
# 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)):
|
||||
if callable(fn) and not isinstance(fn, (Lambda, Component, Island)):
|
||||
result = fn(*args)
|
||||
if inspect.iscoroutine(result):
|
||||
return await result
|
||||
@@ -650,7 +662,9 @@ async def _aser(expr, env, ctx):
|
||||
return await _aser(fn.body, local, ctx)
|
||||
if isinstance(fn, Component):
|
||||
return await _aser_call(f"~{fn.name}", expr[1:], env, ctx)
|
||||
raise EvalError(f"Not callable: {fn!r}")
|
||||
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):
|
||||
@@ -669,28 +683,41 @@ async def _aser_fragment(children, env, ctx):
|
||||
|
||||
|
||||
async def _aser_component(comp, args, env, ctx):
|
||||
kwargs = {}
|
||||
children = []
|
||||
i = 0
|
||||
while i < len(args):
|
||||
arg = args[i]
|
||||
if isinstance(arg, Keyword) and i + 1 < len(args):
|
||||
kwargs[arg.name] = await _aser(args[i + 1], env, ctx)
|
||||
i += 2
|
||||
else:
|
||||
children.append(arg)
|
||||
i += 1
|
||||
local = dict(comp.closure)
|
||||
local.update(env)
|
||||
for p in comp.params:
|
||||
local[p] = kwargs.get(p, NIL)
|
||||
if comp.has_children:
|
||||
child_parts = [serialize(await _aser(c, env, ctx)) for c in children]
|
||||
local["children"] = SxExpr("(<> " + " ".join(child_parts) + ")")
|
||||
return await _aser(comp.body, local, ctx)
|
||||
_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)
|
||||
@@ -730,6 +757,7 @@ async def _aser_call(name, args, env, ctx):
|
||||
_merge_class_into_parts(parts, extra_class)
|
||||
return SxExpr("(" + " ".join(parts) + ")")
|
||||
finally:
|
||||
_aser_stack.pop()
|
||||
if token is not None:
|
||||
_svg_context.reset(token)
|
||||
|
||||
@@ -885,7 +913,7 @@ async def _assf_thread_first(expr, env, ctx):
|
||||
else:
|
||||
fn = await async_eval(form, env, ctx)
|
||||
fn_args = [result]
|
||||
if callable(fn) and not isinstance(fn, (Lambda, Component)):
|
||||
if callable(fn) and not isinstance(fn, (Lambda, Component, Island)):
|
||||
result = fn(*fn_args)
|
||||
if inspect.iscoroutine(result):
|
||||
result = await result
|
||||
@@ -981,6 +1009,7 @@ _ASER_FORMS = {
|
||||
"defcomp": _assf_define,
|
||||
"defmacro": _assf_define,
|
||||
"defhandler": _assf_define,
|
||||
"defisland": _assf_define,
|
||||
"begin": _assf_begin,
|
||||
"do": _assf_begin,
|
||||
"quote": _assf_quote,
|
||||
|
||||
Reference in New Issue
Block a user