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>
This commit is contained in:
2026-03-14 01:13:31 +00:00
parent 5c4a8c8cc2
commit 893c767238
6 changed files with 728 additions and 2 deletions

View File

@@ -0,0 +1,258 @@
#!/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)
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?"] = 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 (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
def _invoke(f, *args):
"""Call f with args — handles both native callables and SX lambdas."""
if isinstance(f, Lambda):
return trampoline(sx_ref.call_lambda(f, list(args), env))
elif callable(f):
return f(*args)
return NIL
env["invoke"] = _invoke
# 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

@@ -10,9 +10,18 @@ sys.path.insert(0, _PROJECT)
from shared.sx.parser import parse_all
from shared.sx.ref import sx_ref
from shared.sx.ref.sx_ref import (
eval_expr, trampoline, make_env, env_get, env_has, env_set,
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,

View File

@@ -0,0 +1,154 @@
;; ==========================================================================
;; 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) (invoke d)) disposers)
;; Change signal — no update should fire
(reset! s 999)
(assert-equal 0 (len update-calls)))))

View File

@@ -226,7 +226,9 @@
(dict :label "Scoped Effects" :href "/sx/(etc.(plan.scoped-effects))"
:summary "Algebraic effects as the unified foundation — spreads, islands, lakes, signals, and context are all instances of one primitive: a named scope with downward value, upward accumulation, and a propagation mode.")
(dict :label "Foundations" :href "/sx/(etc.(plan.foundations))"
:summary "The computational floor — from scoped effects through algebraic effects and delimited continuations to the CEK machine. Why three registers are irreducible, and the three-axis model (depth, topology, linearity).")))
:summary "The computational floor — from scoped effects through algebraic effects and delimited continuations to the CEK machine. Why three registers are irreducible, and the three-axis model (depth, topology, linearity).")
(dict :label "Deref as Shift" :href "/sx/(etc.(plan.cek-reactive))"
:summary "Phase B: replace explicit effect wrapping with implicit continuation capture. Deref inside reactive-reset performs shift, capturing the rest of the expression as the subscriber.")))
(define reactive-islands-nav-items (list
(dict :label "Overview" :href "/sx/(geography.(reactive))"

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

@@ -578,6 +578,7 @@
"sx-protocol" (~plans/sx-protocol/plan-sx-protocol-content)
"scoped-effects" (~plans/scoped-effects/plan-scoped-effects-content)
"foundations" (~plans/foundations/plan-foundations-content)
"cek-reactive" (~plans/cek-reactive/plan-cek-reactive-content)
:else (~plans/index/plans-index-content))))
;; ---------------------------------------------------------------------------