Commit Graph

1021 Commits

Author SHA1 Message Date
99e2009c2b Fix sx_docs Dockerfile: install dune + set PATH for OCaml build
The opam base image has dune in the switch but not on PATH.
RUN eval $(opam env) doesn't persist across layers. Install dune
explicitly and set PATH so dune is available in build steps.

Also fix run-tests.sh to respect QUICK env var from caller
(was being overwritten to false).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 01:20:48 +00:00
1ae5906ff6 Skip Playwright in deploy (needs running server) 2026-03-25 00:49:50 +00:00
4dfaf09e04 Add lib/ to CI test Dockerfile
Missed during spec/lib split — CI image copied spec/ and web/
but not lib/ (compiler, freeze, vm, etc.).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 00:36:57 +00:00
b174a57c9c Fix spec/freeze.sx → lib/freeze.sx in CI test scripts
Missed during spec/lib split — the OCaml bridge test loaded
freeze.sx from the old spec/ path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 00:35:06 +00:00
0fce6934cb Use dom-on for event handlers; add CI config and stepper Playwright test
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 1m5s
- web/orchestration.sx, web/signals.sx: dom-listen → dom-on (trampoline
  wrapper that resolves TCO thunks from Lambda event handlers)
- .gitea/: CI workflow and Dockerfile for automated test runs
- tests/playwright/stepper.spec.js: stepper widget smoke test
- Remove stale artdag .pyc file

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 00:19:35 +00:00
7d7de86034 Fix stepper client-side [object Object] flash and missing CSSX styles
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m54s
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>
2026-03-25 00:11:06 +00:00
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