Data-first HO forms, fix plan pages, aser error handling (1080/1080)
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled

Evaluator: data-first higher-order forms — ho-swap-args auto-detects
(map coll fn) vs (map fn coll), both work. Threading + HO: (-> data
(map fn)) dispatches through CEK HO machinery via quoted-value splice.
17 new tests in test-cek-advanced.sx.

Fix plan pages: add mother-language, isolated-evaluator, rust-wasm-host
to page-functions.sx plan() — were in defpage but missing from URL router.

Aser error handling: pages.py now catches EvalError separately, renders
visible error banner instead of silently sending empty content. All
except blocks include traceback in logs.

Scope primitives: register collect!/collected/clear-collected!/emitted/
emit!/context in shared/sx/primitives.py so hand-written _aser can
resolve them (fixes ~cssx/flush expansion failure).

New test file: shared/sx/tests/test_aser_errors.py — 19 pytest tests
for error propagation through all aser control flow forms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 18:05:00 +00:00
parent bdbf594bc8
commit 3a268e7277
10 changed files with 615 additions and 51 deletions

View File

@@ -23,11 +23,28 @@ import logging
import os
from typing import Any
from .types import PageDef
import traceback
from .types import EvalError, PageDef
logger = logging.getLogger("sx.pages")
def _eval_error_sx(e: EvalError, context: str) -> str:
"""Render an EvalError as SX content that's visible to the developer."""
from .ref.sx_ref import escape_html as _esc
msg = _esc(str(e))
ctx = _esc(context)
return (
f'(div :class "sx-eval-error" :style '
f'"background:#fef2f2;border:1px solid #fca5a5;'
f'color:#991b1b;padding:1rem;margin:1rem 0;'
f'border-radius:0.5rem;font-family:monospace;white-space:pre-wrap"'
f' (p :style "font-weight:700;margin:0 0 0.5rem" "SX EvalError in {ctx}")'
f' (p :style "margin:0" "{msg}"))'
)
# ---------------------------------------------------------------------------
# Registry — service → page-name → PageDef
# ---------------------------------------------------------------------------
@@ -511,8 +528,12 @@ async def execute_page_streaming(
aside_sx = await _eval_slot(page_def.aside_expr, data_env, ctx) if page_def.aside_expr else ""
menu_sx = await _eval_slot(page_def.menu_expr, data_env, ctx) if page_def.menu_expr else ""
await _stream_queue.put(("data-single", content_sx, filter_sx, aside_sx, menu_sx))
except EvalError as e:
logger.error("Streaming data task failed (EvalError): %s\n%s", e, traceback.format_exc())
error_sx = _eval_error_sx(e, "page content")
await _stream_queue.put(("data-single", error_sx, "", "", ""))
except Exception as e:
logger.error("Streaming data task failed: %s", e)
logger.error("Streaming data task failed: %s\n%s", e, traceback.format_exc())
await _stream_queue.put(("data-done",))
async def _eval_headers():
@@ -524,7 +545,7 @@ async def execute_page_streaming(
menu = await layout.mobile_menu(tctx, **layout_kwargs)
await _stream_queue.put(("headers", rows, menu))
except Exception as e:
logger.error("Streaming headers task failed: %s", e)
logger.error("Streaming headers task failed: %s\n%s", e, traceback.format_exc())
await _stream_queue.put(("headers", "", ""))
data_task = asyncio.create_task(_eval_data_and_content())
@@ -629,7 +650,7 @@ async def execute_page_streaming(
elif kind == "data-done":
remaining -= 1
except Exception as e:
logger.error("Streaming resolve failed for %s: %s", kind, e)
logger.error("Streaming resolve failed for %s: %s\n%s", kind, e, traceback.format_exc())
yield "\n</body>\n</html>"
@@ -733,8 +754,13 @@ async def execute_page_streaming_oob(
await _stream_queue.put(("data-done",))
return
await _stream_queue.put(("data-done",))
except EvalError as e:
logger.error("Streaming OOB data task failed (EvalError): %s\n%s", e, traceback.format_exc())
error_sx = _eval_error_sx(e, "page content")
await _stream_queue.put(("data", "stream-content", error_sx))
await _stream_queue.put(("data-done",))
except Exception as e:
logger.error("Streaming OOB data task failed: %s", e)
logger.error("Streaming OOB data task failed: %s\n%s", e, traceback.format_exc())
await _stream_queue.put(("data-done",))
async def _eval_oob_headers():
@@ -745,7 +771,7 @@ async def execute_page_streaming_oob(
else:
await _stream_queue.put(("headers", ""))
except Exception as e:
logger.error("Streaming OOB headers task failed: %s", e)
logger.error("Streaming OOB headers task failed: %s\n%s", e, traceback.format_exc())
await _stream_queue.put(("headers", ""))
data_task = asyncio.create_task(_eval_data())
@@ -836,7 +862,7 @@ async def execute_page_streaming_oob(
elif kind == "data-done":
remaining -= 1
except Exception as e:
logger.error("Streaming OOB resolve failed for %s: %s", kind, e)
logger.error("Streaming OOB resolve failed for %s: %s\n%s", kind, e, traceback.format_exc())
return _stream_oob_chunks()