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>
Root cause: sf-letrec returns a thunk (for TCO) but the CEK dispatch
wrapped it as a value without evaluating. The thunk leaked as the
return value of letrec expressions, breaking sx-parse and any function
using letrec.
Fix: step-sf-letrec unwraps the thunk into a CEK state, so the last
letrec body expression is properly evaluated by the CEK machine.
Also:
- compile-letrec: two-phase (nil-init then assign) for mutual recursion
- Skip JIT for inner functions (closure.bindings != globals) in both
vm_call and JIT hook
- vm-reset-fn for sx-parse removed (no longer needed)
- Parser regression test: letrec with mutable pos + recursive sublists
Test results: JS 943/17, OCaml 955/0, Python 747/0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace 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>
- get primitive returns nil for type mismatches (list+string) instead
of raising — matches JS/Python behavior, fixes find-nav-match errors
- scope-peek, collect!, collected, clear-collected! registered as real
primitives in sx_primitives table (not just env bindings) so the CEK
step-sf-context can find them via get-primitive
- step-sf-context checks scope-peek hashtable BEFORE walking CEK
continuation — bridges aser's scope-push!/pop! with CEK's context
- context, emit!, emitted added to SPECIAL_FORM_NAMES and handled in
aser-special (scope operations in aser rendering mode)
- sx-context NativeFn for VM-compiled code paths
- VM execution errors no longer mark functions as permanently failed —
bytecode is correct, errors are from runtime data
- kbd, samp, var added to HTML_TAGS + sx-browser.js rebuilt
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>
adapter-sx.sx: aser-expand-component expands :affinity :server components
inline during SX wire format serialization. Binds keyword args via
eval-expr, children via aser (handles HTML tags), then asers the body.
ocaml_bridge.py: 10MB readline buffer for large spec responses.
nav-data.sx: evaluator.sx filename fix.
Page rendering stays on Python _eval_slot for now — full OCaml rendering
needs the page shell IO (headers, CSRF, CSS) migrated to OCaml IO bridge.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three changes that together enable the full 46-function stdlib migration:
1. CEK callable unification (spec/evaluator.sx):
cek-call now routes both native callables and SX lambdas through
continue-with-call, so replacing a native function with an SX lambda
doesn't change shift/reset behavior.
2. Named-let transpiler support (hosts/javascript/transpiler.sx):
(let loop ((i 0)) body...) now transpiles to a named IIFE:
(function loop(i) { body })(0)
This was the cause of the 3 test regressions (produced [object Object]).
3. Full stdlib via runtime eval (hosts/javascript/bootstrap.py):
stdlib.sx is eval'd at runtime (not transpiled) so its defines go
into PRIMITIVES without shadowing module-scope variables that the
transpiled evaluator uses directly.
stdlib.sx now contains all 46 library functions:
Logic: not
Comparison: != <= >= eq? eqv? equal?
Predicates: boolean? number? string? list? dict? continuation?
zero? odd? even? empty?
Arithmetic: inc dec abs ceil round min max clamp
Collections: first last rest nth cons append reverse flatten
range chunk-every zip-pairs
Dict: vals has-key? assoc dissoc into
Strings: upcase downcase string-length substring string-contains?
starts-with? ends-with? split join replace contains?
Text: pluralize escape parse-datetime assert
All hosts: JS 957+1080, Python 744, OCaml 952 — zero regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Evaluator: data-first higher-order forms — ho-swap-args auto-detects
(map coll fn) vs (map fn coll), both work. Threading + HO: (-> data
(map fn)) dispatches through CEK HO machinery via quoted-value splice.
17 new tests in test-cek-advanced.sx.
Fix plan pages: add mother-language, isolated-evaluator, rust-wasm-host
to page-functions.sx plan() — were in defpage but missing from URL router.
Aser error handling: pages.py now catches EvalError separately, renders
visible error banner instead of silently sending empty content. All
except blocks include traceback in logs.
Scope primitives: register collect!/collected/clear-collected!/emitted/
emit!/context in shared/sx/primitives.py so hand-written _aser can
resolve them (fixes ~cssx/flush expansion failure).
New test file: shared/sx/tests/test_aser_errors.py — 19 pytest tests
for error propagation through all aser control flow forms.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The eval-expr forward declaration was an error-throwing stub that
the CEK fixup was supposed to override. If anything prevented the
fixup from running (or if eval-expr was captured by value before
the fixup), the stub would throw "CEK fixup not loaded".
Fix: define eval-expr and trampoline as real CEK wrappers at the
end of evaluator.sx (after cek-run is defined). The forward
declaration is now a harmless nil-returning stub. The fixup still
overrides with the iterative version, but even without it, eval
works correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
provide/context and scope/emit!/emitted now use CEK continuation
frames instead of an imperative global stack. Scope state is part
of the continuation — captured by shift, restored by k invocation.
New frame types:
- ProvideFrame: holds name + value, consumed when body completes
- ScopeAccFrame: holds name + mutable emitted list
New CEK special forms:
- context: walks kont for nearest ProvideFrame, returns value
- emit!: walks kont for nearest ScopeAccFrame, appends to emitted
- emitted: walks kont for nearest ScopeAccFrame, returns list
Kont walkers: kont-find-provide, kont-find-scope-acc
This fixes the last 2 test failures:
- provide survives resume: scope captured by shift, restored by k
- scope and emit across shift: accumulator preserved in continuation
JS Full: 870/870 (100%)
JS Standard: 747/747 (100%)
Python: 679/679 (100%)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Continuations are now multi-shot — k can be invoked multiple times.
Each invocation runs the captured frames via nested cek-run and
returns the result to the caller's continuation.
Fix: continue-with-call runs ONLY the captured delimited frames
(not rest-kont), so the continuation terminates and returns rather
than escaping to the outer program.
Fixed 4 continuation tests:
- shift with multiple invokes: (list (k 10) (k 20)) → (11 21)
- k returned from reset: continuation callable after escaping
- invoke k multiple times: same k reusable
- k in data structure: store in list, retrieve, invoke
Remaining 2 failures: scope/provide across shift boundaries.
These need scope state tracked in frames (not imperative push/pop).
JS 747/747, Full 868/870, Python 679/679.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Higher-order forms (map, filter, reduce, some, every?, for-each,
map-indexed) now evaluate their arguments via CEK frames instead
of nested trampoline(eval-expr(...)) calls.
Added HoSetupFrame — staged evaluation of HO form arguments.
When all args are evaluated, ho-setup-dispatch sets up the
iteration frame. This keeps a single linear CEK continuation
chain instead of spawning nested CEK instances.
14 nested eval-expr calls eliminated (39 → 25 remaining).
The remaining 25 are in delegate functions (sf-letrec, sf-scope,
parse-keyword-args, qq-expand, etc.) called infrequently.
All tests unchanged: JS 747/747, Full 864/870, Python 679/679.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The core spec is now one file: spec/evaluator.sx (2275 lines).
Three parts:
Part 1: CEK frames — state and continuation frame constructors
Part 2: Evaluation utilities — call, parse, define, macro, strict
Part 3: CEK machine — the sole evaluator
Deleted:
- spec/eval.sx (merged into evaluator.sx)
- spec/frames.sx (merged into evaluator.sx)
- spec/cek.sx (merged into evaluator.sx)
- spec/continuations.sx (dead — CEK handles shift/reset natively)
Updated bootstrappers (JS + Python) to load evaluator.sx as core.
Removed frames/cek from SPEC_MODULES (now part of core).
Bundle size: 392KB → 377KB standard, 418KB → 403KB full.
All tests unchanged: JS 747/747, Full 864/870, Python 679/679.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>