evaluator.sx: 11 section headers + 27 subgroup/function comments
documenting the CEK machine structure (state, frames, kont ops,
extension points, eval utilities, machine core, special forms,
call dispatch, HO forms, continue phase, entry points).
mcp_tree.ml: sx_summarise and sx_read_tree now inject file comments
into their output — comments appear as un-numbered annotation lines
between indexed entries, so indices stay correct for editing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When aser manages scope via scope_stacks but a sub-expression falls
through to the CEK machine, context/emit!/emitted couldn't find the
scope frames (they're in scope_stacks, not on the kont). Now the CEK
special forms fall back to env-bound primitives when kont lookup fails.
2568/2568 tests pass (was 2566/2568).
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>
- guard as CEK special form in evaluator.sx, desugars to call/cc +
handler-bind with sentinel-based re-raise (avoids handler loop)
- bootstrap.py: fix bind_lambda_with_rest type annotations, auto-inject
make_raise_guard_frame when transpiler drops it
- mcp_tree: add timeout param to sx_test (default 300s)
- 2566/2568 tests pass (2 pre-existing scope failures)
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>
Every define now has a ;; comment explaining its purpose. Groups:
- CEK state constructors and accessors (0-7)
- Continuation frames — one per special form (8-42)
- Continuation stack operations (43-56)
- Configuration and global state (57-67)
- Core evaluation functions (68-71)
- Special form helpers (72-89)
- CEK machine core (90-92)
- Special form CEK steps (93-119)
- Call dispatch and higher-order forms (120-133)
- Continuation dispatch (134-136)
- Entry points (137-140)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Named let's sf-named-let used call-lambda which returns a thunk that
was never trampolined. The body executed in a disconnected env, so
set! couldn't reach outer let bindings. Fixed by using cek-call which
evaluates through the full CEK machine with proper env chain.
Also converted test-named-let-set.sx from assert= (uses broken = for
lists) to deftest/assert-equal (uses deep equal?).
JS standard: 1120/1120, JS full: 1600/1600. Zero failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OCaml HTML renderer (sx_render.ml) silently returned "" when env_get
failed for primitive function calls (str, +, len, etc.) inside HTML
elements. The Eval_error catch now falls through to eval_expr which
resolves primitives correctly. Fixes 21 rendering tests.
Rename condition system special form from "signal" to "signal-condition"
in evaluator.sx, matching the OCaml bootstrapped evaluator (sx_ref.ml).
This avoids clashing with the reactive signal function. Fixes 9
condition system tests.
1166 passed, 0 failed.
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>
Two paren bugs in home-stepper.sx caused the home page to render blank:
1. Line 222 had one extra ) that prematurely closed the letrec bindings
list — rebuild-preview and do-back became body expressions instead
of bindings, making them undefined in scope.
2. Lines 241-308 were outside the let/letrec scope entirely — the outer
let closed at line 240, so freeze-scope, cookie restore, source
parsing, and the entire div rendering tree had no access to signals
or letrec functions.
Also hardens defisland to wrap multi-expression bodies in (begin ...),
matching the Python-side fix from 9f0c541. Both spec/evaluator.sx and
the OCaml transpiled sx_ref.ml are updated.
Adds SX Tools essay under Applications — the revised plan for structural
tree reading/editing tools for .sx files, motivated by this exact bug.
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>
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>