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:
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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")
|
||||
"\"")))
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user