"""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, Keyword, Lambda, Macro, NIL, StyleValue, 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) # For everything else, use the sync transpiled evaluator result = sx_ref.eval_expr(expr, env) return sx_ref.trampoline(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 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 if name.startswith("~"): val = env.get(name) 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 # StyleValue → class style_val = attrs.get("style") if isinstance(style_val, StyleValue): from ..css_registry import register_generated_rule register_generated_rule(style_val) existing = attrs.get("class") if existing and existing is not NIL and existing is not False: attrs["class"] = f"{existing} {style_val.class_name}" else: attrs["class"] = style_val.class_name del attrs["style"] 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)}" 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, "defkeyframes": _arsf_define, "defcomp": _arsf_define, "defmacro": _arsf_define, "defhandler": _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)) 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 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 raise EvalError(f"Undefined symbol: {name}") 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 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(): 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 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)): 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) raise EvalError(f"Not callable: {fn!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): 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) async def _aser_call(name, args, env, ctx): 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: if arg.name == "style" and isinstance(val, StyleValue): from ..css_registry import register_generated_rule register_generated_rule(val) extra_class = val.class_name else: 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: 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)): 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, "defkeyframes": _assf_define, "defcomp": _assf_define, "defmacro": _assf_define, "defhandler": _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, }