Compare commits
15 Commits
11fdd1a840
...
2211655060
| Author | SHA1 | Date | |
|---|---|---|---|
| 2211655060 | |||
| d0a5ce1070 | |||
| 6581211a10 | |||
| 455e48df07 | |||
| 30d9d4aa4c | |||
| b06cc2daca | |||
| 4b746e4c8b | |||
| f96506024e | |||
| 203f9a49a1 | |||
| 893c767238 | |||
| 5c4a8c8cc2 | |||
| 90febbd91e | |||
| f3a9f3ccc0 | |||
| dcc73a68d5 | |||
| 1765216335 |
File diff suppressed because it is too large
Load Diff
@@ -39,10 +39,10 @@ def _load_declarations() -> None:
|
||||
len(_DECLARED_PURE), len(_DECLARED_IO), len(_DECLARED_HELPERS),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to load boundary declarations: %s", e)
|
||||
_DECLARED_PURE = frozenset()
|
||||
_DECLARED_IO = frozenset()
|
||||
_DECLARED_HELPERS = {}
|
||||
# Don't cache failure — parser may not be ready yet (circular import
|
||||
# during startup). Will retry on next call. Validation functions
|
||||
# skip checks when declarations aren't loaded.
|
||||
logger.debug("Boundary declarations not ready yet: %s", e)
|
||||
|
||||
|
||||
def _is_strict() -> bool:
|
||||
@@ -63,7 +63,8 @@ def _report(message: str) -> None:
|
||||
def validate_primitive(name: str) -> None:
|
||||
"""Validate that a pure primitive is declared in primitives.sx."""
|
||||
_load_declarations()
|
||||
assert _DECLARED_PURE is not None
|
||||
if _DECLARED_PURE is None:
|
||||
return # Not ready yet (circular import during startup), skip
|
||||
if name not in _DECLARED_PURE:
|
||||
_report(f"Undeclared pure primitive: {name!r}. Add to primitives.sx.")
|
||||
|
||||
@@ -71,7 +72,8 @@ def validate_primitive(name: str) -> None:
|
||||
def validate_io(name: str) -> None:
|
||||
"""Validate that an I/O primitive is declared in boundary.sx or boundary-app.sx."""
|
||||
_load_declarations()
|
||||
assert _DECLARED_IO is not None
|
||||
if _DECLARED_IO is None:
|
||||
return # Not ready yet, skip
|
||||
if name not in _DECLARED_IO:
|
||||
_report(
|
||||
f"Undeclared I/O primitive: {name!r}. "
|
||||
@@ -82,7 +84,8 @@ def validate_io(name: str) -> None:
|
||||
def validate_helper(service: str, name: str) -> None:
|
||||
"""Validate that a page helper is declared in {service}/sx/boundary.sx."""
|
||||
_load_declarations()
|
||||
assert _DECLARED_HELPERS is not None
|
||||
if _DECLARED_HELPERS is None:
|
||||
return # Not ready yet, skip
|
||||
svc_helpers = _DECLARED_HELPERS.get(service, frozenset())
|
||||
if name not in svc_helpers:
|
||||
_report(
|
||||
@@ -129,17 +132,14 @@ def validate_boundary_value(value: Any, context: str = "") -> None:
|
||||
|
||||
def declared_pure() -> frozenset[str]:
|
||||
_load_declarations()
|
||||
assert _DECLARED_PURE is not None
|
||||
return _DECLARED_PURE
|
||||
return _DECLARED_PURE or frozenset()
|
||||
|
||||
|
||||
def declared_io() -> frozenset[str]:
|
||||
_load_declarations()
|
||||
assert _DECLARED_IO is not None
|
||||
return _DECLARED_IO
|
||||
return _DECLARED_IO or frozenset()
|
||||
|
||||
|
||||
def declared_helpers() -> dict[str, frozenset[str]]:
|
||||
_load_declarations()
|
||||
assert _DECLARED_HELPERS is not None
|
||||
return dict(_DECLARED_HELPERS)
|
||||
return dict(_DECLARED_HELPERS) if _DECLARED_HELPERS else {}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
;; Signal → reactive text in island scope, deref outside
|
||||
:else
|
||||
(if (signal? expr)
|
||||
(if *island-scope*
|
||||
(if (context "sx-island-scope" nil)
|
||||
(reactive-text expr)
|
||||
(create-text-node (str (deref expr))))
|
||||
(create-text-node (str expr))))))
|
||||
@@ -143,7 +143,7 @@
|
||||
(render-dom-element name args env ns)
|
||||
|
||||
;; deref in island scope → reactive text node
|
||||
(and (= name "deref") *island-scope*)
|
||||
(and (= name "deref") (context "sx-island-scope" nil))
|
||||
(let ((sig-or-val (trampoline (eval-expr (first args) env))))
|
||||
(if (signal? sig-or-val)
|
||||
(reactive-text sig-or-val)
|
||||
@@ -215,7 +215,7 @@
|
||||
;; Inside island scope: reactive attribute binding.
|
||||
;; The effect tracks signal deps automatically — if none
|
||||
;; are deref'd, it fires once and never again (safe).
|
||||
*island-scope*
|
||||
(context "sx-island-scope" nil)
|
||||
(reactive-attr el attr-name
|
||||
(fn () (trampoline (eval-expr attr-expr env))))
|
||||
;; Static attribute (outside islands)
|
||||
@@ -237,7 +237,7 @@
|
||||
(let ((child (render-to-dom arg env new-ns)))
|
||||
(cond
|
||||
;; Reactive spread: track signal deps, update attrs on change
|
||||
(and (spread? child) *island-scope*)
|
||||
(and (spread? child) (context "sx-island-scope" nil))
|
||||
(reactive-spread el (fn () (render-to-dom arg env new-ns)))
|
||||
;; Static spread: already emitted via provide, skip
|
||||
(spread? child) nil
|
||||
@@ -392,7 +392,7 @@
|
||||
(cond
|
||||
;; if — reactive inside islands (re-renders when signal deps change)
|
||||
(= name "if")
|
||||
(if *island-scope*
|
||||
(if (context "sx-island-scope" nil)
|
||||
(let ((marker (create-comment "r-if"))
|
||||
(current-nodes (list))
|
||||
(initial-result nil))
|
||||
@@ -440,7 +440,7 @@
|
||||
|
||||
;; when — reactive inside islands
|
||||
(= name "when")
|
||||
(if *island-scope*
|
||||
(if (context "sx-island-scope" nil)
|
||||
(let ((marker (create-comment "r-when"))
|
||||
(current-nodes (list))
|
||||
(initial-result nil))
|
||||
@@ -486,7 +486,7 @@
|
||||
|
||||
;; cond — reactive inside islands
|
||||
(= name "cond")
|
||||
(if *island-scope*
|
||||
(if (context "sx-island-scope" nil)
|
||||
(let ((marker (create-comment "r-cond"))
|
||||
(current-nodes (list))
|
||||
(initial-result nil))
|
||||
@@ -563,7 +563,7 @@
|
||||
;; map — reactive-list when mapping over a signal inside an island
|
||||
(= name "map")
|
||||
(let ((coll-expr (nth expr 2)))
|
||||
(if (and *island-scope*
|
||||
(if (and (context "sx-island-scope" nil)
|
||||
(= (type-of coll-expr) "list")
|
||||
(> (len coll-expr) 1)
|
||||
(= (type-of (first coll-expr)) "symbol")
|
||||
@@ -1107,6 +1107,48 @@
|
||||
(reset! sig (dom-get-prop el "value"))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; CEK-based reactive rendering (opt-in, deref-as-shift)
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; When enabled, (deref sig) inside a reactive-reset boundary performs
|
||||
;; continuation capture: "the rest of this expression" becomes the subscriber.
|
||||
;; No explicit effect() wrapping needed for text/attr bindings.
|
||||
|
||||
(define *use-cek-reactive* true)
|
||||
(define enable-cek-reactive! (fn () (set! *use-cek-reactive* true)))
|
||||
|
||||
;; cek-reactive-text — create a text node bound via continuation capture
|
||||
(define cek-reactive-text :effects [render mutation]
|
||||
(fn (expr env)
|
||||
(let ((node (create-text-node ""))
|
||||
(update-fn (fn (val)
|
||||
(dom-set-text-content node (str val)))))
|
||||
(let ((initial (cek-run
|
||||
(make-cek-state expr env
|
||||
(list (make-reactive-reset-frame env update-fn true))))))
|
||||
(dom-set-text-content node (str initial))
|
||||
node))))
|
||||
|
||||
;; cek-reactive-attr — bind an attribute via continuation capture
|
||||
(define cek-reactive-attr :effects [render mutation]
|
||||
(fn (el attr-name expr env)
|
||||
(let ((update-fn (fn (val)
|
||||
(cond
|
||||
(or (nil? val) (= val false)) (dom-remove-attr el attr-name)
|
||||
(= val true) (dom-set-attr el attr-name "")
|
||||
:else (dom-set-attr el attr-name (str val))))))
|
||||
;; Mark for morph protection
|
||||
(let ((existing (or (dom-get-attr el "data-sx-reactive-attrs") ""))
|
||||
(updated (if (empty? existing) attr-name (str existing "," attr-name))))
|
||||
(dom-set-attr el "data-sx-reactive-attrs" updated))
|
||||
;; Initial render via CEK with ReactiveResetFrame
|
||||
(let ((initial (cek-run
|
||||
(make-cek-state expr env
|
||||
(list (make-reactive-reset-frame env update-fn true))))))
|
||||
(cek-call update-fn (list initial))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-dom-portal — render children into a remote target element
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -1168,7 +1210,7 @@
|
||||
(dom-set-attr container "data-sx-boundary" "true")
|
||||
|
||||
;; The entire body is rendered inside ONE effect + try-catch.
|
||||
;; Body renders WITHOUT *island-scope* so that if/when/cond use static
|
||||
;; Body renders WITHOUT island scope so that if/when/cond use static
|
||||
;; paths — their signal reads become direct deref calls tracked by THIS
|
||||
;; effect. Errors from signal changes throw synchronously within try-catch.
|
||||
;; The error boundary's own effect handles all reactivity for its subtree.
|
||||
@@ -1179,31 +1221,30 @@
|
||||
;; Clear container
|
||||
(dom-set-prop container "innerHTML" "")
|
||||
|
||||
;; Save and clear island scope BEFORE try-catch so it can be
|
||||
;; restored in both success and error paths.
|
||||
(let ((saved-scope *island-scope*))
|
||||
(set! *island-scope* nil)
|
||||
(try-catch
|
||||
(fn ()
|
||||
;; Body renders statically — signal reads tracked by THIS effect,
|
||||
;; throws propagate to our try-catch.
|
||||
(let ((frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (child)
|
||||
(dom-append frag (render-to-dom child env ns)))
|
||||
body-exprs)
|
||||
(dom-append container frag))
|
||||
(set! *island-scope* saved-scope))
|
||||
(fn (err)
|
||||
;; Restore scope first, then render fallback
|
||||
(set! *island-scope* saved-scope)
|
||||
;; Push nil island scope to suppress reactive rendering in body.
|
||||
;; Pop in both success and error paths.
|
||||
(scope-push! "sx-island-scope" nil)
|
||||
(try-catch
|
||||
(fn ()
|
||||
;; Body renders statically — signal reads tracked by THIS effect,
|
||||
;; throws propagate to our try-catch.
|
||||
(let ((frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (child)
|
||||
(dom-append frag (render-to-dom child env ns)))
|
||||
body-exprs)
|
||||
(dom-append container frag))
|
||||
(scope-pop! "sx-island-scope"))
|
||||
(fn (err)
|
||||
;; Pop scope first, then render fallback
|
||||
(scope-pop! "sx-island-scope")
|
||||
(let ((fallback-fn (trampoline (eval-expr fallback-expr env)))
|
||||
(retry-fn (fn () (swap! retry-version (fn (n) (+ n 1))))))
|
||||
(let ((fallback-dom
|
||||
(if (lambda? fallback-fn)
|
||||
(render-lambda-dom fallback-fn (list err retry-fn) env ns)
|
||||
(render-to-dom (apply fallback-fn (list err retry-fn)) env ns))))
|
||||
(dom-append container fallback-dom))))))))
|
||||
(dom-append container fallback-dom)))))))
|
||||
|
||||
container)))
|
||||
|
||||
|
||||
@@ -291,7 +291,7 @@
|
||||
(let ((local (env-merge (lambda-closure f) env)))
|
||||
(env-set! local (first (lambda-params f)) item)
|
||||
(aser (lambda-body f) local))
|
||||
(invoke f item)))
|
||||
(cek-call f (list item))))
|
||||
coll))
|
||||
|
||||
;; map-indexed
|
||||
@@ -304,7 +304,7 @@
|
||||
(env-set! local (first (lambda-params f)) i)
|
||||
(env-set! local (nth (lambda-params f) 1) item)
|
||||
(aser (lambda-body f) local))
|
||||
(invoke f i item)))
|
||||
(cek-call f (list i item))))
|
||||
coll))
|
||||
|
||||
;; for-each — evaluate for side effects, aser each body
|
||||
@@ -317,7 +317,7 @@
|
||||
(let ((local (env-merge (lambda-closure f) env)))
|
||||
(env-set! local (first (lambda-params f)) item)
|
||||
(append! results (aser (lambda-body f) local)))
|
||||
(invoke f item)))
|
||||
(cek-call f (list item))))
|
||||
coll)
|
||||
(if (empty? results) nil results))
|
||||
|
||||
|
||||
@@ -85,7 +85,12 @@ class PyEmitter:
|
||||
if name == "define-async":
|
||||
return self._emit_define_async(expr, indent)
|
||||
if name == "set!":
|
||||
return f"{pad}{self._mangle(expr[1].name)} = {self.emit(expr[2])}"
|
||||
varname = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
|
||||
py_var = self._mangle(varname)
|
||||
cell_vars = getattr(self, '_current_cell_vars', set())
|
||||
if py_var in cell_vars:
|
||||
return f"{pad}_cells[{self._py_string(py_var)}] = {self.emit(expr[2])}"
|
||||
return f"{pad}{py_var} = {self.emit(expr[2])}"
|
||||
if name == "when":
|
||||
return self._emit_when_stmt(expr, indent)
|
||||
if name == "do" or name == "begin":
|
||||
@@ -165,12 +170,6 @@ class PyEmitter:
|
||||
"signal-remove-sub!": "signal_remove_sub",
|
||||
"signal-deps": "signal_deps",
|
||||
"signal-set-deps!": "signal_set_deps",
|
||||
"set-tracking-context!": "set_tracking_context",
|
||||
"get-tracking-context": "get_tracking_context",
|
||||
"make-tracking-context": "make_tracking_context",
|
||||
"tracking-context-deps": "tracking_context_deps",
|
||||
"tracking-context-add-dep!": "tracking_context_add_dep",
|
||||
"tracking-context-notify-fn": "tracking_context_notify_fn",
|
||||
"identical?": "is_identical",
|
||||
"notify-subscribers": "notify_subscribers",
|
||||
"flush-subscribers": "flush_subscribers",
|
||||
@@ -179,7 +178,6 @@ class PyEmitter:
|
||||
"register-in-scope": "register_in_scope",
|
||||
"*batch-depth*": "_batch_depth",
|
||||
"*batch-queue*": "_batch_queue",
|
||||
"*island-scope*": "_island_scope",
|
||||
"*store-registry*": "_store_registry",
|
||||
"def-store": "def_store",
|
||||
"use-store": "use_store",
|
||||
@@ -754,15 +752,24 @@ class PyEmitter:
|
||||
nested_set_vars = self._find_nested_set_vars(body)
|
||||
def_kw = "async def" if is_async else "def"
|
||||
lines = [f"{pad}{def_kw} {py_name}({params_str}):"]
|
||||
if nested_set_vars:
|
||||
lines.append(f"{pad} _cells = {{}}")
|
||||
# Emit body with cell var tracking (and async context if needed)
|
||||
old_cells = getattr(self, '_current_cell_vars', set())
|
||||
if nested_set_vars and not old_cells:
|
||||
lines.append(f"{pad} _cells = {{}}")
|
||||
old_async = self._in_async
|
||||
self._current_cell_vars = nested_set_vars
|
||||
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)
|
||||
@@ -801,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:]
|
||||
@@ -817,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":
|
||||
@@ -847,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."""
|
||||
@@ -862,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."""
|
||||
@@ -875,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."""
|
||||
@@ -917,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."""
|
||||
@@ -942,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."""
|
||||
@@ -1129,17 +1305,23 @@ try:
|
||||
from .platform_py import (
|
||||
PREAMBLE, PLATFORM_PY, PRIMITIVES_PY_PRE, PRIMITIVES_PY_POST,
|
||||
PRIMITIVES_PY_MODULES, _ALL_PY_MODULES,
|
||||
PLATFORM_DEPS_PY, PLATFORM_ASYNC_PY, FIXUPS_PY, CONTINUATIONS_PY,
|
||||
PLATFORM_PARSER_PY,
|
||||
PLATFORM_DEPS_PY, PLATFORM_CEK_PY, CEK_FIXUPS_PY, PLATFORM_ASYNC_PY,
|
||||
FIXUPS_PY, CONTINUATIONS_PY,
|
||||
_assemble_primitives_py, public_api_py,
|
||||
ADAPTER_FILES, SPEC_MODULES, EXTENSION_NAMES, EXTENSION_FORMS,
|
||||
ADAPTER_FILES, SPEC_MODULES, SPEC_MODULE_ORDER,
|
||||
EXTENSION_NAMES, EXTENSION_FORMS,
|
||||
)
|
||||
except ImportError:
|
||||
from shared.sx.ref.platform_py import (
|
||||
PREAMBLE, PLATFORM_PY, PRIMITIVES_PY_PRE, PRIMITIVES_PY_POST,
|
||||
PRIMITIVES_PY_MODULES, _ALL_PY_MODULES,
|
||||
PLATFORM_DEPS_PY, PLATFORM_ASYNC_PY, FIXUPS_PY, CONTINUATIONS_PY,
|
||||
PLATFORM_PARSER_PY,
|
||||
PLATFORM_DEPS_PY, PLATFORM_CEK_PY, CEK_FIXUPS_PY, PLATFORM_ASYNC_PY,
|
||||
FIXUPS_PY, CONTINUATIONS_PY,
|
||||
_assemble_primitives_py, public_api_py,
|
||||
ADAPTER_FILES, SPEC_MODULES, EXTENSION_NAMES, EXTENSION_FORMS,
|
||||
ADAPTER_FILES, SPEC_MODULES, SPEC_MODULE_ORDER,
|
||||
EXTENSION_NAMES, EXTENSION_FORMS,
|
||||
)
|
||||
|
||||
|
||||
@@ -1227,7 +1409,7 @@ def compile_ref_to_py(
|
||||
|
||||
Args:
|
||||
adapters: List of adapter names to include.
|
||||
Valid names: html, sx.
|
||||
Valid names: parser, html, sx.
|
||||
None = include all server-side adapters.
|
||||
modules: List of primitive module names to include.
|
||||
core.* are always included. stdlib.* are opt-in.
|
||||
@@ -1280,7 +1462,11 @@ def compile_ref_to_py(
|
||||
spec_mod_set.add("page-helpers")
|
||||
if "router" in SPEC_MODULES:
|
||||
spec_mod_set.add("router")
|
||||
# CEK is the canonical evaluator — always include
|
||||
spec_mod_set.add("cek")
|
||||
spec_mod_set.add("frames")
|
||||
has_deps = "deps" in spec_mod_set
|
||||
has_cek = "cek" in spec_mod_set
|
||||
|
||||
# Core files always included, then selected adapters, then spec modules
|
||||
sx_files = [
|
||||
@@ -1288,11 +1474,20 @@ def compile_ref_to_py(
|
||||
("forms.sx", "forms (server definition forms)"),
|
||||
("render.sx", "render (core)"),
|
||||
]
|
||||
# Parser before html/sx — provides serialize used by adapters
|
||||
if "parser" in adapter_set:
|
||||
sx_files.append(ADAPTER_FILES["parser"])
|
||||
for name in ("html", "sx"):
|
||||
if name in adapter_set:
|
||||
sx_files.append(ADAPTER_FILES[name])
|
||||
# Use explicit ordering for spec modules (respects dependencies)
|
||||
for name in SPEC_MODULE_ORDER:
|
||||
if name in spec_mod_set:
|
||||
sx_files.append(SPEC_MODULES[name])
|
||||
# Any spec modules not in the order list (future-proofing)
|
||||
for name in sorted(spec_mod_set):
|
||||
sx_files.append(SPEC_MODULES[name])
|
||||
if name not in SPEC_MODULE_ORDER:
|
||||
sx_files.append(SPEC_MODULES[name])
|
||||
|
||||
# Pre-scan define-async names (needed before transpilation so emitter
|
||||
# knows which calls require 'await')
|
||||
@@ -1341,6 +1536,7 @@ def compile_ref_to_py(
|
||||
# Build output
|
||||
has_html = "html" in adapter_set
|
||||
has_sx = "sx" in adapter_set
|
||||
has_parser = "parser" in adapter_set
|
||||
|
||||
parts = []
|
||||
parts.append(PREAMBLE)
|
||||
@@ -1349,9 +1545,15 @@ def compile_ref_to_py(
|
||||
parts.append(_assemble_primitives_py(prim_modules))
|
||||
parts.append(PRIMITIVES_PY_POST)
|
||||
|
||||
if has_parser:
|
||||
parts.append(PLATFORM_PARSER_PY)
|
||||
|
||||
if has_deps:
|
||||
parts.append(PLATFORM_DEPS_PY)
|
||||
|
||||
if has_cek:
|
||||
parts.append(PLATFORM_CEK_PY)
|
||||
|
||||
if has_async:
|
||||
parts.append(PLATFORM_ASYNC_PY)
|
||||
|
||||
@@ -1363,6 +1565,8 @@ def compile_ref_to_py(
|
||||
parts.append("")
|
||||
|
||||
parts.append(FIXUPS_PY)
|
||||
if has_cek:
|
||||
parts.append(CEK_FIXUPS_PY)
|
||||
if has_continuations:
|
||||
parts.append(CONTINUATIONS_PY)
|
||||
parts.append(public_api_py(has_html, has_sx, has_deps, has_async))
|
||||
|
||||
@@ -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:
|
||||
@@ -283,10 +287,62 @@ def parse_boundary_sx() -> tuple[frozenset[str], dict[str, frozenset[str]]]:
|
||||
return frozenset(all_io), frozen_helpers
|
||||
|
||||
|
||||
def parse_boundary_effects() -> dict[str, list[str]]:
|
||||
"""Parse boundary.sx and return effect annotations for all declared primitives.
|
||||
|
||||
Returns a dict mapping primitive name to its declared effects list.
|
||||
E.g. {"current-user": ["io"], "reset!": ["mutation"], "signal": []}.
|
||||
|
||||
Only includes primitives that have an explicit :effects declaration.
|
||||
Pure primitives from primitives.sx are not included (they have no effects).
|
||||
"""
|
||||
source = _read_file("boundary.sx")
|
||||
exprs = _get_parse_all()(source)
|
||||
result: dict[str, list[str]] = {}
|
||||
|
||||
_DECL_FORMS = {
|
||||
"define-io-primitive", "declare-signal-primitive",
|
||||
"declare-spread-primitive",
|
||||
}
|
||||
|
||||
for expr in exprs:
|
||||
if not isinstance(expr, list) or len(expr) < 2:
|
||||
continue
|
||||
head = expr[0]
|
||||
if not isinstance(head, Symbol) or head.name not in _DECL_FORMS:
|
||||
continue
|
||||
|
||||
name = expr[1]
|
||||
if not isinstance(name, str):
|
||||
continue
|
||||
|
||||
effects_val = _extract_keyword_arg(expr, "effects")
|
||||
if effects_val is None:
|
||||
# IO primitives default to [io] if no explicit :effects
|
||||
if head.name == "define-io-primitive":
|
||||
result[name] = ["io"]
|
||||
continue
|
||||
|
||||
if isinstance(effects_val, list):
|
||||
effect_names = []
|
||||
for item in effects_val:
|
||||
if isinstance(item, Symbol):
|
||||
effect_names.append(item.name)
|
||||
elif isinstance(item, str):
|
||||
effect_names.append(item)
|
||||
result[name] = effect_names
|
||||
else:
|
||||
# Might be a single symbol
|
||||
if isinstance(effects_val, Symbol):
|
||||
result[name] = [effects_val.name]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
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)
|
||||
|
||||
1034
shared/sx/ref/cek.sx
Normal file
1034
shared/sx/ref/cek.sx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -530,7 +530,7 @@
|
||||
(if (and env new-html (not (empty? new-html)))
|
||||
;; Parse new content as SX and re-evaluate in island scope
|
||||
(let ((parsed (parse new-html)))
|
||||
(let ((sx-content (if transform (invoke transform parsed) parsed)))
|
||||
(let ((sx-content (if transform (cek-call transform (list parsed)) parsed)))
|
||||
;; Dispose old reactive bindings in this marsh
|
||||
(dispose-marsh-scope old-marsh)
|
||||
;; Evaluate the SX in a new marsh scope — creates new reactive bindings
|
||||
|
||||
@@ -941,14 +941,8 @@
|
||||
(let ((before (trampoline (eval-expr (first args) env)))
|
||||
(body (trampoline (eval-expr (nth args 1) env)))
|
||||
(after (trampoline (eval-expr (nth args 2) env))))
|
||||
;; Call entry thunk
|
||||
(call-thunk before env)
|
||||
;; Push wind record, run body, pop, call exit
|
||||
(push-wind! before after)
|
||||
(let ((result (call-thunk body env)))
|
||||
(pop-wind!)
|
||||
(call-thunk after env)
|
||||
result))))
|
||||
;; Delegate to platform — needs try/finally for error safety
|
||||
(dynamic-wind-call before body after env))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
262
shared/sx/ref/frames.sx
Normal file
262
shared/sx/ref/frames.sx
Normal file
@@ -0,0 +1,262 @@
|
||||
;; ==========================================================================
|
||||
;; frames.sx — CEK machine frame types
|
||||
;;
|
||||
;; Defines the continuation frame types used by the explicit CEK evaluator.
|
||||
;; Each frame represents a "what to do next" when a sub-evaluation completes.
|
||||
;;
|
||||
;; A CEK state is a dict:
|
||||
;; {:control expr — expression being evaluated (or nil in continue phase)
|
||||
;; :env env — current environment
|
||||
;; :kont list — continuation: list of frames (stack, head = top)
|
||||
;; :phase "eval"|"continue"
|
||||
;; :value any} — value produced (only in continue phase)
|
||||
;;
|
||||
;; Two-phase step function:
|
||||
;; step-eval: control is expression → dispatch → push frame + new control
|
||||
;; step-continue: value produced → pop frame → dispatch → new state
|
||||
;;
|
||||
;; Terminal state: phase = "continue" and kont is empty → value is final result.
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 1. CEK State constructors
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define make-cek-state
|
||||
(fn (control env kont)
|
||||
{:control control :env env :kont kont :phase "eval" :value nil}))
|
||||
|
||||
(define make-cek-value
|
||||
(fn (value env kont)
|
||||
{:control nil :env env :kont kont :phase "continue" :value value}))
|
||||
|
||||
(define cek-terminal?
|
||||
(fn (state)
|
||||
(and (= (get state "phase") "continue")
|
||||
(empty? (get state "kont")))))
|
||||
|
||||
(define cek-control (fn (s) (get s "control")))
|
||||
(define cek-env (fn (s) (get s "env")))
|
||||
(define cek-kont (fn (s) (get s "kont")))
|
||||
(define cek-phase (fn (s) (get s "phase")))
|
||||
(define cek-value (fn (s) (get s "value")))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 2. Frame constructors
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Each frame type is a dict with a "type" key and frame-specific data.
|
||||
|
||||
;; IfFrame: waiting for condition value
|
||||
;; After condition evaluates, choose then or else branch
|
||||
(define make-if-frame
|
||||
(fn (then-expr else-expr env)
|
||||
{:type "if" :then then-expr :else else-expr :env env}))
|
||||
|
||||
;; WhenFrame: waiting for condition value
|
||||
;; If truthy, evaluate body exprs sequentially
|
||||
(define make-when-frame
|
||||
(fn (body-exprs env)
|
||||
{:type "when" :body body-exprs :env env}))
|
||||
|
||||
;; BeginFrame: sequential evaluation
|
||||
;; Remaining expressions to evaluate after current one
|
||||
(define make-begin-frame
|
||||
(fn (remaining env)
|
||||
{:type "begin" :remaining remaining :env env}))
|
||||
|
||||
;; LetFrame: binding evaluation in progress
|
||||
;; name = current binding name, remaining = remaining (name val) pairs
|
||||
;; body = body expressions to evaluate after all bindings
|
||||
(define make-let-frame
|
||||
(fn (name remaining body local)
|
||||
{:type "let" :name name :remaining remaining :body body :env local}))
|
||||
|
||||
;; DefineFrame: waiting for value to bind
|
||||
(define make-define-frame
|
||||
(fn (name env has-effects effect-list)
|
||||
{:type "define" :name name :env env
|
||||
:has-effects has-effects :effect-list effect-list}))
|
||||
|
||||
;; SetFrame: waiting for value to assign
|
||||
(define make-set-frame
|
||||
(fn (name env)
|
||||
{:type "set" :name name :env env}))
|
||||
|
||||
;; ArgFrame: evaluating function arguments
|
||||
;; f = function value (already evaluated), evaled = already evaluated args
|
||||
;; remaining = remaining arg expressions
|
||||
(define make-arg-frame
|
||||
(fn (f evaled remaining env raw-args)
|
||||
{:type "arg" :f f :evaled evaled :remaining remaining :env env
|
||||
:raw-args raw-args}))
|
||||
|
||||
;; CallFrame: about to call with fully evaluated args
|
||||
(define make-call-frame
|
||||
(fn (f args env)
|
||||
{:type "call" :f f :args args :env env}))
|
||||
|
||||
;; CondFrame: evaluating cond clauses
|
||||
(define make-cond-frame
|
||||
(fn (remaining env scheme?)
|
||||
{:type "cond" :remaining remaining :env env :scheme scheme?}))
|
||||
|
||||
;; CaseFrame: evaluating case clauses
|
||||
(define make-case-frame
|
||||
(fn (match-val remaining env)
|
||||
{:type "case" :match-val match-val :remaining remaining :env env}))
|
||||
|
||||
;; ThreadFirstFrame: pipe threading
|
||||
(define make-thread-frame
|
||||
(fn (remaining env)
|
||||
{:type "thread" :remaining remaining :env env}))
|
||||
|
||||
;; MapFrame: higher-order map/map-indexed in progress
|
||||
(define make-map-frame
|
||||
(fn (f remaining results env)
|
||||
{:type "map" :f f :remaining remaining :results results :env env :indexed false}))
|
||||
|
||||
(define make-map-indexed-frame
|
||||
(fn (f remaining results env)
|
||||
{:type "map" :f f :remaining remaining :results results :env env :indexed true}))
|
||||
|
||||
;; FilterFrame: higher-order filter in progress
|
||||
(define make-filter-frame
|
||||
(fn (f remaining results current-item env)
|
||||
{:type "filter" :f f :remaining remaining :results results
|
||||
:current-item current-item :env env}))
|
||||
|
||||
;; ReduceFrame: higher-order reduce in progress
|
||||
(define make-reduce-frame
|
||||
(fn (f remaining env)
|
||||
{:type "reduce" :f f :remaining remaining :env env}))
|
||||
|
||||
;; ForEachFrame: higher-order for-each in progress
|
||||
(define make-for-each-frame
|
||||
(fn (f remaining env)
|
||||
{:type "for-each" :f f :remaining remaining :env env}))
|
||||
|
||||
;; SomeFrame: higher-order some (short-circuit on first truthy)
|
||||
(define make-some-frame
|
||||
(fn (f remaining env)
|
||||
{:type "some" :f f :remaining remaining :env env}))
|
||||
|
||||
;; EveryFrame: higher-order every? (short-circuit on first falsy)
|
||||
(define make-every-frame
|
||||
(fn (f remaining env)
|
||||
{:type "every" :f f :remaining remaining :env env}))
|
||||
|
||||
;; ScopeFrame: scope-pop! when frame pops
|
||||
(define make-scope-frame
|
||||
(fn (name remaining env)
|
||||
{:type "scope" :name name :remaining remaining :env env}))
|
||||
|
||||
;; ResetFrame: delimiter for shift/reset continuations
|
||||
(define make-reset-frame
|
||||
(fn (env)
|
||||
{:type "reset" :env env}))
|
||||
|
||||
;; DictFrame: evaluating dict values
|
||||
(define make-dict-frame
|
||||
(fn (remaining results env)
|
||||
{:type "dict" :remaining remaining :results results :env env}))
|
||||
|
||||
;; AndFrame: short-circuit and
|
||||
(define make-and-frame
|
||||
(fn (remaining env)
|
||||
{:type "and" :remaining remaining :env env}))
|
||||
|
||||
;; OrFrame: short-circuit or
|
||||
(define make-or-frame
|
||||
(fn (remaining env)
|
||||
{:type "or" :remaining remaining :env env}))
|
||||
|
||||
;; QuasiquoteFrame (not a real frame — QQ is handled specially)
|
||||
|
||||
;; DynamicWindFrame: phases of dynamic-wind
|
||||
(define make-dynamic-wind-frame
|
||||
(fn (phase body-thunk after-thunk env)
|
||||
{:type "dynamic-wind" :phase phase
|
||||
:body-thunk body-thunk :after-thunk after-thunk :env env}))
|
||||
|
||||
;; ReactiveResetFrame: delimiter for reactive deref-as-shift
|
||||
;; Carries an update-fn that gets called with new values on re-render.
|
||||
(define make-reactive-reset-frame
|
||||
(fn (env update-fn first-render?)
|
||||
{:type "reactive-reset" :env env :update-fn update-fn
|
||||
:first-render first-render?}))
|
||||
|
||||
;; DerefFrame: awaiting evaluation of deref's argument
|
||||
(define make-deref-frame
|
||||
(fn (env)
|
||||
{:type "deref" :env env}))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3. Frame accessors
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define frame-type (fn (f) (get f "type")))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 4. Continuation operations
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define kont-push
|
||||
(fn (frame kont) (cons frame kont)))
|
||||
|
||||
(define kont-top
|
||||
(fn (kont) (first kont)))
|
||||
|
||||
(define kont-pop
|
||||
(fn (kont) (rest kont)))
|
||||
|
||||
(define kont-empty?
|
||||
(fn (kont) (empty? kont)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 5. CEK shift/reset support
|
||||
;; --------------------------------------------------------------------------
|
||||
;; shift captures all frames up to the nearest ResetFrame.
|
||||
;; reset pushes a ResetFrame.
|
||||
|
||||
(define kont-capture-to-reset
|
||||
(fn (kont)
|
||||
;; Returns (captured-frames remaining-kont).
|
||||
;; captured-frames: frames from top up to (not including) ResetFrame.
|
||||
;; remaining-kont: frames after ResetFrame.
|
||||
;; Stops at either "reset" or "reactive-reset" frames.
|
||||
(define scan
|
||||
(fn (k captured)
|
||||
(if (empty? k)
|
||||
(error "shift without enclosing reset")
|
||||
(let ((frame (first k)))
|
||||
(if (or (= (frame-type frame) "reset")
|
||||
(= (frame-type frame) "reactive-reset"))
|
||||
(list captured (rest k))
|
||||
(scan (rest k) (append captured (list frame))))))))
|
||||
(scan kont (list))))
|
||||
|
||||
;; Check if a ReactiveResetFrame exists anywhere in the continuation
|
||||
(define has-reactive-reset-frame?
|
||||
(fn (kont)
|
||||
(if (empty? kont) false
|
||||
(if (= (frame-type (first kont)) "reactive-reset") true
|
||||
(has-reactive-reset-frame? (rest kont))))))
|
||||
|
||||
;; Capture frames up to nearest ReactiveResetFrame.
|
||||
;; Returns (captured-frames, reset-frame, remaining-kont).
|
||||
(define kont-capture-to-reactive-reset
|
||||
(fn (kont)
|
||||
(define scan
|
||||
(fn (k captured)
|
||||
(if (empty? k)
|
||||
(error "reactive deref without enclosing reactive-reset")
|
||||
(let ((frame (first k)))
|
||||
(if (= (frame-type frame) "reactive-reset")
|
||||
(list captured frame (rest k))
|
||||
(scan (rest k) (append captured (list frame))))))))
|
||||
(scan kont (list))))
|
||||
@@ -87,12 +87,6 @@
|
||||
"signal-remove-sub!" "signalRemoveSub"
|
||||
"signal-deps" "signalDeps"
|
||||
"signal-set-deps!" "signalSetDeps"
|
||||
"set-tracking-context!" "setTrackingContext"
|
||||
"get-tracking-context" "getTrackingContext"
|
||||
"make-tracking-context" "makeTrackingContext"
|
||||
"tracking-context-deps" "trackingContextDeps"
|
||||
"tracking-context-add-dep!" "trackingContextAddDep"
|
||||
"tracking-context-notify-fn" "trackingContextNotifyFn"
|
||||
"identical?" "isIdentical"
|
||||
"notify-subscribers" "notifySubscribers"
|
||||
"flush-subscribers" "flushSubscribers"
|
||||
@@ -101,7 +95,6 @@
|
||||
"register-in-scope" "registerInScope"
|
||||
"*batch-depth*" "_batchDepth"
|
||||
"*batch-queue*" "_batchQueue"
|
||||
"*island-scope*" "_islandScope"
|
||||
"*store-registry*" "_storeRegistry"
|
||||
"def-store" "defStore"
|
||||
"use-store" "useStore"
|
||||
@@ -221,6 +214,10 @@
|
||||
"render-dom-island" "renderDomIsland"
|
||||
"reactive-text" "reactiveText"
|
||||
"reactive-attr" "reactiveAttr"
|
||||
"cek-reactive-text" "cekReactiveText"
|
||||
"cek-reactive-attr" "cekReactiveAttr"
|
||||
"*use-cek-reactive*" "_useCekReactive"
|
||||
"enable-cek-reactive!" "enableCekReactive"
|
||||
"reactive-fragment" "reactiveFragment"
|
||||
"reactive-list" "reactiveList"
|
||||
"dom-create-element" "domCreateElement"
|
||||
@@ -527,6 +524,80 @@
|
||||
"collect!" "sxCollect"
|
||||
"collected" "sxCollected"
|
||||
"clear-collected!" "sxClearCollected"
|
||||
"make-cek-continuation" "makeCekContinuation"
|
||||
"continuation-data" "continuationData"
|
||||
"make-cek-state" "makeCekState"
|
||||
"make-cek-value" "makeCekValue"
|
||||
"cek-terminal?" "cekTerminal_p"
|
||||
"cek-run" "cekRun"
|
||||
"cek-step" "cekStep"
|
||||
"cek-control" "cekControl"
|
||||
"cek-env" "cekEnv"
|
||||
"cek-kont" "cekKont"
|
||||
"cek-phase" "cekPhase"
|
||||
"cek-value" "cekValue"
|
||||
"kont-push" "kontPush"
|
||||
"kont-top" "kontTop"
|
||||
"kont-pop" "kontPop"
|
||||
"kont-empty?" "kontEmpty_p"
|
||||
"kont-capture-to-reset" "kontCaptureToReset"
|
||||
"kont-capture-to-reactive-reset" "kontCaptureToReactiveReset"
|
||||
"has-reactive-reset-frame?" "hasReactiveResetFrame_p"
|
||||
"frame-type" "frameType"
|
||||
"make-if-frame" "makeIfFrame"
|
||||
"make-when-frame" "makeWhenFrame"
|
||||
"make-begin-frame" "makeBeginFrame"
|
||||
"make-let-frame" "makeLetFrame"
|
||||
"make-define-frame" "makeDefineFrame"
|
||||
"make-set-frame" "makeSetFrame"
|
||||
"make-arg-frame" "makeArgFrame"
|
||||
"make-call-frame" "makeCallFrame"
|
||||
"make-cond-frame" "makeCondFrame"
|
||||
"make-case-frame" "makeCaseFrame"
|
||||
"make-thread-frame" "makeThreadFrame"
|
||||
"make-map-frame" "makeMapFrame"
|
||||
"make-filter-frame" "makeFilterFrame"
|
||||
"make-reduce-frame" "makeReduceFrame"
|
||||
"make-for-each-frame" "makeForEachFrame"
|
||||
"make-scope-frame" "makeScopeFrame"
|
||||
"make-reset-frame" "makeResetFrame"
|
||||
"make-dict-frame" "makeDictFrame"
|
||||
"make-and-frame" "makeAndFrame"
|
||||
"make-or-frame" "makeOrFrame"
|
||||
"make-dynamic-wind-frame" "makeDynamicWindFrame"
|
||||
"make-reactive-reset-frame" "makeReactiveResetFrame"
|
||||
"make-deref-frame" "makeDerefFrame"
|
||||
"step-eval" "stepEval"
|
||||
"step-continue" "stepContinue"
|
||||
"step-eval-list" "stepEvalList"
|
||||
"step-eval-call" "stepEvalCall"
|
||||
"step-sf-if" "stepSfIf"
|
||||
"step-sf-when" "stepSfWhen"
|
||||
"step-sf-begin" "stepSfBegin"
|
||||
"step-sf-let" "stepSfLet"
|
||||
"step-sf-define" "stepSfDefine"
|
||||
"step-sf-set!" "stepSfSet"
|
||||
"step-sf-and" "stepSfAnd"
|
||||
"step-sf-or" "stepSfOr"
|
||||
"step-sf-cond" "stepSfCond"
|
||||
"step-sf-case" "stepSfCase"
|
||||
"step-sf-thread-first" "stepSfThreadFirst"
|
||||
"step-sf-lambda" "stepSfLambda"
|
||||
"step-sf-scope" "stepSfScope"
|
||||
"step-sf-provide" "stepSfProvide"
|
||||
"step-sf-reset" "stepSfReset"
|
||||
"step-sf-shift" "stepSfShift"
|
||||
"step-sf-deref" "stepSfDeref"
|
||||
"step-ho-map" "stepHoMap"
|
||||
"step-ho-filter" "stepHoFilter"
|
||||
"step-ho-reduce" "stepHoReduce"
|
||||
"step-ho-for-each" "stepHoForEach"
|
||||
"continue-with-call" "continueWithCall"
|
||||
"sf-case-step-loop" "sfCaseStepLoop"
|
||||
"eval-expr-cek" "evalExprCek"
|
||||
"trampoline-cek" "trampolineCek"
|
||||
"reactive-shift-deref" "reactiveShiftDeref"
|
||||
"cond-scheme?" "condScheme_p"
|
||||
"scope-push!" "scopePush"
|
||||
"scope-pop!" "scopePop"
|
||||
"provide-push!" "providePush"
|
||||
@@ -584,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
|
||||
|
||||
@@ -46,8 +46,14 @@ SPEC_MODULES = {
|
||||
"router": ("router.sx", "router (client-side route matching)"),
|
||||
"signals": ("signals.sx", "signals (reactive signal runtime)"),
|
||||
"page-helpers": ("page-helpers.sx", "page-helpers (pure data transformation helpers)"),
|
||||
"frames": ("frames.sx", "frames (CEK continuation frames)"),
|
||||
"cek": ("cek.sx", "cek (explicit CEK machine evaluator)"),
|
||||
}
|
||||
|
||||
# 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", "cek", "signals"]
|
||||
|
||||
|
||||
EXTENSION_NAMES = {"continuations"}
|
||||
CONTINUATIONS_JS = '''
|
||||
@@ -851,20 +857,6 @@ PREAMBLE = '''\
|
||||
}
|
||||
Island.prototype._island = true;
|
||||
|
||||
function SxSignal(value) {
|
||||
this.value = value;
|
||||
this.subscribers = [];
|
||||
this.deps = [];
|
||||
}
|
||||
SxSignal.prototype._signal = true;
|
||||
|
||||
function TrackingCtx(notifyFn) {
|
||||
this.notifyFn = notifyFn;
|
||||
this.deps = [];
|
||||
}
|
||||
|
||||
var _trackingContext = null;
|
||||
|
||||
function Macro(params, restParam, body, closure, name) {
|
||||
this.params = params;
|
||||
this.restParam = restParam;
|
||||
@@ -1012,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;
|
||||
@@ -1148,7 +1140,6 @@ PLATFORM_JS_PRE = '''
|
||||
if (x._lambda) return "lambda";
|
||||
if (x._component) return "component";
|
||||
if (x._island) return "island";
|
||||
if (x._signal) return "signal";
|
||||
if (x._spread) return "spread";
|
||||
if (x._macro) return "macro";
|
||||
if (x._raw) return "raw-html";
|
||||
@@ -1259,34 +1250,6 @@ PLATFORM_JS_PRE = '''
|
||||
return new Island(name, params, hasChildren, body, merge(env));
|
||||
}
|
||||
|
||||
// Signal platform
|
||||
function makeSignal(value) { return new SxSignal(value); }
|
||||
function isSignal(x) { return x != null && x._signal === true; }
|
||||
function signalValue(s) { return s.value; }
|
||||
function signalSetValue(s, v) { s.value = v; }
|
||||
function signalSubscribers(s) { return s.subscribers.slice(); }
|
||||
function signalAddSub(s, fn) { if (s.subscribers.indexOf(fn) < 0) s.subscribers.push(fn); }
|
||||
function signalRemoveSub(s, fn) { var i = s.subscribers.indexOf(fn); if (i >= 0) s.subscribers.splice(i, 1); }
|
||||
function signalDeps(s) { return s.deps.slice(); }
|
||||
function signalSetDeps(s, deps) { s.deps = Array.isArray(deps) ? deps.slice() : []; }
|
||||
function setTrackingContext(ctx) { _trackingContext = ctx; }
|
||||
function getTrackingContext() { return _trackingContext || NIL; }
|
||||
function makeTrackingContext(notifyFn) { return new TrackingCtx(notifyFn); }
|
||||
function trackingContextDeps(ctx) { return ctx ? ctx.deps : []; }
|
||||
function trackingContextAddDep(ctx, s) { if (ctx && ctx.deps.indexOf(s) < 0) ctx.deps.push(s); }
|
||||
function trackingContextNotifyFn(ctx) { return ctx ? ctx.notifyFn : NIL; }
|
||||
|
||||
// invoke — call any callable (native fn or SX lambda) with args.
|
||||
// Transpiled code emits direct calls f(args) which fail on SX lambdas
|
||||
// from runtime-evaluated island bodies. invoke dispatches correctly.
|
||||
function invoke() {
|
||||
var f = arguments[0];
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
if (isLambda(f)) return trampoline(callLambda(f, args, lambdaClosure(f)));
|
||||
if (typeof f === 'function') return f.apply(null, args);
|
||||
return NIL;
|
||||
}
|
||||
|
||||
// JSON / dict helpers for island state serialization
|
||||
function jsonSerialize(obj) {
|
||||
return JSON.stringify(obj);
|
||||
@@ -1389,6 +1352,9 @@ PLATFORM_JS_POST = '''
|
||||
}
|
||||
function mapDict(fn, d) { var r = {}; for (var k in d) r[k] = fn(k, d[k]); return r; }
|
||||
|
||||
// Predicate aliases used by transpiled code
|
||||
var isDict = PRIMITIVES["dict?"];
|
||||
|
||||
// List primitives used directly by transpiled code
|
||||
var len = PRIMITIVES["len"];
|
||||
var first = PRIMITIVES["first"];
|
||||
@@ -1505,6 +1471,46 @@ PLATFORM_JS_POST = '''
|
||||
};'''
|
||||
|
||||
|
||||
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"];
|
||||
var zip_pairs = PRIMITIVES["zip-pairs"];
|
||||
|
||||
var continuation_p = PRIMITIVES["continuation?"];
|
||||
|
||||
function makeCekContinuation(captured, restKont) {
|
||||
var c = new Continuation(function(v) { return v !== undefined ? v : NIL; });
|
||||
c._cek_data = {"captured": captured, "rest-kont": restKont};
|
||||
return c;
|
||||
}
|
||||
function continuationData(c) {
|
||||
return (c && c._cek_data) ? c._cek_data : {};
|
||||
}
|
||||
'''
|
||||
|
||||
# Iterative override for cek_run — replaces transpiled recursive version
|
||||
CEK_FIXUPS_JS = '''
|
||||
// Override recursive cekRun with iterative loop (avoids stack overflow)
|
||||
cekRun = function(state) {
|
||||
while (!cekTerminal_p(state)) { state = cekStep(state); }
|
||||
return cekValue(state);
|
||||
};
|
||||
'''
|
||||
|
||||
|
||||
PLATFORM_DEPS_JS = '''
|
||||
// =========================================================================
|
||||
// Platform: deps module — component dependency analysis
|
||||
@@ -1599,10 +1605,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); }
|
||||
@@ -1792,8 +1798,8 @@ PLATFORM_DOM_JS = """
|
||||
// If lambda takes 0 params, call without event arg (convenience for on-click handlers)
|
||||
var wrapped = isLambda(handler)
|
||||
? (lambdaParams(handler).length === 0
|
||||
? function(e) { try { invoke(handler); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
|
||||
: function(e) { try { invoke(handler, e); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } })
|
||||
? function(e) { try { cekCall(handler, NIL); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
|
||||
: function(e) { try { cekCall(handler, [e]); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } })
|
||||
: handler;
|
||||
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
|
||||
el.addEventListener(name, wrapped);
|
||||
@@ -2427,6 +2433,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);
|
||||
}
|
||||
@@ -2516,8 +2526,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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -2542,21 +2556,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);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -2994,7 +3012,6 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_
|
||||
PRIMITIVES["stop-propagation"] = stopPropagation_;
|
||||
PRIMITIVES["error-message"] = errorMessage;
|
||||
PRIMITIVES["schedule-idle"] = scheduleIdle;
|
||||
PRIMITIVES["invoke"] = invoke;
|
||||
PRIMITIVES["error"] = function(msg) { throw new Error(msg); };
|
||||
PRIMITIVES["filter"] = filter;
|
||||
// DOM primitives for sx-on:* handlers and data-init scripts
|
||||
@@ -3067,7 +3084,7 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps=False, has_router=False, has_signals=False, has_page_helpers=False):
|
||||
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps=False, has_router=False, has_signals=False, has_page_helpers=False, has_cek=False):
|
||||
# Parser: use compiled sxParse from parser.sx, or inline a minimal fallback
|
||||
if has_parser:
|
||||
parser = '''
|
||||
@@ -3263,6 +3280,15 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has
|
||||
api_lines.append(' context: sxContext,')
|
||||
api_lines.append(' emit: sxEmit,')
|
||||
api_lines.append(' emitted: sxEmitted,')
|
||||
if has_cek:
|
||||
api_lines.append(' cekRun: cekRun,')
|
||||
api_lines.append(' makeCekState: makeCekState,')
|
||||
api_lines.append(' makeCekValue: makeCekValue,')
|
||||
api_lines.append(' cekStep: cekStep,')
|
||||
api_lines.append(' cekTerminal: cekTerminal_p,')
|
||||
api_lines.append(' cekValue: cekValue,')
|
||||
api_lines.append(' makeReactiveResetFrame: makeReactiveResetFrame,')
|
||||
api_lines.append(' evalExpr: evalExpr,')
|
||||
api_lines.append(f' _version: "{version}"')
|
||||
api_lines.append(' };')
|
||||
api_lines.append('')
|
||||
|
||||
@@ -225,8 +225,6 @@ def type_of(x):
|
||||
return "component"
|
||||
if isinstance(x, Island):
|
||||
return "island"
|
||||
if isinstance(x, _Signal):
|
||||
return "signal"
|
||||
if isinstance(x, _Spread):
|
||||
return "spread"
|
||||
if isinstance(x, Macro):
|
||||
@@ -468,105 +466,6 @@ def is_identical(a, b):
|
||||
return a is b
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Signal platform -- reactive state primitives
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
class _Signal:
|
||||
"""Reactive signal container."""
|
||||
__slots__ = ("value", "subscribers", "deps")
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
self.subscribers = []
|
||||
self.deps = []
|
||||
|
||||
|
||||
class _TrackingContext:
|
||||
"""Context for discovering signal dependencies."""
|
||||
__slots__ = ("notify_fn", "deps")
|
||||
def __init__(self, notify_fn):
|
||||
self.notify_fn = notify_fn
|
||||
self.deps = []
|
||||
|
||||
|
||||
_tracking_context = None
|
||||
|
||||
|
||||
def make_signal(value):
|
||||
return _Signal(value)
|
||||
|
||||
|
||||
def is_signal(x):
|
||||
return isinstance(x, _Signal)
|
||||
|
||||
|
||||
def signal_value(s):
|
||||
return s.value if isinstance(s, _Signal) else s
|
||||
|
||||
|
||||
def signal_set_value(s, v):
|
||||
if isinstance(s, _Signal):
|
||||
s.value = v
|
||||
|
||||
|
||||
def signal_subscribers(s):
|
||||
return list(s.subscribers) if isinstance(s, _Signal) else []
|
||||
|
||||
|
||||
def signal_add_sub(s, fn):
|
||||
if isinstance(s, _Signal) and fn not in s.subscribers:
|
||||
s.subscribers.append(fn)
|
||||
|
||||
|
||||
def signal_remove_sub(s, fn):
|
||||
if isinstance(s, _Signal) and fn in s.subscribers:
|
||||
s.subscribers.remove(fn)
|
||||
|
||||
|
||||
def signal_deps(s):
|
||||
return list(s.deps) if isinstance(s, _Signal) else []
|
||||
|
||||
|
||||
def signal_set_deps(s, deps):
|
||||
if isinstance(s, _Signal):
|
||||
s.deps = list(deps) if isinstance(deps, list) else []
|
||||
|
||||
|
||||
def set_tracking_context(ctx):
|
||||
global _tracking_context
|
||||
_tracking_context = ctx
|
||||
|
||||
|
||||
def get_tracking_context():
|
||||
global _tracking_context
|
||||
return _tracking_context if _tracking_context is not None else NIL
|
||||
|
||||
|
||||
def make_tracking_context(notify_fn):
|
||||
return _TrackingContext(notify_fn)
|
||||
|
||||
|
||||
def tracking_context_deps(ctx):
|
||||
return ctx.deps if isinstance(ctx, _TrackingContext) else []
|
||||
|
||||
|
||||
def tracking_context_add_dep(ctx, s):
|
||||
if isinstance(ctx, _TrackingContext) and s not in ctx.deps:
|
||||
ctx.deps.append(s)
|
||||
|
||||
|
||||
def tracking_context_notify_fn(ctx):
|
||||
return ctx.notify_fn if isinstance(ctx, _TrackingContext) else NIL
|
||||
|
||||
|
||||
def invoke(f, *args):
|
||||
"""Call f with args — handles both native callables and SX lambdas.
|
||||
|
||||
In Python, all transpiled lambdas are natively callable, so this is
|
||||
just a direct call. The JS host needs dispatch logic here because
|
||||
SX lambdas from runtime-evaluated code are objects, not functions.
|
||||
"""
|
||||
return f(*args)
|
||||
|
||||
|
||||
def json_serialize(obj):
|
||||
@@ -751,51 +650,6 @@ def escape_string(s):
|
||||
.replace("</script", "<\\\\/script"))
|
||||
|
||||
|
||||
def serialize(val):
|
||||
"""Serialize an SX value to SX source text.
|
||||
|
||||
Note: parser.sx defines sx-serialize with a serialize alias, but parser.sx
|
||||
is only included in JS builds (for client-side parsing). Python builds
|
||||
provide this as a platform function.
|
||||
"""
|
||||
t = type_of(val)
|
||||
if t == "sx-expr":
|
||||
return val.source
|
||||
if t == "nil":
|
||||
return "nil"
|
||||
if t == "boolean":
|
||||
return "true" if val else "false"
|
||||
if t == "number":
|
||||
return str(val)
|
||||
if t == "string":
|
||||
return '"' + escape_string(val) + '"'
|
||||
if t == "symbol":
|
||||
return symbol_name(val)
|
||||
if t == "keyword":
|
||||
return ":" + keyword_name(val)
|
||||
if t == "raw-html":
|
||||
escaped = escape_string(raw_html_content(val))
|
||||
return '(raw! "' + escaped + '")'
|
||||
if t == "list":
|
||||
if not val:
|
||||
return "()"
|
||||
items = [serialize(x) for x in val]
|
||||
return "(" + " ".join(items) + ")"
|
||||
if t == "dict":
|
||||
items = []
|
||||
for k, v in val.items():
|
||||
items.append(":" + str(k))
|
||||
items.append(serialize(v))
|
||||
return "{" + " ".join(items) + "}"
|
||||
if callable(val):
|
||||
return "nil"
|
||||
return str(val)
|
||||
|
||||
# Aliases for transpiled code — parser.sx defines sx-serialize/sx-serialize-dict
|
||||
# but parser.sx is JS-only. Provide aliases so transpiled render.sx works.
|
||||
sx_serialize = serialize
|
||||
sx_serialize_dict = lambda d: serialize(d)
|
||||
|
||||
_SPECIAL_FORM_NAMES = frozenset() # Placeholder — overridden by transpiled adapter-sx.sx
|
||||
_HO_FORM_NAMES = frozenset()
|
||||
|
||||
@@ -1095,6 +949,37 @@ def for_each_indexed(fn, coll):
|
||||
def map_dict(fn, d):
|
||||
return {k: fn(k, v) for k, v in d.items()}
|
||||
|
||||
# Dynamic wind support (used by sf-dynamic-wind in eval.sx)
|
||||
_wind_stack = []
|
||||
|
||||
def push_wind_b(before, after):
|
||||
_wind_stack.append((before, after))
|
||||
return NIL
|
||||
|
||||
def pop_wind_b():
|
||||
if _wind_stack:
|
||||
_wind_stack.pop()
|
||||
return NIL
|
||||
|
||||
def call_thunk(f, env):
|
||||
"""Call a zero-arg function/lambda."""
|
||||
if is_callable(f) and not is_lambda(f):
|
||||
return f()
|
||||
if is_lambda(f):
|
||||
return trampoline(call_lambda(f, [], env))
|
||||
return trampoline(eval_expr([f], env))
|
||||
|
||||
def dynamic_wind_call(before, body, after, env):
|
||||
"""Execute dynamic-wind with try/finally for error safety."""
|
||||
call_thunk(before, env)
|
||||
push_wind_b(before, after)
|
||||
try:
|
||||
result = call_thunk(body, env)
|
||||
finally:
|
||||
pop_wind_b()
|
||||
call_thunk(after, env)
|
||||
return result
|
||||
|
||||
# Aliases used directly by transpiled code
|
||||
first = PRIMITIVES["first"]
|
||||
last = PRIMITIVES["last"]
|
||||
@@ -1124,8 +1009,58 @@ replace = PRIMITIVES["replace"]
|
||||
parse_int = PRIMITIVES["parse-int"]
|
||||
upper = PRIMITIVES["upper"]
|
||||
has_key_p = PRIMITIVES["has-key?"]
|
||||
dict_p = PRIMITIVES["dict?"]
|
||||
dissoc = PRIMITIVES["dissoc"]
|
||||
index_of = PRIMITIVES["index-of"]
|
||||
lower = PRIMITIVES["lower"]
|
||||
char_from_code = PRIMITIVES["char-from-code"]
|
||||
'''
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Platform: parser module — character classification, number parsing,
|
||||
# reader macro registry
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
PLATFORM_PARSER_PY = '''
|
||||
# =========================================================================
|
||||
# Platform interface — Parser
|
||||
# =========================================================================
|
||||
|
||||
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_~*+\\-><=/!?.:&/#,]")
|
||||
|
||||
|
||||
def ident_start_p(ch):
|
||||
return bool(_IDENT_START_RE.match(ch))
|
||||
|
||||
|
||||
def ident_char_p(ch):
|
||||
return bool(_IDENT_CHAR_RE.match(ch))
|
||||
|
||||
|
||||
def parse_number(s):
|
||||
"""Parse a numeric string to int or float."""
|
||||
try:
|
||||
if "." in s or "e" in s or "E" in s:
|
||||
return float(s)
|
||||
return int(s)
|
||||
except (ValueError, TypeError):
|
||||
return float(s)
|
||||
|
||||
|
||||
# Reader macro registry
|
||||
_reader_macros = {}
|
||||
|
||||
|
||||
def reader_macro_get(name):
|
||||
return _reader_macros.get(name, NIL)
|
||||
|
||||
|
||||
def reader_macro_set_b(name, handler):
|
||||
_reader_macros[name] = handler
|
||||
return NIL
|
||||
'''
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1184,6 +1119,60 @@ PLATFORM_DEPS_PY = (
|
||||
' c.io_refs = set(refs) if not isinstance(refs, set) else refs\n'
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Platform: CEK module — explicit CEK machine support
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
PLATFORM_CEK_PY = '''
|
||||
# =========================================================================
|
||||
# Platform: CEK module — explicit CEK machine
|
||||
# =========================================================================
|
||||
|
||||
# Standalone aliases for primitives used by cek.sx / frames.sx
|
||||
inc = PRIMITIVES["inc"]
|
||||
dec = PRIMITIVES["dec"]
|
||||
zip_pairs = PRIMITIVES["zip-pairs"]
|
||||
|
||||
continuation_p = PRIMITIVES["continuation?"]
|
||||
|
||||
def make_cek_continuation(captured, rest_kont):
|
||||
"""Create a Continuation storing captured CEK frames as data."""
|
||||
c = Continuation(lambda v=NIL: v)
|
||||
c._cek_data = {"captured": captured, "rest-kont": rest_kont}
|
||||
return c
|
||||
|
||||
def continuation_data(c):
|
||||
"""Return the _cek_data dict from a CEK continuation."""
|
||||
return getattr(c, '_cek_data', {}) or {}
|
||||
'''
|
||||
|
||||
# Iterative override for cek_run — replaces transpiled recursive version
|
||||
CEK_FIXUPS_PY = '''
|
||||
# Override recursive cek_run with iterative loop (avoids Python stack overflow)
|
||||
def cek_run(state):
|
||||
"""Drive CEK machine to completion (iterative)."""
|
||||
while not cek_terminal_p(state):
|
||||
state = cek_step(state)
|
||||
return cek_value(state)
|
||||
|
||||
# CEK is the canonical evaluator — override eval_expr to use it.
|
||||
# The tree-walk evaluator (eval_expr from eval.sx) is superseded.
|
||||
_tree_walk_eval_expr = eval_expr
|
||||
|
||||
def eval_expr(expr, env):
|
||||
"""Evaluate expr using the CEK machine."""
|
||||
return cek_run(make_cek_state(expr, env, []))
|
||||
|
||||
# CEK never produces thunks — trampoline becomes identity
|
||||
_tree_walk_trampoline = trampoline
|
||||
|
||||
def trampoline(val):
|
||||
"""In CEK mode, values are immediate — resolve any legacy thunks."""
|
||||
if is_thunk(val):
|
||||
return eval_expr(thunk_expr(val), thunk_env(val))
|
||||
return val
|
||||
'''
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Platform: async adapter — async evaluation, I/O dispatch
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1194,7 +1183,7 @@ PLATFORM_ASYNC_PY = '''
|
||||
# =========================================================================
|
||||
|
||||
import contextvars
|
||||
import inspect
|
||||
import inspect as _inspect
|
||||
|
||||
from shared.sx.primitives_io import (
|
||||
IO_PRIMITIVES, RequestContext, execute_io,
|
||||
@@ -1281,13 +1270,8 @@ def number_p(x):
|
||||
return isinstance(x, (int, float)) and not isinstance(x, bool)
|
||||
|
||||
|
||||
def sx_parse(src):
|
||||
from shared.sx.parser import parse_all
|
||||
return parse_all(src)
|
||||
|
||||
|
||||
def is_async_coroutine(x):
|
||||
return inspect.iscoroutine(x)
|
||||
return _inspect.iscoroutine(x)
|
||||
|
||||
|
||||
async def async_await(x):
|
||||
@@ -1542,6 +1526,68 @@ def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False,
|
||||
'def make_env(**kwargs):',
|
||||
' """Create an environment with initial bindings."""',
|
||||
' return _Env(dict(kwargs))',
|
||||
'',
|
||||
'',
|
||||
'def populate_effect_annotations(env, effect_map=None):',
|
||||
' """Populate *effect-annotations* in env from boundary declarations.',
|
||||
'',
|
||||
' If effect_map is provided, use it directly (dict of name -> effects list).',
|
||||
' Otherwise, parse boundary.sx via boundary_parser.',
|
||||
' """',
|
||||
' if effect_map is None:',
|
||||
' from shared.sx.ref.boundary_parser import parse_boundary_effects',
|
||||
' effect_map = parse_boundary_effects()',
|
||||
' anns = env.get("*effect-annotations*", {})',
|
||||
' if not isinstance(anns, dict):',
|
||||
' anns = {}',
|
||||
' anns.update(effect_map)',
|
||||
' env["*effect-annotations*"] = anns',
|
||||
' return anns',
|
||||
'',
|
||||
'',
|
||||
'def check_component_effects(env, comp_name=None):',
|
||||
' """Check effect violations for components in env.',
|
||||
'',
|
||||
' If comp_name is given, check only that component.',
|
||||
' Returns list of diagnostic dicts (warnings, not errors).',
|
||||
' """',
|
||||
' anns = env.get("*effect-annotations*")',
|
||||
' if not anns:',
|
||||
' return []',
|
||||
' diagnostics = []',
|
||||
' names = [comp_name] if comp_name else [k for k in env if isinstance(k, str) and k.startswith("~")]',
|
||||
' for name in names:',
|
||||
' val = env.get(name)',
|
||||
' if val is not None and type_of(val) == "component":',
|
||||
' comp_effects = anns.get(name)',
|
||||
' if comp_effects is None:',
|
||||
' continue # unannotated — skip',
|
||||
' body = val.body if hasattr(val, "body") else None',
|
||||
' if body is None:',
|
||||
' continue',
|
||||
' _walk_effects(body, name, comp_effects, anns, diagnostics)',
|
||||
' return diagnostics',
|
||||
'',
|
||||
'',
|
||||
'def _walk_effects(node, comp_name, caller_effects, anns, diagnostics):',
|
||||
' """Walk AST node and check effect calls."""',
|
||||
' if not isinstance(node, list) or not node:',
|
||||
' return',
|
||||
' head = node[0]',
|
||||
' if isinstance(head, Symbol):',
|
||||
' callee = head.name',
|
||||
' callee_effects = anns.get(callee)',
|
||||
' if callee_effects is not None and caller_effects is not None:',
|
||||
' for e in callee_effects:',
|
||||
' if e not in caller_effects:',
|
||||
' diagnostics.append({',
|
||||
' "level": "warning",',
|
||||
' "message": f"`{callee}` has effects {callee_effects} but `{comp_name}` only allows {caller_effects or \'[pure]\'}",',
|
||||
' "component": comp_name,',
|
||||
' })',
|
||||
' break',
|
||||
' for child in node[1:]:',
|
||||
' _walk_effects(child, comp_name, caller_effects, anns, diagnostics)',
|
||||
])
|
||||
return '\n'.join(lines)
|
||||
|
||||
@@ -1551,9 +1597,10 @@ def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False,
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ADAPTER_FILES = {
|
||||
"html": ("adapter-html.sx", "adapter-html"),
|
||||
"sx": ("adapter-sx.sx", "adapter-sx"),
|
||||
"async": ("adapter-async.sx", "adapter-async"),
|
||||
"parser": ("parser.sx", "parser"),
|
||||
"html": ("adapter-html.sx", "adapter-html"),
|
||||
"sx": ("adapter-sx.sx", "adapter-sx"),
|
||||
"async": ("adapter-async.sx", "adapter-async"),
|
||||
}
|
||||
|
||||
SPEC_MODULES = {
|
||||
@@ -1563,8 +1610,16 @@ SPEC_MODULES = {
|
||||
"signals": ("signals.sx", "signals (reactive signal runtime)"),
|
||||
"page-helpers": ("page-helpers.sx", "page-helpers (pure data transformation helpers)"),
|
||||
"types": ("types.sx", "types (gradual type system)"),
|
||||
"frames": ("frames.sx", "frames (CEK continuation frames)"),
|
||||
"cek": ("cek.sx", "cek (explicit CEK machine evaluator)"),
|
||||
}
|
||||
|
||||
# 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", "cek", "signals", "types",
|
||||
]
|
||||
|
||||
EXTENSION_NAMES = {"continuations"}
|
||||
|
||||
EXTENSION_FORMS = {
|
||||
|
||||
@@ -84,12 +84,6 @@
|
||||
"signal-remove-sub!" "signal_remove_sub"
|
||||
"signal-deps" "signal_deps"
|
||||
"signal-set-deps!" "signal_set_deps"
|
||||
"set-tracking-context!" "set_tracking_context"
|
||||
"get-tracking-context" "get_tracking_context"
|
||||
"make-tracking-context" "make_tracking_context"
|
||||
"tracking-context-deps" "tracking_context_deps"
|
||||
"tracking-context-add-dep!" "tracking_context_add_dep"
|
||||
"tracking-context-notify-fn" "tracking_context_notify_fn"
|
||||
"identical?" "is_identical"
|
||||
"notify-subscribers" "notify_subscribers"
|
||||
"flush-subscribers" "flush_subscribers"
|
||||
@@ -98,7 +92,6 @@
|
||||
"register-in-scope" "register_in_scope"
|
||||
"*batch-depth*" "_batch_depth"
|
||||
"*batch-queue*" "_batch_queue"
|
||||
"*island-scope*" "_island_scope"
|
||||
"*store-registry*" "_store_registry"
|
||||
"def-store" "def_store"
|
||||
"use-store" "use_store"
|
||||
|
||||
249
shared/sx/ref/run_cek_reactive_tests.py
Normal file
249
shared/sx/ref/run_cek_reactive_tests.py
Normal file
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run test-cek-reactive.sx — tests for deref-as-shift reactive rendering."""
|
||||
from __future__ import annotations
|
||||
import os, sys
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
|
||||
sys.path.insert(0, _PROJECT)
|
||||
sys.setrecursionlimit(20000)
|
||||
|
||||
from shared.sx.parser import parse_all
|
||||
from shared.sx.ref import sx_ref
|
||||
from shared.sx.ref.sx_ref import (
|
||||
make_env, env_get, env_has, env_set,
|
||||
env_extend, env_merge,
|
||||
)
|
||||
# Use tree-walk evaluator for interpreting .sx test files.
|
||||
# The CEK override (eval_expr = cek_run) would cause the interpreted cek.sx
|
||||
# to delegate to the transpiled CEK, not the interpreted one being tested.
|
||||
# Override both the local names AND the module-level names so that transpiled
|
||||
# functions (ho_map, call_lambda, etc.) also use tree-walk internally.
|
||||
eval_expr = sx_ref._tree_walk_eval_expr
|
||||
trampoline = sx_ref._tree_walk_trampoline
|
||||
sx_ref.eval_expr = eval_expr
|
||||
sx_ref.trampoline = trampoline
|
||||
from shared.sx.types import (
|
||||
NIL, Symbol, Keyword, Lambda, Component, Island, Continuation, Macro,
|
||||
_ShiftSignal,
|
||||
)
|
||||
|
||||
# Build env with primitives
|
||||
env = make_env()
|
||||
|
||||
# Platform test functions
|
||||
_suite_stack: list[str] = []
|
||||
_pass_count = 0
|
||||
_fail_count = 0
|
||||
|
||||
def _try_call(thunk):
|
||||
try:
|
||||
trampoline(eval_expr([thunk], env))
|
||||
return {"ok": True}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
|
||||
def _report_pass(name):
|
||||
global _pass_count
|
||||
_pass_count += 1
|
||||
ctx = " > ".join(_suite_stack)
|
||||
print(f" PASS: {ctx} > {name}")
|
||||
return NIL
|
||||
|
||||
def _report_fail(name, error):
|
||||
global _fail_count
|
||||
_fail_count += 1
|
||||
ctx = " > ".join(_suite_stack)
|
||||
print(f" FAIL: {ctx} > {name}: {error}")
|
||||
return NIL
|
||||
|
||||
def _push_suite(name):
|
||||
_suite_stack.append(name)
|
||||
print(f"{' ' * (len(_suite_stack)-1)}Suite: {name}")
|
||||
return NIL
|
||||
|
||||
def _pop_suite():
|
||||
if _suite_stack:
|
||||
_suite_stack.pop()
|
||||
return NIL
|
||||
|
||||
def _test_env():
|
||||
return env
|
||||
|
||||
def _sx_parse(source):
|
||||
return parse_all(source)
|
||||
|
||||
def _sx_parse_one(source):
|
||||
"""Parse a single expression."""
|
||||
exprs = parse_all(source)
|
||||
return exprs[0] if exprs else NIL
|
||||
|
||||
def _make_continuation(fn):
|
||||
return Continuation(fn)
|
||||
|
||||
env["try-call"] = _try_call
|
||||
env["report-pass"] = _report_pass
|
||||
env["report-fail"] = _report_fail
|
||||
env["push-suite"] = _push_suite
|
||||
env["pop-suite"] = _pop_suite
|
||||
env["test-env"] = _test_env
|
||||
env["sx-parse"] = _sx_parse
|
||||
env["sx-parse-one"] = _sx_parse_one
|
||||
env["env-get"] = env_get
|
||||
env["env-has?"] = env_has
|
||||
env["env-set!"] = env_set
|
||||
env["env-extend"] = env_extend
|
||||
env["make-continuation"] = _make_continuation
|
||||
env["continuation?"] = lambda x: isinstance(x, Continuation)
|
||||
env["continuation-fn"] = lambda c: c.fn
|
||||
|
||||
def _make_cek_continuation_with_data(captured, rest_kont):
|
||||
c = Continuation(lambda v=NIL: v)
|
||||
c._cek_data = {"captured": captured, "rest-kont": rest_kont}
|
||||
return c
|
||||
|
||||
env["make-cek-continuation"] = _make_cek_continuation_with_data
|
||||
env["continuation-data"] = lambda c: getattr(c, '_cek_data', {})
|
||||
|
||||
# Type predicates and constructors
|
||||
env["callable?"] = lambda x: callable(x) or isinstance(x, (Lambda, Component, Island, Continuation))
|
||||
env["lambda?"] = lambda x: isinstance(x, Lambda)
|
||||
env["component?"] = lambda x: isinstance(x, Component)
|
||||
env["island?"] = lambda x: isinstance(x, Island)
|
||||
env["macro?"] = lambda x: isinstance(x, Macro)
|
||||
env["thunk?"] = sx_ref.is_thunk
|
||||
env["thunk-expr"] = sx_ref.thunk_expr
|
||||
env["thunk-env"] = sx_ref.thunk_env
|
||||
env["make-thunk"] = sx_ref.make_thunk
|
||||
env["make-lambda"] = sx_ref.make_lambda
|
||||
env["make-component"] = sx_ref.make_component
|
||||
env["make-island"] = sx_ref.make_island
|
||||
env["make-macro"] = sx_ref.make_macro
|
||||
env["make-symbol"] = lambda n: Symbol(n)
|
||||
env["lambda-params"] = lambda f: f.params
|
||||
env["lambda-body"] = lambda f: f.body
|
||||
env["lambda-closure"] = lambda f: f.closure
|
||||
env["lambda-name"] = lambda f: f.name
|
||||
env["set-lambda-name!"] = lambda f, n: setattr(f, 'name', n) or NIL
|
||||
env["component-params"] = lambda c: c.params
|
||||
env["component-body"] = lambda c: c.body
|
||||
env["component-closure"] = lambda c: c.closure
|
||||
env["component-has-children?"] = lambda c: c.has_children
|
||||
env["component-affinity"] = lambda c: getattr(c, 'affinity', 'auto')
|
||||
env["component-set-param-types!"] = lambda c, t: setattr(c, 'param_types', t) or NIL
|
||||
env["macro-params"] = lambda m: m.params
|
||||
env["macro-rest-param"] = lambda m: m.rest_param
|
||||
env["macro-body"] = lambda m: m.body
|
||||
env["macro-closure"] = lambda m: m.closure
|
||||
env["env-merge"] = env_merge
|
||||
env["symbol-name"] = lambda s: s.name if isinstance(s, Symbol) else str(s)
|
||||
env["keyword-name"] = lambda k: k.name if isinstance(k, Keyword) else str(k)
|
||||
env["type-of"] = sx_ref.type_of
|
||||
env["primitive?"] = sx_ref.is_primitive
|
||||
env["get-primitive"] = sx_ref.get_primitive
|
||||
env["strip-prefix"] = lambda s, p: s[len(p):] if s.startswith(p) else s
|
||||
env["inspect"] = repr
|
||||
env["debug-log"] = lambda *args: None
|
||||
env["error"] = sx_ref.error
|
||||
env["apply"] = lambda f, args: f(*args)
|
||||
|
||||
# Functions from eval.sx that cek.sx references
|
||||
env["trampoline"] = trampoline
|
||||
env["eval-expr"] = eval_expr
|
||||
env["eval-list"] = sx_ref.eval_list
|
||||
env["eval-call"] = sx_ref.eval_call
|
||||
env["call-lambda"] = sx_ref.call_lambda
|
||||
env["call-component"] = sx_ref.call_component
|
||||
env["parse-keyword-args"] = sx_ref.parse_keyword_args
|
||||
env["sf-lambda"] = sx_ref.sf_lambda
|
||||
env["sf-defcomp"] = sx_ref.sf_defcomp
|
||||
env["sf-defisland"] = sx_ref.sf_defisland
|
||||
env["sf-defmacro"] = sx_ref.sf_defmacro
|
||||
env["sf-defstyle"] = sx_ref.sf_defstyle
|
||||
env["sf-deftype"] = sx_ref.sf_deftype
|
||||
env["sf-defeffect"] = sx_ref.sf_defeffect
|
||||
env["sf-letrec"] = sx_ref.sf_letrec
|
||||
env["sf-named-let"] = sx_ref.sf_named_let
|
||||
env["sf-dynamic-wind"] = sx_ref.sf_dynamic_wind
|
||||
env["sf-scope"] = sx_ref.sf_scope
|
||||
env["sf-provide"] = sx_ref.sf_provide
|
||||
env["qq-expand"] = sx_ref.qq_expand
|
||||
env["expand-macro"] = sx_ref.expand_macro
|
||||
env["cond-scheme?"] = sx_ref.cond_scheme_p
|
||||
|
||||
# Higher-order form handlers
|
||||
env["ho-map"] = sx_ref.ho_map
|
||||
env["ho-map-indexed"] = sx_ref.ho_map_indexed
|
||||
env["ho-filter"] = sx_ref.ho_filter
|
||||
env["ho-reduce"] = sx_ref.ho_reduce
|
||||
env["ho-some"] = sx_ref.ho_some
|
||||
env["ho-every"] = sx_ref.ho_every
|
||||
env["ho-for-each"] = sx_ref.ho_for_each
|
||||
env["call-fn"] = sx_ref.call_fn
|
||||
|
||||
# Render-related (stub for testing — no active rendering)
|
||||
env["render-active?"] = lambda: False
|
||||
env["is-render-expr?"] = lambda expr: False
|
||||
env["render-expr"] = lambda expr, env: NIL
|
||||
|
||||
# Scope primitives (needed for reactive-shift-deref island cleanup)
|
||||
env["scope-push!"] = sx_ref.PRIMITIVES.get("scope-push!", lambda *a: NIL)
|
||||
env["scope-pop!"] = sx_ref.PRIMITIVES.get("scope-pop!", lambda *a: NIL)
|
||||
env["context"] = sx_ref.PRIMITIVES.get("context", lambda *a: NIL)
|
||||
env["emit!"] = sx_ref.PRIMITIVES.get("emit!", lambda *a: NIL)
|
||||
env["emitted"] = sx_ref.PRIMITIVES.get("emitted", lambda *a: [])
|
||||
|
||||
# Dynamic wind
|
||||
env["push-wind!"] = lambda before, after: NIL
|
||||
env["pop-wind!"] = lambda: NIL
|
||||
env["call-thunk"] = lambda f, e: f() if callable(f) else trampoline(eval_expr([f], e))
|
||||
|
||||
# Mutation helpers
|
||||
env["dict-get"] = lambda d, k: d.get(k, NIL) if isinstance(d, dict) else NIL
|
||||
env["identical?"] = lambda a, b: a is b
|
||||
|
||||
# defhandler, defpage, defquery, defaction stubs
|
||||
for name in ["sf-defhandler", "sf-defpage", "sf-defquery", "sf-defaction"]:
|
||||
pyname = name.replace("-", "_")
|
||||
fn = getattr(sx_ref, pyname, None)
|
||||
if fn:
|
||||
env[name] = fn
|
||||
else:
|
||||
env[name] = lambda args, e, _n=name: NIL
|
||||
|
||||
# Load test framework
|
||||
with open(os.path.join(_HERE, "test-framework.sx")) as f:
|
||||
for expr in parse_all(f.read()):
|
||||
trampoline(eval_expr(expr, env))
|
||||
|
||||
# Load signals module
|
||||
print("Loading signals.sx ...")
|
||||
with open(os.path.join(_HERE, "signals.sx")) as f:
|
||||
for expr in parse_all(f.read()):
|
||||
trampoline(eval_expr(expr, env))
|
||||
|
||||
# Load frames module
|
||||
print("Loading frames.sx ...")
|
||||
with open(os.path.join(_HERE, "frames.sx")) as f:
|
||||
for expr in parse_all(f.read()):
|
||||
trampoline(eval_expr(expr, env))
|
||||
|
||||
# Load CEK module
|
||||
print("Loading cek.sx ...")
|
||||
with open(os.path.join(_HERE, "cek.sx")) as f:
|
||||
for expr in parse_all(f.read()):
|
||||
trampoline(eval_expr(expr, env))
|
||||
|
||||
# Run tests
|
||||
print("=" * 60)
|
||||
print("Running test-cek-reactive.sx")
|
||||
print("=" * 60)
|
||||
|
||||
with open(os.path.join(_HERE, "test-cek-reactive.sx")) as f:
|
||||
for expr in parse_all(f.read()):
|
||||
trampoline(eval_expr(expr, env))
|
||||
|
||||
print("=" * 60)
|
||||
print(f"Results: {_pass_count} passed, {_fail_count} failed")
|
||||
print("=" * 60)
|
||||
sys.exit(1 if _fail_count > 0 else 0)
|
||||
265
shared/sx/ref/run_cek_tests.py
Normal file
265
shared/sx/ref/run_cek_tests.py
Normal file
@@ -0,0 +1,265 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run test-cek.sx using the bootstrapped evaluator with CEK module loaded."""
|
||||
from __future__ import annotations
|
||||
import os, 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.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,
|
||||
env_extend, env_merge,
|
||||
)
|
||||
# Use tree-walk evaluator for interpreting .sx test files.
|
||||
# The CEK override (eval_expr = cek_run) would cause the interpreted cek.sx
|
||||
# to delegate to the transpiled CEK, not the interpreted one being tested.
|
||||
# Override both the local names AND the module-level names so that transpiled
|
||||
# functions (ho_map, call_lambda, etc.) also use tree-walk internally.
|
||||
eval_expr = sx_ref._tree_walk_eval_expr
|
||||
trampoline = sx_ref._tree_walk_trampoline
|
||||
sx_ref.eval_expr = eval_expr
|
||||
sx_ref.trampoline = trampoline
|
||||
from shared.sx.types import (
|
||||
NIL, Symbol, Keyword, Lambda, Component, Island, Continuation, Macro,
|
||||
_ShiftSignal,
|
||||
)
|
||||
|
||||
# Build env with primitives
|
||||
env = make_env()
|
||||
|
||||
# Platform test functions
|
||||
_suite_stack: list[str] = []
|
||||
_pass_count = 0
|
||||
_fail_count = 0
|
||||
|
||||
def _try_call(thunk):
|
||||
try:
|
||||
trampoline(eval_expr([thunk], env))
|
||||
return {"ok": True}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
|
||||
def _report_pass(name):
|
||||
global _pass_count
|
||||
_pass_count += 1
|
||||
ctx = " > ".join(_suite_stack)
|
||||
print(f" PASS: {ctx} > {name}")
|
||||
return NIL
|
||||
|
||||
def _report_fail(name, error):
|
||||
global _fail_count
|
||||
_fail_count += 1
|
||||
ctx = " > ".join(_suite_stack)
|
||||
print(f" FAIL: {ctx} > {name}: {error}")
|
||||
return NIL
|
||||
|
||||
def _push_suite(name):
|
||||
_suite_stack.append(name)
|
||||
print(f"{' ' * (len(_suite_stack)-1)}Suite: {name}")
|
||||
return NIL
|
||||
|
||||
def _pop_suite():
|
||||
if _suite_stack:
|
||||
_suite_stack.pop()
|
||||
return NIL
|
||||
|
||||
def _test_env():
|
||||
return env
|
||||
|
||||
def _sx_parse(source):
|
||||
return parse_all(source)
|
||||
|
||||
def _sx_parse_one(source):
|
||||
"""Parse a single expression."""
|
||||
exprs = parse_all(source)
|
||||
return exprs[0] if exprs else NIL
|
||||
|
||||
def _make_continuation(fn):
|
||||
return Continuation(fn)
|
||||
|
||||
env["try-call"] = _try_call
|
||||
env["report-pass"] = _report_pass
|
||||
env["report-fail"] = _report_fail
|
||||
env["push-suite"] = _push_suite
|
||||
env["pop-suite"] = _pop_suite
|
||||
env["test-env"] = _test_env
|
||||
env["sx-parse"] = _sx_parse
|
||||
env["sx-parse-one"] = _sx_parse_one
|
||||
env["env-get"] = env_get
|
||||
env["env-has?"] = env_has
|
||||
env["env-set!"] = env_set
|
||||
env["env-extend"] = env_extend
|
||||
env["make-continuation"] = _make_continuation
|
||||
env["continuation?"] = lambda x: isinstance(x, Continuation)
|
||||
env["continuation-fn"] = lambda c: c.fn
|
||||
|
||||
def _make_cek_continuation(captured, rest_kont):
|
||||
"""Create a Continuation that stores captured CEK frames as data."""
|
||||
data = {"captured": captured, "rest-kont": rest_kont}
|
||||
# The fn is a dummy — invocation happens via CEK's continue-with-call
|
||||
return Continuation(lambda v=NIL: v)
|
||||
|
||||
# Monkey-patch to store data
|
||||
_orig_make_cek_cont = _make_cek_continuation
|
||||
def _make_cek_continuation_with_data(captured, rest_kont):
|
||||
c = _orig_make_cek_cont(captured, rest_kont)
|
||||
c._cek_data = {"captured": captured, "rest-kont": rest_kont}
|
||||
return c
|
||||
|
||||
env["make-cek-continuation"] = _make_cek_continuation_with_data
|
||||
env["continuation-data"] = lambda c: getattr(c, '_cek_data', {})
|
||||
|
||||
# Register platform functions from sx_ref that cek.sx and eval.sx need
|
||||
# These are normally available as transpiled Python but need to be in the
|
||||
# SX env when interpreting .sx files directly.
|
||||
|
||||
# Type predicates and constructors
|
||||
env["callable?"] = lambda x: callable(x) or isinstance(x, (Lambda, Component, Island, Continuation))
|
||||
env["lambda?"] = lambda x: isinstance(x, Lambda)
|
||||
env["component?"] = lambda x: isinstance(x, Component)
|
||||
env["island?"] = lambda x: isinstance(x, Island)
|
||||
env["macro?"] = lambda x: isinstance(x, Macro)
|
||||
env["thunk?"] = sx_ref.is_thunk
|
||||
env["thunk-expr"] = sx_ref.thunk_expr
|
||||
env["thunk-env"] = sx_ref.thunk_env
|
||||
env["make-thunk"] = sx_ref.make_thunk
|
||||
env["make-lambda"] = sx_ref.make_lambda
|
||||
env["make-component"] = sx_ref.make_component
|
||||
env["make-island"] = sx_ref.make_island
|
||||
env["make-macro"] = sx_ref.make_macro
|
||||
env["make-symbol"] = lambda n: Symbol(n)
|
||||
env["lambda-params"] = lambda f: f.params
|
||||
env["lambda-body"] = lambda f: f.body
|
||||
env["lambda-closure"] = lambda f: f.closure
|
||||
env["lambda-name"] = lambda f: f.name
|
||||
env["set-lambda-name!"] = lambda f, n: setattr(f, 'name', n) or NIL
|
||||
env["component-params"] = lambda c: c.params
|
||||
env["component-body"] = lambda c: c.body
|
||||
env["component-closure"] = lambda c: c.closure
|
||||
env["component-has-children?"] = lambda c: c.has_children
|
||||
env["component-affinity"] = lambda c: getattr(c, 'affinity', 'auto')
|
||||
env["component-set-param-types!"] = lambda c, t: setattr(c, 'param_types', t) or NIL
|
||||
env["macro-params"] = lambda m: m.params
|
||||
env["macro-rest-param"] = lambda m: m.rest_param
|
||||
env["macro-body"] = lambda m: m.body
|
||||
env["macro-closure"] = lambda m: m.closure
|
||||
env["env-merge"] = env_merge
|
||||
env["symbol-name"] = lambda s: s.name if isinstance(s, Symbol) else str(s)
|
||||
env["keyword-name"] = lambda k: k.name if isinstance(k, Keyword) else str(k)
|
||||
env["type-of"] = sx_ref.type_of
|
||||
env["primitive?"] = lambda n: n in sx_ref.PRIMITIVES
|
||||
env["get-primitive"] = lambda n: sx_ref.PRIMITIVES.get(n)
|
||||
env["strip-prefix"] = lambda s, p: s[len(p):] if s.startswith(p) else s
|
||||
env["inspect"] = repr
|
||||
env["debug-log"] = lambda *args: None
|
||||
env["error"] = sx_ref.error
|
||||
env["apply"] = lambda f, args: f(*args)
|
||||
|
||||
# Functions from eval.sx that cek.sx references
|
||||
env["trampoline"] = trampoline
|
||||
env["eval-expr"] = eval_expr
|
||||
env["eval-list"] = sx_ref.eval_list
|
||||
env["eval-call"] = sx_ref.eval_call
|
||||
env["call-lambda"] = sx_ref.call_lambda
|
||||
env["call-component"] = sx_ref.call_component
|
||||
env["parse-keyword-args"] = sx_ref.parse_keyword_args
|
||||
env["sf-lambda"] = sx_ref.sf_lambda
|
||||
env["sf-defcomp"] = sx_ref.sf_defcomp
|
||||
env["sf-defisland"] = sx_ref.sf_defisland
|
||||
env["sf-defmacro"] = sx_ref.sf_defmacro
|
||||
env["sf-defstyle"] = sx_ref.sf_defstyle
|
||||
env["sf-deftype"] = sx_ref.sf_deftype
|
||||
env["sf-defeffect"] = sx_ref.sf_defeffect
|
||||
env["sf-letrec"] = sx_ref.sf_letrec
|
||||
env["sf-named-let"] = sx_ref.sf_named_let
|
||||
env["sf-dynamic-wind"] = sx_ref.sf_dynamic_wind
|
||||
env["sf-scope"] = sx_ref.sf_scope
|
||||
env["sf-provide"] = sx_ref.sf_provide
|
||||
env["qq-expand"] = sx_ref.qq_expand
|
||||
env["expand-macro"] = sx_ref.expand_macro
|
||||
env["cond-scheme?"] = sx_ref.cond_scheme_p
|
||||
|
||||
# Higher-order form handlers
|
||||
env["ho-map"] = sx_ref.ho_map
|
||||
env["ho-map-indexed"] = sx_ref.ho_map_indexed
|
||||
env["ho-filter"] = sx_ref.ho_filter
|
||||
env["ho-reduce"] = sx_ref.ho_reduce
|
||||
env["ho-some"] = sx_ref.ho_some
|
||||
env["ho-every"] = sx_ref.ho_every
|
||||
env["ho-for-each"] = sx_ref.ho_for_each
|
||||
env["call-fn"] = sx_ref.call_fn
|
||||
|
||||
# Render-related (stub for testing — no active rendering)
|
||||
env["render-active?"] = lambda: False
|
||||
env["is-render-expr?"] = lambda expr: False
|
||||
env["render-expr"] = lambda expr, env: NIL
|
||||
|
||||
# Scope primitives
|
||||
env["scope-push!"] = sx_ref.PRIMITIVES.get("scope-push!", lambda *a: NIL)
|
||||
env["scope-pop!"] = sx_ref.PRIMITIVES.get("scope-pop!", lambda *a: NIL)
|
||||
env["context"] = sx_ref.PRIMITIVES.get("context", lambda *a: NIL)
|
||||
env["emit!"] = sx_ref.PRIMITIVES.get("emit!", lambda *a: NIL)
|
||||
env["emitted"] = sx_ref.PRIMITIVES.get("emitted", lambda *a: [])
|
||||
|
||||
# Dynamic wind
|
||||
env["push-wind!"] = lambda before, after: NIL
|
||||
env["pop-wind!"] = lambda: NIL
|
||||
env["call-thunk"] = lambda f, e: f() if callable(f) else trampoline(eval_expr([f], e))
|
||||
|
||||
# Mutation helpers used by parse-keyword-args etc
|
||||
env["dict-get"] = lambda d, k: d.get(k, NIL) if isinstance(d, dict) else NIL
|
||||
|
||||
# defhandler, defpage, defquery, defaction — these are registrations
|
||||
# Use the bootstrapped versions if they exist, otherwise stub
|
||||
for name in ["sf-defhandler", "sf-defpage", "sf-defquery", "sf-defaction"]:
|
||||
pyname = name.replace("-", "_")
|
||||
fn = getattr(sx_ref, pyname, None)
|
||||
if fn:
|
||||
env[name] = fn
|
||||
else:
|
||||
env[name] = lambda args, e, _n=name: NIL
|
||||
|
||||
# Load test framework
|
||||
with open(os.path.join(_HERE, "test-framework.sx")) as f:
|
||||
for expr in parse_all(f.read()):
|
||||
trampoline(eval_expr(expr, env))
|
||||
|
||||
# Load frames module
|
||||
print("Loading frames.sx ...")
|
||||
with open(os.path.join(_HERE, "frames.sx")) as f:
|
||||
for expr in parse_all(f.read()):
|
||||
trampoline(eval_expr(expr, env))
|
||||
|
||||
# Load CEK module
|
||||
print("Loading cek.sx ...")
|
||||
with open(os.path.join(_HERE, "cek.sx")) as f:
|
||||
for expr in parse_all(f.read()):
|
||||
trampoline(eval_expr(expr, env))
|
||||
|
||||
# Define cek-eval helper in SX
|
||||
for expr in parse_all("""
|
||||
(define cek-eval
|
||||
(fn (source)
|
||||
(let ((exprs (sx-parse source)))
|
||||
(let ((result nil))
|
||||
(for-each (fn (e) (set! result (eval-expr-cek e (test-env)))) exprs)
|
||||
result))))
|
||||
"""):
|
||||
trampoline(eval_expr(expr, env))
|
||||
|
||||
# Run tests
|
||||
print("=" * 60)
|
||||
print("Running test-cek.sx")
|
||||
print("=" * 60)
|
||||
|
||||
with open(os.path.join(_HERE, "test-cek.sx")) as f:
|
||||
for expr in parse_all(f.read()):
|
||||
trampoline(eval_expr(expr, env))
|
||||
|
||||
print("=" * 60)
|
||||
print(f"Results: {_pass_count} passed, {_fail_count} failed")
|
||||
print("=" * 60)
|
||||
sys.exit(1 if _fail_count > 0 else 0)
|
||||
106
shared/sx/ref/run_continuation_tests.py
Normal file
106
shared/sx/ref/run_continuation_tests.py
Normal file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run test-continuations.sx using the bootstrapped evaluator with continuations enabled."""
|
||||
from __future__ import annotations
|
||||
import os, sys, subprocess, tempfile
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
|
||||
sys.path.insert(0, _PROJECT)
|
||||
|
||||
# Bootstrap a fresh sx_ref with continuations enabled
|
||||
print("Bootstrapping with --extensions continuations ...")
|
||||
result = subprocess.run(
|
||||
[sys.executable, os.path.join(_HERE, "bootstrap_py.py"),
|
||||
"--extensions", "continuations"],
|
||||
capture_output=True, text=True, cwd=_PROJECT,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print("Bootstrap FAILED:")
|
||||
print(result.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Write to temp file and import
|
||||
tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, dir=_HERE)
|
||||
tmp.write(result.stdout)
|
||||
tmp.close()
|
||||
|
||||
try:
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("sx_ref_cont", tmp.name)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
finally:
|
||||
os.unlink(tmp.name)
|
||||
|
||||
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
|
||||
# transpiled HO forms (ho_map, etc.) don't re-enter CEK mid-evaluation.
|
||||
eval_expr = mod._tree_walk_eval_expr
|
||||
trampoline = mod._tree_walk_trampoline
|
||||
mod.eval_expr = eval_expr
|
||||
mod.trampoline = trampoline
|
||||
env = mod.make_env()
|
||||
|
||||
# Platform test functions
|
||||
_suite_stack: list[str] = []
|
||||
_pass_count = 0
|
||||
_fail_count = 0
|
||||
|
||||
def _try_call(thunk):
|
||||
try:
|
||||
trampoline(eval_expr([thunk], env))
|
||||
return {"ok": True}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
|
||||
def _report_pass(name):
|
||||
global _pass_count
|
||||
_pass_count += 1
|
||||
ctx = " > ".join(_suite_stack)
|
||||
print(f" PASS: {ctx} > {name}")
|
||||
return NIL
|
||||
|
||||
def _report_fail(name, error):
|
||||
global _fail_count
|
||||
_fail_count += 1
|
||||
ctx = " > ".join(_suite_stack)
|
||||
print(f" FAIL: {ctx} > {name}: {error}")
|
||||
return NIL
|
||||
|
||||
def _push_suite(name):
|
||||
_suite_stack.append(name)
|
||||
print(f"{' ' * (len(_suite_stack)-1)}Suite: {name}")
|
||||
return NIL
|
||||
|
||||
def _pop_suite():
|
||||
if _suite_stack:
|
||||
_suite_stack.pop()
|
||||
return NIL
|
||||
|
||||
env["try-call"] = _try_call
|
||||
env["report-pass"] = _report_pass
|
||||
env["report-fail"] = _report_fail
|
||||
env["push-suite"] = _push_suite
|
||||
env["pop-suite"] = _pop_suite
|
||||
|
||||
# Load test framework
|
||||
with open(os.path.join(_HERE, "test-framework.sx")) as f:
|
||||
for expr in parse_all(f.read()):
|
||||
trampoline(eval_expr(expr, env))
|
||||
|
||||
# Run tests
|
||||
print("=" * 60)
|
||||
print("Running test-continuations.sx")
|
||||
print("=" * 60)
|
||||
|
||||
with open(os.path.join(_HERE, "test-continuations.sx")) as f:
|
||||
for expr in parse_all(f.read()):
|
||||
trampoline(eval_expr(expr, env))
|
||||
|
||||
print("=" * 60)
|
||||
print(f"Results: {_pass_count} passed, {_fail_count} failed")
|
||||
print("=" * 60)
|
||||
sys.exit(1 if _fail_count > 0 else 0)
|
||||
@@ -24,11 +24,12 @@ from shared.sx.parser import parse_all
|
||||
from shared.sx.types import Symbol
|
||||
from shared.sx.ref.platform_js import (
|
||||
extract_defines,
|
||||
ADAPTER_FILES, ADAPTER_DEPS, SPEC_MODULES, EXTENSION_NAMES,
|
||||
ADAPTER_FILES, ADAPTER_DEPS, SPEC_MODULES, SPEC_MODULE_ORDER, EXTENSION_NAMES,
|
||||
PREAMBLE, PLATFORM_JS_PRE, PLATFORM_JS_POST,
|
||||
PRIMITIVES_JS_MODULES, _ALL_JS_MODULES, _assemble_primitives_js,
|
||||
PLATFORM_DEPS_JS, PLATFORM_PARSER_JS, PLATFORM_DOM_JS,
|
||||
PLATFORM_ENGINE_PURE_JS, PLATFORM_ORCHESTRATION_JS, PLATFORM_BOOT_JS,
|
||||
PLATFORM_CEK_JS, CEK_FIXUPS_JS,
|
||||
CONTINUATIONS_JS, ASYNC_IO_JS,
|
||||
fixups_js, public_api_js, EPILOGUE,
|
||||
)
|
||||
@@ -105,9 +106,17 @@ def compile_ref_to_js(
|
||||
spec_mod_set.add("deps")
|
||||
if "page-helpers" in SPEC_MODULES:
|
||||
spec_mod_set.add("page-helpers")
|
||||
# CEK needed for reactive rendering (deref-as-shift)
|
||||
if "dom" in adapter_set:
|
||||
spec_mod_set.add("cek")
|
||||
spec_mod_set.add("frames")
|
||||
# cek module requires frames
|
||||
if "cek" in spec_mod_set:
|
||||
spec_mod_set.add("frames")
|
||||
has_deps = "deps" in spec_mod_set
|
||||
has_router = "router" in spec_mod_set
|
||||
has_page_helpers = "page-helpers" in spec_mod_set
|
||||
has_cek = "cek" in spec_mod_set
|
||||
|
||||
# Resolve extensions
|
||||
ext_set = set()
|
||||
@@ -126,8 +135,14 @@ def compile_ref_to_js(
|
||||
for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "boot"):
|
||||
if name in adapter_set:
|
||||
sx_files.append(ADAPTER_FILES[name])
|
||||
# Use explicit ordering for spec modules (respects dependencies)
|
||||
for name in SPEC_MODULE_ORDER:
|
||||
if name in spec_mod_set:
|
||||
sx_files.append(SPEC_MODULES[name])
|
||||
# Any spec modules not in the order list (future-proofing)
|
||||
for name in sorted(spec_mod_set):
|
||||
sx_files.append(SPEC_MODULES[name])
|
||||
if name not in SPEC_MODULE_ORDER:
|
||||
sx_files.append(SPEC_MODULES[name])
|
||||
|
||||
has_html = "html" in adapter_set
|
||||
has_sx = "sx" in adapter_set
|
||||
@@ -175,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)
|
||||
@@ -202,11 +221,13 @@ def compile_ref_to_js(
|
||||
parts.append(adapter_platform[name])
|
||||
|
||||
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)
|
||||
if has_continuations:
|
||||
parts.append(CONTINUATIONS_JS)
|
||||
if has_dom:
|
||||
parts.append(ASYNC_IO_JS)
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router, has_signals, has_page_helpers))
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router, has_signals, has_page_helpers, has_cek))
|
||||
parts.append(EPILOGUE)
|
||||
|
||||
build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
162
shared/sx/ref/run_signal_tests.py
Normal file
162
shared/sx/ref/run_signal_tests.py
Normal file
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run test-signals.sx using the bootstrapped evaluator with signal primitives.
|
||||
|
||||
Uses bootstrapped signal functions from sx_ref.py directly, patching apply
|
||||
to handle SX lambdas from the interpreter (test expressions create lambdas
|
||||
that need evaluator dispatch).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import os, 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.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
|
||||
# Use tree-walk evaluator for interpreting .sx test files.
|
||||
eval_expr = sx_ref._tree_walk_eval_expr
|
||||
trampoline = sx_ref._tree_walk_trampoline
|
||||
sx_ref.eval_expr = eval_expr
|
||||
sx_ref.trampoline = trampoline
|
||||
|
||||
# Build env with primitives
|
||||
env = make_env()
|
||||
|
||||
# --- Patch apply BEFORE anything else ---
|
||||
# Test expressions create SX Lambdas that bootstrapped code calls via apply.
|
||||
# Patch the module-level function so all bootstrapped functions see it.
|
||||
|
||||
# apply is used by swap! and other forms to call functions with arg lists
|
||||
def _apply(f, args):
|
||||
if isinstance(f, Lambda):
|
||||
return trampoline(eval_expr([f] + list(args), env))
|
||||
return f(*args)
|
||||
sx_ref.__dict__["apply"] = _apply
|
||||
|
||||
# cons needs to handle tuples from Python *args (swap! passes &rest as tuple)
|
||||
_orig_cons = sx_ref.PRIMITIVES.get("cons")
|
||||
def _cons(x, c):
|
||||
if isinstance(c, tuple):
|
||||
c = list(c)
|
||||
return [x] + (c or [])
|
||||
sx_ref.__dict__["cons"] = _cons
|
||||
sx_ref.PRIMITIVES["cons"] = _cons
|
||||
|
||||
# Platform test functions
|
||||
_suite_stack: list[str] = []
|
||||
_pass_count = 0
|
||||
_fail_count = 0
|
||||
|
||||
def _try_call(thunk):
|
||||
try:
|
||||
trampoline(eval_expr([thunk], env))
|
||||
return {"ok": True}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
|
||||
def _report_pass(name):
|
||||
global _pass_count
|
||||
_pass_count += 1
|
||||
ctx = " > ".join(_suite_stack)
|
||||
print(f" PASS: {ctx} > {name}")
|
||||
return NIL
|
||||
|
||||
def _report_fail(name, error):
|
||||
global _fail_count
|
||||
_fail_count += 1
|
||||
ctx = " > ".join(_suite_stack)
|
||||
print(f" FAIL: {ctx} > {name}: {error}")
|
||||
return NIL
|
||||
|
||||
def _push_suite(name):
|
||||
_suite_stack.append(name)
|
||||
print(f"{' ' * (len(_suite_stack)-1)}Suite: {name}")
|
||||
return NIL
|
||||
|
||||
def _pop_suite():
|
||||
if _suite_stack:
|
||||
_suite_stack.pop()
|
||||
return NIL
|
||||
|
||||
env["try-call"] = _try_call
|
||||
env["report-pass"] = _report_pass
|
||||
env["report-fail"] = _report_fail
|
||||
env["push-suite"] = _push_suite
|
||||
env["pop-suite"] = _pop_suite
|
||||
|
||||
# Signal functions are now pure SX (transpiled into sx_ref.py from signals.sx)
|
||||
# Wire both low-level dict-based signal functions and high-level API
|
||||
env["identical?"] = sx_ref.is_identical
|
||||
env["island?"] = lambda x: isinstance(x, Island)
|
||||
|
||||
# Scope primitives (used by signals.sx for reactive tracking)
|
||||
env["scope-push!"] = scope_push
|
||||
env["scope-pop!"] = scope_pop
|
||||
env["context"] = sx_context
|
||||
|
||||
# Low-level signal functions (now pure SX, transpiled from signals.sx)
|
||||
env["make-signal"] = sx_ref.make_signal
|
||||
env["signal?"] = sx_ref.is_signal
|
||||
env["signal-value"] = sx_ref.signal_value
|
||||
env["signal-set-value!"] = sx_ref.signal_set_value
|
||||
env["signal-subscribers"] = sx_ref.signal_subscribers
|
||||
env["signal-add-sub!"] = sx_ref.signal_add_sub
|
||||
env["signal-remove-sub!"] = sx_ref.signal_remove_sub
|
||||
env["signal-deps"] = sx_ref.signal_deps
|
||||
env["signal-set-deps!"] = sx_ref.signal_set_deps
|
||||
|
||||
# Bootstrapped signal functions from sx_ref.py
|
||||
env["signal"] = sx_ref.signal
|
||||
env["deref"] = sx_ref.deref
|
||||
env["reset!"] = sx_ref.reset_b
|
||||
env["swap!"] = sx_ref.swap_b
|
||||
env["computed"] = sx_ref.computed
|
||||
env["effect"] = sx_ref.effect
|
||||
# batch has a bootstrapper issue with _batch_depth global variable access.
|
||||
# Wrap it to work correctly in the test context.
|
||||
def _batch(thunk):
|
||||
sx_ref._batch_depth = getattr(sx_ref, '_batch_depth', 0) + 1
|
||||
sx_ref.cek_call(thunk, None)
|
||||
sx_ref._batch_depth -= 1
|
||||
if sx_ref._batch_depth == 0:
|
||||
queue = list(sx_ref._batch_queue)
|
||||
sx_ref._batch_queue = []
|
||||
seen = []
|
||||
pending = []
|
||||
for s in queue:
|
||||
for sub in sx_ref.signal_subscribers(s):
|
||||
if sub not in seen:
|
||||
seen.append(sub)
|
||||
pending.append(sub)
|
||||
for sub in pending:
|
||||
sub()
|
||||
return NIL
|
||||
env["batch"] = _batch
|
||||
env["notify-subscribers"] = sx_ref.notify_subscribers
|
||||
env["flush-subscribers"] = sx_ref.flush_subscribers
|
||||
env["dispose-computed"] = sx_ref.dispose_computed
|
||||
env["with-island-scope"] = sx_ref.with_island_scope
|
||||
env["register-in-scope"] = sx_ref.register_in_scope
|
||||
env["callable?"] = sx_ref.is_callable
|
||||
|
||||
# Load test framework
|
||||
with open(os.path.join(_HERE, "test-framework.sx")) as f:
|
||||
for expr in parse_all(f.read()):
|
||||
trampoline(eval_expr(expr, env))
|
||||
|
||||
# Run tests
|
||||
print("=" * 60)
|
||||
print("Running test-signals.sx")
|
||||
print("=" * 60)
|
||||
|
||||
with open(os.path.join(_HERE, "test-signals.sx")) as f:
|
||||
for expr in parse_all(f.read()):
|
||||
trampoline(eval_expr(expr, env))
|
||||
|
||||
print("=" * 60)
|
||||
print(f"Results: {_pass_count} passed, {_fail_count} failed")
|
||||
print("=" * 60)
|
||||
sys.exit(1 if _fail_count > 0 else 0)
|
||||
@@ -7,9 +7,17 @@ _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 eval_expr, trampoline, make_env
|
||||
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
|
||||
# Use tree-walk evaluator for interpreting .sx test files.
|
||||
# CEK is now the default, but the test runners need tree-walk so that
|
||||
# transpiled HO forms (ho_map, etc.) don't re-enter CEK mid-evaluation.
|
||||
eval_expr = sx_ref._tree_walk_eval_expr
|
||||
trampoline = sx_ref._tree_walk_trampoline
|
||||
sx_ref.eval_expr = eval_expr
|
||||
sx_ref.trampoline = trampoline
|
||||
|
||||
# Build env with primitives
|
||||
env = make_env()
|
||||
@@ -154,6 +162,9 @@ env["component-params"] = _component_params
|
||||
env["component-body"] = _component_body
|
||||
env["component-has-children"] = _component_has_children
|
||||
env["map-dict"] = _map_dict
|
||||
env["env-get"] = env_get
|
||||
env["env-has?"] = env_has
|
||||
env["env-set!"] = env_set
|
||||
|
||||
# Load test framework (macros + assertion helpers)
|
||||
with open(os.path.join(_HERE, "test-framework.sx")) as f:
|
||||
|
||||
@@ -9,34 +9,59 @@
|
||||
;; layer (adapter-dom.sx) subscribes DOM nodes to signals. The server
|
||||
;; adapter (adapter-html.sx) reads signal values without subscribing.
|
||||
;;
|
||||
;; Platform interface required:
|
||||
;; (make-signal value) → Signal — create signal container
|
||||
;; (signal? x) → boolean — type predicate
|
||||
;; (signal-value s) → any — read current value (no tracking)
|
||||
;; (signal-set-value! s v) → void — write value (no notification)
|
||||
;; (signal-subscribers s) → list — list of subscriber fns
|
||||
;; (signal-add-sub! s fn) → void — add subscriber
|
||||
;; (signal-remove-sub! s fn) → void — remove subscriber
|
||||
;; (signal-deps s) → list — dependency list (for computed)
|
||||
;; (signal-set-deps! s deps) → void — set dependency list
|
||||
;; Signals are plain dicts with a "__signal" marker key. No platform
|
||||
;; primitives needed — all signal operations are pure SX.
|
||||
;;
|
||||
;; Global state required:
|
||||
;; *tracking-context* → nil | Effect/Computed currently evaluating
|
||||
;; (set-tracking-context! c) → void
|
||||
;; (get-tracking-context) → context or nil
|
||||
;; Reactive tracking and island lifecycle use the general scoped effects
|
||||
;; system (scope-push!/scope-pop!/context) instead of separate globals.
|
||||
;; Two scope names:
|
||||
;; "sx-reactive" — tracking context for computed/effect dep discovery
|
||||
;; "sx-island-scope" — island disposable collector
|
||||
;;
|
||||
;; 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.
|
||||
;; Scope-based tracking:
|
||||
;; (scope-push! "sx-reactive" {:deps (list) :notify fn}) → void
|
||||
;; (scope-pop! "sx-reactive") → void
|
||||
;; (context "sx-reactive" nil) → dict or nil
|
||||
;;
|
||||
;; 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.
|
||||
;;
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Signal container — plain dict with marker key
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; A signal is a dict: {"__signal" true, "value" v, "subscribers" [], "deps" []}
|
||||
;; type-of returns "dict". Use signal? to distinguish from regular dicts.
|
||||
|
||||
(define make-signal (fn (value)
|
||||
(dict "__signal" true "value" value "subscribers" (list) "deps" (list))))
|
||||
|
||||
(define signal? (fn (x)
|
||||
(and (dict? x) (has-key? x "__signal"))))
|
||||
|
||||
(define signal-value (fn (s) (get s "value")))
|
||||
(define signal-set-value! (fn (s v) (dict-set! s "value" v)))
|
||||
(define signal-subscribers (fn (s) (get s "subscribers")))
|
||||
|
||||
(define signal-add-sub! (fn (s f)
|
||||
(when (not (contains? (get s "subscribers") f))
|
||||
(append! (get s "subscribers") f))))
|
||||
|
||||
(define signal-remove-sub! (fn (s f)
|
||||
(dict-set! s "subscribers"
|
||||
(filter (fn (sub) (not (identical? sub f)))
|
||||
(get s "subscribers")))))
|
||||
|
||||
(define signal-deps (fn (s) (get s "deps")))
|
||||
(define signal-set-deps! (fn (s deps) (dict-set! s "deps" deps)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 1. signal — create a reactive container
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -58,12 +83,14 @@
|
||||
(fn ((s :as any))
|
||||
(if (not (signal? s))
|
||||
s ;; non-signal values pass through
|
||||
(let ((ctx (get-tracking-context)))
|
||||
(let ((ctx (context "sx-reactive" nil)))
|
||||
(when ctx
|
||||
;; Register this signal as a dependency of the current context
|
||||
(tracking-context-add-dep! ctx s)
|
||||
;; Subscribe the context to this signal
|
||||
(signal-add-sub! s (tracking-context-notify-fn ctx)))
|
||||
(let ((dep-list (get ctx "deps"))
|
||||
(notify-fn (get ctx "notify")))
|
||||
(when (not (contains? dep-list s))
|
||||
(append! dep-list s)
|
||||
(signal-add-sub! s notify-fn))))
|
||||
(signal-value s)))))
|
||||
|
||||
|
||||
@@ -117,19 +144,18 @@
|
||||
(signal-deps s))
|
||||
(signal-set-deps! s (list))
|
||||
|
||||
;; Create tracking context for this computed
|
||||
(let ((ctx (make-tracking-context recompute)))
|
||||
(let ((prev (get-tracking-context)))
|
||||
(set-tracking-context! ctx)
|
||||
(let ((new-val (invoke compute-fn)))
|
||||
(set-tracking-context! prev)
|
||||
;; Save discovered deps
|
||||
(signal-set-deps! s (tracking-context-deps ctx))
|
||||
;; Update value + notify downstream
|
||||
(let ((old (signal-value s)))
|
||||
(signal-set-value! s new-val)
|
||||
(when (not (identical? old new-val))
|
||||
(notify-subscribers s)))))))))
|
||||
;; Push scope-based tracking context for this computed
|
||||
(let ((ctx (dict "deps" (list) "notify" recompute)))
|
||||
(scope-push! "sx-reactive" ctx)
|
||||
(let ((new-val (cek-call compute-fn nil)))
|
||||
(scope-pop! "sx-reactive")
|
||||
;; Save discovered deps
|
||||
(signal-set-deps! s (get ctx "deps"))
|
||||
;; Update value + notify downstream
|
||||
(let ((old (signal-value s)))
|
||||
(signal-set-value! s new-val)
|
||||
(when (not (identical? old new-val))
|
||||
(notify-subscribers s))))))))
|
||||
|
||||
;; Initial computation
|
||||
(recompute)
|
||||
@@ -155,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
|
||||
@@ -163,16 +189,15 @@
|
||||
deps)
|
||||
(set! deps (list))
|
||||
|
||||
;; Track new deps
|
||||
(let ((ctx (make-tracking-context run-effect)))
|
||||
(let ((prev (get-tracking-context)))
|
||||
(set-tracking-context! ctx)
|
||||
(let ((result (invoke effect-fn)))
|
||||
(set-tracking-context! prev)
|
||||
(set! deps (tracking-context-deps ctx))
|
||||
;; If effect returns a function, it's the cleanup
|
||||
(when (callable? result)
|
||||
(set! cleanup-fn result)))))))))
|
||||
;; Push scope-based tracking context
|
||||
(let ((ctx (dict "deps" (list) "notify" run-effect)))
|
||||
(scope-push! "sx-reactive" ctx)
|
||||
(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
|
||||
(when (callable? result)
|
||||
(set! cleanup-fn result))))))))
|
||||
|
||||
;; Initial run
|
||||
(run-effect)
|
||||
@@ -181,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)
|
||||
@@ -204,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*))
|
||||
@@ -246,19 +271,13 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 9. Tracking context
|
||||
;; 9. Reactive tracking context
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; A tracking context is an ephemeral object created during effect/computed
|
||||
;; evaluation to discover signal dependencies. Platform must provide:
|
||||
;;
|
||||
;; (make-tracking-context notify-fn) → context
|
||||
;; (tracking-context-deps ctx) → list of signals
|
||||
;; (tracking-context-add-dep! ctx s) → void (adds s to ctx's dep list)
|
||||
;; (tracking-context-notify-fn ctx) → the notify function
|
||||
;;
|
||||
;; These are platform primitives because the context is mutable state
|
||||
;; that must be efficient (often a Set in the host language).
|
||||
;; Tracking is now scope-based. computed/effect push a dict
|
||||
;; {:deps (list) :notify fn} onto the "sx-reactive" scope stack via
|
||||
;; scope-push!/scope-pop!. deref reads it via (context "sx-reactive" nil).
|
||||
;; No platform primitives needed — uses the existing scope infrastructure.
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -284,25 +303,24 @@
|
||||
;; When an island is created, all signals, effects, and computeds created
|
||||
;; within it are tracked. When the island is removed from the DOM, they
|
||||
;; are all disposed.
|
||||
|
||||
(define *island-scope* nil)
|
||||
;;
|
||||
;; Uses "sx-island-scope" scope name. The scope value is a collector
|
||||
;; function (fn (disposable) ...) that appends to the island's disposer list.
|
||||
|
||||
(define with-island-scope :effects [mutation]
|
||||
(fn ((scope-fn :as lambda) (body-fn :as lambda))
|
||||
(let ((prev *island-scope*))
|
||||
(set! *island-scope* scope-fn)
|
||||
(let ((result (body-fn)))
|
||||
(set! *island-scope* prev)
|
||||
result))))
|
||||
(scope-push! "sx-island-scope" scope-fn)
|
||||
(let ((result (body-fn)))
|
||||
(scope-pop! "sx-island-scope")
|
||||
result)))
|
||||
|
||||
;; Hook into signal/effect/computed creation for scope tracking.
|
||||
;; The platform's make-signal should call (register-in-scope s) if
|
||||
;; *island-scope* is non-nil.
|
||||
|
||||
(define register-in-scope :effects [mutation]
|
||||
(fn ((disposable :as lambda))
|
||||
(when *island-scope*
|
||||
(*island-scope* disposable))))
|
||||
(let ((collector (context "sx-island-scope" nil)))
|
||||
(when collector
|
||||
(cek-call collector (list disposable))))))
|
||||
|
||||
|
||||
;; ==========================================================================
|
||||
@@ -341,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)))))
|
||||
|
||||
|
||||
@@ -363,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 []
|
||||
@@ -422,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
|
||||
@@ -453,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)))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
279
shared/sx/ref/test-cek-reactive.sx
Normal file
279
shared/sx/ref/test-cek-reactive.sx
Normal file
@@ -0,0 +1,279 @@
|
||||
;; ==========================================================================
|
||||
;; test-cek-reactive.sx — Tests for deref-as-shift reactive rendering
|
||||
;;
|
||||
;; Tests that (deref signal) inside a reactive-reset boundary performs
|
||||
;; continuation capture: the rest of the expression becomes the subscriber.
|
||||
;;
|
||||
;; Requires: test-framework.sx, frames.sx, cek.sx, signals.sx loaded first.
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Basic deref behavior through CEK
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "deref pass-through"
|
||||
(deftest "deref non-signal passes through"
|
||||
(let ((result (eval-expr-cek
|
||||
(sx-parse-one "(deref 42)")
|
||||
(test-env))))
|
||||
(assert-equal 42 result)))
|
||||
|
||||
(deftest "deref nil passes through"
|
||||
(let ((result (eval-expr-cek
|
||||
(sx-parse-one "(deref nil)")
|
||||
(test-env))))
|
||||
(assert-nil result)))
|
||||
|
||||
(deftest "deref string passes through"
|
||||
(let ((result (eval-expr-cek
|
||||
(sx-parse-one "(deref \"hello\")")
|
||||
(test-env))))
|
||||
(assert-equal "hello" result))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Deref signal without reactive-reset (no shift)
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "deref signal without reactive-reset"
|
||||
(deftest "deref signal returns current value"
|
||||
(let ((s (signal 99)))
|
||||
(env-set! (test-env) "test-sig" s)
|
||||
(let ((result (eval-expr-cek
|
||||
(sx-parse-one "(deref test-sig)")
|
||||
(test-env))))
|
||||
(assert-equal 99 result))))
|
||||
|
||||
(deftest "deref signal in expression returns computed value"
|
||||
(let ((s (signal 10)))
|
||||
(env-set! (test-env) "test-sig" s)
|
||||
(let ((result (eval-expr-cek
|
||||
(sx-parse-one "(+ 5 (deref test-sig))")
|
||||
(test-env))))
|
||||
(assert-equal 15 result)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Reactive reset + deref: continuation capture
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "reactive-reset shift"
|
||||
(deftest "deref signal with reactive-reset captures continuation"
|
||||
(let ((s (signal 42))
|
||||
(captured-val nil))
|
||||
;; Run CEK with a ReactiveResetFrame
|
||||
(let ((result (cek-run
|
||||
(make-cek-state
|
||||
(sx-parse-one "(deref test-sig)")
|
||||
(let ((e (env-extend (test-env))))
|
||||
(env-set! e "test-sig" s)
|
||||
e)
|
||||
(list (make-reactive-reset-frame
|
||||
(test-env)
|
||||
(fn (v) (set! captured-val v))
|
||||
true))))))
|
||||
;; Initial render: returns current value, update-fn NOT called (first-render)
|
||||
(assert-equal 42 result)
|
||||
(assert-nil captured-val))))
|
||||
|
||||
(deftest "signal change invokes subscriber with update-fn"
|
||||
(let ((s (signal 10))
|
||||
(update-calls (list)))
|
||||
;; Set up reactive-reset with tracking update-fn
|
||||
(scope-push! "sx-island-scope" nil)
|
||||
(let ((e (env-extend (test-env))))
|
||||
(env-set! e "test-sig" s)
|
||||
(cek-run
|
||||
(make-cek-state
|
||||
(sx-parse-one "(deref test-sig)")
|
||||
e
|
||||
(list (make-reactive-reset-frame
|
||||
e
|
||||
(fn (v) (append! update-calls v))
|
||||
true)))))
|
||||
;; Change signal — subscriber should fire
|
||||
(reset! s 20)
|
||||
(assert-equal 1 (len update-calls))
|
||||
(assert-equal 20 (first update-calls))
|
||||
;; Change again
|
||||
(reset! s 30)
|
||||
(assert-equal 2 (len update-calls))
|
||||
(assert-equal 30 (nth update-calls 1))
|
||||
(scope-pop! "sx-island-scope")))
|
||||
|
||||
(deftest "expression with deref captures rest as continuation"
|
||||
(let ((s (signal 5))
|
||||
(update-calls (list)))
|
||||
(scope-push! "sx-island-scope" nil)
|
||||
(let ((e (env-extend (test-env))))
|
||||
(env-set! e "test-sig" s)
|
||||
;; (str "val=" (deref test-sig)) — continuation captures (str "val=" [HOLE])
|
||||
(let ((result (cek-run
|
||||
(make-cek-state
|
||||
(sx-parse-one "(str \"val=\" (deref test-sig))")
|
||||
e
|
||||
(list (make-reactive-reset-frame
|
||||
e
|
||||
(fn (v) (append! update-calls v))
|
||||
true))))))
|
||||
(assert-equal "val=5" result)))
|
||||
;; Change signal — should get updated string
|
||||
(reset! s 42)
|
||||
(assert-equal 1 (len update-calls))
|
||||
(assert-equal "val=42" (first update-calls))
|
||||
(scope-pop! "sx-island-scope"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Disposal and cleanup
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "disposal"
|
||||
(deftest "scope cleanup unsubscribes continuation"
|
||||
(let ((s (signal 1))
|
||||
(update-calls (list))
|
||||
(disposers (list)))
|
||||
;; Create island scope with collector that accumulates disposers
|
||||
(scope-push! "sx-island-scope" (fn (d) (append! disposers d)))
|
||||
(let ((e (env-extend (test-env))))
|
||||
(env-set! e "test-sig" s)
|
||||
(cek-run
|
||||
(make-cek-state
|
||||
(sx-parse-one "(deref test-sig)")
|
||||
e
|
||||
(list (make-reactive-reset-frame
|
||||
e
|
||||
(fn (v) (append! update-calls v))
|
||||
true)))))
|
||||
;; Pop scope — call all disposers
|
||||
(scope-pop! "sx-island-scope")
|
||||
(for-each (fn (d) (cek-call d nil)) disposers)
|
||||
;; Change signal — no update should fire
|
||||
(reset! s 999)
|
||||
(assert-equal 0 (len update-calls)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; cek-call integration — computed/effect use cek-call dispatch
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "cek-call dispatch"
|
||||
(deftest "cek-call invokes native function"
|
||||
(let ((log (list)))
|
||||
(cek-call (fn (x) (append! log x)) (list 42))
|
||||
(assert-equal (list 42) log)))
|
||||
|
||||
(deftest "cek-call invokes zero-arg lambda"
|
||||
(let ((result (cek-call (fn () (+ 1 2)) nil)))
|
||||
(assert-equal 3 result)))
|
||||
|
||||
(deftest "cek-call with nil function returns nil"
|
||||
(assert-nil (cek-call nil nil)))
|
||||
|
||||
(deftest "computed tracks deps via cek-call"
|
||||
(let ((s (signal 10)))
|
||||
(let ((c (computed (fn () (* 2 (deref s))))))
|
||||
(assert-equal 20 (deref c))
|
||||
(reset! s 5)
|
||||
(assert-equal 10 (deref c)))))
|
||||
|
||||
(deftest "effect runs and re-runs via cek-call"
|
||||
(let ((s (signal "a"))
|
||||
(log (list)))
|
||||
(effect (fn () (append! log (deref s))))
|
||||
(assert-equal (list "a") log)
|
||||
(reset! s "b")
|
||||
(assert-equal (list "a" "b") log)))
|
||||
|
||||
(deftest "effect cleanup runs on re-trigger"
|
||||
(let ((s (signal 0))
|
||||
(log (list)))
|
||||
(effect (fn ()
|
||||
(let ((val (deref s)))
|
||||
(append! log (str "run:" val))
|
||||
;; Return cleanup function
|
||||
(fn () (append! log (str "clean:" val))))))
|
||||
(assert-equal (list "run:0") log)
|
||||
(reset! s 1)
|
||||
(assert-equal (list "run:0" "clean:0" "run:1") log)))
|
||||
|
||||
(deftest "batch coalesces via cek-call"
|
||||
(let ((s (signal 0))
|
||||
(count (signal 0)))
|
||||
(effect (fn () (do (deref s) (swap! count inc))))
|
||||
(assert-equal 1 (deref count))
|
||||
(batch (fn ()
|
||||
(reset! s 1)
|
||||
(reset! s 2)
|
||||
(reset! s 3)))
|
||||
;; batch should coalesce — effect runs once, not three times
|
||||
(assert-equal 2 (deref count)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; CEK-native higher-order forms
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "CEK higher-order forms"
|
||||
(deftest "map through CEK"
|
||||
(let ((result (eval-expr-cek
|
||||
(sx-parse-one "(map (fn (x) (* x 2)) (list 1 2 3))")
|
||||
(test-env))))
|
||||
(assert-equal (list 2 4 6) result)))
|
||||
|
||||
(deftest "map-indexed through CEK"
|
||||
(let ((result (eval-expr-cek
|
||||
(sx-parse-one "(map-indexed (fn (i x) (+ i x)) (list 10 20 30))")
|
||||
(test-env))))
|
||||
(assert-equal (list 10 21 32) result)))
|
||||
|
||||
(deftest "filter through CEK"
|
||||
(let ((result (eval-expr-cek
|
||||
(sx-parse-one "(filter (fn (x) (> x 2)) (list 1 2 3 4 5))")
|
||||
(test-env))))
|
||||
(assert-equal (list 3 4 5) result)))
|
||||
|
||||
(deftest "reduce through CEK"
|
||||
(let ((result (eval-expr-cek
|
||||
(sx-parse-one "(reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3))")
|
||||
(test-env))))
|
||||
(assert-equal 6 result)))
|
||||
|
||||
(deftest "some through CEK"
|
||||
(let ((result (eval-expr-cek
|
||||
(sx-parse-one "(some (fn (x) (> x 3)) (list 1 2 3 4 5))")
|
||||
(test-env))))
|
||||
(assert-true result)))
|
||||
|
||||
(deftest "some returns false when none match"
|
||||
(let ((result (eval-expr-cek
|
||||
(sx-parse-one "(some (fn (x) (> x 10)) (list 1 2 3))")
|
||||
(test-env))))
|
||||
(assert-false result)))
|
||||
|
||||
(deftest "every? through CEK"
|
||||
(let ((result (eval-expr-cek
|
||||
(sx-parse-one "(every? (fn (x) (> x 0)) (list 1 2 3))")
|
||||
(test-env))))
|
||||
(assert-true result)))
|
||||
|
||||
(deftest "every? returns false on first falsy"
|
||||
(let ((result (eval-expr-cek
|
||||
(sx-parse-one "(every? (fn (x) (> x 2)) (list 1 2 3))")
|
||||
(test-env))))
|
||||
(assert-false result)))
|
||||
|
||||
(deftest "for-each through CEK"
|
||||
(let ((log (list)))
|
||||
(env-set! (test-env) "test-log" log)
|
||||
(eval-expr-cek
|
||||
(sx-parse-one "(for-each (fn (x) (append! test-log x)) (list 1 2 3))")
|
||||
(test-env))
|
||||
(assert-equal (list 1 2 3) log)))
|
||||
|
||||
(deftest "map on empty list"
|
||||
(let ((result (eval-expr-cek
|
||||
(sx-parse-one "(map (fn (x) x) (list))")
|
||||
(test-env))))
|
||||
(assert-equal (list) result))))
|
||||
241
shared/sx/ref/test-cek.sx
Normal file
241
shared/sx/ref/test-cek.sx
Normal file
@@ -0,0 +1,241 @@
|
||||
;; ==========================================================================
|
||||
;; test-cek.sx — Tests for the explicit CEK machine evaluator
|
||||
;;
|
||||
;; Tests that eval-expr-cek produces identical results to eval-expr.
|
||||
;; Requires: test-framework.sx, frames.sx, cek.sx loaded.
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 1. Literals
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "cek-literals"
|
||||
(deftest "number"
|
||||
(assert-equal 42 (eval-expr-cek 42 (test-env))))
|
||||
|
||||
(deftest "string"
|
||||
(assert-equal "hello" (eval-expr-cek "hello" (test-env))))
|
||||
|
||||
(deftest "boolean true"
|
||||
(assert-equal true (eval-expr-cek true (test-env))))
|
||||
|
||||
(deftest "boolean false"
|
||||
(assert-equal false (eval-expr-cek false (test-env))))
|
||||
|
||||
(deftest "nil"
|
||||
(assert-nil (eval-expr-cek nil (test-env)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 2. Symbol lookup
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "cek-symbols"
|
||||
(deftest "env lookup"
|
||||
(assert-equal 42
|
||||
(cek-eval "(do (define x 42) x)")))
|
||||
|
||||
(deftest "primitive call resolves"
|
||||
(assert-equal "hello"
|
||||
(cek-eval "(str \"hello\")"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3. Special forms
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "cek-if"
|
||||
(deftest "if true branch"
|
||||
(assert-equal 1
|
||||
(cek-eval "(if true 1 2)")))
|
||||
|
||||
(deftest "if false branch"
|
||||
(assert-equal 2
|
||||
(cek-eval "(if false 1 2)")))
|
||||
|
||||
(deftest "if no else"
|
||||
(assert-nil (cek-eval "(if false 1)"))))
|
||||
|
||||
|
||||
(defsuite "cek-when"
|
||||
(deftest "when true"
|
||||
(assert-equal 42
|
||||
(cek-eval "(when true 42)")))
|
||||
|
||||
(deftest "when false"
|
||||
(assert-nil (cek-eval "(when false 42)")))
|
||||
|
||||
(deftest "when multiple body"
|
||||
(assert-equal 3
|
||||
(cek-eval "(when true 1 2 3)"))))
|
||||
|
||||
|
||||
(defsuite "cek-begin"
|
||||
(deftest "do returns last"
|
||||
(assert-equal 3
|
||||
(cek-eval "(do 1 2 3)")))
|
||||
|
||||
(deftest "empty do"
|
||||
(assert-nil (cek-eval "(do)"))))
|
||||
|
||||
|
||||
(defsuite "cek-let"
|
||||
(deftest "basic let"
|
||||
(assert-equal 3
|
||||
(cek-eval "(let ((x 1) (y 2)) (+ x y))")))
|
||||
|
||||
(deftest "let body sequence"
|
||||
(assert-equal 10
|
||||
(cek-eval "(let ((x 5)) 1 2 (+ x 5))")))
|
||||
|
||||
(deftest "nested let"
|
||||
(assert-equal 5
|
||||
(cek-eval "(let ((x 1)) (let ((y 2)) (+ x y (* x y))))"))))
|
||||
|
||||
|
||||
(defsuite "cek-and-or"
|
||||
(deftest "and all true"
|
||||
(assert-equal 3
|
||||
(cek-eval "(and 1 2 3)")))
|
||||
|
||||
(deftest "and short circuit"
|
||||
(assert-false (cek-eval "(and 1 false 3)")))
|
||||
|
||||
(deftest "or first true"
|
||||
(assert-equal 1
|
||||
(cek-eval "(or 1 2 3)")))
|
||||
|
||||
(deftest "or all false"
|
||||
(assert-false (cek-eval "(or false false false)"))))
|
||||
|
||||
|
||||
(defsuite "cek-cond"
|
||||
(deftest "cond first match"
|
||||
(assert-equal "a"
|
||||
(cek-eval "(cond true \"a\" true \"b\")")))
|
||||
|
||||
(deftest "cond second match"
|
||||
(assert-equal "b"
|
||||
(cek-eval "(cond false \"a\" true \"b\")")))
|
||||
|
||||
(deftest "cond else"
|
||||
(assert-equal "c"
|
||||
(cek-eval "(cond false \"a\" :else \"c\")"))))
|
||||
|
||||
|
||||
(defsuite "cek-case"
|
||||
(deftest "case match"
|
||||
(assert-equal "yes"
|
||||
(cek-eval "(case 1 1 \"yes\" 2 \"no\")")))
|
||||
|
||||
(deftest "case no match"
|
||||
(assert-nil
|
||||
(cek-eval "(case 3 1 \"yes\" 2 \"no\")")))
|
||||
|
||||
(deftest "case else"
|
||||
(assert-equal "default"
|
||||
(cek-eval "(case 3 1 \"yes\" :else \"default\")"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 4. Function calls
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "cek-calls"
|
||||
(deftest "primitive call"
|
||||
(assert-equal 3
|
||||
(cek-eval "(+ 1 2)")))
|
||||
|
||||
(deftest "nested calls"
|
||||
(assert-equal 6
|
||||
(cek-eval "(+ 1 (+ 2 3))")))
|
||||
|
||||
(deftest "lambda call"
|
||||
(assert-equal 10
|
||||
(cek-eval "((fn (x) (* x 2)) 5)")))
|
||||
|
||||
(deftest "defined function"
|
||||
(assert-equal 25
|
||||
(cek-eval "(do (define square (fn (x) (* x x))) (square 5))"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 5. Define and set!
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "cek-define"
|
||||
(deftest "define binds"
|
||||
(assert-equal 42
|
||||
(cek-eval "(do (define x 42) x)")))
|
||||
|
||||
(deftest "set! mutates"
|
||||
(assert-equal 10
|
||||
(cek-eval "(do (define x 1) (set! x 10) x)"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 6. Quote and quasiquote
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "cek-quote"
|
||||
(deftest "quote"
|
||||
(let ((result (cek-eval "(quote (1 2 3))")))
|
||||
(assert-equal 3 (len result))))
|
||||
|
||||
(deftest "quasiquote with unquote"
|
||||
(assert-equal (list 1 42 3)
|
||||
(cek-eval "(let ((x 42)) `(1 ,x 3))"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 7. Thread-first
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "cek-thread-first"
|
||||
(deftest "simple thread"
|
||||
(assert-equal 3
|
||||
(cek-eval "(-> 1 (+ 2))")))
|
||||
|
||||
(deftest "multi-step thread"
|
||||
(assert-equal 6
|
||||
(cek-eval "(-> 1 (+ 2) (* 2))"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 8. CEK-specific: stepping
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "cek-stepping"
|
||||
(deftest "single step literal"
|
||||
(let ((state (make-cek-state 42 (test-env) (list))))
|
||||
(let ((stepped (cek-step state)))
|
||||
(assert-equal "continue" (cek-phase stepped))
|
||||
(assert-equal 42 (cek-value stepped))
|
||||
(assert-true (cek-terminal? stepped)))))
|
||||
|
||||
(deftest "single step if pushes frame"
|
||||
(let ((state (make-cek-state (sx-parse-one "(if true 1 2)") (test-env) (list))))
|
||||
(let ((stepped (cek-step state)))
|
||||
(assert-equal "eval" (cek-phase stepped))
|
||||
;; Should have pushed an IfFrame
|
||||
(assert-true (> (len (cek-kont stepped)) 0))
|
||||
(assert-equal "if" (frame-type (first (cek-kont stepped))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 9. Native continuations (shift/reset in CEK)
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "cek-continuations"
|
||||
(deftest "reset passthrough"
|
||||
(assert-equal 42
|
||||
(cek-eval "(reset 42)")))
|
||||
|
||||
(deftest "shift abort"
|
||||
(assert-equal 42
|
||||
(cek-eval "(reset (+ 1 (shift k 42)))")))
|
||||
|
||||
(deftest "shift with invoke"
|
||||
(assert-equal 11
|
||||
(cek-eval "(reset (+ 1 (shift k (k 10))))"))))
|
||||
140
shared/sx/ref/test-continuations.sx
Normal file
140
shared/sx/ref/test-continuations.sx
Normal file
@@ -0,0 +1,140 @@
|
||||
;; ==========================================================================
|
||||
;; test-continuations.sx — Tests for delimited continuations (shift/reset)
|
||||
;;
|
||||
;; Requires: test-framework.sx loaded, continuations extension enabled.
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 1. Basic shift/reset
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "basic-shift-reset"
|
||||
(deftest "reset passthrough"
|
||||
(assert-equal 42 (reset 42)))
|
||||
|
||||
(deftest "reset evaluates expression"
|
||||
(assert-equal 3 (reset (+ 1 2))))
|
||||
|
||||
(deftest "shift aborts to reset"
|
||||
(assert-equal 42 (reset (+ 1 (shift k 42)))))
|
||||
|
||||
(deftest "shift with single invoke"
|
||||
(assert-equal 11 (reset (+ 1 (shift k (k 10))))))
|
||||
|
||||
(deftest "shift with multiple invokes"
|
||||
(assert-equal (list 11 21)
|
||||
(reset (+ 1 (shift k (list (k 10) (k 20)))))))
|
||||
|
||||
(deftest "shift returns string"
|
||||
(assert-equal "aborted"
|
||||
(reset (+ 1 (shift k "aborted")))))
|
||||
|
||||
(deftest "shift returns nil"
|
||||
(assert-nil (reset (+ 1 (shift k nil)))))
|
||||
|
||||
(deftest "nested expression with shift"
|
||||
(assert-equal 16
|
||||
(+ 1 (reset (+ 10 (shift k (k 5))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 2. Continuation predicates
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "continuation-predicates"
|
||||
(deftest "k is a continuation inside shift"
|
||||
(assert-true
|
||||
(reset (shift k (continuation? k)))))
|
||||
|
||||
(deftest "number is not a continuation"
|
||||
(assert-false (continuation? 42)))
|
||||
|
||||
(deftest "function is not a continuation"
|
||||
(assert-false (continuation? (fn (x) x))))
|
||||
|
||||
(deftest "nil is not a continuation"
|
||||
(assert-false (continuation? nil)))
|
||||
|
||||
(deftest "string is not a continuation"
|
||||
(assert-false (continuation? "hello"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3. Continuation as value
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "continuation-as-value"
|
||||
(deftest "k returned from reset"
|
||||
;; shift body returns k itself — reset returns the continuation
|
||||
(let ((k (reset (+ 1 (shift k k)))))
|
||||
(assert-true (continuation? k))
|
||||
(assert-equal 11 (k 10))))
|
||||
|
||||
(deftest "invoke returned k multiple times"
|
||||
(let ((k (reset (+ 1 (shift k k)))))
|
||||
(assert-equal 11 (k 10))
|
||||
(assert-equal 21 (k 20))
|
||||
(assert-equal 2 (k 1))))
|
||||
|
||||
(deftest "pass k to another function"
|
||||
(let ((apply-k (fn (k v) (k v))))
|
||||
(assert-equal 15
|
||||
(reset (+ 5 (shift k (apply-k k 10)))))))
|
||||
|
||||
(deftest "k in data structure"
|
||||
(let ((result (reset (+ 1 (shift k (list k 42))))))
|
||||
(assert-equal 42 (nth result 1))
|
||||
(assert-equal 100 ((first result) 99)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 4. Nested reset
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "nested-reset"
|
||||
(deftest "inner reset captures independently"
|
||||
(assert-equal 12
|
||||
(reset (+ 1 (reset (+ 10 (shift k (k 1))))))))
|
||||
|
||||
(deftest "inner abort outer continues"
|
||||
(assert-equal 43
|
||||
(reset (+ 1 (reset (+ 10 (shift k 42)))))))
|
||||
|
||||
(deftest "outer shift captures outer reset"
|
||||
(assert-equal 100
|
||||
(reset (+ 1 (shift k (k 99)))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 5. Interaction with scoped effects
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "continuations-with-scopes"
|
||||
(deftest "provide survives resume"
|
||||
(assert-equal "dark"
|
||||
(reset (provide "theme" "dark"
|
||||
(+ 0 (shift k (k 0)))
|
||||
(context "theme")))))
|
||||
|
||||
(deftest "scope and emit across shift"
|
||||
(assert-equal (list "a")
|
||||
(reset (scope "acc"
|
||||
(emit! "acc" "a")
|
||||
(+ 0 (shift k (k 0)))
|
||||
(emitted "acc"))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 6. TCO interaction
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "tco-interaction"
|
||||
(deftest "shift in tail position"
|
||||
(assert-equal 42
|
||||
(reset (if true (shift k (k 42)) 0))))
|
||||
|
||||
(deftest "shift in let body"
|
||||
(assert-equal 10
|
||||
(reset (let ((x 5))
|
||||
(+ x (shift k (k 5))))))))
|
||||
@@ -171,3 +171,46 @@
|
||||
(list "wrap" children))
|
||||
(assert-equal (list "wrap" (list "a" "b"))
|
||||
(~wrapper "a" "b"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Scope integration — reactive tracking uses scope-push!/scope-pop!
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "scope integration"
|
||||
(deftest "deref outside reactive scope does not subscribe"
|
||||
(let ((s (signal 42)))
|
||||
;; Reading outside any reactive context should not add subscribers
|
||||
(assert-equal 42 (deref s))
|
||||
(assert-equal 0 (len (signal-subscribers s)))))
|
||||
|
||||
(deftest "computed uses scope for tracking"
|
||||
(let ((a (signal 1))
|
||||
(b (signal 2))
|
||||
(sum (computed (fn () (+ (deref a) (deref b))))))
|
||||
;; Each signal should have exactly 1 subscriber (the computed's recompute)
|
||||
(assert-equal 1 (len (signal-subscribers a)))
|
||||
(assert-equal 1 (len (signal-subscribers b)))
|
||||
;; Verify computed value
|
||||
(assert-equal 3 (deref sum))))
|
||||
|
||||
(deftest "nested effects with overlapping deps use scope correctly"
|
||||
(let ((shared (signal 0))
|
||||
(inner-only (signal 0))
|
||||
(outer-count (signal 0))
|
||||
(inner-count (signal 0)))
|
||||
;; Outer effect tracks shared
|
||||
(effect (fn () (do (deref shared) (swap! outer-count inc))))
|
||||
;; Inner effect tracks shared AND inner-only
|
||||
(effect (fn () (do (deref shared) (deref inner-only) (swap! inner-count inc))))
|
||||
;; Both ran once
|
||||
(assert-equal 1 (deref outer-count))
|
||||
(assert-equal 1 (deref inner-count))
|
||||
;; Changing shared triggers both
|
||||
(reset! shared 1)
|
||||
(assert-equal 2 (deref outer-count))
|
||||
(assert-equal 2 (deref inner-count))
|
||||
;; Changing inner-only triggers only inner
|
||||
(reset! inner-only 1)
|
||||
(assert-equal 2 (deref outer-count))
|
||||
(assert-equal 3 (deref inner-count)))))
|
||||
|
||||
@@ -597,3 +597,56 @@
|
||||
|
||||
(deftest "nil caller allows all"
|
||||
(assert-true (effects-subset? (list "io") nil))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; build-effect-annotations
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "build-effect-annotations"
|
||||
(deftest "builds annotations from io declarations"
|
||||
(let ((decls (list {"name" "fetch"} {"name" "save!"}))
|
||||
(anns (build-effect-annotations decls)))
|
||||
(assert-equal (list "io") (get anns "fetch"))
|
||||
(assert-equal (list "io") (get anns "save!"))))
|
||||
|
||||
(deftest "skips entries without name"
|
||||
(let ((decls (list {"name" "fetch"} {"other" "x"}))
|
||||
(anns (build-effect-annotations decls)))
|
||||
(assert-true (has-key? anns "fetch"))
|
||||
(assert-false (has-key? anns "other"))))
|
||||
|
||||
(deftest "empty declarations produce empty dict"
|
||||
(let ((anns (build-effect-annotations (list))))
|
||||
(assert-equal 0 (len (keys anns))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; check-component-effects
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
;; Define test components at top level so they're in the main env
|
||||
(defcomp ~eff-pure-card () :effects []
|
||||
(div (fetch "url")))
|
||||
|
||||
(defcomp ~eff-io-card () :effects [io]
|
||||
(div (fetch "url")))
|
||||
|
||||
(defcomp ~eff-unannot-card ()
|
||||
(div (fetch "url")))
|
||||
|
||||
(defsuite "check-component-effects"
|
||||
(deftest "pure component calling io produces diagnostic"
|
||||
(let ((anns {"~eff-pure-card" () "fetch" ("io")})
|
||||
(diagnostics (check-component-effects "~eff-pure-card" (test-env) anns)))
|
||||
(assert-true (> (len diagnostics) 0))))
|
||||
|
||||
(deftest "io component calling io produces no diagnostic"
|
||||
(let ((anns {"~eff-io-card" ("io") "fetch" ("io")})
|
||||
(diagnostics (check-component-effects "~eff-io-card" (test-env) anns)))
|
||||
(assert-equal 0 (len diagnostics))))
|
||||
|
||||
(deftest "unannotated component skips check"
|
||||
(let ((anns {"fetch" ("io")})
|
||||
(diagnostics (check-component-effects "~eff-unannot-card" (test-env) anns)))
|
||||
(assert-equal 0 (len diagnostics)))))
|
||||
|
||||
@@ -860,6 +860,40 @@
|
||||
annotations)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 15. Check component effects — convenience wrapper
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Validates that components respect their declared effect annotations.
|
||||
;; Delegates to check-body-walk with nil type checking (effects only).
|
||||
|
||||
(define check-component-effects
|
||||
(fn ((comp-name :as string) env effect-annotations)
|
||||
;; Check a single component's effect usage. Returns diagnostics list.
|
||||
;; Skips type checking — only checks effect violations.
|
||||
(let ((comp (env-get env comp-name))
|
||||
(diagnostics (list)))
|
||||
(when (= (type-of comp) "component")
|
||||
(let ((body (component-body comp)))
|
||||
(check-body-walk body comp-name (dict) (dict) nil env
|
||||
diagnostics nil effect-annotations)))
|
||||
diagnostics)))
|
||||
|
||||
(define check-all-effects
|
||||
(fn (env effect-annotations)
|
||||
;; Check all components in env for effect violations.
|
||||
;; Returns list of all diagnostics.
|
||||
(let ((all-diagnostics (list)))
|
||||
(for-each
|
||||
(fn (name)
|
||||
(let ((val (env-get env name)))
|
||||
(when (= (type-of val) "component")
|
||||
(for-each
|
||||
(fn (d) (append! all-diagnostics d))
|
||||
(check-component-effects name env effect-annotations)))))
|
||||
(keys env))
|
||||
all-diagnostics)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface summary
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -361,11 +361,15 @@ class Continuation:
|
||||
|
||||
Callable with one argument — provides the value that the shift
|
||||
expression "returns" within the delimited context.
|
||||
|
||||
_cek_data: optional dict with CEK frame data (captured frames, rest-kont)
|
||||
for continuations created by the explicit CEK machine.
|
||||
"""
|
||||
__slots__ = ("fn",)
|
||||
__slots__ = ("fn", "_cek_data")
|
||||
|
||||
def __init__(self, fn):
|
||||
self.fn = fn
|
||||
self._cek_data = None
|
||||
|
||||
def __call__(self, value=NIL):
|
||||
return self.fn(value)
|
||||
@@ -397,6 +401,43 @@ class EvalError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SxExpr
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class SxExpr(str):
|
||||
"""Pre-built sx source that serialize() outputs unquoted.
|
||||
|
||||
``SxExpr`` is a ``str`` subclass, so it works everywhere a plain
|
||||
string does (join, startswith, f-strings, isinstance checks). The
|
||||
only difference: ``serialize()`` emits it unquoted instead of
|
||||
wrapping it in double-quotes.
|
||||
|
||||
Use this to nest sx call strings inside other sx_call() invocations
|
||||
without them being quoted as strings::
|
||||
|
||||
sx_call("parent", child=sx_call("child", x=1))
|
||||
# => (~parent :child (~child :x 1))
|
||||
"""
|
||||
|
||||
def __new__(cls, source: str = "") -> "SxExpr":
|
||||
return str.__new__(cls, source)
|
||||
|
||||
@property
|
||||
def source(self) -> str:
|
||||
"""The raw SX source string (backward compat)."""
|
||||
return str.__str__(self)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"SxExpr({str.__repr__(self)})"
|
||||
|
||||
def __add__(self, other: object) -> "SxExpr":
|
||||
return SxExpr(str.__add__(self, str(other)))
|
||||
|
||||
def __radd__(self, other: object) -> "SxExpr":
|
||||
return SxExpr(str.__add__(str(other), self))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Type alias
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
195
sx/sx/geography/cek.sx
Normal file
195
sx/sx/geography/cek.sx
Normal file
@@ -0,0 +1,195 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; CEK Machine — Geography section
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Island demos
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; Counter: signal + deref in text position
|
||||
(defisland ~geography/cek/demo-counter (&key initial)
|
||||
(let ((count (signal (or initial 0)))
|
||||
(doubled (computed (fn () (* 2 (deref count))))))
|
||||
(div :class "rounded-lg border border-stone-200 p-4 space-y-2"
|
||||
(div :class "flex items-center gap-3"
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm"
|
||||
:on-click (fn (e) (swap! count dec)) "-")
|
||||
(span :class "text-2xl font-bold text-violet-700 min-w-[3ch] text-center"
|
||||
(deref count))
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm"
|
||||
:on-click (fn (e) (swap! count inc)) "+"))
|
||||
(p :class "text-sm text-stone-500"
|
||||
(str "doubled: " (deref doubled))))))
|
||||
|
||||
;; Computed chain: base -> doubled -> quadrupled
|
||||
(defisland ~geography/cek/demo-chain ()
|
||||
(let ((base (signal 1))
|
||||
(doubled (computed (fn () (* (deref base) 2))))
|
||||
(quadrupled (computed (fn () (* (deref doubled) 2)))))
|
||||
(div :class "rounded-lg border border-stone-200 p-4 space-y-2"
|
||||
(div :class "flex items-center gap-3"
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm"
|
||||
:on-click (fn (e) (swap! base dec)) "-")
|
||||
(span :class "text-2xl font-bold text-violet-700 min-w-[3ch] text-center"
|
||||
(deref base))
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm"
|
||||
:on-click (fn (e) (swap! base inc)) "+"))
|
||||
(p :class "text-sm text-stone-500"
|
||||
(str "doubled: " (deref doubled) " | quadrupled: " (deref quadrupled))))))
|
||||
|
||||
;; Reactive attribute: (deref sig) in :class position
|
||||
(defisland ~geography/cek/demo-reactive-attr ()
|
||||
(let ((danger (signal false)))
|
||||
(div :class "rounded-lg border border-stone-200 p-4 space-y-3"
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm"
|
||||
:on-click (fn (e) (swap! danger not))
|
||||
(if (deref danger) "Safe mode" "Danger mode"))
|
||||
(div :class (str "p-3 rounded font-medium transition-colors "
|
||||
(if (deref danger)
|
||||
"bg-red-100 text-red-800"
|
||||
"bg-green-100 text-green-800"))
|
||||
(if (deref danger)
|
||||
"DANGER: reactive class binding via CEK"
|
||||
"SAFE: reactive class binding via CEK")))))
|
||||
|
||||
;; Stopwatch: effect + cleanup
|
||||
(defisland ~geography/cek/demo-stopwatch ()
|
||||
(let ((running (signal false))
|
||||
(elapsed (signal 0))
|
||||
(time-text (create-text-node "0.0s"))
|
||||
(btn-text (create-text-node "Start")))
|
||||
(effect (fn ()
|
||||
(when (deref running)
|
||||
(let ((id (set-interval (fn () (swap! elapsed inc)) 100)))
|
||||
(fn () (clear-interval id))))))
|
||||
(effect (fn ()
|
||||
(let ((e (deref elapsed)))
|
||||
(dom-set-text-content time-text
|
||||
(str (floor (/ e 10)) "." (mod e 10) "s")))))
|
||||
(effect (fn ()
|
||||
(dom-set-text-content btn-text
|
||||
(if (deref running) "Stop" "Start"))))
|
||||
(div :class "rounded-lg border border-stone-200 p-4"
|
||||
(div :class "flex items-center gap-3"
|
||||
(span :class "text-2xl font-bold text-violet-700 font-mono min-w-[5ch]" time-text)
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm"
|
||||
:on-click (fn (e) (swap! running not)) btn-text)
|
||||
(button :class "px-3 py-1 rounded bg-stone-400 text-white text-sm"
|
||||
:on-click (fn (e) (reset! running false) (reset! elapsed 0)) "Reset")))))
|
||||
|
||||
;; Batch: two signals, one notification
|
||||
(defisland ~geography/cek/demo-batch ()
|
||||
(let ((first-sig (signal 0))
|
||||
(second-sig (signal 0))
|
||||
(renders (signal 0)))
|
||||
(effect (fn ()
|
||||
(deref first-sig) (deref second-sig)
|
||||
(swap! renders inc)))
|
||||
(div :class "rounded-lg border border-stone-200 p-4 space-y-2"
|
||||
(div :class "flex items-center gap-4 text-sm"
|
||||
(span (str "first: " (deref first-sig)))
|
||||
(span (str "second: " (deref second-sig)))
|
||||
(span :class "px-2 py-0.5 rounded bg-green-100 text-green-800 text-xs font-semibold"
|
||||
(str "renders: " (deref renders))))
|
||||
(div :class "flex items-center gap-2"
|
||||
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm"
|
||||
:on-click (fn (e)
|
||||
(batch (fn ()
|
||||
(swap! first-sig inc)
|
||||
(swap! second-sig inc))))
|
||||
"Batch +1")
|
||||
(button :class "px-3 py-1 rounded bg-stone-400 text-white text-sm"
|
||||
:on-click (fn (e)
|
||||
(swap! first-sig inc)
|
||||
(swap! second-sig inc))
|
||||
"No-batch +1")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Overview page content
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~geography/cek/cek-content ()
|
||||
(~docs/page :title "CEK Machine"
|
||||
|
||||
(~docs/section :title "Three registers" :id "registers"
|
||||
(p "The CEK machine makes evaluation explicit. Every step is a pure function from state to state:")
|
||||
(ul :class "space-y-1 text-stone-600 list-disc pl-5"
|
||||
(li (strong "C") "ontrol — the expression being evaluated")
|
||||
(li (strong "E") "nvironment — the bindings in scope")
|
||||
(li (strong "K") "ontinuation — what to do with the result"))
|
||||
(p "The tree-walk evaluator uses the same three things, but hides them in the call stack. The CEK makes them " (em "data") " — inspectable, serializable, capturable."))
|
||||
|
||||
(~docs/section :title "Why it matters" :id "why"
|
||||
(p "Making the continuation explicit enables:")
|
||||
(ul :class "space-y-1 text-stone-600 list-disc pl-5"
|
||||
(li (strong "Stepping") " — pause evaluation, inspect state, resume")
|
||||
(li (strong "Serialization") " — save a computation mid-flight, restore later")
|
||||
(li (strong "Delimited continuations") " — " (code "shift") "/" (code "reset") " capture \"the rest of this expression\" as a value")
|
||||
(li (strong "Deref-as-shift") " — " (code "(deref sig)") " inside a reactive boundary captures the continuation as the subscriber")))
|
||||
|
||||
(~docs/section :title "Default evaluator" :id "default"
|
||||
(p "CEK is the default evaluator on both client (JS) and server (Python). Every " (code "eval-expr") " call goes through " (code "cek-run") ". The tree-walk evaluator is preserved as " (code "_tree_walk_eval_expr") " for test runners that interpret " (code ".sx") " files.")
|
||||
(p "The CEK is defined in two spec files:")
|
||||
(ul :class "space-y-1 text-stone-600 list-disc pl-5"
|
||||
(li (code "frames.sx") " — frame types (IfFrame, ArgFrame, ResetFrame, ReactiveResetFrame, ...)")
|
||||
(li (code "cek.sx") " — step function, run loop, special form handlers, continuation operations")))
|
||||
|
||||
(~docs/section :title "Deref as shift" :id "deref-as-shift"
|
||||
(p "The reactive payoff. When " (code "(deref sig)") " encounters a signal inside a " (code "reactive-reset") " boundary:")
|
||||
(ol :class "space-y-1 text-stone-600 list-decimal pl-5"
|
||||
(li (strong "Shift") " — capture all frames between here and the reactive-reset")
|
||||
(li (strong "Subscribe") " — register the captured continuation as a signal subscriber")
|
||||
(li (strong "Return") " — flow the current signal value through the rest of the expression"))
|
||||
(p "When the signal changes, the captured continuation is re-invoked with the new value. The " (code "update-fn") " on the ReactiveResetFrame mutates the DOM. No explicit " (code "effect()") " wrapping needed.")
|
||||
(~docs/code :code (highlight
|
||||
";; User writes:\n(div :class (str \"count-\" (deref counter))\n (str \"Value: \" (deref counter)))\n\n;; CEK sees (deref counter) → signal? → reactive-reset on stack?\n;; Yes: capture (str \"count-\" [HOLE]) as continuation\n;; Register as subscriber. Return current value.\n;; When counter changes: re-invoke continuation → update DOM."
|
||||
"lisp")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Demo page content
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~geography/cek/cek-demo-content ()
|
||||
(~docs/page :title "CEK Demo"
|
||||
|
||||
(~docs/section :title "What this demonstrates" :id "what"
|
||||
(p "These are " (strong "live islands") " evaluated by the CEK machine. Every " (code "eval-expr") " goes through " (code "cek-run") ". Every " (code "(deref sig)") " in an island creates a reactive DOM binding via continuation frames.")
|
||||
(p "The CEK machine is defined in " (code "cek.sx") " (160 lines) and " (code "frames.sx") " (100 lines) — pure s-expressions, bootstrapped to both JavaScript and Python."))
|
||||
|
||||
(~docs/section :title "1. Counter" :id "demo-counter"
|
||||
(p (code "(deref count)") " in text position creates a reactive text node. " (code "(deref doubled)") " is a computed that updates when count changes.")
|
||||
(~geography/cek/demo-counter :initial 0)
|
||||
(~docs/code :code (highlight
|
||||
"(defisland ~demo-counter (&key initial)\n (let ((count (signal (or initial 0)))\n (doubled (computed (fn () (* 2 (deref count))))))\n (div\n (button :on-click (fn (e) (swap! count dec)) \"-\")\n (span (deref count))\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (p (str \"doubled: \" (deref doubled))))))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/section :title "2. Computed chain" :id "demo-chain"
|
||||
(p "Three levels of computed: base -> doubled -> quadrupled. Change base, all propagate.")
|
||||
(~geography/cek/demo-chain)
|
||||
(~docs/code :code (highlight
|
||||
"(let ((base (signal 1))\n (doubled (computed (fn () (* (deref base) 2))))\n (quadrupled (computed (fn () (* (deref doubled) 2)))))\n (span (deref base))\n (p (str \"doubled: \" (deref doubled)\n \" | quadrupled: \" (deref quadrupled))))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/section :title "3. Reactive attributes" :id "demo-attr"
|
||||
(p (code "(deref sig)") " in " (code ":class") " position. The CEK evaluates the " (code "str") " expression, and when the signal changes, the continuation re-evaluates and updates the attribute.")
|
||||
(~geography/cek/demo-reactive-attr)
|
||||
(~docs/code :code (highlight
|
||||
"(div :class (str \"p-3 rounded font-medium \"\n (if (deref danger)\n \"bg-red-100 text-red-800\"\n \"bg-green-100 text-green-800\"))\n (if (deref danger) \"DANGER\" \"SAFE\"))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/section :title "4. Effect + cleanup" :id "demo-stopwatch"
|
||||
(p "Effects still work through CEK. This stopwatch uses " (code "effect") " with cleanup — toggling the signal clears the interval.")
|
||||
(~geography/cek/demo-stopwatch)
|
||||
(~docs/code :code (highlight
|
||||
"(effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! elapsed inc)) 100)))\n (fn () (clear-interval id))))))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/section :title "5. Batch coalescing" :id "demo-batch"
|
||||
(p "Two signals updated in " (code "batch") " — one notification cycle. Compare render counts between batch and no-batch.")
|
||||
(~geography/cek/demo-batch)
|
||||
(~docs/code :code (highlight
|
||||
"(batch (fn ()\n (swap! first-sig inc)\n (swap! second-sig inc)))\n;; One render pass, not two."
|
||||
"lisp")))))
|
||||
@@ -166,6 +166,12 @@
|
||||
(dict :label "Optimistic" :href "/sx/(geography.(isomorphism.optimistic))")
|
||||
(dict :label "Offline" :href "/sx/(geography.(isomorphism.offline))")))
|
||||
|
||||
(define cek-nav-items (list
|
||||
(dict :label "Overview" :href "/sx/(geography.(cek))"
|
||||
:summary "The CEK machine — explicit evaluator with Control, Environment, Kontinuation. Three registers, pure step function.")
|
||||
(dict :label "Demo" :href "/sx/(geography.(cek.demo))"
|
||||
:summary "Live islands evaluated by the CEK machine. Counter, computed chains, reactive attributes — all through explicit continuation frames.")))
|
||||
|
||||
(define plans-nav-items (list
|
||||
(dict :label "Status" :href "/sx/(etc.(plan.status))"
|
||||
:summary "Audit of all plans — what's done, what's in progress, and what remains.")
|
||||
@@ -226,7 +232,13 @@
|
||||
(dict :label "Scoped Effects" :href "/sx/(etc.(plan.scoped-effects))"
|
||||
:summary "Algebraic effects as the unified foundation — spreads, islands, lakes, signals, and context are all instances of one primitive: a named scope with downward value, upward accumulation, and a propagation mode.")
|
||||
(dict :label "Foundations" :href "/sx/(etc.(plan.foundations))"
|
||||
:summary "The computational floor — from scoped effects through algebraic effects and delimited continuations to the CEK machine. Why three registers are irreducible, and the three-axis model (depth, topology, linearity).")))
|
||||
:summary "The computational floor — from scoped effects through algebraic effects and delimited continuations to the CEK machine. Why three registers are irreducible, and the three-axis model (depth, topology, linearity).")
|
||||
(dict :label "Deref as Shift" :href "/sx/(etc.(plan.cek-reactive))"
|
||||
:summary "Phase B: replace explicit effect wrapping with implicit continuation capture. Deref inside reactive-reset performs shift, capturing the rest of the expression as the subscriber.")
|
||||
(dict :label "Reactive Runtime" :href "/sx/(etc.(plan.reactive-runtime))"
|
||||
:summary "Seven feature layers — ref, foreign FFI, state machines, commands with undo/redo, render loops, keyed lists, client-first app shell. Zero new platform primitives.")
|
||||
(dict :label "Rust/WASM Host" :href "/sx/(etc.(plan.rust-wasm-host))"
|
||||
:summary "Bootstrap the SX spec to Rust, compile to WASM, replace sx-browser.js. Shared platform layer for DOM, phased rollout from parse to full parity.")))
|
||||
|
||||
(define reactive-islands-nav-items (list
|
||||
(dict :label "Overview" :href "/sx/(geography.(reactive))"
|
||||
@@ -382,7 +394,8 @@
|
||||
:summary "Child-to-parent communication across render boundaries — spread, collect!, reactive-spread, built on scopes."}
|
||||
{:label "Marshes" :href "/sx/(geography.(marshes))"
|
||||
:summary "Where reactivity and hypermedia interpenetrate — server writes to signals, reactive transforms reshape server content, client state modifies how hypermedia is interpreted."}
|
||||
{:label "Isomorphism" :href "/sx/(geography.(isomorphism))" :children isomorphism-nav-items})}
|
||||
{:label "Isomorphism" :href "/sx/(geography.(isomorphism))" :children isomorphism-nav-items}
|
||||
{:label "CEK Machine" :href "/sx/(geography.(cek))" :children cek-nav-items})}
|
||||
{:label "Language" :href "/sx/(language)"
|
||||
:children (list
|
||||
{:label "Docs" :href "/sx/(language.(doc))" :children docs-nav-items}
|
||||
|
||||
@@ -60,6 +60,18 @@
|
||||
"phase2" '(~reactive-islands/phase2/reactive-islands-phase2-content)
|
||||
:else '(~reactive-islands/index/reactive-islands-index-content)))))
|
||||
|
||||
(define cek
|
||||
(fn (slug)
|
||||
(if (nil? slug)
|
||||
'(~geography/cek/cek-content)
|
||||
(case slug
|
||||
"demo" '(~geography/cek/cek-demo-content)
|
||||
:else '(~geography/cek/cek-content)))))
|
||||
|
||||
(define provide
|
||||
(fn (content)
|
||||
(if (nil? content) '(~geography/provide-content) content)))
|
||||
|
||||
(define scopes
|
||||
(fn (content)
|
||||
(if (nil? content) '(~geography/scopes-content) content)))
|
||||
@@ -535,4 +547,6 @@
|
||||
"sx-protocol" '(~plans/sx-protocol/plan-sx-protocol-content)
|
||||
"scoped-effects" '(~plans/scoped-effects/plan-scoped-effects-content)
|
||||
"foundations" '(~plans/foundations/plan-foundations-content)
|
||||
"cek-reactive" '(~plans/cek-reactive/plan-cek-reactive-content)
|
||||
"reactive-runtime" '(~plans/reactive-runtime/plan-reactive-runtime-content)
|
||||
:else '(~plans/index/plans-index-content)))))
|
||||
|
||||
302
sx/sx/plans/cek-reactive.sx
Normal file
302
sx/sx/plans/cek-reactive.sx
Normal file
@@ -0,0 +1,302 @@
|
||||
;; Deref as Shift — CEK-Based Reactive DOM Renderer
|
||||
;; Phase B: replace explicit effects with implicit continuation capture.
|
||||
|
||||
(defcomp ~plans/cek-reactive/plan-cek-reactive-content ()
|
||||
(~docs/page :title "Deref as Shift — CEK-Based Reactive DOM Renderer"
|
||||
|
||||
(p :class "text-stone-500 text-sm italic mb-8"
|
||||
"Phase A collapsed signals to plain dicts with zero platform primitives. "
|
||||
"Phase B replaces explicit effect wrapping in the reactive DOM renderer "
|
||||
"with implicit continuation capture: when " (code "deref") " encounters a signal "
|
||||
"inside a " (code "reactive-reset") " boundary, it performs " (code "shift") ", "
|
||||
"capturing the rest of the expression as a continuation. "
|
||||
"That continuation IS the subscriber.")
|
||||
|
||||
;; =====================================================================
|
||||
;; The Insight
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "The Insight" :id "insight"
|
||||
|
||||
(p "Each reactive binding is a micro-computation:")
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (code "reactive-text") ": given signal value, set text node content to " (code "(str value)"))
|
||||
(li (code "reactive-attr") ": given signal value, set attribute to " (code "(str value)")))
|
||||
|
||||
(p "Currently wrapped in explicit " (code "effect") " calls. With deref-as-shift, "
|
||||
"the continuation captures this automatically:")
|
||||
|
||||
(~docs/code :code (highlight
|
||||
";; User writes:\n(div :class (str \"count-\" (deref counter))\n (str \"Value: \" (deref counter)))\n\n;; Renderer internally wraps each expression:\n(div :class (reactive-reset update-attr-fn (str \"count-\" (deref counter)))\n (reactive-reset update-text-fn (str \"Value: \" (deref counter))))\n\n;; When (deref counter) hits a signal inside reactive-reset:\n;; 1. Shift: capture continuation (str \"count-\" [HOLE])\n;; 2. Register continuation as signal subscriber\n;; 3. Return current value for initial render\n;; When counter changes:\n;; Re-invoke continuation with new value → update-fn updates DOM"
|
||||
"lisp")))
|
||||
|
||||
;; =====================================================================
|
||||
;; Step 1: Bootstrap CEK to JavaScript
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Step 1: Bootstrap CEK to JavaScript" :id "step-1"
|
||||
|
||||
(p "Add " (code "frames.sx") " + " (code "cek.sx") " to the JS build pipeline. "
|
||||
"Currently CEK is Python-only.")
|
||||
|
||||
(~docs/subsection :title "1a. platform_js.py — SPEC_MODULES + platform code"
|
||||
|
||||
(p "Add to " (code "SPEC_MODULES") " dict:")
|
||||
(~docs/code :code (highlight
|
||||
"\"frames\": (\"frames.sx\", \"frames (CEK continuation frames)\"),\n\"cek\": (\"cek.sx\", \"cek (explicit CEK machine evaluator)\"),\n\n# Add ordering (new constant):\nSPEC_MODULE_ORDER = [\"deps\", \"frames\", \"page-helpers\", \"router\", \"signals\", \"cek\"]"
|
||||
"python"))
|
||||
|
||||
(p "Add " (code "PLATFORM_CEK_JS") " constant (mirrors " (code "PLATFORM_CEK_PY") "):")
|
||||
(~docs/code :code (highlight
|
||||
"// Primitive aliases used by cek.sx\nvar inc = PRIMITIVES[\"inc\"];\nvar dec = PRIMITIVES[\"dec\"];\nvar zip_pairs = PRIMITIVES[\"zip-pairs\"];\n\nfunction makeCekContinuation(captured, restKont) {\n var c = new Continuation(function(v) { return v !== undefined ? v : NIL; });\n c._cek_data = {\"captured\": captured, \"rest-kont\": restKont};\n return c;\n}\nfunction continuationData(c) {\n return (c && c._cek_data) ? c._cek_data : {};\n}"
|
||||
"javascript"))
|
||||
|
||||
(p "Add " (code "CEK_FIXUPS_JS") " — iterative " (code "cek-run") " override:")
|
||||
(~docs/code :code (highlight
|
||||
"cekRun = function(state) {\n while (!cekTerminal_p(state)) { state = cekStep(state); }\n return cekValue(state);\n};"
|
||||
"javascript")))
|
||||
|
||||
(~docs/subsection :title "1b. run_js_sx.py — Update compile_ref_to_js"
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li "Auto-add " (code "\"frames\"") " when " (code "\"cek\"") " in spec_mod_set (mirror Python " (code "bootstrap_py.py") ")")
|
||||
(li "Auto-add " (code "\"cek\"") " + " (code "\"frames\"") " when " (code "\"dom\"") " adapter included (CEK needed for reactive rendering)")
|
||||
(li "Use " (code "SPEC_MODULE_ORDER") " for ordering instead of " (code "sorted()"))
|
||||
(li "Add " (code "has_cek") " flag")
|
||||
(li "Include " (code "PLATFORM_CEK_JS") " after transpiled code when " (code "has_cek"))
|
||||
(li "Include " (code "CEK_FIXUPS_JS") " in fixups section when " (code "has_cek"))))
|
||||
|
||||
(~docs/subsection :title "1c. js.sx — RENAMES for predicate functions"
|
||||
(p "Default mangling handles most names. Only add RENAMES where " (code "?")
|
||||
" suffix needs clean JS names:")
|
||||
(~docs/code :code (highlight
|
||||
"\"cek-terminal?\" \"cekTerminalP\"\n\"kont-empty?\" \"kontEmptyP\"\n\"make-cek-continuation\" \"makeCekContinuation\"\n\"continuation-data\" \"continuationData\""
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "1d. bootstrap_py.py — RENAMES for CEK predicates"
|
||||
(~docs/code :code (highlight
|
||||
"\"cek-terminal?\": \"cek_terminal_p\",\n\"kont-empty?\": \"kont_empty_p\",\n\"make-cek-continuation\": \"make_cek_continuation\",\n\"continuation-data\": \"continuation_data\","
|
||||
"python")))
|
||||
|
||||
(~docs/subsection :title "Verification"
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li "Rebootstrap JS: " (code "python3 bootstrap_js.py"))
|
||||
(li "Check output contains frame constructors + CEK step functions")
|
||||
(li "Run existing CEK Python tests: " (code "python3 run_cek_tests.py") " (should still pass)"))))
|
||||
|
||||
;; =====================================================================
|
||||
;; Step 2: ReactiveResetFrame + DerefFrame
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Step 2: ReactiveResetFrame + DerefFrame" :id "step-2"
|
||||
|
||||
(p "New frame types in " (code "frames.sx") " that enable deref-as-shift.")
|
||||
|
||||
(~docs/subsection :title "2a. New frame constructors"
|
||||
(~docs/code :code (highlight
|
||||
";; ReactiveResetFrame: delimiter for reactive deref-as-shift\n;; Carries an update-fn that gets called with new values on re-render.\n(define make-reactive-reset-frame\n (fn (env update-fn first-render?)\n {:type \"reactive-reset\" :env env :update-fn update-fn\n :first-render first-render?}))\n\n;; DerefFrame: awaiting evaluation of deref's argument\n(define make-deref-frame\n (fn (env)\n {:type \"deref\" :env env}))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "2b. Update kont-capture-to-reset"
|
||||
(p "Must stop at EITHER " (code "\"reset\"") " OR " (code "\"reactive-reset\"") ":")
|
||||
(~docs/code :code (highlight
|
||||
"(define kont-capture-to-reset\n (fn (kont)\n (define scan\n (fn (k captured)\n (if (empty? k)\n (error \"shift without enclosing reset\")\n (let ((frame (first k)))\n (if (or (= (frame-type frame) \"reset\")\n (= (frame-type frame) \"reactive-reset\"))\n (list captured (rest k))\n (scan (rest k) (append captured (list frame))))))))\n (scan kont (list))))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "2c. Helpers to scan for ReactiveResetFrame"
|
||||
(~docs/code :code (highlight
|
||||
"(define has-reactive-reset-frame?\n (fn (kont)\n (if (empty? kont) false\n (if (= (frame-type (first kont)) \"reactive-reset\") true\n (has-reactive-reset-frame? (rest kont))))))\n\n;; Returns 3 values: (captured, frame, rest)\n(define kont-capture-to-reactive-reset\n (fn (kont)\n (define scan\n (fn (k captured)\n (if (empty? k)\n (error \"reactive deref without enclosing reactive-reset\")\n (let ((frame (first k)))\n (if (= (frame-type frame) \"reactive-reset\")\n (list captured frame (rest k))\n (scan (rest k) (append captured (list frame))))))))\n (scan kont (list))))"
|
||||
"lisp"))))
|
||||
|
||||
;; =====================================================================
|
||||
;; Step 3: Make deref a CEK Special Form
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Step 3: Make deref a CEK Special Form" :id "step-3"
|
||||
|
||||
(p "When " (code "deref") " encounters a signal inside a " (code "reactive-reset")
|
||||
", perform shift.")
|
||||
|
||||
(~docs/subsection :title "3a. Add to special form dispatch in cek.sx"
|
||||
(p "In the dispatch table (around where " (code "reset") " and " (code "shift") " are):")
|
||||
(~docs/code :code (highlight
|
||||
"(= name \"deref\") (step-sf-deref args env kont)"
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "3b. step-sf-deref"
|
||||
(p "Evaluates the argument first (push DerefFrame), then decides whether to shift:")
|
||||
(~docs/code :code (highlight
|
||||
"(define step-sf-deref\n (fn (args env kont)\n (make-cek-state\n (first args) env\n (kont-push (make-deref-frame env) kont))))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "3c. Handle DerefFrame in step-continue"
|
||||
(p "When the deref argument is evaluated, decide: shift or return.")
|
||||
(~docs/code :code (highlight
|
||||
"(= ft \"deref\")\n (let ((val value)\n (fenv (get frame \"env\")))\n (if (not (signal? val))\n ;; Not a signal: pass through\n (make-cek-value val fenv rest-k)\n ;; Signal: check for ReactiveResetFrame\n (if (has-reactive-reset-frame? rest-k)\n ;; Perform reactive shift\n (reactive-shift-deref val fenv rest-k)\n ;; No reactive-reset: normal deref (scope-based tracking)\n (do\n (let ((ctx (context \"sx-reactive\" nil)))\n (when ctx\n (let ((dep-list (get ctx \"deps\"))\n (notify-fn (get ctx \"notify\")))\n (when (not (contains? dep-list val))\n (append! dep-list val)\n (signal-add-sub! val notify-fn)))))\n (make-cek-value (signal-value val) fenv rest-k)))))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "3d. reactive-shift-deref — the heart"
|
||||
(~docs/code :code (highlight
|
||||
"(define reactive-shift-deref\n (fn (sig env kont)\n (let ((scan-result (kont-capture-to-reactive-reset kont))\n (captured-frames (first scan-result))\n (reset-frame (nth scan-result 1))\n (remaining-kont (nth scan-result 2))\n (update-fn (get reset-frame \"update-fn\")))\n ;; Sub-scope for nested subscriber cleanup on re-invocation\n (let ((sub-disposers (list)))\n (let ((subscriber\n (fn ()\n ;; Dispose previous nested subscribers\n (for-each (fn (d) (invoke d)) sub-disposers)\n (set! sub-disposers (list))\n ;; Re-invoke: push fresh ReactiveResetFrame (first-render=false)\n (let ((new-reset (make-reactive-reset-frame env update-fn false))\n (new-kont (concat captured-frames\n (list new-reset)\n remaining-kont)))\n (with-island-scope\n (fn (d) (append! sub-disposers d))\n (fn ()\n (cek-run\n (make-cek-value (signal-value sig) env new-kont))))))))\n ;; Register subscriber\n (signal-add-sub! sig subscriber)\n ;; Register cleanup with island scope\n (register-in-scope\n (fn ()\n (signal-remove-sub! sig subscriber)\n (for-each (fn (d) (invoke d)) sub-disposers)))\n ;; Return current value for initial render\n (make-cek-value (signal-value sig) env remaining-kont))))))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "3e. Handle ReactiveResetFrame in step-continue"
|
||||
(p "When expression completes normally (or after re-invocation):")
|
||||
(~docs/code :code (highlight
|
||||
"(= ft \"reactive-reset\")\n (let ((update-fn (get frame \"update-fn\"))\n (first? (get frame \"first-render\")))\n ;; On re-render (not first), call update-fn with new value\n (when (and update-fn (not first?))\n (invoke update-fn value))\n (make-cek-value value env rest-k))"
|
||||
"lisp"))
|
||||
(p (strong "Key:") " On first render, update-fn is NOT called — the value flows back to the caller "
|
||||
"who inserts it into the DOM. On re-render (subscriber fires), update-fn IS called "
|
||||
"to mutate the existing DOM.")))
|
||||
|
||||
;; =====================================================================
|
||||
;; Step 4: Integrate into adapter-dom.sx
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Step 4: Integrate into adapter-dom.sx" :id "step-4"
|
||||
|
||||
(p "Add CEK reactive path alongside existing effect-based path, controlled by opt-in flag.")
|
||||
|
||||
(~docs/subsection :title "4a. Opt-in flag"
|
||||
(~docs/code :code (highlight
|
||||
"(define *use-cek-reactive* false)\n(define enable-cek-reactive! (fn () (set! *use-cek-reactive* true)))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "4b. CEK reactive attribute binding"
|
||||
(~docs/code :code (highlight
|
||||
"(define cek-reactive-attr\n (fn (el attr-name expr env)\n (let ((update-fn (fn (val)\n (cond\n (or (nil? val) (= val false)) (dom-remove-attr el attr-name)\n (= val true) (dom-set-attr el attr-name \"\")\n :else (dom-set-attr el attr-name (str val))))))\n ;; Mark for morph protection\n (let ((existing (or (dom-get-attr el \"data-sx-reactive-attrs\") \"\"))\n (updated (if (empty? existing) attr-name (str existing \",\" attr-name))))\n (dom-set-attr el \"data-sx-reactive-attrs\" updated))\n ;; Initial render via CEK with ReactiveResetFrame\n (let ((initial (cek-run\n (make-cek-state expr env\n (list (make-reactive-reset-frame env update-fn true))))))\n (invoke update-fn initial)))))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "4c. Modify render-dom-element dispatch"
|
||||
(p "In attribute processing, add conditional:")
|
||||
(~docs/code :code (highlight
|
||||
"(context \"sx-island-scope\" nil)\n (if *use-cek-reactive*\n (cek-reactive-attr el attr-name attr-expr env)\n (reactive-attr el attr-name\n (fn () (trampoline (eval-expr attr-expr env)))))"
|
||||
"lisp"))
|
||||
(p "Similarly for text positions and conditional rendering."))
|
||||
|
||||
(~docs/subsection :title "4d. CEK reactive text"
|
||||
(~docs/code :code (highlight
|
||||
"(define cek-reactive-text\n (fn (expr env)\n (let ((node (create-text-node \"\"))\n (update-fn (fn (val)\n (dom-set-text-content node (str val)))))\n (let ((initial (cek-run\n (make-cek-state expr env\n (list (make-reactive-reset-frame env update-fn true))))))\n (dom-set-text-content node (str initial))\n node))))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "4e. What stays unchanged"
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (code "reactive-list") " — keyed reconciliation is complex; keep effect-based for now")
|
||||
(li (code "reactive-spread") " — spread tracking is complex; keep effect-based")
|
||||
(li (code "effect") ", " (code "computed") " — still needed for non-rendering side effects")
|
||||
(li "Existing " (code "reactive-*") " functions — remain as default path"))))
|
||||
|
||||
;; =====================================================================
|
||||
;; Step 5: Tests
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Step 5: Tests" :id "step-5"
|
||||
|
||||
(~docs/subsection :title "5a. test-cek-reactive.sx"
|
||||
(p "Tests:")
|
||||
(ol :class "list-decimal pl-6 mb-4 space-y-1"
|
||||
(li (code "deref") " non-signal passes through (no shift)")
|
||||
(li (code "deref") " signal without reactive-reset: returns value, no subscription")
|
||||
(li (code "deref") " signal with reactive-reset: shifts, registers subscriber, update-fn called on change")
|
||||
(li "Expression with deref: " (code "(str \"hello \" (deref sig))") " — continuation captures rest")
|
||||
(li "Multi-deref: both signals create subscribers, both fire correctly")
|
||||
(li "Disposal: removing island scope unsubscribes all continuations")
|
||||
(li "Stale subscriber cleanup: re-invocation disposes nested subscribers")))
|
||||
|
||||
(~docs/subsection :title "5b. run_cek_reactive_tests.py"
|
||||
(p "Mirrors " (code "run_cek_tests.py") ". "
|
||||
"Loads frames.sx, cek.sx, signals.sx, runs test-cek-reactive.sx.")))
|
||||
|
||||
;; =====================================================================
|
||||
;; Step 6: Browser Demo
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Step 6: Browser Demo" :id "step-6"
|
||||
(p "Demo showing:")
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li "Counter island with implicit reactivity (no explicit effects)")
|
||||
(li (code "(deref counter)") " in text position auto-updates")
|
||||
(li (code "(str \"count-\" (deref class-sig))") " in attr position auto-updates")
|
||||
(li "Side-by-side comparison: effect-based vs continuation-based code")))
|
||||
|
||||
;; =====================================================================
|
||||
;; Multi-Deref Handling
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Multi-Deref Handling" :id "multi-deref"
|
||||
|
||||
(~docs/code :code (highlight "(str (deref first-name) \" \" (deref last-name))" "lisp"))
|
||||
|
||||
(ol :class "list-decimal pl-6 mb-6 space-y-3"
|
||||
(li (strong "Initial render:") " First " (code "deref") " hits signal → shifts, captures "
|
||||
(code "(str [HOLE] \" \" (deref last-name))") ". Subscriber registered for "
|
||||
(code "first-name") ". Returns current value. Second " (code "deref")
|
||||
" runs (no ReactiveResetFrame between it and the already-consumed one) — "
|
||||
"falls through to normal scope-based tracking.")
|
||||
(li (strong "first-name changes:") " Subscriber fires → re-pushes ReactiveResetFrame → "
|
||||
"re-invokes continuation with new first-name value → second " (code "deref")
|
||||
" hits ReactiveResetFrame again → shifts, creates NEW subscriber for "
|
||||
(code "last-name") ". Old last-name subscriber cleaned up via sub-scope disposal.")
|
||||
(li (strong "last-name changes:") " Its subscriber fires → re-invokes inner continuation → "
|
||||
"update-fn called with new result."))
|
||||
|
||||
(p "This creates O(n) nested continuations for n derefs. Fine for small reactive expressions."))
|
||||
|
||||
;; =====================================================================
|
||||
;; Commit Strategy
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Commit Strategy" :id "commits"
|
||||
|
||||
(ol :class "list-decimal pl-6 mb-4 space-y-1"
|
||||
(li (strong "Commit 1:") " Bootstrap CEK to JS (Step 1) — mechanical, independent")
|
||||
(li (strong "Commit 2:") " ReactiveResetFrame + DerefFrame (Step 2) — new frame types")
|
||||
(li (strong "Commit 3:") " Deref-as-shift + adapter integration + tests (Steps 3-5) — the core change")
|
||||
(li (strong "Commit 4:") " Browser demo (Step 6)")))
|
||||
|
||||
;; =====================================================================
|
||||
;; Files
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Files" :id "files"
|
||||
|
||||
(div :class "overflow-x-auto mb-6"
|
||||
(table :class "min-w-full text-sm"
|
||||
(thead (tr
|
||||
(th :class "text-left pr-4 pb-2 font-semibold" "File")
|
||||
(th :class "text-left pb-2 font-semibold" "Change")))
|
||||
(tbody
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/platform_js.py")
|
||||
(td "SPEC_MODULES entries, PLATFORM_CEK_JS, CEK_FIXUPS_JS, SPEC_MODULE_ORDER"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/run_js_sx.py")
|
||||
(td "compile_ref_to_js: has_cek, auto-inclusion, ordering, platform code"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/js.sx")
|
||||
(td "RENAMES for CEK predicate functions"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/bootstrap_py.py")
|
||||
(td "RENAMES for CEK predicates"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/frames.sx")
|
||||
(td "ReactiveResetFrame, DerefFrame, has-reactive-reset-frame?, kont-capture-to-reactive-reset"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/cek.sx")
|
||||
(td "step-sf-deref, reactive-shift-deref, deref in dispatch, ReactiveResetFrame in step-continue"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/adapter-dom.sx")
|
||||
(td "*use-cek-reactive* flag, cek-reactive-attr, cek-reactive-text, conditional dispatch"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/test-cek-reactive.sx")
|
||||
(td (strong "New:") " continuation-based reactivity tests"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/run_cek_reactive_tests.py")
|
||||
(td (strong "New:") " Python test runner"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/sx_ref.py")
|
||||
(td "Rebootstrap (generated)"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/static/scripts/sx-browser.js")
|
||||
(td "Rebootstrap (generated)"))))))
|
||||
|
||||
;; =====================================================================
|
||||
;; Risks
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Risks" :id "risks"
|
||||
|
||||
(ol :class "list-decimal pl-6 mb-4 space-y-2"
|
||||
(li (strong "Performance:") " CEK allocates a dict per step. Mitigated: opt-in flag, tree-walk remains default.")
|
||||
(li (strong "Multi-deref stale subscribers:") " Mitigated: sub-scope disposal before re-invocation.")
|
||||
(li (strong "Interaction with user shift/reset:") " " (code "kont-capture-to-reactive-reset")
|
||||
" only scans for " (code "\"reactive-reset\"") ", not " (code "\"reset\"") ". Orthogonal.")
|
||||
(li (strong "JS bootstrapper complexity:") " ~10 RENAMES for predicates. Default mangling handles the rest.")))))
|
||||
209
sx/sx/plans/reactive-runtime.sx
Normal file
209
sx/sx/plans/reactive-runtime.sx
Normal file
@@ -0,0 +1,209 @@
|
||||
;; Reactive Application Runtime — 7 Feature Layers
|
||||
;; Zero new platform primitives. All macros composing existing signals + DOM ops.
|
||||
|
||||
(defcomp ~plans/reactive-runtime/plan-reactive-runtime-content ()
|
||||
(~docs/page :title "Reactive Application Runtime"
|
||||
|
||||
(p :class "text-stone-500 text-sm italic mb-8"
|
||||
"Seven feature layers that take SX from reactive values to a full reactive application runtime. "
|
||||
"Every layer is a macro expanding to existing primitives — zero new CEK frames, "
|
||||
"zero new platform primitives. Proves the existing platform interface is sufficient "
|
||||
"for any application pattern.")
|
||||
|
||||
;; =====================================================================
|
||||
;; Motivation
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Motivation" :id "motivation"
|
||||
|
||||
(p "SX has signals, computed, effects, batch, islands, lakes, stores, and bridge events — "
|
||||
"sufficient for reactive documents (forms, toggles, counters, live search). "
|
||||
"But complex client-heavy apps (drawing tools, editors, games) need structure on top:")
|
||||
|
||||
(table :class "w-full mb-6 text-sm"
|
||||
(thead
|
||||
(tr (th :class "text-left p-2" "Have") (th :class "text-left p-2" "Missing")))
|
||||
(tbody
|
||||
(tr (td :class "p-2" "Signal holds a value") (td :class "p-2" "Ref holds a value " (em "without") " reactivity"))
|
||||
(tr (td :class "p-2" "Effect runs on change") (td :class "p-2" "Loop runs " (em "continuously")))
|
||||
(tr (td :class "p-2" "Store shares state") (td :class "p-2" "Machine manages " (em "modal") " state"))
|
||||
(tr (td :class "p-2" "reset!/swap! update") (td :class "p-2" "Commands update " (em "with history")))
|
||||
(tr (td :class "p-2" "DOM rendering") (td :class "p-2" "Foreign calls " (em "any host API")))
|
||||
(tr (td :class "p-2" "Server-first hydration") (td :class "p-2" "Client-first " (em "app shell"))))))
|
||||
|
||||
;; =====================================================================
|
||||
;; Implementation Order
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Implementation Order" :id "order"
|
||||
|
||||
(~docs/code :code (highlight
|
||||
"L0 Ref -> standalone, trivial (~95 LOC)\nL1 Foreign FFI -> standalone, macro over dom-call-method (~140 LOC)\nL5 Keyed Lists -> enhances existing reactive-list (~155 LOC)\nL2 State Machine -> uses signals (~200 LOC)\nL4 Render Loop -> uses refs (L0), existing RAF (~140 LOC)\nL3 Commands -> extends def-store, uses signals (~320 LOC)\nL6 App Shell -> orchestrates all above (~330 LOC)\n Total: ~1380 LOC"
|
||||
"text"))
|
||||
|
||||
(p "Order rationale: L0 and L1 are independent foundations. L5 enhances existing code. "
|
||||
"L2 and L4 depend on L0. L3 builds on signals. L6 ties everything together."))
|
||||
|
||||
;; =====================================================================
|
||||
;; L0: Ref
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Layer 0: Ref (Mutable Box Without Reactivity)" :id "l0-ref"
|
||||
|
||||
(p "A " (code "ref") " is like a signal but with NO subscriber tracking, NO notifications. "
|
||||
"Just a mutable cell for DOM handles, canvas contexts, timer IDs.")
|
||||
|
||||
(~docs/code :code (highlight
|
||||
"(ref initial-value) ;; create ref, auto-registers dispose in island scope\n(ref-deref r) ;; read (no tracking)\n(ref-set! r v) ;; write (no notification)\n(ref? x) ;; predicate"
|
||||
"lisp"))
|
||||
|
||||
(p "Plain dicts with " (code "__ref") " marker (mirrors " (code "__signal") " pattern). "
|
||||
"On island disposal, auto-nil'd via " (code "register-in-scope") ". ~35 lines of spec.")
|
||||
|
||||
(~docs/subsection :title "Files"
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (strong "NEW") " " (code "shared/sx/ref/refs.sx") " — ref, ref-deref, ref-set!, ref?")
|
||||
(li (strong "NEW") " " (code "shared/sx/ref/test-refs.sx") " — tests: create, read, set, predicate, scope disposal")
|
||||
(li "Add " (code "\"refs\"") " to " (code "SPEC_MODULES") " in both platform files"))))
|
||||
|
||||
;; =====================================================================
|
||||
;; L1: Foreign
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Layer 1: Foreign (Host API Interop)" :id "l1-foreign"
|
||||
|
||||
(p "Clean boundary for calling host APIs (Canvas, WebGL, WebAudio) from SX code. "
|
||||
"Uses " (strong "existing") " platform primitives — no new ones needed:")
|
||||
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (code "dom-call-method(obj, \"methodName\", args...)") " — method calls")
|
||||
(li (code "dom-get-prop(obj, \"propName\")") " — property getter")
|
||||
(li (code "dom-set-prop(obj, \"propName\", value)") " — property setter"))
|
||||
|
||||
(p (code "def-foreign") " is a macro that generates calls to these existing primitives.")
|
||||
|
||||
(~docs/code :code (highlight
|
||||
"(def-foreign canvas-2d\n (fill-rect x y w h) ;; method call\n (:fill-style color)) ;; property setter (keyword = property)\n\n;; Get host object via existing primitive\n(let ((ctx (dom-call-method canvas \"getContext\" \"2d\")))\n (canvas-2d.fill-rect ctx 0 0 100 100)\n (canvas-2d.fill-style! ctx \"red\"))"
|
||||
"lisp"))
|
||||
|
||||
(~docs/subsection :title "Macro Expansion"
|
||||
(~docs/code :code (highlight
|
||||
"(def-foreign canvas-2d\n (fill-rect x y w h)\n (:fill-style color))\n;; expands to:\n(do\n (define canvas-2d.fill-rect\n (fn (ctx x y w h) (dom-call-method ctx \"fillRect\" x y w h)))\n (define canvas-2d.fill-style!\n (fn (ctx color) (dom-set-prop ctx \"fillStyle\" color))))"
|
||||
"lisp"))
|
||||
|
||||
(p "Dot notation works because " (code "ident-char?") " includes " (code ".") ". "
|
||||
"The macro converts SX naming (" (code "fill-rect") ") to host naming (" (code "fillRect") ") via camelCase transform.")))
|
||||
|
||||
;; =====================================================================
|
||||
;; L5: Keyed Lists
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Layer 5: Keyed List Reconciliation" :id "l5-keyed"
|
||||
|
||||
(p "Enhance existing " (code "reactive-list") " (adapter-dom.sx:1000) with explicit " (code ":key") " parameter. "
|
||||
"Current code already has keyed reconciliation via DOM " (code "key") " attributes — this adds "
|
||||
"an explicit key extraction callback and stable identity tracking.")
|
||||
|
||||
(~docs/code :code (highlight
|
||||
"(map (fn (el) (~shape-handle el)) (deref items) :key (fn (el) (get el :id)))"
|
||||
"lisp"))
|
||||
|
||||
(p "Changes to " (code "render-dom-list") " detection (line 563-575) and " (code "reactive-list") " implementation. "
|
||||
"No new platform primitives — existing DOM ops suffice."))
|
||||
|
||||
;; =====================================================================
|
||||
;; L2: State Machines
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Layer 2: State Machines (defmachine)" :id "l2-machine"
|
||||
|
||||
(p "Modal state management for complex UI modes. Machine state IS a signal — "
|
||||
"composes naturally with computed/effects.")
|
||||
|
||||
(~docs/code :code (highlight
|
||||
"(defmachine drawing-tool\n :initial :idle\n :states {\n :idle {:on {:pointer-down (fn (ev) {:state :drawing\n :actions [(start-shape! ev)]})}}\n :drawing {:on {:pointer-move (fn (ev) {:state :drawing\n :actions [(update-shape! ev)]})\n :pointer-up (fn (ev) {:state :idle\n :actions [(finish-shape! ev)]})}}})\n\n(machine-send! drawing-tool :pointer-down event)\n(machine-matches? drawing-tool :drawing) ;; reactive via deref\n(machine-state drawing-tool) ;; returns the state signal"
|
||||
"lisp"))
|
||||
|
||||
(p (code "defmachine") " is a macro expanding to a " (code "let") " with a signal for current state, "
|
||||
"a transitions dict, and a " (code "send!") " function. Built on signals + dicts."))
|
||||
|
||||
;; =====================================================================
|
||||
;; L4: Render Loop
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Layer 4: Continuous Rendering Loop (defloop)" :id "l4-loop"
|
||||
|
||||
(p (code "requestAnimationFrame") " integration for canvas/animation apps, "
|
||||
"with island lifecycle cleanup.")
|
||||
|
||||
(~docs/code :code (highlight
|
||||
"(defloop render-loop (fn (timestamp dt)\n (let ((ctx (ref-deref ctx-ref)))\n (clear-canvas! ctx)\n (draw-scene! ctx (deref elements)))))\n\n(loop-start! render-loop)\n(loop-stop! render-loop)\n(loop-running? render-loop) ;; reactive signal"
|
||||
"lisp"))
|
||||
|
||||
(p "Uses the " (strong "running-ref pattern") " to avoid needing " (code "cancelAnimationFrame") ":")
|
||||
|
||||
(~docs/code :code (highlight
|
||||
"(let ((running (ref true))\n (last-ts (ref 0)))\n (define tick (fn (ts)\n (when (ref-deref running)\n (let ((dt (- ts (ref-deref last-ts))))\n (ref-set! last-ts ts)\n (user-fn ts dt))\n (request-animation-frame tick))))\n (request-animation-frame tick)\n ;; stop: (ref-set! running false) -- loop dies on next frame"
|
||||
"lisp"))
|
||||
|
||||
(p "Uses existing " (code "request-animation-frame") " (already wired in JS). "
|
||||
"Auto-stops on island disposal via " (code "register-in-scope") ". "
|
||||
"Depends on L0 (refs)."))
|
||||
|
||||
;; =====================================================================
|
||||
;; L3: Commands
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Layer 3: Commands with History (Undo/Redo)" :id "l3-commands"
|
||||
|
||||
(p "Command pattern built into signal stores. Commands are s-expressions "
|
||||
"in a history stack — trivially serializable.")
|
||||
|
||||
(~docs/code :code (highlight
|
||||
"(def-command-store canvas-state\n :initial {:elements '() :selection nil}\n :commands {\n :add-element (fn (state el)\n (assoc state :elements\n (append (get state :elements) (list el))))\n :move-element (fn (state id dx dy) ...)\n }\n :max-history 100)\n\n(dispatch! canvas-state :add-element rect-1)\n(undo! canvas-state)\n(redo! canvas-state)\n(can-undo? canvas-state) ;; reactive\n(can-redo? canvas-state) ;; reactive\n(group-start! canvas-state \"drag\") ;; transaction grouping\n(group-end! canvas-state)"
|
||||
"lisp"))
|
||||
|
||||
(p "Macro wraps signal with undo-stack and redo-stack (both signals of lists). "
|
||||
(code "group-start!") "/" (code "group-end!") " collapses multiple dispatches "
|
||||
"into one undo entry — essential for drag operations."))
|
||||
|
||||
;; =====================================================================
|
||||
;; L6: App Shell
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Layer 6: Client-First App Shell (defapp)" :id "l6-app"
|
||||
|
||||
(p "Skip SSR entirely for canvas-heavy apps. Server returns minimal HTML shell, "
|
||||
"all rendering client-side.")
|
||||
|
||||
(~docs/code :code (highlight
|
||||
"(defapp excalidraw\n :render :client\n :entry ~drawing-app\n :stores (canvas-state tool-state)\n :routes {\"/\" ~drawing-app\n \"/gallery\" ~gallery-view}\n :head [(link :rel \"stylesheet\" :href \"/static/app.css\")])"
|
||||
"lisp"))
|
||||
|
||||
(p "Macro generates:")
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (strong "Server:") " minimal HTML shell (doctype + " (code "<div id=\"sx-app-root\">") " + SX loader)")
|
||||
(li (strong "Client:") " " (code "sx-app-boot") " function using existing " (code "sx-mount") " for initial render")))
|
||||
|
||||
;; =====================================================================
|
||||
;; Zero Platform Primitives
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Zero New Platform Primitives" :id "zero-primitives"
|
||||
|
||||
(p "All 7 layers are pure " (code ".sx") " macros composing existing primitives:")
|
||||
|
||||
(table :class "w-full mb-6 text-sm"
|
||||
(thead
|
||||
(tr (th :class "text-left p-2" "Existing Primitive") (th :class "text-left p-2" "Used By")))
|
||||
(tbody
|
||||
(tr (td :class "p-2" (code "signal, deref, reset!, swap!, computed, effect")) (td :class "p-2" "L2, L3, L4"))
|
||||
(tr (td :class "p-2" (code "dom-call-method, dom-get-prop, dom-set-prop")) (td :class "p-2" "L1 (Foreign FFI)"))
|
||||
(tr (td :class "p-2" (code "request-animation-frame")) (td :class "p-2" "L4 (Render Loop)"))
|
||||
(tr (td :class "p-2" (code "register-in-scope, scope-push!, scope-pop!")) (td :class "p-2" "L0, L4"))
|
||||
(tr (td :class "p-2" (code "sx-mount, sx-hydrate-islands")) (td :class "p-2" "L6 (App Shell)"))
|
||||
(tr (td :class "p-2" (code "DOM ops (dom-insert-after, dom-remove, ...)")) (td :class "p-2" "L5 (Keyed Lists)"))))
|
||||
|
||||
(p :class "text-stone-600 italic"
|
||||
"This validates SX's architecture: the existing platform interface is complete "
|
||||
"enough for any application pattern."))))
|
||||
@@ -578,6 +578,9 @@
|
||||
"sx-protocol" (~plans/sx-protocol/plan-sx-protocol-content)
|
||||
"scoped-effects" (~plans/scoped-effects/plan-scoped-effects-content)
|
||||
"foundations" (~plans/foundations/plan-foundations-content)
|
||||
"cek-reactive" (~plans/cek-reactive/plan-cek-reactive-content)
|
||||
"reactive-runtime" (~plans/reactive-runtime/plan-reactive-runtime-content)
|
||||
"rust-wasm-host" (~plans/rust-wasm-host/plan-rust-wasm-host-content)
|
||||
:else (~plans/index/plans-index-content))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -649,6 +652,25 @@
|
||||
:layout :sx-docs
|
||||
:content (~layouts/doc :path "/sx/(geography.(marshes))" (~reactive-islands/marshes/reactive-islands-marshes-content)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; CEK Machine section (under Geography)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defpage cek-index
|
||||
:path "/geography/cek/"
|
||||
:auth :public
|
||||
:layout :sx-docs
|
||||
:content (~layouts/doc :path "/sx/(geography.(cek))" (~geography/cek/cek-content)))
|
||||
|
||||
(defpage cek-page
|
||||
:path "/geography/cek/<slug>"
|
||||
:auth :public
|
||||
:layout :sx-docs
|
||||
:content (~layouts/doc :path (str "/sx/(geography.(cek." slug "))")
|
||||
(case slug
|
||||
"demo" (~geography/cek/cek-demo-content)
|
||||
:else (~geography/cek/cek-content))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Bootstrapped page helpers demo
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
@@ -267,6 +267,8 @@ _REDIRECT_PATTERNS = [
|
||||
lambda m: f"/sx/(geography.(reactive.{m.group(1)}))"),
|
||||
(re.compile(r"^/geography/isomorphism/(.+?)/?$"),
|
||||
lambda m: f"/sx/(geography.(isomorphism.{m.group(1)}))"),
|
||||
(re.compile(r"^/geography/cek/(.+?)/?$"),
|
||||
lambda m: f"/sx/(geography.(cek.{m.group(1)}))"),
|
||||
(re.compile(r"^/geography/spreads/?$"),
|
||||
"/sx/(geography.(spreads))"),
|
||||
(re.compile(r"^/geography/marshes/?$"),
|
||||
@@ -290,6 +292,7 @@ _REDIRECT_PATTERNS = [
|
||||
(re.compile(r"^/geography/hypermedia/?$"), "/sx/(geography.(hypermedia))"),
|
||||
(re.compile(r"^/geography/reactive/?$"), "/sx/(geography.(reactive))"),
|
||||
(re.compile(r"^/geography/isomorphism/?$"), "/sx/(geography.(isomorphism))"),
|
||||
(re.compile(r"^/geography/cek/?$"), "/sx/(geography.(cek))"),
|
||||
(re.compile(r"^/geography/?$"), "/sx/(geography)"),
|
||||
(re.compile(r"^/applications/cssx/?$"), "/sx/(applications.(cssx))"),
|
||||
(re.compile(r"^/applications/protocols/?$"), "/sx/(applications.(protocol))"),
|
||||
|
||||
Reference in New Issue
Block a user