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:
@@ -104,3 +104,8 @@
|
||||
:params ()
|
||||
:returns "dict"
|
||||
:service "sx")
|
||||
|
||||
(define-page-helper "page-helpers-demo-data"
|
||||
:params ()
|
||||
:returns "dict"
|
||||
:service "sx")
|
||||
|
||||
@@ -227,7 +227,8 @@
|
||||
(dict :label "JavaScript" :href "/bootstrappers/javascript")
|
||||
(dict :label "Python" :href "/bootstrappers/python")
|
||||
(dict :label "Self-Hosting (py.sx)" :href "/bootstrappers/self-hosting")
|
||||
(dict :label "Self-Hosting JS (js.sx)" :href "/bootstrappers/self-hosting-js")))
|
||||
(dict :label "Self-Hosting JS (js.sx)" :href "/bootstrappers/self-hosting-js")
|
||||
(dict :label "Page Helpers" :href "/bootstrappers/page-helpers")))
|
||||
|
||||
;; Spec file registry — canonical metadata for spec viewer pages.
|
||||
;; Python only handles file I/O (read-spec-file); all metadata lives here.
|
||||
|
||||
265
sx/sx/page-helpers-demo.sx
Normal file
265
sx/sx/page-helpers-demo.sx
Normal file
@@ -0,0 +1,265 @@
|
||||
;; page-helpers-demo.sx — Demo: same SX spec functions on server and client
|
||||
;;
|
||||
;; Shows page-helpers.sx functions running on Python (server-side, via sx_ref.py)
|
||||
;; and JavaScript (client-side, via sx-browser.js) with identical results.
|
||||
;; Server renders with render-to-html. Client runs as a defisland — pure SX,
|
||||
;; no JavaScript file. The button click triggers spec functions via signals.
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Shared card component — used by both server and client results
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~demo-result-card (&key title ms desc theme &rest children)
|
||||
(let ((border (if (= theme "blue") "border-blue-200 bg-blue-50/30" "border-stone-200"))
|
||||
(title-c (if (= theme "blue") "text-blue-700" "text-stone-700"))
|
||||
(badge-c (if (= theme "blue") "text-blue-400" "text-stone-400"))
|
||||
(desc-c (if (= theme "blue") "text-blue-500" "text-stone-500"))
|
||||
(body-c (if (= theme "blue") "text-blue-600" "text-stone-600")))
|
||||
(div :class (str "rounded-lg border p-4 " border)
|
||||
(h4 :class (str "font-semibold text-sm mb-1 " title-c)
|
||||
title " "
|
||||
(span :class (str "text-xs " badge-c) (str ms "ms")))
|
||||
(p :class (str "text-xs mb-2 " desc-c) desc)
|
||||
(div :class (str "text-xs space-y-0.5 " body-c)
|
||||
children))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Client-side island — runs spec functions in the browser on button click
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defisland ~demo-client-runner (&key sf-source attr-detail req-attrs attr-keys)
|
||||
(let ((results (signal nil))
|
||||
(running (signal false))
|
||||
(run-demo (fn (e)
|
||||
(reset! running true)
|
||||
(let* ((t0 (now-ms))
|
||||
|
||||
;; 1. categorize-special-forms
|
||||
(t1 (now-ms))
|
||||
(sf-exprs (sx-parse sf-source))
|
||||
(sf-result (categorize-special-forms sf-exprs))
|
||||
(sf-ms (- (now-ms) t1))
|
||||
(sf-cats {})
|
||||
(sf-total 0)
|
||||
|
||||
;; 2. build-reference-data
|
||||
(t2 (now-ms))
|
||||
(ref-result (build-reference-data "attributes"
|
||||
{"req-attrs" req-attrs "beh-attrs" (list) "uniq-attrs" (list)}
|
||||
attr-keys))
|
||||
(ref-ms (- (now-ms) t2))
|
||||
(ref-sample (slice (or (get ref-result "req-attrs") (list)) 0 3))
|
||||
|
||||
;; 3. build-attr-detail
|
||||
(t3 (now-ms))
|
||||
(attr-result (build-attr-detail "sx-get" attr-detail))
|
||||
(attr-ms (- (now-ms) t3))
|
||||
|
||||
;; 4. build-component-source
|
||||
(t4 (now-ms))
|
||||
(comp-result (build-component-source
|
||||
{"type" "component" "name" "~demo-card"
|
||||
"params" (list "title" "subtitle")
|
||||
"has-children" true
|
||||
"body-sx" "(div :class \"card\"\n (h2 title)\n (when subtitle (p subtitle))\n children)"
|
||||
"affinity" "auto"}))
|
||||
(comp-ms (- (now-ms) t4))
|
||||
|
||||
;; 5. build-routing-analysis
|
||||
(t5 (now-ms))
|
||||
(routing-result (build-routing-analysis (list
|
||||
{"name" "home" "path" "/" "has-data" false "content-src" "(~home-content)"}
|
||||
{"name" "dashboard" "path" "/dash" "has-data" true "content-src" "(~dashboard)"}
|
||||
{"name" "about" "path" "/about" "has-data" false "content-src" "(~about-content)"}
|
||||
{"name" "settings" "path" "/settings" "has-data" true "content-src" "(~settings)"})))
|
||||
(routing-ms (- (now-ms) t5))
|
||||
|
||||
(total-ms (- (now-ms) t0)))
|
||||
|
||||
;; Post-process sf-result: count forms per category
|
||||
(for-each (fn (k)
|
||||
(let ((count (len (get sf-result k))))
|
||||
(set! sf-cats (assoc sf-cats k count))
|
||||
(set! sf-total (+ sf-total count))))
|
||||
(keys sf-result))
|
||||
|
||||
(reset! results
|
||||
{"sf-cats" sf-cats "sf-total" sf-total "sf-ms" sf-ms
|
||||
"ref-sample" ref-sample "ref-ms" ref-ms
|
||||
"attr-result" attr-result "attr-ms" attr-ms
|
||||
"comp-result" comp-result "comp-ms" comp-ms
|
||||
"routing-result" routing-result "routing-ms" routing-ms
|
||||
"total-ms" total-ms})))))
|
||||
|
||||
(<>
|
||||
(button
|
||||
:class (if (deref running)
|
||||
"px-4 py-2 rounded-md bg-blue-600 text-white font-medium text-sm cursor-default mb-4"
|
||||
"px-4 py-2 rounded-md bg-violet-600 text-white font-medium text-sm hover:bg-violet-700 transition-colors mb-4")
|
||||
:on-click run-demo
|
||||
(if (deref running)
|
||||
(str "Done (" (get (deref results) "total-ms") "ms total)")
|
||||
"Run in Browser"))
|
||||
|
||||
(when (deref results)
|
||||
(let ((r (deref results)))
|
||||
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
|
||||
(~demo-result-card
|
||||
:title "categorize-special-forms"
|
||||
:ms (get r "sf-ms") :theme "blue"
|
||||
:desc "Parses special-forms.sx and classifies each form by category (control flow, binding, quoting, etc)."
|
||||
(p :class "text-sm mb-1"
|
||||
(str (get r "sf-total") " forms in "
|
||||
(len (keys (get r "sf-cats"))) " categories"))
|
||||
(map (fn (k)
|
||||
(div (str k ": " (get (get r "sf-cats") k))))
|
||||
(keys (get r "sf-cats"))))
|
||||
|
||||
(~demo-result-card
|
||||
:title "build-reference-data"
|
||||
:ms (get r "ref-ms") :theme "blue"
|
||||
:desc "Takes raw attribute tuples and generates reference table rows with documentation hrefs."
|
||||
(p :class "text-sm mb-1"
|
||||
(str (len (get r "ref-sample")) " attributes with detail page links"))
|
||||
(map (fn (item)
|
||||
(div (str (get item "name") " → "
|
||||
(or (get item "href") "no detail page"))))
|
||||
(get r "ref-sample")))
|
||||
|
||||
(~demo-result-card
|
||||
:title "build-attr-detail"
|
||||
:ms (get r "attr-ms") :theme "blue"
|
||||
:desc "Builds a detail page data structure for a single attribute (sx-get): title, wire ID, handler status."
|
||||
(div (str "title: " (get (get r "attr-result") "attr-title")))
|
||||
(div (str "wire-id: " (or (get (get r "attr-result") "attr-wire-id") "none")))
|
||||
(div (str "has handler: " (if (get (get r "attr-result") "attr-handler") "yes" "no"))))
|
||||
|
||||
(~demo-result-card
|
||||
:title "build-component-source"
|
||||
:ms (get r "comp-ms") :theme "blue"
|
||||
:desc "Reconstructs a defcomp source definition from a component metadata dict (name, params, body)."
|
||||
(pre :class "bg-blue-50 p-2 rounded overflow-x-auto"
|
||||
(get r "comp-result")))
|
||||
|
||||
(div :class "rounded-lg border border-blue-200 bg-blue-50/30 p-4 md:col-span-2"
|
||||
(h4 :class "font-semibold text-blue-700 text-sm mb-1"
|
||||
"build-routing-analysis "
|
||||
(span :class "text-xs text-blue-400" (str (get r "routing-ms") "ms")))
|
||||
(p :class "text-xs text-blue-500 mb-2"
|
||||
"Classifies pages as client-routable or server-only based on whether they have data dependencies.")
|
||||
(div :class "text-xs text-blue-600"
|
||||
(p :class "text-sm mb-1"
|
||||
(str (get (get r "routing-result") "total-pages") " pages: "
|
||||
(get (get r "routing-result") "client-count") " client-routable, "
|
||||
(get (get r "routing-result") "server-count") " server-only"))
|
||||
(div :class "space-y-0.5"
|
||||
(map (fn (pg)
|
||||
(div (str (get pg "name") " → " (get pg "mode")
|
||||
(when (not (empty? (get pg "reason")))
|
||||
(str " (" (get pg "reason") ")")))))
|
||||
(get (get r "routing-result") "pages")))))))))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Main page component — server-rendered content + client island
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~page-helpers-demo-content (&key
|
||||
sf-categories sf-total sf-ms
|
||||
ref-sample ref-ms
|
||||
attr-result attr-ms
|
||||
comp-source comp-ms
|
||||
routing-result routing-ms
|
||||
server-total-ms
|
||||
sf-source
|
||||
attr-detail req-attrs attr-keys)
|
||||
|
||||
(div :class "max-w-3xl mx-auto px-4"
|
||||
(div :class "mb-8"
|
||||
(h2 :class "text-2xl font-bold text-stone-800 mb-2" "Bootstrapped Page Helpers")
|
||||
(p :class "text-stone-600 mb-4"
|
||||
"These functions are defined once in "
|
||||
(code :class "text-violet-700" "page-helpers.sx")
|
||||
" and bootstrapped to both Python ("
|
||||
(code :class "text-violet-700" "sx_ref.py")
|
||||
") and JavaScript ("
|
||||
(code :class "text-violet-700" "sx-browser.js")
|
||||
"). The server ran them in Python during this page load. Click the button below to run the identical functions client-side in the browser — same spec, same inputs, same results."))
|
||||
|
||||
;; Server results
|
||||
(div :class "mb-8"
|
||||
(h3 :class "text-lg font-semibold text-stone-700 mb-3"
|
||||
"Server Results "
|
||||
(span :class "text-sm font-normal text-stone-500"
|
||||
(str "(Python, " server-total-ms "ms total)")))
|
||||
|
||||
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
|
||||
(~demo-result-card
|
||||
:title "categorize-special-forms"
|
||||
:ms sf-ms :theme "stone"
|
||||
:desc "Parses special-forms.sx and classifies each form by category (control flow, binding, quoting, etc)."
|
||||
(p :class "text-sm mb-1"
|
||||
(str sf-total " forms in "
|
||||
(len (keys sf-categories)) " categories"))
|
||||
(map (fn (k)
|
||||
(div (str k ": " (get sf-categories k))))
|
||||
(keys sf-categories)))
|
||||
|
||||
(~demo-result-card
|
||||
:title "build-reference-data"
|
||||
:ms ref-ms :theme "stone"
|
||||
:desc "Takes raw attribute tuples and generates reference table rows with documentation hrefs."
|
||||
(p :class "text-sm mb-1"
|
||||
(str (len ref-sample) " attributes with detail page links"))
|
||||
(map (fn (item)
|
||||
(div (str (get item "name") " → "
|
||||
(or (get item "href") "no detail page"))))
|
||||
ref-sample))
|
||||
|
||||
(~demo-result-card
|
||||
:title "build-attr-detail"
|
||||
:ms attr-ms :theme "stone"
|
||||
:desc "Builds a detail page data structure for a single attribute (sx-get): title, wire ID, handler status."
|
||||
(div (str "title: " (get attr-result "attr-title")))
|
||||
(div (str "wire-id: " (or (get attr-result "attr-wire-id") "none")))
|
||||
(div (str "has handler: " (if (get attr-result "attr-handler") "yes" "no"))))
|
||||
|
||||
(~demo-result-card
|
||||
:title "build-component-source"
|
||||
:ms comp-ms :theme "stone"
|
||||
:desc "Reconstructs a defcomp source definition from a component metadata dict (name, params, body)."
|
||||
(pre :class "bg-stone-50 p-2 rounded overflow-x-auto"
|
||||
comp-source))
|
||||
|
||||
(div :class "rounded-lg border border-stone-200 p-4 md:col-span-2"
|
||||
(h4 :class "font-semibold text-stone-700 text-sm mb-1"
|
||||
"build-routing-analysis "
|
||||
(span :class "text-xs text-stone-400" (str routing-ms "ms")))
|
||||
(p :class "text-xs text-stone-500 mb-2"
|
||||
"Classifies pages as client-routable or server-only based on whether they have data dependencies.")
|
||||
(div :class "text-xs text-stone-600"
|
||||
(p :class "text-sm mb-1"
|
||||
(str (get routing-result "total-pages") " pages: "
|
||||
(get routing-result "client-count") " client-routable, "
|
||||
(get routing-result "server-count") " server-only"))
|
||||
(div :class "space-y-0.5"
|
||||
(map (fn (pg)
|
||||
(div (str (get pg "name") " → " (get pg "mode")
|
||||
(when (not (empty? (get pg "reason")))
|
||||
(str " (" (get pg "reason") ")")))))
|
||||
(get routing-result "pages")))))))
|
||||
|
||||
;; Client execution area — pure SX island, no JavaScript file
|
||||
(div :class "mb-8"
|
||||
(h3 :class "text-lg font-semibold text-stone-700 mb-3"
|
||||
"Client Results "
|
||||
(span :class "text-sm font-normal text-stone-500" "(JavaScript, sx-browser.js)"))
|
||||
|
||||
(~demo-client-runner
|
||||
:sf-source sf-source
|
||||
:attr-detail attr-detail
|
||||
:req-attrs req-attrs
|
||||
:attr-keys attr-keys))))
|
||||
@@ -553,6 +553,28 @@
|
||||
"phase2" (~reactive-islands-phase2-content)
|
||||
:else (~reactive-islands-index-content))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Bootstrapped page helpers demo
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defpage page-helpers-demo
|
||||
:path "/bootstrappers/page-helpers"
|
||||
:auth :public
|
||||
:layout :sx-docs
|
||||
:data (page-helpers-demo-data)
|
||||
:content (~sx-doc :path "/bootstrappers/page-helpers"
|
||||
(~page-helpers-demo-content
|
||||
:sf-categories sf-categories :sf-total sf-total :sf-ms sf-ms
|
||||
:ref-sample ref-sample :ref-ms ref-ms
|
||||
:attr-result attr-result :attr-ms attr-ms
|
||||
:comp-source comp-source :comp-ms comp-ms
|
||||
:routing-result routing-result :routing-ms routing-ms
|
||||
:server-total-ms server-total-ms
|
||||
:sf-source sf-source
|
||||
:attr-detail attr-detail
|
||||
:req-attrs req-attrs
|
||||
:attr-keys attr-keys)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Testing section
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
@@ -33,6 +33,7 @@ def _register_sx_helpers() -> None:
|
||||
"action:add-demo-item": _add_demo_item,
|
||||
"offline-demo-data": _offline_demo_data,
|
||||
"prove-data": _prove_data,
|
||||
"page-helpers-demo-data": _page_helpers_demo_data,
|
||||
})
|
||||
|
||||
|
||||
@@ -41,26 +42,29 @@ def _component_source(name: str) -> str:
|
||||
from shared.sx.jinja_bridge import get_component_env
|
||||
from shared.sx.parser import serialize
|
||||
from shared.sx.types import Component, Island
|
||||
from shared.sx.ref.sx_ref import build_component_source
|
||||
|
||||
comp = get_component_env().get(name)
|
||||
if isinstance(comp, Island):
|
||||
param_strs = (["&key"] + list(comp.params)) if comp.params else []
|
||||
if comp.has_children:
|
||||
param_strs.extend(["&rest", "children"])
|
||||
params_sx = "(" + " ".join(param_strs) + ")"
|
||||
body_sx = serialize(comp.body, pretty=True)
|
||||
return f"(defisland {name} {params_sx}\n {body_sx})"
|
||||
return build_component_source({
|
||||
"type": "island", "name": name,
|
||||
"params": list(comp.params) if comp.params else [],
|
||||
"has-children": comp.has_children,
|
||||
"body-sx": serialize(comp.body, pretty=True),
|
||||
"affinity": None,
|
||||
})
|
||||
if not isinstance(comp, Component):
|
||||
return f";; component {name} not found"
|
||||
param_strs = ["&key"] + list(comp.params)
|
||||
if comp.has_children:
|
||||
param_strs.extend(["&rest", "children"])
|
||||
params_sx = "(" + " ".join(param_strs) + ")"
|
||||
body_sx = serialize(comp.body, pretty=True)
|
||||
affinity = ""
|
||||
if comp.render_target == "server":
|
||||
affinity = " :affinity :server"
|
||||
return f"(defcomp {name} {params_sx}{affinity}\n {body_sx})"
|
||||
return build_component_source({
|
||||
"type": "not-found", "name": name,
|
||||
"params": [], "has-children": False, "body-sx": "", "affinity": None,
|
||||
})
|
||||
return build_component_source({
|
||||
"type": "component", "name": name,
|
||||
"params": list(comp.params),
|
||||
"has-children": comp.has_children,
|
||||
"body-sx": serialize(comp.body, pretty=True),
|
||||
"affinity": comp.affinity,
|
||||
})
|
||||
|
||||
|
||||
def _primitives_data() -> dict:
|
||||
@@ -70,168 +74,57 @@ def _primitives_data() -> dict:
|
||||
|
||||
|
||||
def _special_forms_data() -> dict:
|
||||
"""Parse special-forms.sx and return categorized form data.
|
||||
|
||||
Returns a dict of category → list of form dicts, each with:
|
||||
name, syntax, doc, tail_position, example
|
||||
"""
|
||||
"""Parse special-forms.sx and return categorized form data."""
|
||||
import os
|
||||
from shared.sx.parser import parse_all, serialize
|
||||
from shared.sx.types import Symbol, Keyword
|
||||
from shared.sx.parser import parse_all
|
||||
from shared.sx.ref.sx_ref import categorize_special_forms
|
||||
|
||||
ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref")
|
||||
if not os.path.isdir(ref_dir):
|
||||
ref_dir = "/app/shared/sx/ref"
|
||||
ref_dir = _ref_dir()
|
||||
spec_path = os.path.join(ref_dir, "special-forms.sx")
|
||||
with open(spec_path) as f:
|
||||
exprs = parse_all(f.read())
|
||||
|
||||
# Categories inferred from comment sections in the file.
|
||||
# We assign forms to categories based on their order in the spec.
|
||||
categories: dict[str, list[dict]] = {}
|
||||
current_category = "Other"
|
||||
|
||||
# Map form names to categories
|
||||
category_map = {
|
||||
"if": "Control Flow", "when": "Control Flow", "cond": "Control Flow",
|
||||
"case": "Control Flow", "and": "Control Flow", "or": "Control Flow",
|
||||
"let": "Binding", "let*": "Binding", "letrec": "Binding",
|
||||
"define": "Binding", "set!": "Binding",
|
||||
"lambda": "Functions & Components", "fn": "Functions & Components",
|
||||
"defcomp": "Functions & Components", "defmacro": "Functions & Components",
|
||||
"begin": "Sequencing & Threading", "do": "Sequencing & Threading",
|
||||
"->": "Sequencing & Threading",
|
||||
"quote": "Quoting", "quasiquote": "Quoting",
|
||||
"reset": "Continuations", "shift": "Continuations",
|
||||
"dynamic-wind": "Guards",
|
||||
"map": "Higher-Order Forms", "map-indexed": "Higher-Order Forms",
|
||||
"filter": "Higher-Order Forms", "reduce": "Higher-Order Forms",
|
||||
"some": "Higher-Order Forms", "every?": "Higher-Order Forms",
|
||||
"for-each": "Higher-Order Forms",
|
||||
"defstyle": "Domain Definitions",
|
||||
"defhandler": "Domain Definitions", "defpage": "Domain Definitions",
|
||||
"defquery": "Domain Definitions", "defaction": "Domain Definitions",
|
||||
}
|
||||
|
||||
for expr in exprs:
|
||||
if not isinstance(expr, list) or len(expr) < 2:
|
||||
continue
|
||||
head = expr[0]
|
||||
if not isinstance(head, Symbol) or head.name != "define-special-form":
|
||||
continue
|
||||
|
||||
name = expr[1]
|
||||
# Extract keyword args
|
||||
kwargs: dict[str, str] = {}
|
||||
i = 2
|
||||
while i < len(expr) - 1:
|
||||
if isinstance(expr[i], Keyword):
|
||||
key = expr[i].name
|
||||
val = expr[i + 1]
|
||||
if isinstance(val, list):
|
||||
# For :syntax, avoid quote sugar (quasiquote → `x)
|
||||
items = [serialize(item) for item in val]
|
||||
kwargs[key] = "(" + " ".join(items) + ")"
|
||||
else:
|
||||
kwargs[key] = str(val)
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
|
||||
category = category_map.get(name, "Other")
|
||||
if category not in categories:
|
||||
categories[category] = []
|
||||
categories[category].append({
|
||||
"name": name,
|
||||
"syntax": kwargs.get("syntax", ""),
|
||||
"doc": kwargs.get("doc", ""),
|
||||
"tail-position": kwargs.get("tail-position", ""),
|
||||
"example": kwargs.get("example", ""),
|
||||
})
|
||||
|
||||
return categories
|
||||
return categorize_special_forms(exprs)
|
||||
|
||||
|
||||
def _reference_data(slug: str) -> dict:
|
||||
"""Return reference table data for a given slug.
|
||||
|
||||
Returns a dict whose keys become SX env bindings:
|
||||
- attributes: req-attrs, beh-attrs, uniq-attrs
|
||||
- headers: req-headers, resp-headers
|
||||
- events: events-list
|
||||
- js-api: js-api-list
|
||||
"""
|
||||
"""Return reference table data for a given slug."""
|
||||
from content.pages import (
|
||||
REQUEST_ATTRS, BEHAVIOR_ATTRS, SX_UNIQUE_ATTRS,
|
||||
REQUEST_HEADERS, RESPONSE_HEADERS,
|
||||
EVENTS, JS_API, ATTR_DETAILS, HEADER_DETAILS,
|
||||
)
|
||||
from shared.sx.ref.sx_ref import build_reference_data
|
||||
|
||||
# Build raw data dict and detail keys based on slug
|
||||
if slug == "attributes":
|
||||
return {
|
||||
"req-attrs": [
|
||||
{"name": a, "desc": d, "exists": e,
|
||||
"href": f"/hypermedia/reference/attributes/{a}" if e and a in ATTR_DETAILS else None}
|
||||
for a, d, e in REQUEST_ATTRS
|
||||
],
|
||||
"beh-attrs": [
|
||||
{"name": a, "desc": d, "exists": e,
|
||||
"href": f"/hypermedia/reference/attributes/{a}" if e and a in ATTR_DETAILS else None}
|
||||
for a, d, e in BEHAVIOR_ATTRS
|
||||
],
|
||||
"uniq-attrs": [
|
||||
{"name": a, "desc": d, "exists": e,
|
||||
"href": f"/hypermedia/reference/attributes/{a}" if e and a in ATTR_DETAILS else None}
|
||||
for a, d, e in SX_UNIQUE_ATTRS
|
||||
],
|
||||
raw = {
|
||||
"req-attrs": [list(t) for t in REQUEST_ATTRS],
|
||||
"beh-attrs": [list(t) for t in BEHAVIOR_ATTRS],
|
||||
"uniq-attrs": [list(t) for t in SX_UNIQUE_ATTRS],
|
||||
}
|
||||
detail_keys = list(ATTR_DETAILS.keys())
|
||||
elif slug == "headers":
|
||||
return {
|
||||
"req-headers": [
|
||||
{"name": n, "value": v, "desc": d,
|
||||
"href": f"/hypermedia/reference/headers/{n}" if n in HEADER_DETAILS else None}
|
||||
for n, v, d in REQUEST_HEADERS
|
||||
],
|
||||
"resp-headers": [
|
||||
{"name": n, "value": v, "desc": d,
|
||||
"href": f"/hypermedia/reference/headers/{n}" if n in HEADER_DETAILS else None}
|
||||
for n, v, d in RESPONSE_HEADERS
|
||||
],
|
||||
raw = {
|
||||
"req-headers": [list(t) for t in REQUEST_HEADERS],
|
||||
"resp-headers": [list(t) for t in RESPONSE_HEADERS],
|
||||
}
|
||||
detail_keys = list(HEADER_DETAILS.keys())
|
||||
elif slug == "events":
|
||||
from content.pages import EVENT_DETAILS
|
||||
return {
|
||||
"events-list": [
|
||||
{"name": n, "desc": d,
|
||||
"href": f"/hypermedia/reference/events/{n}" if n in EVENT_DETAILS else None}
|
||||
for n, d in EVENTS
|
||||
],
|
||||
}
|
||||
raw = {"events-list": [list(t) for t in EVENTS]}
|
||||
detail_keys = list(EVENT_DETAILS.keys())
|
||||
elif slug == "js-api":
|
||||
return {
|
||||
"js-api-list": [
|
||||
{"name": n, "desc": d}
|
||||
for n, d in JS_API
|
||||
],
|
||||
raw = {"js-api-list": [list(t) for t in JS_API]}
|
||||
detail_keys = []
|
||||
else:
|
||||
raw = {
|
||||
"req-attrs": [list(t) for t in REQUEST_ATTRS],
|
||||
"beh-attrs": [list(t) for t in BEHAVIOR_ATTRS],
|
||||
"uniq-attrs": [list(t) for t in SX_UNIQUE_ATTRS],
|
||||
}
|
||||
# Default — return attrs data for fallback
|
||||
return {
|
||||
"req-attrs": [
|
||||
{"name": a, "desc": d, "exists": e,
|
||||
"href": f"/hypermedia/reference/attributes/{a}" if e and a in ATTR_DETAILS else None}
|
||||
for a, d, e in REQUEST_ATTRS
|
||||
],
|
||||
"beh-attrs": [
|
||||
{"name": a, "desc": d, "exists": e,
|
||||
"href": f"/hypermedia/reference/attributes/{a}" if e and a in ATTR_DETAILS else None}
|
||||
for a, d, e in BEHAVIOR_ATTRS
|
||||
],
|
||||
"uniq-attrs": [
|
||||
{"name": a, "desc": d, "exists": e,
|
||||
"href": f"/hypermedia/reference/attributes/{a}" if e and a in ATTR_DETAILS else None}
|
||||
for a, d, e in SX_UNIQUE_ATTRS
|
||||
],
|
||||
}
|
||||
detail_keys = list(ATTR_DETAILS.keys())
|
||||
|
||||
return build_reference_data(slug, raw, detail_keys)
|
||||
|
||||
|
||||
def _read_spec_file(filename: str) -> str:
|
||||
@@ -425,6 +318,7 @@ def _js_self_hosting_data(ref_dir: str) -> dict:
|
||||
return {
|
||||
"bootstrapper-not-found": None,
|
||||
"js-sx-source": js_sx_source,
|
||||
"defines-matched": str(total),
|
||||
"defines-total": str(total),
|
||||
"js-sx-lines": str(len(js_sx_source.splitlines())),
|
||||
"verification-status": status,
|
||||
@@ -438,6 +332,7 @@ def _bundle_analyzer_data() -> dict:
|
||||
from shared.sx.deps import components_needed, scan_components_from_sx
|
||||
from shared.sx.parser import serialize
|
||||
from shared.sx.types import Component, Macro
|
||||
from shared.sx.ref.sx_ref import build_bundle_analysis
|
||||
|
||||
env = get_component_env()
|
||||
total_components = sum(1 for v in env.values() if isinstance(v, Component))
|
||||
@@ -445,68 +340,47 @@ def _bundle_analyzer_data() -> dict:
|
||||
pure_count = sum(1 for v in env.values() if isinstance(v, Component) and v.is_pure)
|
||||
io_count = total_components - pure_count
|
||||
|
||||
pages_data = []
|
||||
# Extract raw data at I/O edge — Python accesses Component objects, serializes bodies
|
||||
pages_raw = []
|
||||
components_raw: dict[str, dict] = {}
|
||||
for name, page_def in sorted(get_all_pages("sx").items()):
|
||||
content_sx = serialize(page_def.content_expr)
|
||||
direct = scan_components_from_sx(content_sx)
|
||||
needed = components_needed(content_sx, env)
|
||||
n = len(needed)
|
||||
pct = round(n / total_components * 100) if total_components else 0
|
||||
savings = 100 - pct
|
||||
needed = sorted(components_needed(content_sx, env))
|
||||
|
||||
# IO classification + component details for this page
|
||||
pure_in_page = 0
|
||||
io_in_page = 0
|
||||
page_io_refs: set[str] = set()
|
||||
comp_details = []
|
||||
for comp_name in sorted(needed):
|
||||
val = env.get(comp_name)
|
||||
if isinstance(val, Component):
|
||||
is_pure = val.is_pure
|
||||
if is_pure:
|
||||
pure_in_page += 1
|
||||
else:
|
||||
io_in_page += 1
|
||||
page_io_refs.update(val.io_refs)
|
||||
# Reconstruct defcomp source
|
||||
param_strs = ["&key"] + list(val.params)
|
||||
if val.has_children:
|
||||
param_strs.extend(["&rest", "children"])
|
||||
params_sx = "(" + " ".join(param_strs) + ")"
|
||||
body_sx = serialize(val.body, pretty=True)
|
||||
source = f"(defcomp ~{val.name} {params_sx}\n {body_sx})"
|
||||
comp_details.append({
|
||||
"name": comp_name,
|
||||
"is-pure": is_pure,
|
||||
"affinity": val.affinity,
|
||||
"render-target": val.render_target,
|
||||
"io-refs": sorted(val.io_refs),
|
||||
"deps": sorted(val.deps),
|
||||
"source": source,
|
||||
})
|
||||
for comp_name in needed:
|
||||
if comp_name not in components_raw:
|
||||
val = env.get(comp_name)
|
||||
if isinstance(val, Component):
|
||||
param_strs = ["&key"] + list(val.params)
|
||||
if val.has_children:
|
||||
param_strs.extend(["&rest", "children"])
|
||||
params_sx = "(" + " ".join(param_strs) + ")"
|
||||
body_sx = serialize(val.body, pretty=True)
|
||||
components_raw[comp_name] = {
|
||||
"is-pure": val.is_pure,
|
||||
"affinity": val.affinity,
|
||||
"render-target": val.render_target,
|
||||
"io-refs": sorted(val.io_refs),
|
||||
"deps": sorted(val.deps),
|
||||
"source": f"(defcomp ~{val.name} {params_sx}\n {body_sx})",
|
||||
}
|
||||
|
||||
pages_data.append({
|
||||
pages_raw.append({
|
||||
"name": name,
|
||||
"path": page_def.path,
|
||||
"direct": len(direct),
|
||||
"needed": n,
|
||||
"pct": pct,
|
||||
"savings": savings,
|
||||
"io-refs": len(page_io_refs),
|
||||
"pure-in-page": pure_in_page,
|
||||
"io-in-page": io_in_page,
|
||||
"components": comp_details,
|
||||
"needed-names": needed,
|
||||
})
|
||||
|
||||
pages_data.sort(key=lambda p: p["needed"], reverse=True)
|
||||
|
||||
return {
|
||||
"pages": pages_data,
|
||||
"total-components": total_components,
|
||||
"total-macros": total_macros,
|
||||
"pure-count": pure_count,
|
||||
"io-count": io_count,
|
||||
}
|
||||
# Pure data transformation in SX spec
|
||||
result = build_bundle_analysis(
|
||||
pages_raw, components_raw,
|
||||
total_components, total_macros, pure_count, io_count,
|
||||
)
|
||||
# Sort pages by needed count (descending) — SX has no sort primitive
|
||||
result["pages"] = sorted(result["pages"], key=lambda p: p["needed"], reverse=True)
|
||||
return result
|
||||
|
||||
|
||||
def _routing_analyzer_data() -> dict:
|
||||
@@ -514,12 +388,11 @@ def _routing_analyzer_data() -> dict:
|
||||
from shared.sx.pages import get_all_pages
|
||||
from shared.sx.parser import serialize as sx_serialize
|
||||
from shared.sx.helpers import _sx_literal
|
||||
from shared.sx.ref.sx_ref import build_routing_analysis
|
||||
|
||||
pages_data = []
|
||||
full_content: list[tuple[str, str, bool]] = [] # (name, full_content, has_data)
|
||||
client_count = 0
|
||||
server_count = 0
|
||||
|
||||
# I/O edge: extract page data from page registry
|
||||
pages_raw = []
|
||||
full_content: list[tuple[str, str, bool]] = []
|
||||
for name, page_def in sorted(get_all_pages("sx").items()):
|
||||
has_data = page_def.data_expr is not None
|
||||
content_src = ""
|
||||
@@ -528,37 +401,21 @@ def _routing_analyzer_data() -> dict:
|
||||
content_src = sx_serialize(page_def.content_expr)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
pages_raw.append({
|
||||
"name": name, "path": page_def.path,
|
||||
"has-data": has_data, "content-src": content_src,
|
||||
})
|
||||
full_content.append((name, content_src, has_data))
|
||||
|
||||
# Determine routing mode and reason
|
||||
if has_data:
|
||||
mode = "server"
|
||||
reason = "Has :data expression — needs server IO"
|
||||
server_count += 1
|
||||
elif not content_src:
|
||||
mode = "server"
|
||||
reason = "No content expression"
|
||||
server_count += 1
|
||||
else:
|
||||
mode = "client"
|
||||
reason = ""
|
||||
client_count += 1
|
||||
# Pure classification in SX spec
|
||||
result = build_routing_analysis(pages_raw)
|
||||
# Sort: client pages first, then server (SX has no sort primitive)
|
||||
result["pages"] = sorted(
|
||||
result["pages"],
|
||||
key=lambda p: (0 if p["mode"] == "client" else 1, p["name"]),
|
||||
)
|
||||
|
||||
pages_data.append({
|
||||
"name": name,
|
||||
"path": page_def.path,
|
||||
"mode": mode,
|
||||
"has-data": has_data,
|
||||
"content-expr": content_src[:80] + ("..." if len(content_src) > 80 else ""),
|
||||
"reason": reason,
|
||||
})
|
||||
|
||||
# Sort: client pages first, then server
|
||||
pages_data.sort(key=lambda p: (0 if p["mode"] == "client" else 1, p["name"]))
|
||||
|
||||
# Build a sample of the SX page registry format (use full content, first 3)
|
||||
total = client_count + server_count
|
||||
# Build registry sample (uses _sx_literal which is Python string escaping)
|
||||
sample_entries = []
|
||||
sorted_full = sorted(full_content, key=lambda x: x[0])
|
||||
for name, csrc, hd in sorted_full[:3]:
|
||||
@@ -574,86 +431,50 @@ def _routing_analyzer_data() -> dict:
|
||||
+ "\n :closure {}}"
|
||||
)
|
||||
sample_entries.append(entry)
|
||||
registry_sample = "\n\n".join(sample_entries)
|
||||
result["registry-sample"] = "\n\n".join(sample_entries)
|
||||
|
||||
return {
|
||||
"pages": pages_data,
|
||||
"total-pages": total,
|
||||
"client-count": client_count,
|
||||
"server-count": server_count,
|
||||
"registry-sample": registry_sample,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def _attr_detail_data(slug: str) -> dict:
|
||||
"""Return attribute detail data for a specific attribute slug.
|
||||
|
||||
Returns a dict whose keys become SX env bindings:
|
||||
- attr-title, attr-description, attr-example, attr-handler
|
||||
- attr-demo (component call or None)
|
||||
- attr-wire-id (wire placeholder id or None)
|
||||
- attr-not-found (truthy if not found)
|
||||
"""
|
||||
"""Return attribute detail data for a specific attribute slug."""
|
||||
from content.pages import ATTR_DETAILS
|
||||
from shared.sx.helpers import sx_call
|
||||
from shared.sx.ref.sx_ref import build_attr_detail
|
||||
|
||||
detail = ATTR_DETAILS.get(slug)
|
||||
if not detail:
|
||||
return {"attr-not-found": True}
|
||||
|
||||
demo_name = detail.get("demo")
|
||||
wire_id = None
|
||||
if "handler" in detail:
|
||||
wire_id = f"ref-wire-{slug.replace(':', '-').replace('*', 'star')}"
|
||||
|
||||
return {
|
||||
"attr-not-found": None,
|
||||
"attr-title": slug,
|
||||
"attr-description": detail["description"],
|
||||
"attr-example": detail["example"],
|
||||
"attr-handler": detail.get("handler"),
|
||||
"attr-demo": sx_call(demo_name) if demo_name else None,
|
||||
"attr-wire-id": wire_id,
|
||||
}
|
||||
result = build_attr_detail(slug, detail)
|
||||
# Convert demo name to sx_call if present
|
||||
demo_name = result.get("attr-demo")
|
||||
if demo_name:
|
||||
result["attr-demo"] = sx_call(demo_name)
|
||||
return result
|
||||
|
||||
|
||||
def _header_detail_data(slug: str) -> dict:
|
||||
"""Return header detail data for a specific header slug."""
|
||||
from content.pages import HEADER_DETAILS
|
||||
from shared.sx.helpers import sx_call
|
||||
from shared.sx.ref.sx_ref import build_header_detail
|
||||
|
||||
detail = HEADER_DETAILS.get(slug)
|
||||
if not detail:
|
||||
return {"header-not-found": True}
|
||||
|
||||
demo_name = detail.get("demo")
|
||||
return {
|
||||
"header-not-found": None,
|
||||
"header-title": slug,
|
||||
"header-direction": detail["direction"],
|
||||
"header-description": detail["description"],
|
||||
"header-example": detail.get("example"),
|
||||
"header-demo": sx_call(demo_name) if demo_name else None,
|
||||
}
|
||||
result = build_header_detail(slug, HEADER_DETAILS.get(slug))
|
||||
demo_name = result.get("header-demo")
|
||||
if demo_name:
|
||||
result["header-demo"] = sx_call(demo_name)
|
||||
return result
|
||||
|
||||
|
||||
def _event_detail_data(slug: str) -> dict:
|
||||
"""Return event detail data for a specific event slug."""
|
||||
from content.pages import EVENT_DETAILS
|
||||
from shared.sx.helpers import sx_call
|
||||
from shared.sx.ref.sx_ref import build_event_detail
|
||||
|
||||
detail = EVENT_DETAILS.get(slug)
|
||||
if not detail:
|
||||
return {"event-not-found": True}
|
||||
|
||||
demo_name = detail.get("demo")
|
||||
return {
|
||||
"event-not-found": None,
|
||||
"event-title": slug,
|
||||
"event-description": detail["description"],
|
||||
"event-example": detail.get("example"),
|
||||
"event-demo": sx_call(demo_name) if demo_name else None,
|
||||
}
|
||||
result = build_event_detail(slug, EVENT_DETAILS.get(slug))
|
||||
demo_name = result.get("event-demo")
|
||||
if demo_name:
|
||||
result["event-demo"] = sx_call(demo_name)
|
||||
return result
|
||||
|
||||
|
||||
def _run_spec_tests() -> dict:
|
||||
@@ -1089,35 +910,30 @@ def _affinity_demo_data() -> dict:
|
||||
from shared.sx.jinja_bridge import get_component_env
|
||||
from shared.sx.types import Component
|
||||
from shared.sx.pages import get_all_pages
|
||||
from shared.sx.ref.sx_ref import build_affinity_analysis
|
||||
|
||||
# I/O edge: extract component data and page render plans
|
||||
env = get_component_env()
|
||||
demo_names = [
|
||||
"~aff-demo-auto",
|
||||
"~aff-demo-client",
|
||||
"~aff-demo-server",
|
||||
"~aff-demo-io-auto",
|
||||
"~aff-demo-io-client",
|
||||
"~aff-demo-auto", "~aff-demo-client", "~aff-demo-server",
|
||||
"~aff-demo-io-auto", "~aff-demo-io-client",
|
||||
]
|
||||
components = []
|
||||
for name in demo_names:
|
||||
val = env.get(name)
|
||||
if isinstance(val, Component):
|
||||
components.append({
|
||||
"name": name,
|
||||
"affinity": val.affinity,
|
||||
"name": name, "affinity": val.affinity,
|
||||
"render-target": val.render_target,
|
||||
"io-refs": sorted(val.io_refs),
|
||||
"is-pure": val.is_pure,
|
||||
"io-refs": sorted(val.io_refs), "is-pure": val.is_pure,
|
||||
})
|
||||
|
||||
# Collect render plans from all sx service pages
|
||||
page_plans = []
|
||||
for page_def in get_all_pages("sx").values():
|
||||
plan = page_def.render_plan
|
||||
if plan:
|
||||
page_plans.append({
|
||||
"name": page_def.name,
|
||||
"path": page_def.path,
|
||||
"name": page_def.name, "path": page_def.path,
|
||||
"server-count": len(plan.get("server", [])),
|
||||
"client-count": len(plan.get("client", [])),
|
||||
"server": plan.get("server", []),
|
||||
@@ -1125,7 +941,7 @@ def _affinity_demo_data() -> dict:
|
||||
"io-deps": plan.get("io-deps", []),
|
||||
})
|
||||
|
||||
return {"components": components, "page-plans": page_plans}
|
||||
return build_affinity_analysis(components, page_plans)
|
||||
|
||||
|
||||
def _optimistic_demo_data() -> dict:
|
||||
@@ -1271,3 +1087,84 @@ def _offline_demo_data() -> dict:
|
||||
],
|
||||
"server-time": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
||||
}
|
||||
|
||||
|
||||
def _page_helpers_demo_data() -> dict:
|
||||
"""Run page-helpers.sx functions server-side, return results for comparison with client."""
|
||||
import os
|
||||
import time
|
||||
from shared.sx.parser import parse_all
|
||||
from shared.sx.ref.sx_ref import (
|
||||
categorize_special_forms, build_reference_data,
|
||||
build_attr_detail, build_component_source,
|
||||
build_routing_analysis,
|
||||
)
|
||||
|
||||
ref_dir = _ref_dir()
|
||||
results = {}
|
||||
|
||||
# 1. categorize-special-forms
|
||||
t0 = time.monotonic()
|
||||
with open(os.path.join(ref_dir, "special-forms.sx")) as f:
|
||||
sf_exprs = parse_all(f.read())
|
||||
sf_result = categorize_special_forms(sf_exprs)
|
||||
sf_ms = round((time.monotonic() - t0) * 1000, 1)
|
||||
sf_summary = {cat: len(forms) for cat, forms in sf_result.items()}
|
||||
results["sf-categories"] = sf_summary
|
||||
results["sf-total"] = sum(sf_summary.values())
|
||||
results["sf-ms"] = sf_ms
|
||||
|
||||
# 2. build-reference-data
|
||||
from content.pages import REQUEST_ATTRS, ATTR_DETAILS
|
||||
t1 = time.monotonic()
|
||||
ref_result = build_reference_data("attributes", {
|
||||
"req-attrs": [list(t) for t in REQUEST_ATTRS[:5]],
|
||||
"beh-attrs": [], "uniq-attrs": [],
|
||||
}, list(ATTR_DETAILS.keys()))
|
||||
ref_ms = round((time.monotonic() - t1) * 1000, 1)
|
||||
results["ref-sample"] = ref_result.get("req-attrs", [])[:3]
|
||||
results["ref-ms"] = ref_ms
|
||||
|
||||
# 3. build-attr-detail
|
||||
t2 = time.monotonic()
|
||||
detail = ATTR_DETAILS.get("sx-get")
|
||||
attr_result = build_attr_detail("sx-get", detail)
|
||||
attr_ms = round((time.monotonic() - t2) * 1000, 1)
|
||||
results["attr-result"] = attr_result
|
||||
results["attr-ms"] = attr_ms
|
||||
|
||||
# 4. build-component-source
|
||||
t3 = time.monotonic()
|
||||
comp_result = build_component_source({
|
||||
"type": "component", "name": "~demo-card",
|
||||
"params": ["title", "subtitle"],
|
||||
"has-children": True,
|
||||
"body-sx": "(div :class \"card\"\n (h2 title)\n (when subtitle (p subtitle))\n children)",
|
||||
"affinity": "auto",
|
||||
})
|
||||
comp_ms = round((time.monotonic() - t3) * 1000, 1)
|
||||
results["comp-source"] = comp_result
|
||||
results["comp-ms"] = comp_ms
|
||||
|
||||
# 5. build-routing-analysis
|
||||
t4 = time.monotonic()
|
||||
routing_result = build_routing_analysis([
|
||||
{"name": "home", "path": "/", "has-data": False, "content-src": "(~home-content)"},
|
||||
{"name": "dashboard", "path": "/dash", "has-data": True, "content-src": "(~dashboard)"},
|
||||
{"name": "about", "path": "/about", "has-data": False, "content-src": "(~about-content)"},
|
||||
{"name": "settings", "path": "/settings", "has-data": True, "content-src": "(~settings)"},
|
||||
])
|
||||
routing_ms = round((time.monotonic() - t4) * 1000, 1)
|
||||
results["routing-result"] = routing_result
|
||||
results["routing-ms"] = routing_ms
|
||||
|
||||
# Total
|
||||
results["server-total-ms"] = round(sf_ms + ref_ms + attr_ms + comp_ms + routing_ms, 1)
|
||||
|
||||
# Pass raw inputs for client-side island (serialized as data-sx-state)
|
||||
results["sf-source"] = open(os.path.join(ref_dir, "special-forms.sx")).read()
|
||||
results["attr-detail"] = detail
|
||||
results["req-attrs"] = [list(t) for t in REQUEST_ATTRS[:5]]
|
||||
results["attr-keys"] = list(ATTR_DETAILS.keys())
|
||||
|
||||
return results
|
||||
|
||||
Reference in New Issue
Block a user