Replace raw CEK state serialization with named freeze scopes.
A freeze scope collects signals registered within it. On freeze,
signal values are serialized to SX. On thaw, values are restored.
- freeze-scope: scoped effect delimiter for signal collection
- freeze-signal: register a signal with a name in the current scope
- cek-freeze-scope / cek-thaw-scope: freeze/thaw by scope name
- freeze-to-sx / thaw-from-sx: full SX text round-trip
- cek-freeze-all / cek-thaw-all: batch operations
Also: register boolean?, symbol?, keyword? predicates in both
Python and JS platforms with proper var aliases.
Demo: counter + name input with Freeze/Thaw buttons.
Frozen SX: {:name "demo" :signals {:count 5 :name "world"}}
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents and demonstrates serializable CEK state. Type an expression,
step to any point, click Freeze to see the frozen SX. Click Thaw to
resume from the frozen state and get the result.
- New page at /sx/(geography.(cek.freeze))
- Nav entry under CEK Machine
- Interactive island demo with step/run/freeze/thaw buttons
- Documentation: the idea, freeze format, thaw/resume, what it enables
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Freeze a CEK state to pure s-expressions. Thaw it back to a live
state and resume with cek-run. Full round-trip through SX text works:
freeze → sx-serialize → sx-parse → thaw → resume → same result.
- cek-freeze: serialize control/env/kont/value to SX dicts
- cek-thaw: reconstruct live state from frozen SX
- Native functions serialize as (primitive "name"), looked up on resume
- Lambdas serialize as (lambda (params) body)
- Environments serialize as flat dicts of visible bindings
- Continuation frames serialize as typed dicts
Enables: localStorage persistence, content-addressed computation,
cross-machine migration, time-travel debugging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Home page stepper: reactive code view with syntax colouring where
tokens highlight as you step through DOM construction. Each token
is a span with signal-driven classes — current step bold+violet,
completed steps dimmed, upcoming normal. CSSX styling via ~cssx/tw
spreads. Lake preserves imperative DOM across reactive re-renders.
Also fixes: bare lowercase 'nil' in platform_js.py eventDetail and
domGetData — should be NIL (the SX sentinel object).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cache the style element reference in _cssx-style-el so flush-cssx-to-dom
never creates more than one. Previous code called dom-query on every
flush, which could miss the element during rapid successive calls,
creating duplicates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add css_extras parameter to create_base_app. Legacy apps (blog, market
etc) get the default extras (basics.css, cards.css, blog-content.css,
prism.css, FontAwesome). SX app passes css_extras=[] — it uses CSSX
for styling and custom highlighting, not Prism/FA/Ghost.
Reduces <style id="sx-css"> from ~100KB+ of irrelevant CSS to ~5KB
of Tailwind resets + only the utility rules the page actually uses.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
A defisland that lets users type an SX expression, step through CEK
evaluation one transition at a time, and see C/E/K registers update
live. Demonstrates that cek-step is pure data->data.
- cek.sx geography: add ~geography/cek/demo-stepper island with
source input, step/run/reset buttons, state display, step history
- platform_js.py: register CEK stepping primitives (make-cek-state,
cek-step, cek-terminal?, cek-value, make-env, sx-serialize) so
island code can access them
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- Add `scope` special form to eval.sx: (scope name body...) or
(scope name :value v body...) — general dynamic scope primitive
- `provide` becomes sugar: (provide name value body...) calls scope
- Rename provide-push!/provide-pop! to scope-push!/scope-pop! throughout
all adapters (async, dom, html, sx) and platform implementations
- Update boundary.sx: Tier 5 now "Scoped effects" with scope-push!/
scope-pop! as primary, provide-push!/provide-pop! as aliases
- Add scope form handling to async adapter and aser wire format
- Update sx-browser.js, sx_ref.py (bootstrapped output)
- Add scopes.sx docs page, update provide/spreads/demo docs
- Update nav-data, page-functions, docs page definitions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Spreads now emit their attrs into the nearest element's provide scope
instead of requiring per-child spread? checks at every intermediate
layer. emit! is tolerant (no-op when no provider), so spreads in
non-element contexts silently vanish.
- adapter-html: element/lake/marsh wrap children in provide, collect
emitted; removed 14 spread filters from fragment, forms, components
- adapter-sx: aser wraps result to catch spread values from fn calls;
aser-call uses provide with attr-parts/child-parts ordering
- adapter-async: same pattern for both render and aser paths
- adapter-dom: added emit! in spread dispatch + provide in element
rendering; kept spread? checks for reactive/island and DOM safety
- platform: emit! returns NIL when no provider instead of erroring
- 3 new aser tests: stored spread, nested element, silent drop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Spread values from make-spread were crashing the wire format serializer:
- serialize() had no "spread" case, fell through to (str val) producing
Python repr "<shared.sx.ref.sx_ref._Spread...>" which was treated as
an undefined symbol
- aser-call/async-aser-call didn't handle spread children — now merges
spread attrs as keyword args into the parent element
- aser-fragment/async-aser-fragment didn't filter spreads — now filters
them (fragments have no parent element to merge into)
- serialize() now handles spread type: (make-spread {:key "val"})
Added 3 aser-spreads tests. All 562 tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add \uXXXX unicode escape support to parser.py and parser.sx spec
- Add char-from-code primitive (Python chr(), JS String.fromCharCode())
- Fix variadic infix operators in both bootstrappers (js.sx, py.sx) —
(+ a b c d) was silently dropping terms, now left-folds correctly
- Rebootstrap sx_ref.py and sx-browser.js with all fixes
- Fix 3 pre-existing map-dict test failures in shared/sx/tests/run.py
- Add live demos alongside examples in spreads essay (side-by-side layout)
- Add scoped-effects plan: algebraic effects as unified foundation for
spread/collect/island/lake/signal/context
- Add foundations plan: CEK machine, the computational floor, three-axis
model (depth/topology/linearity), Curry-Howard correspondence
- Route both plans in page-functions.sx and nav-data.sx
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a spread value (e.g. from ~cssx/tw) appears inside an island with
signal-dependent tokens, reactive-spread tracks deps and updates the
element's class/attrs when signals change. Old classes are surgically
removed, new ones appended, and freshly collected CSS rules are flushed
to the live stylesheet. Multiple reactive spreads on one element are safe.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
adapter-dom.sx: if/when/cond reactive paths now check whether
initial-result is a spread. If so, return it directly — spreads
aren't DOM nodes and can't be appended to fragments. This lets
any spread-returning component (like ~cssx/tw) work inside islands
without the spread being silently dropped.
cssx.sx: revert make-spread workaround — the root cause is now
fixed in the adapter. ~cssx/tw can use a natural top-level if.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
deps.sx: scan island bodies for component deps (was only scanning
"component" and "macro", missing "island" type). This ensures
~cssx/tw and its dependencies are sent to the client for islands.
cssx.sx: move if inside make-spread arg so it's evaluated by
eval-expr (no reactive wrapping) instead of render-to-dom which
applies reactive-if inside island scope, converting the spread
into a fragment and losing the class attrs.
Added island dep tests at 3 levels: test-deps.sx (spec),
test_deps.py (Python), test_parity.py (ref vs fallback).
sx-browser.js: temporary debug logging at spread detection points.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Islands render independently on the client, so ~cssx/tw calls
collect!("cssx", rule) but no ~cssx/flush runs. Add flush-cssx-to-dom
in boot.sx that injects collected rules into a persistent <style>
element in <head>.
Called at all lifecycle points: boot-init, sx-mount, resolve-suspense,
post-swap (navigation morph), and swap-rendered-content (client routes).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous fix only guarded domAppend/domInsertAfter, but many
platform JS functions (asyncRenderChildren, asyncRenderElement,
asyncRenderMap, render, sxRenderWithEnv) call appendChild directly.
Add _spread guards to all direct appendChild sites. For async element
rendering, merge spread attrs onto parent (class/style join, others
overwrite) matching the sync adapter behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Spread values (from ~cssx/tw etc.) are attribute dicts, not DOM nodes.
When they appear in non-element contexts (fragments, islands, lakes,
reactive branches), they must not be passed to appendChild/insertBefore.
Add _spread guard to platform domAppend and domInsertAfter — fixes
TypeError: Node.appendChild: Argument 1 does not implement interface Node.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Spread values returned by components like ~cssx/tw are not DOM nodes
and cannot be passed to appendChild. Filter them in fragment, let,
begin/do, component children, and data list rendering paths — matching
the HTML adapter's existing spread filtering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Four new primitives for scoped downward value passing and upward
accumulation through the render tree. Specced in .sx, bootstrapped
to Python and JS across all adapters (eval, html, sx, dom, async).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New SX primitives for child-to-parent communication in the render tree:
- spread type: make-spread, spread?, spread-attrs — child injects attrs
onto parent element (class joins with space, style with semicolon)
- collect!/collected/clear-collected! — render-time accumulation with
dedup into named buckets
~cssx/tw is now a proper defcomp returning a spread value instead of a
macro wrapping children. ~cssx/flush reads collected "cssx" rules and
emits a single <style data-cssx> tag.
All four render adapters (html, async, dom, aser) handle spread values.
Both bootstraps (Python + JS) regenerated. Also fixes length→len in
cssx.sx (length was never a registered primitive).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the three-layer cssx system (macro + value functions + class
components) with a single token resolver. Tokens like "bg-yellow-199",
"hover:bg-rose-500", "md:text-xl" are parsed into CSS declarations.
Two delivery mechanisms, same token format:
- tw() function: returns inline style string for :style
- ~cssx/tw macro: injects JIT class + <style> onto first child element
The resolver handles: colours (21 names, any shade 0-950), spacing,
typography, display, max-width, rounded, opacity, w/h, gap, text
decoration, cursor, overflow, transitions. States (hover/focus/active)
and responsive breakpoints (sm/md/lg/xl/2xl) for class-based usage.
Next step: replace macro/function approach with spec-level primitives
(defcontext/provide/context + spread) so ~cssx/tw becomes a proper
component returning spread values, with rules collected via context.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix O(n²) postprocessing: compute_all_deps/io_refs/hash were called
per-file (92x for sx app). Now deferred to single finalize_components()
call after all files load.
- Add pickle cache in shared/sx/.cache/ keyed by file mtimes+sizes.
Cache stores fully-processed Component/Island/Macro objects with deps,
io_refs, and css_classes pre-computed. Closures stripped before pickle,
rebuilt from global env after restore.
- Smart finalization: cached loads skip deps/io_refs recomputation
(already in pickle), only recompute component hash.
- Fix ~sx-header → ~layouts/header ref in docs-content.sx
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add url-to-expr, auto-quote-unknowns, prepare-url-expr to router.sx —
the canonical URL-to-expression pipeline. Dots→spaces, parse, then
auto-quote unknown symbols as strings (slugs). The same spec serves
both server (Python) and client (JS) route handling.
- router.sx: three new pure functions for URL evaluation
- bootstrap_py.py: auto-include router module with html adapter
- platform_js.py: export urlToExpr/autoQuoteUnknowns/prepareUrlExpr
- sx_router.py: replace hand-written auto_quote_slugs with bootstrapped
prepare_url_expr — delete ~50 lines of hardcoded function name sets
- Rebootstrap sx_ref.py (4331 lines) and sx-browser.js
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>