Merge branch 'worktree-react' into macros
# Conflicts: # shared/sx/tests/run.py
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from .types import Component, Continuation, HandlerDef, Keyword, Lambda, Macro, NIL, PageDef, RelationDef, Symbol, _ShiftSignal
|
||||
from .types import Component, Continuation, HandlerDef, Island, Keyword, Lambda, Macro, NIL, PageDef, RelationDef, Symbol, _ShiftSignal
|
||||
from .primitives import _PRIMITIVES
|
||||
|
||||
|
||||
@@ -147,13 +147,13 @@ def _eval(expr: Any, env: dict[str, Any]) -> Any:
|
||||
fn = _trampoline(_eval(head, env))
|
||||
args = [_trampoline(_eval(a, env)) for a in expr[1:]]
|
||||
|
||||
if callable(fn) and not isinstance(fn, (Lambda, Component)):
|
||||
if callable(fn) and not isinstance(fn, (Lambda, Component, Island)):
|
||||
return fn(*args)
|
||||
|
||||
if isinstance(fn, Lambda):
|
||||
return _call_lambda(fn, args, env)
|
||||
|
||||
if isinstance(fn, Component):
|
||||
if isinstance(fn, (Component, Island)):
|
||||
return _call_component(fn, expr[1:], env)
|
||||
|
||||
raise EvalError(f"Not callable: {fn!r}")
|
||||
@@ -555,6 +555,51 @@ def _sf_defcomp(expr: list, env: dict) -> Component:
|
||||
return comp
|
||||
|
||||
|
||||
def _sf_defisland(expr: list, env: dict) -> Island:
|
||||
"""``(defisland ~name (&key ...) body)``"""
|
||||
if len(expr) < 4:
|
||||
raise EvalError("defisland requires name, params, and body")
|
||||
name_sym = expr[1]
|
||||
if not isinstance(name_sym, Symbol):
|
||||
raise EvalError(f"defisland name must be symbol, got {type(name_sym).__name__}")
|
||||
comp_name = name_sym.name.lstrip("~")
|
||||
|
||||
params_expr = expr[2]
|
||||
if not isinstance(params_expr, list):
|
||||
raise EvalError("defisland params must be a list")
|
||||
|
||||
params: list[str] = []
|
||||
has_children = False
|
||||
in_key = False
|
||||
for p in params_expr:
|
||||
if isinstance(p, Symbol):
|
||||
if p.name == "&key":
|
||||
in_key = True
|
||||
continue
|
||||
if p.name == "&rest":
|
||||
has_children = True
|
||||
continue
|
||||
if in_key or has_children:
|
||||
if not has_children:
|
||||
params.append(p.name)
|
||||
else:
|
||||
params.append(p.name)
|
||||
elif isinstance(p, str):
|
||||
params.append(p)
|
||||
|
||||
body = expr[-1]
|
||||
|
||||
island = Island(
|
||||
name=comp_name,
|
||||
params=params,
|
||||
has_children=has_children,
|
||||
body=body,
|
||||
closure=dict(env),
|
||||
)
|
||||
env[name_sym.name] = island
|
||||
return island
|
||||
|
||||
|
||||
def _defcomp_kwarg(expr: list, key: str, default: str) -> str:
|
||||
"""Extract a keyword annotation from defcomp, e.g. :affinity :client."""
|
||||
# Scan from index 3 to second-to-last for :key value pairs
|
||||
@@ -592,7 +637,7 @@ def _sf_thread_first(expr: list, env: dict) -> Any:
|
||||
else:
|
||||
fn = _trampoline(_eval(form, env))
|
||||
args = [result]
|
||||
if callable(fn) and not isinstance(fn, (Lambda, Component)):
|
||||
if callable(fn) and not isinstance(fn, (Lambda, Component, Island)):
|
||||
result = fn(*args)
|
||||
elif isinstance(fn, Lambda):
|
||||
result = _trampoline(_call_lambda(fn, args, env))
|
||||
@@ -1021,6 +1066,7 @@ _SPECIAL_FORMS: dict[str, Any] = {
|
||||
"define": _sf_define,
|
||||
"defstyle": _sf_defstyle,
|
||||
"defcomp": _sf_defcomp,
|
||||
"defisland": _sf_defisland,
|
||||
"defrelation": _sf_defrelation,
|
||||
"begin": _sf_begin,
|
||||
"do": _sf_begin,
|
||||
|
||||
@@ -102,6 +102,12 @@
|
||||
(contains? HTML_TAGS name)
|
||||
(render-dom-element name args env ns)
|
||||
|
||||
;; Island (~name) — reactive component
|
||||
(and (starts-with? name "~")
|
||||
(env-has? env name)
|
||||
(island? (env-get env name)))
|
||||
(render-dom-island (env-get env name) args env ns)
|
||||
|
||||
;; Component (~name)
|
||||
(starts-with? name "~")
|
||||
(let ((comp (env-get env name)))
|
||||
@@ -284,7 +290,7 @@
|
||||
|
||||
(define RENDER_DOM_FORMS
|
||||
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
|
||||
"define" "defcomp" "defmacro" "defstyle" "defhandler"
|
||||
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
|
||||
"map" "map-indexed" "filter" "for-each"))
|
||||
|
||||
(define render-dom-form?
|
||||
@@ -414,6 +420,153 @@
|
||||
(render-to-dom (lambda-body f) local ns))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-dom-island — render a reactive island
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Islands render like components but wrapped in a reactive context.
|
||||
;; The island container element gets data-sx-island and data-sx-state
|
||||
;; attributes for identification and hydration.
|
||||
;;
|
||||
;; Inside the island body, deref calls create reactive DOM subscriptions:
|
||||
;; - Text bindings: (deref sig) in text position → reactive text node
|
||||
;; - Attribute bindings: (deref sig) in attr → reactive attribute
|
||||
;; - Conditional fragments: (when (deref sig) ...) → reactive show/hide
|
||||
|
||||
(define render-dom-island
|
||||
(fn (island args env ns)
|
||||
;; Parse kwargs and children (same as component)
|
||||
(let ((kwargs (dict))
|
||||
(children (list)))
|
||||
(reduce
|
||||
(fn (state arg)
|
||||
(let ((skip (get state "skip")))
|
||||
(if skip
|
||||
(assoc state "skip" false "i" (inc (get state "i")))
|
||||
(if (and (= (type-of arg) "keyword")
|
||||
(< (inc (get state "i")) (len args)))
|
||||
(let ((val (trampoline
|
||||
(eval-expr (nth args (inc (get state "i"))) env))))
|
||||
(dict-set! kwargs (keyword-name arg) val)
|
||||
(assoc state "skip" true "i" (inc (get state "i"))))
|
||||
(do
|
||||
(append! children arg)
|
||||
(assoc state "i" (inc (get state "i"))))))))
|
||||
(dict "i" 0 "skip" false)
|
||||
args)
|
||||
|
||||
;; Build island env: closure + caller env + params
|
||||
(let ((local (env-merge (component-closure island) env))
|
||||
(island-name (component-name island)))
|
||||
|
||||
;; Bind params from kwargs
|
||||
(for-each
|
||||
(fn (p)
|
||||
(env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(component-params island))
|
||||
|
||||
;; If island accepts children, pre-render them to a fragment
|
||||
(when (component-has-children? island)
|
||||
(let ((child-frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (c) (dom-append child-frag (render-to-dom c env ns)))
|
||||
children)
|
||||
(env-set! local "children" child-frag)))
|
||||
|
||||
;; Create the island container element
|
||||
(let ((container (dom-create-element "div" nil))
|
||||
(disposers (list)))
|
||||
|
||||
;; Mark as island
|
||||
(dom-set-attr container "data-sx-island" island-name)
|
||||
|
||||
;; Render island body inside a scope that tracks disposers
|
||||
(let ((body-dom
|
||||
(with-island-scope
|
||||
(fn (disposable) (append! disposers disposable))
|
||||
(fn () (render-to-dom (component-body island) local ns)))))
|
||||
(dom-append container body-dom)
|
||||
|
||||
;; Store disposers on the container for cleanup
|
||||
(dom-set-data container "sx-disposers" disposers)
|
||||
|
||||
container))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Reactive DOM rendering helpers
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; These functions create reactive bindings between signals and DOM nodes.
|
||||
;; They are called by the platform's renderDOM when it detects deref
|
||||
;; calls inside an island context.
|
||||
|
||||
;; reactive-text — create a text node bound to a signal
|
||||
;; Used when (deref sig) appears in a text position inside an island.
|
||||
(define reactive-text
|
||||
(fn (sig)
|
||||
(let ((node (create-text-node (str (deref sig)))))
|
||||
(effect (fn ()
|
||||
(dom-set-text-content node (str (deref sig)))))
|
||||
node)))
|
||||
|
||||
;; reactive-attr — bind an element attribute to a signal expression
|
||||
;; Used when an attribute value contains (deref sig) inside an island.
|
||||
(define reactive-attr
|
||||
(fn (el attr-name compute-fn)
|
||||
(effect (fn ()
|
||||
(let ((val (compute-fn)))
|
||||
(cond
|
||||
(or (nil? val) (= val false))
|
||||
(dom-remove-attr el attr-name)
|
||||
(= val true)
|
||||
(dom-set-attr el attr-name "")
|
||||
:else
|
||||
(dom-set-attr el attr-name (str val))))))))
|
||||
|
||||
;; reactive-fragment — conditionally render a fragment based on a signal
|
||||
;; Used for (when (deref sig) ...) or (if (deref sig) ...) inside an island.
|
||||
(define reactive-fragment
|
||||
(fn (test-fn render-fn env ns)
|
||||
(let ((marker (create-comment "island-fragment"))
|
||||
(current-nodes (list)))
|
||||
(effect (fn ()
|
||||
;; Remove previous nodes
|
||||
(for-each (fn (n) (dom-remove n)) current-nodes)
|
||||
(set! current-nodes (list))
|
||||
;; If test passes, render and insert after marker
|
||||
(when (test-fn)
|
||||
(let ((frag (render-fn)))
|
||||
(set! current-nodes (dom-child-nodes frag))
|
||||
(dom-insert-after marker frag)))))
|
||||
marker)))
|
||||
|
||||
;; reactive-list — render a keyed list bound to a signal
|
||||
;; Used for (map fn (deref items)) inside an island.
|
||||
(define reactive-list
|
||||
(fn (map-fn items-sig env ns)
|
||||
(let ((container (create-fragment))
|
||||
(marker (create-comment "island-list")))
|
||||
(dom-append container marker)
|
||||
(effect (fn ()
|
||||
;; Simple strategy: clear and re-render
|
||||
;; Future: keyed reconciliation
|
||||
(let ((parent (dom-parent marker)))
|
||||
(when parent
|
||||
;; Remove all nodes after marker until next sibling marker
|
||||
(dom-remove-children-after marker)
|
||||
;; Render new items
|
||||
(let ((items (deref items-sig)))
|
||||
(for-each
|
||||
(fn (item)
|
||||
(let ((rendered (if (lambda? map-fn)
|
||||
(render-lambda-dom map-fn (list item) env ns)
|
||||
(render-to-dom (apply map-fn (list item)) env ns))))
|
||||
(dom-insert-after marker rendered)))
|
||||
(reverse items)))))))
|
||||
container)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface — DOM adapter
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -422,11 +575,20 @@
|
||||
;; (dom-create-element tag ns) → Element (ns=nil for HTML, string for SVG/MathML)
|
||||
;; (create-text-node s) → Text node
|
||||
;; (create-fragment) → DocumentFragment
|
||||
;; (create-comment s) → Comment node
|
||||
;;
|
||||
;; Tree mutation:
|
||||
;; (dom-append parent child) → void (appendChild)
|
||||
;; (dom-set-attr el name val) → void (setAttribute)
|
||||
;; (dom-remove-attr el name) → void (removeAttribute)
|
||||
;; (dom-get-attr el name) → string or nil (getAttribute)
|
||||
;; (dom-set-text-content n s) → void (set textContent)
|
||||
;; (dom-remove node) → void (remove from parent)
|
||||
;; (dom-insert-after ref node) → void (insert node after ref)
|
||||
;; (dom-parent node) → parent Element or nil
|
||||
;; (dom-child-nodes frag) → list of child nodes
|
||||
;; (dom-remove-children-after m)→ void (remove all siblings after marker)
|
||||
;; (dom-set-data el key val) → void (store arbitrary data on element)
|
||||
;;
|
||||
;; Content parsing:
|
||||
;; (dom-parse-html s) → DocumentFragment from HTML string
|
||||
@@ -441,11 +603,15 @@
|
||||
;; From eval.sx:
|
||||
;; eval-expr, trampoline, expand-macro, process-bindings, eval-cond
|
||||
;; env-has?, env-get, env-set!, env-merge
|
||||
;; lambda?, component?, macro?
|
||||
;; lambda?, component?, island?, macro?
|
||||
;; lambda-closure, lambda-params, lambda-body
|
||||
;; component-params, component-body, component-closure,
|
||||
;; component-has-children?, component-name
|
||||
;;
|
||||
;; From signals.sx:
|
||||
;; signal, deref, reset!, swap!, computed, effect, batch
|
||||
;; signal?, with-island-scope
|
||||
;;
|
||||
;; Iteration:
|
||||
;; (for-each-indexed fn coll) → call fn(index, item) for each element
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
;; parse-element-args, render-attrs, definition-form?
|
||||
;; eval.sx — eval-expr, trampoline, expand-macro, process-bindings,
|
||||
;; eval-cond, env-has?, env-get, env-set!, env-merge,
|
||||
;; lambda?, component?, macro?,
|
||||
;; lambda?, component?, island?, macro?,
|
||||
;; lambda-closure, lambda-params, lambda-body
|
||||
;; ==========================================================================
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
(define RENDER_HTML_FORMS
|
||||
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
|
||||
"define" "defcomp" "defmacro" "defstyle" "defhandler"
|
||||
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
|
||||
"map" "map-indexed" "filter" "for-each"))
|
||||
|
||||
(define render-html-form?
|
||||
@@ -85,6 +85,12 @@
|
||||
(contains? HTML_TAGS name)
|
||||
(render-html-element name args env)
|
||||
|
||||
;; Island (~name) — reactive component, SSR with hydration markers
|
||||
(and (starts-with? name "~")
|
||||
(env-has? env name)
|
||||
(island? (env-get env name)))
|
||||
(render-html-island (env-get env name) args env)
|
||||
|
||||
;; Component or macro call (~name)
|
||||
(starts-with? name "~")
|
||||
(let ((val (env-get env name)))
|
||||
@@ -287,6 +293,85 @@
|
||||
"</" tag ">"))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-html-island — SSR rendering of a reactive island
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Renders the island body as static HTML wrapped in a container element
|
||||
;; with data-sx-island and data-sx-state attributes. The client hydrates
|
||||
;; this by finding these elements and re-rendering with reactive context.
|
||||
;;
|
||||
;; On the server, signal/deref/reset!/swap! are simple passthrough:
|
||||
;; (signal val) → returns val (no container needed server-side)
|
||||
;; (deref s) → returns s (signal values are plain values server-side)
|
||||
;; (reset! s v) → no-op
|
||||
;; (swap! s f) → no-op
|
||||
|
||||
(define render-html-island
|
||||
(fn (island args env)
|
||||
;; Parse kwargs and children (same pattern as render-html-component)
|
||||
(let ((kwargs (dict))
|
||||
(children (list)))
|
||||
(reduce
|
||||
(fn (state arg)
|
||||
(let ((skip (get state "skip")))
|
||||
(if skip
|
||||
(assoc state "skip" false "i" (inc (get state "i")))
|
||||
(if (and (= (type-of arg) "keyword")
|
||||
(< (inc (get state "i")) (len args)))
|
||||
(let ((val (trampoline
|
||||
(eval-expr (nth args (inc (get state "i"))) env))))
|
||||
(dict-set! kwargs (keyword-name arg) val)
|
||||
(assoc state "skip" true "i" (inc (get state "i"))))
|
||||
(do
|
||||
(append! children arg)
|
||||
(assoc state "i" (inc (get state "i"))))))))
|
||||
(dict "i" 0 "skip" false)
|
||||
args)
|
||||
|
||||
;; Build island env: closure + caller env + params
|
||||
(let ((local (env-merge (component-closure island) env))
|
||||
(island-name (component-name island)))
|
||||
|
||||
;; Bind params from kwargs
|
||||
(for-each
|
||||
(fn (p)
|
||||
(env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(component-params island))
|
||||
|
||||
;; If island accepts children, pre-render them to raw HTML
|
||||
(when (component-has-children? island)
|
||||
(env-set! local "children"
|
||||
(make-raw-html
|
||||
(join "" (map (fn (c) (render-to-html c env)) children)))))
|
||||
|
||||
;; Render the island body as HTML
|
||||
(let ((body-html (render-to-html (component-body island) local))
|
||||
(state-json (serialize-island-state kwargs)))
|
||||
;; Wrap in container with hydration attributes
|
||||
(str "<div data-sx-island=\"" (escape-attr island-name) "\""
|
||||
(if state-json
|
||||
(str " data-sx-state=\"" (escape-attr state-json) "\"")
|
||||
"")
|
||||
">"
|
||||
body-html
|
||||
"</div>"))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; serialize-island-state — serialize kwargs to JSON for hydration
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Only serializes simple values (numbers, strings, booleans, nil, lists, dicts).
|
||||
;; Functions, components, and other non-serializable values are skipped.
|
||||
|
||||
(define serialize-island-state
|
||||
(fn (kwargs)
|
||||
(if (empty-dict? kwargs)
|
||||
nil
|
||||
(json-serialize kwargs))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface — HTML adapter
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -297,7 +382,7 @@
|
||||
;; From eval.sx:
|
||||
;; eval-expr, trampoline, expand-macro, process-bindings, eval-cond
|
||||
;; env-has?, env-get, env-set!, env-merge
|
||||
;; lambda?, component?, macro?
|
||||
;; lambda?, component?, island?, macro?
|
||||
;; lambda-closure, lambda-params, lambda-body
|
||||
;; component-params, component-body, component-closure,
|
||||
;; component-has-children?, component-name
|
||||
@@ -305,6 +390,11 @@
|
||||
;; Raw HTML construction:
|
||||
;; (make-raw-html s) → wrap string as raw HTML (not double-escaped)
|
||||
;;
|
||||
;; JSON serialization (for island state):
|
||||
;; (json-serialize dict) → JSON string
|
||||
;; (empty-dict? d) → boolean
|
||||
;; (escape-attr s) → HTML attribute escape
|
||||
;;
|
||||
;; Iteration:
|
||||
;; (for-each-indexed fn coll) → call fn(index, item) for each element
|
||||
;; (map-indexed fn coll) → map fn(index, item) over each element
|
||||
|
||||
@@ -83,12 +83,14 @@
|
||||
(let ((f (trampoline (eval-expr head env)))
|
||||
(evaled-args (map (fn (a) (trampoline (eval-expr a env))) args)))
|
||||
(cond
|
||||
(and (callable? f) (not (lambda? f)) (not (component? f)))
|
||||
(and (callable? f) (not (lambda? f)) (not (component? f)) (not (island? f)))
|
||||
(apply f evaled-args)
|
||||
(lambda? f)
|
||||
(trampoline (call-lambda f evaled-args env))
|
||||
(component? f)
|
||||
(aser-call (str "~" (component-name f)) args env)
|
||||
(island? f)
|
||||
(aser-call (str "~" (component-name f)) args env)
|
||||
:else (error (str "Not callable: " (inspect f)))))))))))
|
||||
|
||||
|
||||
|
||||
@@ -139,6 +139,32 @@ class JSEmitter:
|
||||
"callable?": "isCallable",
|
||||
"lambda?": "isLambda",
|
||||
"component?": "isComponent",
|
||||
"island?": "isIsland",
|
||||
"make-island": "makeIsland",
|
||||
"make-signal": "makeSignal",
|
||||
"signal?": "isSignal",
|
||||
"signal-value": "signalValue",
|
||||
"signal-set-value!": "signalSetValue",
|
||||
"signal-subscribers": "signalSubscribers",
|
||||
"signal-add-sub!": "signalAddSub",
|
||||
"signal-remove-sub!": "signalRemoveSub",
|
||||
"signal-deps": "signalDeps",
|
||||
"signal-set-deps!": "signalSetDeps",
|
||||
"set-tracking-context!": "setTrackingContext",
|
||||
"get-tracking-context": "getTrackingContext",
|
||||
"make-tracking-context": "makeTrackingContext",
|
||||
"tracking-context-deps": "trackingContextDeps",
|
||||
"tracking-context-add-dep!": "trackingContextAddDep",
|
||||
"tracking-context-notify-fn": "trackingContextNotifyFn",
|
||||
"identical?": "isIdentical",
|
||||
"notify-subscribers": "notifySubscribers",
|
||||
"flush-subscribers": "flushSubscribers",
|
||||
"dispose-computed": "disposeComputed",
|
||||
"with-island-scope": "withIslandScope",
|
||||
"register-in-scope": "registerInScope",
|
||||
"*batch-depth*": "_batchDepth",
|
||||
"*batch-queue*": "_batchQueue",
|
||||
"*island-scope*": "_islandScope",
|
||||
"macro?": "isMacro",
|
||||
"primitive?": "isPrimitive",
|
||||
"get-primitive": "getPrimitive",
|
||||
@@ -166,6 +192,10 @@ class JSEmitter:
|
||||
"render-list-to-html": "renderListToHtml",
|
||||
"render-html-element": "renderHtmlElement",
|
||||
"render-html-component": "renderHtmlComponent",
|
||||
"render-html-island": "renderHtmlIsland",
|
||||
"serialize-island-state": "serializeIslandState",
|
||||
"json-serialize": "jsonSerialize",
|
||||
"empty-dict?": "isEmptyDict",
|
||||
"parse-element-args": "parseElementArgs",
|
||||
"render-attrs": "renderAttrs",
|
||||
"aser-list": "aserList",
|
||||
@@ -191,6 +221,7 @@ class JSEmitter:
|
||||
"sf-lambda": "sfLambda",
|
||||
"sf-define": "sfDefine",
|
||||
"sf-defcomp": "sfDefcomp",
|
||||
"sf-defisland": "sfDefisland",
|
||||
"defcomp-kwarg": "defcompKwarg",
|
||||
"sf-defmacro": "sfDefmacro",
|
||||
"sf-begin": "sfBegin",
|
||||
@@ -240,6 +271,11 @@ class JSEmitter:
|
||||
"render-dom-form?": "isRenderDomForm",
|
||||
"dispatch-render-form": "dispatchRenderForm",
|
||||
"render-lambda-dom": "renderLambdaDom",
|
||||
"render-dom-island": "renderDomIsland",
|
||||
"reactive-text": "reactiveText",
|
||||
"reactive-attr": "reactiveAttr",
|
||||
"reactive-fragment": "reactiveFragment",
|
||||
"reactive-list": "reactiveList",
|
||||
"dom-create-element": "domCreateElement",
|
||||
"dom-append": "domAppend",
|
||||
"dom-set-attr": "domSetAttr",
|
||||
@@ -281,6 +317,11 @@ class JSEmitter:
|
||||
"dom-query": "domQuery",
|
||||
"dom-query-all": "domQueryAll",
|
||||
"dom-tag-name": "domTagName",
|
||||
"create-comment": "createComment",
|
||||
"dom-remove": "domRemove",
|
||||
"dom-child-nodes": "domChildNodes",
|
||||
"dom-remove-children-after": "domRemoveChildrenAfter",
|
||||
"dom-set-data": "domSetData",
|
||||
"dict-has?": "dictHas",
|
||||
"dict-delete!": "dictDelete",
|
||||
"process-bindings": "processBindings",
|
||||
@@ -605,17 +646,39 @@ class JSEmitter:
|
||||
params = expr[1]
|
||||
body = expr[2:]
|
||||
param_names = []
|
||||
for p in params:
|
||||
rest_name = None
|
||||
i = 0
|
||||
while i < len(params):
|
||||
p = params[i]
|
||||
if isinstance(p, Symbol) and p.name == "&rest":
|
||||
# Next param is the rest parameter
|
||||
if i + 1 < len(params):
|
||||
rest_name = self._mangle(params[i + 1].name if isinstance(params[i + 1], Symbol) else str(params[i + 1]))
|
||||
i += 2
|
||||
continue
|
||||
else:
|
||||
i += 1
|
||||
continue
|
||||
if isinstance(p, Symbol):
|
||||
param_names.append(self._mangle(p.name))
|
||||
else:
|
||||
param_names.append(str(p))
|
||||
i += 1
|
||||
params_str = ", ".join(param_names)
|
||||
# Build rest-param preamble if needed
|
||||
rest_preamble = ""
|
||||
if rest_name:
|
||||
n = len(param_names)
|
||||
rest_preamble = f"var {rest_name} = Array.prototype.slice.call(arguments, {n}); "
|
||||
if len(body) == 1:
|
||||
body_js = self.emit(body[0])
|
||||
if rest_preamble:
|
||||
return f"function({params_str}) {{ {rest_preamble}return {body_js}; }}"
|
||||
return f"function({params_str}) {{ return {body_js}; }}"
|
||||
# Multi-expression body: statements then return last
|
||||
parts = []
|
||||
if rest_preamble:
|
||||
parts.append(rest_preamble.rstrip())
|
||||
for b in body[:-1]:
|
||||
parts.append(self.emit_statement(b))
|
||||
parts.append(f"return {self.emit(body[-1])};")
|
||||
@@ -1045,6 +1108,7 @@ ADAPTER_DEPS = {
|
||||
SPEC_MODULES = {
|
||||
"deps": ("deps.sx", "deps (component dependency analysis)"),
|
||||
"router": ("router.sx", "router (client-side route matching)"),
|
||||
"signals": ("signals.sx", "signals (reactive signal runtime)"),
|
||||
}
|
||||
|
||||
|
||||
@@ -1833,6 +1897,9 @@ def compile_ref_to_js(
|
||||
if sm not in SPEC_MODULES:
|
||||
raise ValueError(f"Unknown spec module: {sm!r}. Valid: {', '.join(SPEC_MODULES)}")
|
||||
spec_mod_set.add(sm)
|
||||
# dom adapter uses signal runtime for reactive islands
|
||||
if "dom" in adapter_set and "signals" in SPEC_MODULES:
|
||||
spec_mod_set.add("signals")
|
||||
# boot.sx uses parse-route-pattern from router.sx
|
||||
if "boot" in adapter_set:
|
||||
spec_mod_set.add("router")
|
||||
@@ -1877,6 +1944,7 @@ def compile_ref_to_js(
|
||||
has_orch = "orchestration" in adapter_set
|
||||
has_boot = "boot" in adapter_set
|
||||
has_parser = "parser" in adapter_set
|
||||
has_signals = "signals" in spec_mod_set
|
||||
adapter_label = "+".join(sorted(adapter_set)) if adapter_set else "core-only"
|
||||
|
||||
# Determine which primitive modules to include
|
||||
@@ -1925,7 +1993,7 @@ def compile_ref_to_js(
|
||||
parts.append(CONTINUATIONS_JS)
|
||||
if has_dom:
|
||||
parts.append(ASYNC_IO_JS)
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router))
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router, has_signals))
|
||||
parts.append(EPILOGUE)
|
||||
from datetime import datetime, timezone
|
||||
build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
@@ -1984,6 +2052,29 @@ PREAMBLE = '''\
|
||||
}
|
||||
Component.prototype._component = true;
|
||||
|
||||
function Island(name, params, hasChildren, body, closure) {
|
||||
this.name = name;
|
||||
this.params = params;
|
||||
this.hasChildren = hasChildren;
|
||||
this.body = body;
|
||||
this.closure = closure || {};
|
||||
}
|
||||
Island.prototype._island = true;
|
||||
|
||||
function SxSignal(value) {
|
||||
this.value = value;
|
||||
this.subscribers = [];
|
||||
this.deps = [];
|
||||
}
|
||||
SxSignal.prototype._signal = true;
|
||||
|
||||
function TrackingCtx(notifyFn) {
|
||||
this.notifyFn = notifyFn;
|
||||
this.deps = [];
|
||||
}
|
||||
|
||||
var _trackingContext = null;
|
||||
|
||||
function Macro(params, restParam, body, closure, name) {
|
||||
this.params = params;
|
||||
this.restParam = restParam;
|
||||
@@ -2240,6 +2331,8 @@ PLATFORM_JS_PRE = '''
|
||||
if (x._thunk) return "thunk";
|
||||
if (x._lambda) return "lambda";
|
||||
if (x._component) return "component";
|
||||
if (x._island) return "island";
|
||||
if (x._signal) return "signal";
|
||||
if (x._macro) return "macro";
|
||||
if (x._raw) return "raw-html";
|
||||
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
|
||||
@@ -2287,7 +2380,41 @@ PLATFORM_JS_PRE = '''
|
||||
function isCallable(x) { return typeof x === "function" || (x != null && x._lambda === true); }
|
||||
function isLambda(x) { return x != null && x._lambda === true; }
|
||||
function isComponent(x) { return x != null && x._component === true; }
|
||||
function isIsland(x) { return x != null && x._island === true; }
|
||||
function isMacro(x) { return x != null && x._macro === true; }
|
||||
function isIdentical(a, b) { return a === b; }
|
||||
|
||||
// Island platform
|
||||
function makeIsland(name, params, hasChildren, body, env) {
|
||||
return new Island(name, params, hasChildren, body, merge(env));
|
||||
}
|
||||
|
||||
// Signal platform
|
||||
function makeSignal(value) { return new SxSignal(value); }
|
||||
function isSignal(x) { return x != null && x._signal === true; }
|
||||
function signalValue(s) { return s.value; }
|
||||
function signalSetValue(s, v) { s.value = v; }
|
||||
function signalSubscribers(s) { return s.subscribers.slice(); }
|
||||
function signalAddSub(s, fn) { if (s.subscribers.indexOf(fn) < 0) s.subscribers.push(fn); }
|
||||
function signalRemoveSub(s, fn) { var i = s.subscribers.indexOf(fn); if (i >= 0) s.subscribers.splice(i, 1); }
|
||||
function signalDeps(s) { return s.deps.slice(); }
|
||||
function signalSetDeps(s, deps) { s.deps = Array.isArray(deps) ? deps.slice() : []; }
|
||||
function setTrackingContext(ctx) { _trackingContext = ctx; }
|
||||
function getTrackingContext() { return _trackingContext || NIL; }
|
||||
function makeTrackingContext(notifyFn) { return new TrackingCtx(notifyFn); }
|
||||
function trackingContextDeps(ctx) { return ctx ? ctx.deps : []; }
|
||||
function trackingContextAddDep(ctx, s) { if (ctx && ctx.deps.indexOf(s) < 0) ctx.deps.push(s); }
|
||||
function trackingContextNotifyFn(ctx) { return ctx ? ctx.notifyFn : NIL; }
|
||||
|
||||
// JSON / dict helpers for island state serialization
|
||||
function jsonSerialize(obj) {
|
||||
try { return JSON.stringify(obj); } catch(e) { return "{}"; }
|
||||
}
|
||||
function isEmptyDict(d) {
|
||||
if (!d || typeof d !== "object") return true;
|
||||
for (var k in d) if (d.hasOwnProperty(k)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function envHas(env, name) { return name in env; }
|
||||
function envGet(env, name) { return env[name]; }
|
||||
@@ -2616,6 +2743,10 @@ PLATFORM_DOM_JS = """
|
||||
return _hasDom ? document.createTextNode(s) : null;
|
||||
}
|
||||
|
||||
function createComment(s) {
|
||||
return _hasDom ? document.createComment(s || "") : null;
|
||||
}
|
||||
|
||||
function createFragment() {
|
||||
return _hasDom ? document.createDocumentFragment() : null;
|
||||
}
|
||||
@@ -2750,6 +2881,23 @@ PLATFORM_DOM_JS = """
|
||||
|
||||
function domTagName(el) { return el && el.tagName ? el.tagName : ""; }
|
||||
|
||||
// Island DOM helpers
|
||||
function domRemove(node) {
|
||||
if (node && node.parentNode) node.parentNode.removeChild(node);
|
||||
}
|
||||
function domChildNodes(el) {
|
||||
if (!el || !el.childNodes) return [];
|
||||
return Array.prototype.slice.call(el.childNodes);
|
||||
}
|
||||
function domRemoveChildrenAfter(marker) {
|
||||
if (!marker || !marker.parentNode) return;
|
||||
var parent = marker.parentNode;
|
||||
while (marker.nextSibling) parent.removeChild(marker.nextSibling);
|
||||
}
|
||||
function domSetData(el, key, val) {
|
||||
if (el) { if (!el._sxData) el._sxData = {}; el._sxData[key] = val; }
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Performance overrides — replace transpiled spec with imperative JS
|
||||
// =========================================================================
|
||||
@@ -3868,7 +4016,7 @@ def fixups_js(has_html, has_sx, has_dom):
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps=False, has_router=False):
|
||||
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps=False, has_router=False, has_signals=False):
|
||||
# Parser: use compiled sxParse from parser.sx, or inline a minimal fallback
|
||||
if has_parser:
|
||||
parser = '''
|
||||
@@ -4020,6 +4168,16 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has
|
||||
api_lines.append(' registerIoDeps: typeof registerIoDeps === "function" ? registerIoDeps : null,')
|
||||
api_lines.append(' asyncRender: typeof asyncSxRenderWithEnv === "function" ? asyncSxRenderWithEnv : null,')
|
||||
api_lines.append(' asyncRenderToDom: typeof asyncRenderToDom === "function" ? asyncRenderToDom : null,')
|
||||
if has_signals:
|
||||
api_lines.append(' signal: signal,')
|
||||
api_lines.append(' deref: deref,')
|
||||
api_lines.append(' reset: reset_b,')
|
||||
api_lines.append(' swap: swap_b,')
|
||||
api_lines.append(' computed: computed,')
|
||||
api_lines.append(' effect: effect,')
|
||||
api_lines.append(' batch: batch,')
|
||||
api_lines.append(' isSignal: isSignal,')
|
||||
api_lines.append(' makeSignal: makeSignal,')
|
||||
api_lines.append(f' _version: "{version}"')
|
||||
api_lines.append(' };')
|
||||
api_lines.append('')
|
||||
|
||||
@@ -148,6 +148,32 @@ class PyEmitter:
|
||||
"callable?": "is_callable",
|
||||
"lambda?": "is_lambda",
|
||||
"component?": "is_component",
|
||||
"island?": "is_island",
|
||||
"make-island": "make_island",
|
||||
"make-signal": "make_signal",
|
||||
"signal?": "is_signal",
|
||||
"signal-value": "signal_value",
|
||||
"signal-set-value!": "signal_set_value",
|
||||
"signal-subscribers": "signal_subscribers",
|
||||
"signal-add-sub!": "signal_add_sub",
|
||||
"signal-remove-sub!": "signal_remove_sub",
|
||||
"signal-deps": "signal_deps",
|
||||
"signal-set-deps!": "signal_set_deps",
|
||||
"set-tracking-context!": "set_tracking_context",
|
||||
"get-tracking-context": "get_tracking_context",
|
||||
"make-tracking-context": "make_tracking_context",
|
||||
"tracking-context-deps": "tracking_context_deps",
|
||||
"tracking-context-add-dep!": "tracking_context_add_dep",
|
||||
"tracking-context-notify-fn": "tracking_context_notify_fn",
|
||||
"identical?": "is_identical",
|
||||
"notify-subscribers": "notify_subscribers",
|
||||
"flush-subscribers": "flush_subscribers",
|
||||
"dispose-computed": "dispose_computed",
|
||||
"with-island-scope": "with_island_scope",
|
||||
"register-in-scope": "register_in_scope",
|
||||
"*batch-depth*": "_batch_depth",
|
||||
"*batch-queue*": "_batch_queue",
|
||||
"*island-scope*": "_island_scope",
|
||||
"macro?": "is_macro",
|
||||
"primitive?": "is_primitive",
|
||||
"get-primitive": "get_primitive",
|
||||
@@ -232,6 +258,11 @@ class PyEmitter:
|
||||
"dispatch-html-form": "dispatch_html_form",
|
||||
"render-lambda-html": "render_lambda_html",
|
||||
"make-raw-html": "make_raw_html",
|
||||
"render-html-island": "render_html_island",
|
||||
"serialize-island-state": "serialize_island_state",
|
||||
"json-serialize": "json_serialize",
|
||||
"empty-dict?": "is_empty_dict",
|
||||
"sf-defisland": "sf_defisland",
|
||||
# adapter-sx.sx
|
||||
"render-to-sx": "render_to_sx",
|
||||
"aser": "aser",
|
||||
@@ -379,11 +410,26 @@ class PyEmitter:
|
||||
params = expr[1]
|
||||
body = expr[2:]
|
||||
param_names = []
|
||||
for p in params:
|
||||
rest_name = None
|
||||
i = 0
|
||||
while i < len(params):
|
||||
p = params[i]
|
||||
if isinstance(p, Symbol) and p.name == "&rest":
|
||||
# Next param is the rest parameter
|
||||
if i + 1 < len(params):
|
||||
rest_name = self._mangle(params[i + 1].name if isinstance(params[i + 1], Symbol) else str(params[i + 1]))
|
||||
i += 2
|
||||
continue
|
||||
else:
|
||||
i += 1
|
||||
continue
|
||||
if isinstance(p, Symbol):
|
||||
param_names.append(self._mangle(p.name))
|
||||
else:
|
||||
param_names.append(str(p))
|
||||
i += 1
|
||||
if rest_name:
|
||||
param_names.append(f"*{rest_name}")
|
||||
params_str = ", ".join(param_names)
|
||||
if len(body) == 1:
|
||||
body_py = self.emit(body[0])
|
||||
@@ -867,6 +913,7 @@ SPEC_MODULES = {
|
||||
"deps": ("deps.sx", "deps (component dependency analysis)"),
|
||||
"router": ("router.sx", "router (client-side route matching)"),
|
||||
"engine": ("engine.sx", "engine (fetch/swap/trigger pure logic)"),
|
||||
"signals": ("signals.sx", "signals (reactive signal runtime)"),
|
||||
}
|
||||
|
||||
|
||||
@@ -1005,6 +1052,9 @@ def compile_ref_to_py(
|
||||
raise ValueError(f"Unknown spec module: {sm!r}. Valid: {', '.join(SPEC_MODULES)}")
|
||||
spec_mod_set.add(sm)
|
||||
has_deps = "deps" in spec_mod_set
|
||||
# html adapter uses signal runtime for server-side island rendering
|
||||
if "html" in adapter_set and "signals" in SPEC_MODULES:
|
||||
spec_mod_set.add("signals")
|
||||
|
||||
# Core files always included, then selected adapters, then spec modules
|
||||
sx_files = [
|
||||
@@ -1092,7 +1142,7 @@ from typing import Any
|
||||
# =========================================================================
|
||||
|
||||
from shared.sx.types import (
|
||||
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro,
|
||||
NIL, Symbol, Keyword, Lambda, Component, Island, Continuation, Macro,
|
||||
HandlerDef, QueryDef, ActionDef, PageDef, _ShiftSignal,
|
||||
)
|
||||
from shared.sx.parser import SxExpr
|
||||
@@ -1197,6 +1247,10 @@ def type_of(x):
|
||||
return "lambda"
|
||||
if isinstance(x, Component):
|
||||
return "component"
|
||||
if isinstance(x, Island):
|
||||
return "island"
|
||||
if isinstance(x, _Signal):
|
||||
return "signal"
|
||||
if isinstance(x, Macro):
|
||||
return "macro"
|
||||
if isinstance(x, _RawHTML):
|
||||
@@ -1235,6 +1289,11 @@ def make_component(name, params, has_children, body, env, affinity="auto"):
|
||||
body=body, closure=dict(env), affinity=str(affinity) if affinity else "auto")
|
||||
|
||||
|
||||
def make_island(name, params, has_children, body, env):
|
||||
return Island(name=name, params=list(params), has_children=has_children,
|
||||
body=body, closure=dict(env))
|
||||
|
||||
|
||||
def make_macro(params, rest_param, body, env, name=None):
|
||||
return Macro(params=list(params), rest_param=rest_param, body=body,
|
||||
closure=dict(env), name=name)
|
||||
@@ -1369,6 +1428,119 @@ def is_macro(x):
|
||||
return isinstance(x, Macro)
|
||||
|
||||
|
||||
def is_island(x):
|
||||
return isinstance(x, Island)
|
||||
|
||||
|
||||
def is_identical(a, b):
|
||||
return a is b
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Signal platform -- reactive state primitives
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
class _Signal:
|
||||
"""Reactive signal container."""
|
||||
__slots__ = ("value", "subscribers", "deps")
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
self.subscribers = []
|
||||
self.deps = []
|
||||
|
||||
|
||||
class _TrackingContext:
|
||||
"""Context for discovering signal dependencies."""
|
||||
__slots__ = ("notify_fn", "deps")
|
||||
def __init__(self, notify_fn):
|
||||
self.notify_fn = notify_fn
|
||||
self.deps = []
|
||||
|
||||
|
||||
_tracking_context = None
|
||||
|
||||
|
||||
def make_signal(value):
|
||||
return _Signal(value)
|
||||
|
||||
|
||||
def is_signal(x):
|
||||
return isinstance(x, _Signal)
|
||||
|
||||
|
||||
def signal_value(s):
|
||||
return s.value if isinstance(s, _Signal) else s
|
||||
|
||||
|
||||
def signal_set_value(s, v):
|
||||
if isinstance(s, _Signal):
|
||||
s.value = v
|
||||
|
||||
|
||||
def signal_subscribers(s):
|
||||
return list(s.subscribers) if isinstance(s, _Signal) else []
|
||||
|
||||
|
||||
def signal_add_sub(s, fn):
|
||||
if isinstance(s, _Signal) and fn not in s.subscribers:
|
||||
s.subscribers.append(fn)
|
||||
|
||||
|
||||
def signal_remove_sub(s, fn):
|
||||
if isinstance(s, _Signal) and fn in s.subscribers:
|
||||
s.subscribers.remove(fn)
|
||||
|
||||
|
||||
def signal_deps(s):
|
||||
return list(s.deps) if isinstance(s, _Signal) else []
|
||||
|
||||
|
||||
def signal_set_deps(s, deps):
|
||||
if isinstance(s, _Signal):
|
||||
s.deps = list(deps) if isinstance(deps, list) else []
|
||||
|
||||
|
||||
def set_tracking_context(ctx):
|
||||
global _tracking_context
|
||||
_tracking_context = ctx
|
||||
|
||||
|
||||
def get_tracking_context():
|
||||
global _tracking_context
|
||||
return _tracking_context if _tracking_context is not None else NIL
|
||||
|
||||
|
||||
def make_tracking_context(notify_fn):
|
||||
return _TrackingContext(notify_fn)
|
||||
|
||||
|
||||
def tracking_context_deps(ctx):
|
||||
return ctx.deps if isinstance(ctx, _TrackingContext) else []
|
||||
|
||||
|
||||
def tracking_context_add_dep(ctx, s):
|
||||
if isinstance(ctx, _TrackingContext) and s not in ctx.deps:
|
||||
ctx.deps.append(s)
|
||||
|
||||
|
||||
def tracking_context_notify_fn(ctx):
|
||||
return ctx.notify_fn if isinstance(ctx, _TrackingContext) else NIL
|
||||
|
||||
|
||||
def json_serialize(obj):
|
||||
import json
|
||||
try:
|
||||
return json.dumps(obj)
|
||||
except (TypeError, ValueError):
|
||||
return "{}"
|
||||
|
||||
|
||||
def is_empty_dict(d):
|
||||
if not isinstance(d, dict):
|
||||
return True
|
||||
return len(d) == 0
|
||||
|
||||
|
||||
def env_has(env, name):
|
||||
return name in env
|
||||
|
||||
|
||||
@@ -124,3 +124,55 @@
|
||||
(define-boundary-types
|
||||
(list "number" "string" "boolean" "nil" "keyword"
|
||||
"list" "dict" "sx-source"))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Tier 3: Signal primitives — reactive state for islands
|
||||
;;
|
||||
;; These are pure primitives (no IO) but are separated from primitives.sx
|
||||
;; because they introduce a new type (signal) and depend on signals.sx.
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(declare-tier :signals :source "signals.sx")
|
||||
|
||||
(declare-signal-primitive "signal"
|
||||
:params (initial-value)
|
||||
:returns "signal"
|
||||
:doc "Create a reactive signal container with an initial value.")
|
||||
|
||||
(declare-signal-primitive "deref"
|
||||
:params (signal)
|
||||
:returns "any"
|
||||
:doc "Read a signal's current value. In a reactive context (inside an island),
|
||||
subscribes the current DOM binding to the signal. Outside reactive
|
||||
context, just returns the value.")
|
||||
|
||||
(declare-signal-primitive "reset!"
|
||||
:params (signal value)
|
||||
:returns "nil"
|
||||
:doc "Set a signal to a new value. Notifies all subscribers.")
|
||||
|
||||
(declare-signal-primitive "swap!"
|
||||
:params (signal f &rest args)
|
||||
:returns "nil"
|
||||
:doc "Update a signal by applying f to its current value. (swap! s inc)
|
||||
is equivalent to (reset! s (inc (deref s))) but atomic.")
|
||||
|
||||
(declare-signal-primitive "computed"
|
||||
:params (compute-fn)
|
||||
:returns "signal"
|
||||
:doc "Create a derived signal that recomputes when its dependencies change.
|
||||
Dependencies are discovered automatically by tracking deref calls.")
|
||||
|
||||
(declare-signal-primitive "effect"
|
||||
:params (effect-fn)
|
||||
:returns "lambda"
|
||||
:doc "Run a side effect that re-runs when its signal dependencies change.
|
||||
Returns a dispose function. If the effect function returns a function,
|
||||
it is called as cleanup before the next run.")
|
||||
|
||||
(declare-signal-primitive "batch"
|
||||
:params (thunk)
|
||||
:returns "any"
|
||||
:doc "Group multiple signal writes. Subscribers are notified once at the end,
|
||||
after all values have been updated.")
|
||||
|
||||
182
shared/sx/ref/demo-signals.html
Normal file
182
shared/sx/ref/demo-signals.html
Normal file
@@ -0,0 +1,182 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>SX Reactive Islands Demo</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: system-ui, sans-serif; max-width: 640px; margin: 40px auto; padding: 0 20px; color: #1a1a2e; background: #f8f8fc; }
|
||||
h1 { margin-bottom: 8px; font-size: 1.5rem; }
|
||||
.subtitle { color: #666; margin-bottom: 32px; font-size: 0.9rem; }
|
||||
.demo { background: white; border: 1px solid #e2e2ea; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
|
||||
.demo h2 { font-size: 1.1rem; margin-bottom: 12px; color: #2d2d4e; }
|
||||
.demo-row { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
|
||||
button { background: #4a3f8a; color: white; border: none; border-radius: 4px; padding: 6px 16px; cursor: pointer; font-size: 0.9rem; }
|
||||
button:hover { background: #5b4fa0; }
|
||||
button:active { background: #3a2f7a; }
|
||||
.value { font-size: 1.4rem; font-weight: 600; min-width: 3ch; text-align: center; }
|
||||
.derived { color: #666; font-size: 0.85rem; }
|
||||
.effect-log { background: #f0f0f8; border-radius: 4px; padding: 8px 12px; font-family: monospace; font-size: 0.8rem; max-height: 120px; overflow-y: auto; white-space: pre-wrap; }
|
||||
.batch-indicator { display: inline-block; background: #e8f5e9; color: #2e7d32; padding: 2px 8px; border-radius: 3px; font-size: 0.8rem; }
|
||||
code { background: #f0f0f8; padding: 2px 6px; border-radius: 3px; font-size: 0.85rem; }
|
||||
.note { color: #888; font-size: 0.8rem; margin-top: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>SX Reactive Islands</h1>
|
||||
<p class="subtitle">Signals transpiled from <code>signals.sx</code> spec via <code>bootstrap_js.py</code></p>
|
||||
|
||||
<!-- Demo 1: Basic signal -->
|
||||
<div class="demo" id="demo-counter">
|
||||
<h2>1. Signal: Counter</h2>
|
||||
<div class="demo-row">
|
||||
<button onclick="decr()">-</button>
|
||||
<span class="value" id="count-display">0</span>
|
||||
<button onclick="incr()">+</button>
|
||||
</div>
|
||||
<div class="derived" id="doubled-display"></div>
|
||||
<p class="note"><code>signal</code> + <code>computed</code> + <code>effect</code></p>
|
||||
</div>
|
||||
|
||||
<!-- Demo 2: Batch -->
|
||||
<div class="demo" id="demo-batch">
|
||||
<h2>2. Batch: Two signals, one notification</h2>
|
||||
<div class="demo-row">
|
||||
<span>first: <strong id="first-display">0</strong></span>
|
||||
<span>second: <strong id="second-display">0</strong></span>
|
||||
<span class="batch-indicator" id="render-count"></span>
|
||||
</div>
|
||||
<div class="demo-row">
|
||||
<button onclick="batchBoth()">Batch increment both</button>
|
||||
<button onclick="noBatchBoth()">No-batch increment both</button>
|
||||
</div>
|
||||
<p class="note"><code>batch</code> coalesces writes: 2 updates, 1 re-render</p>
|
||||
</div>
|
||||
|
||||
<!-- Demo 3: Effect with cleanup -->
|
||||
<div class="demo" id="demo-effect">
|
||||
<h2>3. Effect: Auto-tracking + Cleanup</h2>
|
||||
<div class="demo-row">
|
||||
<button onclick="togglePolling()">Toggle polling</button>
|
||||
<span id="poll-status"></span>
|
||||
</div>
|
||||
<div class="effect-log" id="effect-log"></div>
|
||||
<p class="note"><code>effect</code> returns cleanup fn; dispose stops tracking</p>
|
||||
</div>
|
||||
|
||||
<!-- Demo 4: Computed chains -->
|
||||
<div class="demo" id="demo-chain">
|
||||
<h2>4. Computed chain: base → doubled → quadrupled</h2>
|
||||
<div class="demo-row">
|
||||
<button onclick="chainDecr()">-</button>
|
||||
<span>base: <strong id="chain-base">1</strong></span>
|
||||
<button onclick="chainIncr()">+</button>
|
||||
</div>
|
||||
<div class="derived">
|
||||
doubled: <strong id="chain-doubled"></strong>
|
||||
quadrupled: <strong id="chain-quad"></strong>
|
||||
</div>
|
||||
<p class="note">Three-level computed dependency graph, auto-propagation</p>
|
||||
</div>
|
||||
|
||||
<script src="sx-ref.js"></script>
|
||||
<script>
|
||||
// Grab signal primitives from transpiled runtime
|
||||
var S = window.Sx;
|
||||
var signal = S.signal;
|
||||
var deref = S.deref;
|
||||
var reset = S.reset;
|
||||
var swap = S.swap;
|
||||
var computed = S.computed;
|
||||
var effect = S.effect;
|
||||
var batch = S.batch;
|
||||
|
||||
// ---- Demo 1: Counter ----
|
||||
var count = signal(0);
|
||||
var doubled = computed(function() { return deref(count) * 2; });
|
||||
|
||||
effect(function() {
|
||||
document.getElementById("count-display").textContent = deref(count);
|
||||
});
|
||||
effect(function() {
|
||||
document.getElementById("doubled-display").textContent = "doubled: " + deref(doubled);
|
||||
});
|
||||
|
||||
function incr() { swap(count, function(n) { return n + 1; }); }
|
||||
function decr() { swap(count, function(n) { return n - 1; }); }
|
||||
|
||||
// ---- Demo 2: Batch ----
|
||||
var first = signal(0);
|
||||
var second = signal(0);
|
||||
var renders = signal(0);
|
||||
|
||||
effect(function() {
|
||||
document.getElementById("first-display").textContent = deref(first);
|
||||
document.getElementById("second-display").textContent = deref(second);
|
||||
swap(renders, function(n) { return n + 1; });
|
||||
});
|
||||
effect(function() {
|
||||
document.getElementById("render-count").textContent = "renders: " + deref(renders);
|
||||
});
|
||||
|
||||
function batchBoth() {
|
||||
batch(function() {
|
||||
swap(first, function(n) { return n + 1; });
|
||||
swap(second, function(n) { return n + 1; });
|
||||
});
|
||||
}
|
||||
function noBatchBoth() {
|
||||
swap(first, function(n) { return n + 1; });
|
||||
swap(second, function(n) { return n + 1; });
|
||||
}
|
||||
|
||||
// ---- Demo 3: Effect with cleanup ----
|
||||
var polling = signal(false);
|
||||
var pollDispose = null;
|
||||
var logEl = document.getElementById("effect-log");
|
||||
|
||||
function log(msg) {
|
||||
logEl.textContent += msg + "\n";
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
effect(function() {
|
||||
var active = deref(polling);
|
||||
document.getElementById("poll-status").textContent = active ? "polling..." : "stopped";
|
||||
if (active) {
|
||||
var n = 0;
|
||||
var id = setInterval(function() {
|
||||
n++;
|
||||
log("poll #" + n);
|
||||
}, 500);
|
||||
log("effect: started interval");
|
||||
// Return cleanup function
|
||||
return function() {
|
||||
clearInterval(id);
|
||||
log("cleanup: cleared interval");
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function togglePolling() { swap(polling, function(v) { return !v; }); }
|
||||
|
||||
// ---- Demo 4: Computed chain ----
|
||||
var base = signal(1);
|
||||
var chainDoubled = computed(function() { return deref(base) * 2; });
|
||||
var quadrupled = computed(function() { return deref(chainDoubled) * 2; });
|
||||
|
||||
effect(function() {
|
||||
document.getElementById("chain-base").textContent = deref(base);
|
||||
});
|
||||
effect(function() {
|
||||
document.getElementById("chain-doubled").textContent = deref(chainDoubled);
|
||||
});
|
||||
effect(function() {
|
||||
document.getElementById("chain-quad").textContent = deref(quadrupled);
|
||||
});
|
||||
|
||||
function chainIncr() { swap(base, function(n) { return n + 1; }); }
|
||||
function chainDecr() { swap(base, function(n) { return n - 1; }); }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -35,12 +35,14 @@
|
||||
;; lambda — closure: {params, body, closure-env, name?}
|
||||
;; macro — AST transformer: {params, rest-param, body, closure-env}
|
||||
;; component — UI component: {name, params, has-children, body, closure-env}
|
||||
;; island — reactive component: like component but with island flag
|
||||
;; thunk — deferred eval for TCO: {expr, env}
|
||||
;;
|
||||
;; Each target must provide:
|
||||
;; (type-of x) → one of the strings above
|
||||
;; (make-lambda ...) → platform Lambda value
|
||||
;; (make-component ..) → platform Component value
|
||||
;; (make-island ...) → platform Island value (component + island flag)
|
||||
;; (make-macro ...) → platform Macro value
|
||||
;; (make-thunk ...) → platform Thunk value
|
||||
;;
|
||||
@@ -141,6 +143,7 @@
|
||||
(= name "fn") (sf-lambda args env)
|
||||
(= name "define") (sf-define args env)
|
||||
(= name "defcomp") (sf-defcomp args env)
|
||||
(= name "defisland") (sf-defisland args env)
|
||||
(= name "defmacro") (sf-defmacro args env)
|
||||
(= name "defstyle") (sf-defstyle args env)
|
||||
(= name "defhandler") (sf-defhandler args env)
|
||||
@@ -192,7 +195,7 @@
|
||||
(evaluated-args (map (fn (a) (trampoline (eval-expr a env))) args)))
|
||||
(cond
|
||||
;; Native callable (primitive function)
|
||||
(and (callable? f) (not (lambda? f)) (not (component? f)))
|
||||
(and (callable? f) (not (lambda? f)) (not (component? f)) (not (island? f)))
|
||||
(apply f evaluated-args)
|
||||
|
||||
;; Lambda
|
||||
@@ -203,6 +206,10 @@
|
||||
(component? f)
|
||||
(call-component f args env)
|
||||
|
||||
;; Island (reactive component) — same calling convention
|
||||
(island? f)
|
||||
(call-component f args env)
|
||||
|
||||
:else (error (str "Not callable: " (inspect f)))))))
|
||||
|
||||
|
||||
@@ -543,6 +550,24 @@
|
||||
(list params has-children))))
|
||||
|
||||
|
||||
(define sf-defisland
|
||||
(fn (args env)
|
||||
;; (defisland ~name (params) body)
|
||||
;; Like defcomp but creates an island (reactive component).
|
||||
;; Islands have the same calling convention as components but
|
||||
;; render with a reactive context on the client.
|
||||
(let ((name-sym (first args))
|
||||
(params-raw (nth args 1))
|
||||
(body (last args))
|
||||
(comp-name (strip-prefix (symbol-name name-sym) "~"))
|
||||
(parsed (parse-comp-params params-raw))
|
||||
(params (first parsed))
|
||||
(has-children (nth parsed 1)))
|
||||
(let ((island (make-island comp-name params has-children body env)))
|
||||
(env-set! env (symbol-name name-sym) island)
|
||||
island))))
|
||||
|
||||
|
||||
(define sf-defmacro
|
||||
(fn (args env)
|
||||
(let ((name-sym (first args))
|
||||
@@ -903,6 +928,11 @@
|
||||
;; (component-closure c) → env
|
||||
;; (component-has-children? c) → boolean
|
||||
;; (component-affinity c) → "auto" | "client" | "server"
|
||||
;;
|
||||
;; (make-island name params has-children body env) → Island
|
||||
;; (island? x) → boolean
|
||||
;; ;; Islands reuse component accessors: component-params, component-body, etc.
|
||||
;;
|
||||
;; (macro-params m) → list of strings
|
||||
;; (macro-rest-param m) → string or nil
|
||||
;; (macro-body m) → expr
|
||||
@@ -915,6 +945,7 @@
|
||||
;; (callable? x) → boolean (native function or lambda)
|
||||
;; (lambda? x) → boolean
|
||||
;; (component? x) → boolean
|
||||
;; (island? x) → boolean
|
||||
;; (macro? x) → boolean
|
||||
;; (primitive? name) → boolean (is name a registered primitive?)
|
||||
;; (get-primitive name) → function
|
||||
|
||||
@@ -73,8 +73,8 @@
|
||||
|
||||
(define definition-form?
|
||||
(fn (name)
|
||||
(or (= name "define") (= name "defcomp") (= name "defmacro")
|
||||
(= name "defstyle") (= name "defhandler"))))
|
||||
(or (= name "define") (= name "defcomp") (= name "defisland")
|
||||
(= name "defmacro") (= name "defstyle") (= name "defhandler"))))
|
||||
|
||||
|
||||
(define parse-element-args
|
||||
|
||||
290
shared/sx/ref/signals.sx
Normal file
290
shared/sx/ref/signals.sx
Normal file
@@ -0,0 +1,290 @@
|
||||
;; ==========================================================================
|
||||
;; signals.sx — Reactive signal runtime specification
|
||||
;;
|
||||
;; Defines the signal primitive: a container for a value that notifies
|
||||
;; subscribers when it changes. Signals are the reactive state primitive
|
||||
;; for SX islands.
|
||||
;;
|
||||
;; Signals are pure computation — no DOM, no IO. The reactive rendering
|
||||
;; layer (adapter-dom.sx) subscribes DOM nodes to signals. The server
|
||||
;; adapter (adapter-html.sx) reads signal values without subscribing.
|
||||
;;
|
||||
;; Platform interface required:
|
||||
;; (make-signal value) → Signal — create signal container
|
||||
;; (signal? x) → boolean — type predicate
|
||||
;; (signal-value s) → any — read current value (no tracking)
|
||||
;; (signal-set-value! s v) → void — write value (no notification)
|
||||
;; (signal-subscribers s) → list — list of subscriber fns
|
||||
;; (signal-add-sub! s fn) → void — add subscriber
|
||||
;; (signal-remove-sub! s fn) → void — remove subscriber
|
||||
;; (signal-deps s) → list — dependency list (for computed)
|
||||
;; (signal-set-deps! s deps) → void — set dependency list
|
||||
;;
|
||||
;; Global state required:
|
||||
;; *tracking-context* → nil | Effect/Computed currently evaluating
|
||||
;; (set-tracking-context! c) → void
|
||||
;; (get-tracking-context) → context or nil
|
||||
;;
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 1. signal — create a reactive container
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define signal
|
||||
(fn (initial-value)
|
||||
(make-signal initial-value)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 2. deref — read signal value, subscribe current reactive context
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; In a reactive context (inside effect or computed), deref registers the
|
||||
;; signal as a dependency. Outside reactive context, deref just returns
|
||||
;; the current value — no subscription, no overhead.
|
||||
|
||||
(define deref
|
||||
(fn (s)
|
||||
(if (not (signal? s))
|
||||
s ;; non-signal values pass through
|
||||
(let ((ctx (get-tracking-context)))
|
||||
(when ctx
|
||||
;; Register this signal as a dependency of the current context
|
||||
(tracking-context-add-dep! ctx s)
|
||||
;; Subscribe the context to this signal
|
||||
(signal-add-sub! s (tracking-context-notify-fn ctx)))
|
||||
(signal-value s)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3. reset! — write a new value, notify subscribers
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define reset!
|
||||
(fn (s value)
|
||||
(when (signal? s)
|
||||
(let ((old (signal-value s)))
|
||||
(when (not (identical? old value))
|
||||
(signal-set-value! s value)
|
||||
(notify-subscribers s))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 4. swap! — update signal via function
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define swap!
|
||||
(fn (s f &rest args)
|
||||
(when (signal? s)
|
||||
(let ((old (signal-value s))
|
||||
(new-val (apply f (cons old args))))
|
||||
(when (not (identical? old new-val))
|
||||
(signal-set-value! s new-val)
|
||||
(notify-subscribers s))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 5. computed — derived signal with automatic dependency tracking
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; A computed signal wraps a zero-arg function. It re-evaluates when any
|
||||
;; of its dependencies change. The dependency set is discovered automatically
|
||||
;; by tracking deref calls during evaluation.
|
||||
|
||||
(define computed
|
||||
(fn (compute-fn)
|
||||
(let ((s (make-signal nil))
|
||||
(deps (list))
|
||||
(compute-ctx nil))
|
||||
|
||||
;; The notify function — called when a dependency changes
|
||||
(let ((recompute
|
||||
(fn ()
|
||||
;; Unsubscribe from old deps
|
||||
(for-each
|
||||
(fn (dep) (signal-remove-sub! dep recompute))
|
||||
(signal-deps s))
|
||||
(signal-set-deps! s (list))
|
||||
|
||||
;; Create tracking context for this computed
|
||||
(let ((ctx (make-tracking-context recompute)))
|
||||
(let ((prev (get-tracking-context)))
|
||||
(set-tracking-context! ctx)
|
||||
(let ((new-val (compute-fn)))
|
||||
(set-tracking-context! prev)
|
||||
;; Save discovered deps
|
||||
(signal-set-deps! s (tracking-context-deps ctx))
|
||||
;; Update value + notify downstream
|
||||
(let ((old (signal-value s)))
|
||||
(signal-set-value! s new-val)
|
||||
(when (not (identical? old new-val))
|
||||
(notify-subscribers s)))))))))
|
||||
|
||||
;; Initial computation
|
||||
(recompute)
|
||||
s))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 6. effect — side effect that runs when dependencies change
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Like computed, but doesn't produce a signal value. Returns a dispose
|
||||
;; function that tears down the effect.
|
||||
|
||||
(define effect
|
||||
(fn (effect-fn)
|
||||
(let ((deps (list))
|
||||
(disposed false)
|
||||
(cleanup-fn nil))
|
||||
|
||||
(let ((run-effect
|
||||
(fn ()
|
||||
(when (not disposed)
|
||||
;; Run previous cleanup if any
|
||||
(when cleanup-fn (cleanup-fn))
|
||||
|
||||
;; Unsubscribe from old deps
|
||||
(for-each
|
||||
(fn (dep) (signal-remove-sub! dep run-effect))
|
||||
deps)
|
||||
(set! deps (list))
|
||||
|
||||
;; Track new deps
|
||||
(let ((ctx (make-tracking-context run-effect)))
|
||||
(let ((prev (get-tracking-context)))
|
||||
(set-tracking-context! ctx)
|
||||
(let ((result (effect-fn)))
|
||||
(set-tracking-context! prev)
|
||||
(set! deps (tracking-context-deps ctx))
|
||||
;; If effect returns a function, it's the cleanup
|
||||
(when (callable? result)
|
||||
(set! cleanup-fn result)))))))))
|
||||
|
||||
;; Initial run
|
||||
(run-effect)
|
||||
|
||||
;; Return dispose function
|
||||
(fn ()
|
||||
(set! disposed true)
|
||||
(when cleanup-fn (cleanup-fn))
|
||||
(for-each
|
||||
(fn (dep) (signal-remove-sub! dep run-effect))
|
||||
deps)
|
||||
(set! deps (list)))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 7. batch — group multiple signal writes into one notification pass
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; During a batch, signal writes are deferred. Subscribers are notified
|
||||
;; once at the end, after all values have been updated.
|
||||
|
||||
(define *batch-depth* 0)
|
||||
(define *batch-queue* (list))
|
||||
|
||||
(define batch
|
||||
(fn (thunk)
|
||||
(set! *batch-depth* (+ *batch-depth* 1))
|
||||
(thunk)
|
||||
(set! *batch-depth* (- *batch-depth* 1))
|
||||
(when (= *batch-depth* 0)
|
||||
(let ((queue *batch-queue*))
|
||||
(set! *batch-queue* (list))
|
||||
;; Collect unique subscribers across all queued signals,
|
||||
;; then notify each exactly once.
|
||||
(let ((seen (list))
|
||||
(pending (list)))
|
||||
(for-each
|
||||
(fn (s)
|
||||
(for-each
|
||||
(fn (sub)
|
||||
(when (not (contains? seen sub))
|
||||
(append! seen sub)
|
||||
(append! pending sub)))
|
||||
(signal-subscribers s)))
|
||||
queue)
|
||||
(for-each (fn (sub) (sub)) pending))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 8. notify-subscribers — internal notification dispatch
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; If inside a batch, queues the signal. Otherwise, notifies immediately.
|
||||
|
||||
(define notify-subscribers
|
||||
(fn (s)
|
||||
(if (> *batch-depth* 0)
|
||||
(when (not (contains? *batch-queue* s))
|
||||
(append! *batch-queue* s))
|
||||
(flush-subscribers s))))
|
||||
|
||||
(define flush-subscribers
|
||||
(fn (s)
|
||||
(for-each
|
||||
(fn (sub) (sub))
|
||||
(signal-subscribers s))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 9. Tracking context
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; A tracking context is an ephemeral object created during effect/computed
|
||||
;; evaluation to discover signal dependencies. Platform must provide:
|
||||
;;
|
||||
;; (make-tracking-context notify-fn) → context
|
||||
;; (tracking-context-deps ctx) → list of signals
|
||||
;; (tracking-context-add-dep! ctx s) → void (adds s to ctx's dep list)
|
||||
;; (tracking-context-notify-fn ctx) → the notify function
|
||||
;;
|
||||
;; These are platform primitives because the context is mutable state
|
||||
;; that must be efficient (often a Set in the host language).
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 10. dispose — tear down a computed signal
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; For computed signals, unsubscribe from all dependencies.
|
||||
;; For effects, the dispose function is returned by effect itself.
|
||||
|
||||
(define dispose-computed
|
||||
(fn (s)
|
||||
(when (signal? s)
|
||||
(for-each
|
||||
(fn (dep) (signal-remove-sub! dep nil))
|
||||
(signal-deps s))
|
||||
(signal-set-deps! s (list)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 11. Island scope — automatic cleanup of signals within an island
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; When an island is created, all signals, effects, and computeds created
|
||||
;; within it are tracked. When the island is removed from the DOM, they
|
||||
;; are all disposed.
|
||||
|
||||
(define *island-scope* nil)
|
||||
|
||||
(define with-island-scope
|
||||
(fn (scope-fn body-fn)
|
||||
(let ((prev *island-scope*))
|
||||
(set! *island-scope* scope-fn)
|
||||
(let ((result (body-fn)))
|
||||
(set! *island-scope* prev)
|
||||
result))))
|
||||
|
||||
;; Hook into signal/effect/computed creation for scope tracking.
|
||||
;; The platform's make-signal should call (register-in-scope s) if
|
||||
;; *island-scope* is non-nil.
|
||||
|
||||
(define register-in-scope
|
||||
(fn (disposable)
|
||||
(when *island-scope*
|
||||
(*island-scope* disposable))))
|
||||
@@ -182,6 +182,23 @@
|
||||
(when subtitle (p subtitle))
|
||||
children))")
|
||||
|
||||
(define-special-form "defisland"
|
||||
:syntax (defisland ~name (&key param1 param2 &rest children) body)
|
||||
:doc "Define a reactive island. Islands have the same calling convention
|
||||
as components (defcomp) but create a reactive boundary. Inside an
|
||||
island, signals are tracked — deref subscribes DOM nodes to signals,
|
||||
and signal changes update only the affected nodes.
|
||||
|
||||
On the server, islands render as static HTML wrapped in a
|
||||
data-sx-island container with serialized initial state. On the
|
||||
client, islands hydrate into reactive contexts."
|
||||
:tail-position "body"
|
||||
:example "(defisland ~counter (&key initial)
|
||||
(let ((count (signal (or initial 0))))
|
||||
(div :class \"counter\"
|
||||
(span (deref count))
|
||||
(button :on-click (fn (e) (swap! count inc)) \"+\"))))")
|
||||
|
||||
(define-special-form "defmacro"
|
||||
:syntax (defmacro name (params ...) body)
|
||||
:doc "Define a macro. Macros receive their arguments unevaluated (as raw
|
||||
|
||||
@@ -18,7 +18,7 @@ from typing import Any
|
||||
# =========================================================================
|
||||
|
||||
from shared.sx.types import (
|
||||
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro,
|
||||
NIL, Symbol, Keyword, Lambda, Component, Island, Continuation, Macro,
|
||||
HandlerDef, QueryDef, ActionDef, PageDef, _ShiftSignal,
|
||||
)
|
||||
from shared.sx.parser import SxExpr
|
||||
@@ -122,6 +122,10 @@ def type_of(x):
|
||||
return "lambda"
|
||||
if isinstance(x, Component):
|
||||
return "component"
|
||||
if isinstance(x, Island):
|
||||
return "island"
|
||||
if isinstance(x, _Signal):
|
||||
return "signal"
|
||||
if isinstance(x, Macro):
|
||||
return "macro"
|
||||
if isinstance(x, _RawHTML):
|
||||
@@ -160,6 +164,11 @@ def make_component(name, params, has_children, body, env, affinity="auto"):
|
||||
body=body, closure=dict(env), affinity=str(affinity) if affinity else "auto")
|
||||
|
||||
|
||||
def make_island(name, params, has_children, body, env):
|
||||
return Island(name=name, params=list(params), has_children=has_children,
|
||||
body=body, closure=dict(env))
|
||||
|
||||
|
||||
def make_macro(params, rest_param, body, env, name=None):
|
||||
return Macro(params=list(params), rest_param=rest_param, body=body,
|
||||
closure=dict(env), name=name)
|
||||
@@ -294,6 +303,119 @@ def is_macro(x):
|
||||
return isinstance(x, Macro)
|
||||
|
||||
|
||||
def is_island(x):
|
||||
return isinstance(x, Island)
|
||||
|
||||
|
||||
def is_identical(a, b):
|
||||
return a is b
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Signal platform -- reactive state primitives
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
class _Signal:
|
||||
"""Reactive signal container."""
|
||||
__slots__ = ("value", "subscribers", "deps")
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
self.subscribers = []
|
||||
self.deps = []
|
||||
|
||||
|
||||
class _TrackingContext:
|
||||
"""Context for discovering signal dependencies."""
|
||||
__slots__ = ("notify_fn", "deps")
|
||||
def __init__(self, notify_fn):
|
||||
self.notify_fn = notify_fn
|
||||
self.deps = []
|
||||
|
||||
|
||||
_tracking_context = None
|
||||
|
||||
|
||||
def make_signal(value):
|
||||
return _Signal(value)
|
||||
|
||||
|
||||
def is_signal(x):
|
||||
return isinstance(x, _Signal)
|
||||
|
||||
|
||||
def signal_value(s):
|
||||
return s.value if isinstance(s, _Signal) else s
|
||||
|
||||
|
||||
def signal_set_value(s, v):
|
||||
if isinstance(s, _Signal):
|
||||
s.value = v
|
||||
|
||||
|
||||
def signal_subscribers(s):
|
||||
return list(s.subscribers) if isinstance(s, _Signal) else []
|
||||
|
||||
|
||||
def signal_add_sub(s, fn):
|
||||
if isinstance(s, _Signal) and fn not in s.subscribers:
|
||||
s.subscribers.append(fn)
|
||||
|
||||
|
||||
def signal_remove_sub(s, fn):
|
||||
if isinstance(s, _Signal) and fn in s.subscribers:
|
||||
s.subscribers.remove(fn)
|
||||
|
||||
|
||||
def signal_deps(s):
|
||||
return list(s.deps) if isinstance(s, _Signal) else []
|
||||
|
||||
|
||||
def signal_set_deps(s, deps):
|
||||
if isinstance(s, _Signal):
|
||||
s.deps = list(deps) if isinstance(deps, list) else []
|
||||
|
||||
|
||||
def set_tracking_context(ctx):
|
||||
global _tracking_context
|
||||
_tracking_context = ctx
|
||||
|
||||
|
||||
def get_tracking_context():
|
||||
global _tracking_context
|
||||
return _tracking_context if _tracking_context is not None else NIL
|
||||
|
||||
|
||||
def make_tracking_context(notify_fn):
|
||||
return _TrackingContext(notify_fn)
|
||||
|
||||
|
||||
def tracking_context_deps(ctx):
|
||||
return ctx.deps if isinstance(ctx, _TrackingContext) else []
|
||||
|
||||
|
||||
def tracking_context_add_dep(ctx, s):
|
||||
if isinstance(ctx, _TrackingContext) and s not in ctx.deps:
|
||||
ctx.deps.append(s)
|
||||
|
||||
|
||||
def tracking_context_notify_fn(ctx):
|
||||
return ctx.notify_fn if isinstance(ctx, _TrackingContext) else NIL
|
||||
|
||||
|
||||
def json_serialize(obj):
|
||||
import json
|
||||
try:
|
||||
return json.dumps(obj)
|
||||
except (TypeError, ValueError):
|
||||
return "{}"
|
||||
|
||||
|
||||
def is_empty_dict(d):
|
||||
if not isinstance(d, dict):
|
||||
return True
|
||||
return len(d) == 0
|
||||
|
||||
|
||||
def env_has(env, name):
|
||||
return name in env
|
||||
|
||||
@@ -890,54 +1012,6 @@ has_key_p = PRIMITIVES["has-key?"]
|
||||
dissoc = PRIMITIVES["dissoc"]
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Platform: deps module — component dependency analysis
|
||||
# =========================================================================
|
||||
|
||||
import re as _re
|
||||
|
||||
def component_deps(c):
|
||||
"""Return cached deps list for a component (may be empty)."""
|
||||
return list(c.deps) if hasattr(c, "deps") and c.deps else []
|
||||
|
||||
def component_set_deps(c, deps):
|
||||
"""Cache deps on a component."""
|
||||
c.deps = set(deps) if not isinstance(deps, set) else deps
|
||||
|
||||
def component_css_classes(c):
|
||||
"""Return pre-scanned CSS class list for a component."""
|
||||
return list(c.css_classes) if hasattr(c, "css_classes") and c.css_classes else []
|
||||
|
||||
def env_components(env):
|
||||
"""Return list of component/macro names in an environment."""
|
||||
return [k for k, v in env.items()
|
||||
if isinstance(v, (Component, Macro))]
|
||||
|
||||
def regex_find_all(pattern, source):
|
||||
"""Return list of capture group 1 matches."""
|
||||
return [m.group(1) for m in _re.finditer(pattern, source)]
|
||||
|
||||
def scan_css_classes(source):
|
||||
"""Extract CSS class strings from SX source."""
|
||||
classes = set()
|
||||
for m in _re.finditer(r':class\s+"([^"]*)"', source):
|
||||
classes.update(m.group(1).split())
|
||||
for m in _re.finditer(r':class\s+\(str\s+((?:"[^"]*"\s*)+)\)', source):
|
||||
for s in _re.findall(r'"([^"]*)"', m.group(1)):
|
||||
classes.update(s.split())
|
||||
for m in _re.finditer(r';;\s*@css\s+(.+)', source):
|
||||
classes.update(m.group(1).split())
|
||||
return list(classes)
|
||||
|
||||
def component_io_refs(c):
|
||||
"""Return cached IO refs list for a component (may be empty)."""
|
||||
return list(c.io_refs) if hasattr(c, "io_refs") and c.io_refs else []
|
||||
|
||||
def component_set_io_refs(c, refs):
|
||||
"""Cache IO refs on a component."""
|
||||
c.io_refs = set(refs) if not isinstance(refs, set) else refs
|
||||
|
||||
|
||||
# === Transpiled from eval ===
|
||||
|
||||
# trampoline
|
||||
@@ -947,10 +1021,10 @@ trampoline = lambda val: (lambda result: (trampoline(eval_expr(thunk_expr(result
|
||||
eval_expr = lambda expr, env: _sx_case(type_of(expr), [('number', lambda: expr), ('string', lambda: expr), ('boolean', lambda: expr), ('nil', lambda: NIL), ('symbol', lambda: (lambda name: (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))))(symbol_name(expr))), ('keyword', lambda: keyword_name(expr)), ('dict', lambda: map_dict(lambda k, v: trampoline(eval_expr(v, env)), expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else eval_list(expr, env))), (None, lambda: expr)])
|
||||
|
||||
# eval-list
|
||||
eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_letrec(args, env) if sx_truthy((name == 'letrec')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defhandler(args, env) if sx_truthy((name == 'defhandler')) else (sf_defpage(args, env) if sx_truthy((name == 'defpage')) else (sf_defquery(args, env) if sx_truthy((name == 'defquery')) else (sf_defaction(args, env) if sx_truthy((name == 'defaction')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (sf_reset(args, env) if sx_truthy((name == 'reset')) else (sf_shift(args, env) if sx_truthy((name == 'shift')) else (sf_dynamic_wind(args, env) if sx_truthy((name == 'dynamic-wind')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy(is_render_expr(expr)) else eval_call(head, args, env)))))))))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr))
|
||||
eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_letrec(args, env) if sx_truthy((name == 'letrec')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defisland(args, env) if sx_truthy((name == 'defisland')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defhandler(args, env) if sx_truthy((name == 'defhandler')) else (sf_defpage(args, env) if sx_truthy((name == 'defpage')) else (sf_defquery(args, env) if sx_truthy((name == 'defquery')) else (sf_defaction(args, env) if sx_truthy((name == 'defaction')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (sf_reset(args, env) if sx_truthy((name == 'reset')) else (sf_shift(args, env) if sx_truthy((name == 'shift')) else (sf_dynamic_wind(args, env) if sx_truthy((name == 'dynamic-wind')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy(is_render_expr(expr)) else eval_call(head, args, env))))))))))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr))
|
||||
|
||||
# eval-call
|
||||
eval_call = lambda head, args, env: (lambda f: (lambda evaluated_args: (apply(f, evaluated_args) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else (not sx_truthy(is_component(f)))))) else (call_lambda(f, evaluated_args, env) if sx_truthy(is_lambda(f)) else (call_component(f, args, env) if sx_truthy(is_component(f)) else error(sx_str('Not callable: ', inspect(f)))))))(map(lambda a: trampoline(eval_expr(a, env)), args)))(trampoline(eval_expr(head, env)))
|
||||
eval_call = lambda head, args, env: (lambda f: (lambda evaluated_args: (apply(f, evaluated_args) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else ((not sx_truthy(is_component(f))) if not sx_truthy((not sx_truthy(is_component(f)))) else (not sx_truthy(is_island(f))))))) else (call_lambda(f, evaluated_args, env) if sx_truthy(is_lambda(f)) else (call_component(f, args, env) if sx_truthy(is_component(f)) else (call_component(f, args, env) if sx_truthy(is_island(f)) else error(sx_str('Not callable: ', inspect(f))))))))(map(lambda a: trampoline(eval_expr(a, env)), args)))(trampoline(eval_expr(head, env)))
|
||||
|
||||
# call-lambda
|
||||
call_lambda = lambda f, args, caller_env: (lambda params: (lambda local: (error(sx_str((lambda_name(f) if sx_truthy(lambda_name(f)) else 'lambda'), ' expects ', len(params), ' args, got ', len(args))) if sx_truthy((len(args) != len(params))) else _sx_begin(for_each(lambda pair: _sx_dict_set(local, first(pair), nth(pair, 1)), zip(params, args)), make_thunk(lambda_body(f), local))))(env_merge(lambda_closure(f), caller_env)))(lambda_params(f))
|
||||
@@ -1040,6 +1114,9 @@ def parse_comp_params(params_expr):
|
||||
params.append(name)
|
||||
return [params, _cells['has_children']]
|
||||
|
||||
# sf-defisland
|
||||
sf_defisland = lambda args, env: (lambda name_sym: (lambda params_raw: (lambda body: (lambda comp_name: (lambda parsed: (lambda params: (lambda has_children: (lambda island: _sx_begin(_sx_dict_set(env, symbol_name(name_sym), island), island))(make_island(comp_name, params, has_children, body, env)))(nth(parsed, 1)))(first(parsed)))(parse_comp_params(params_raw)))(strip_prefix(symbol_name(name_sym), '~')))(last(args)))(nth(args, 1)))(first(args))
|
||||
|
||||
# sf-defmacro
|
||||
sf_defmacro = lambda args, env: (lambda name_sym: (lambda params_raw: (lambda body: (lambda parsed: (lambda params: (lambda rest_param: (lambda mac: _sx_begin(_sx_dict_set(env, symbol_name(name_sym), mac), mac))(make_macro(params, rest_param, body, env, symbol_name(name_sym))))(nth(parsed, 1)))(first(parsed)))(parse_macro_params(params_raw)))(nth(args, 2)))(nth(args, 1)))(first(args))
|
||||
|
||||
@@ -1164,7 +1241,7 @@ VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'li
|
||||
BOOLEAN_ATTRS = ['async', 'autofocus', 'autoplay', 'checked', 'controls', 'default', 'defer', 'disabled', 'formnovalidate', 'hidden', 'inert', 'ismap', 'loop', 'multiple', 'muted', 'nomodule', 'novalidate', 'open', 'playsinline', 'readonly', 'required', 'reversed', 'selected']
|
||||
|
||||
# definition-form?
|
||||
is_definition_form = lambda name: ((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else (name == 'defhandler')))))
|
||||
is_definition_form = lambda name: ((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defisland') if sx_truthy((name == 'defisland')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else (name == 'defhandler'))))))
|
||||
|
||||
# parse-element-args
|
||||
parse_element_args = lambda args, env: (lambda attrs: (lambda children: _sx_begin(reduce(lambda state, arg: (lambda skip: (assoc(state, 'skip', False, 'i', (get(state, 'i') + 1)) if sx_truthy(skip) else ((lambda val: _sx_begin(_sx_dict_set(attrs, keyword_name(arg), val), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(trampoline(eval_expr(nth(args, (get(state, 'i') + 1)), env))) if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((get(state, 'i') + 1) < len(args)))) else _sx_begin(_sx_append(children, arg), assoc(state, 'i', (get(state, 'i') + 1))))))(get(state, 'skip')), {'i': 0, 'skip': False}, args), [attrs, children]))([]))({})
|
||||
@@ -1194,13 +1271,13 @@ render_to_html = lambda expr, env: _sx_case(type_of(expr), [('nil', lambda: ''),
|
||||
render_value_to_html = lambda val, env: _sx_case(type_of(val), [('nil', lambda: ''), ('string', lambda: escape_html(val)), ('number', lambda: sx_str(val)), ('boolean', lambda: ('true' if sx_truthy(val) else 'false')), ('list', lambda: render_list_to_html(val, env)), ('raw-html', lambda: raw_html_content(val)), (None, lambda: escape_html(sx_str(val)))])
|
||||
|
||||
# RENDER_HTML_FORMS
|
||||
RENDER_HTML_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'map', 'map-indexed', 'filter', 'for-each']
|
||||
RENDER_HTML_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'map', 'map-indexed', 'filter', 'for-each']
|
||||
|
||||
# render-html-form?
|
||||
is_render_html_form = lambda name: contains_p(RENDER_HTML_FORMS, name)
|
||||
|
||||
# render-list-to-html
|
||||
render_list_to_html = lambda expr, env: ('' if sx_truthy(empty_p(expr)) else (lambda head: (join('', map(lambda x: render_value_to_html(x, env), expr)) if sx_truthy((not sx_truthy((type_of(head) == 'symbol')))) else (lambda name: (lambda args: (join('', map(lambda x: render_to_html(x, env), args)) if sx_truthy((name == '<>')) else (join('', map(lambda x: sx_str(trampoline(eval_expr(x, env))), args)) if sx_truthy((name == 'raw!')) else (render_html_element(name, args, env) if sx_truthy(contains_p(HTML_TAGS, name)) else ((lambda val: (render_html_component(val, args, env) if sx_truthy(is_component(val)) else (render_to_html(expand_macro(val, args, env), env) if sx_truthy(is_macro(val)) else error(sx_str('Unknown component: ', name)))))(env_get(env, name)) if sx_truthy(starts_with_p(name, '~')) else (dispatch_html_form(name, expr, env) if sx_truthy(is_render_html_form(name)) else (render_to_html(expand_macro(env_get(env, name), args, env), env) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else render_value_to_html(trampoline(eval_expr(expr, env)), env))))))))(rest(expr)))(symbol_name(head))))(first(expr)))
|
||||
render_list_to_html = lambda expr, env: ('' if sx_truthy(empty_p(expr)) else (lambda head: (join('', map(lambda x: render_value_to_html(x, env), expr)) if sx_truthy((not sx_truthy((type_of(head) == 'symbol')))) else (lambda name: (lambda args: (join('', map(lambda x: render_to_html(x, env), args)) if sx_truthy((name == '<>')) else (join('', map(lambda x: sx_str(trampoline(eval_expr(x, env))), args)) if sx_truthy((name == 'raw!')) else (render_html_element(name, args, env) if sx_truthy(contains_p(HTML_TAGS, name)) else (render_html_island(env_get(env, name), args, env) if sx_truthy((starts_with_p(name, '~') if not sx_truthy(starts_with_p(name, '~')) else (env_has(env, name) if not sx_truthy(env_has(env, name)) else is_island(env_get(env, name))))) else ((lambda val: (render_html_component(val, args, env) if sx_truthy(is_component(val)) else (render_to_html(expand_macro(val, args, env), env) if sx_truthy(is_macro(val)) else error(sx_str('Unknown component: ', name)))))(env_get(env, name)) if sx_truthy(starts_with_p(name, '~')) else (dispatch_html_form(name, expr, env) if sx_truthy(is_render_html_form(name)) else (render_to_html(expand_macro(env_get(env, name), args, env), env) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else render_value_to_html(trampoline(eval_expr(expr, env)), env)))))))))(rest(expr)))(symbol_name(head))))(first(expr)))
|
||||
|
||||
# dispatch-html-form
|
||||
dispatch_html_form = lambda name, expr, env: ((lambda cond_val: (render_to_html(nth(expr, 2), env) if sx_truthy(cond_val) else (render_to_html(nth(expr, 3), env) if sx_truthy((len(expr) > 3)) else '')))(trampoline(eval_expr(nth(expr, 1), env))) if sx_truthy((name == 'if')) else (('' if sx_truthy((not sx_truthy(trampoline(eval_expr(nth(expr, 1), env))))) else join('', map(lambda i: render_to_html(nth(expr, i), env), range(2, len(expr))))) if sx_truthy((name == 'when')) else ((lambda branch: (render_to_html(branch, env) if sx_truthy(branch) else ''))(eval_cond(rest(expr), env)) if sx_truthy((name == 'cond')) else (render_to_html(trampoline(eval_expr(expr, env)), env) if sx_truthy((name == 'case')) else ((lambda local: join('', map(lambda i: render_to_html(nth(expr, i), local), range(2, len(expr)))))(process_bindings(nth(expr, 1), env)) if sx_truthy(((name == 'let') if sx_truthy((name == 'let')) else (name == 'let*'))) else (join('', map(lambda i: render_to_html(nth(expr, i), env), range(1, len(expr)))) if sx_truthy(((name == 'begin') if sx_truthy((name == 'begin')) else (name == 'do'))) else (_sx_begin(trampoline(eval_expr(expr, env)), '') if sx_truthy(is_definition_form(name)) else ((lambda f: (lambda coll: join('', map(lambda item: (render_lambda_html(f, [item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [item]), env)), coll)))(trampoline(eval_expr(nth(expr, 2), env))))(trampoline(eval_expr(nth(expr, 1), env))) if sx_truthy((name == 'map')) else ((lambda f: (lambda coll: join('', map_indexed(lambda i, item: (render_lambda_html(f, [i, item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [i, item]), env)), coll)))(trampoline(eval_expr(nth(expr, 2), env))))(trampoline(eval_expr(nth(expr, 1), env))) if sx_truthy((name == 'map-indexed')) else (render_to_html(trampoline(eval_expr(expr, env)), env) if sx_truthy((name == 'filter')) else ((lambda f: (lambda coll: join('', map(lambda item: (render_lambda_html(f, [item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [item]), env)), coll)))(trampoline(eval_expr(nth(expr, 2), env))))(trampoline(eval_expr(nth(expr, 1), env))) if sx_truthy((name == 'for-each')) else render_value_to_html(trampoline(eval_expr(expr, env)), env))))))))))))
|
||||
@@ -1214,6 +1291,12 @@ render_html_component = lambda comp, args, env: (lambda kwargs: (lambda children
|
||||
# render-html-element
|
||||
render_html_element = lambda tag, args, env: (lambda parsed: (lambda attrs: (lambda children: (lambda is_void: sx_str('<', tag, render_attrs(attrs), (' />' if sx_truthy(is_void) else sx_str('>', join('', map(lambda c: render_to_html(c, env), children)), '</', tag, '>'))))(contains_p(VOID_ELEMENTS, tag)))(nth(parsed, 1)))(first(parsed)))(parse_element_args(args, env))
|
||||
|
||||
# render-html-island
|
||||
render_html_island = lambda island, args, env: (lambda kwargs: (lambda children: _sx_begin(reduce(lambda state, arg: (lambda skip: (assoc(state, 'skip', False, 'i', (get(state, 'i') + 1)) if sx_truthy(skip) else ((lambda val: _sx_begin(_sx_dict_set(kwargs, keyword_name(arg), val), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(trampoline(eval_expr(nth(args, (get(state, 'i') + 1)), env))) if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((get(state, 'i') + 1) < len(args)))) else _sx_begin(_sx_append(children, arg), assoc(state, 'i', (get(state, 'i') + 1))))))(get(state, 'skip')), {'i': 0, 'skip': False}, args), (lambda local: (lambda island_name: _sx_begin(for_each(lambda p: _sx_dict_set(local, p, (dict_get(kwargs, p) if sx_truthy(dict_has(kwargs, p)) else NIL)), component_params(island)), (_sx_dict_set(local, 'children', make_raw_html(join('', map(lambda c: render_to_html(c, env), children)))) if sx_truthy(component_has_children(island)) else NIL), (lambda body_html: (lambda state_json: sx_str('<div data-sx-island="', escape_attr(island_name), '"', (sx_str(' data-sx-state="', escape_attr(state_json), '"') if sx_truthy(state_json) else ''), '>', body_html, '</div>'))(serialize_island_state(kwargs)))(render_to_html(component_body(island), local))))(component_name(island)))(env_merge(component_closure(island), env))))([]))({})
|
||||
|
||||
# serialize-island-state
|
||||
serialize_island_state = lambda kwargs: (NIL if sx_truthy(is_empty_dict(kwargs)) else json_serialize(kwargs))
|
||||
|
||||
|
||||
# === Transpiled from adapter-sx ===
|
||||
|
||||
@@ -1224,7 +1307,7 @@ render_to_sx = lambda expr, env: (lambda result: (result if sx_truthy((type_of(r
|
||||
aser = lambda expr, env: _sx_case(type_of(expr), [('number', lambda: expr), ('string', lambda: expr), ('boolean', lambda: expr), ('nil', lambda: NIL), ('symbol', lambda: (lambda name: (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))))(symbol_name(expr))), ('keyword', lambda: keyword_name(expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else aser_list(expr, env))), (None, lambda: expr)])
|
||||
|
||||
# aser-list
|
||||
aser_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: aser(x, env), expr) if sx_truthy((not sx_truthy((type_of(head) == 'symbol')))) else (lambda name: (aser_fragment(args, env) if sx_truthy((name == '<>')) else (aser_call(name, args, env) if sx_truthy(starts_with_p(name, '~')) else (aser_call(name, args, env) if sx_truthy(contains_p(HTML_TAGS, name)) else (aser_special(name, expr, env) if sx_truthy((is_special_form(name) if sx_truthy(is_special_form(name)) else is_ho_form(name))) else (aser(expand_macro(env_get(env, name), args, env), env) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (lambda f: (lambda evaled_args: (apply(f, evaled_args) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else (not sx_truthy(is_component(f)))))) else (trampoline(call_lambda(f, evaled_args, env)) if sx_truthy(is_lambda(f)) else (aser_call(sx_str('~', component_name(f)), args, env) if sx_truthy(is_component(f)) else error(sx_str('Not callable: ', inspect(f)))))))(map(lambda a: trampoline(eval_expr(a, env)), args)))(trampoline(eval_expr(head, env)))))))))(symbol_name(head))))(rest(expr)))(first(expr))
|
||||
aser_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: aser(x, env), expr) if sx_truthy((not sx_truthy((type_of(head) == 'symbol')))) else (lambda name: (aser_fragment(args, env) if sx_truthy((name == '<>')) else (aser_call(name, args, env) if sx_truthy(starts_with_p(name, '~')) else (aser_call(name, args, env) if sx_truthy(contains_p(HTML_TAGS, name)) else (aser_special(name, expr, env) if sx_truthy((is_special_form(name) if sx_truthy(is_special_form(name)) else is_ho_form(name))) else (aser(expand_macro(env_get(env, name), args, env), env) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (lambda f: (lambda evaled_args: (apply(f, evaled_args) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else ((not sx_truthy(is_component(f))) if not sx_truthy((not sx_truthy(is_component(f)))) else (not sx_truthy(is_island(f))))))) else (trampoline(call_lambda(f, evaled_args, env)) if sx_truthy(is_lambda(f)) else (aser_call(sx_str('~', component_name(f)), args, env) if sx_truthy(is_component(f)) else (aser_call(sx_str('~', component_name(f)), args, env) if sx_truthy(is_island(f)) else error(sx_str('Not callable: ', inspect(f))))))))(map(lambda a: trampoline(eval_expr(a, env)), args)))(trampoline(eval_expr(head, env)))))))))(symbol_name(head))))(rest(expr)))(first(expr))
|
||||
|
||||
# aser-fragment
|
||||
aser_fragment = lambda children, env: (lambda parts: ('' if sx_truthy(empty_p(parts)) else sx_str('(<> ', join(' ', map(serialize, parts)), ')')))(filter(lambda x: (not sx_truthy(is_nil(x))), map(lambda c: aser(c, env), children)))
|
||||
@@ -1233,61 +1316,77 @@ aser_fragment = lambda children, env: (lambda parts: ('' if sx_truthy(empty_p(pa
|
||||
aser_call = lambda name, args, env: (lambda parts: _sx_begin(reduce(lambda state, arg: (lambda skip: (assoc(state, 'skip', False, 'i', (get(state, 'i') + 1)) if sx_truthy(skip) else ((lambda val: _sx_begin((_sx_begin(_sx_append(parts, sx_str(':', keyword_name(arg))), _sx_append(parts, serialize(val))) if sx_truthy((not sx_truthy(is_nil(val)))) else NIL), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(aser(nth(args, (get(state, 'i') + 1)), env)) if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((get(state, 'i') + 1) < len(args)))) else (lambda val: _sx_begin((_sx_append(parts, serialize(val)) if sx_truthy((not sx_truthy(is_nil(val)))) else NIL), assoc(state, 'i', (get(state, 'i') + 1))))(aser(arg, env)))))(get(state, 'skip')), {'i': 0, 'skip': False}, args), sx_str('(', join(' ', parts), ')')))([name])
|
||||
|
||||
|
||||
# === Transpiled from deps (component dependency analysis) ===
|
||||
# === Transpiled from signals (reactive signal runtime) ===
|
||||
|
||||
# scan-refs
|
||||
scan_refs = lambda node: (lambda refs: _sx_begin(scan_refs_walk(node, refs), refs))([])
|
||||
# signal
|
||||
signal = lambda initial_value: make_signal(initial_value)
|
||||
|
||||
# scan-refs-walk
|
||||
scan_refs_walk = lambda node, refs: ((lambda name: ((_sx_append(refs, name) if sx_truthy((not sx_truthy(contains_p(refs, name)))) else NIL) if sx_truthy(starts_with_p(name, '~')) else NIL))(symbol_name(node)) if sx_truthy((type_of(node) == 'symbol')) else (for_each(lambda item: scan_refs_walk(item, refs), node) if sx_truthy((type_of(node) == 'list')) else (for_each(lambda key: scan_refs_walk(dict_get(node, key), refs), keys(node)) if sx_truthy((type_of(node) == 'dict')) else NIL)))
|
||||
# deref
|
||||
deref = lambda s: (s if sx_truthy((not sx_truthy(is_signal(s)))) else (lambda ctx: _sx_begin((_sx_begin(tracking_context_add_dep(ctx, s), signal_add_sub(s, tracking_context_notify_fn(ctx))) if sx_truthy(ctx) else NIL), signal_value(s)))(get_tracking_context()))
|
||||
|
||||
# transitive-deps-walk
|
||||
transitive_deps_walk = lambda n, seen, env: (_sx_begin(_sx_append(seen, n), (lambda val: (for_each(lambda ref: transitive_deps_walk(ref, seen, env), scan_refs(component_body(val))) if sx_truthy((type_of(val) == 'component')) else (for_each(lambda ref: transitive_deps_walk(ref, seen, env), scan_refs(macro_body(val))) if sx_truthy((type_of(val) == 'macro')) else NIL)))(env_get(env, n))) if sx_truthy((not sx_truthy(contains_p(seen, n)))) else NIL)
|
||||
# reset!
|
||||
reset_b = lambda s, value: ((lambda old: (_sx_begin(signal_set_value(s, value), notify_subscribers(s)) if sx_truthy((not sx_truthy(is_identical(old, value)))) else NIL))(signal_value(s)) if sx_truthy(is_signal(s)) else NIL)
|
||||
|
||||
# transitive-deps
|
||||
transitive_deps = lambda name, env: (lambda seen: (lambda key: _sx_begin(transitive_deps_walk(key, seen, env), filter(lambda x: (not sx_truthy((x == key))), seen)))((name if sx_truthy(starts_with_p(name, '~')) else sx_str('~', name))))([])
|
||||
# swap!
|
||||
swap_b = lambda s, f, *args: ((lambda old: (lambda new_val: (_sx_begin(signal_set_value(s, new_val), notify_subscribers(s)) if sx_truthy((not sx_truthy(is_identical(old, new_val)))) else NIL))(apply(f, cons(old, args))))(signal_value(s)) if sx_truthy(is_signal(s)) else NIL)
|
||||
|
||||
# compute-all-deps
|
||||
compute_all_deps = lambda env: for_each(lambda name: (lambda val: (component_set_deps(val, transitive_deps(name, env)) if sx_truthy((type_of(val) == 'component')) else NIL))(env_get(env, name)), env_components(env))
|
||||
# computed
|
||||
computed = lambda compute_fn: (lambda s: (lambda deps: (lambda compute_ctx: (lambda recompute: _sx_begin(recompute(), s))(_sx_fn(lambda : (
|
||||
for_each(lambda dep: signal_remove_sub(dep, recompute), signal_deps(s)),
|
||||
signal_set_deps(s, []),
|
||||
(lambda ctx: (lambda prev: _sx_begin(set_tracking_context(ctx), (lambda new_val: _sx_begin(set_tracking_context(prev), signal_set_deps(s, tracking_context_deps(ctx)), (lambda old: _sx_begin(signal_set_value(s, new_val), (notify_subscribers(s) if sx_truthy((not sx_truthy(is_identical(old, new_val)))) else NIL)))(signal_value(s))))(compute_fn())))(get_tracking_context()))(make_tracking_context(recompute))
|
||||
)[-1])))(NIL))([]))(make_signal(NIL))
|
||||
|
||||
# scan-components-from-source
|
||||
scan_components_from_source = lambda source: (lambda matches: map(lambda m: sx_str('~', m), matches))(regex_find_all('\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)', source))
|
||||
# effect
|
||||
def effect(effect_fn):
|
||||
_cells = {}
|
||||
_cells['deps'] = []
|
||||
_cells['disposed'] = False
|
||||
_cells['cleanup_fn'] = NIL
|
||||
run_effect = lambda : (_sx_begin((cleanup_fn() if sx_truthy(_cells['cleanup_fn']) else NIL), for_each(lambda dep: signal_remove_sub(dep, run_effect), _cells['deps']), _sx_cell_set(_cells, 'deps', []), (lambda ctx: (lambda prev: _sx_begin(set_tracking_context(ctx), (lambda result: _sx_begin(set_tracking_context(prev), _sx_cell_set(_cells, 'deps', tracking_context_deps(ctx)), (_sx_cell_set(_cells, 'cleanup_fn', result) if sx_truthy(is_callable(result)) else NIL)))(effect_fn())))(get_tracking_context()))(make_tracking_context(run_effect))) if sx_truthy((not sx_truthy(_cells['disposed']))) else NIL)
|
||||
run_effect()
|
||||
return _sx_fn(lambda : (
|
||||
_sx_cell_set(_cells, 'disposed', True),
|
||||
(cleanup_fn() if sx_truthy(_cells['cleanup_fn']) else NIL),
|
||||
for_each(lambda dep: signal_remove_sub(dep, run_effect), _cells['deps']),
|
||||
_sx_cell_set(_cells, 'deps', [])
|
||||
)[-1])
|
||||
|
||||
# components-needed
|
||||
components_needed = lambda page_source, env: (lambda direct: (lambda all_needed: _sx_begin(for_each(_sx_fn(lambda name: (
|
||||
(_sx_append(all_needed, name) if sx_truthy((not sx_truthy(contains_p(all_needed, name)))) else NIL),
|
||||
(lambda val: (lambda deps: for_each(lambda dep: (_sx_append(all_needed, dep) if sx_truthy((not sx_truthy(contains_p(all_needed, dep)))) else NIL), deps))((component_deps(val) if sx_truthy(((type_of(val) == 'component') if not sx_truthy((type_of(val) == 'component')) else (not sx_truthy(empty_p(component_deps(val)))))) else transitive_deps(name, env))))(env_get(env, name))
|
||||
)[-1]), direct), all_needed))([]))(scan_components_from_source(page_source))
|
||||
# *batch-depth*
|
||||
_batch_depth = 0
|
||||
|
||||
# page-component-bundle
|
||||
page_component_bundle = lambda page_source, env: components_needed(page_source, env)
|
||||
# *batch-queue*
|
||||
_batch_queue = []
|
||||
|
||||
# page-css-classes
|
||||
page_css_classes = lambda page_source, env: (lambda needed: (lambda classes: _sx_begin(for_each(lambda name: (lambda val: (for_each(lambda cls: (_sx_append(classes, cls) if sx_truthy((not sx_truthy(contains_p(classes, cls)))) else NIL), component_css_classes(val)) if sx_truthy((type_of(val) == 'component')) else NIL))(env_get(env, name)), needed), for_each(lambda cls: (_sx_append(classes, cls) if sx_truthy((not sx_truthy(contains_p(classes, cls)))) else NIL), scan_css_classes(page_source)), classes))([]))(components_needed(page_source, env))
|
||||
# batch
|
||||
def batch(thunk):
|
||||
_batch_depth = (_batch_depth + 1)
|
||||
thunk()
|
||||
_batch_depth = (_batch_depth - 1)
|
||||
return ((lambda queue: _sx_begin(_sx_cell_set(_cells, '_batch_queue', []), (lambda seen: (lambda pending: _sx_begin(for_each(lambda s: for_each(lambda sub: (_sx_begin(_sx_append(seen, sub), _sx_append(pending, sub)) if sx_truthy((not sx_truthy(contains_p(seen, sub)))) else NIL), signal_subscribers(s)), queue), for_each(lambda sub: sub(), pending)))([]))([])))(_batch_queue) if sx_truthy((_batch_depth == 0)) else NIL)
|
||||
|
||||
# scan-io-refs-walk
|
||||
scan_io_refs_walk = lambda node, io_names, refs: ((lambda name: ((_sx_append(refs, name) if sx_truthy((not sx_truthy(contains_p(refs, name)))) else NIL) if sx_truthy(contains_p(io_names, name)) else NIL))(symbol_name(node)) if sx_truthy((type_of(node) == 'symbol')) else (for_each(lambda item: scan_io_refs_walk(item, io_names, refs), node) if sx_truthy((type_of(node) == 'list')) else (for_each(lambda key: scan_io_refs_walk(dict_get(node, key), io_names, refs), keys(node)) if sx_truthy((type_of(node) == 'dict')) else NIL)))
|
||||
# notify-subscribers
|
||||
notify_subscribers = lambda s: ((_sx_append(_batch_queue, s) if sx_truthy((not sx_truthy(contains_p(_batch_queue, s)))) else NIL) if sx_truthy((_batch_depth > 0)) else flush_subscribers(s))
|
||||
|
||||
# scan-io-refs
|
||||
scan_io_refs = lambda node, io_names: (lambda refs: _sx_begin(scan_io_refs_walk(node, io_names, refs), refs))([])
|
||||
# flush-subscribers
|
||||
flush_subscribers = lambda s: for_each(lambda sub: sub(), signal_subscribers(s))
|
||||
|
||||
# transitive-io-refs-walk
|
||||
transitive_io_refs_walk = lambda n, seen, all_refs, env, io_names: (_sx_begin(_sx_append(seen, n), (lambda val: (_sx_begin(for_each(lambda ref: (_sx_append(all_refs, ref) if sx_truthy((not sx_truthy(contains_p(all_refs, ref)))) else NIL), scan_io_refs(component_body(val), io_names)), for_each(lambda dep: transitive_io_refs_walk(dep, seen, all_refs, env, io_names), scan_refs(component_body(val)))) if sx_truthy((type_of(val) == 'component')) else (_sx_begin(for_each(lambda ref: (_sx_append(all_refs, ref) if sx_truthy((not sx_truthy(contains_p(all_refs, ref)))) else NIL), scan_io_refs(macro_body(val), io_names)), for_each(lambda dep: transitive_io_refs_walk(dep, seen, all_refs, env, io_names), scan_refs(macro_body(val)))) if sx_truthy((type_of(val) == 'macro')) else NIL)))(env_get(env, n))) if sx_truthy((not sx_truthy(contains_p(seen, n)))) else NIL)
|
||||
# dispose-computed
|
||||
dispose_computed = lambda s: (_sx_begin(for_each(lambda dep: signal_remove_sub(dep, NIL), signal_deps(s)), signal_set_deps(s, [])) if sx_truthy(is_signal(s)) else NIL)
|
||||
|
||||
# transitive-io-refs
|
||||
transitive_io_refs = lambda name, env, io_names: (lambda all_refs: (lambda seen: (lambda key: _sx_begin(transitive_io_refs_walk(key, seen, all_refs, env, io_names), all_refs))((name if sx_truthy(starts_with_p(name, '~')) else sx_str('~', name))))([]))([])
|
||||
# *island-scope*
|
||||
_island_scope = NIL
|
||||
|
||||
# compute-all-io-refs
|
||||
compute_all_io_refs = lambda env, io_names: for_each(lambda name: (lambda val: (component_set_io_refs(val, transitive_io_refs(name, env, io_names)) if sx_truthy((type_of(val) == 'component')) else NIL))(env_get(env, name)), env_components(env))
|
||||
# with-island-scope
|
||||
def with_island_scope(scope_fn, body_fn):
|
||||
prev = _island_scope
|
||||
_island_scope = scope_fn
|
||||
result = body_fn()
|
||||
_island_scope = prev
|
||||
return result
|
||||
|
||||
# component-pure?
|
||||
component_pure_p = lambda name, env, io_names: empty_p(transitive_io_refs(name, env, io_names))
|
||||
|
||||
# render-target
|
||||
render_target = lambda name, env, io_names: (lambda key: (lambda val: ('server' if sx_truthy((not sx_truthy((type_of(val) == 'component')))) else (lambda affinity: ('server' if sx_truthy((affinity == 'server')) else ('client' if sx_truthy((affinity == 'client')) else ('server' if sx_truthy((not sx_truthy(component_pure_p(name, env, io_names)))) else 'client'))))(component_affinity(val))))(env_get(env, key)))((name if sx_truthy(starts_with_p(name, '~')) else sx_str('~', name)))
|
||||
|
||||
# page-render-plan
|
||||
page_render_plan = lambda page_source, env, io_names: (lambda needed: (lambda comp_targets: (lambda server_list: (lambda client_list: (lambda io_deps: _sx_begin(for_each(lambda name: (lambda target: _sx_begin(_sx_dict_set(comp_targets, name, target), (_sx_begin(_sx_append(server_list, name), for_each(lambda io_ref: (_sx_append(io_deps, io_ref) if sx_truthy((not sx_truthy(contains_p(io_deps, io_ref)))) else NIL), transitive_io_refs(name, env, io_names))) if sx_truthy((target == 'server')) else _sx_append(client_list, name))))(render_target(name, env, io_names)), needed), {'components': comp_targets, 'server': server_list, 'client': client_list, 'io-deps': io_deps}))([]))([]))([]))({}))(components_needed(page_source, env))
|
||||
# register-in-scope
|
||||
register_in_scope = lambda disposable: (_island_scope(disposable) if sx_truthy(_island_scope) else NIL)
|
||||
|
||||
|
||||
# =========================================================================
|
||||
|
||||
173
shared/sx/ref/test-signals.sx
Normal file
173
shared/sx/ref/test-signals.sx
Normal file
@@ -0,0 +1,173 @@
|
||||
;; ==========================================================================
|
||||
;; test-signals.sx — Tests for signals and reactive islands
|
||||
;;
|
||||
;; Requires: test-framework.sx loaded first.
|
||||
;; Modules tested: signals.sx, eval.sx (defisland)
|
||||
;;
|
||||
;; Note: Multi-expression lambda bodies are wrapped in (do ...) for
|
||||
;; compatibility with the hand-written evaluator which only supports
|
||||
;; single-expression lambda bodies.
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Signal creation and basic read/write
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "signal basics"
|
||||
(deftest "signal creates a reactive container"
|
||||
(let ((s (signal 42)))
|
||||
(assert-true (signal? s))
|
||||
(assert-equal 42 (deref s))))
|
||||
|
||||
(deftest "deref on non-signal passes through"
|
||||
(assert-equal 5 (deref 5))
|
||||
(assert-equal "hello" (deref "hello"))
|
||||
(assert-nil (deref nil)))
|
||||
|
||||
(deftest "reset! changes value"
|
||||
(let ((s (signal 0)))
|
||||
(reset! s 10)
|
||||
(assert-equal 10 (deref s))))
|
||||
|
||||
(deftest "reset! does not notify when value unchanged"
|
||||
(let ((s (signal 5))
|
||||
(count (signal 0)))
|
||||
(effect (fn () (do (deref s) (swap! count inc))))
|
||||
;; Effect runs once on creation → count=1
|
||||
(let ((c1 (deref count)))
|
||||
(reset! s 5) ;; same value — no notification
|
||||
(assert-equal c1 (deref count)))))
|
||||
|
||||
(deftest "swap! applies function to current value"
|
||||
(let ((s (signal 10)))
|
||||
(swap! s inc)
|
||||
(assert-equal 11 (deref s))))
|
||||
|
||||
(deftest "swap! passes extra args"
|
||||
(let ((s (signal 10)))
|
||||
(swap! s + 5)
|
||||
(assert-equal 15 (deref s)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Computed signals
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "computed"
|
||||
(deftest "computed derives initial value"
|
||||
(let ((a (signal 3))
|
||||
(b (signal 4))
|
||||
(sum (computed (fn () (+ (deref a) (deref b))))))
|
||||
(assert-equal 7 (deref sum))))
|
||||
|
||||
(deftest "computed updates when dependency changes"
|
||||
(let ((a (signal 2))
|
||||
(doubled (computed (fn () (* 2 (deref a))))))
|
||||
(assert-equal 4 (deref doubled))
|
||||
(reset! a 5)
|
||||
(assert-equal 10 (deref doubled))))
|
||||
|
||||
(deftest "computed chains"
|
||||
(let ((base (signal 1))
|
||||
(doubled (computed (fn () (* 2 (deref base)))))
|
||||
(quadrupled (computed (fn () (* 2 (deref doubled))))))
|
||||
(assert-equal 4 (deref quadrupled))
|
||||
(reset! base 3)
|
||||
(assert-equal 12 (deref quadrupled)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Effects
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "effects"
|
||||
(deftest "effect runs immediately"
|
||||
(let ((ran (signal false)))
|
||||
(effect (fn () (reset! ran true)))
|
||||
(assert-true (deref ran))))
|
||||
|
||||
(deftest "effect re-runs when dependency changes"
|
||||
(let ((source (signal "a"))
|
||||
(log (signal (list))))
|
||||
(effect (fn ()
|
||||
(swap! log (fn (l) (append l (deref source))))))
|
||||
;; Initial run logs "a"
|
||||
(assert-equal (list "a") (deref log))
|
||||
;; Change triggers re-run
|
||||
(reset! source "b")
|
||||
(assert-equal (list "a" "b") (deref log))))
|
||||
|
||||
(deftest "effect dispose stops tracking"
|
||||
(let ((source (signal 0))
|
||||
(count (signal 0)))
|
||||
(let ((dispose (effect (fn () (do
|
||||
(deref source)
|
||||
(swap! count inc))))))
|
||||
;; Effect ran once
|
||||
(assert-equal 1 (deref count))
|
||||
;; Trigger
|
||||
(reset! source 1)
|
||||
(assert-equal 2 (deref count))
|
||||
;; Dispose
|
||||
(dispose)
|
||||
;; Should NOT trigger
|
||||
(reset! source 2)
|
||||
(assert-equal 2 (deref count)))))
|
||||
|
||||
(deftest "effect cleanup runs before re-run"
|
||||
(let ((source (signal 0))
|
||||
(cleanups (signal 0)))
|
||||
(effect (fn () (do
|
||||
(deref source)
|
||||
(fn () (swap! cleanups inc))))) ;; return cleanup fn
|
||||
;; No cleanup yet (first run)
|
||||
(assert-equal 0 (deref cleanups))
|
||||
;; Change triggers cleanup of previous run
|
||||
(reset! source 1)
|
||||
(assert-equal 1 (deref cleanups)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Batch
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "batch"
|
||||
(deftest "batch defers notifications"
|
||||
(let ((a (signal 0))
|
||||
(b (signal 0))
|
||||
(run-count (signal 0)))
|
||||
(effect (fn () (do
|
||||
(deref a) (deref b)
|
||||
(swap! run-count inc))))
|
||||
;; Initial run
|
||||
(assert-equal 1 (deref run-count))
|
||||
;; Without batch: 2 writes → 2 effect runs
|
||||
;; With batch: 2 writes → 1 effect run
|
||||
(batch (fn () (do
|
||||
(reset! a 1)
|
||||
(reset! b 2))))
|
||||
;; Should be 2 (initial + 1 batched), not 3
|
||||
(assert-equal 2 (deref run-count)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; defisland
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "defisland"
|
||||
(deftest "defisland creates an island"
|
||||
(defisland ~test-island (&key value)
|
||||
(list "island" value))
|
||||
(assert-true (island? ~test-island)))
|
||||
|
||||
(deftest "island is callable like component"
|
||||
(defisland ~greeting (&key name)
|
||||
(str "Hello, " name "!"))
|
||||
(assert-equal "Hello, World!" (~greeting :name "World")))
|
||||
|
||||
(deftest "island accepts children"
|
||||
(defisland ~wrapper (&rest children)
|
||||
(list "wrap" children))
|
||||
(assert-equal (list "wrap" (list "a" "b"))
|
||||
(~wrapper "a" "b"))))
|
||||
@@ -21,7 +21,7 @@ sys.path.insert(0, _PROJECT)
|
||||
|
||||
from shared.sx.parser import parse_all
|
||||
from shared.sx.evaluator import _eval, _trampoline, _call_lambda
|
||||
from shared.sx.types import Symbol, Keyword, Lambda, NIL
|
||||
from shared.sx.types import Symbol, Keyword, Lambda, NIL, Component, Island
|
||||
|
||||
# --- Test state ---
|
||||
suite_stack: list[str] = []
|
||||
@@ -134,16 +134,134 @@ def render_html(sx_source):
|
||||
return result
|
||||
|
||||
|
||||
# --- Signal platform primitives ---
|
||||
# Implements the signal runtime platform interface for testing signals.sx
|
||||
|
||||
class Signal:
|
||||
"""A reactive signal container."""
|
||||
__slots__ = ("value", "subscribers", "deps")
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
self.subscribers = [] # list of callables
|
||||
self.deps = [] # list of Signal (for computed)
|
||||
|
||||
|
||||
class TrackingContext:
|
||||
"""Tracks signal dependencies during effect/computed evaluation."""
|
||||
__slots__ = ("notify_fn", "deps")
|
||||
|
||||
def __init__(self, notify_fn):
|
||||
self.notify_fn = notify_fn
|
||||
self.deps = []
|
||||
|
||||
|
||||
_tracking_context = [None] # mutable cell
|
||||
|
||||
|
||||
def _make_signal(value):
|
||||
s = Signal(value)
|
||||
return s
|
||||
|
||||
|
||||
def _signal_p(x):
|
||||
return isinstance(x, Signal)
|
||||
|
||||
|
||||
def _signal_value(s):
|
||||
return s.value
|
||||
|
||||
|
||||
def _signal_set_value(s, v):
|
||||
s.value = v
|
||||
return NIL
|
||||
|
||||
|
||||
def _signal_subscribers(s):
|
||||
return list(s.subscribers)
|
||||
|
||||
|
||||
def _signal_add_sub(s, fn):
|
||||
if fn not in s.subscribers:
|
||||
s.subscribers.append(fn)
|
||||
return NIL
|
||||
|
||||
|
||||
def _signal_remove_sub(s, fn):
|
||||
if fn in s.subscribers:
|
||||
s.subscribers.remove(fn)
|
||||
return NIL
|
||||
|
||||
|
||||
def _signal_deps(s):
|
||||
return list(s.deps)
|
||||
|
||||
|
||||
def _signal_set_deps(s, deps):
|
||||
s.deps = list(deps)
|
||||
return NIL
|
||||
|
||||
|
||||
def _set_tracking_context(ctx):
|
||||
_tracking_context[0] = ctx
|
||||
return NIL
|
||||
|
||||
|
||||
def _get_tracking_context():
|
||||
return _tracking_context[0] or NIL
|
||||
|
||||
|
||||
def _make_tracking_context(notify_fn):
|
||||
return TrackingContext(notify_fn)
|
||||
|
||||
|
||||
def _tracking_context_deps(ctx):
|
||||
if isinstance(ctx, TrackingContext):
|
||||
return ctx.deps
|
||||
return []
|
||||
|
||||
|
||||
def _tracking_context_add_dep(ctx, s):
|
||||
if isinstance(ctx, TrackingContext) and s not in ctx.deps:
|
||||
ctx.deps.append(s)
|
||||
return NIL
|
||||
|
||||
|
||||
def _tracking_context_notify_fn(ctx):
|
||||
if isinstance(ctx, TrackingContext):
|
||||
return ctx.notify_fn
|
||||
return NIL
|
||||
|
||||
|
||||
def _identical(a, b):
|
||||
return a is b
|
||||
|
||||
|
||||
def _island_p(x):
|
||||
return isinstance(x, Island)
|
||||
|
||||
|
||||
def _make_island(name, params, has_children, body, closure):
|
||||
return Island(
|
||||
name=name,
|
||||
params=list(params),
|
||||
has_children=has_children,
|
||||
body=body,
|
||||
closure=dict(closure) if isinstance(closure, dict) else {},
|
||||
)
|
||||
|
||||
|
||||
# --- Spec registry ---
|
||||
|
||||
SPECS = {
|
||||
"eval": {"file": "test-eval.sx", "needs": []},
|
||||
"parser": {"file": "test-parser.sx", "needs": ["sx-parse"]},
|
||||
"router": {"file": "test-router.sx", "needs": []},
|
||||
"render": {"file": "test-render.sx", "needs": ["render-html"]},
|
||||
"deps": {"file": "test-deps.sx", "needs": []},
|
||||
"engine": {"file": "test-engine.sx", "needs": []},
|
||||
"eval": {"file": "test-eval.sx", "needs": []},
|
||||
"parser": {"file": "test-parser.sx", "needs": ["sx-parse"]},
|
||||
"router": {"file": "test-router.sx", "needs": []},
|
||||
"render": {"file": "test-render.sx", "needs": ["render-html"]},
|
||||
"deps": {"file": "test-deps.sx", "needs": []},
|
||||
"engine": {"file": "test-engine.sx", "needs": []},
|
||||
"orchestration": {"file": "test-orchestration.sx", "needs": []},
|
||||
"signals": {"file": "test-signals.sx", "needs": ["make-signal"]},
|
||||
}
|
||||
|
||||
REF_DIR = os.path.join(_HERE, "..", "ref")
|
||||
@@ -187,6 +305,31 @@ env = {
|
||||
"inc": lambda n: n + 1,
|
||||
# Component accessor for affinity (Phase 7)
|
||||
"component-affinity": lambda c: getattr(c, 'affinity', 'auto'),
|
||||
# Signal platform primitives
|
||||
"make-signal": _make_signal,
|
||||
"signal?": _signal_p,
|
||||
"signal-value": _signal_value,
|
||||
"signal-set-value!": _signal_set_value,
|
||||
"signal-subscribers": _signal_subscribers,
|
||||
"signal-add-sub!": _signal_add_sub,
|
||||
"signal-remove-sub!": _signal_remove_sub,
|
||||
"signal-deps": _signal_deps,
|
||||
"signal-set-deps!": _signal_set_deps,
|
||||
"set-tracking-context!": _set_tracking_context,
|
||||
"get-tracking-context": _get_tracking_context,
|
||||
"make-tracking-context": _make_tracking_context,
|
||||
"tracking-context-deps": _tracking_context_deps,
|
||||
"tracking-context-add-dep!": _tracking_context_add_dep,
|
||||
"tracking-context-notify-fn": _tracking_context_notify_fn,
|
||||
"identical?": _identical,
|
||||
# Island platform primitives
|
||||
"island?": _island_p,
|
||||
"make-island": _make_island,
|
||||
"component-name": lambda c: getattr(c, 'name', ''),
|
||||
"component-params": lambda c: list(getattr(c, 'params', [])),
|
||||
"component-body": lambda c: getattr(c, 'body', NIL),
|
||||
"component-closure": lambda c: dict(getattr(c, 'closure', {})),
|
||||
"component-has-children?": lambda c: getattr(c, 'has_children', False),
|
||||
}
|
||||
|
||||
|
||||
@@ -412,6 +555,171 @@ def _load_forms_from_bootstrap(env):
|
||||
eval_file("forms.sx", env)
|
||||
|
||||
|
||||
def _load_signals(env):
|
||||
"""Load signals.sx spec — defines signal, deref, reset!, swap!, etc.
|
||||
|
||||
The hand-written evaluator doesn't support &rest in define/fn,
|
||||
so we override swap! with a native implementation after loading.
|
||||
"""
|
||||
# callable? is needed by effect (to check if return value is cleanup fn)
|
||||
env["callable?"] = lambda x: callable(x) or isinstance(x, Lambda)
|
||||
eval_file("signals.sx", env)
|
||||
|
||||
# Override signal functions that need to call Lambda subscribers.
|
||||
# The hand-written evaluator's Lambda objects can't be called directly
|
||||
# from Python — they need _call_lambda. So we provide native versions
|
||||
# of functions that bridge native→Lambda calls.
|
||||
|
||||
def _call_sx_fn(fn, args):
|
||||
"""Call an SX function (Lambda or native) from Python."""
|
||||
if isinstance(fn, Lambda):
|
||||
return _trampoline(_call_lambda(fn, list(args), env))
|
||||
if callable(fn):
|
||||
return fn(*args)
|
||||
return NIL
|
||||
|
||||
def _flush_subscribers(s):
|
||||
for sub in list(s.subscribers):
|
||||
_call_sx_fn(sub, [])
|
||||
return NIL
|
||||
|
||||
def _notify_subscribers(s):
|
||||
batch_depth = env.get("*batch-depth*", 0)
|
||||
if batch_depth and batch_depth > 0:
|
||||
batch_queue = env.get("*batch-queue*", [])
|
||||
if s not in batch_queue:
|
||||
batch_queue.append(s)
|
||||
return NIL
|
||||
_flush_subscribers(s)
|
||||
return NIL
|
||||
env["notify-subscribers"] = _notify_subscribers
|
||||
env["flush-subscribers"] = _flush_subscribers
|
||||
|
||||
def _reset_bang(s, value):
|
||||
if not isinstance(s, Signal):
|
||||
return NIL
|
||||
old = s.value
|
||||
if old is not value:
|
||||
s.value = value
|
||||
_notify_subscribers(s)
|
||||
return NIL
|
||||
env["reset!"] = _reset_bang
|
||||
|
||||
def _swap_bang(s, f, *args):
|
||||
if not isinstance(s, Signal):
|
||||
return NIL
|
||||
old = s.value
|
||||
all_args = [old] + list(args)
|
||||
new_val = _call_sx_fn(f, all_args)
|
||||
if old is not new_val:
|
||||
s.value = new_val
|
||||
_notify_subscribers(s)
|
||||
return NIL
|
||||
env["swap!"] = _swap_bang
|
||||
|
||||
def _computed(compute_fn):
|
||||
s = Signal(NIL)
|
||||
|
||||
def recompute():
|
||||
# Unsubscribe from old deps
|
||||
for dep in s.deps:
|
||||
if recompute in dep.subscribers:
|
||||
dep.subscribers.remove(recompute)
|
||||
s.deps = []
|
||||
|
||||
# Create tracking context
|
||||
ctx = TrackingContext(recompute)
|
||||
prev = _tracking_context[0]
|
||||
_tracking_context[0] = ctx
|
||||
|
||||
new_val = _call_sx_fn(compute_fn, [])
|
||||
|
||||
_tracking_context[0] = prev
|
||||
s.deps = list(ctx.deps)
|
||||
|
||||
old = s.value
|
||||
s.value = new_val
|
||||
if old is not new_val:
|
||||
_flush_subscribers(s)
|
||||
|
||||
recompute()
|
||||
return s
|
||||
env["computed"] = _computed
|
||||
|
||||
def _effect(effect_fn):
|
||||
deps = []
|
||||
disposed = [False]
|
||||
cleanup_fn = [None]
|
||||
|
||||
def run_effect():
|
||||
if disposed[0]:
|
||||
return NIL
|
||||
# Run previous cleanup
|
||||
if cleanup_fn[0]:
|
||||
_call_sx_fn(cleanup_fn[0], [])
|
||||
cleanup_fn[0] = None
|
||||
|
||||
# Unsubscribe from old deps
|
||||
for dep in deps:
|
||||
if run_effect in dep.subscribers:
|
||||
dep.subscribers.remove(run_effect)
|
||||
deps.clear()
|
||||
|
||||
# Track new deps
|
||||
ctx = TrackingContext(run_effect)
|
||||
prev = _tracking_context[0]
|
||||
_tracking_context[0] = ctx
|
||||
|
||||
result = _call_sx_fn(effect_fn, [])
|
||||
|
||||
_tracking_context[0] = prev
|
||||
deps.extend(ctx.deps)
|
||||
|
||||
# If effect returns a callable, it's cleanup
|
||||
if callable(result) or isinstance(result, Lambda):
|
||||
cleanup_fn[0] = result
|
||||
|
||||
return NIL
|
||||
|
||||
run_effect()
|
||||
|
||||
def dispose():
|
||||
disposed[0] = True
|
||||
if cleanup_fn[0]:
|
||||
_call_sx_fn(cleanup_fn[0], [])
|
||||
for dep in deps:
|
||||
if run_effect in dep.subscribers:
|
||||
dep.subscribers.remove(run_effect)
|
||||
deps.clear()
|
||||
return NIL
|
||||
|
||||
return dispose
|
||||
env["effect"] = _effect
|
||||
|
||||
def _batch(thunk):
|
||||
depth = env.get("*batch-depth*", 0)
|
||||
env["*batch-depth*"] = depth + 1
|
||||
_call_sx_fn(thunk, [])
|
||||
env["*batch-depth*"] = env["*batch-depth*"] - 1
|
||||
if env["*batch-depth*"] == 0:
|
||||
queue = env.get("*batch-queue*", [])
|
||||
env["*batch-queue*"] = []
|
||||
# Collect unique subscribers across all queued signals
|
||||
seen = set()
|
||||
pending = []
|
||||
for s in queue:
|
||||
for sub in s.subscribers:
|
||||
sub_id = id(sub)
|
||||
if sub_id not in seen:
|
||||
seen.add(sub_id)
|
||||
pending.append(sub)
|
||||
# Notify each unique subscriber exactly once
|
||||
for sub in pending:
|
||||
_call_sx_fn(sub, [])
|
||||
return NIL
|
||||
env["batch"] = _batch
|
||||
|
||||
|
||||
def main():
|
||||
global passed, failed, test_num
|
||||
|
||||
@@ -457,6 +765,8 @@ def main():
|
||||
_load_engine_from_bootstrap(env)
|
||||
if spec_name == "orchestration":
|
||||
_load_orchestration(env)
|
||||
if spec_name == "signals":
|
||||
_load_signals(env)
|
||||
|
||||
print(f"# --- {spec_name} ---")
|
||||
eval_file(spec["file"], env)
|
||||
|
||||
@@ -189,6 +189,31 @@ class Component:
|
||||
return f"<Component ~{self.name}({', '.join(self.params)})>"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Island
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class Island:
|
||||
"""A reactive UI component defined via ``(defisland ~name (&key ...) body)``.
|
||||
|
||||
Islands are like components but create a reactive boundary. Inside an
|
||||
island, signals are tracked — deref subscribes DOM nodes to signals.
|
||||
On the server, islands render as static HTML with hydration attributes.
|
||||
"""
|
||||
name: str
|
||||
params: list[str]
|
||||
has_children: bool
|
||||
body: Any
|
||||
closure: dict[str, Any] = field(default_factory=dict)
|
||||
css_classes: set[str] = field(default_factory=set)
|
||||
deps: set[str] = field(default_factory=set)
|
||||
io_refs: set[str] = field(default_factory=set)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Island ~{self.name}({', '.join(self.params)})>"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HandlerDef
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -355,4 +380,4 @@ class _ShiftSignal(BaseException):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# An s-expression value after evaluation
|
||||
SExp = int | float | str | bool | Symbol | Keyword | Lambda | Macro | Component | Continuation | HandlerDef | RelationDef | PageDef | QueryDef | ActionDef | list | dict | _Nil | None
|
||||
SExp = int | float | str | bool | Symbol | Keyword | Lambda | Macro | Component | Island | Continuation | HandlerDef | RelationDef | PageDef | QueryDef | ActionDef | list | dict | _Nil | None
|
||||
|
||||
Reference in New Issue
Block a user