Commit Graph

1015 Commits

Author SHA1 Message Date
f3f70cc00b Move stdlib out of spec — clean spec/library boundary
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>
2026-03-24 23:18:30 +00:00
50871780a3 Add call-lambda + trampoline handler tests for dom-on pattern
Regression tests for the silent failure where callLambda returns a
Thunk (TCO) that must be trampolined for side effects to execute.
Without trampoline, event handlers (swap!, reset!) silently did nothing.

5 tests covering: single mutation, event arg passing, multi-statement
body, repeated accumulation, and nested lambda calls — all through
the (trampoline (call-lambda handler args)) pattern that dom-on uses.

Tests: 1322 JS (full), 1114 OCaml

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:37:21 +00:00
57cffb8bcc Fix isomorphic SSR: revert inline opcodes, add named let compilation, fix cookie decode
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>
2026-03-24 22:32:51 +00:00
eb4233ff36 Add inline VM opcodes for hot primitives (OP_ADD through OP_DEC)
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>
2026-03-24 20:10:48 +00:00
5b2ef0a2af Fix island reactivity: trampoline callLambda result in dom-on handlers
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>
2026-03-24 19:26:43 +00:00
32df71abd4 Add 21 demo interaction + health check Playwright tests
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>
2026-03-24 19:08:55 +00:00
91cf39153b Add promise-delayed SSR stub; fix startup JIT DISABLED noise
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>
2026-03-24 18:39:48 +00:00
953f0ec744 Fix handler aser keyword loss: re-serialize evaluated HTML elements
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>
2026-03-24 18:37:21 +00:00
13ba5ee423 Unify JIT to lazy-only: remove allowlist, all lambdas compile on first call
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>
2026-03-24 18:34:27 +00:00
a6e0e84521 Split setup_type_operations into 6 focused functions
125-line monolith split into:
- setup_core_operations (assert, append!, apply, equal?, primitive?)
- setup_type_constructors (make-keyword, make-symbol, escape-string, etc.)
- setup_character_classification (ident-start?, ident-char?, char-numeric?)
- setup_env_operations (env-get, env-has?, env-bind!, env-set!, etc.)
- setup_strict_mode (gradual type system support)
- setup_io_bridges (json-encode, into, sleep, response headers)

make_server_env now calls 12 focused setup functions total.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:31:37 +00:00
3ae49b69f5 Fix env-shadowing: rebind host extension points after .sx file load
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>
2026-03-24 18:29:29 +00:00
2d8741779e Fix transpiler call-expression bug: ((get d k) args) now emits function call
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>
2026-03-24 18:24:45 +00:00
945b4c1dd7 Add failing Playwright tests for handler response rendering bug
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>
2026-03-24 18:18:36 +00:00
33af6b9266 Fix serialize_value for SxExpr/Spread; handle List-of-SxExpr in aser output
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>
2026-03-24 18:16:33 +00:00
c8280e156f Add comprehensive Playwright tests for all geography demos (61 tests)
Tests every page under /sx/(geography...):
- 9 section index pages (geography, reactive, hypermedia, marshes, etc.)
- 16 reactive island demos with interaction tests (counter, temperature,
  stopwatch, input-binding, dynamic-class, reactive-list, stores, etc.)
- 27 hypermedia demos (click-to-load, form-submission, tabs, etc.)
- Cross-navigation reactivity (counter → temperature → counter)
- Sequential 5-demo navigation test
- CEK, marshes, isomorphism, scopes, spreads, provide, reference pages

Total Playwright tests: 72 (6 isomorphic + 5 reactive-nav + 61 geography)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:50:48 +00:00
732d733eac Fix island reactivity lost on client-side navigation; add Playwright tests
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>
2026-03-24 17:36:51 +00:00
3df8c41ca1 Split make_server_env, eliminate all runtime sx_ref imports, fix auth-menu tests
make_server_env split into 7 focused setup functions:
- setup_browser_stubs (22 DOM no-ops)
- setup_scope_env (18 scope primitives from sx_scope.ml)
- setup_evaluator_bridge (CEK eval-expr, trampoline, expand-macro, etc.)
- setup_introspection (type predicates, component/lambda accessors)
- setup_type_operations (string/env/dict/equality/parser helpers)
- setup_html_tags (~100 HTML tag functions)
- setup_io_env (query, action, helper IO bridge)

Eliminate ALL runtime sx_ref.py imports:
- sx/sxc/pages/helpers.py: 24 imports → _ocaml_helpers.py bridge
- sx/sxc/pages/sx_router.py: remove SX_USE_REF fallback
- shared/sx/query_registry.py: use register_components instead of eval

Unify JIT compilation: pre-compile list derived from allowlist
(no manual duplication), only compiler internals pre-compiled.

Fix test_components auth-menu: ~auth-menu → ~shared:fragments/auth-menu

Tests: 1114 OCaml, 29/29 components, 35/35 regression, 6/6 Playwright

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:23:09 +00:00
6ef9688bd2 Fix primitive? lookup + replace coercion; remove debug output
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>
2026-03-24 16:29:52 +00:00
f9f810ffd7 Complete Python eval removal: epoch protocol, scope consolidation, JIT fixes
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>
2026-03-24 16:14:40 +00:00
e887c0d978 Fix defisland→Component bug in jinja_bridge; add island reactivity test
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>
2026-03-24 15:48:19 +00:00
7434de53a6 Add OCaml bridge integration test for custom special forms
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>
2026-03-24 15:18:20 +00:00
d735e28b39 Delete sx_ref.py — OCaml is the sole SX evaluator
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>
2026-03-24 14:32:55 +00:00
482bc0ca5e Remove Python SX tests from run-tests.sh — sx_ref.py being eliminated
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>
2026-03-24 13:46:32 +00:00
aa88c06c00 Add run-tests.sh unified test runner; register log-info/log-warn as PRIMITIVES
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>
2026-03-24 13:31:35 +00:00
ee868f686b Migrate 6 reactive demo handlers from Python f-strings to SX defhandlers
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>
2026-03-24 13:26:25 +00:00
96f2862385 Fix island rendering in OCaml test runner — add Island cases to component accessors
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>
2026-03-24 12:55:47 +00:00
26e16f6aa4 Move defstyle/deftype/defeffect to web-forms.sx — domain forms, not core
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>
2026-03-24 12:22:08 +00:00
9caf8b6e94 Fix runtime PRIMITIVES for dom/browser library functions
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>
2026-03-24 12:10:54 +00:00
8e6e7dce43 Transpile dom.sx + browser.sx into bundle; add FFI variable aliases
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>
2026-03-24 11:43:42 +00:00
bc7da977a0 Platform FFI reduction: remove 99 redundant PRIMITIVES registrations
Move DOM/browser operations to SX library wrappers (dom.sx, browser.sx)
using the 8 FFI primitives, eliminating duplicate native implementations.
Add scope-emitted transpiler rename — fixes 199 pre-existing test failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:25:51 +00:00
efb2d92b99 Transpiler: emit NativeFn for SX lambdas, bare OCaml for HO inlines
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>
2026-03-24 10:40:26 +00:00
89543e0152 Fix modifier-key click guard in orchestration verb handler
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>
2026-03-24 10:17:18 +00:00
0c7567925e Align OCaml parser with spec/parser.sx character classification
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>
2026-03-24 10:13:03 +00:00
2a9a4b41bd Stable extension point for definition-form? — no monkey-patching
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>
2026-03-24 10:06:05 +00:00
8a08de26cd Web extension module for def-forms + modifier-key clicks + CSSX SSR fix
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>
2026-03-24 10:01:41 +00:00
8ccf5f7c1e Stepper: steps-to-preview for isomorphic preview text (WIP)
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>
2026-03-24 04:24:12 +00:00
bf305deae1 Isomorphic cookie support + stepper cookie persistence
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>
2026-03-24 04:13:53 +00:00
e021184935 Stepper: isomorphic code highlighting + steps-to-preview (WIP)
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>
2026-03-24 03:37:27 +00:00
55061d6451 Revert boot.sx CSSX flush — client morph needs different approach
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>
2026-03-24 02:57:23 +00:00
ce9c5d3a08 Add scope-collected/scope-clear-collected!/scope-emitted primitives
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>
2026-03-24 02:50:23 +00:00
49fd4a51d6 Remove debug logging from component-has-children?, restore island test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 02:36:46 +00:00
7d793ec76c Fix CSSX styling: trampoline wiring + scope-emit!/emitted for adapter-html.sx
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>
2026-03-24 02:23:00 +00:00
e4cabcbb59 Fix env-merge for Dict/Nil args + add adapter-html.sx primitives
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>
2026-03-24 01:49:21 +00:00
284572c7a9 Wire adapter-html.sx into OCaml server, replacing hand-written renderer
sx_server.ml: sx_render_to_html() calls the SX adapter-html.sx render-to-html
via CEK eval, falling back to Sx_render.render_to_html if adapter not loaded.
CLI --render mode now loads render.sx + adapter-html.sx.

sx_primitives.ml: Added ~25 primitives needed by adapter-html.sx:
  scope-push!/pop!/peek/emit!, emitted, provide-push!/pop! (hashtable stack),
  lambda?/island?/component?/macro?, component-closure/name/params/body/
  has-children?, lambda-closure/params/body, is-else-clause?, for-each-indexed,
  empty-dict?, make-raw-html, raw-html-content

run_tests.ml: Loads render.sx + adapter-html.sx for test-render-html.sx.
Registers trampoline, eval-expr, scope stubs, expand-macro, cond-scheme?.

Status: 1105/1114 OCaml tests pass. 8 remaining failures are env-merge
edge cases in render-lambda-html/component-children/island rendering —
same adapter code works in JS (143/143).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 01:38:18 +00:00
70a58bddd8 Exhaustive HTML render tests — 143 tests for adapter-html.sx
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>
2026-03-24 01:29:59 +00:00
23c8b97cb1 VM spec in SX + 72 tests passing on both JS and OCaml
spec/vm.sx — bytecode VM written in SX (the spec):
  - Stack-based interpreter for bytecode from compiler.sx
  - 24 opcodes: constants, variables (local/upvalue/global), control flow,
    function calls (with TCO), closures with upvalue capture, collections,
    string concat, define
  - Upvalue cells for shared mutable closure variables
  - Call dispatch: vm-closure (fast path), native-fn, CEK fallback
  - Platform interface: 7 primitives (vm-stack-*, call-primitive, cek-call,
    get-primitive, env-parent)

spec/tests/test-vm.sx — 72 tests exercising compile→bytecode→VM pipeline:
  constants, arithmetic, comparison, control flow (if/when/cond/case/and/or),
  let bindings, lambda, closures, upvalue mutation, TCO (10K iterations),
  collections, strings, define, letrec, quasiquote, threading, integration
  (fibonacci, recursive map/filter/reduce, compose)

spec/compiler.sx — fix :else keyword detection in case/cond compilation
  (was comparing Keyword object to evaluated string, now checks type)

Platform primitives added (JS + OCaml):
  make-vm-stack, vm-stack-get, vm-stack-set!, vm-stack-length, vm-stack-copy!,
  primitive?, get-primitive, call-primitive, set-nth! (JS)

Test runners updated to load bytecode.sx + compiler.sx + vm.sx for --full.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 01:20:00 +00:00
5270d2e956 JIT allowlist + integration tests + --test mode + clean up debug logging
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>
2026-03-23 23:58:40 +00:00
dd057247a5 VM: VmClosure value type + iterative run loop + define hoisting + SSR fixes
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>
2026-03-23 23:39:35 +00:00
8958714c85 VM: closure env chain for GLOBAL_GET/SET + remove JIT skip
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>
2026-03-23 21:23:27 +00:00
30cfbf777a Fix letrec thunk resolution + compiler letrec support + closure JIT check
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>
2026-03-23 21:04:47 +00:00