15 Commits

Author SHA1 Message Date
2211655060 CEK-native higher-order forms: map, filter, reduce, some, every?, for-each
Some checks are pending
Build and Deploy / build-and-deploy (push) Has started running
Higher-order forms now step element-by-element through the CEK machine
using dedicated frames instead of delegating to tree-walk ho-map etc.
Each callback invocation goes through continue-with-call, so deref-as-shift
works inside map/filter/reduce callbacks in reactive island contexts.

- cek.sx: rewrite step-ho-* to use CEK frames, add frame handlers in
  step-continue for map, filter, reduce, for-each, some, every
- frames.sx: add SomeFrame, EveryFrame, MapIndexedFrame
- test-cek-reactive.sx: add 10 tests for CEK-native HO forms

89 tests pass (20 signal + 43 CEK + 26 CEK reactive).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 10:45:36 +00:00
d0a5ce1070 Remove invoke from platform interfaces, add cek-call integration tests
- platform_js.py: remove invoke function definition and PRIMITIVES
  registration, switch domListen handler wrapping to cek-call
- platform_py.py: remove invoke function definition
- run_signal_tests.py: remove invoke patch, use cek_call in batch wrapper
- run_cek_reactive_tests.py: remove invoke, fix primitive lookup to use
  two-level is_primitive/get_primitive, increase recursion limit for
  interpreted CEK evaluation
- test-cek-reactive.sx: add 7 new tests covering cek-call dispatch with
  computed, effect, cleanup, batch coalescing

All 79 tests pass (20 signal + 43 CEK + 16 CEK reactive).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 10:29:32 +00:00
6581211a10 Replace invoke with cek-call in adapters and engine
Completes the invoke→cek-call migration across all spec .sx files:
- adapter-sx.sx: map/filter/for-each in aser wire format
- adapter-dom.sx: island render update-fn
- engine.sx: fetch transform callback
- test-cek-reactive.sx: disposal test

Only async-invoke (adapter-async.sx) remains — separate async pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 10:16:47 +00:00
455e48df07 Replace invoke with cek-call in reactive island primitives
All signal operations (computed, effect, batch, etc.) now dispatch
function calls through cek-call, which routes SX lambdas via cek-run
and native callables via apply. This replaces the invoke shim.

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 10:11:48 +00:00
30d9d4aa4c Add missing plan routes for cek-reactive and reactive-runtime
Both plans had nav entries and component files but were missing from
the page-functions.sx case statement, causing 404s on their URLs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 02:02:54 +00:00
b06cc2daca Fix bootstrapper cell variable scoping for nested closures
Two bugs in _emit_define_as_def: (1) nested def's _current_cell_vars
was replaced instead of unioned with parent — inner functions lost
access to parent's cell vars (skip_ws/skip_comment used bare pos
instead of _cells['pos']). (2) statement-context set! didn't check
_current_cell_vars, always emitting bare assignment instead of
_cells[...]. (3) nested functions that access parent _cells no longer
shadow it with their own empty _cells = {}.

Fixes UnboundLocalError in bootstrapped parser (sx_parse skip_ws)
that crashed production URL routing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 01:46:15 +00:00
4b746e4c8b Bootstrap parser.sx to Python, add reactive runtime plan
Replace hand-written serialize/sx_serialize/sx_parse in Python with
spec-derived versions from parser.sx. Add parser as a Python adapter
alongside html/sx/async — all 48 parser spec tests pass.

Add reactive runtime plan to sx-docs: 7 feature layers (ref, foreign
FFI, state machines, commands with undo/redo, render loops, keyed
lists, client-first app shell) — zero new platform primitives.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 01:45:17 +00:00
f96506024e Add CEK Machine section under Geography with live island demos
geography/cek.sx: overview page (three registers, deref-as-shift
explanation) + demo page with 5 live islands (counter, computed chain,
reactive attrs, stopwatch effect+cleanup, batch coalescing). Nav entry,
router routes, defpage definitions. CEK exports (cekRun, makeCekState,
makeReactiveResetFrame, evalExpr) added to Sx public API via
platform_js.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 01:37:16 +00:00
203f9a49a1 Fix remaining test runners for CEK-default mode: override to tree-walk
run_type_tests.py, run_signal_tests.py, run_continuation_tests.py all
needed the same sx_ref.eval_expr/trampoline override to tree-walk that
was applied to the CEK test runners. Without this, transpiled HO forms
(ho_map, etc.) re-entered CEK mid-evaluation causing "Unknown frame
type: map" errors. All 186 tests now pass across 5 suites.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 01:17:47 +00:00
893c767238 Add CEK reactive tests (9/9), fix test runners for CEK-default mode
test-cek-reactive.sx: 9 tests across 4 suites — deref pass-through,
signal without reactive-reset, reactive-reset shift with continuation
capture, scope disposal cleanup. run_cek_reactive_tests.py: new runner
loading signals+frames+cek. Both test runners override sx_ref.eval_expr
back to tree-walk so interpreted .sx uses tree-walk internally.
Plan page added to sx-docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 01:13:31 +00:00
5c4a8c8cc2 Implement deref-as-shift: ReactiveResetFrame, DerefFrame, continuation capture
frames.sx: ReactiveResetFrame + DerefFrame constructors,
kont-capture-to-reactive-reset, has-reactive-reset-frame?.
cek.sx: deref as CEK special form, step-sf-deref pushes DerefFrame,
reactive-shift-deref captures continuation as signal subscriber,
ReactiveResetFrame in step-continue calls update-fn on re-render.
adapter-dom.sx: cek-reactive-text/cek-reactive-attr using cek-run
with ReactiveResetFrame for implicit DOM bindings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 01:13:21 +00:00
90febbd91e Bootstrap CEK as default evaluator on both JS and Python sides
SPEC_MODULES + SPEC_MODULE_ORDER for frames/cek in platform_js.py,
PLATFORM_CEK_JS + CEK_FIXUPS_JS constants, auto-inclusion in
run_js_sx.py, 70+ RENAMES in js.sx. Python: CEK always-include in
bootstrap_py.py, eval_expr/trampoline overridden to cek_run in
platform_py.py with _tree_walk_* preserved for test runners.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 01:13:11 +00:00
f3a9f3ccc0 Collapse signal platform primitives into pure SX dicts
Replace _Signal class (Python) and SxSignal constructor (JS) with plain
dicts keyed by "__signal". Nine platform accessor functions become ~20
lines of pure SX in signals.sx. type-of returns "dict" for signals;
signal? is now a structural predicate (dict? + has-key?).

Net: -168 lines platform, +120 lines SX. Zero platform primitives for
reactivity — signals compile to any host via the bootstrappers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:04:38 +00:00
dcc73a68d5 Collapse reactive islands into scopes: replace TrackingContext and *island-scope* with scope-push!/scope-pop!/context
Reactive tracking (deref/computed/effect dep discovery) and island lifecycle
now use the general scoped effects system instead of parallel infrastructure.
Two scope names: "sx-reactive" for tracking context, "sx-island-scope" for
island disposable collection. Eliminates ~98 net lines: _TrackingContext class,
7 tracking context platform functions (Python + JS), *island-scope* global,
and corresponding RENAME_MAP entries. All 20 signal tests pass (17 original +
3 new scope integration tests), plus CEK/continuation/type tests clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 23:09:09 +00:00
1765216335 Implement explicit CEK machine, continuations, effect signatures, fix dynamic-wind and inspect shadowing
Three-phase foundations implementation:

Phase A — Activate dormant shift/reset continuations with 24 SX-native tests
covering basic semantics, predicates, stored continuations, nested reset,
scope interaction, and TCO.

Phase B — Bridge compile-time effect system to runtime: boundary_parser extracts
46 effect annotations, platform provides populate_effect_annotations() and
check_component_effects() for static analysis. 6 new type tests.

Phase C — Explicit CEK machine (frames.sx + cek.sx): evaluation state as data
({control, env, kont, phase, value}), 21 frame types, two-phase step function
(step-eval/step-continue), native shift/reset via frame capture. Bootstrapper
integration: --spec-modules cek transpiles to Python with iterative cek_run.
43 interpreted + 49 transpiled tests passing.

Bug fixes:
- inspect() shadowed by `import inspect` in PLATFORM_ASYNC_PY — renamed to
  `import inspect as _inspect`
- dynamic-wind missing platform functions (call_thunk, push_wind!, pop_wind!) —
  added with try/finally error safety via dynamic_wind_call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 22:14:55 +00:00
37 changed files with 6763 additions and 694 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -39,10 +39,10 @@ def _load_declarations() -> None:
len(_DECLARED_PURE), len(_DECLARED_IO), len(_DECLARED_HELPERS), len(_DECLARED_PURE), len(_DECLARED_IO), len(_DECLARED_HELPERS),
) )
except Exception as e: except Exception as e:
logger.warning("Failed to load boundary declarations: %s", e) # Don't cache failure — parser may not be ready yet (circular import
_DECLARED_PURE = frozenset() # during startup). Will retry on next call. Validation functions
_DECLARED_IO = frozenset() # skip checks when declarations aren't loaded.
_DECLARED_HELPERS = {} logger.debug("Boundary declarations not ready yet: %s", e)
def _is_strict() -> bool: def _is_strict() -> bool:
@@ -63,7 +63,8 @@ def _report(message: str) -> None:
def validate_primitive(name: str) -> None: def validate_primitive(name: str) -> None:
"""Validate that a pure primitive is declared in primitives.sx.""" """Validate that a pure primitive is declared in primitives.sx."""
_load_declarations() _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: if name not in _DECLARED_PURE:
_report(f"Undeclared pure primitive: {name!r}. Add to primitives.sx.") _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: def validate_io(name: str) -> None:
"""Validate that an I/O primitive is declared in boundary.sx or boundary-app.sx.""" """Validate that an I/O primitive is declared in boundary.sx or boundary-app.sx."""
_load_declarations() _load_declarations()
assert _DECLARED_IO is not None if _DECLARED_IO is None:
return # Not ready yet, skip
if name not in _DECLARED_IO: if name not in _DECLARED_IO:
_report( _report(
f"Undeclared I/O primitive: {name!r}. " 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: def validate_helper(service: str, name: str) -> None:
"""Validate that a page helper is declared in {service}/sx/boundary.sx.""" """Validate that a page helper is declared in {service}/sx/boundary.sx."""
_load_declarations() _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()) svc_helpers = _DECLARED_HELPERS.get(service, frozenset())
if name not in svc_helpers: if name not in svc_helpers:
_report( _report(
@@ -129,17 +132,14 @@ def validate_boundary_value(value: Any, context: str = "") -> None:
def declared_pure() -> frozenset[str]: def declared_pure() -> frozenset[str]:
_load_declarations() _load_declarations()
assert _DECLARED_PURE is not None return _DECLARED_PURE or frozenset()
return _DECLARED_PURE
def declared_io() -> frozenset[str]: def declared_io() -> frozenset[str]:
_load_declarations() _load_declarations()
assert _DECLARED_IO is not None return _DECLARED_IO or frozenset()
return _DECLARED_IO
def declared_helpers() -> dict[str, frozenset[str]]: def declared_helpers() -> dict[str, frozenset[str]]:
_load_declarations() _load_declarations()
assert _DECLARED_HELPERS is not None return dict(_DECLARED_HELPERS) if _DECLARED_HELPERS else {}
return dict(_DECLARED_HELPERS)

View File

@@ -59,7 +59,7 @@
;; Signal → reactive text in island scope, deref outside ;; Signal → reactive text in island scope, deref outside
:else :else
(if (signal? expr) (if (signal? expr)
(if *island-scope* (if (context "sx-island-scope" nil)
(reactive-text expr) (reactive-text expr)
(create-text-node (str (deref expr)))) (create-text-node (str (deref expr))))
(create-text-node (str expr)))))) (create-text-node (str expr))))))
@@ -143,7 +143,7 @@
(render-dom-element name args env ns) (render-dom-element name args env ns)
;; deref in island scope → reactive text node ;; 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)))) (let ((sig-or-val (trampoline (eval-expr (first args) env))))
(if (signal? sig-or-val) (if (signal? sig-or-val)
(reactive-text sig-or-val) (reactive-text sig-or-val)
@@ -215,7 +215,7 @@
;; Inside island scope: reactive attribute binding. ;; Inside island scope: reactive attribute binding.
;; The effect tracks signal deps automatically — if none ;; The effect tracks signal deps automatically — if none
;; are deref'd, it fires once and never again (safe). ;; are deref'd, it fires once and never again (safe).
*island-scope* (context "sx-island-scope" nil)
(reactive-attr el attr-name (reactive-attr el attr-name
(fn () (trampoline (eval-expr attr-expr env)))) (fn () (trampoline (eval-expr attr-expr env))))
;; Static attribute (outside islands) ;; Static attribute (outside islands)
@@ -237,7 +237,7 @@
(let ((child (render-to-dom arg env new-ns))) (let ((child (render-to-dom arg env new-ns)))
(cond (cond
;; Reactive spread: track signal deps, update attrs on change ;; 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))) (reactive-spread el (fn () (render-to-dom arg env new-ns)))
;; Static spread: already emitted via provide, skip ;; Static spread: already emitted via provide, skip
(spread? child) nil (spread? child) nil
@@ -392,7 +392,7 @@
(cond (cond
;; if — reactive inside islands (re-renders when signal deps change) ;; if — reactive inside islands (re-renders when signal deps change)
(= name "if") (= name "if")
(if *island-scope* (if (context "sx-island-scope" nil)
(let ((marker (create-comment "r-if")) (let ((marker (create-comment "r-if"))
(current-nodes (list)) (current-nodes (list))
(initial-result nil)) (initial-result nil))
@@ -440,7 +440,7 @@
;; when — reactive inside islands ;; when — reactive inside islands
(= name "when") (= name "when")
(if *island-scope* (if (context "sx-island-scope" nil)
(let ((marker (create-comment "r-when")) (let ((marker (create-comment "r-when"))
(current-nodes (list)) (current-nodes (list))
(initial-result nil)) (initial-result nil))
@@ -486,7 +486,7 @@
;; cond — reactive inside islands ;; cond — reactive inside islands
(= name "cond") (= name "cond")
(if *island-scope* (if (context "sx-island-scope" nil)
(let ((marker (create-comment "r-cond")) (let ((marker (create-comment "r-cond"))
(current-nodes (list)) (current-nodes (list))
(initial-result nil)) (initial-result nil))
@@ -563,7 +563,7 @@
;; map — reactive-list when mapping over a signal inside an island ;; map — reactive-list when mapping over a signal inside an island
(= name "map") (= name "map")
(let ((coll-expr (nth expr 2))) (let ((coll-expr (nth expr 2)))
(if (and *island-scope* (if (and (context "sx-island-scope" nil)
(= (type-of coll-expr) "list") (= (type-of coll-expr) "list")
(> (len coll-expr) 1) (> (len coll-expr) 1)
(= (type-of (first coll-expr)) "symbol") (= (type-of (first coll-expr)) "symbol")
@@ -1107,6 +1107,48 @@
(reset! sig (dom-get-prop el "value")))))))) (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 ;; render-dom-portal — render children into a remote target element
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -1168,7 +1210,7 @@
(dom-set-attr container "data-sx-boundary" "true") (dom-set-attr container "data-sx-boundary" "true")
;; The entire body is rendered inside ONE effect + try-catch. ;; 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 ;; paths — their signal reads become direct deref calls tracked by THIS
;; effect. Errors from signal changes throw synchronously within try-catch. ;; effect. Errors from signal changes throw synchronously within try-catch.
;; The error boundary's own effect handles all reactivity for its subtree. ;; The error boundary's own effect handles all reactivity for its subtree.
@@ -1179,10 +1221,9 @@
;; Clear container ;; Clear container
(dom-set-prop container "innerHTML" "") (dom-set-prop container "innerHTML" "")
;; Save and clear island scope BEFORE try-catch so it can be ;; Push nil island scope to suppress reactive rendering in body.
;; restored in both success and error paths. ;; Pop in both success and error paths.
(let ((saved-scope *island-scope*)) (scope-push! "sx-island-scope" nil)
(set! *island-scope* nil)
(try-catch (try-catch
(fn () (fn ()
;; Body renders statically — signal reads tracked by THIS effect, ;; Body renders statically — signal reads tracked by THIS effect,
@@ -1193,17 +1234,17 @@
(dom-append frag (render-to-dom child env ns))) (dom-append frag (render-to-dom child env ns)))
body-exprs) body-exprs)
(dom-append container frag)) (dom-append container frag))
(set! *island-scope* saved-scope)) (scope-pop! "sx-island-scope"))
(fn (err) (fn (err)
;; Restore scope first, then render fallback ;; Pop scope first, then render fallback
(set! *island-scope* saved-scope) (scope-pop! "sx-island-scope")
(let ((fallback-fn (trampoline (eval-expr fallback-expr env))) (let ((fallback-fn (trampoline (eval-expr fallback-expr env)))
(retry-fn (fn () (swap! retry-version (fn (n) (+ n 1)))))) (retry-fn (fn () (swap! retry-version (fn (n) (+ n 1))))))
(let ((fallback-dom (let ((fallback-dom
(if (lambda? fallback-fn) (if (lambda? fallback-fn)
(render-lambda-dom fallback-fn (list err retry-fn) env ns) (render-lambda-dom fallback-fn (list err retry-fn) env ns)
(render-to-dom (apply 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))) container)))

View File

@@ -291,7 +291,7 @@
(let ((local (env-merge (lambda-closure f) env))) (let ((local (env-merge (lambda-closure f) env)))
(env-set! local (first (lambda-params f)) item) (env-set! local (first (lambda-params f)) item)
(aser (lambda-body f) local)) (aser (lambda-body f) local))
(invoke f item))) (cek-call f (list item))))
coll)) coll))
;; map-indexed ;; map-indexed
@@ -304,7 +304,7 @@
(env-set! local (first (lambda-params f)) i) (env-set! local (first (lambda-params f)) i)
(env-set! local (nth (lambda-params f) 1) item) (env-set! local (nth (lambda-params f) 1) item)
(aser (lambda-body f) local)) (aser (lambda-body f) local))
(invoke f i item))) (cek-call f (list i item))))
coll)) coll))
;; for-each — evaluate for side effects, aser each body ;; for-each — evaluate for side effects, aser each body
@@ -317,7 +317,7 @@
(let ((local (env-merge (lambda-closure f) env))) (let ((local (env-merge (lambda-closure f) env)))
(env-set! local (first (lambda-params f)) item) (env-set! local (first (lambda-params f)) item)
(append! results (aser (lambda-body f) local))) (append! results (aser (lambda-body f) local)))
(invoke f item))) (cek-call f (list item))))
coll) coll)
(if (empty? results) nil results)) (if (empty? results) nil results))

View File

@@ -85,7 +85,12 @@ class PyEmitter:
if name == "define-async": if name == "define-async":
return self._emit_define_async(expr, indent) return self._emit_define_async(expr, indent)
if name == "set!": 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": if name == "when":
return self._emit_when_stmt(expr, indent) return self._emit_when_stmt(expr, indent)
if name == "do" or name == "begin": if name == "do" or name == "begin":
@@ -165,12 +170,6 @@ class PyEmitter:
"signal-remove-sub!": "signal_remove_sub", "signal-remove-sub!": "signal_remove_sub",
"signal-deps": "signal_deps", "signal-deps": "signal_deps",
"signal-set-deps!": "signal_set_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", "identical?": "is_identical",
"notify-subscribers": "notify_subscribers", "notify-subscribers": "notify_subscribers",
"flush-subscribers": "flush_subscribers", "flush-subscribers": "flush_subscribers",
@@ -179,7 +178,6 @@ class PyEmitter:
"register-in-scope": "register_in_scope", "register-in-scope": "register_in_scope",
"*batch-depth*": "_batch_depth", "*batch-depth*": "_batch_depth",
"*batch-queue*": "_batch_queue", "*batch-queue*": "_batch_queue",
"*island-scope*": "_island_scope",
"*store-registry*": "_store_registry", "*store-registry*": "_store_registry",
"def-store": "def_store", "def-store": "def_store",
"use-store": "use_store", "use-store": "use_store",
@@ -754,14 +752,23 @@ class PyEmitter:
nested_set_vars = self._find_nested_set_vars(body) nested_set_vars = self._find_nested_set_vars(body)
def_kw = "async def" if is_async else "def" def_kw = "async def" if is_async else "def"
lines = [f"{pad}{def_kw} {py_name}({params_str}):"] 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) # Emit body with cell var tracking (and async context if needed)
old_cells = getattr(self, '_current_cell_vars', set()) 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 old_async = self._in_async
self._current_cell_vars = nested_set_vars self._current_cell_vars = old_cells | nested_set_vars
if is_async: if is_async:
self._in_async = True self._in_async = True
# 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._emit_body_stmts(body, lines, indent + 1)
self._current_cell_vars = old_cells self._current_cell_vars = old_cells
self._in_async = old_async self._in_async = old_async
@@ -801,14 +808,20 @@ class PyEmitter:
Handles let as local variable declarations, and returns the last Handles let as local variable declarations, and returns the last
expression. Control flow in tail position (if, cond, case, when) expression. Control flow in tail position (if, cond, case, when)
is flattened to if/elif statements with returns in each branch. 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 pad = " " * indent
for i, expr in enumerate(body): idx = 0
is_last = (i == len(body) - 1) while idx < len(body):
expr = body[idx]
is_last = (idx == len(body) - 1)
if isinstance(expr, list) and expr and isinstance(expr[0], Symbol): if isinstance(expr, list) and expr and isinstance(expr[0], Symbol):
name = expr[0].name name = expr[0].name
if name in ("let", "let*"): if name in ("let", "let*"):
self._emit_let_as_stmts(expr, lines, indent, is_last) self._emit_let_as_stmts(expr, lines, indent, is_last)
idx += 1
continue continue
if name in ("do", "begin"): if name in ("do", "begin"):
sub_body = expr[1:] sub_body = expr[1:]
@@ -817,15 +830,172 @@ class PyEmitter:
else: else:
for sub in sub_body: for sub in sub_body:
lines.append(self.emit_statement(sub, indent)) lines.append(self.emit_statement(sub, indent))
idx += 1
continue 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: if is_last:
self._emit_return_expr(expr, lines, indent) self._emit_return_expr(expr, lines, indent)
else: else:
self._emit_stmt_recursive(expr, lines, indent) 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: def _emit_return_expr(self, expr, lines: list, indent: int) -> None:
"""Emit an expression in return position, flattening control flow.""" """Emit an expression in return position, flattening control flow."""
pad = " " * indent 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): if isinstance(expr, list) and expr and isinstance(expr[0], Symbol):
name = expr[0].name name = expr[0].name
if name == "if": if name == "if":
@@ -847,10 +1017,16 @@ class PyEmitter:
self._emit_body_stmts(expr[1:], lines, indent) self._emit_body_stmts(expr[1:], lines, indent)
return return
if name == "for-each": 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(self._emit_for_each_stmt(expr, indent))
lines.append(f"{pad}return NIL") self._emit_nil_return(lines, indent)
return return
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)}") lines.append(f"{pad}return {self.emit(expr)}")
def _emit_if_return(self, expr, lines: list, indent: int) -> None: def _emit_if_return(self, expr, lines: list, indent: int) -> None:
@@ -862,7 +1038,7 @@ class PyEmitter:
lines.append(f"{pad}else:") lines.append(f"{pad}else:")
self._emit_return_expr(expr[3], lines, indent + 1) self._emit_return_expr(expr[3], lines, indent + 1)
else: else:
lines.append(f"{pad}return NIL") self._emit_nil_return(lines, indent)
def _emit_when_return(self, expr, lines: list, indent: int) -> None: def _emit_when_return(self, expr, lines: list, indent: int) -> None:
"""Emit when as statement with return in body, else return NIL.""" """Emit when as statement with return in body, else return NIL."""
@@ -875,7 +1051,7 @@ class PyEmitter:
for b in body_parts[:-1]: for b in body_parts[:-1]:
lines.append(self.emit_statement(b, indent + 1)) lines.append(self.emit_statement(b, indent + 1))
self._emit_return_expr(body_parts[-1], lines, 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: def _emit_cond_return(self, expr, lines: list, indent: int) -> None:
"""Emit cond as if/elif/else with returns in each branch.""" """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) self._emit_return_expr(body, lines, indent + 1)
i += 2 i += 2
if not has_else: 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: def _emit_case_return(self, expr, lines: list, indent: int) -> None:
"""Emit case as if/elif/else with returns in each branch.""" """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) self._emit_return_expr(body, lines, indent + 1)
i += 2 i += 2
if not has_else: 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: def _emit_let_as_stmts(self, expr, lines: list, indent: int, is_last: bool) -> None:
"""Emit a let expression as local variable declarations.""" """Emit a let expression as local variable declarations."""
@@ -1129,17 +1305,23 @@ try:
from .platform_py import ( from .platform_py import (
PREAMBLE, PLATFORM_PY, PRIMITIVES_PY_PRE, PRIMITIVES_PY_POST, PREAMBLE, PLATFORM_PY, PRIMITIVES_PY_PRE, PRIMITIVES_PY_POST,
PRIMITIVES_PY_MODULES, _ALL_PY_MODULES, 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, _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: except ImportError:
from shared.sx.ref.platform_py import ( from shared.sx.ref.platform_py import (
PREAMBLE, PLATFORM_PY, PRIMITIVES_PY_PRE, PRIMITIVES_PY_POST, PREAMBLE, PLATFORM_PY, PRIMITIVES_PY_PRE, PRIMITIVES_PY_POST,
PRIMITIVES_PY_MODULES, _ALL_PY_MODULES, 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, _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: Args:
adapters: List of adapter names to include. adapters: List of adapter names to include.
Valid names: html, sx. Valid names: parser, html, sx.
None = include all server-side adapters. None = include all server-side adapters.
modules: List of primitive module names to include. modules: List of primitive module names to include.
core.* are always included. stdlib.* are opt-in. core.* are always included. stdlib.* are opt-in.
@@ -1280,7 +1462,11 @@ def compile_ref_to_py(
spec_mod_set.add("page-helpers") spec_mod_set.add("page-helpers")
if "router" in SPEC_MODULES: if "router" in SPEC_MODULES:
spec_mod_set.add("router") 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_deps = "deps" in spec_mod_set
has_cek = "cek" in spec_mod_set
# Core files always included, then selected adapters, then spec modules # Core files always included, then selected adapters, then spec modules
sx_files = [ sx_files = [
@@ -1288,10 +1474,19 @@ def compile_ref_to_py(
("forms.sx", "forms (server definition forms)"), ("forms.sx", "forms (server definition forms)"),
("render.sx", "render (core)"), ("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"): for name in ("html", "sx"):
if name in adapter_set: if name in adapter_set:
sx_files.append(ADAPTER_FILES[name]) 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): for name in sorted(spec_mod_set):
if name not in SPEC_MODULE_ORDER:
sx_files.append(SPEC_MODULES[name]) sx_files.append(SPEC_MODULES[name])
# Pre-scan define-async names (needed before transpilation so emitter # Pre-scan define-async names (needed before transpilation so emitter
@@ -1341,6 +1536,7 @@ def compile_ref_to_py(
# Build output # Build output
has_html = "html" in adapter_set has_html = "html" in adapter_set
has_sx = "sx" in adapter_set has_sx = "sx" in adapter_set
has_parser = "parser" in adapter_set
parts = [] parts = []
parts.append(PREAMBLE) parts.append(PREAMBLE)
@@ -1349,9 +1545,15 @@ def compile_ref_to_py(
parts.append(_assemble_primitives_py(prim_modules)) parts.append(_assemble_primitives_py(prim_modules))
parts.append(PRIMITIVES_PY_POST) parts.append(PRIMITIVES_PY_POST)
if has_parser:
parts.append(PLATFORM_PARSER_PY)
if has_deps: if has_deps:
parts.append(PLATFORM_DEPS_PY) parts.append(PLATFORM_DEPS_PY)
if has_cek:
parts.append(PLATFORM_CEK_PY)
if has_async: if has_async:
parts.append(PLATFORM_ASYNC_PY) parts.append(PLATFORM_ASYNC_PY)
@@ -1363,6 +1565,8 @@ def compile_ref_to_py(
parts.append("") parts.append("")
parts.append(FIXUPS_PY) parts.append(FIXUPS_PY)
if has_cek:
parts.append(CEK_FIXUPS_PY)
if has_continuations: if has_continuations:
parts.append(CONTINUATIONS_PY) parts.append(CONTINUATIONS_PY)
parts.append(public_api_py(has_html, has_sx, has_deps, has_async)) parts.append(public_api_py(has_html, has_sx, has_deps, has_async))

View File

@@ -20,17 +20,21 @@ logger = logging.getLogger("sx.boundary_parser")
# Allow standalone use (from bootstrappers) or in-project imports # Allow standalone use (from bootstrappers) or in-project imports
try: try:
from shared.sx.parser import parse_all
from shared.sx.types import Symbol, Keyword, NIL as SX_NIL from shared.sx.types import Symbol, Keyword, NIL as SX_NIL
except ImportError: except ImportError:
import sys import sys
_HERE = os.path.dirname(os.path.abspath(__file__)) _HERE = os.path.dirname(os.path.abspath(__file__))
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", "..")) _PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
sys.path.insert(0, _PROJECT) sys.path.insert(0, _PROJECT)
from shared.sx.parser import parse_all
from shared.sx.types import Symbol, Keyword, NIL as SX_NIL 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: def _ref_dir() -> str:
return os.path.dirname(os.path.abspath(__file__)) return os.path.dirname(os.path.abspath(__file__))
@@ -81,7 +85,7 @@ def _extract_declarations(
Returns (io_names, {service: helper_names}). Returns (io_names, {service: helper_names}).
""" """
exprs = parse_all(source) exprs = _get_parse_all()(source)
io_names: set[str] = set() io_names: set[str] = set()
helpers: dict[str, set[str]] = {} helpers: dict[str, set[str]] = {}
@@ -144,7 +148,7 @@ def parse_primitives_sx() -> frozenset[str]:
def parse_primitives_by_module() -> dict[str, frozenset[str]]: def parse_primitives_by_module() -> dict[str, frozenset[str]]:
"""Parse primitives.sx and return primitives grouped by module.""" """Parse primitives.sx and return primitives grouped by module."""
source = _read_file("primitives.sx") source = _read_file("primitives.sx")
exprs = parse_all(source) exprs = _get_parse_all()(source)
modules: dict[str, set[str]] = {} modules: dict[str, set[str]] = {}
current_module = "_unscoped" 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). type of the &rest parameter (or None if no &rest, or None if untyped &rest).
""" """
source = _read_file("primitives.sx") source = _read_file("primitives.sx")
exprs = parse_all(source) exprs = _get_parse_all()(source)
result: dict[str, dict] = {} result: dict[str, dict] = {}
for expr in exprs: 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 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]: def parse_boundary_types() -> frozenset[str]:
"""Parse boundary.sx and return the declared boundary type names.""" """Parse boundary.sx and return the declared boundary type names."""
source = _read_file("boundary.sx") source = _read_file("boundary.sx")
exprs = parse_all(source) exprs = _get_parse_all()(source)
for expr in exprs: for expr in exprs:
if (isinstance(expr, list) and len(expr) >= 2 if (isinstance(expr, list) and len(expr) >= 2
and isinstance(expr[0], Symbol) and isinstance(expr[0], Symbol)

1034
shared/sx/ref/cek.sx Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -530,7 +530,7 @@
(if (and env new-html (not (empty? new-html))) (if (and env new-html (not (empty? new-html)))
;; Parse new content as SX and re-evaluate in island scope ;; Parse new content as SX and re-evaluate in island scope
(let ((parsed (parse new-html))) (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 old reactive bindings in this marsh
(dispose-marsh-scope old-marsh) (dispose-marsh-scope old-marsh)
;; Evaluate the SX in a new marsh scope — creates new reactive bindings ;; Evaluate the SX in a new marsh scope — creates new reactive bindings

View File

@@ -941,14 +941,8 @@
(let ((before (trampoline (eval-expr (first args) env))) (let ((before (trampoline (eval-expr (first args) env)))
(body (trampoline (eval-expr (nth args 1) env))) (body (trampoline (eval-expr (nth args 1) env)))
(after (trampoline (eval-expr (nth args 2) env)))) (after (trampoline (eval-expr (nth args 2) env))))
;; Call entry thunk ;; Delegate to platform — needs try/finally for error safety
(call-thunk before env) (dynamic-wind-call before body after 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))))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------

262
shared/sx/ref/frames.sx Normal file
View 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))))

View File

@@ -87,12 +87,6 @@
"signal-remove-sub!" "signalRemoveSub" "signal-remove-sub!" "signalRemoveSub"
"signal-deps" "signalDeps" "signal-deps" "signalDeps"
"signal-set-deps!" "signalSetDeps" "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" "identical?" "isIdentical"
"notify-subscribers" "notifySubscribers" "notify-subscribers" "notifySubscribers"
"flush-subscribers" "flushSubscribers" "flush-subscribers" "flushSubscribers"
@@ -101,7 +95,6 @@
"register-in-scope" "registerInScope" "register-in-scope" "registerInScope"
"*batch-depth*" "_batchDepth" "*batch-depth*" "_batchDepth"
"*batch-queue*" "_batchQueue" "*batch-queue*" "_batchQueue"
"*island-scope*" "_islandScope"
"*store-registry*" "_storeRegistry" "*store-registry*" "_storeRegistry"
"def-store" "defStore" "def-store" "defStore"
"use-store" "useStore" "use-store" "useStore"
@@ -221,6 +214,10 @@
"render-dom-island" "renderDomIsland" "render-dom-island" "renderDomIsland"
"reactive-text" "reactiveText" "reactive-text" "reactiveText"
"reactive-attr" "reactiveAttr" "reactive-attr" "reactiveAttr"
"cek-reactive-text" "cekReactiveText"
"cek-reactive-attr" "cekReactiveAttr"
"*use-cek-reactive*" "_useCekReactive"
"enable-cek-reactive!" "enableCekReactive"
"reactive-fragment" "reactiveFragment" "reactive-fragment" "reactiveFragment"
"reactive-list" "reactiveList" "reactive-list" "reactiveList"
"dom-create-element" "domCreateElement" "dom-create-element" "domCreateElement"
@@ -527,6 +524,80 @@
"collect!" "sxCollect" "collect!" "sxCollect"
"collected" "sxCollected" "collected" "sxCollected"
"clear-collected!" "sxClearCollected" "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-push!" "scopePush"
"scope-pop!" "scopePop" "scope-pop!" "scopePop"
"provide-push!" "providePush" "provide-push!" "providePush"
@@ -584,7 +655,7 @@
(fn ((s :as string)) (fn ((s :as string))
(str "\"" (str "\""
(replace (replace (replace (replace (replace (replace (replace (replace (replace (replace (replace (replace
s "\\" "\\\\") "\"" "\\\"") "\n" "\\n") "\r" "\\r") "\t" "\\t") "\0" "\\0") s "\\" "\\\\") "\"" "\\\"") "\n" "\\n") "\r" "\\r") "\t" "\\t") (char-from-code 0) "\\u0000")
"\""))) "\"")))

View File

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

View File

@@ -46,8 +46,14 @@ SPEC_MODULES = {
"router": ("router.sx", "router (client-side route matching)"), "router": ("router.sx", "router (client-side route matching)"),
"signals": ("signals.sx", "signals (reactive signal runtime)"), "signals": ("signals.sx", "signals (reactive signal runtime)"),
"page-helpers": ("page-helpers.sx", "page-helpers (pure data transformation helpers)"), "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"} EXTENSION_NAMES = {"continuations"}
CONTINUATIONS_JS = ''' CONTINUATIONS_JS = '''
@@ -851,20 +857,6 @@ PREAMBLE = '''\
} }
Island.prototype._island = true; 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) { function Macro(params, restParam, body, closure, name) {
this.params = params; this.params = params;
this.restParam = restParam; 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["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["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; };
PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); }; 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["append!"] = function(arr, x) { arr.push(x); return arr; };
PRIMITIVES["chunk-every"] = function(c, n) { 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; 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._lambda) return "lambda";
if (x._component) return "component"; if (x._component) return "component";
if (x._island) return "island"; if (x._island) return "island";
if (x._signal) return "signal";
if (x._spread) return "spread"; if (x._spread) return "spread";
if (x._macro) return "macro"; if (x._macro) return "macro";
if (x._raw) return "raw-html"; if (x._raw) return "raw-html";
@@ -1259,34 +1250,6 @@ PLATFORM_JS_PRE = '''
return new Island(name, params, hasChildren, body, merge(env)); return new Island(name, params, hasChildren, body, merge(env));
} }
// Signal platform
function makeSignal(value) { return new SxSignal(value); }
function isSignal(x) { return x != null && x._signal === true; }
function signalValue(s) { return s.value; }
function signalSetValue(s, v) { s.value = v; }
function signalSubscribers(s) { return s.subscribers.slice(); }
function signalAddSub(s, fn) { if (s.subscribers.indexOf(fn) < 0) s.subscribers.push(fn); }
function signalRemoveSub(s, fn) { var i = s.subscribers.indexOf(fn); if (i >= 0) s.subscribers.splice(i, 1); }
function signalDeps(s) { return s.deps.slice(); }
function signalSetDeps(s, deps) { s.deps = Array.isArray(deps) ? deps.slice() : []; }
function setTrackingContext(ctx) { _trackingContext = ctx; }
function getTrackingContext() { return _trackingContext || NIL; }
function makeTrackingContext(notifyFn) { return new TrackingCtx(notifyFn); }
function trackingContextDeps(ctx) { return ctx ? ctx.deps : []; }
function trackingContextAddDep(ctx, s) { if (ctx && ctx.deps.indexOf(s) < 0) ctx.deps.push(s); }
function trackingContextNotifyFn(ctx) { return ctx ? ctx.notifyFn : NIL; }
// 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 // JSON / dict helpers for island state serialization
function jsonSerialize(obj) { function jsonSerialize(obj) {
return JSON.stringify(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; } 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 // List primitives used directly by transpiled code
var len = PRIMITIVES["len"]; var len = PRIMITIVES["len"];
var first = PRIMITIVES["first"]; 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_JS = '''
// ========================================================================= // =========================================================================
// Platform: deps module — component dependency analysis // Platform: deps module — component dependency analysis
@@ -1599,10 +1605,10 @@ PLATFORM_PARSER_JS = r"""
// ========================================================================= // =========================================================================
// Character classification derived from the grammar: // Character classification derived from the grammar:
// ident-start → [a-zA-Z_~*+\-><=/!?&] // ident-start → [a-zA-Z_~*+\-><=/!?&]
// ident-char → ident-start + [0-9.:\/\[\]#,] // ident-char → ident-start + [0-9.:\/\#,]
var _identStartRe = /[a-zA-Z_~*+\-><=/!?&]/; 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 isIdentStart(ch) { return _identStartRe.test(ch); }
function isIdentChar(ch) { return _identCharRe.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) // If lambda takes 0 params, call without event arg (convenience for on-click handlers)
var wrapped = isLambda(handler) var wrapped = isLambda(handler)
? (lambdaParams(handler).length === 0 ? (lambdaParams(handler).length === 0
? function(e) { try { invoke(handler); } 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 { invoke(handler, e); } 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; : handler;
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler)); if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
el.addEventListener(name, wrapped); el.addEventListener(name, wrapped);
@@ -2427,6 +2433,10 @@ PLATFORM_ORCHESTRATION_JS = """
} }
function scheduleIdle(fn) { function scheduleIdle(fn) {
var cb = _wrapSxFn(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); if (typeof requestIdleCallback !== "undefined") requestIdleCallback(cb);
else setTimeout(cb, 0); else setTimeout(cb, 0);
} }
@@ -2516,8 +2526,12 @@ PLATFORM_ORCHESTRATION_JS = """
e.preventDefault(); e.preventDefault();
// Re-read href from element at click time (not closed-over value) // Re-read href from element at click time (not closed-over value)
var liveHref = el.getAttribute("href") || _href; 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() { 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) {} 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) // Re-read href from element at click time (not closed-over value)
var liveHref = link.getAttribute("href") || _href; var liveHref = link.getAttribute("href") || _href;
var pathname = urlPathname(liveHref); 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 // Find target selector: sx-boost ancestor, explicit sx-target, or #main-panel
var boostEl = link.closest("[sx-boost]"); var boostEl = link.closest("[sx-boost]");
var targetSel = boostEl ? boostEl.getAttribute("sx-boost") : null; var targetSel = boostEl ? boostEl.getAttribute("sx-boost") : null;
if (!targetSel || targetSel === "true") { if (!targetSel || targetSel === "true") {
targetSel = link.getAttribute("sx-target") || "#main-panel"; targetSel = link.getAttribute("sx-target") || "#main-panel";
} }
console.log("[sx-debug] targetSel:", targetSel, "trying client route...");
if (tryClientRoute(pathname, targetSel)) { if (tryClientRoute(pathname, targetSel)) {
console.log("[sx-debug] client route SUCCESS, pushState:", liveHref);
try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {}
if (typeof window !== "undefined") window.scrollTo(0, 0); if (typeof window !== "undefined") window.scrollTo(0, 0);
} else { } else {
logInfo("sx:route server " + pathname); console.log("[sx-debug] client route FAILED, server fetch:", liveHref);
executeRequest(link, { method: "GET", url: liveHref }).then(function() { 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) {} try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {}
}).catch(function(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["stop-propagation"] = stopPropagation_;
PRIMITIVES["error-message"] = errorMessage; PRIMITIVES["error-message"] = errorMessage;
PRIMITIVES["schedule-idle"] = scheduleIdle; PRIMITIVES["schedule-idle"] = scheduleIdle;
PRIMITIVES["invoke"] = invoke;
PRIMITIVES["error"] = function(msg) { throw new Error(msg); }; PRIMITIVES["error"] = function(msg) { throw new Error(msg); };
PRIMITIVES["filter"] = filter; PRIMITIVES["filter"] = filter;
// DOM primitives for sx-on:* handlers and data-init scripts // 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) 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 # Parser: use compiled sxParse from parser.sx, or inline a minimal fallback
if has_parser: if has_parser:
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(' context: sxContext,')
api_lines.append(' emit: sxEmit,') api_lines.append(' emit: sxEmit,')
api_lines.append(' emitted: sxEmitted,') 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(f' _version: "{version}"')
api_lines.append(' };') api_lines.append(' };')
api_lines.append('') api_lines.append('')

View File

@@ -225,8 +225,6 @@ def type_of(x):
return "component" return "component"
if isinstance(x, Island): if isinstance(x, Island):
return "island" return "island"
if isinstance(x, _Signal):
return "signal"
if isinstance(x, _Spread): if isinstance(x, _Spread):
return "spread" return "spread"
if isinstance(x, Macro): if isinstance(x, Macro):
@@ -468,105 +466,6 @@ def is_identical(a, b):
return a is 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): def json_serialize(obj):
@@ -751,51 +650,6 @@ def escape_string(s):
.replace("</script", "<\\\\/script")) .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 _SPECIAL_FORM_NAMES = frozenset() # Placeholder — overridden by transpiled adapter-sx.sx
_HO_FORM_NAMES = frozenset() _HO_FORM_NAMES = frozenset()
@@ -1095,6 +949,37 @@ def for_each_indexed(fn, coll):
def map_dict(fn, d): def map_dict(fn, d):
return {k: fn(k, v) for k, v in d.items()} 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 # Aliases used directly by transpiled code
first = PRIMITIVES["first"] first = PRIMITIVES["first"]
last = PRIMITIVES["last"] last = PRIMITIVES["last"]
@@ -1124,8 +1009,58 @@ replace = PRIMITIVES["replace"]
parse_int = PRIMITIVES["parse-int"] parse_int = PRIMITIVES["parse-int"]
upper = PRIMITIVES["upper"] upper = PRIMITIVES["upper"]
has_key_p = PRIMITIVES["has-key?"] has_key_p = PRIMITIVES["has-key?"]
dict_p = PRIMITIVES["dict?"]
dissoc = PRIMITIVES["dissoc"] dissoc = PRIMITIVES["dissoc"]
index_of = PRIMITIVES["index-of"] 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' ' 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 # Platform: async adapter — async evaluation, I/O dispatch
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -1194,7 +1183,7 @@ PLATFORM_ASYNC_PY = '''
# ========================================================================= # =========================================================================
import contextvars import contextvars
import inspect import inspect as _inspect
from shared.sx.primitives_io import ( from shared.sx.primitives_io import (
IO_PRIMITIVES, RequestContext, execute_io, IO_PRIMITIVES, RequestContext, execute_io,
@@ -1281,13 +1270,8 @@ def number_p(x):
return isinstance(x, (int, float)) and not isinstance(x, bool) 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): def is_async_coroutine(x):
return inspect.iscoroutine(x) return _inspect.iscoroutine(x)
async def async_await(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):', 'def make_env(**kwargs):',
' """Create an environment with initial bindings."""', ' """Create an environment with initial bindings."""',
' return _Env(dict(kwargs))', ' 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) return '\n'.join(lines)
@@ -1551,6 +1597,7 @@ def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False,
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
ADAPTER_FILES = { ADAPTER_FILES = {
"parser": ("parser.sx", "parser"),
"html": ("adapter-html.sx", "adapter-html"), "html": ("adapter-html.sx", "adapter-html"),
"sx": ("adapter-sx.sx", "adapter-sx"), "sx": ("adapter-sx.sx", "adapter-sx"),
"async": ("adapter-async.sx", "adapter-async"), "async": ("adapter-async.sx", "adapter-async"),
@@ -1563,8 +1610,16 @@ SPEC_MODULES = {
"signals": ("signals.sx", "signals (reactive signal runtime)"), "signals": ("signals.sx", "signals (reactive signal runtime)"),
"page-helpers": ("page-helpers.sx", "page-helpers (pure data transformation helpers)"), "page-helpers": ("page-helpers.sx", "page-helpers (pure data transformation helpers)"),
"types": ("types.sx", "types (gradual type system)"), "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_NAMES = {"continuations"}
EXTENSION_FORMS = { EXTENSION_FORMS = {

View File

@@ -84,12 +84,6 @@
"signal-remove-sub!" "signal_remove_sub" "signal-remove-sub!" "signal_remove_sub"
"signal-deps" "signal_deps" "signal-deps" "signal_deps"
"signal-set-deps!" "signal_set_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" "identical?" "is_identical"
"notify-subscribers" "notify_subscribers" "notify-subscribers" "notify_subscribers"
"flush-subscribers" "flush_subscribers" "flush-subscribers" "flush_subscribers"
@@ -98,7 +92,6 @@
"register-in-scope" "register_in_scope" "register-in-scope" "register_in_scope"
"*batch-depth*" "_batch_depth" "*batch-depth*" "_batch_depth"
"*batch-queue*" "_batch_queue" "*batch-queue*" "_batch_queue"
"*island-scope*" "_island_scope"
"*store-registry*" "_store_registry" "*store-registry*" "_store_registry"
"def-store" "def_store" "def-store" "def_store"
"use-store" "use_store" "use-store" "use_store"

View 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)

View 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)

View 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)

View File

@@ -24,11 +24,12 @@ from shared.sx.parser import parse_all
from shared.sx.types import Symbol from shared.sx.types import Symbol
from shared.sx.ref.platform_js import ( from shared.sx.ref.platform_js import (
extract_defines, 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, PREAMBLE, PLATFORM_JS_PRE, PLATFORM_JS_POST,
PRIMITIVES_JS_MODULES, _ALL_JS_MODULES, _assemble_primitives_js, PRIMITIVES_JS_MODULES, _ALL_JS_MODULES, _assemble_primitives_js,
PLATFORM_DEPS_JS, PLATFORM_PARSER_JS, PLATFORM_DOM_JS, PLATFORM_DEPS_JS, PLATFORM_PARSER_JS, PLATFORM_DOM_JS,
PLATFORM_ENGINE_PURE_JS, PLATFORM_ORCHESTRATION_JS, PLATFORM_BOOT_JS, PLATFORM_ENGINE_PURE_JS, PLATFORM_ORCHESTRATION_JS, PLATFORM_BOOT_JS,
PLATFORM_CEK_JS, CEK_FIXUPS_JS,
CONTINUATIONS_JS, ASYNC_IO_JS, CONTINUATIONS_JS, ASYNC_IO_JS,
fixups_js, public_api_js, EPILOGUE, fixups_js, public_api_js, EPILOGUE,
) )
@@ -105,9 +106,17 @@ def compile_ref_to_js(
spec_mod_set.add("deps") spec_mod_set.add("deps")
if "page-helpers" in SPEC_MODULES: if "page-helpers" in SPEC_MODULES:
spec_mod_set.add("page-helpers") 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_deps = "deps" in spec_mod_set
has_router = "router" in spec_mod_set has_router = "router" in spec_mod_set
has_page_helpers = "page-helpers" in spec_mod_set has_page_helpers = "page-helpers" in spec_mod_set
has_cek = "cek" in spec_mod_set
# Resolve extensions # Resolve extensions
ext_set = set() ext_set = set()
@@ -126,7 +135,13 @@ def compile_ref_to_js(
for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "boot"): for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "boot"):
if name in adapter_set: if name in adapter_set:
sx_files.append(ADAPTER_FILES[name]) 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): for name in sorted(spec_mod_set):
if name not in SPEC_MODULE_ORDER:
sx_files.append(SPEC_MODULES[name]) sx_files.append(SPEC_MODULES[name])
has_html = "html" in adapter_set has_html = "html" in adapter_set
@@ -175,6 +190,10 @@ def compile_ref_to_js(
if has_parser: if has_parser:
parts.append(adapter_platform["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 # Translate each spec file using js.sx
for filename, label in sx_files: for filename, label in sx_files:
filepath = os.path.join(ref_dir, filename) filepath = os.path.join(ref_dir, filename)
@@ -202,11 +221,13 @@ def compile_ref_to_js(
parts.append(adapter_platform[name]) parts.append(adapter_platform[name])
parts.append(fixups_js(has_html, has_sx, has_dom, has_signals, has_deps, has_page_helpers)) 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: if has_continuations:
parts.append(CONTINUATIONS_JS) parts.append(CONTINUATIONS_JS)
if has_dom: if has_dom:
parts.append(ASYNC_IO_JS) 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) parts.append(EPILOGUE)
build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

View 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)

View File

@@ -7,9 +7,17 @@ _HERE = os.path.dirname(os.path.abspath(__file__))
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", "..")) _PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
sys.path.insert(0, _PROJECT) sys.path.insert(0, _PROJECT)
from shared.sx.parser import parse_all from shared.sx.ref.sx_ref import sx_parse as parse_all
from shared.sx.ref.sx_ref import eval_expr, trampoline, make_env 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 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 # Build env with primitives
env = make_env() env = make_env()
@@ -154,6 +162,9 @@ env["component-params"] = _component_params
env["component-body"] = _component_body env["component-body"] = _component_body
env["component-has-children"] = _component_has_children env["component-has-children"] = _component_has_children
env["map-dict"] = _map_dict 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) # Load test framework (macros + assertion helpers)
with open(os.path.join(_HERE, "test-framework.sx")) as f: with open(os.path.join(_HERE, "test-framework.sx")) as f:

View File

@@ -9,34 +9,59 @@
;; layer (adapter-dom.sx) subscribes DOM nodes to signals. The server ;; layer (adapter-dom.sx) subscribes DOM nodes to signals. The server
;; adapter (adapter-html.sx) reads signal values without subscribing. ;; adapter (adapter-html.sx) reads signal values without subscribing.
;; ;;
;; Platform interface required: ;; Signals are plain dicts with a "__signal" marker key. No platform
;; (make-signal value) → Signal — create signal container ;; primitives needed — all signal operations are pure SX.
;; (signal? x) → boolean — type predicate
;; (signal-value s) → any — read current value (no tracking)
;; (signal-set-value! s v) → void — write value (no notification)
;; (signal-subscribers s) → list — list of subscriber fns
;; (signal-add-sub! s fn) → void — add subscriber
;; (signal-remove-sub! s fn) → void — remove subscriber
;; (signal-deps s) → list — dependency list (for computed)
;; (signal-set-deps! s deps) → void — set dependency list
;; ;;
;; Global state required: ;; Reactive tracking and island lifecycle use the general scoped effects
;; *tracking-context* → nil | Effect/Computed currently evaluating ;; system (scope-push!/scope-pop!/context) instead of separate globals.
;; (set-tracking-context! c) → void ;; Two scope names:
;; (get-tracking-context) → context or nil ;; "sx-reactive" — tracking context for computed/effect dep discovery
;; "sx-island-scope" — island disposable collector
;; ;;
;; Runtime callable dispatch: ;; Scope-based tracking:
;; (invoke f &rest args) → any — call f with args; handles both ;; (scope-push! "sx-reactive" {:deps (list) :notify fn}) → void
;; native host functions AND SX lambdas ;; (scope-pop! "sx-reactive") → void
;; from runtime-evaluated code (islands). ;; (context "sx-reactive" nil) → dict or nil
;; Transpiled code emits direct calls ;;
;; f(args) which fail on SX lambdas. ;; CEK callable dispatch:
;; invoke goes through the evaluator's ;; (cek-call f args) → any — call f with args list via CEK.
;; dispatch (call-fn) so either works. ;; 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 ;; 1. signal — create a reactive container
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -58,12 +83,14 @@
(fn ((s :as any)) (fn ((s :as any))
(if (not (signal? s)) (if (not (signal? s))
s ;; non-signal values pass through s ;; non-signal values pass through
(let ((ctx (get-tracking-context))) (let ((ctx (context "sx-reactive" nil)))
(when ctx (when ctx
;; Register this signal as a dependency of the current context ;; Register this signal as a dependency of the current context
(tracking-context-add-dep! ctx s) (let ((dep-list (get ctx "deps"))
;; Subscribe the context to this signal (notify-fn (get ctx "notify")))
(signal-add-sub! s (tracking-context-notify-fn ctx))) (when (not (contains? dep-list s))
(append! dep-list s)
(signal-add-sub! s notify-fn))))
(signal-value s))))) (signal-value s)))))
@@ -117,19 +144,18 @@
(signal-deps s)) (signal-deps s))
(signal-set-deps! s (list)) (signal-set-deps! s (list))
;; Create tracking context for this computed ;; Push scope-based tracking context for this computed
(let ((ctx (make-tracking-context recompute))) (let ((ctx (dict "deps" (list) "notify" recompute)))
(let ((prev (get-tracking-context))) (scope-push! "sx-reactive" ctx)
(set-tracking-context! ctx) (let ((new-val (cek-call compute-fn nil)))
(let ((new-val (invoke compute-fn))) (scope-pop! "sx-reactive")
(set-tracking-context! prev)
;; Save discovered deps ;; Save discovered deps
(signal-set-deps! s (tracking-context-deps ctx)) (signal-set-deps! s (get ctx "deps"))
;; Update value + notify downstream ;; Update value + notify downstream
(let ((old (signal-value s))) (let ((old (signal-value s)))
(signal-set-value! s new-val) (signal-set-value! s new-val)
(when (not (identical? old new-val)) (when (not (identical? old new-val))
(notify-subscribers s))))))))) (notify-subscribers s))))))))
;; Initial computation ;; Initial computation
(recompute) (recompute)
@@ -155,7 +181,7 @@
(fn () (fn ()
(when (not disposed) (when (not disposed)
;; Run previous cleanup if any ;; Run previous cleanup if any
(when cleanup-fn (invoke cleanup-fn)) (when cleanup-fn (cek-call cleanup-fn nil))
;; Unsubscribe from old deps ;; Unsubscribe from old deps
(for-each (for-each
@@ -163,16 +189,15 @@
deps) deps)
(set! deps (list)) (set! deps (list))
;; Track new deps ;; Push scope-based tracking context
(let ((ctx (make-tracking-context run-effect))) (let ((ctx (dict "deps" (list) "notify" run-effect)))
(let ((prev (get-tracking-context))) (scope-push! "sx-reactive" ctx)
(set-tracking-context! ctx) (let ((result (cek-call effect-fn nil)))
(let ((result (invoke effect-fn))) (scope-pop! "sx-reactive")
(set-tracking-context! prev) (set! deps (get ctx "deps"))
(set! deps (tracking-context-deps ctx))
;; If effect returns a function, it's the cleanup ;; If effect returns a function, it's the cleanup
(when (callable? result) (when (callable? result)
(set! cleanup-fn result))))))))) (set! cleanup-fn result))))))))
;; Initial run ;; Initial run
(run-effect) (run-effect)
@@ -181,7 +206,7 @@
(let ((dispose-fn (let ((dispose-fn
(fn () (fn ()
(set! disposed true) (set! disposed true)
(when cleanup-fn (invoke cleanup-fn)) (when cleanup-fn (cek-call cleanup-fn nil))
(for-each (for-each
(fn ((dep :as signal)) (signal-remove-sub! dep run-effect)) (fn ((dep :as signal)) (signal-remove-sub! dep run-effect))
deps) deps)
@@ -204,7 +229,7 @@
(define batch :effects [mutation] (define batch :effects [mutation]
(fn ((thunk :as lambda)) (fn ((thunk :as lambda))
(set! *batch-depth* (+ *batch-depth* 1)) (set! *batch-depth* (+ *batch-depth* 1))
(invoke thunk) (cek-call thunk nil)
(set! *batch-depth* (- *batch-depth* 1)) (set! *batch-depth* (- *batch-depth* 1))
(when (= *batch-depth* 0) (when (= *batch-depth* 0)
(let ((queue *batch-queue*)) (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 ;; Tracking is now scope-based. computed/effect push a dict
;; evaluation to discover signal dependencies. Platform must provide: ;; {:deps (list) :notify fn} onto the "sx-reactive" scope stack via
;; ;; scope-push!/scope-pop!. deref reads it via (context "sx-reactive" nil).
;; (make-tracking-context notify-fn) → context ;; No platform primitives needed — uses the existing scope infrastructure.
;; (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).
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -284,25 +303,24 @@
;; When an island is created, all signals, effects, and computeds created ;; When an island is created, all signals, effects, and computeds created
;; within it are tracked. When the island is removed from the DOM, they ;; within it are tracked. When the island is removed from the DOM, they
;; are all disposed. ;; 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] (define with-island-scope :effects [mutation]
(fn ((scope-fn :as lambda) (body-fn :as lambda)) (fn ((scope-fn :as lambda) (body-fn :as lambda))
(let ((prev *island-scope*)) (scope-push! "sx-island-scope" scope-fn)
(set! *island-scope* scope-fn)
(let ((result (body-fn))) (let ((result (body-fn)))
(set! *island-scope* prev) (scope-pop! "sx-island-scope")
result)))) result)))
;; Hook into signal/effect/computed creation for scope tracking. ;; 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] (define register-in-scope :effects [mutation]
(fn ((disposable :as lambda)) (fn ((disposable :as lambda))
(when *island-scope* (let ((collector (context "sx-island-scope" nil)))
(*island-scope* disposable)))) (when collector
(cek-call collector (list disposable))))))
;; ========================================================================== ;; ==========================================================================
@@ -341,7 +359,7 @@
;; Parent island scope and sibling marshes are unaffected. ;; Parent island scope and sibling marshes are unaffected.
(let ((disposers (dom-get-data marsh-el "sx-marsh-disposers"))) (let ((disposers (dom-get-data marsh-el "sx-marsh-disposers")))
(when 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))))) (dom-set-data marsh-el "sx-marsh-disposers" nil)))))
@@ -363,7 +381,7 @@
(let ((registry *store-registry*)) (let ((registry *store-registry*))
;; Only create the store once — subsequent calls return existing ;; Only create the store once — subsequent calls return existing
(when (not (has-key? registry name)) (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)))) (get *store-registry* name))))
(define use-store :effects [] (define use-store :effects []
@@ -422,7 +440,7 @@
(fn (e) (fn (e)
(let ((detail (event-detail e)) (let ((detail (event-detail e))
(new-val (if transform-fn (new-val (if transform-fn
(invoke transform-fn detail) (cek-call transform-fn (list detail))
detail))) detail)))
(reset! target-signal new-val)))))) (reset! target-signal new-val))))))
;; Return cleanup — removes listener on dispose/re-run ;; Return cleanup — removes listener on dispose/re-run
@@ -453,7 +471,7 @@
(fn ((fetch-fn :as lambda)) (fn ((fetch-fn :as lambda))
(let ((state (signal (dict "loading" true "data" nil "error" nil)))) (let ((state (signal (dict "loading" true "data" nil "error" nil))))
;; Kick off the async operation ;; 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 (data) (reset! state (dict "loading" false "data" data "error" nil)))
(fn (err) (reset! state (dict "loading" false "data" nil "error" err)))) (fn (err) (reset! state (dict "loading" false "data" nil "error" err))))
state))) state)))

File diff suppressed because it is too large Load Diff

View 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
View 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))))"))))

View 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))))))))

View File

@@ -171,3 +171,46 @@
(list "wrap" children)) (list "wrap" children))
(assert-equal (list "wrap" (list "a" "b")) (assert-equal (list "wrap" (list "a" "b"))
(~wrapper "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)))))

View File

@@ -597,3 +597,56 @@
(deftest "nil caller allows all" (deftest "nil caller allows all"
(assert-true (effects-subset? (list "io") nil)))) (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)))))

View File

@@ -860,6 +860,40 @@
annotations))) 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 ;; Platform interface summary
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------

View File

@@ -361,11 +361,15 @@ class Continuation:
Callable with one argument — provides the value that the shift Callable with one argument — provides the value that the shift
expression "returns" within the delimited context. 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): def __init__(self, fn):
self.fn = fn self.fn = fn
self._cek_data = None
def __call__(self, value=NIL): def __call__(self, value=NIL):
return self.fn(value) return self.fn(value)
@@ -397,6 +401,43 @@ class EvalError(Exception):
pass 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 # Type alias
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

195
sx/sx/geography/cek.sx Normal file
View 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")))))

View File

@@ -166,6 +166,12 @@
(dict :label "Optimistic" :href "/sx/(geography.(isomorphism.optimistic))") (dict :label "Optimistic" :href "/sx/(geography.(isomorphism.optimistic))")
(dict :label "Offline" :href "/sx/(geography.(isomorphism.offline))"))) (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 (define plans-nav-items (list
(dict :label "Status" :href "/sx/(etc.(plan.status))" (dict :label "Status" :href "/sx/(etc.(plan.status))"
:summary "Audit of all plans — what's done, what's in progress, and what remains.") :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))" (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.") :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))" (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 (define reactive-islands-nav-items (list
(dict :label "Overview" :href "/sx/(geography.(reactive))" (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."} :summary "Child-to-parent communication across render boundaries — spread, collect!, reactive-spread, built on scopes."}
{:label "Marshes" :href "/sx/(geography.(marshes))" {: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."} :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)" {:label "Language" :href "/sx/(language)"
:children (list :children (list
{:label "Docs" :href "/sx/(language.(doc))" :children docs-nav-items} {:label "Docs" :href "/sx/(language.(doc))" :children docs-nav-items}

View File

@@ -60,6 +60,18 @@
"phase2" '(~reactive-islands/phase2/reactive-islands-phase2-content) "phase2" '(~reactive-islands/phase2/reactive-islands-phase2-content)
:else '(~reactive-islands/index/reactive-islands-index-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 (define scopes
(fn (content) (fn (content)
(if (nil? content) '(~geography/scopes-content) content))) (if (nil? content) '(~geography/scopes-content) content)))
@@ -535,4 +547,6 @@
"sx-protocol" '(~plans/sx-protocol/plan-sx-protocol-content) "sx-protocol" '(~plans/sx-protocol/plan-sx-protocol-content)
"scoped-effects" '(~plans/scoped-effects/plan-scoped-effects-content) "scoped-effects" '(~plans/scoped-effects/plan-scoped-effects-content)
"foundations" '(~plans/foundations/plan-foundations-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))))) :else '(~plans/index/plans-index-content)))))

302
sx/sx/plans/cek-reactive.sx Normal file
View 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.")))))

View 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."))))

View File

@@ -578,6 +578,9 @@
"sx-protocol" (~plans/sx-protocol/plan-sx-protocol-content) "sx-protocol" (~plans/sx-protocol/plan-sx-protocol-content)
"scoped-effects" (~plans/scoped-effects/plan-scoped-effects-content) "scoped-effects" (~plans/scoped-effects/plan-scoped-effects-content)
"foundations" (~plans/foundations/plan-foundations-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)))) :else (~plans/index/plans-index-content))))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
@@ -649,6 +652,25 @@
:layout :sx-docs :layout :sx-docs
:content (~layouts/doc :path "/sx/(geography.(marshes))" (~reactive-islands/marshes/reactive-islands-marshes-content))) :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 ;; Bootstrapped page helpers demo
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------

View File

@@ -267,6 +267,8 @@ _REDIRECT_PATTERNS = [
lambda m: f"/sx/(geography.(reactive.{m.group(1)}))"), lambda m: f"/sx/(geography.(reactive.{m.group(1)}))"),
(re.compile(r"^/geography/isomorphism/(.+?)/?$"), (re.compile(r"^/geography/isomorphism/(.+?)/?$"),
lambda m: f"/sx/(geography.(isomorphism.{m.group(1)}))"), 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/?$"), (re.compile(r"^/geography/spreads/?$"),
"/sx/(geography.(spreads))"), "/sx/(geography.(spreads))"),
(re.compile(r"^/geography/marshes/?$"), (re.compile(r"^/geography/marshes/?$"),
@@ -290,6 +292,7 @@ _REDIRECT_PATTERNS = [
(re.compile(r"^/geography/hypermedia/?$"), "/sx/(geography.(hypermedia))"), (re.compile(r"^/geography/hypermedia/?$"), "/sx/(geography.(hypermedia))"),
(re.compile(r"^/geography/reactive/?$"), "/sx/(geography.(reactive))"), (re.compile(r"^/geography/reactive/?$"), "/sx/(geography.(reactive))"),
(re.compile(r"^/geography/isomorphism/?$"), "/sx/(geography.(isomorphism))"), (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"^/geography/?$"), "/sx/(geography)"),
(re.compile(r"^/applications/cssx/?$"), "/sx/(applications.(cssx))"), (re.compile(r"^/applications/cssx/?$"), "/sx/(applications.(cssx))"),
(re.compile(r"^/applications/protocols/?$"), "/sx/(applications.(protocol))"), (re.compile(r"^/applications/protocols/?$"), "/sx/(applications.(protocol))"),