Replace invoke with cek-call in reactive island primitives

All signal operations (computed, effect, batch, etc.) now dispatch
function calls through cek-call, which routes SX lambdas via cek-run
and native callables via apply. This replaces the invoke shim.

Key changes:
- cek.sx: add cek-call (defined before reactive-shift-deref), replace
  invoke in subscriber disposal and ReactiveResetFrame handler
- signals.sx: replace all 11 invoke calls with cek-call
- js.sx: fix octal escape in js-quote-string (char-from-code 0)
- platform_js.py: fix JS append to match Python (list concat semantics),
  add Continuation type guard in PLATFORM_CEK_JS, add scheduleIdle
  safety check, module ordering (cek before signals)
- platform_py.py: fix ident-char regex (remove [ ] from valid chars),
  module ordering (cek before signals)
- run_js_sx.py: emit PLATFORM_CEK_JS before transpiled spec files
- page-functions.sx: add cek and provide page functions for SX URLs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-14 10:11:48 +00:00
parent 30d9d4aa4c
commit 455e48df07
20 changed files with 911 additions and 600 deletions

View File

@@ -760,7 +760,16 @@ class PyEmitter:
self._current_cell_vars = old_cells | nested_set_vars
if is_async:
self._in_async = True
self._emit_body_stmts(body, lines, indent + 1)
# Self-tail-recursive 0-param functions: wrap body in while True
if (not param_names and not is_async
and self._has_self_tail_call(body, name)):
lines.append(f"{pad} while True:")
old_loop = getattr(self, '_current_loop_name', None)
self._current_loop_name = name
self._emit_body_stmts(body, lines, indent + 2)
self._current_loop_name = old_loop
else:
self._emit_body_stmts(body, lines, indent + 1)
self._current_cell_vars = old_cells
self._in_async = old_async
return "\n".join(lines)
@@ -799,14 +808,20 @@ class PyEmitter:
Handles let as local variable declarations, and returns the last
expression. Control flow in tail position (if, cond, case, when)
is flattened to if/elif statements with returns in each branch.
Detects self-tail-recursive (define name (fn () ...)) followed by
(name) and emits as while True loop instead of recursive def.
"""
pad = " " * indent
for i, expr in enumerate(body):
is_last = (i == len(body) - 1)
idx = 0
while idx < len(body):
expr = body[idx]
is_last = (idx == len(body) - 1)
if isinstance(expr, list) and expr and isinstance(expr[0], Symbol):
name = expr[0].name
if name in ("let", "let*"):
self._emit_let_as_stmts(expr, lines, indent, is_last)
idx += 1
continue
if name in ("do", "begin"):
sub_body = expr[1:]
@@ -815,15 +830,172 @@ class PyEmitter:
else:
for sub in sub_body:
lines.append(self.emit_statement(sub, indent))
idx += 1
continue
# Detect self-tail-recursive loop pattern:
# (define loop-name (fn () body...))
# (loop-name)
# Emit as: while True: <body with self-calls as continue>
if (name == "define" and not is_last
and idx + 1 < len(body)):
loop_info = self._detect_tail_loop(expr, body[idx + 1])
if loop_info:
loop_name, fn_body = loop_info
remaining = body[idx + 2:]
# Only optimize if the function isn't called again later
if not self._name_in_exprs(loop_name, remaining):
self._emit_while_loop(loop_name, fn_body, lines, indent)
# Skip the invocation; emit remaining body
for j, rem in enumerate(remaining):
if j == len(remaining) - 1:
self._emit_return_expr(rem, lines, indent)
else:
self._emit_stmt_recursive(rem, lines, indent)
return
if is_last:
self._emit_return_expr(expr, lines, indent)
else:
self._emit_stmt_recursive(expr, lines, indent)
idx += 1
def _detect_tail_loop(self, define_expr, next_expr):
"""Detect pattern: (define name (fn () body...)) followed by (name).
Returns (loop_name, fn_body) if tail-recursive, else None.
The function must have 0 params and body must end with self-call
in all tail positions.
"""
# Extract name and fn from define
dname = define_expr[1].name if isinstance(define_expr[1], Symbol) else None
if not dname:
return None
# Skip :effects annotation
if (len(define_expr) >= 5 and isinstance(define_expr[2], Keyword)
and define_expr[2].name == "effects"):
val_expr = define_expr[4]
else:
val_expr = define_expr[2] if len(define_expr) > 2 else None
if not (isinstance(val_expr, list) and val_expr
and isinstance(val_expr[0], Symbol)
and val_expr[0].name in ("fn", "lambda")):
return None
params = val_expr[1]
if not isinstance(params, list) or len(params) != 0:
return None # Must be 0-param function
fn_body = val_expr[2:]
# Check next expression is (name) — invocation
if not (isinstance(next_expr, list) and len(next_expr) == 1
and isinstance(next_expr[0], Symbol)
and next_expr[0].name == dname):
return None
# Check that fn_body has self-call in tail position(s)
if not self._has_self_tail_call(fn_body, dname):
return None
return (dname, fn_body)
def _has_self_tail_call(self, body, name):
"""Check if body is safe for while-loop optimization.
Returns True only when ALL tail positions are either:
- self-calls (name) → will become continue
- nil/void returns → will become break
- error() calls → raise, don't return
- when blocks → implicit nil else is fine
No tail position may return a computed value, since while-loop
break discards return values.
"""
if not body:
return False
last = body[-1]
# Non-list terminal: nil is ok, anything else is a value return
if not isinstance(last, list) or not last:
return (last is None or last is SX_NIL
or (isinstance(last, Symbol) and last.name == "nil"))
head = last[0] if isinstance(last[0], Symbol) else None
if not head:
return False
# Direct self-call in tail position
if head.name == name and len(last) == 1:
return True
# error() — raises, safe
if head.name == "error":
return True
# if — ALL branches must be safe
if head.name == "if":
then_ok = self._has_self_tail_call(
[last[2]] if len(last) > 2 else [None], name)
else_ok = self._has_self_tail_call(
[last[3]] if len(last) > 3 else [None], name)
return then_ok and else_ok
# do/begin — check last expression
if head.name in ("do", "begin"):
return self._has_self_tail_call(last[1:], name)
# when — body must be safe (implicit nil else is ok)
if head.name == "when":
return self._has_self_tail_call(last[2:], name)
# let/let* — check body (skip bindings)
if head.name in ("let", "let*"):
return self._has_self_tail_call(last[2:], name)
# cond — ALL branches must be safe
if head.name == "cond":
clauses = last[1:]
is_scheme = (
all(isinstance(c, list) and len(c) == 2 for c in clauses)
and not any(isinstance(c, Keyword) for c in clauses)
)
if is_scheme:
for clause in clauses:
if not self._has_self_tail_call([clause[1]], name):
return False
return True
else:
i = 0
while i < len(clauses) - 1:
if not self._has_self_tail_call([clauses[i + 1]], name):
return False
i += 2
return True
return False
def _name_in_exprs(self, name, exprs):
"""Check if a symbol name appears anywhere in a list of expressions."""
for expr in exprs:
if isinstance(expr, Symbol) and expr.name == name:
return True
if isinstance(expr, list):
if self._name_in_exprs(name, expr):
return True
return False
def _emit_while_loop(self, loop_name, fn_body, lines, indent):
"""Emit a self-tail-recursive function body as a while True loop."""
pad = " " * indent
lines.append(f"{pad}while True:")
# Track the loop name so _emit_return_expr can emit 'continue'
old_loop = getattr(self, '_current_loop_name', None)
self._current_loop_name = loop_name
self._emit_body_stmts(fn_body, lines, indent + 1)
self._current_loop_name = old_loop
def _emit_nil_return(self, lines: list, indent: int) -> None:
"""Emit 'return NIL' or 'break' depending on while-loop context."""
pad = " " * indent
if getattr(self, '_current_loop_name', None):
lines.append(f"{pad}break")
else:
lines.append(f"{pad}return NIL")
def _emit_return_expr(self, expr, lines: list, indent: int) -> None:
"""Emit an expression in return position, flattening control flow."""
pad = " " * indent
# Inside a while loop (self-tail-recursive define optimization):
# self-call → continue
loop_name = getattr(self, '_current_loop_name', None)
if loop_name:
if (isinstance(expr, list) and len(expr) == 1
and isinstance(expr[0], Symbol) and expr[0].name == loop_name):
lines.append(f"{pad}continue")
return
if isinstance(expr, list) and expr and isinstance(expr[0], Symbol):
name = expr[0].name
if name == "if":
@@ -845,11 +1017,17 @@ class PyEmitter:
self._emit_body_stmts(expr[1:], lines, indent)
return
if name == "for-each":
# for-each in return position: emit as statement, return NIL
# for-each in return position: emit as statement, then return/break
lines.append(self._emit_for_each_stmt(expr, indent))
lines.append(f"{pad}return NIL")
self._emit_nil_return(lines, indent)
return
lines.append(f"{pad}return {self.emit(expr)}")
if loop_name:
emitted = self.emit(expr)
if emitted != "NIL":
lines.append(f"{pad}{emitted}")
lines.append(f"{pad}break")
else:
lines.append(f"{pad}return {self.emit(expr)}")
def _emit_if_return(self, expr, lines: list, indent: int) -> None:
"""Emit if as statement with returns in each branch."""
@@ -860,7 +1038,7 @@ class PyEmitter:
lines.append(f"{pad}else:")
self._emit_return_expr(expr[3], lines, indent + 1)
else:
lines.append(f"{pad}return NIL")
self._emit_nil_return(lines, indent)
def _emit_when_return(self, expr, lines: list, indent: int) -> None:
"""Emit when as statement with return in body, else return NIL."""
@@ -873,7 +1051,7 @@ class PyEmitter:
for b in body_parts[:-1]:
lines.append(self.emit_statement(b, indent + 1))
self._emit_return_expr(body_parts[-1], lines, indent + 1)
lines.append(f"{pad}return NIL")
self._emit_nil_return(lines, indent)
def _emit_cond_return(self, expr, lines: list, indent: int) -> None:
"""Emit cond as if/elif/else with returns in each branch."""
@@ -915,7 +1093,7 @@ class PyEmitter:
self._emit_return_expr(body, lines, indent + 1)
i += 2
if not has_else:
lines.append(f"{pad}return NIL")
self._emit_nil_return(lines, indent)
def _emit_case_return(self, expr, lines: list, indent: int) -> None:
"""Emit case as if/elif/else with returns in each branch."""
@@ -940,7 +1118,7 @@ class PyEmitter:
self._emit_return_expr(body, lines, indent + 1)
i += 2
if not has_else:
lines.append(f"{pad}return NIL")
self._emit_nil_return(lines, indent)
def _emit_let_as_stmts(self, expr, lines: list, indent: int, is_last: bool) -> None:
"""Emit a let expression as local variable declarations."""

View File

@@ -20,17 +20,21 @@ logger = logging.getLogger("sx.boundary_parser")
# Allow standalone use (from bootstrappers) or in-project imports
try:
from shared.sx.parser import parse_all
from shared.sx.types import Symbol, Keyword, NIL as SX_NIL
except ImportError:
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
sys.path.insert(0, _PROJECT)
from shared.sx.parser import parse_all
from shared.sx.types import Symbol, Keyword, NIL as SX_NIL
def _get_parse_all():
"""Lazy import to avoid circular dependency when parser.py loads sx_ref.py."""
from shared.sx.parser import parse_all
return parse_all
def _ref_dir() -> str:
return os.path.dirname(os.path.abspath(__file__))
@@ -81,7 +85,7 @@ def _extract_declarations(
Returns (io_names, {service: helper_names}).
"""
exprs = parse_all(source)
exprs = _get_parse_all()(source)
io_names: set[str] = set()
helpers: dict[str, set[str]] = {}
@@ -144,7 +148,7 @@ def parse_primitives_sx() -> frozenset[str]:
def parse_primitives_by_module() -> dict[str, frozenset[str]]:
"""Parse primitives.sx and return primitives grouped by module."""
source = _read_file("primitives.sx")
exprs = parse_all(source)
exprs = _get_parse_all()(source)
modules: dict[str, set[str]] = {}
current_module = "_unscoped"
@@ -204,7 +208,7 @@ def parse_primitive_param_types() -> dict[str, dict]:
type of the &rest parameter (or None if no &rest, or None if untyped &rest).
"""
source = _read_file("primitives.sx")
exprs = parse_all(source)
exprs = _get_parse_all()(source)
result: dict[str, dict] = {}
for expr in exprs:
@@ -293,7 +297,7 @@ def parse_boundary_effects() -> dict[str, list[str]]:
Pure primitives from primitives.sx are not included (they have no effects).
"""
source = _read_file("boundary.sx")
exprs = parse_all(source)
exprs = _get_parse_all()(source)
result: dict[str, list[str]] = {}
_DECL_FORMS = {
@@ -338,7 +342,7 @@ def parse_boundary_effects() -> dict[str, list[str]]:
def parse_boundary_types() -> frozenset[str]:
"""Parse boundary.sx and return the declared boundary type names."""
source = _read_file("boundary.sx")
exprs = parse_all(source)
exprs = _get_parse_all()(source)
for expr in exprs:
if (isinstance(expr, list) and len(expr) >= 2
and isinstance(expr[0], Symbol)

View File

@@ -407,6 +407,16 @@
(first args) env
(kont-push (make-deref-frame env) kont))))
;; cek-call — call a function via CEK (replaces invoke)
(define cek-call
(fn (f args)
(let ((a (if (nil? args) (list) args)))
(cond
(nil? f) nil
(lambda? f) (cek-run (continue-with-call f a (dict) a (list)))
(callable? f) (apply f a)
:else nil))))
;; reactive-shift-deref: the heart of deref-as-shift
;; When deref encounters a signal inside a reactive-reset boundary,
;; capture the continuation up to the reactive-reset as the subscriber.
@@ -422,7 +432,7 @@
(let ((subscriber
(fn ()
;; Dispose previous nested subscribers
(for-each (fn (d) (invoke d)) sub-disposers)
(for-each (fn (d) (cek-call d nil)) sub-disposers)
(set! sub-disposers (list))
;; Re-invoke: push fresh ReactiveResetFrame (first-render=false)
(let ((new-reset (make-reactive-reset-frame env update-fn false))
@@ -440,7 +450,7 @@
(register-in-scope
(fn ()
(signal-remove-sub! sig subscriber)
(for-each (fn (d) (invoke d)) sub-disposers)))
(for-each (fn (d) (cek-call d nil)) sub-disposers)))
;; Initial render: value flows through captured frames + reset (first-render=true)
;; so the full expression completes normally
(let ((initial-kont (concat captured-frames
@@ -782,7 +792,7 @@
(first? (get frame "first-render")))
;; On re-render (not first), call update-fn with new value
(when (and update-fn (not first?))
(invoke update-fn value))
(cek-call update-fn (list value)))
(make-cek-value value env rest-k))
;; --- ScopeFrame: body result ---

View File

@@ -655,7 +655,7 @@
(fn ((s :as string))
(str "\""
(replace (replace (replace (replace (replace (replace
s "\\" "\\\\") "\"" "\\\"") "\n" "\\n") "\r" "\\r") "\t" "\\t") "\0" "\\0")
s "\\" "\\\\") "\"" "\\\"") "\n" "\\n") "\r" "\\r") "\t" "\\t") (char-from-code 0) "\\u0000")
"\"")))

View File

@@ -25,6 +25,7 @@
;; comment → ';' to end of line (discarded)
;;
;; Quote sugar:
;; 'expr → (quote expr)
;; `expr → (quasiquote expr)
;; ,expr → (unquote expr)
;; ,@expr → (splice-unquote expr)
@@ -267,6 +268,11 @@
(= ch ":")
(read-keyword)
;; Quote sugar
(= ch "'")
(do (set! pos (inc pos))
(list (make-symbol "quote") (read-expr)))
;; Quasiquote sugar
(= ch "`")
(do (set! pos (inc pos))
@@ -395,7 +401,7 @@
;; True for: a-z A-Z _ ~ * + - > < = / ! ? &
;;
;; (ident-char? ch) → boolean
;; True for: ident-start chars plus: 0-9 . : / [ ] # ,
;; True for: ident-start chars plus: 0-9 . : / # ,
;;
;; Constructors (provided by the SX runtime):
;; (make-symbol name) → Symbol value

View File

@@ -52,7 +52,7 @@ SPEC_MODULES = {
# Explicit ordering for spec modules with dependencies.
# Modules listed here are emitted in this order; any not listed use alphabetical.
SPEC_MODULE_ORDER = ["deps", "frames", "page-helpers", "router", "signals", "cek"]
SPEC_MODULE_ORDER = ["deps", "frames", "page-helpers", "router", "cek", "signals"]
EXTENSION_NAMES = {"continuations"}
@@ -1004,7 +1004,7 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
PRIMITIVES["rest"] = function(c) { if (c && typeof c.slice !== "function") { console.error("[sx-debug] rest called on non-sliceable:", typeof c, c, new Error().stack); return []; } return c ? c.slice(1) : []; };
PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; };
PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); };
PRIMITIVES["append"] = function(c, x) { return (c || []).concat([x]); };
PRIMITIVES["append"] = function(c, x) { return (c || []).concat(Array.isArray(x) ? x : [x]); };
PRIMITIVES["append!"] = function(arr, x) { arr.push(x); return arr; };
PRIMITIVES["chunk-every"] = function(c, n) {
var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r;
@@ -1487,6 +1487,14 @@ PLATFORM_CEK_JS = '''
// Platform: CEK module — explicit CEK machine
// =========================================================================
// Continuation type (needed by CEK even without the tree-walk shift/reset extension)
if (typeof Continuation === "undefined") {
function Continuation(fn) { this.fn = fn; }
Continuation.prototype._continuation = true;
Continuation.prototype.call = function(value) { return this.fn(value !== undefined ? value : NIL); };
PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; };
}
// Standalone aliases for primitives used by cek.sx / frames.sx
var inc = PRIMITIVES["inc"];
var dec = PRIMITIVES["dec"];
@@ -1608,10 +1616,10 @@ PLATFORM_PARSER_JS = r"""
// =========================================================================
// Character classification derived from the grammar:
// ident-start → [a-zA-Z_~*+\-><=/!?&]
// ident-char → ident-start + [0-9.:\/\[\]#,]
// ident-char → ident-start + [0-9.:\/\#,]
var _identStartRe = /[a-zA-Z_~*+\-><=/!?&]/;
var _identCharRe = /[a-zA-Z0-9_~*+\-><=/!?.:&/\[\]#,]/;
var _identCharRe = /[a-zA-Z0-9_~*+\-><=/!?.:&/#,]/;
function isIdentStart(ch) { return _identStartRe.test(ch); }
function isIdentChar(ch) { return _identCharRe.test(ch); }
@@ -2436,6 +2444,10 @@ PLATFORM_ORCHESTRATION_JS = """
}
function scheduleIdle(fn) {
var cb = _wrapSxFn(fn);
if (typeof cb !== "function") {
console.error("[sx-ref] scheduleIdle: callback not callable, fn type:", typeof fn, "fn:", fn, "_lambda:", fn && fn._lambda);
return;
}
if (typeof requestIdleCallback !== "undefined") requestIdleCallback(cb);
else setTimeout(cb, 0);
}
@@ -2525,8 +2537,12 @@ PLATFORM_ORCHESTRATION_JS = """
e.preventDefault();
// Re-read href from element at click time (not closed-over value)
var liveHref = el.getAttribute("href") || _href;
console.log("[sx-debug] bindBoostLink click:", liveHref, "el:", el.tagName, el.textContent.slice(0,30));
executeRequest(el, { method: "GET", url: liveHref }).then(function() {
console.log("[sx-debug] boost fetch OK, pushState:", liveHref);
try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {}
}).catch(function(err) {
console.error("[sx-debug] boost fetch ERROR:", err);
});
});
}
@@ -2551,21 +2567,25 @@ PLATFORM_ORCHESTRATION_JS = """
// Re-read href from element at click time (not closed-over value)
var liveHref = link.getAttribute("href") || _href;
var pathname = urlPathname(liveHref);
console.log("[sx-debug] bindClientRouteClick:", pathname, "el:", link.tagName, link.textContent.slice(0,30));
// Find target selector: sx-boost ancestor, explicit sx-target, or #main-panel
var boostEl = link.closest("[sx-boost]");
var targetSel = boostEl ? boostEl.getAttribute("sx-boost") : null;
if (!targetSel || targetSel === "true") {
targetSel = link.getAttribute("sx-target") || "#main-panel";
}
console.log("[sx-debug] targetSel:", targetSel, "trying client route...");
if (tryClientRoute(pathname, targetSel)) {
console.log("[sx-debug] client route SUCCESS, pushState:", liveHref);
try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {}
if (typeof window !== "undefined") window.scrollTo(0, 0);
} else {
logInfo("sx:route server " + pathname);
console.log("[sx-debug] client route FAILED, server fetch:", liveHref);
executeRequest(link, { method: "GET", url: liveHref }).then(function() {
console.log("[sx-debug] server fetch OK, pushState:", liveHref);
try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {}
}).catch(function(err) {
logWarn("sx:route server fetch error: " + (err && err.message ? err.message : err));
console.error("[sx-debug] server fetch ERROR:", err);
});
}
});

View File

@@ -1038,7 +1038,7 @@ PLATFORM_PARSER_PY = '''
import re as _re_parser
_IDENT_START_RE = _re_parser.compile(r"[a-zA-Z_~*+\\-><=/!?&]")
_IDENT_CHAR_RE = _re_parser.compile(r"[a-zA-Z0-9_~*+\\-><=/!?.:&/\\[\\]#,]")
_IDENT_CHAR_RE = _re_parser.compile(r"[a-zA-Z0-9_~*+\\-><=/!?.:&/#,]")
def ident_start_p(ch):
@@ -1626,7 +1626,7 @@ SPEC_MODULES = {
# Explicit ordering for spec modules with dependencies.
# Modules listed here are emitted in this order; any not listed use alphabetical.
SPEC_MODULE_ORDER = [
"deps", "engine", "frames", "page-helpers", "router", "signals", "types", "cek",
"deps", "engine", "frames", "page-helpers", "router", "cek", "signals", "types",
]
EXTENSION_NAMES = {"continuations"}

View File

@@ -7,7 +7,7 @@ _HERE = os.path.dirname(os.path.abspath(__file__))
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
sys.path.insert(0, _PROJECT)
from shared.sx.parser import parse_all
from shared.sx.ref.sx_ref import sx_parse as parse_all
from shared.sx.ref import sx_ref
from shared.sx.ref.sx_ref import (
make_env, env_get, env_has, env_set,

View File

@@ -32,8 +32,8 @@ try:
finally:
os.unlink(tmp.name)
from shared.sx.parser import parse_all
from shared.sx.types import NIL
parse_all = mod.sx_parse
# Use tree-walk evaluator for interpreting .sx test files.
# CEK is now the default, but test runners need tree-walk so that

View File

@@ -190,6 +190,10 @@ def compile_ref_to_js(
if has_parser:
parts.append(adapter_platform["parser"])
# CEK platform aliases must come before transpiled cek.sx (which uses them)
if has_cek:
parts.append(PLATFORM_CEK_JS)
# Translate each spec file using js.sx
for filename, label in sx_files:
filepath = os.path.join(ref_dir, filename)
@@ -216,9 +220,6 @@ def compile_ref_to_js(
if name in adapter_set and name in adapter_platform:
parts.append(adapter_platform[name])
if has_cek:
parts.append(PLATFORM_CEK_JS)
parts.append(fixups_js(has_html, has_sx, has_dom, has_signals, has_deps, has_page_helpers))
if has_cek:
parts.append(CEK_FIXUPS_JS)

View File

@@ -12,7 +12,7 @@ _HERE = os.path.dirname(os.path.abspath(__file__))
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
sys.path.insert(0, _PROJECT)
from shared.sx.parser import parse_all
from shared.sx.ref.sx_ref import sx_parse as parse_all
from shared.sx.ref import sx_ref
from shared.sx.ref.sx_ref import make_env, scope_push, scope_pop, sx_context
from shared.sx.types import NIL, Island, Lambda

View File

@@ -7,7 +7,7 @@ _HERE = os.path.dirname(os.path.abspath(__file__))
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
sys.path.insert(0, _PROJECT)
from shared.sx.parser import parse_all
from shared.sx.ref.sx_ref import sx_parse as parse_all
from shared.sx.ref import sx_ref
from shared.sx.ref.sx_ref import make_env, env_get, env_has, env_set
from shared.sx.types import NIL, Component

View File

@@ -23,14 +23,11 @@
;; (scope-pop! "sx-reactive") → void
;; (context "sx-reactive" nil) → dict or nil
;;
;; Runtime callable dispatch:
;; (invoke f &rest args) → any — call f with args; handles both
;; native host functions AND SX lambdas
;; from runtime-evaluated code (islands).
;; Transpiled code emits direct calls
;; f(args) which fail on SX lambdas.
;; invoke goes through the evaluator's
;; dispatch (call-fn) so either works.
;; CEK callable dispatch:
;; (cek-call f args) → any — call f with args list via CEK.
;; Dispatches through cek-run for SX
;; lambdas, apply for native callables.
;; Defined in cek.sx.
;;
;; ==========================================================================
@@ -150,7 +147,7 @@
;; Push scope-based tracking context for this computed
(let ((ctx (dict "deps" (list) "notify" recompute)))
(scope-push! "sx-reactive" ctx)
(let ((new-val (invoke compute-fn)))
(let ((new-val (cek-call compute-fn nil)))
(scope-pop! "sx-reactive")
;; Save discovered deps
(signal-set-deps! s (get ctx "deps"))
@@ -184,7 +181,7 @@
(fn ()
(when (not disposed)
;; Run previous cleanup if any
(when cleanup-fn (invoke cleanup-fn))
(when cleanup-fn (cek-call cleanup-fn nil))
;; Unsubscribe from old deps
(for-each
@@ -195,7 +192,7 @@
;; Push scope-based tracking context
(let ((ctx (dict "deps" (list) "notify" run-effect)))
(scope-push! "sx-reactive" ctx)
(let ((result (invoke effect-fn)))
(let ((result (cek-call effect-fn nil)))
(scope-pop! "sx-reactive")
(set! deps (get ctx "deps"))
;; If effect returns a function, it's the cleanup
@@ -209,7 +206,7 @@
(let ((dispose-fn
(fn ()
(set! disposed true)
(when cleanup-fn (invoke cleanup-fn))
(when cleanup-fn (cek-call cleanup-fn nil))
(for-each
(fn ((dep :as signal)) (signal-remove-sub! dep run-effect))
deps)
@@ -232,7 +229,7 @@
(define batch :effects [mutation]
(fn ((thunk :as lambda))
(set! *batch-depth* (+ *batch-depth* 1))
(invoke thunk)
(cek-call thunk nil)
(set! *batch-depth* (- *batch-depth* 1))
(when (= *batch-depth* 0)
(let ((queue *batch-queue*))
@@ -323,7 +320,7 @@
(fn ((disposable :as lambda))
(let ((collector (context "sx-island-scope" nil)))
(when collector
(invoke collector disposable)))))
(cek-call collector (list disposable))))))
;; ==========================================================================
@@ -362,7 +359,7 @@
;; Parent island scope and sibling marshes are unaffected.
(let ((disposers (dom-get-data marsh-el "sx-marsh-disposers")))
(when disposers
(for-each (fn ((d :as lambda)) (invoke d)) disposers)
(for-each (fn ((d :as lambda)) (cek-call d nil)) disposers)
(dom-set-data marsh-el "sx-marsh-disposers" nil)))))
@@ -384,7 +381,7 @@
(let ((registry *store-registry*))
;; Only create the store once — subsequent calls return existing
(when (not (has-key? registry name))
(set! *store-registry* (assoc registry name (invoke init-fn))))
(set! *store-registry* (assoc registry name (cek-call init-fn nil))))
(get *store-registry* name))))
(define use-store :effects []
@@ -443,7 +440,7 @@
(fn (e)
(let ((detail (event-detail e))
(new-val (if transform-fn
(invoke transform-fn detail)
(cek-call transform-fn (list detail))
detail)))
(reset! target-signal new-val))))))
;; Return cleanup — removes listener on dispose/re-run
@@ -474,7 +471,7 @@
(fn ((fetch-fn :as lambda))
(let ((state (signal (dict "loading" true "data" nil "error" nil))))
;; Kick off the async operation
(promise-then (invoke fetch-fn)
(promise-then (cek-call fetch-fn nil)
(fn (data) (reset! state (dict "loading" false "data" data "error" nil)))
(fn (err) (reset! state (dict "loading" false "data" nil "error" err))))
state)))

View File

@@ -952,7 +952,7 @@ char_from_code = PRIMITIVES["char-from-code"]
import re as _re_parser
_IDENT_START_RE = _re_parser.compile(r"[a-zA-Z_~*+\-><=/!?&]")
_IDENT_CHAR_RE = _re_parser.compile(r"[a-zA-Z0-9_~*+\-><=/!?.:&/\[\]#,]")
_IDENT_CHAR_RE = _re_parser.compile(r"[a-zA-Z0-9_~*+\-><=/!?.:&/#,]")
def ident_start_p(ch):
@@ -2179,36 +2179,39 @@ def sx_parse(source):
_cells['pos'] = 0
len_src = len(source)
def skip_comment():
if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (not sx_truthy((nth(source, _cells['pos']) == '\n'))))):
_cells['pos'] = (_cells['pos'] + 1)
return skip_comment()
return NIL
while True:
if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (not sx_truthy((nth(source, _cells['pos']) == '\n'))))):
_cells['pos'] = (_cells['pos'] + 1)
continue
break
def skip_ws():
if sx_truthy((_cells['pos'] < len_src)):
ch = nth(source, _cells['pos'])
if sx_truthy(((ch == ' ') if sx_truthy((ch == ' ')) else ((ch == '\t') if sx_truthy((ch == '\t')) else ((ch == '\n') if sx_truthy((ch == '\n')) else (ch == '\r'))))):
_cells['pos'] = (_cells['pos'] + 1)
return skip_ws()
elif sx_truthy((ch == ';')):
_cells['pos'] = (_cells['pos'] + 1)
skip_comment()
return skip_ws()
else:
return NIL
return NIL
while True:
if sx_truthy((_cells['pos'] < len_src)):
ch = nth(source, _cells['pos'])
if sx_truthy(((ch == ' ') if sx_truthy((ch == ' ')) else ((ch == '\t') if sx_truthy((ch == '\t')) else ((ch == '\n') if sx_truthy((ch == '\n')) else (ch == '\r'))))):
_cells['pos'] = (_cells['pos'] + 1)
continue
elif sx_truthy((ch == ';')):
_cells['pos'] = (_cells['pos'] + 1)
skip_comment()
continue
else:
break
break
def hex_digit_value(ch):
return index_of('0123456789abcdef', lower(ch))
def read_string():
_cells['pos'] = (_cells['pos'] + 1)
_cells['buf'] = ''
def read_str_loop():
while True:
if sx_truthy((_cells['pos'] >= len_src)):
return error('Unterminated string')
error('Unterminated string')
break
else:
ch = nth(source, _cells['pos'])
if sx_truthy((ch == '"')):
_cells['pos'] = (_cells['pos'] + 1)
return NIL
break
elif sx_truthy((ch == '\\')):
_cells['pos'] = (_cells['pos'] + 1)
esc = nth(source, _cells['pos'])
@@ -2223,25 +2226,23 @@ def sx_parse(source):
d3 = hex_digit_value(nth(source, _cells['pos']))
_ = _sx_cell_set(_cells, 'pos', (_cells['pos'] + 1))
_cells['buf'] = sx_str(_cells['buf'], char_from_code(((d0 * 4096) + (d1 * 256))))
return read_str_loop()
continue
else:
_cells['buf'] = sx_str(_cells['buf'], ('\n' if sx_truthy((esc == 'n')) else ('\t' if sx_truthy((esc == 't')) else ('\r' if sx_truthy((esc == 'r')) else esc))))
_cells['pos'] = (_cells['pos'] + 1)
return read_str_loop()
continue
else:
_cells['buf'] = sx_str(_cells['buf'], ch)
_cells['pos'] = (_cells['pos'] + 1)
return read_str_loop()
read_str_loop()
continue
return _cells['buf']
def read_ident():
start = _cells['pos']
def read_ident_loop():
while True:
if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else ident_char_p(nth(source, _cells['pos'])))):
_cells['pos'] = (_cells['pos'] + 1)
return read_ident_loop()
return NIL
read_ident_loop()
continue
break
return slice(source, start, _cells['pos'])
def read_keyword():
_cells['pos'] = (_cells['pos'] + 1)
@@ -2251,10 +2252,11 @@ def sx_parse(source):
if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (nth(source, _cells['pos']) == '-'))):
_cells['pos'] = (_cells['pos'] + 1)
def read_digits():
if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (lambda c: ((c >= '0') if not sx_truthy((c >= '0')) else (c <= '9')))(nth(source, _cells['pos'])))):
_cells['pos'] = (_cells['pos'] + 1)
return read_digits()
return NIL
while True:
if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (lambda c: ((c >= '0') if not sx_truthy((c >= '0')) else (c <= '9')))(nth(source, _cells['pos'])))):
_cells['pos'] = (_cells['pos'] + 1)
continue
break
read_digits()
if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (nth(source, _cells['pos']) == '.'))):
_cells['pos'] = (_cells['pos'] + 1)
@@ -2277,52 +2279,52 @@ def sx_parse(source):
return make_symbol(name)
def read_list(close_ch):
items = []
def read_list_loop():
while True:
skip_ws()
if sx_truthy((_cells['pos'] >= len_src)):
return error('Unterminated list')
error('Unterminated list')
break
else:
if sx_truthy((nth(source, _cells['pos']) == close_ch)):
_cells['pos'] = (_cells['pos'] + 1)
return NIL
break
else:
items.append(read_expr())
return read_list_loop()
read_list_loop()
continue
return items
def read_map():
result = {}
def read_map_loop():
while True:
skip_ws()
if sx_truthy((_cells['pos'] >= len_src)):
return error('Unterminated map')
error('Unterminated map')
break
else:
if sx_truthy((nth(source, _cells['pos']) == '}')):
_cells['pos'] = (_cells['pos'] + 1)
return NIL
break
else:
key_expr = read_expr()
key_str = (keyword_name(key_expr) if sx_truthy((type_of(key_expr) == 'keyword')) else sx_str(key_expr))
val_expr = read_expr()
result[key_str] = val_expr
return read_map_loop()
read_map_loop()
continue
return result
def read_raw_string():
_cells['buf'] = ''
def raw_loop():
while True:
if sx_truthy((_cells['pos'] >= len_src)):
return error('Unterminated raw string')
error('Unterminated raw string')
break
else:
ch = nth(source, _cells['pos'])
if sx_truthy((ch == '|')):
_cells['pos'] = (_cells['pos'] + 1)
return NIL
break
else:
_cells['buf'] = sx_str(_cells['buf'], ch)
_cells['pos'] = (_cells['pos'] + 1)
return raw_loop()
raw_loop()
continue
return _cells['buf']
def read_expr():
skip_ws()
@@ -2343,6 +2345,9 @@ def sx_parse(source):
return read_string()
elif sx_truthy((ch == ':')):
return read_keyword()
elif sx_truthy((ch == "'")):
_cells['pos'] = (_cells['pos'] + 1)
return [make_symbol('quote'), read_expr()]
elif sx_truthy((ch == '`')):
_cells['pos'] = (_cells['pos'] + 1)
return [make_symbol('quasiquote'), read_expr()]
@@ -2388,13 +2393,12 @@ def sx_parse(source):
else:
return error(sx_str('Unexpected character: ', ch))
exprs = []
def parse_loop():
while True:
skip_ws()
if sx_truthy((_cells['pos'] < len_src)):
exprs.append(read_expr())
return parse_loop()
return NIL
parse_loop()
continue
break
return exprs
# sx-serialize
@@ -3862,234 +3866,6 @@ def prepare_url_expr(url_path, env):
return auto_quote_unknowns(expr, env)
# === Transpiled from signals (reactive signal runtime) ===
# make-signal
def make_signal(value):
return {'__signal': True, 'value': value, 'subscribers': [], 'deps': []}
# signal?
def is_signal(x):
return (dict_p(x) if not sx_truthy(dict_p(x)) else has_key_p(x, '__signal'))
# signal-value
def signal_value(s):
return get(s, 'value')
# signal-set-value!
def signal_set_value(s, v):
return _sx_dict_set(s, 'value', v)
# signal-subscribers
def signal_subscribers(s):
return get(s, 'subscribers')
# signal-add-sub!
def signal_add_sub(s, f):
if sx_truthy((not sx_truthy(contains_p(get(s, 'subscribers'), f)))):
return _sx_append(get(s, 'subscribers'), f)
return NIL
# signal-remove-sub!
def signal_remove_sub(s, f):
return _sx_dict_set(s, 'subscribers', filter(lambda sub: (not sx_truthy(is_identical(sub, f))), get(s, 'subscribers')))
# signal-deps
def signal_deps(s):
return get(s, 'deps')
# signal-set-deps!
def signal_set_deps(s, deps):
return _sx_dict_set(s, 'deps', deps)
# signal
def signal(initial_value):
return make_signal(initial_value)
# deref
def deref(s):
if sx_truthy((not sx_truthy(is_signal(s)))):
return s
else:
ctx = sx_context('sx-reactive', NIL)
if sx_truthy(ctx):
dep_list = get(ctx, 'deps')
notify_fn = get(ctx, 'notify')
if sx_truthy((not sx_truthy(contains_p(dep_list, s)))):
dep_list.append(s)
signal_add_sub(s, notify_fn)
return signal_value(s)
# reset!
def reset_b(s, value):
if sx_truthy(is_signal(s)):
old = signal_value(s)
if sx_truthy((not sx_truthy(is_identical(old, value)))):
signal_set_value(s, value)
return notify_subscribers(s)
return NIL
return NIL
# swap!
def swap_b(s, f, *args):
if sx_truthy(is_signal(s)):
old = signal_value(s)
new_val = apply(f, cons(old, args))
if sx_truthy((not sx_truthy(is_identical(old, new_val)))):
signal_set_value(s, new_val)
return notify_subscribers(s)
return NIL
return NIL
# computed
def computed(compute_fn):
s = make_signal(NIL)
deps = []
compute_ctx = NIL
recompute = _sx_fn(lambda : (
for_each(lambda dep: signal_remove_sub(dep, recompute), signal_deps(s)),
signal_set_deps(s, []),
(lambda ctx: _sx_begin(scope_push('sx-reactive', ctx), (lambda new_val: _sx_begin(scope_pop('sx-reactive'), signal_set_deps(s, get(ctx, 'deps')), (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))))(invoke(compute_fn))))({'deps': [], 'notify': recompute})
)[-1])
recompute()
register_in_scope(lambda : dispose_computed(s))
return s
# effect
def effect(effect_fn):
_cells = {}
_cells['deps'] = []
_cells['disposed'] = False
_cells['cleanup_fn'] = NIL
run_effect = lambda : (_sx_begin((invoke(_cells['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: _sx_begin(scope_push('sx-reactive', ctx), (lambda result: _sx_begin(scope_pop('sx-reactive'), _sx_cell_set(_cells, 'deps', get(ctx, 'deps')), (_sx_cell_set(_cells, 'cleanup_fn', result) if sx_truthy(is_callable(result)) else NIL)))(invoke(effect_fn))))({'deps': [], 'notify': run_effect})) if sx_truthy((not sx_truthy(_cells['disposed']))) else NIL)
run_effect()
dispose_fn = _sx_fn(lambda : (
_sx_cell_set(_cells, 'disposed', True),
(invoke(_cells['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])
register_in_scope(dispose_fn)
return dispose_fn
# *batch-depth*
_batch_depth = 0
# *batch-queue*
_batch_queue = []
# batch
def batch(thunk):
_batch_depth = (_batch_depth + 1)
invoke(thunk)
_batch_depth = (_batch_depth - 1)
if sx_truthy((_batch_depth == 0)):
queue = _batch_queue
_batch_queue = []
seen = []
pending = []
for s in queue:
for sub in signal_subscribers(s):
if sx_truthy((not sx_truthy(contains_p(seen, sub)))):
seen.append(sub)
pending.append(sub)
for sub in pending:
sub()
return NIL
return NIL
# notify-subscribers
def notify_subscribers(s):
if sx_truthy((_batch_depth > 0)):
if sx_truthy((not sx_truthy(contains_p(_batch_queue, s)))):
return _sx_append(_batch_queue, s)
return NIL
else:
return flush_subscribers(s)
# flush-subscribers
def flush_subscribers(s):
for sub in signal_subscribers(s):
sub()
return NIL
# dispose-computed
def dispose_computed(s):
if sx_truthy(is_signal(s)):
for dep in signal_deps(s):
signal_remove_sub(dep, NIL)
return signal_set_deps(s, [])
return NIL
# with-island-scope
def with_island_scope(scope_fn, body_fn):
scope_push('sx-island-scope', scope_fn)
result = body_fn()
scope_pop('sx-island-scope')
return result
# register-in-scope
def register_in_scope(disposable):
collector = sx_context('sx-island-scope', NIL)
if sx_truthy(collector):
return invoke(collector, disposable)
return NIL
# with-marsh-scope
def with_marsh_scope(marsh_el, body_fn):
disposers = []
with_island_scope(lambda d: _sx_append(disposers, d), body_fn)
return dom_set_data(marsh_el, 'sx-marsh-disposers', disposers)
# dispose-marsh-scope
def dispose_marsh_scope(marsh_el):
disposers = dom_get_data(marsh_el, 'sx-marsh-disposers')
if sx_truthy(disposers):
for d in disposers:
invoke(d)
return dom_set_data(marsh_el, 'sx-marsh-disposers', NIL)
return NIL
# *store-registry*
_store_registry = {}
# def-store
def def_store(name, init_fn):
registry = _store_registry
if sx_truthy((not sx_truthy(has_key_p(registry, name)))):
_store_registry = assoc(registry, name, invoke(init_fn))
return get(_store_registry, name)
# use-store
def use_store(name):
if sx_truthy(has_key_p(_store_registry, name)):
return get(_store_registry, name)
else:
return error(sx_str('Store not found: ', name, '. Call (def-store ...) before (use-store ...).'))
# clear-stores
def clear_stores():
return _sx_cell_set(_cells, '_store_registry', {})
# emit-event
def emit_event(el, event_name, detail):
return dom_dispatch(el, event_name, detail)
# on-event
def on_event(el, event_name, handler):
return dom_listen(el, event_name, handler)
# bridge-event
def bridge_event(el, event_name, target_signal, transform_fn):
return effect(lambda : (lambda remove: remove)(dom_listen(el, event_name, lambda e: (lambda detail: (lambda new_val: reset_b(target_signal, new_val))((invoke(transform_fn, detail) if sx_truthy(transform_fn) else detail)))(event_detail(e)))))
# resource
def resource(fetch_fn):
state = signal({'loading': True, 'data': NIL, 'error': NIL})
promise_then(invoke(fetch_fn), lambda data: reset_b(state, {'loading': False, 'data': data, 'error': NIL}), lambda err: reset_b(state, {'loading': False, 'data': NIL, 'error': err}))
return state
# === Transpiled from cek (explicit CEK machine evaluator) ===
# cek-run
@@ -4371,6 +4147,18 @@ def step_sf_shift(args, env, kont):
def step_sf_deref(args, env, kont):
return make_cek_state(first(args), env, kont_push(make_deref_frame(env), kont))
# cek-call
def cek_call(f, args):
a = ([] if sx_truthy(is_nil(args)) else args)
if sx_truthy(is_nil(f)):
return NIL
elif sx_truthy(is_lambda(f)):
return cek_run(continue_with_call(f, a, {}, a, []))
elif sx_truthy(is_callable(f)):
return apply(f, a)
else:
return NIL
# reactive-shift-deref
def reactive_shift_deref(sig, env, kont):
_cells = {}
@@ -4381,14 +4169,14 @@ def reactive_shift_deref(sig, env, kont):
update_fn = get(reset_frame, 'update-fn')
_cells['sub_disposers'] = []
subscriber = _sx_fn(lambda : (
for_each(lambda d: invoke(d), _cells['sub_disposers']),
for_each(lambda d: cek_call(d, NIL), _cells['sub_disposers']),
_sx_cell_set(_cells, 'sub_disposers', []),
(lambda new_reset: (lambda new_kont: with_island_scope(lambda d: _sx_append(_cells['sub_disposers'], d), lambda : cek_run(make_cek_value(signal_value(sig), env, new_kont))))(concat(captured_frames, [new_reset], remaining_kont)))(make_reactive_reset_frame(env, update_fn, False))
)[-1])
signal_add_sub(sig, subscriber)
register_in_scope(_sx_fn(lambda : (
signal_remove_sub(sig, subscriber),
for_each(lambda d: invoke(d), _cells['sub_disposers'])
for_each(lambda d: cek_call(d, NIL), _cells['sub_disposers'])
)[-1]))
initial_kont = concat(captured_frames, [reset_frame], remaining_kont)
return make_cek_value(signal_value(sig), env, initial_kont)
@@ -4610,7 +4398,7 @@ def step_continue(state):
update_fn = get(frame, 'update-fn')
first_p = get(frame, 'first-render')
if sx_truthy((update_fn if not sx_truthy(update_fn) else (not sx_truthy(first_p)))):
invoke(update_fn, value)
cek_call(update_fn, [value])
return make_cek_value(value, env, rest_k)
elif sx_truthy((ft == 'scope')):
name = get(frame, 'name')
@@ -4686,6 +4474,234 @@ def trampoline_cek(val):
return val
# === Transpiled from signals (reactive signal runtime) ===
# make-signal
def make_signal(value):
return {'__signal': True, 'value': value, 'subscribers': [], 'deps': []}
# signal?
def is_signal(x):
return (dict_p(x) if not sx_truthy(dict_p(x)) else has_key_p(x, '__signal'))
# signal-value
def signal_value(s):
return get(s, 'value')
# signal-set-value!
def signal_set_value(s, v):
return _sx_dict_set(s, 'value', v)
# signal-subscribers
def signal_subscribers(s):
return get(s, 'subscribers')
# signal-add-sub!
def signal_add_sub(s, f):
if sx_truthy((not sx_truthy(contains_p(get(s, 'subscribers'), f)))):
return _sx_append(get(s, 'subscribers'), f)
return NIL
# signal-remove-sub!
def signal_remove_sub(s, f):
return _sx_dict_set(s, 'subscribers', filter(lambda sub: (not sx_truthy(is_identical(sub, f))), get(s, 'subscribers')))
# signal-deps
def signal_deps(s):
return get(s, 'deps')
# signal-set-deps!
def signal_set_deps(s, deps):
return _sx_dict_set(s, 'deps', deps)
# signal
def signal(initial_value):
return make_signal(initial_value)
# deref
def deref(s):
if sx_truthy((not sx_truthy(is_signal(s)))):
return s
else:
ctx = sx_context('sx-reactive', NIL)
if sx_truthy(ctx):
dep_list = get(ctx, 'deps')
notify_fn = get(ctx, 'notify')
if sx_truthy((not sx_truthy(contains_p(dep_list, s)))):
dep_list.append(s)
signal_add_sub(s, notify_fn)
return signal_value(s)
# reset!
def reset_b(s, value):
if sx_truthy(is_signal(s)):
old = signal_value(s)
if sx_truthy((not sx_truthy(is_identical(old, value)))):
signal_set_value(s, value)
return notify_subscribers(s)
return NIL
return NIL
# swap!
def swap_b(s, f, *args):
if sx_truthy(is_signal(s)):
old = signal_value(s)
new_val = apply(f, cons(old, args))
if sx_truthy((not sx_truthy(is_identical(old, new_val)))):
signal_set_value(s, new_val)
return notify_subscribers(s)
return NIL
return NIL
# computed
def computed(compute_fn):
s = make_signal(NIL)
deps = []
compute_ctx = NIL
recompute = _sx_fn(lambda : (
for_each(lambda dep: signal_remove_sub(dep, recompute), signal_deps(s)),
signal_set_deps(s, []),
(lambda ctx: _sx_begin(scope_push('sx-reactive', ctx), (lambda new_val: _sx_begin(scope_pop('sx-reactive'), signal_set_deps(s, get(ctx, 'deps')), (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))))(cek_call(compute_fn, NIL))))({'deps': [], 'notify': recompute})
)[-1])
recompute()
register_in_scope(lambda : dispose_computed(s))
return s
# effect
def effect(effect_fn):
_cells = {}
_cells['deps'] = []
_cells['disposed'] = False
_cells['cleanup_fn'] = NIL
run_effect = lambda : (_sx_begin((cek_call(_cells['cleanup_fn'], NIL) 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: _sx_begin(scope_push('sx-reactive', ctx), (lambda result: _sx_begin(scope_pop('sx-reactive'), _sx_cell_set(_cells, 'deps', get(ctx, 'deps')), (_sx_cell_set(_cells, 'cleanup_fn', result) if sx_truthy(is_callable(result)) else NIL)))(cek_call(effect_fn, NIL))))({'deps': [], 'notify': run_effect})) if sx_truthy((not sx_truthy(_cells['disposed']))) else NIL)
run_effect()
dispose_fn = _sx_fn(lambda : (
_sx_cell_set(_cells, 'disposed', True),
(cek_call(_cells['cleanup_fn'], NIL) 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])
register_in_scope(dispose_fn)
return dispose_fn
# *batch-depth*
_batch_depth = 0
# *batch-queue*
_batch_queue = []
# batch
def batch(thunk):
_batch_depth = (_batch_depth + 1)
cek_call(thunk, NIL)
_batch_depth = (_batch_depth - 1)
if sx_truthy((_batch_depth == 0)):
queue = _batch_queue
_batch_queue = []
seen = []
pending = []
for s in queue:
for sub in signal_subscribers(s):
if sx_truthy((not sx_truthy(contains_p(seen, sub)))):
seen.append(sub)
pending.append(sub)
for sub in pending:
sub()
return NIL
return NIL
# notify-subscribers
def notify_subscribers(s):
if sx_truthy((_batch_depth > 0)):
if sx_truthy((not sx_truthy(contains_p(_batch_queue, s)))):
return _sx_append(_batch_queue, s)
return NIL
else:
return flush_subscribers(s)
# flush-subscribers
def flush_subscribers(s):
for sub in signal_subscribers(s):
sub()
return NIL
# dispose-computed
def dispose_computed(s):
if sx_truthy(is_signal(s)):
for dep in signal_deps(s):
signal_remove_sub(dep, NIL)
return signal_set_deps(s, [])
return NIL
# with-island-scope
def with_island_scope(scope_fn, body_fn):
scope_push('sx-island-scope', scope_fn)
result = body_fn()
scope_pop('sx-island-scope')
return result
# register-in-scope
def register_in_scope(disposable):
collector = sx_context('sx-island-scope', NIL)
if sx_truthy(collector):
return cek_call(collector, [disposable])
return NIL
# with-marsh-scope
def with_marsh_scope(marsh_el, body_fn):
disposers = []
with_island_scope(lambda d: _sx_append(disposers, d), body_fn)
return dom_set_data(marsh_el, 'sx-marsh-disposers', disposers)
# dispose-marsh-scope
def dispose_marsh_scope(marsh_el):
disposers = dom_get_data(marsh_el, 'sx-marsh-disposers')
if sx_truthy(disposers):
for d in disposers:
cek_call(d, NIL)
return dom_set_data(marsh_el, 'sx-marsh-disposers', NIL)
return NIL
# *store-registry*
_store_registry = {}
# def-store
def def_store(name, init_fn):
registry = _store_registry
if sx_truthy((not sx_truthy(has_key_p(registry, name)))):
_store_registry = assoc(registry, name, cek_call(init_fn, NIL))
return get(_store_registry, name)
# use-store
def use_store(name):
if sx_truthy(has_key_p(_store_registry, name)):
return get(_store_registry, name)
else:
return error(sx_str('Store not found: ', name, '. Call (def-store ...) before (use-store ...).'))
# clear-stores
def clear_stores():
return _sx_cell_set(_cells, '_store_registry', {})
# emit-event
def emit_event(el, event_name, detail):
return dom_dispatch(el, event_name, detail)
# on-event
def on_event(el, event_name, handler):
return dom_listen(el, event_name, handler)
# bridge-event
def bridge_event(el, event_name, target_signal, transform_fn):
return effect(lambda : (lambda remove: remove)(dom_listen(el, event_name, lambda e: (lambda detail: (lambda new_val: reset_b(target_signal, new_val))((cek_call(transform_fn, [detail]) if sx_truthy(transform_fn) else detail)))(event_detail(e)))))
# resource
def resource(fetch_fn):
state = signal({'loading': True, 'data': NIL, 'error': NIL})
promise_then(cek_call(fetch_fn, NIL), lambda data: reset_b(state, {'loading': False, 'data': data, 'error': NIL}), lambda err: reset_b(state, {'loading': False, 'data': NIL, 'error': err}))
return state
# === Transpiled from adapter-async ===
# async-render