- Add VmFrame/VmMachine types to sx_types.ml (alongside CekState/CekFrame)
- Add VmFrame/VmMachine value variants to the value sum type
- Extend get_val in sx_runtime.ml to dispatch on VmFrame/VmMachine fields
- Extend sx_dict_set_b for VmFrame/VmMachine field mutation
- Extend transpiler ml-emit-dict-native to detect VM dict patterns
and emit native OCaml record construction (same mechanism as CekState)
- Retranspile evaluator — no diff (transpiler extension is additive)
- Update bootstrap_vm.py output location
The transpiler now handles 4 native record types:
CekState (5 fields), CekFrame (10 fields),
VmFrame (4 fields), VmMachine (5 fields)
Full VM replacement (sx_vm.ml → transpiled) still needs vm.sx
feature parity: JIT dispatch, CEK fallback, suspension handling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- make-raise-guard-frame: was never defined in spec — added it
- *last-error-kont*: set at error origination (host-error calls), not
wrapped around every cek-run step. Zero overhead on normal path.
- JIT: jit-try-call runtime function called from spec. Platform
registers hook via _jit_try_call_fn ref. No bootstrap patching.
- bootstrap.py compile_spec_to_ml() now returns transpiled output
with zero post-processing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1 engine step 4 — R7RS compatibility primitives for the CEK evaluator.
call/cc: undelimited continuation capture with separate CallccContinuation
type (distinct from delimited shift/reset continuations). Escape semantics —
invoking k replaces the current continuation entirely.
raise/raise-continuable: proper CEK arg evaluation via raise-eval frame.
Non-continuable raise uses raise-guard frame that errors on handler return.
host-error primitive for safe unhandled exception fallback.
Multi-arity map: (map fn list1 list2 ...) zips multiple lists. Single-list
path unchanged for performance. New multi-map frame type.
cond =>: arrow clause syntax (cond (test => fn)) calls fn with test value.
New cond-arrow frame type.
R7RS do: shape-detecting dispatch — (do ((var init step) ...) (test result) body)
desugars to named let. Existing (do expr1 expr2) sequential form preserved.
integer? primitive, host-error alias. Transpiler fixes: match/case routing,
wildcard _ support, nested match arm handling.
2522/2524 OCaml tests pass (2 pre-existing scope failures from transpiler
match codegen, not related to these changes).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverse hook syncs VM GLOBAL_SET mutations back to global_env so CEK reads
see JIT-written values. Isomorphic nav: store primitives, event-bridge,
client? predicate. Browser JS and bytecode rebuilt.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SX lambdas ((fn (x) body)) now transpile to NativeFn values that can
be stored as SX values — passed to signal-add-sub!, stored in dicts,
used as reactive subscribers. Previously emitted as bare OCaml closures
which couldn't be stored in the SX value type system.
ml-emit-fn → NativeFn("λ", fun args -> match args with [...] -> body)
ml-emit-fn-bare → (fun params -> body) — used by HO inliners and
recursive let bindings (let rec) which call themselves directly.
HO forms (map, filter, reduce, for-each, map-indexed, map-dict) use
cek_call for non-inline function arguments, bare OCaml lambdas for
inline (fn ...) arguments.
Runtime: with_island_scope accepts NativeFn values (pattern match on
value type) since transpiled lambdas are now NativeFn-wrapped.
Unblocks WASM reactive signals — the bootstrap FIXUPS that manually
wrapped reactive_shift_deref's subscriber as NativeFn are no longer
needed when merging to the wasm branch.
1314/1314 JS tests, 4/4 Playwright isomorphic tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
Islands now render their initial state as HTML on the server, like
React SSR. The client hydrates with reactive behavior on boot.
Root causes fixed:
- is_signal/signal_value now recognize Dict-based signals (from
signals.sx) in addition to native Signal values
- Register "context" as a primitive so the CEK deref frame handler
can read scope stacks for reactive tracking
- Load adapter-html.sx into kernel for SX-level render-to-html
(islands use this instead of the OCaml render module)
- Component accessors (params, body, has-children?, affinity) handle
Island values with ? suffix aliases
- Add platform primitives: make-raw-html, raw-html-content,
empty-dict?, for-each-indexed, cek-call
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The second get implementation in sx_runtime.ml (used by transpiled code)
was still raising on type mismatches. Now returns nil like sx_primitives.
Remove per-call [vm-call-closure] FAIL logging — the jit-hook already
logs failures at the right level. Reduces 70K log lines to ~5.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace continuation-based scope frames with hashtable stacks for all
scope operations. The CEK evaluator's scope/provide/context/emit!/emitted
now use scope-push!/pop!/peek/emit! primitives (registered in
sx_primitives table) instead of walking continuation frames.
This eliminates the two-world problem where the aser used hashtable
stacks (scope-push!/pop!) but eval-expr used continuation frames
(ScopeFrame/ScopeAccFrame). Now both paths share the same mechanism.
Benefits:
- scope/context works inside eval-expr calls (e.g. (str ... (context x)))
- O(1) scope lookup vs O(n) continuation walking
- Simpler — no ScopeFrame/ScopeAccFrame/ProvideFrame creation/dispatch
- VM-compiled code and CEK code both see the same scope state
Also registers scope-push!/pop!/peek/emit!/collect!/collected/
clear-collected! as real primitives (sx_primitives table) so the
transpiled evaluator can call them directly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Compiler fixes:
- Upvalue re-lookup returns own position (uv-index), not parent slot
- Spec: cek-call uses (make-env) not (dict) — OCaml Dict≠Env
- Bootstrap post-processes transpiler Dict→Env for cek_call
VM runtime fixes:
- compile_adapter evaluates constant defines (SPECIAL_FORM_NAMES etc.)
via execute_module instead of wrapping as NativeFn closures
- Native primitives: map-indexed, some, every?
- Nil-safe HO forms: map/filter/for-each/some/every? accept nil as empty
- expand-components? set in kernel env (not just VM globals)
- unwrap_env diagnostic: reports actual type received
sx-page-full command:
- Single OCaml call: aser-slot body + render-to-html shell
- Eliminates two pipe round-trips (was: aser-slot→Python→shell render)
- Shell statics (component_defs, CSS, pages_sx) cached in Python,
injected into kernel once, referenced by symbol in per-request command
- Large blobs use placeholder tokens — Python splices post-render,
pipe transfers ~51KB instead of 2MB
Performance (warm):
- Server total: 0.55s (was ~2s)
- aser-slot VM: 0.3s, shell render: 0.01s, pipe: 0.06s
- kwargs computation: 0.000s (cached)
SX_STANDALONE mode for sx_docs dev (skips fragment fetches).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Transpiler detects dict literals with a "type" string field and emits
CekFrame records instead of Dict(Hashtbl). Maps frame-specific fields
to generic record slots:
cf_type, cf_env, cf_name, cf_body, cf_remaining, cf_f,
cf_args (also evaled), cf_results (also raw-args),
cf_extra (ho-type/scheme/indexed/match-val/current-item/...),
cf_extra2 (emitted/effect-list/first-render)
Runtime get_val handles CekFrame with direct field match — O(1)
field access vs Hashtbl.find.
Bootstrapper: skip stdlib.sx entirely (already OCaml primitives).
Result: 29 CekFrame + 2 CekState = 31 record types, only 8
Hashtbl.create remaining (effect-annotations, empty dicts).
Benchmark (200 divs): 2.94s → 1.71s (1.7x speedup from baseline).
Real pages: ~same as CekState-only (frames are <20% of allocations;
states dominate at 199K/page).
Foundation for JIT: record-based value representation enables
typed compilation — JIT can emit direct field access instead of
hash table lookups.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Transpiler (transpiler.sx): detects CEK state dict literals (5 fields:
control/env/kont/phase/value) and emits CekState OCaml record instead
of Dict(Hashtbl). Eliminates 200K Hashtbl allocations per page.
Bootstrapper: skip stdlib.sx (functions already registered as OCaml
primitives). Only transpile evaluator.sx.
Runtime: get_val handles CekState with direct field access. type_of
returns "dict" for CekState (backward compat).
Profiling results (root cause of slowness):
Pure eval: OCaml 1.6x FASTER than Python (expected)
Aser: OCaml 28x SLOWER than Python (unexpected!)
Root cause: Python has a native optimized aser. OCaml runs the SX
adapter-sx.sx through the CEK machine — each aserCall is ~50 CEK
steps with closures, scope operations, string building.
Fix needed: native OCaml aser (like Python's), not SX adapter
through CEK machine.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SX-to-OCaml transpiler (transpiler.sx) generates sx_ref.ml (~90KB, ~135
mutually recursive functions) from the spec evaluator. Foundation tests
all pass: parser, primitives, env operations, type system.
Key design decisions:
- Env variant added to value type for CEK state dict storage
- Continuation carries optional data dict for captured frames
- Dynamic var tracking distinguishes OCaml fn calls from SX value dispatch
- Single let rec...and block for forward references between all defines
- Unused ref pre-declarations eliminated via let-bound name detection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>