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>
Reactive island tests (14): counter, temperature, stopwatch, input-binding,
dynamic-class, reactive-list, stores, refs, portal, imperative,
error-boundary, event-bridge, transition, resource
Marshes tests (5): hypermedia-feeds, on-settle, server-signals,
signal-triggers, view-transform
Health checks (2): no JS errors on reactive or marshes pages
Known failures: island signal reactivity broken on first page load
(buttons render but on-click handlers don't attach). Regression from
commits 2d87417/3ae49b6/13ba5ee — needs investigation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
promise-delayed is a browser-only primitive used by the resource island
demo. The SSR renderer needs it as a stub to avoid "Undefined symbol"
errors during render-to-html JIT compilation.
The stub returns the value argument (skipping the delay), so SSR renders
the resolved state immediately.
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>
Replace the manual jit_allowlist (StringSet of ~40 function names) with
universal lazy compilation. Every named lambda gets one compile attempt
on first call; failures are sentineled and never retried.
Compiler internals are still pre-compiled at startup (bootstrapping the
JIT itself), but everything else compiles lazily — no manual curation.
Remove jit-allow command (no longer needed). Remove StringSet module.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
evaluator.sx defines *custom-special-forms* and register-special-form!
which shadow the host's native bindings when loaded at runtime. The
native bindings route to Sx_ref.custom_special_forms (the dict the CEK
evaluator checks), but the SX-level defines create a separate dict.
Fix: rebind_host_extensions runs after every load command, re-asserting
the native register-special-form! and *custom-special-forms* bindings.
Add regression test: custom form registered before evaluator.sx load
survives and remains callable via CEK dispatch afterward.
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>
Handler responses from defhandler (bulk-update, delete-row, etc.) render
"class" as text content instead of HTML attributes. The SX wire format
has "class" "value" (two strings) where :class "value" (keyword + string)
is needed. Tests check for 'classpx' in text content to detect the bug.
3 tests currently fail — will pass once handler aser keyword fix lands.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
serialize_value was falling through to "nil" for SxExpr and Spread values.
Now SxExpr passes through as raw SX text, Spread serializes as make-spread.
The aser command's result handler now joins a List of SxExprs as a
space-separated fragment (from map/filter producing multiple SxExprs).
Investigation ongoing: handler aser responses still have "class" strings
where :class keywords should be — the component expansion path in aser
loses keyword types during CEK evaluation of component bodies.
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>
primitive? in make_server_env was checking env bindings only (NativeFn),
missing all 132 primitives in the Sx_primitives hashtable. Now checks
both primitives table and env. get-primitive similarly fixed.
replace primitive now coerces SxExpr/Thunk/RawHTML/etc to strings instead
of crashing with "replace: 3 string args" — fixes aser JIT DISABLED.
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>
Tests that all 8 web definition forms (defhandler, defquery, defaction,
defpage, defrelation, defstyle, deftype, defeffect) are registered and
callable via the OCaml kernel. Catches the evaluator.sx env-shadowing
bug where loading evaluator.sx creates a new *custom-special-forms*
dict that shadows the native one.
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>
OCaml kernel is the evaluator. Python host tests via sx_ref.py are
no longer relevant to the deploy gate.
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>
The test runner's component-body/component-params/component-has-children
bindings only handled Component values, not Island. When adapter-html.sx
called (component-body island), it hit the fallback and returned nil,
producing empty island bodies. Also removed debug logging from
component-has-children? primitive.
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>
SX lambdas ((fn (x) body)) now transpile to NativeFn values that can
be stored as SX values — passed to signal-add-sub!, stored in dicts,
used as reactive subscribers. Previously emitted as bare OCaml closures
which couldn't be stored in the SX value type system.
ml-emit-fn → NativeFn("λ", fun args -> match args with [...] -> body)
ml-emit-fn-bare → (fun params -> body) — used by HO inliners and
recursive let bindings (let rec) which call themselves directly.
HO forms (map, filter, reduce, for-each, map-indexed, map-dict) use
cek_call for non-inline function arguments, bare OCaml lambdas for
inline (fn ...) arguments.
Runtime: with_island_scope accepts NativeFn values (pattern match on
value type) since transpiled lambdas are now NativeFn-wrapped.
Unblocks WASM reactive signals — the bootstrap FIXUPS that manually
wrapped reactive_shift_deref's subscriber as NativeFn are no longer
needed when merging to the wasm branch.
1314/1314 JS tests, 4/4 Playwright isomorphic tests.
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 permissive is_symbol_char (negative check — everything not a
delimiter) with spec-compliant is_ident_start/is_ident_char (positive
check matching the exact character sets documented in parser.sx).
Changes:
- ident-start: remove extra chars (|, %, ^, $) not in spec
- ident-char: add comma (,) per spec
- Comma (,) now handled as dedicated unquote case in match, not in
the catch-all fallback — matches spec dispatch order
- Remove ~@ splice-unquote alias (spec only defines ,@)
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>
Code view: SSR now uses same highlighting logic as client update-code-highlight
(bg-amber-100 for current step, font-bold for active, opacity-40 for future).
steps-to-preview: pure function that replays step machine as SX expression
tree — intended for isomorphic preview rendering. Currently working for
simple cases but needs fix for partial step counts (close-loop issue).
Close steps now carry open-attrs/open-spreads for steps-to-preview.
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>
sx_runtime.ml: unwrap_env now accepts Dict and Nil (converts to Env),
fixing env-merge when adapter-html.sx passes dict-as-env.
sx_server.ml + run_tests.ml: env-merge bindings use Sx_runtime.env_merge
(which handles Dict/Nil) instead of requiring strict Env pattern match.
sx_primitives.ml: Added scope stack (scope-push!/pop!/peek/emit!, emitted),
type predicates (lambda?/island?/component?/macro?), component accessors
(closure/name/params/body/has-children?), lambda accessors, for-each-indexed,
empty-dict?, make-raw-html, raw-html-content, is-else-clause?.
8 OCaml render tests still fail (env propagation in render-lambda-html) —
same adapter code works in JS and in production via Python bridge.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
spec/tests/test-render-html.sx covers the full HTML serialization surface:
text/literals, content escaping, attribute escaping, normal elements,
all 14 void elements, 18 boolean attributes, regular/data-*/aria-* attrs,
fragments, raw HTML, headings, lists, tables, forms, media, semantic
elements, SVG, control flow (if/when/cond), let bindings, map/for-each,
components (simple/children/keyword+children/nested), macros, begin/do,
letrec, scope/provide, islands with hydration markers, lakes, marshes,
threading, define-in-template.
Validates adapter-html.sx can replace sx_render.ml as the canonical renderer.
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>
Replace raw source <pre> with styled spans from build-code-tokens.
Each token gets its colour class, and tokens before the current
step get font-bold text-xs, tokens after get opacity-40.
Home page currently blocked by a separate Map key parse error.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Test 2 now compares JSON.stringify of the full DOM tree structure
(tags, ids, classes, island markers, lake markers) and exact text
content between JS-disabled and JS-enabled renders.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4 tests verifying server-client rendering parity:
1. SSR renders visible content without JavaScript (headings, islands, logo)
2. JS-rendered content matches SSR structure (same article text)
3. CSSX styling works without JS (violet class, rules in <head>)
4. SPA navigation preserves island state (colour + copyright path)
Run: cd tests/playwright && npx playwright test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>