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:
258
shared/sx/ref/run_cek_reactive_tests.py
Normal file
258
shared/sx/ref/run_cek_reactive_tests.py
Normal 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)
|
||||
@@ -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,
|
||||
|
||||
154
shared/sx/ref/test-cek-reactive.sx
Normal file
154
shared/sx/ref/test-cek-reactive.sx
Normal 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)))))
|
||||
@@ -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
302
sx/sx/plans/cek-reactive.sx
Normal file
@@ -0,0 +1,302 @@
|
||||
;; Deref as Shift — CEK-Based Reactive DOM Renderer
|
||||
;; Phase B: replace explicit effects with implicit continuation capture.
|
||||
|
||||
(defcomp ~plans/cek-reactive/plan-cek-reactive-content ()
|
||||
(~docs/page :title "Deref as Shift — CEK-Based Reactive DOM Renderer"
|
||||
|
||||
(p :class "text-stone-500 text-sm italic mb-8"
|
||||
"Phase A collapsed signals to plain dicts with zero platform primitives. "
|
||||
"Phase B replaces explicit effect wrapping in the reactive DOM renderer "
|
||||
"with implicit continuation capture: when " (code "deref") " encounters a signal "
|
||||
"inside a " (code "reactive-reset") " boundary, it performs " (code "shift") ", "
|
||||
"capturing the rest of the expression as a continuation. "
|
||||
"That continuation IS the subscriber.")
|
||||
|
||||
;; =====================================================================
|
||||
;; The Insight
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "The Insight" :id "insight"
|
||||
|
||||
(p "Each reactive binding is a micro-computation:")
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (code "reactive-text") ": given signal value, set text node content to " (code "(str value)"))
|
||||
(li (code "reactive-attr") ": given signal value, set attribute to " (code "(str value)")))
|
||||
|
||||
(p "Currently wrapped in explicit " (code "effect") " calls. With deref-as-shift, "
|
||||
"the continuation captures this automatically:")
|
||||
|
||||
(~docs/code :code (highlight
|
||||
";; User writes:\n(div :class (str \"count-\" (deref counter))\n (str \"Value: \" (deref counter)))\n\n;; Renderer internally wraps each expression:\n(div :class (reactive-reset update-attr-fn (str \"count-\" (deref counter)))\n (reactive-reset update-text-fn (str \"Value: \" (deref counter))))\n\n;; When (deref counter) hits a signal inside reactive-reset:\n;; 1. Shift: capture continuation (str \"count-\" [HOLE])\n;; 2. Register continuation as signal subscriber\n;; 3. Return current value for initial render\n;; When counter changes:\n;; Re-invoke continuation with new value → update-fn updates DOM"
|
||||
"lisp")))
|
||||
|
||||
;; =====================================================================
|
||||
;; Step 1: Bootstrap CEK to JavaScript
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Step 1: Bootstrap CEK to JavaScript" :id "step-1"
|
||||
|
||||
(p "Add " (code "frames.sx") " + " (code "cek.sx") " to the JS build pipeline. "
|
||||
"Currently CEK is Python-only.")
|
||||
|
||||
(~docs/subsection :title "1a. platform_js.py — SPEC_MODULES + platform code"
|
||||
|
||||
(p "Add to " (code "SPEC_MODULES") " dict:")
|
||||
(~docs/code :code (highlight
|
||||
"\"frames\": (\"frames.sx\", \"frames (CEK continuation frames)\"),\n\"cek\": (\"cek.sx\", \"cek (explicit CEK machine evaluator)\"),\n\n# Add ordering (new constant):\nSPEC_MODULE_ORDER = [\"deps\", \"frames\", \"page-helpers\", \"router\", \"signals\", \"cek\"]"
|
||||
"python"))
|
||||
|
||||
(p "Add " (code "PLATFORM_CEK_JS") " constant (mirrors " (code "PLATFORM_CEK_PY") "):")
|
||||
(~docs/code :code (highlight
|
||||
"// Primitive aliases used by cek.sx\nvar inc = PRIMITIVES[\"inc\"];\nvar dec = PRIMITIVES[\"dec\"];\nvar zip_pairs = PRIMITIVES[\"zip-pairs\"];\n\nfunction makeCekContinuation(captured, restKont) {\n var c = new Continuation(function(v) { return v !== undefined ? v : NIL; });\n c._cek_data = {\"captured\": captured, \"rest-kont\": restKont};\n return c;\n}\nfunction continuationData(c) {\n return (c && c._cek_data) ? c._cek_data : {};\n}"
|
||||
"javascript"))
|
||||
|
||||
(p "Add " (code "CEK_FIXUPS_JS") " — iterative " (code "cek-run") " override:")
|
||||
(~docs/code :code (highlight
|
||||
"cekRun = function(state) {\n while (!cekTerminal_p(state)) { state = cekStep(state); }\n return cekValue(state);\n};"
|
||||
"javascript")))
|
||||
|
||||
(~docs/subsection :title "1b. run_js_sx.py — Update compile_ref_to_js"
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li "Auto-add " (code "\"frames\"") " when " (code "\"cek\"") " in spec_mod_set (mirror Python " (code "bootstrap_py.py") ")")
|
||||
(li "Auto-add " (code "\"cek\"") " + " (code "\"frames\"") " when " (code "\"dom\"") " adapter included (CEK needed for reactive rendering)")
|
||||
(li "Use " (code "SPEC_MODULE_ORDER") " for ordering instead of " (code "sorted()"))
|
||||
(li "Add " (code "has_cek") " flag")
|
||||
(li "Include " (code "PLATFORM_CEK_JS") " after transpiled code when " (code "has_cek"))
|
||||
(li "Include " (code "CEK_FIXUPS_JS") " in fixups section when " (code "has_cek"))))
|
||||
|
||||
(~docs/subsection :title "1c. js.sx — RENAMES for predicate functions"
|
||||
(p "Default mangling handles most names. Only add RENAMES where " (code "?")
|
||||
" suffix needs clean JS names:")
|
||||
(~docs/code :code (highlight
|
||||
"\"cek-terminal?\" \"cekTerminalP\"\n\"kont-empty?\" \"kontEmptyP\"\n\"make-cek-continuation\" \"makeCekContinuation\"\n\"continuation-data\" \"continuationData\""
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "1d. bootstrap_py.py — RENAMES for CEK predicates"
|
||||
(~docs/code :code (highlight
|
||||
"\"cek-terminal?\": \"cek_terminal_p\",\n\"kont-empty?\": \"kont_empty_p\",\n\"make-cek-continuation\": \"make_cek_continuation\",\n\"continuation-data\": \"continuation_data\","
|
||||
"python")))
|
||||
|
||||
(~docs/subsection :title "Verification"
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li "Rebootstrap JS: " (code "python3 bootstrap_js.py"))
|
||||
(li "Check output contains frame constructors + CEK step functions")
|
||||
(li "Run existing CEK Python tests: " (code "python3 run_cek_tests.py") " (should still pass)"))))
|
||||
|
||||
;; =====================================================================
|
||||
;; Step 2: ReactiveResetFrame + DerefFrame
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Step 2: ReactiveResetFrame + DerefFrame" :id "step-2"
|
||||
|
||||
(p "New frame types in " (code "frames.sx") " that enable deref-as-shift.")
|
||||
|
||||
(~docs/subsection :title "2a. New frame constructors"
|
||||
(~docs/code :code (highlight
|
||||
";; ReactiveResetFrame: delimiter for reactive deref-as-shift\n;; Carries an update-fn that gets called with new values on re-render.\n(define make-reactive-reset-frame\n (fn (env update-fn first-render?)\n {:type \"reactive-reset\" :env env :update-fn update-fn\n :first-render first-render?}))\n\n;; DerefFrame: awaiting evaluation of deref's argument\n(define make-deref-frame\n (fn (env)\n {:type \"deref\" :env env}))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "2b. Update kont-capture-to-reset"
|
||||
(p "Must stop at EITHER " (code "\"reset\"") " OR " (code "\"reactive-reset\"") ":")
|
||||
(~docs/code :code (highlight
|
||||
"(define kont-capture-to-reset\n (fn (kont)\n (define scan\n (fn (k captured)\n (if (empty? k)\n (error \"shift without enclosing reset\")\n (let ((frame (first k)))\n (if (or (= (frame-type frame) \"reset\")\n (= (frame-type frame) \"reactive-reset\"))\n (list captured (rest k))\n (scan (rest k) (append captured (list frame))))))))\n (scan kont (list))))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "2c. Helpers to scan for ReactiveResetFrame"
|
||||
(~docs/code :code (highlight
|
||||
"(define has-reactive-reset-frame?\n (fn (kont)\n (if (empty? kont) false\n (if (= (frame-type (first kont)) \"reactive-reset\") true\n (has-reactive-reset-frame? (rest kont))))))\n\n;; Returns 3 values: (captured, frame, rest)\n(define kont-capture-to-reactive-reset\n (fn (kont)\n (define scan\n (fn (k captured)\n (if (empty? k)\n (error \"reactive deref without enclosing reactive-reset\")\n (let ((frame (first k)))\n (if (= (frame-type frame) \"reactive-reset\")\n (list captured frame (rest k))\n (scan (rest k) (append captured (list frame))))))))\n (scan kont (list))))"
|
||||
"lisp"))))
|
||||
|
||||
;; =====================================================================
|
||||
;; Step 3: Make deref a CEK Special Form
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Step 3: Make deref a CEK Special Form" :id "step-3"
|
||||
|
||||
(p "When " (code "deref") " encounters a signal inside a " (code "reactive-reset")
|
||||
", perform shift.")
|
||||
|
||||
(~docs/subsection :title "3a. Add to special form dispatch in cek.sx"
|
||||
(p "In the dispatch table (around where " (code "reset") " and " (code "shift") " are):")
|
||||
(~docs/code :code (highlight
|
||||
"(= name \"deref\") (step-sf-deref args env kont)"
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "3b. step-sf-deref"
|
||||
(p "Evaluates the argument first (push DerefFrame), then decides whether to shift:")
|
||||
(~docs/code :code (highlight
|
||||
"(define step-sf-deref\n (fn (args env kont)\n (make-cek-state\n (first args) env\n (kont-push (make-deref-frame env) kont))))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "3c. Handle DerefFrame in step-continue"
|
||||
(p "When the deref argument is evaluated, decide: shift or return.")
|
||||
(~docs/code :code (highlight
|
||||
"(= ft \"deref\")\n (let ((val value)\n (fenv (get frame \"env\")))\n (if (not (signal? val))\n ;; Not a signal: pass through\n (make-cek-value val fenv rest-k)\n ;; Signal: check for ReactiveResetFrame\n (if (has-reactive-reset-frame? rest-k)\n ;; Perform reactive shift\n (reactive-shift-deref val fenv rest-k)\n ;; No reactive-reset: normal deref (scope-based tracking)\n (do\n (let ((ctx (context \"sx-reactive\" nil)))\n (when ctx\n (let ((dep-list (get ctx \"deps\"))\n (notify-fn (get ctx \"notify\")))\n (when (not (contains? dep-list val))\n (append! dep-list val)\n (signal-add-sub! val notify-fn)))))\n (make-cek-value (signal-value val) fenv rest-k)))))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "3d. reactive-shift-deref — the heart"
|
||||
(~docs/code :code (highlight
|
||||
"(define reactive-shift-deref\n (fn (sig env kont)\n (let ((scan-result (kont-capture-to-reactive-reset kont))\n (captured-frames (first scan-result))\n (reset-frame (nth scan-result 1))\n (remaining-kont (nth scan-result 2))\n (update-fn (get reset-frame \"update-fn\")))\n ;; Sub-scope for nested subscriber cleanup on re-invocation\n (let ((sub-disposers (list)))\n (let ((subscriber\n (fn ()\n ;; Dispose previous nested subscribers\n (for-each (fn (d) (invoke d)) sub-disposers)\n (set! sub-disposers (list))\n ;; Re-invoke: push fresh ReactiveResetFrame (first-render=false)\n (let ((new-reset (make-reactive-reset-frame env update-fn false))\n (new-kont (concat captured-frames\n (list new-reset)\n remaining-kont)))\n (with-island-scope\n (fn (d) (append! sub-disposers d))\n (fn ()\n (cek-run\n (make-cek-value (signal-value sig) env new-kont))))))))\n ;; Register subscriber\n (signal-add-sub! sig subscriber)\n ;; Register cleanup with island scope\n (register-in-scope\n (fn ()\n (signal-remove-sub! sig subscriber)\n (for-each (fn (d) (invoke d)) sub-disposers)))\n ;; Return current value for initial render\n (make-cek-value (signal-value sig) env remaining-kont))))))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "3e. Handle ReactiveResetFrame in step-continue"
|
||||
(p "When expression completes normally (or after re-invocation):")
|
||||
(~docs/code :code (highlight
|
||||
"(= ft \"reactive-reset\")\n (let ((update-fn (get frame \"update-fn\"))\n (first? (get frame \"first-render\")))\n ;; On re-render (not first), call update-fn with new value\n (when (and update-fn (not first?))\n (invoke update-fn value))\n (make-cek-value value env rest-k))"
|
||||
"lisp"))
|
||||
(p (strong "Key:") " On first render, update-fn is NOT called — the value flows back to the caller "
|
||||
"who inserts it into the DOM. On re-render (subscriber fires), update-fn IS called "
|
||||
"to mutate the existing DOM.")))
|
||||
|
||||
;; =====================================================================
|
||||
;; Step 4: Integrate into adapter-dom.sx
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Step 4: Integrate into adapter-dom.sx" :id "step-4"
|
||||
|
||||
(p "Add CEK reactive path alongside existing effect-based path, controlled by opt-in flag.")
|
||||
|
||||
(~docs/subsection :title "4a. Opt-in flag"
|
||||
(~docs/code :code (highlight
|
||||
"(define *use-cek-reactive* false)\n(define enable-cek-reactive! (fn () (set! *use-cek-reactive* true)))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "4b. CEK reactive attribute binding"
|
||||
(~docs/code :code (highlight
|
||||
"(define cek-reactive-attr\n (fn (el attr-name expr env)\n (let ((update-fn (fn (val)\n (cond\n (or (nil? val) (= val false)) (dom-remove-attr el attr-name)\n (= val true) (dom-set-attr el attr-name \"\")\n :else (dom-set-attr el attr-name (str val))))))\n ;; Mark for morph protection\n (let ((existing (or (dom-get-attr el \"data-sx-reactive-attrs\") \"\"))\n (updated (if (empty? existing) attr-name (str existing \",\" attr-name))))\n (dom-set-attr el \"data-sx-reactive-attrs\" updated))\n ;; Initial render via CEK with ReactiveResetFrame\n (let ((initial (cek-run\n (make-cek-state expr env\n (list (make-reactive-reset-frame env update-fn true))))))\n (invoke update-fn initial)))))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "4c. Modify render-dom-element dispatch"
|
||||
(p "In attribute processing, add conditional:")
|
||||
(~docs/code :code (highlight
|
||||
"(context \"sx-island-scope\" nil)\n (if *use-cek-reactive*\n (cek-reactive-attr el attr-name attr-expr env)\n (reactive-attr el attr-name\n (fn () (trampoline (eval-expr attr-expr env)))))"
|
||||
"lisp"))
|
||||
(p "Similarly for text positions and conditional rendering."))
|
||||
|
||||
(~docs/subsection :title "4d. CEK reactive text"
|
||||
(~docs/code :code (highlight
|
||||
"(define cek-reactive-text\n (fn (expr env)\n (let ((node (create-text-node \"\"))\n (update-fn (fn (val)\n (dom-set-text-content node (str val)))))\n (let ((initial (cek-run\n (make-cek-state expr env\n (list (make-reactive-reset-frame env update-fn true))))))\n (dom-set-text-content node (str initial))\n node))))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/subsection :title "4e. What stays unchanged"
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (code "reactive-list") " — keyed reconciliation is complex; keep effect-based for now")
|
||||
(li (code "reactive-spread") " — spread tracking is complex; keep effect-based")
|
||||
(li (code "effect") ", " (code "computed") " — still needed for non-rendering side effects")
|
||||
(li "Existing " (code "reactive-*") " functions — remain as default path"))))
|
||||
|
||||
;; =====================================================================
|
||||
;; Step 5: Tests
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Step 5: Tests" :id "step-5"
|
||||
|
||||
(~docs/subsection :title "5a. test-cek-reactive.sx"
|
||||
(p "Tests:")
|
||||
(ol :class "list-decimal pl-6 mb-4 space-y-1"
|
||||
(li (code "deref") " non-signal passes through (no shift)")
|
||||
(li (code "deref") " signal without reactive-reset: returns value, no subscription")
|
||||
(li (code "deref") " signal with reactive-reset: shifts, registers subscriber, update-fn called on change")
|
||||
(li "Expression with deref: " (code "(str \"hello \" (deref sig))") " — continuation captures rest")
|
||||
(li "Multi-deref: both signals create subscribers, both fire correctly")
|
||||
(li "Disposal: removing island scope unsubscribes all continuations")
|
||||
(li "Stale subscriber cleanup: re-invocation disposes nested subscribers")))
|
||||
|
||||
(~docs/subsection :title "5b. run_cek_reactive_tests.py"
|
||||
(p "Mirrors " (code "run_cek_tests.py") ". "
|
||||
"Loads frames.sx, cek.sx, signals.sx, runs test-cek-reactive.sx.")))
|
||||
|
||||
;; =====================================================================
|
||||
;; Step 6: Browser Demo
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Step 6: Browser Demo" :id "step-6"
|
||||
(p "Demo showing:")
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li "Counter island with implicit reactivity (no explicit effects)")
|
||||
(li (code "(deref counter)") " in text position auto-updates")
|
||||
(li (code "(str \"count-\" (deref class-sig))") " in attr position auto-updates")
|
||||
(li "Side-by-side comparison: effect-based vs continuation-based code")))
|
||||
|
||||
;; =====================================================================
|
||||
;; Multi-Deref Handling
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Multi-Deref Handling" :id "multi-deref"
|
||||
|
||||
(~docs/code :code (highlight "(str (deref first-name) \" \" (deref last-name))" "lisp"))
|
||||
|
||||
(ol :class "list-decimal pl-6 mb-6 space-y-3"
|
||||
(li (strong "Initial render:") " First " (code "deref") " hits signal → shifts, captures "
|
||||
(code "(str [HOLE] \" \" (deref last-name))") ". Subscriber registered for "
|
||||
(code "first-name") ". Returns current value. Second " (code "deref")
|
||||
" runs (no ReactiveResetFrame between it and the already-consumed one) — "
|
||||
"falls through to normal scope-based tracking.")
|
||||
(li (strong "first-name changes:") " Subscriber fires → re-pushes ReactiveResetFrame → "
|
||||
"re-invokes continuation with new first-name value → second " (code "deref")
|
||||
" hits ReactiveResetFrame again → shifts, creates NEW subscriber for "
|
||||
(code "last-name") ". Old last-name subscriber cleaned up via sub-scope disposal.")
|
||||
(li (strong "last-name changes:") " Its subscriber fires → re-invokes inner continuation → "
|
||||
"update-fn called with new result."))
|
||||
|
||||
(p "This creates O(n) nested continuations for n derefs. Fine for small reactive expressions."))
|
||||
|
||||
;; =====================================================================
|
||||
;; Commit Strategy
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Commit Strategy" :id "commits"
|
||||
|
||||
(ol :class "list-decimal pl-6 mb-4 space-y-1"
|
||||
(li (strong "Commit 1:") " Bootstrap CEK to JS (Step 1) — mechanical, independent")
|
||||
(li (strong "Commit 2:") " ReactiveResetFrame + DerefFrame (Step 2) — new frame types")
|
||||
(li (strong "Commit 3:") " Deref-as-shift + adapter integration + tests (Steps 3-5) — the core change")
|
||||
(li (strong "Commit 4:") " Browser demo (Step 6)")))
|
||||
|
||||
;; =====================================================================
|
||||
;; Files
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Files" :id "files"
|
||||
|
||||
(div :class "overflow-x-auto mb-6"
|
||||
(table :class "min-w-full text-sm"
|
||||
(thead (tr
|
||||
(th :class "text-left pr-4 pb-2 font-semibold" "File")
|
||||
(th :class "text-left pb-2 font-semibold" "Change")))
|
||||
(tbody
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/platform_js.py")
|
||||
(td "SPEC_MODULES entries, PLATFORM_CEK_JS, CEK_FIXUPS_JS, SPEC_MODULE_ORDER"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/run_js_sx.py")
|
||||
(td "compile_ref_to_js: has_cek, auto-inclusion, ordering, platform code"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/js.sx")
|
||||
(td "RENAMES for CEK predicate functions"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/bootstrap_py.py")
|
||||
(td "RENAMES for CEK predicates"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/frames.sx")
|
||||
(td "ReactiveResetFrame, DerefFrame, has-reactive-reset-frame?, kont-capture-to-reactive-reset"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/cek.sx")
|
||||
(td "step-sf-deref, reactive-shift-deref, deref in dispatch, ReactiveResetFrame in step-continue"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/adapter-dom.sx")
|
||||
(td "*use-cek-reactive* flag, cek-reactive-attr, cek-reactive-text, conditional dispatch"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/test-cek-reactive.sx")
|
||||
(td (strong "New:") " continuation-based reactivity tests"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/run_cek_reactive_tests.py")
|
||||
(td (strong "New:") " Python test runner"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/sx/ref/sx_ref.py")
|
||||
(td "Rebootstrap (generated)"))
|
||||
(tr (td :class "pr-4 py-1 font-mono text-xs" "shared/static/scripts/sx-browser.js")
|
||||
(td "Rebootstrap (generated)"))))))
|
||||
|
||||
;; =====================================================================
|
||||
;; Risks
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Risks" :id "risks"
|
||||
|
||||
(ol :class "list-decimal pl-6 mb-4 space-y-2"
|
||||
(li (strong "Performance:") " CEK allocates a dict per step. Mitigated: opt-in flag, tree-walk remains default.")
|
||||
(li (strong "Multi-deref stale subscribers:") " Mitigated: sub-scope disposal before re-invocation.")
|
||||
(li (strong "Interaction with user shift/reset:") " " (code "kont-capture-to-reactive-reset")
|
||||
" only scans for " (code "\"reactive-reset\"") ", not " (code "\"reset\"") ". Orthogonal.")
|
||||
(li (strong "JS bootstrapper complexity:") " ~10 RENAMES for predicates. Default mangling handles the rest.")))))
|
||||
@@ -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))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user