Mirror of OCaml Step 5 to the JavaScript host. Native ADT representation
for define-type instances, with the same dict-shaped shim approach so
spec-level match-pattern code in evaluator.sx works without changes.
- platform.py typeOf: recognize ._adtv tag, return ._type (so
(type-of (Just 42)) returns "Maybe" not "dict").
- platform.py adds makeAdtValue/isAdtValue helpers and registers
PRIMITIVES["adt?"], "make-adt-value", "adt-value?".
- platform.py inspect: format AdtValue as "(Ctor f1 f2 ...)" and
register as a primitive (was missing entirely on JS).
- fixups_js: hand-written define-type override that constructs
AdtValue via makeAdtValue, with arity check, type/ctor predicates,
and field accessors. Re-registered via registerSpecialForm so the
CEK dispatch routes through it.
- dict? unchanged: AdtValue still passes (no _adtv exclusion) so
the existing (and (dict? v) (get v :_adt) ...) checks in spec
predicates keep working.
Tests: 2578 pass (was 2575), zero regressions. All 43 ADT tests
pass on the JS host (was 40, the 3 new Step 5 tests for type-of /
adt? / inspect are now green).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- parser: `toggle $var between v1 and v2 ...` → `(toggle-var-cycle $var (v1 v2 ...))`
- compiler: emit `(hs-toggle-var-cycle! win var-name values)` for new AST node
- runtime: `hs-toggle-var-cycle!` cycles through a list of values on a variable
- parser: `closest .sel to .target` / `closest #id to .target` / `closest sel to .target`
now consumes the `to` keyword and parses the target expr instead of defaulting to beingTold
- tokenizer: `read-class-name` handles backslash escapes and allows `(`, `)`, `&`
chars so Tailwind classes like `group-[:nth-of-type(3)_&]:block` tokenize correctly
- platform.py: `domListen` drives async result via `_driveAsync` after `cekCall`
- test: fixed-time toggle asserts `.foo` IS present after click (toggle started, 10ms window open)
- generate-sx-tests.py: aligned MANUAL_TEST_BODIES for timed toggle with corrected assertion
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
28 tests, passes on both JS and OCaml.
- spec/stdlib.sx: pure SX format function
- spec/primitives.sx: format primitive declaration
- lib/r7rs.sx: fix number->string to support optional radix arg
- hosts/ocaml: add format-decimal primitive, load stdlib.sx in test runner
- hosts/javascript: load stdlib.sx in test runner
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds 9 regexp primitives to stdlib.regexp. OCaml: SxRegexp(src,flags,Re.re)
using Re.Pcre; $&/$1 capture expansion in replace. JS: native RegExp
with SxRegexp wrapper; regexp-match returns {:match :start :end :groups}.
32 tests in test-regexp.sx, all pass on both hosts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds 13 set primitives to stdlib.sets. OCaml: SxSet as (string,value)
Hashtbl keyed by inspect(val); JS: SxSet wrapping Map keyed by
write-to-string. Structural equality — (make-set '(1 2)) contains 1.
Includes union, intersection, difference, for-each, map.
33 tests in test-sets.sx, all pass on both JS and OCaml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds read, write, display, newline, write-to-string, display-to-string
and current-*-port primitives to both JS and OCaml hosts.
JS: sxReadNormalize (#t/#f→true/false), sxReadConvert (()→nil),
sxEq array comparison, sxWriteVal symbol/keyword name fix,
readerMacroGet/readerMacroSet registry in parser platform.
OCaml: sx_write_val/sx_display_val helpers, read/write/display/newline
primitives on port types; parser extended for #t/#f and N/D rationals.
42 new tests (test-read-write.sx), all passing on JS and OCaml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SxRational type in OCaml (Rational of int * int, stored reduced, denom>0)
and JS (SxRational class with _rational marker). n/d reader syntax in
spec/parser.sx. Arithmetic contagion: int op rational → rational, rational
op float → float. JS keeps int/int → float for CSS backward compatibility.
OCaml as_number + safe_eq extended for cross-type rational equality so
(= 2.5 5/2) → true. 62 tests in test-rationals.sx, all pass.
JS: 2232 passed. OCaml: 4532 passed (+11 vs pre-fix baseline).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 14: port type + eof-object. Input ports track _pos cursor; output ports
accumulate _buffer. All 15 port primitives in spec/primitives.sx (stdlib.ports
module), platform.py (JS), and 39/39 tests in spec/tests/test-ports.sx.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- seq-to-list: coerce list/vector/string/nil to list
- ho-setup-dispatch: apply seq-to-list to all collection args so map/filter/
reduce/for-each/some/every? work over vectors and strings natively
- sequence->list, sequence->vector, sequence-length, sequence-ref,
sequence-append: full polymorphic sequence helpers
- in-range: list-returning range generator (eager, works with all HO forms)
- Restore 3 accidentally-deleted make-cek-state/make-cek-value/make-cek-suspended
- Fix 8 shorthand define forms (transpiler requires long form)
- Add vector->list/list->vector to transpiler js-renames + platform aliases
- JS: 2137 passing (+28 vs HEAD baseline)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OCaml: StringBuffer of Buffer.t in sx_types.ml; 5 primitives in
sx_primitives.ml (make-string-buffer, string-buffer?, string-buffer-append!,
string-buffer->string, string-buffer-length); inspect case added.
JS: SxStringBuffer with array+join backend; _string_buffer marker for
typeOf dispatch and dict? exclusion (also excludes _vector from dict?).
spec/primitives.sx: 5 define-primitive entries.
17/17 tests pass on both OCaml and JS.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All CEK primitives (cek-step-loop/cek-resume/make-cek-state/cek-suspended?/
cek-io-request/cek-terminal?/cek-value) were already registered in sx-browser.js.
Root cause of test failure: (import (sx coroutines)) creates an io-suspended
state when the library isn't pre-loaded; overridden cekRun throws on suspension.
Fix: pre-load spec/signals.sx + spec/coroutines.sx before test files run.
17/17 coroutine tests pass in JS. 1965/2500 total (+25 vs 1940 baseline),
zero new failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add integer?/float?/exact?/inexact? predicates (Number.isInteger check).
Add truncate/remainder/modulo/random-int/exact->inexact/inexact->exact/parse-number.
inexact->exact uses Math.round (rounds to nearest, matching OCaml).
Fix sx_server.ml epoch/blob/io-response protocol to accept Integer as
well as Number — parser now produces Integer for whole-number literals.
JS: 60 new passing tests (1880→1940). OCaml: 4874/394 baseline unchanged.
Note: 6 tests fail in JS due to platform limitation (JS cannot distinguish
float 2.0 from integer 2).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix PRIMITIVES["index-of"] for arrays: return NIL when not found (matching
OCaml semantics) so bind-lambda-params correctly detects absent &rest params.
Previously String(array).indexOf() returned -1, which passed number? check
and mis-fired the &rest branch, leaving non-&rest params unbound.
- Declare var _lastErrorKont_ and var hostError in IIFE scope (strict mode fix)
- Add PRIMITIVES["host-error"], ["try-catch"], ["without-io-hook"]
- Add env["test-allowed?"] stub in run_tests.js
- Add spec/tests/test-vectors.sx: 42 tests for all vector primitives
- Rebuild sx-browser.js: 1847 standard / 2362 full tests pass (up from 5)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New tests: scope/context, multi-signal, reactive DOM, VM HO forms.
Fix rest primitive to not error-log on nil input (defensive).
1 remaining pre-existing failure: named-let set! scoping in JS
bootstrapper — not related to WASM/hydration changes.
32/32 WASM, 1593/1594 JS full.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Registering map/for-each/reduce as PRIMITIVES caused the compiler to
emit CALL_PRIM for them everywhere. The OCaml VM's call-primitive
can't invoke VM closures, causing "Undefined symbol: resource" crashes.
Revert vm.sx to original CALL_PRIM handler. Remove map/for-each/reduce
from JS PRIMITIVES so compiler emits OP_CALL instead (handled by
vm-call which dispatches correctly).
3 JS VM tests remain failing (VM closure interop) but production is stable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Convert large cond chains doing string equality dispatch to use the
match special form: step-eval-list (42 arms), step-continue (31 arms),
compile-list (30 arms), ho-setup-dispatch (7 arms), value-matches-type?
(10 arms). Also fix test-canonical.sx to use defsuite/deftest format
and load canonical.sx in both test runners.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
spec/harness.sx — spec-level test harness with:
- Mock platform (30+ default IO mocks: fetch, query, DOM, storage, etc.)
- Session management (make-harness, harness-reset!, harness-set!/get)
- IO interception (make-interceptor, install-interceptors)
- IO log queries (io-calls, io-call-count, io-call-nth, io-call-args)
- IO assertions (assert-io-called, assert-no-io, assert-io-count, etc.)
15 harness tests passing on both OCaml (1116/1116) and JS (15/15).
Loaded automatically by both test runners.
MCP tool: sx_harness_eval — evaluate SX with mock IO, returns result + IO trace.
The harness is extensible: new platforms just add entries to the platform dict.
Components can ship with deftest forms that verify IO behavior against mocks.
Tests are independent objects that can be published separately (by CID).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1-3 of the SX Tools plan — structural reading, editing, and
MCP server for .sx files.
lib/tree-tools.sx — Pure SX functions for tree comprehension and editing:
Comprehension: annotate-tree, summarise, read-subtree, get-context,
find-all, get-siblings, validate, navigate
Editing: replace-node, insert-child, delete-node, wrap-node, tree-set
Helpers: list-replace, list-insert, list-remove, replace-placeholder
lib/tests/test-tree-tools.sx — 107 tests covering all functions.
hosts/ocaml/bin/mcp_tree.ml — MCP server (stdio JSON-RPC) exposing
11 tools. Loads tree-tools.sx into the OCaml evaluator, parses .sx
files with the native parser, calls SX functions for tree operations.
The MCP server can be configured in Claude Code's settings.json as:
"mcpServers": { "sx-tree": { "command": "path/to/mcp_tree.exe" } }
1429 tests passing (1322 existing + 107 new tree-tools).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three issues in the stepper island's client-side rendering:
1. do-step used eval-expr with empty env for ~cssx/tw spreads — component
not found, result leaked as [object Object]. Fixed: call ~cssx/tw
directly (in scope from island env) with trampoline.
2. steps-to-preview excluded spreads — SSR preview had no styling.
Fixed: include spreads in the tree so both SSR and client render
with CSSX classes.
3. build-children used named let (let loop ...) which produces
unresolved Thunks in render mode due to the named-let compiler
desugaring interacting with the render/eval boundary. Fixed:
rewrote as plain recursive function bc-loop avoiding named let.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
spec/ now contains only the language definition (5 files):
evaluator.sx, parser.sx, primitives.sx, render.sx, special-forms.sx
lib/ contains code written IN the language (8 files):
stdlib.sx, types.sx, freeze.sx, content.sx,
bytecode.sx, compiler.sx, vm.sx, callcc.sx
Test files follow source: spec/tests/ for core language tests,
lib/tests/ for library tests (continuations, freeze, types, vm).
Updated all consumers:
- JS/Python/OCaml bootstrappers: added lib/ to source search paths
- OCaml bridge: spec_dir for parser/render, lib_dir for compiler/freeze
- JS test runner: scans spec/tests/ (always) + lib/tests/ (--full)
- OCaml test runner: scans spec/tests/, lib tests via explicit request
- Docker dev mounts: added ./lib:/app/lib:ro
Tests: 1041 JS standard, 1322 JS full, 1101 OCaml — all pass
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Removes the 5993-line bootstrapped Python evaluator (sx_ref.py) and all
code that depended on it exclusively. Both bootstrappers (JS + OCaml)
now use a new synchronous OCaml bridge (ocaml_sync.py) to run the
transpiler. JS build produces identical output; OCaml bootstrap produces
byte-identical sx_ref.ml.
Key changes:
- New shared/sx/ocaml_sync.py: sync subprocess bridge to sx_server.exe
- hosts/javascript/bootstrap.py: serialize defines → temp file → OCaml eval
- hosts/ocaml/bootstrap.py: same pattern for OCaml transpiler
- shared/sx/{html,async_eval,resolver,jinja_bridge,handlers,pages,deps,helpers}:
stub or remove sx_ref imports; runtime uses OCaml bridge (SX_USE_OCAML=1)
- sx/sxc/pages: parse defpage/defhandler from AST instead of Python eval
- hosts/ocaml/lib/sx_primitives.ml: append handles non-list 2nd arg per spec
- Deleted: sx_ref.py, async_eval_ref.py, 6 Python test runners, misc ref/ files
Test results: JS 1078/1078, OCaml 1114/1114.
sx_docs SSR has pre-existing rendering issues to investigate separately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
run-tests.sh runs all suites: JS (standard + full), Python, OCaml,
Playwright (isomorphic + demos). deploy.sh calls it as gate.
Register log-info and log-warn as PRIMITIVES so runtime-eval'd SX code
(init-client.sx.txt) can use them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
Register hashtable-based scope accessors that bypass the CEK special form
dispatch, for use by adapter-html.sx and shell templates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root causes of missing CSSX classes in SSR:
1. _sx_trampoline_fn in sx_primitives.ml was never wired — call_any in
HO forms (map/filter/for-each) returned unresolved Thunks, so callbacks
like render-lambda-html's param binding never executed. Fixed in
bootstrap.py FIXUPS: wire Sx_primitives._sx_trampoline_fn after eval_expr.
2. adapter-html.sx used (emit! ...) and (emitted ...) which are CEK special
forms (walk kont for ScopeAccFrame), but scope-push!/scope-pop! use the
hashtable. CEK frames and hashtable are two different scope systems.
Fixed: adapter uses scope-emit!/scope-emitted (hashtable primitives).
3. env-* operations (env-has?, env-get, env-bind!, env-set!, env-extend,
env-merge) only accepted Env type. adapter-html.sx passes Dict as env.
Fixed: all env ops go through unwrap_env which handles Dict/Nil.
Also: fix merge conflict in sx/sx/geography/index.sx, remove duplicate
scope primitives from sx_primitives.ml (sx_server.ml registers them).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
JIT allowlist (sx_server.ml):
- Replace try-every-lambda strategy with StringSet allowlist. Only
functions in the list get JIT compiled (compiler, parser, pure transforms).
Render functions that need dynamic scope skip JIT entirely — no retry
overhead, no silent fallbacks.
- Add (jit-allow name) command for dynamic expansion from Python bridge.
- JIT failures log once with "[jit] DISABLED fn — reason" then go silent.
Standalone --test mode (sx_server.ml):
- New --test flag loads full env (spec + adapters + compiler + signals),
supports --eval and --load flags. Quick kernel testing without Docker.
Example: dune exec bin/sx_server.exe -- --test --eval '(len HTML_TAGS)'
Integration tests (integration_tests.ml):
- New binary exercising the full rendering pipeline: loads spec + adapters
into a server-like env, renders HTML via both native and SX adapter paths.
- 26 tests: HTML tags, special forms (when/if/let), letrec with side
effects, component rendering, eval-expr with HTML tag functions.
- Would have caught the "Undefined symbol: div/lake/init" issues from
the previous commit immediately without Docker.
VM cleanup (sx_vm.ml):
- Remove temporary debug logging (insn counter, call_closure counter,
VmClosure depth tracking) added during debugging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>