Runtime visibility fix:
- eval-hs now injects runtime helpers (hs-add, hs-falsy?, hs-strict-eq,
hs-type-check, hs-matches?, hs-contains?, hs-coerce) via outer let
binding so the tree-walker evaluator can resolve them
Parser fixes:
- null/undefined: return (null-literal) AST node instead of bare nil
(nil was indistinguishable from "no parse result" sentinel)
- === / !== tokenized as single 3-char operators
- mod operator: emit (modulo) instead of (%) — modulo is a real primitive
Compiler fixes:
- null-literal → nil
- % → modulo
- contains? → hs-contains? (avoids tree-walker primitive arity conflict)
Runtime additions:
- hs-contains?: wraps list membership + string containment
Tokenizer:
- Added keywords: a, an (removed — broke all tokenization), exist
- Triple operators: === and !== now tokenized correctly
Scorecard: 54/112 test groups passing, +23 from baseline.
Unlocked: really-equals, english comparisons, is-in, null is empty,
null exists, type checks, strict equality, mod.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract pure expression tests from the official _hyperscript test suite
and implement parser/compiler/runtime extensions to pass them.
Test infrastructure:
- 222 fixtures extracted from evalHyperScript calls (no DOM dependency)
- SX data format with eval-hs bridge and run-hs-fixture runner
- 24 suites covering expressions, comparisons, coercion, logic, etc.
Parser extensions (parser.sx):
- mod as infix arithmetic operator
- English comparison phrases (is less than, is greater than or equal to)
- is a/an Type typecheck syntax
- === / !== strict equality operators
- I as me synonym, am as is for comparisons
- does not exist/match/contain postfix
- some/every ... with quantifier expressions
- undefined keyword → nil
Compiler updates (compiler.sx):
- + emits hs-add (type-dispatching: string concat or numeric add)
- no emits hs-falsy? (HS truthiness: empty string is falsy)
- matches? emits hs-matches? (string regex in non-DOM context)
- New cases: not-in?, in?, type-check, strict-eq, some, every
Runtime additions (runtime.sx):
- hs-coerce: Int/Integer truncation via floor
- hs-add: string concat when either operand is string
- hs-falsy?: HS-compatible truthiness (nil, false, "" are falsy)
- hs-matches?: string pattern matching
- hs-type-check/hs-type-check!: lenient/strict type checking
- hs-strict-eq: type + value equality
Tokenizer (tokenizer.sx):
- Added keywords: I, am, does, some, mod, equal, equals, really,
include, includes, contain, undefined, exist
Scorecard: 47/112 test groups passing. 0 non-HS regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ~hyperscript/example component: shows "Try it" button with _= attr
for all on-click examples, source pre wraps long lines
- Added CSS for .active/.light/.dark demo classes with !important
to override Tailwind hover states
- Added #target div for the "put into" example
- Replaced broken examples (items, ~card, js-date-now) with
self-contained ones that use available primitives
- Repeat example left in with note: continuation after loop pending
- New test suite io-suspension-continuation documenting the stub VM
bug: outer do continuation lost after suspension/resume completes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Lambda calls in sx_call now go through the CEK machine instead of
returning a Thunk for the tree-walker trampoline. This lets perform/
IO suspension work everywhere — including hyperscript wait/bounce.
Key changes:
- sx_runtime: Lambda case calls _cek_eval_lambda_ref (forward ref)
- sx_vm: initializes ref with cek_step_loop + stub VM for suspension
- sx_apply_cek: VmSuspended → __vm_suspended marker dict (not exception)
- continue_with_call callable path: handles __vm_suspended with
vm-resume-frame, matching the existing JIT Lambda pattern
- sx_render: let VmSuspended propagate through try_catch
- Remove invalid io-contract test (perform now suspends, not errors)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
compile-let called scope-define-local eagerly as part of the let
binding, adding the new local to the scope before compile-expr ran
for the init expression. When nested lets rebound the same variable
(e.g. the hyperscript parser's 4 chained `parts` bindings), the init
expression resolved the name to the new uninitialized slot instead of
the outer one — producing nil where it should have read the previous
value.
Move scope-define-local after compile-expr so init expressions see the
outer scope's binding. Fixes all 11 JIT hyperscript parser failures.
3127/3127 JIT + non-JIT, 25/25 standalone hyperscript tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- jit_compile_lambda: merge closure bindings into effective_globals so
GLOBAL_GET resolves variables from let/define blocks (emit-on, etc.)
- code_from_value: scan bytecode for max LOCAL_GET/SET slot to compute
vc_locals (fixes LOCAL_GET overflow in large functions like hs-parse)
3127/3127 no-JIT, 3116/3127 JIT (11 hyperscript on-event: specific
bytecode correctness issue in recursive parser — wrong branch taken
strips on/event-name from result).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- jit_compile_lambda: call compile directly via VM when it has bytecode
(100-400x faster JIT compilation, server pre-warm 1.6s vs hung)
- code_from_value: scan bytecode for highest LOCAL_GET/SET slot to
compute vc_locals correctly (fixes hyperscript LOCAL_GET overflow)
- code_from_value: accept both compiler keys (bytecode) and SX VM
keys (vc-bytecode) for interop
- jit_compile_lambda: skip &key/:as params (compiler can't emit them)
- Test runner: seed VM globals with primitives + env bindings,
native vm-execute-module with suspension fallback to SX version,
_jit_refresh_globals syncs globals after module loading,
VmSuspended + "VM undefined" caught and sentineled
3127/3127 without JIT, 3116/3127 with JIT (11 hyperscript on-event
parsing — specific closure/scope issue, not infrastructure).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Break up the 1735-line handle_tool match into 45 individual handler functions
with hashtable-based dispatch. Add mtime-based file parse caching (AST + CST),
consolidated run_command helper replacing 9 bare open_process_in patterns,
require_file/require_dir input validation, and pagination (limit/offset) for
sx_find_across, sx_comp_list, sx_comp_usage. Also includes pending VM changes:
rest-arity support, hyperscript parser, compiler/transpiler updates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: pre-compiled compiler helper functions (compile-expr,
compile-cond, etc.) produce bytecode that loops when processing
deeply nested ASTs like tw-resolve-style. The test suite passes
because _jit_compiling prevents compiled function execution during
compilation — all functions run via CEK. The server pre-compiled
helpers, so they ran as bytecode during compilation, triggering loops.
Fix:
- _jit_compiling guard on the "already compiled" hook branch prevents
compiled functions from running during JIT compilation. Compilation
always uses CEK (correct for all AST sizes). Normal execution uses
bytecode (fast).
- "compile" itself marked as jit_failed_sentinel — never JIT compiled.
Runs via CEK, while its helpers use bytecode for normal (non-compile)
execution.
- Server hook uses call_closure (own VM per call) for IO suspension
safety. MCP uses call_closure_reuse (fast, no IO needed).
The underlying bytecode bug in the compiled helpers remains — fixing
it requires diagnosing which specific helper loops and why. This is
tracked as a separate issue. Server now starts in ~30s (pre-warm)
and serves pages correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three interacting JIT bugs caused infinite loops and server hangs:
1. _jit_compiling cascade: the re-entrancy flag was local to each
binary's hook. When vm_call triggered JIT compilation internally,
compiler functions got JIT-compiled during compilation, creating
infinite cascades. Fix: shared _jit_compiling flag in sx_vm.ml,
set in jit_compile_lambda itself.
2. call_closure always created new VMs: every HO primitive callback
(for-each, map, filter) allocated a fresh VM. With 43K+ calls
during compilation, this was the direct cause of hangs. Fix:
call_closure_reuse reuses the active VM by isolating frames and
running re-entrantly. VmSuspended is handled by merging frames
for proper IO resumption.
3. vm_call for compiled Lambdas: OP_CALL dispatching to a Lambda
with cached bytecode created a new VM instead of pushing a frame
on the current one. Fix: push_closure_frame directly.
Additional MCP server fixes:
- Hot-reload: auto-execv when binary on disk is newer (no restart needed)
- Robust JSON: to_int_safe/to_int_or handle null, string, int params
- sx_summarise depth now optional (default 2)
- Per-request error handling (malformed JSON doesn't crash server)
- sx_test uses pre-built binary (skips dune rebuild overhead)
- Timed module loading for startup diagnostics
sx_server.ml fixes:
- Uses shared _jit_compiling flag
- Marks lambdas as jit_failed_sentinel on compile failure (no retry spam)
- call_closure_reuse with VmSuspended frame merging for IO support
Compiled compiler bytecode bug: deeply nested cond/case/let forms
(e.g. tw-resolve-style) cause the compiled compiler to loop.
Workaround: _jit_compiling guard prevents compiled function execution
during compilation. Compilation uses CEK (slower but correct).
Test suite: 3127/3127 passed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
compile-cond expected flat syntax (cond test body test body ...) but
hs-parse uses clause syntax (cond (test body) (test body) ...). The
compiler treated the whole clause as the test expression, compiling
((and ...) (do ...)) as a function call — which tried to call the
and-result as a function, producing "not callable: false" JIT errors.
Now detects clause syntax (first arg is a list whose first element is
also a list) and flattens to the expected format before compilation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fixes in the HTTP server:
1. Read full POST body: the single 8192-byte read() could miss the body
if it arrived in a separate TCP segment. Now parses Content-Length
and reads remaining bytes in a loop.
2. Handler param binding: for POST/PUT/PATCH, check request-form before
request-arg. The old (or (request-arg n) (request-form n)) pattern
short-circuited on request-arg's "" default (truthy in SX), never
reaching request-form.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous commit fixed lib/vm.sx (SX spec) but the server uses
sx_vm.ml (hand-maintained native OCaml) and sx_vm_ref.ml (transpiled).
Both had the same globals-first lookup bug. Now all three implementations
check closure env before vm.globals, matching vm-global-set.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
vm-global-get checked vm.globals before closure-env, while vm-global-set
wrote to closure-env first. This asymmetry meant set! mutations to mutable
closure variables (e.g. parser position counters) were invisible to sibling
closures reading via JIT — they saw stale snapshots in the globals table.
Reversed vm-global-get lookup order: closure env → globals → primitives,
matching vm-global-set. Also enabled JIT in the MCP harness (compiler.sx
loading, env_bind hook for live globals sync, jit_try_call hook) so
sx_harness_eval exercises the same code path as the server.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- sx_server.ml: url_decode now decodes + as space (RFC 1866)
- parser.sx: changed begin block to do (no behavioral difference)
- handler: clean compile handler with source param
NOTE: hs-parse still returns (do) in ASER mode. Mutable closures
work (counter test passes), tokenizer works (JIT OK), but
hs-parse's 50+ define chain inside a single let fails in ASER.
Investigating as a separate evaluator issue.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sx/sx/hyperscript.sx — _hyperscript playground page at
/sx/(applications.(hyperscript))
- compile-result defcomp (SSR compilation display)
- pipeline documentation (tokenize → parse → compile)
- example showcases with pre-compiled output
- sx-post form → handler for interactive compilation
sx/sx/handlers/hyperscript-api.sx — POST handler:
/sx/(applications.(hyperscript.(api.compile)))
Accepts source param, returns compiled SX + parse tree HTML
NOTE: hs-parse returns (do) in server context — JIT/CEK runtime
issue where parser closures don't evaluate correctly. Works in
test runner (3127/3127). Investigating separately.
sx_server.ml — url_decode fix: decode + as space in form data
Standard application/x-www-form-urlencoded uses + for spaces.
Nav: _hyperscript added to Applications section.
Config: handler:hs- prefix added for handler dispatch.
3127/3127 tests, zero regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two hyperscript extensions beyond stock:
render ~component :key val [into|before|after target]
Tokenizer: ~ + ident → component token type
Parser: render command with kwargs and optional position
Compiler: emits (render-to-html ~comp :key val) or
(hs-put! (render-to-html ...) pos target)
Bridges hyperscript flow to SX component rendering
eval (sx-expression) — SX escape hatch
Inside eval (...), content is SX syntax (not hyperscript)
Parser: collect-sx-source extracts balanced parens from raw source
Compiler: sx-parse at compile time, inlines AST directly
Result: SX runs in handler scope — hyperscript variables visible!
Also supports string form: eval '(+ 1 2)' for backward compat
set name to "Giles"
set greeting to eval (str "Hello " name) -- name is visible!
16 new tests (parser + compiler + integration).
3127/3127 full build, zero regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
lib/hyperscript/integration.sx — connects compiled hyperscript to DOM:
hs-handler(src) — compile source → callable (fn (me) ...) via eval-expr-cek
hs-activate!(el) — read _="...", compile, execute with me=element
hs-boot!() — scan document for [_] elements, activate all
hs-boot-subtree!(root) — activate within subtree (for HTMX swaps)
Handler wraps compiled SX in (fn (me) (let ((it nil) (event nil)) ...))
so each element gets its own me binding and clean it/event state.
Double-activation prevented via data-hs-active marker.
12 integration tests verify full pipeline: source → compile → eval.
Handlers correctly bind me, support arithmetic, conditionals, sequences,
for loops, and repeat. 3111/3111 full build, zero regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tokenizer:
* and % now emit as operators (were silently swallowed)
Added keywords: install, measure, behavior, called
5 new arithmetic operator tests
Parser — expression layer:
Arithmetic (+, -, *, /, %) via parse-arith
Unary not, no, unary minus
the X of Y possessive (parse-the-expr)
as Type conversion, X in Y membership, array literals [...]
fetch URL parsing fixed — no longer consumes "as" meant for fetch
Parser — 8 new commands:
return, throw, append...to, tell...end, for...in...end,
make a Type, install Behavior, measure
Parser — 2 new features:
def name(params)...end, behavior Name(params)...end
Parser — enhanced:
wait for event [from target], on every event modifier
33 new parser tests (16 suites), 5 tokenizer tests.
3043/3043 full build, zero regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
lib/hyperscript/parser.sx — parses token stream from hs-tokenize into
SX AST forms. Covers:
Commands: add/remove/toggle class, set/put, log, hide/show, settle
Events: on with from/filter, command sequences
Sequencing: then, wait (with time units)
Conditionals: if/then/else/end
Expressions: property chains, it, comparisons, exists, refs
DOM traversal: closest, next, previous
Send/trigger events to targets
Repeat: forever, N times
Fetch/call with argument lists
55 tests across 12 suites. 3005/3005 full build, zero regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The /sx/ prefix mismatch: defpage declares paths like /language/docs/<slug>
but browser URLs are /sx/(language.(doc.slug)). find-matching-route used
starts-with? "/(", missing the /sx/ prefix entirely.
Fix: find-matching-route now uses (index-of path "/(") to detect the SX
URL portion regardless of prefix. Works for /sx/, /myapp/, any prefix.
No hardcoded paths.
Also fixed deps-satisfied?: nil deps (unknown) now returns false instead
of true, preventing client-side eval of pages with unresolved components.
Correctly falls back to server fetch.
Verified with Playwright: clicking "Getting Started" on the docs page now
shows "sx:route deps miss for docs-page" → "sx:route server fetch" instead
of the old "sx:route no match (51 routes)".
2 new router tests for prefix stripping. 2914/2914 total, zero failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New primitives in sx_primitives.ml:
char-at, char-code, parse-number — string inspection + conversion
regex-match, regex-match?, regex-find-all — PCRE pattern matching
regex-replace, regex-replace-first — PCRE substitution
regex-split — split by PCRE pattern
Uses Re.Pcre (OCaml re library) so regex patterns use the same syntax
as JS RegExp — patterns in .sx files work identically on browser and
server. Replaces the old test-only regex-find-all stub.
Also: split now handles multi-char separators via Re.
176 new tests (10 suites). 2912/2912 total, zero failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- run_tests.ml: foreign-check-args binding now matches ListRef (from
the list primitive) in addition to List
- test-foreign.sx: replace #t with true in guard clauses — SX parser
treats #t as a symbol, not a boolean
2800/2800 tests, zero failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Native functions (NativeFn/VmClosure) called through the CEK evaluator
can now have their Eval_errors caught by guard/handler-bind. The fix is
at the exact OCaml↔CEK boundary in continue-with-call:
- sx_runtime.ml: sx_apply_cek wraps native calls, returns error marker
dict {__eval_error__: true, message: "..."} instead of raising
- sx_runtime.ml: is_eval_error predicate checks for the marker
- spec/evaluator.sx: continue-with-call callable branch uses apply-cek,
detects error markers, converts to raise-eval CEK state
- transpiler.sx: apply-cek and eval-error? emit cases added
No mutable flags, no re-entry risk. Errors flow through the CEK handler
chain naturally. 2798/2800 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The transpiler's append! emit path didn't check ml-is-mutable-global?,
so (append! *provide-batch-queue* sub) wrote to a dead local variable
instead of the global _ref. This caused the combined test suite hang —
fire-provide-subscribers was silently broken before the local-ref shadow
removal, and now correctly modifies the global batch queue.
Also adds run_with_io error-to-raise conversion (kont_has_handler guard)
so native Eval_errors can be caught by CEK guard/handler-bind when running
through the test runner's IO-aware step loop.
2798/2800 tests pass. 2 foreign-type-checking failures remain: guard can't
catch Eval_error from native fns called through cek_run_iterative (the
handler dispatch itself uses cek_call which re-enters cek_run_iterative,
creating an infinite loop). Fix requires spec-level change: make (error)
use CEK raise instead of host-error.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ml-scan-set now checks ml-is-mutable-global? before adding set!/append!
targets to the needs-ref list. Previously, mutable globals like
*bind-tracking* got local `ref Nil` shadows that masked the global _ref,
causing `append!: expected list, got nil` in 43 bind-tracking tests.
Test runner: bind foreign registry functions (foreign-registered?,
foreign-lookup, foreign-names, foreign-register!, foreign-resolve-binding,
foreign-check-args, foreign-build-lambda) + initialize _cek_call_ref for
with-capabilities. 22/24 foreign tests now pass, 8 capabilities tests fixed.
Retranspiled sx_ref.ml — all mutable global shadows eliminated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FFI: define-foreign special form in evaluator — registry, param parser,
kwargs parser, binding resolver, type checker, lambda builder, dispatcher.
Generates callable lambdas that route through foreign-dispatch to host-call.
24 tests in test-foreign.sx (registry, parsing, resolution, type checking).
Transpiler: fix mutable global ref emission — ml-emit-define now emits
both X_ref = ref <init> and X_ = <init> for starred globals (was missing
the ref definition entirely, broke retranspilation). Add *provide-batch-depth*,
*provide-batch-queue*, *provide-subscribers* to mutable globals list.
Evaluator: add missing (define *provide-batch-queue* (list)) and
(define *provide-subscribers* (dict)) — were only in hand-edited sx_ref.ml.
Known: 36 bind-tracking + 8 capability test failures on retranspilation
(pre-existing transpiler local-ref shadowing bug, not caused by FFI).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tier 1 — Component keyword dispatch on VM:
- Components/islands JIT-compile bodies via jit_compile_comp
- parse_keyword_args matches keyword names against component params
- Added i_compiled field to island type for JIT cache
- Component calls no longer fall back to CEK
Tier 2 — OP_SWAP (opcode 7):
- New stack swap operation for future HO loop compilation
- HO forms already efficient via NativeFn + VmClosure callbacks
Tier 3 — Exception handler stack:
- OP_PUSH_HANDLER (35), OP_POP_HANDLER (36), OP_RAISE (37)
- VM gains handler_stack with frame depth tracking
- Compiler handles guard and raise as bytecode
- Functions with exception handling no longer cause JIT failure
Tier 4 — Scope forms as bytecode:
- Compiler handles provide, context, peek, scope, provide!,
bind, emit!, emitted via CALL_PRIM sequences
- Functions using reactive scope no longer trigger JIT failure
4 new opcodes (SWAP, PUSH_HANDLER, POP_HANDLER, RAISE) → 37 total.
2776/2776 tests pass, zero regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
run_tests.ml had a local _scope_stacks hash table, separate from
Sx_primitives._scope_stacks used by the CEK evaluator. SX-level
scope-push!/scope-peek used the local table, but step-sf-context's
scope_peek used the global one. Aser's provide handler pushed to
one table, context read from the other — always got nil.
Fix: alias run_tests.ml's _scope_stacks to Sx_primitives._scope_stacks.
2768/2768 OCaml tests pass. Zero failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Provide subscribers stored in global *provide-subscribers* dict (keyed
by name) instead of on provide frames. Fixes subscriber loss when
frames are reconstructed, and enables cross-cek_run notification.
Batch integration: batch-begin!/batch-end! primitives manage
*provide-batch-depth*. fire-provide-subscribers defers to queue when
depth > 0, batch-end! flushes deduped. signals.sx batch calls both.
context now prefers scope-peek over frame value — scope stack is the
source of truth since provide! always updates it (even in nested
cek_run where provide frames aren't on the kont).
2754/2768 OCaml (14 pre-existing). 32/32 WASM.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: context called inside lambdas (e.g. swap!) went through
nested cek_run with empty kont, so provide frames weren't found and
never tracked to *bind-tracking*.
Three changes in evaluator.sx:
- step-sf-context: track context names (not frames) to *bind-tracking*
— names work across cek_run boundaries via scope-peek fallback
- bind continue: resolve tracked names to frames via kont-find-provide
on rest-k before registering subscribers
- subscriber: use empty kont instead of kont-extract-provides — old
approach created provide frames whose continue handlers called
scope-pop!, corrupting the scope stack
2752/2768 OCaml tests pass (all 7 bind subscriber tests fixed).
32/32 WASM native tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bind is now a CEK special form that captures its body unevaluated,
establishes a tracking context (*bind-tracking*), and registers
subscribers on provide frames when context reads are tracked.
- bind special form: step-sf-bind, make-bind-frame, bind continue handler
- provide-set frame: provide! evaluates value with kont (fixes peek bug)
- context tracking: step-sf-context appends to *bind-tracking* when active
- scope-stack fallback: provide pushes to scope stack for cek-call contexts
- CekFrame mutation: cf_remaining/cf_results/cf_extra2 now mutable
- Transpiler: subscribers + prev-tracking field mappings, *bind-tracking* in ml-mutable-globals
- Test fixes: string-append → str, restored edge-cases suite
Passing: bind returns initial value, bind with expression, bind with let,
bind no deps is static, bind with conditional deps, provide! updates/multiple/nil,
provide! computed new value, peek read-modify-write, guard inside bind,
bind with string-append, provide! same value does not notify, bind does not
fire on unrelated provide!, bind sees latest value, bind inside provide scope.
Remaining: subscriber re-evaluation on provide! (scope-stack key issue),
batch coalescing (no batch support yet).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- _import_hook verifies library_loaded_p AFTER load_library_file
to catch cases where the file loads but define-library doesn't register
- Re-entry guard (_loading_libs) prevents infinite retry loops
- cek_run import patch deferred — retry approach infinite-loops because
cek_step_loop re-enters deeply nested eval contexts. Root cause:
eval_expr → cek_run → cek_step_loop processes the ENTIRE remaining
kont chain after import resolution, which includes rendering code that
triggers MORE eval_expr calls. Needs architectural solution (step-level
suspension handling, not run-level).
Server runs with 4 harmless IO suspension errors. These don't affect
functionality — symbols load via the global env.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The cek_run import handling (resume after hook loads library) caused
cek_step_loop to infinite-loop during aser page rendering. Root cause
not yet identified — the resumed CEK state never reaches terminal.
Reverted to original cek_run that throws "IO suspension in non-IO
context". The 4 server startup errors are harmless (files load
partially, all needed symbols available via other paths).
Import hook re-entry guard and debug logging retained for future work.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
step_sf_define_library now processes import clauses by evaluating
(import lib-spec) in the library env. Previously import clauses
were silently ignored, so libraries couldn't use symbols from
other libraries.
2694/2694 tests pass (11 new).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Capability primitives promoted from mcp_tree.ml to sx_primitives.ml:
- with-capabilities — push cap set, eval body, restore on exit/error
- current-capabilities — returns active capability list (nil = unrestricted)
- has-capability? — check if capability granted (true when unrestricted)
- require-capability! — raise if capability missing
- capability-restricted? — check if any restrictions active
Infrastructure: _cek_call_ref in sx_types.ml (forward ref pattern)
allows primitives to invoke the CEK evaluator without dependency cycles.
10 new tests: unrestricted defaults, scoping, nesting, restore-on-exit.
2693 total tests, 0 regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>