Add macros, declarative handlers (defhandler), and convert all fragment routes to sx
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Phase 1 — Macros: defmacro + quasiquote syntax (`, ,, ,@) in parser,
evaluator, HTML renderer, and JS mirror. Macro type, expansion, and
round-trip serialization.
Phase 2 — Expanded primitives: app-url, url-for, asset-url, config,
format-date, parse-int (pure); service, request-arg, request-path,
nav-tree, get-children (I/O); jinja-global, relations-from (pure).
Updated _io_service to accept (service "registry-name" "method" :kwargs)
with auto kebab→snake conversion. DTO-to-dict now expands datetime fields
into year/month/day convenience keys. Tuple returns converted to lists.
Phase 3 — Declarative handlers: HandlerDef type, defhandler special form,
handler registry (service → name → HandlerDef), async evaluator+renderer
(async_eval.py) that awaits I/O primitives inline within control flow.
Handler loading from .sx files, execute_handler, blueprint factory.
Phase 4 — Convert all fragment routes: 13 Python fragment handlers across
8 services replaced with declarative .sx handler files. All routes.py
simplified to uniform sx dispatch pattern. Two Jinja HTML handlers
(events/container-cards, events/account-page) kept as Python.
New files: shared/sx/async_eval.py, shared/sx/handlers.py,
shared/sx/tests/test_handlers.py, plus 13 handler .sx files under
{service}/sx/handlers/. MarketService.product_by_slug() added.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from .types import Component, Keyword, Lambda, NIL, RelationDef, Symbol
|
||||
from .types import Component, HandlerDef, Keyword, Lambda, Macro, NIL, RelationDef, Symbol
|
||||
from .primitives import _PRIMITIVES
|
||||
|
||||
|
||||
@@ -117,6 +117,13 @@ def _eval(expr: Any, env: dict[str, Any]) -> Any:
|
||||
if ho is not None:
|
||||
return ho(expr, env)
|
||||
|
||||
# Macro expansion — if head resolves to a Macro, expand then eval
|
||||
if name in env:
|
||||
val = env[name]
|
||||
if isinstance(val, Macro):
|
||||
expanded = _expand_macro(val, expr[1:], env)
|
||||
return _eval(expanded, env)
|
||||
|
||||
# --- function / lambda call -------------------------------------------
|
||||
fn = _eval(head, env)
|
||||
args = [_eval(a, env) for a in expr[1:]]
|
||||
@@ -417,6 +424,135 @@ def _sf_thread_first(expr: list, env: dict) -> Any:
|
||||
return result
|
||||
|
||||
|
||||
def _sf_defmacro(expr: list, env: dict) -> Macro:
|
||||
"""``(defmacro name (params... &rest rest) body)``"""
|
||||
if len(expr) < 4:
|
||||
raise EvalError("defmacro requires name, params, and body")
|
||||
name_sym = expr[1]
|
||||
if not isinstance(name_sym, Symbol):
|
||||
raise EvalError(f"defmacro name must be symbol, got {type(name_sym).__name__}")
|
||||
|
||||
params_expr = expr[2]
|
||||
if not isinstance(params_expr, list):
|
||||
raise EvalError("defmacro params must be a list")
|
||||
|
||||
params: list[str] = []
|
||||
rest_param: str | None = None
|
||||
i = 0
|
||||
while i < len(params_expr):
|
||||
p = params_expr[i]
|
||||
if isinstance(p, Symbol) and p.name == "&rest":
|
||||
if i + 1 < len(params_expr):
|
||||
rp = params_expr[i + 1]
|
||||
rest_param = rp.name if isinstance(rp, Symbol) else str(rp)
|
||||
break
|
||||
if isinstance(p, Symbol):
|
||||
params.append(p.name)
|
||||
elif isinstance(p, str):
|
||||
params.append(p)
|
||||
i += 1
|
||||
|
||||
macro = Macro(
|
||||
params=params,
|
||||
rest_param=rest_param,
|
||||
body=expr[3],
|
||||
closure=dict(env),
|
||||
name=name_sym.name,
|
||||
)
|
||||
env[name_sym.name] = macro
|
||||
return macro
|
||||
|
||||
|
||||
def _sf_quasiquote(expr: list, env: dict) -> Any:
|
||||
"""``(quasiquote template)`` — process quasiquote template."""
|
||||
if len(expr) < 2:
|
||||
raise EvalError("quasiquote requires a template")
|
||||
return _qq_expand(expr[1], env)
|
||||
|
||||
|
||||
def _qq_expand(template: Any, env: dict) -> Any:
|
||||
"""Walk a quasiquote template, replacing unquote/splice-unquote."""
|
||||
if not isinstance(template, list):
|
||||
return template
|
||||
if not template:
|
||||
return []
|
||||
# Check for (unquote x) or (splice-unquote x)
|
||||
head = template[0]
|
||||
if isinstance(head, Symbol):
|
||||
if head.name == "unquote":
|
||||
if len(template) < 2:
|
||||
raise EvalError("unquote requires an expression")
|
||||
return _eval(template[1], env)
|
||||
if head.name == "splice-unquote":
|
||||
raise EvalError("splice-unquote not inside a list")
|
||||
# Walk children, handling splice-unquote
|
||||
result: list[Any] = []
|
||||
for item in template:
|
||||
if isinstance(item, list) and len(item) == 2 and isinstance(item[0], Symbol) and item[0].name == "splice-unquote":
|
||||
spliced = _eval(item[1], env)
|
||||
if isinstance(spliced, list):
|
||||
result.extend(spliced)
|
||||
elif spliced is not None and spliced is not NIL:
|
||||
result.append(spliced)
|
||||
else:
|
||||
result.append(_qq_expand(item, env))
|
||||
return result
|
||||
|
||||
|
||||
def _expand_macro(macro: Macro, raw_args: list[Any], env: dict) -> Any:
|
||||
"""Expand a macro: bind unevaluated args, evaluate body to get new AST."""
|
||||
local = dict(macro.closure)
|
||||
local.update(env)
|
||||
|
||||
# Bind positional params
|
||||
for i, param in enumerate(macro.params):
|
||||
if i < len(raw_args):
|
||||
local[param] = raw_args[i]
|
||||
else:
|
||||
local[param] = NIL
|
||||
|
||||
# Bind &rest param
|
||||
if macro.rest_param is not None:
|
||||
rest_start = len(macro.params)
|
||||
local[macro.rest_param] = list(raw_args[rest_start:])
|
||||
|
||||
return _eval(macro.body, local)
|
||||
|
||||
|
||||
def _sf_defhandler(expr: list, env: dict) -> HandlerDef:
|
||||
"""``(defhandler name (&key param...) body)``"""
|
||||
if len(expr) < 4:
|
||||
raise EvalError("defhandler requires name, params, and body")
|
||||
name_sym = expr[1]
|
||||
if not isinstance(name_sym, Symbol):
|
||||
raise EvalError(f"defhandler name must be symbol, got {type(name_sym).__name__}")
|
||||
|
||||
params_expr = expr[2]
|
||||
if not isinstance(params_expr, list):
|
||||
raise EvalError("defhandler params must be a list")
|
||||
|
||||
params: list[str] = []
|
||||
in_key = False
|
||||
for p in params_expr:
|
||||
if isinstance(p, Symbol):
|
||||
if p.name == "&key":
|
||||
in_key = True
|
||||
continue
|
||||
if in_key:
|
||||
params.append(p.name)
|
||||
elif isinstance(p, str):
|
||||
params.append(p)
|
||||
|
||||
handler = HandlerDef(
|
||||
name=name_sym.name,
|
||||
params=params,
|
||||
body=expr[3],
|
||||
closure=dict(env),
|
||||
)
|
||||
env[f"handler:{name_sym.name}"] = handler
|
||||
return handler
|
||||
|
||||
|
||||
def _sf_set_bang(expr: list, env: dict) -> Any:
|
||||
"""``(set! name value)`` — mutate existing binding."""
|
||||
if len(expr) != 3:
|
||||
@@ -518,6 +654,9 @@ _SPECIAL_FORMS: dict[str, Any] = {
|
||||
"quote": _sf_quote,
|
||||
"->": _sf_thread_first,
|
||||
"set!": _sf_set_bang,
|
||||
"defmacro": _sf_defmacro,
|
||||
"quasiquote": _sf_quasiquote,
|
||||
"defhandler": _sf_defhandler,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user