Platform:
- sx-platform.js: extract ?v= query from script tag URL, append to
all .sx file XHR requests. Prevents stale cached .sx files.
Stepper performance:
- do-back: use rebuild-preview (pure SX→DOM render) instead of
replaying every do-step from 0. O(1) instead of O(n).
- Hydration effect: same rebuild-preview instead of step replay.
- Cookie save moved from do-step to button on-click handlers only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clicks on island elements before hydration completes are captured
and replayed after boot finishes:
- shell.sx: inline script (capture phase) buffers clicks on
[data-sx-island] elements that aren't hydrated yet into window._sxQ
- boot.sx: after hydration + process-elements, replays buffered clicks
by calling target.click() on elements still connected to the DOM
This makes SSR islands feel interactive immediately — the user can
click a button while the SX kernel is still loading/hydrating, and
the action fires as soon as the handler is wired up.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add wasm_hash (MD5 of sx_browser.bc.js) to shell template
- Script tags: /wasm/sx_browser.bc.js?v={hash}, /wasm/sx-platform.js?v={hash}
- Pass wasm_hash through helpers.py and ocaml_bridge.py
- Fix missing close paren in bind-sse-swap (broke SX parsing)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The DOM adapter treated all Dict values as empty (create-fragment).
But DOM nodes (text nodes, elements) from create-text-node/host-call
are wrapped as Dict { __host_handle: N } by js_of_ocaml. Now checks
for __host_handle and passes them through as DOM nodes.
Fixes stopwatch button text ("Start") and timer display ("0.0s")
which were missing because create-text-node results were discarded.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Navigation pipeline now works end-to-end:
- outerHTML swap uses dom-replace-child instead of morph-node (morph has
a CEK continuation issue with nested for-each that needs separate fix)
- swap-dom-nodes returns the new element for outerHTML so post-swap
hydrates the correct (new) DOM, not the detached old element
- sx-render uses marker mode: islands rendered as empty span[data-sx-island]
markers, hydrated by post-swap. Prevents duplicate content from island
body expansion + SX response nav rows.
- dispose-island (singular) called on old island before morph, not just
dispose-islands-in (which only disposes sub-islands)
OCaml runtime:
- safe_eq: Dict equality checks __host_handle for DOM node identity
(js_to_value creates new Dict wrappers per call, breaking physical ==)
- contains?: same host handle check
- to_string: trampoline thunks (fixes <thunk> display)
- as_number: trampoline thunks (fixes arithmetic on leaked thunks)
DOM platform:
- dom-remove, dom-attr-list (name/value pairs), dom-child-list (SX list),
dom-is-active-element?, dom-is-input-element?, dom-is-child-of?, dom-on
All 5 reactive-nav Playwright tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three issues in the stepper island's client-side rendering:
1. do-step used eval-expr with empty env for ~cssx/tw spreads — component
not found, result leaked as [object Object]. Fixed: call ~cssx/tw
directly (in scope from island env) with trampoline.
2. steps-to-preview excluded spreads — SSR preview had no styling.
Fixed: include spreads in the tree so both SSR and client render
with CSSX classes.
3. build-children used named let (let loop ...) which produces
unresolved Thunks in render mode due to the named-let compiler
desugaring interacting with the render/eval boundary. Fixed:
rewrote as plain recursive function bc-loop avoiding named let.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
spec/ now contains only the language definition (5 files):
evaluator.sx, parser.sx, primitives.sx, render.sx, special-forms.sx
lib/ contains code written IN the language (8 files):
stdlib.sx, types.sx, freeze.sx, content.sx,
bytecode.sx, compiler.sx, vm.sx, callcc.sx
Test files follow source: spec/tests/ for core language tests,
lib/tests/ for library tests (continuations, freeze, types, vm).
Updated all consumers:
- JS/Python/OCaml bootstrappers: added lib/ to source search paths
- OCaml bridge: spec_dir for parser/render, lib_dir for compiler/freeze
- JS test runner: scans spec/tests/ (always) + lib/tests/ (--full)
- OCaml test runner: scans spec/tests/, lib tests via explicit request
- Docker dev mounts: added ./lib:/app/lib:ro
Tests: 1041 JS standard, 1322 JS full, 1101 OCaml — all pass
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three bugs broke island SSR rendering of the home stepper widget:
1. Inline VM opcodes (OP_ADD..OP_DEC) broke JIT-compiled functions.
The compiler emitted single-byte opcodes for first/rest/len/= etc.
that produced wrong results in complex recursive code (sx-parse
returned nil, split-tag produced 1 step instead of 16). Reverted
compiler to use CALL_PRIM for all primitives. VM opcode handlers
kept for future use.
2. Named let (let loop ((x init)) body) had no compiler support —
silently produced broken bytecode. Added desugaring to letrec.
3. URL-encoded cookie values not decoded server-side. Client set-cookie
uses encodeURIComponent but Werkzeug doesn't decode cookie values.
Added unquote() in bridge cookie injection.
Also: call-lambda used eval_expr which copies Dict values (signals),
breaking mutations through aser lambda calls. Switched to cek_call.
Also: stepper preview now includes ~cssx/tw spreads for SSR styling.
Tests: 1317 JS, 1114 OCaml, 26 integration (2 pre-existing failures)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
16 new opcodes (160-175) bypass the CALL_PRIM hashtable lookup for
the most frequently called primitives:
Arithmetic: OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_INC, OP_DEC, OP_NEG
Comparison: OP_EQ, OP_LT, OP_GT, OP_NOT
Collection: OP_LEN, OP_FIRST, OP_REST, OP_NTH, OP_CONS
The compiler (compiler.sx) recognizes these names at compile time and
emits the inline opcode instead of CALL_PRIM. The opcode is self-
contained — no constant pool index, no argc byte. Each primitive is
a single byte in the bytecode stream.
Implementation in all three VMs:
- OCaml (sx_vm.ml): direct pattern match, no allocation
- SX spec (vm.sx): delegates to existing primitives
- JS (transpiled): same as SX spec
66 new tests in spec/tests/vm-inline.sx covering arithmetic, comparison,
collection ops, composition, and edge cases.
Tests: 1314 JS (full), 1114 OCaml, 32 Playwright
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
dom-on wraps Lambda event handlers in JS functions that call callLambda.
callLambda returns a Thunk (TCO), but the wrapper never trampolined it,
so the handler body (swap!, set!, etc.) never executed. Buttons rendered
but clicks had no effect.
Fix: wrap callLambda result in trampoline() so thunks resolve and
side effects (signal mutations, DOM updates) execute.
Also use call-lambda instead of direct invocation for Lambda objects
(Lambda is a plain JS object, not callable as a function).
All 100 Playwright tests pass:
- 6 isomorphic SSR
- 5 reactive navigation (cross-demo)
- 61 geography page loads
- 7 handler response rendering
- 21 demo interaction + health checks
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When handler bodies use (let ((rows (map ...))) (<> rows)), the let
binding evaluates the map via CEK, which converts :class keywords to
"class" strings. The aser fragment serializer then outputs "class" as
text content instead of :class as an HTML attribute.
Fix: add aser-reserialize function that detects string pairs in
evaluated element lists where the first string matches known HTML
attribute names (class, id, sx-*, data-*, style, href, src, type,
name, value, etc.) and restores them as :keyword syntax.
All 7 handler response tests now pass:
- bulk-update, delete-row, click-to-load, active-search
- form-submission, edit-row, tabs
Total Playwright: 79/79
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The transpiler treated any list with a non-symbol head as a data list,
emitting [head, args] as a JS array literal. When head is a sub-expression
(another call), it should emit (head)(args) — a function call.
This fixes the custom special forms dispatch in transpiled code:
Before: [get(_customSpecialForms, name), args, env] (array — broken)
After: (get(_customSpecialForms, name))(args, env) (call — correct)
Also fixes IIFE patterns: ((fn (x) body) arg) now emits
(function(x) { ... })(arg) instead of [function(x){...}, arg]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When morphing DOM after server fetch, the morph engine reuses elements
with the same tag. If old element was island A and new is island B,
syncAttrs updates data-sx-island but the JS property _sxBoundisland-hydrated
persists on the reused element. sx-hydrate-islands then skips it.
Fix: in morphNode, when data-sx-island attribute changes between old and
new elements, dispose the old island's signals and clear the hydration
flag so the new island gets properly hydrated.
New Playwright tests:
- counter → temperature navigation: temperature signals work
- temperature → counter navigation: counter signals work
- Direct load verification for both islands
- No JS errors during navigation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Route all rendering through OCaml bridge — render_to_html no longer uses
Python async_eval. Fix register_components to parse &key params and &rest
children from defcomp forms. Remove all dead sx_ref.py imports.
Epoch protocol (prevents pipe desync):
- Every command prefixed with (epoch N), all responses tagged with epoch
- Both sides discard stale-epoch messages — desync structurally impossible
- OCaml main loop discards stale io-responses between commands
Consolidate scope primitives into sx_scope.ml:
- Single source of truth for scope-push!/pop!/peek, collect!/collected,
emit!/emitted, context, and 12 other scope operations
- Removes duplicate registrations from sx_server.ml (including bugs where
scope-emit! and clear-collected! were registered twice with different impls)
- Bind scope prims into env so JIT VM finds them via OP_GLOBAL_GET
JIT VM fixes:
- Trampoline thunks before passing args to CALL_PRIM
- as_list resolves thunks via _sx_trampoline_fn
- len handles all value types (Bool, Number, RawHTML, SxExpr, Spread, etc.)
Other fixes:
- ~cssx/tw signature: (tokens) → (&key tokens) to match callers
- Minimal Python evaluator in html.py for sync sx() Jinja function
- Python scope primitive stubs (thread-local) for non-OCaml paths
- Reader macro resolution via OcamlSync instead of sx_ref.py
Tests: 1114 OCaml, 1078 JS, 35 Python regression, 6/6 Playwright SSR
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
jinja_bridge.py was creating Component objects for both defcomp AND
defisland forms. Islands need Island objects so the serializer emits
defisland (not defcomp) in the client component bundle. Without this,
client-side islands don't get data-sx-island attributes, hydration
fails, and all reactive signals (colour cycling, stepper) stop working.
Add Playwright test: islands hydrate, stepper buttons update count,
reactive colour cycling works on click.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes the 5993-line bootstrapped Python evaluator (sx_ref.py) and all
code that depended on it exclusively. Both bootstrappers (JS + OCaml)
now use a new synchronous OCaml bridge (ocaml_sync.py) to run the
transpiler. JS build produces identical output; OCaml bootstrap produces
byte-identical sx_ref.ml.
Key changes:
- New shared/sx/ocaml_sync.py: sync subprocess bridge to sx_server.exe
- hosts/javascript/bootstrap.py: serialize defines → temp file → OCaml eval
- hosts/ocaml/bootstrap.py: same pattern for OCaml transpiler
- shared/sx/{html,async_eval,resolver,jinja_bridge,handlers,pages,deps,helpers}:
stub or remove sx_ref imports; runtime uses OCaml bridge (SX_USE_OCAML=1)
- sx/sxc/pages: parse defpage/defhandler from AST instead of Python eval
- hosts/ocaml/lib/sx_primitives.ml: append handles non-list 2nd arg per spec
- Deleted: sx_ref.py, async_eval_ref.py, 6 Python test runners, misc ref/ files
Test results: JS 1078/1078, OCaml 1114/1114.
sx_docs SSR has pre-existing rendering issues to investigate separately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
run-tests.sh runs all suites: JS (standard + full), Python, OCaml,
Playwright (isomorphic + demos). deploy.sh calls it as gate.
Register log-info and log-warn as PRIMITIVES so runtime-eval'd SX code
(init-client.sx.txt) can use them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Moved flash-sale, settle-data, search-products/events/posts, and catalog
endpoints from bp/pages/routes.py into sx/sx/handlers/reactive-api.sx.
routes.py now contains only the SSE endpoint (async generators need Python).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These are domain definition forms (same pattern as defhandler, defpage,
etc.), not core language constructs. Moving them to web-forms.sx keeps
the core evaluator + types.sx cleaner for WASM compilation.
web-forms.sx now loaded in both JS and Python build pipelines.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
dom.sx and browser.sx are library source (not transpiled into the bundle),
so their functions need explicit PRIMITIVES registration for runtime-eval'd
SX code (islands, data-init scripts). Restore registrations for all dom/
browser functions used at runtime. Revert bootstrap.py transpilation of
dom-lib/browser-lib which overrode native platform implementations that
have essential runtime integration (cekCall wrapping, post-render hooks).
Add Playwright regression test for [object Object] nav link issue.
Replace console-log calls with log-info in init-client.sx.txt.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
dom-lib and browser-lib were listed in ADAPTER_FILES but never actually
transpiled — their functions only existed as native PLATFORM_*_JS code.
Add them to the build loop so the FFI library wrappers are compiled.
Add hostCall/hostGet/etc. variable aliases for transpiled code, and
console-log to browser.sx for runtime-eval'd SX code.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The set!-based approach (nested when + mutate + re-check) didn't work
because CEK evaluates the outer when condition once. Replace with a
single (when (and should-fire (not modifier-click?)) ...) guard.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the fragile pattern of capturing and wrapping definition-form?
with a mutable *definition-form-extensions* list in render.sx. Web
modules append names to this list instead of redefining the function.
Survives spec reloads without losing registrations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move defhandler/defquery/defaction/defpage/defrelation from hardcoded
evaluator dispatch to web/web-forms.sx extension module, registered via
register-special-form!. Adapters updated to use definition-form? and
dynamically extended form-name lists.
Fix modifier-key clicks (ctrl-click → new tab) in three click handlers:
bindBoostLink, bindClientRouteClick, and orchestration.sx bind-event.
Add event-modifier-key? primitive (eventModifierKey_p for transpiler).
Fix CSSX SSR: ~cssx/flush no longer drains the collected bucket on the
server, so the shell template correctly emits CSSX rules in <head>.
Add missing server-side DOM stubs (create-text-node, dom-append, etc.)
and SSR passthrough for portal/error-boundary/promise-delayed.
Passive event listeners for touch/wheel/scroll to fix touchpad scrolling.
97/97 Playwright demo tests + 4/4 isomorphic SSR tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
steps-to-preview is a pure recursive descent function inside the island's
letrec that builds an SX expression tree from steps[0..target-1].
The preview lake uses it to show partial text (e.g. "the joy of " at step 9).
Still WIP: stepper island doesn't SSR because DOM-only code (effect,
dom-query, dom-create-element) runs in the island body and fails on server.
Need to guard client-only code so SSR can render the pure parts
(code view, counter, preview).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
get-cookie / set-cookie primitives on both server and client:
- JS: reads/writes document.cookie
- OCaml: get-cookie reads from _request_cookies hashtable,
set-cookie is no-op (server sets cookies via HTTP headers)
- Python bridge: _inject_request_cookies_locked() sends
(set-request-cookies {:name "val"}) to kernel before page render
Stepper island (home-stepper.sx):
- Persistence switched from localStorage to cookie (sx-home-stepper)
- freeze-scope/thaw-from-sx mechanism preserved, just different storage
- Server reads cookie → thaw restores step-idx → SSR renders correct step
- Code highlighting: removed imperative code-spans/build-code-dom/
update-code-highlight; replaced with live DOM query that survives morphs
- Removed code-view lake wrapper (now plain reactive DOM)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CSSX persistence after SPA navigation is a client-side issue.
The boot.sx flush added collected-rules-to-head after island hydration,
but this may interfere with the morph/reactive rendering pipeline.
The client-side CSSX persistence fix needs to work with the DOM adapter's
scope mechanism (CEK frames), not the hashtable-based scope-emit!/scope-emitted
used by the server adapter. WASM will unify these — same evaluator on both sides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Register hashtable-based scope accessors that bypass the CEK special form
dispatch, for use by adapter-html.sx and shell templates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root causes of missing CSSX classes in SSR:
1. _sx_trampoline_fn in sx_primitives.ml was never wired — call_any in
HO forms (map/filter/for-each) returned unresolved Thunks, so callbacks
like render-lambda-html's param binding never executed. Fixed in
bootstrap.py FIXUPS: wire Sx_primitives._sx_trampoline_fn after eval_expr.
2. adapter-html.sx used (emit! ...) and (emitted ...) which are CEK special
forms (walk kont for ScopeAccFrame), but scope-push!/scope-pop! use the
hashtable. CEK frames and hashtable are two different scope systems.
Fixed: adapter uses scope-emit!/scope-emitted (hashtable primitives).
3. env-* operations (env-has?, env-get, env-bind!, env-set!, env-extend,
env-merge) only accepted Env type. adapter-html.sx passes Dict as env.
Fixed: all env ops go through unwrap_env which handles Dict/Nil.
Also: fix merge conflict in sx/sx/geography/index.sx, remove duplicate
scope primitives from sx_primitives.ml (sx_server.ml registers them).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
JIT allowlist (sx_server.ml):
- Replace try-every-lambda strategy with StringSet allowlist. Only
functions in the list get JIT compiled (compiler, parser, pure transforms).
Render functions that need dynamic scope skip JIT entirely — no retry
overhead, no silent fallbacks.
- Add (jit-allow name) command for dynamic expansion from Python bridge.
- JIT failures log once with "[jit] DISABLED fn — reason" then go silent.
Standalone --test mode (sx_server.ml):
- New --test flag loads full env (spec + adapters + compiler + signals),
supports --eval and --load flags. Quick kernel testing without Docker.
Example: dune exec bin/sx_server.exe -- --test --eval '(len HTML_TAGS)'
Integration tests (integration_tests.ml):
- New binary exercising the full rendering pipeline: loads spec + adapters
into a server-like env, renders HTML via both native and SX adapter paths.
- 26 tests: HTML tags, special forms (when/if/let), letrec with side
effects, component rendering, eval-expr with HTML tag functions.
- Would have caught the "Undefined symbol: div/lake/init" issues from
the previous commit immediately without Docker.
VM cleanup (sx_vm.ml):
- Remove temporary debug logging (insn counter, call_closure counter,
VmClosure depth tracking) added during debugging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Core VM changes:
- Add VmClosure value variant — inner closures created by OP_CLOSURE are
first-class VM values, not NativeFn wrappers around call_closure
- Convert `run` from recursive to while-loop — zero OCaml stack growth,
true TCO for VmClosure tail calls
- vm_call handles VmClosure by pushing frame on current VM (no new VM
allocation per call)
- Forward ref _vm_call_closure_ref for cross-boundary calls (CEK/primitives)
Compiler (spec/compiler.sx):
- Define hoisting in compile-begin: pre-allocate local slots for all
define forms before compiling any values. Fixes forward references
between inner functions (e.g. read-expr referencing skip-ws in sx-parse)
- scope-define-local made idempotent (skip if slot already exists)
Server (sx_server.ml):
- JIT fail-once sentinel: mark l_compiled as failed after first VM runtime
error. Eliminates thousands of retry attempts per page render.
- HTML tag bindings: register all HTML tags as pass-through NativeFns so
eval-expr can handle (div ...) etc. in island component bodies.
- Log VM FAIL errors with function name before disabling JIT.
SSR fixes:
- adapter-html.sx letrec handler: evaluate bindings in proper letrec scope
(pre-bind nil, then evaluate), render body with render-to-html instead of
eval-expr. Fixes island SSR for components using letrec.
- Add `init` primitive to OCaml kernel (all-but-last of list).
- VmClosure handling in sx_runtime.ml sx_call dispatch.
Tests: 971/971 OCaml (+19 new), 0 failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
vm_closure now stores the original closure env (vm_closure_env).
GLOBAL_GET walks the closure env chain when the variable isn't in
vm.globals. GLOBAL_SET writes to the correct env in the chain.
This enables JIT compilation of all named functions regardless of
closure depth. No more closure skip check needed.
Pre-compile time back to ~7s (was 37s with closure skip).
Note: sx-parse sibling list parsing still has issues — the root
cause is in how the JIT-compiled letrec + OP_CLOSURE interacts
with the upvalue cell mechanism. Investigation ongoing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: sf-letrec returns a thunk (for TCO) but the CEK dispatch
wrapped it as a value without evaluating. The thunk leaked as the
return value of letrec expressions, breaking sx-parse and any function
using letrec.
Fix: step-sf-letrec unwraps the thunk into a CEK state, so the last
letrec body expression is properly evaluated by the CEK machine.
Also:
- compile-letrec: two-phase (nil-init then assign) for mutual recursion
- Skip JIT for inner functions (closure.bindings != globals) in both
vm_call and JIT hook
- vm-reset-fn for sx-parse removed (no longer needed)
- Parser regression test: letrec with mutable pos + recursive sublists
Test results: JS 943/17, OCaml 955/0, Python 747/0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The closure check was only in vm_call (sx_vm.ml) but inner functions
like read-list-loop were also compiled through the JIT hook in
sx_server.ml. The hook compiled them with closure merging, producing
incorrect bytecode (read-list-loop mishandled closing parens).
Added the same closure check to the JIT hook: skip lambdas with
non-empty closures. Now sx-parse works correctly:
(a (b) (c)) → 3 siblings, not (a (b (c)))
Pre-compiled count increased from 17 to 33 — more top-level
functions compiled (inner ones correctly skipped to CEK).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests: parse "(a (b) (c))" must produce 3 siblings, not nested.
Catches JIT compilation bug where closing parens cause sibling
lists to become children.
Reset sx-parse to CEK on the OCaml kernel — the JIT-compiled
version of sx-parse's complex letrec produces wrong bytecode.
CEK interpretation works correctly (tests pass on all platforms).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The compiler was treating letrec as let — binding values sequentially.
This meant mutually recursive functions (like sx-parse's read-list
calling read-expr and vice versa) couldn't reference each other.
compile-letrec uses two phases:
1. Define all local slots initialized to nil
2. Compile and assign values — all names already in scope
This fixes sx-parse producing wrong ASTs (nested instead of sibling
lists) when JIT-compiled, which caused the stepper's step count to
be 2 instead of 16.
Also: skip JIT for lambdas with closure bindings (inner functions
like read-list-loop) — the closure merging into vm_env_ref produces
incorrect variable resolution.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The closure merging in jit_compile_lambda (copying globals + injecting
closure bindings into vm_env_ref) produces incorrect variable resolution
for inner functions. Symptoms: sx-parse's read-list-loop mishandles
closing parens (siblings become children), parser produces wrong ASTs.
Fix: vm_call skips JIT compilation for lambdas with non-empty closures.
These run on CEK which handles closures correctly. Top-level defines
(empty closure) are still JIT-compiled.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
~cssx/flush now appends rules to <style id="sx-css"> in <head>
instead of creating ephemeral inline <style> tags that get morphed
away during SPA navigation. Rules accumulate across navigations.
Future: reference-count rules and remove when no elements use them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ocaml branch introduced force-dispose-islands-in for outerHTML
swaps, which destroyed hydrated islands (including their live
signals). This broke the core hypermedia+reactive pattern: the
header island's colour state was lost on navigation, and lakes
weren't being morph-updated.
Reverted to production behaviour: dispose-islands-in skips hydrated
islands. The morph algorithm then preserves them (matching by
data-sx-island name) and only morphs their lake content.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CSSX colour resolution failure was NOT a JIT compiler bug.
CALL_PRIM looks up primitives table (not env), and parse-int in
the primitives table only handled 1-arg calls. The 2-arg form
(parse-int "699" nil) returned Nil, causing cssx-resolve's colour
branch to fail its and-condition.
Fix: update Sx_primitives.register "parse-int" with same 2-arg
handling as the env binding. Remove the vm-reset-fn workaround.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cssx-resolve has a complex cond with nested and conditions that the
JIT compiler miscompiles — the colour branch is skipped even when
all conditions are true. Reset to jit_failed_sentinel after loading
so it runs on CEK (which evaluates correctly).
Added vm-reset-fn kernel command for targeted JIT bypass.
All CSSX colour tokens now generate rules: text-violet-699,
text-stone-500, bg-stone-50, etc.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sx-mount now checks if the target element has children (server-
rendered HTML). If so, skips the client re-render and only runs
hydration (process-elements, hydrate-islands, hydrate-elements).
This preserves server-rendered CSSX styling and avoids the flash
of unstyled content that occurred when the client replaced the
server HTML before re-rendering.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
letrec in adapter-html.sx: evaluate via CEK (which handles mutual
recursion and returns a thunk), then render-value-to-html unwraps
the thunk and renders the expression with the letrec's local env.
Both islands (~layouts/header and ~home/stepper) now render
server-side with hydration markers and CSS classes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- trampoline resolves Thunk values (sf-letrec returns them for TCO)
- render-to-html handles "thunk" type by unwrapping expr+env
- effect overridden to no-op after loading signals.sx (prevents
reactive loops during SSR — effects are DOM side-effects)
- Added thunk?/thunk-expr/thunk-env primitives
- Added DOM API stubs for SSR (dom-query, schedule-idle, etc.)
Header island renders fully with styling. Stepper island still
fails SSR (letrec + complex body hits "Undefined symbol: div"
in eval path — render mode not active during CEK letrec eval).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added (~cssx/flush) to shell after sx-root div — picks up CSS rules
generated during island SSR via (collect! "cssx" ...). Registered
clear-collected! primitive for the flush component.
Standard CSSX classes now styled server-side. Custom colour shades
(e.g. text-violet-699) still need investigation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>