Consolidates the three-lane review (core K01-K110, hosts J*/C*/JS*/P*/S*,
conformance F1-F15) into plans/sx-review/:
- PLAN.md — 15 workstreams, phased execution, full per-finding coverage
ledger (every ~213 finding-instances mapped to a workstream + status)
- RULINGS.md — 40 draft normative rulings (Phase-0 gate)
- core.md / hosts.md / conformance.md — the lane evidence files
dc7aa709 quick-wins batch marked DONE in the ledger; K01 (guard re-raise
hang), S1 (live HTTP crash), K03 (shift-k), and W14 (test gate) flagged as
the highest-value open work.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1104 lines
77 KiB
Markdown
1104 lines
77 KiB
Markdown
# SX Hosts Review — findings (hosts axis)
|
||
|
||
Reviewer lane: per-target host implementations + FFI seam.
|
||
Targets: hosts/ocaml (kernel, VM/JIT, epoch protocol, server env, HTTP path),
|
||
hosts/javascript, hosts/python, hosts/native, WASM browser kernel, web/ adapters.
|
||
|
||
Status: COMPLETE — all 7 verification agents done (OCaml VM/JIT, sx_server epoch/env/HTTP,
|
||
JS host, Python host, web-FFI seam, cross-host parity, native/WASM). OCaml run_tests
|
||
baseline + `--jit` differential both done. The highest-severity findings were reproduced
|
||
live and self-verified by the lead reviewer (marked "self-verified").
|
||
|
||
Live HTTP-path verification (follow-up): booted `sx_server --http` on free ports (3 bounded runs,
|
||
own PIDs only — never pkill) and drove concurrent load. This UPGRADED three suspected HTTP-race
|
||
items to CONFIRMED — **S1** (multi-Domain render pool crashed intermittently under concurrent load;
|
||
empty responses, no exception, OOM ruled out at ~292 MB peak RSS), **S4** (routing failures served
|
||
as HTTP 200 and cached: cold 2.02s → warm 0.0005s), and **S5** (cache key ignores cookies/query —
|
||
three different `session=` cookies returned one identical cached body). **S2/S3** stay SUSPECTED:
|
||
their code paths were verified live but the stock static-docs app has no request-varying full-page
|
||
render to exhibit a visible symptom. Logs: /tmp/sx-review/http-server{,2,3}.log.
|
||
|
||
Finding tally: ~65 CONFIRMED (incl. S1/S4/S5 upgraded via live repro) + ~13 SUSPECTED
|
||
(incl. S2/S3) + 2 positive/informational. Full detail below.
|
||
|
||
TOP-LINE (most severe; CONFIRMED unless noted):
|
||
- **Production serving-JIT silently miscompiles** (J1): `(-> …)` in argument position and
|
||
user-macro args (J3) yield wrong values; JIT-fallback double-applies side effects (J2). The
|
||
http server registers the JIT hook UNCONDITIONALLY (sx_server.ml:4163) — live on sx.rose-ash.com.
|
||
- **One malformed line kills the whole sx_server process** (C1) — unguarded command-channel parse;
|
||
a non-ASCII byte does the same (C1b).
|
||
- **JS bundle is hollow** (C0a): render/engine/router/signals/all adapters transpile to 0 bytes
|
||
because the transpiler doesn't recurse into `define-library`; JS conformance is 2490 failing (C0b).
|
||
- **Python↔OCaml boundary + parser diverge**: NUL/escape corruption (C25), boundary validation is a
|
||
permanent no-op (C24), bridge desync is unrecoverable (S-bridge).
|
||
- **Cross-host primitive parity is broken** in ~30 confirmed ways (P1–P12): OCaml lossy float wire,
|
||
mixed-number sort, `into` needs a bridge; JS lenient-coercion cluster + missing equal?/eq?.
|
||
- **HTTP serving path breaks under concurrency (live-verified on a booted server):** the
|
||
multi-Domain render pool crashed intermittently under concurrent load — a genuine data race on
|
||
unsynchronized cross-Domain Hashtbls, OOM ruled out (S1, CONFIRMED); routing failures are served
|
||
as HTTP 200 and cached indefinitely (S4, CONFIRMED); the response cache key ignores cookies/query,
|
||
so it would serve one user's response to another on any cookie/auth-varying app (S5, CONFIRMED).
|
||
Still SUSPECTED: per-request globals read by queued workers (S2) and `expand-components?` removed
|
||
on the shared env by AJAX renders (S3) — both code paths verified live, but need a request-varying
|
||
app (not the static docs site) to exhibit a visible symptom.
|
||
|
||
Baseline (no JIT): `run_tests.exe` → **5762 passed, 274 failed**. 273 failures are `hs-*`
|
||
hyperscript suites (in-progress guest project — baseline caveat, not a host bug; note the
|
||
`can-map-an-array` / "map with block" line prints with a blank suite label due to C9 but belongs
|
||
to a hyperscript compat suite). The single genuine non-hyperscript host failure is C2 (r7rs
|
||
string->number shadow). `--jit` run: 5760p/276f, with 2 deterministic JIT-only divergences (J9). Logs:
|
||
/tmp/sx-review/ocaml-run_tests-baseline.log, /tmp/sx-review/ocaml-run_tests-jit.log
|
||
|
||
Numbering note: findings carry mnemonic prefixes (J* serving-JIT, C* OCaml/kernel/protocol,
|
||
JS* JavaScript host, P* cross-host parity, S* suspected). Order within CONFIRMED is roughly
|
||
severity-desc but read the whole section — prefixes were assigned as agents reported, not in rank order.
|
||
|
||
---
|
||
|
||
## CONFIRMED (most severe first)
|
||
|
||
> **Serving-JIT correctness cluster (J1–J8).** The OCaml `http_mode` (the production
|
||
> HTTP server behind sx.rose-ash.com) registers the JIT hook **unconditionally**
|
||
> (sx_server.ml:4163) — it is NOT gated by SX_SERVING_JIT (that env var only gates the
|
||
> epoch/persistent serving mode, sx_server.ml:5011). So every named top-level lambda
|
||
> invoked during page rendering is JIT-compiled on first call, and the divergences below
|
||
> are reachable in production for any rendered lambda that hits these patterns. The default
|
||
> `run_tests --jit` uses a *different, low-hit-rate* JIT path and surfaced only 2 divergent
|
||
> tests (see J9); the serving-JIT path is where the silent-wrong-value bugs live. All J1–J8
|
||
> reproduced live at the kernel level.
|
||
>
|
||
> **IMPORTANT reconciliation:** there is a belief on record (project memory / a comment at
|
||
> sx_server.ml:5001-5011) that "serving-JIT is OPT-IN via SX_SERVING_JIT=1, default OFF." That gate
|
||
> applies ONLY to the epoch/persistent serving mode. The actual HTTP server (`http_mode`) calls
|
||
> `register_jit_hook env` UNCONDITIONALLY at sx_server.ml:4163 with no env-var check. So anyone who
|
||
> concludes "JIT is off in production, therefore J1–J8 don't apply" is wrong — the http server that
|
||
> serves sx.rose-ash.com runs with JIT on. Confirmed by reading the http_mode entry and by the live
|
||
> boot logs (`[jit] … compile in …s` lines during every page render).
|
||
|
||
### J1. `->` threading miscompiles under serving-JIT — silent wrong value + duplicated side effects
|
||
- severity: critical
|
||
- confidence: CONFIRMED (reproduced live, self-verified)
|
||
- where: lib/compiler.sx:934-951 (compile-thread-step); stale twin hosts/ocaml/lib/sx_compiler.ml:178
|
||
- what: For `(-> v f g)` with non-empty rest-forms, compile-thread-step compiles `call-expr`
|
||
(pushing a value that's never popped) then recurses with `call-expr` re-embedded, compiling
|
||
it AGAIN. Each non-final step is evaluated once per remaining step (side effects duplicated)
|
||
and leaves a stack residue. When the `->` is argument ≥2 of a primitive call, CALL_PRIM pops
|
||
the residue instead of the sibling arg → silently wrong value. In arg position of a user call
|
||
the slot misaligns → "not callable" → self-heals via CEK fallback.
|
||
- repro: with lib/compiler.sx loaded, serving-JIT on:
|
||
`(+ 1 (-> 2 inc inc))` on CEK → `5`; JIT'd `(define (t1) (+ 1 (-> 2 inc inc)))` called → `7`
|
||
(self-verified: `(t1 ×5)` → `(7 7 7 7 7)`; same source with JIT off → 5). bytecode-inspect
|
||
shows the residue CONST/CALL_PRIM that's never popped.
|
||
|
||
### J2. JIT-fallback re-runs the entire call on the CEK → side effects execute twice (persist/counter/emit double-apply)
|
||
- severity: high
|
||
- confidence: CONFIRMED (reproduced live, self-verified)
|
||
- where: sx_server.ml:1630-1638, 1674-1680 (catch-all `| e -> … l_compiled <- jit_failed; None`);
|
||
sx_vm.ml:461-464, 490-493 (Component/Island `with _ -> cek_call_or_suspend` re-run)
|
||
- what: Any exception during VM execution of a JIT'd lambda (data-first HO form, macro call,
|
||
raise/error, call/cc, undefined global) marks it jit-failed and re-runs the WHOLE call under
|
||
the CEK. Mutations performed before the failure point are applied twice. A code comment claims
|
||
idempotence "for the host's durable reads" — but it is NOT idempotent for writes (persist
|
||
appends, counters, emits). The Component/Island path catches even VmSuspended, so a component
|
||
that performed IO can have that IO re-issued.
|
||
- repro (self-verified): `(do (define fb-count 0) (define (fb) (set! fb-count (+ fb-count 1))
|
||
(map (list 1 2) (fn (x) x))) (fb) fb-count)` → serving-JIT: `2` after ONE call; JIT off: `1`.
|
||
|
||
### J3. JIT'd functions eagerly evaluate arguments of user-macro calls before falling back
|
||
- severity: high
|
||
- confidence: CONFIRMED (reproduced live, self-verified)
|
||
- where: lib/compiler.sx compile-list/compile-call (no macro-expansion pass); sx_vm.ml:497 (vm_call has no Macro case)
|
||
- what: The compiler treats a user-macro call as an ordinary call — compiles and evaluates ALL
|
||
arguments, then vm_call raises "not callable: <macro>" and the hook re-runs on the CEK. Code a
|
||
macro guards from evaluation executes once in the VM.
|
||
- repro (self-verified): `my-unless` macro that expands `true` → skip body; JIT'd `(um)` that
|
||
does `(my-unless true (set! hit (+ hit 1)))` → serving-JIT: `hit = 1`; JIT off: `hit = 0`.
|
||
|
||
### J4. VM component calls misparse positional string args equal to a param name
|
||
- severity: high
|
||
- confidence: CONFIRMED (agent-reproduced)
|
||
- where: sx_vm.ml:144-156 (parse_keyword_args), fed by compile-expr lowering `:kw` to its string name
|
||
- what: The compiler erases the keyword/string distinction (keywords become their string names in
|
||
the constant pool), so parse_keyword_args treats ANY string matching a declared param as a
|
||
keyword marker and consumes the next value → silent wrong props/children, no error, no fallback.
|
||
- repro: `(defcomp ~box2 (&key title &rest children) (list "BOX" title children))`;
|
||
`(~box2 :title "T" "title" "x")` CEK → `("BOX" "T" ("title" "x"))`; JIT'd → `("BOX" "x" ())`.
|
||
|
||
### J5. Specialized opcodes freeze primitive semantics at compile time — redefinition ignored by JIT'd code
|
||
- severity: medium
|
||
- confidence: CONFIRMED (agent-reproduced)
|
||
- where: lib/compiler.sx:1061-1077 (specializes + - * / = < > cons 2-arg, not len first rest 1-arg → opcodes 160-172); sx_vm.ml:804-914
|
||
- what: CALL_PRIM resolves through vm.globals at run time (redefinitions respected), but the 12
|
||
specialized opcodes inline the original semantics. A function JIT'd before `(define + …)` keeps
|
||
the old `+` forever while the CEK sees the new one → CEK/JIT divergence.
|
||
- repro: `(define (q) (+ 1 2))` JIT'd → 3; `(define + (fn (a b) 999))`; `(q)` → 3 (stale OP_ADD)
|
||
vs fresh CEK → 999.
|
||
|
||
### J6. Redefining a primitive the compiler itself uses poisons subsequent JIT compilation
|
||
- severity: medium
|
||
- confidence: CONFIRMED (agent-reproduced)
|
||
- where: sx_vm.ml:732-766 (CALL_PRIM globals-first lookup) + the compiler running as VM bytecode
|
||
- what: The SX compiler's own bytecode calls +, first, nth… through CALL_PRIM → globals. After a
|
||
user redefines one, every subsequent JIT compile computes with the user's fn — observed as
|
||
corrupted pools/offsets ("CONST index 999 out of bounds"). Currently every hit errors → CEK
|
||
fallback (correct), but a redefinition returning plausible numbers could emit structurally-valid
|
||
wrong bytecode silently.
|
||
|
||
### J7. Data-first HO forms `(map coll fn)` / `-> coll (map fn)` always fail under the VM → permanent deopt (+ J2 double-effect on first call)
|
||
- severity: medium (perf/coverage; correctness preserved by fallback, but triggers J2)
|
||
- confidence: CONFIRMED (agent-reproduced)
|
||
- where: sx_primitives.ml:1584-1637 (map/filter/reduce/some/every?/for-each accept fn-first only); CEK ho-swap-args has no VM counterpart
|
||
- what: Every JIT'd function using the documented data-first arg order (or threading a collection
|
||
into an HO form) errors on first VM call and runs on the CEK forever. Results correct, but never
|
||
benefits from JIT and the first call incurs the J2 double-side-effect hazard.
|
||
|
||
### J8. Local bindings shadow HO-form names in the VM but not the CEK
|
||
- severity: medium-low
|
||
- confidence: CONFIRMED (agent-reproduced)
|
||
- where: compiler compile-call scope resolution (locals win) vs CEK HO-form dispatch (form wins)
|
||
- what: `(let ((map (fn (a b) 42))) (map 1 2))` → JIT'd: `42`; CEK inline: error "rest: 1 list arg"
|
||
(dispatches as HO map). Divergent semantics for the 7 HO-form names.
|
||
|
||
### J9. `run_tests --jit` diverges from interpreter on 2 hyperscript scoping tests (deterministic)
|
||
- severity: medium
|
||
- confidence: CONFIRMED (self-verified, 3× each mode, deterministic)
|
||
- where: default run_tests --jit path (guard-installing lambdas interpret-only); hs-upstream-core/scoping suite
|
||
- what: `hs-upstream-core/scoping > behavior scoping is isolated from other behaviors` and
|
||
`… from the core element scope` PASS interpreter-only but FAIL under `--jit` with an empty
|
||
result (`Expected 20, got `). Deterministic across 3 runs each mode. This is the low-hit-rate
|
||
default-JIT path (distinct from serving-JIT J1–J8), so it's a second, independent JIT/CEK
|
||
divergence surface. (Baseline 5762p/274f → --jit 5760p/276f; the hs-runtime-e2e "def function"
|
||
entry fails in BOTH and is not a JIT regression.)
|
||
|
||
### J10. Stale/incomplete Sx_compiler stub still bound as `compile`; JIT hook registered even if lib/compiler.sx fails to load
|
||
- severity: medium-low
|
||
- confidence: CONFIRMED (binding + staleness) / SUSPECTED (reachability)
|
||
- where: sx_server.ml:1315 (bind), comment at :5014 ("incomplete stub"); hosts/ocaml/lib/sx_compiler.ml (May-7, no POP after LOCAL_SET, no rest-arity/guard/match/scope)
|
||
- what: If lib/compiler.sx fails to load in serving-JIT mode the code warns but still registers
|
||
the JIT hook, so jit_compile_lambda would compile with the stub whose missing LOCAL_SET/POP
|
||
discipline can silently corrupt stacks (let-in-argument-position) rather than merely erroring.
|
||
|
||
### J11. JIT debug/aux paths diverge from the real VM (vm-trace, code_from_value locals-scan, resume-error handler, threshold)
|
||
- severity: low (tooling / edge)
|
||
- confidence: CONFIRMED (by reading; J-threshold self-observed)
|
||
- where: sx_vm.ml:1436-1632 (trace_run), :230-252 (code_from_value), :990-1005 (resume handler); sx_server.ml:1640-1656 (hook ignores jit_threshold)
|
||
- what: (a) vm-trace resolves CALL_PRIM via get_primitive BEFORE globals (real run does opposite →
|
||
user redefs invisible in trace), pushes Nil for non-NativeFn callees, inline ops 160-175 handle
|
||
only Number, OP_EQ uses OCaml structural `=` → traces can show different values than execution.
|
||
(b) code_from_value's max-local scan mis-walks operands for opcodes 8/33/35/144/51 (masked by +16
|
||
headroom). (c) resume-path error handler delivers `String msg` instead of the raised condition
|
||
value (only reachable via extension opcodes / hand-built bytecode). (d) the CEK-side hook ignores
|
||
jit_threshold=4 and compiles on the very FIRST call (why J1's t1 was already wrong on call #1);
|
||
lambdas defined inside let-wrapped library modules never JIT at all (e.g. all of lib/highlight.sx).
|
||
|
||
### J12. (verification, positive) perform/resume stack-misalignment from repro_jit_resume.ml is FIXED
|
||
- confidence: CONFIRMED — all 9 repro cases (direct/multiframe/map-callback/pending_cek/reduce/
|
||
nested-map perform) produce expected values; the "drop all-but-first in map/for-each" hazard from
|
||
the memory note does NOT reproduce at this level. Also verified matching (no divergence): closures
|
||
with set! on captured locals, &rest under/over-application, define in do/let, named let, letrec,
|
||
TCO + non-tail at 100k, quasiquote unquote/splice incl nested, dict literals, cond/case/when
|
||
multi-expr, and/or, string/arith edge prims, keyword equality, apply, guard (correctly JIT-excluded),
|
||
call/cc fallback, map over rest/filter/append lists, CEK↔VM global visibility both ways.
|
||
|
||
### C0a. JS transpiler is blind to `define-library` — the browser/CI bundle is hollow (render/engine/router/signals/all adapters = 0 bytes)
|
||
- severity: critical
|
||
- confidence: CONFIRMED (reproduced live, self-verified — fresh build)
|
||
- where: hosts/javascript/platform.py:15-35 (extract_defines), bootstrap.py:205-226
|
||
- what: extract_defines only extracts top-level `(define …)` / `register-special-form!`. The
|
||
spec+web files were since wrapped in R7RS `(define-library … (begin …))` (spec/render.sx,
|
||
web/adapter-html/-dom/-sx.sx, web/engine.sx, web/router.sx, spec/signals.sx, web/deps.sx,
|
||
web/page-helpers.sx, web/orchestration.sx, web/web-signals.sx, lib/freeze.sx), so those files
|
||
now transpile to ZERO bytes. The bundle still references the missing functions (renderToHtml
|
||
in its own public API; dom-* guarded by `typeof` checks that silently skip), so rendering,
|
||
signals, router, engine are simply ABSENT. spec/content.sx no longer exists and is silently
|
||
skipped. This is the JS host's single biggest defect and reframes its status.
|
||
- repro (self-verified): `python3 hosts/javascript/cli.py --output <scratch>/verify-js.js` then
|
||
per-section byte counts → render/adapter-html/adapter-sx/adapter-dom/engine/orchestration/deps/
|
||
page-helpers/router/signals/freeze/content all = 6 bytes (marker only); evaluator 140KB,
|
||
signals-web 99KB, boot 14KB, web-forms 8KB, parser 12KB survive (not define-library-wrapped).
|
||
|
||
### C0b. `node run_tests.js` fails 2490/5086 — the JS conformance/CI gate is structurally red
|
||
- severity: critical
|
||
- confidence: CONFIRMED (reproduced live, self-verified)
|
||
- where: hosts/javascript/run_tests.js; .gitea/run-ci-tests.sh (CI gates on this)
|
||
- what: `node hosts/javascript/run_tests.js` → **2596 passed, 2490 failed** (exit 1); `--full`
|
||
→ 2453p/3203f. A fresh rebuild produces identical failures — structural, not stale artifact.
|
||
Same corpus on OCaml → 5762p/274f. Failure signature: 1190× `Undefined symbol: dom-set-inner-html`,
|
||
538× hs-to-sx*, 229× `renderToHtml is not defined`, 13× `makeRtd is not defined` — the hollow
|
||
bundle (C0a) plus a runner that never learned to load the newer lib deps (parser-combinators,
|
||
gql, hyperscript) the OCaml runner loads. Memory's "957/957 standard, 1080/1080 full" is stale.
|
||
The JS host is alive as a CI/test host (sx-build-all.sh, run-tests.sh, .gitea CI) but DEAD as a
|
||
served artifact (the live shell injects wasm/sx_browser.bc.wasm.js, not scripts/sx-browser.js) —
|
||
so this drift breaks CI/conformance, not visitors.
|
||
- repro (self-verified): `timeout 400 node hosts/javascript/run_tests.js` → `Results: 2596 passed, 2490 failed`.
|
||
|
||
### C1. Malformed command line crashes the whole persistent/site sx_server process
|
||
- severity: critical
|
||
- confidence: CONFIRMED (reproduced live)
|
||
- where: hosts/ocaml/bin/sx_server.ml:5048 (persistent mode) and :4950 (site_mode)
|
||
- what: The dispatch loop calls `Sx_parser.parse_all line` outside any `try`; the
|
||
enclosing handler only catches `End_of_file`, so `Sx_types.Parse_error` from one
|
||
malformed line kills the entire process (the shared command channel used by
|
||
bridges/conformance runners). site_mode has the identical unguarded parse.
|
||
- repro: `printf '(epoch 2)\n(eval "(+ 1 2"\n(epoch 3)\n(eval "99")\n' | timeout 60 hosts/ocaml/_build/default/bin/sx_server.exe`
|
||
→ `Fatal error: exception Sx_types.Parse_error("Unterminated list")`; subsequent
|
||
commands never run. Same with a plain-garbage line (`not an s-expr ]]] {{{`).
|
||
|
||
### C10. Served browser JIT compiler is one fix behind lib/compiler.sx — the HO-loop desugar parity fix never shipped to the browser
|
||
- severity: high
|
||
- confidence: CONFIRMED (drift) / SUSPECTED (user impact)
|
||
- where: shared/static/wasm/sx/compiler.sx + compiler.sxbc vs lib/compiler.sx:390-443
|
||
- what: Commit 921db09f (2026-06-30, "jit: HO-loop desugar + --jit == CEK parity") added
|
||
map/filter/reduce/for-each lambda-inlining to lib/compiler.sx, but the copy served to
|
||
the browser (content from 59ac51a8, 2026-06-29) lacks all 54 lines; the .sxbc was
|
||
rebuilt later (1e2ff387, Jul 2) but from the stale source. The browser JIT therefore
|
||
misses the exact parity fix for the known "map/for-each drops all-but-first" bug class
|
||
fixed server-side.
|
||
- repro: `diff lib/compiler.sx shared/static/wasm/sx/compiler.sx | head` → `390,443d389`;
|
||
`grep -c "_hml" lib/compiler.sx shared/static/wasm/sx/compiler.sx` → 6 vs 0.
|
||
|
||
### C11. Client reads module-manifest.sx that nothing generates anymore — stale lazy-dep graph
|
||
- severity: high
|
||
- confidence: CONFIRMED
|
||
- where: consumer hosts/ocaml/browser/sx-platform.js:575 (fetches `sx/module-manifest.sx`);
|
||
generator hosts/ocaml/browser/compile-modules.js:504 (writes `module-manifest.json` only)
|
||
- what: `loadWebStack()` drives client module loading from `module-manifest.sx`, but the
|
||
generator now only emits `.json`; no `.sx` writer exists. The served `.sx` manifest is
|
||
from 2026-04-16 while modules are from Jul 1-2. Concretely `_entry :lazy-deps` is
|
||
missing `hs-worker` and `hs-prolog`, so those plugins never lazy-load in the browser;
|
||
any future dep/export change is invisible to the client.
|
||
- repro: `grep '_entry' shared/static/wasm/sx/module-manifest.sx` → lazy-deps ends at
|
||
`hs-integration hs-htmx`; module-manifest.json includes `hs-worker`, `hs-prolog`.
|
||
|
||
### C1b. Non-ASCII byte on the top-level command channel also crashes the sx_server process (2nd instance of C1)
|
||
- severity: critical
|
||
- confidence: CONFIRMED (reproduced live, self-verified)
|
||
- where: hosts/ocaml/lib/sx_parser.ml:38 (is_symbol_char is ASCII-only) + the unguarded
|
||
command-channel parse (C1, sx_server.ml:5048)
|
||
- what: A non-ASCII byte reaching the kernel's top-level command reader raises an uncaught
|
||
`Parse_error` that kills the subprocess — same unguarded-parse root cause as C1, but reachable
|
||
from any client that forwards a unicode-named symbol. The Python parser *accepts and serializes*
|
||
bare unicode symbols (shared/sx/parser.py:158 accepts -), so a Python-bridge caller
|
||
can trigger this. Inside `(eval "…")` it's a catchable error; on the command channel it's fatal.
|
||
- repro: `printf '(epoch 1)\n(eval (quote café))\n(epoch 2)\n(eval "99")\n' | timeout 30 hosts/ocaml/_build/default/bin/sx_server.exe`
|
||
→ `Fatal error: exception Sx_types.Parse_error("Unexpected char: \195 …")`; epoch-2 never runs.
|
||
|
||
### JS1. `define-record-type` broken on JS: `makeRtd` platform constructor missing
|
||
- severity: high
|
||
- confidence: CONFIRMED (agent-reproduced)
|
||
- where: spec/evaluator.sx:2584 calls `(make-rtd …)`; no `makeRtd` in hosts/javascript/platform.py
|
||
- what: The transpiled evaluator's record support calls makeRtd, which the JS platform never
|
||
defines. Any define-record-type throws; test-cross-lang-types.sx (18 deftests) aborts.
|
||
- repro: `(define-record-type point (make-point x y) point? (x point-x) (y point-y))` →
|
||
JS `ERROR: makeRtd is not defined`; OCaml `(point-x (make-point 3 4))` → 3.
|
||
|
||
### JS2. `host-callback` / `host-await` never fire for SX lambdas (wrong type tag)
|
||
- severity: high
|
||
- confidence: CONFIRMED (agent-reproduced)
|
||
- where: hosts/javascript/platform.py:2332-2342 (host-callback), 2354-2361 (host-await)
|
||
- what: Both check `fn._type === "lambda"`, but Lambda instances are tagged `_lambda === true`
|
||
(platform.py:889); the check never matches, so SX lambdas wrapped as JS callbacks become
|
||
silent no-op functions. Any DOM event/promise callback registered through these does nothing.
|
||
- repro: JS `(let ((cb (host-callback (fn (x) (* x 2))))) (cb 21))` → nil; OCaml → 42.
|
||
|
||
### JS3. JS arithmetic silently drops extra arguments (arity not checked)
|
||
- severity: high
|
||
- confidence: CONFIRMED (agent-reproduced)
|
||
- where: hosts/javascript/platform.py:1021 (-), 1045 (/), 1086-1101 (< > <= >=)
|
||
- what: Non-rational `-` returns `args[0]-args[1]` regardless of arity; `/` is strictly 2-arg;
|
||
comparisons ignore args past the 2nd. OCaml errors on wrong arity, so buggy guest code passes
|
||
silently on JS with wrong values.
|
||
- repro: JS `(- 5 1 2)` → 4 (OCaml 2); `(/ 24 2 3)` → 12 (OCaml errors); `(< 1 2 3)` → true (OCaml errors).
|
||
|
||
### JS4. Parser rejects `.` as a symbol — whole hyperscript-parser test file aborts
|
||
- severity: high
|
||
- confidence: CONFIRMED (agent-reproduced)
|
||
- where: hosts/javascript/platform.py:2622 (_identStartRe lacks `.`)
|
||
- what: `.` is a valid symbol on OCaml but "Unexpected character: ." on JS. test-hyperscript-parser.sx
|
||
uses `(quote .)`, so the file fails to parse and all 93 deftests are skipped (counted as 1 failure).
|
||
- repro: JS `(str (quote .))` → ERROR; OCaml → `"."`, type-of → "symbol". (Handoff: spec must rule
|
||
whether `.` is a legal symbol.)
|
||
|
||
### JS5. run_tests.js harness undercounts + shadows semantics
|
||
- severity: medium
|
||
- confidence: CONFIRMED (agent-reproduced)
|
||
- where: hosts/javascript/run_tests.js:457-467, 78-79, 88-96, 126
|
||
- what: (1) Each test file is wrapped in one try/catch — the first throw abandons the rest of the
|
||
file and counts as ONE failure (~111 deftests silently skipped this run). (2) `env-bind!` and
|
||
`env-set!` are defined identically (`e[k]=v`) — no scope-chain walk for env-set!, contradicting
|
||
the platform's own envSet (platform.py:2098) and the spec bind/set distinction, so tests
|
||
exercising that semantics test the wrong thing. (3) `sha3-256` is a fake 32-bit hash padded to
|
||
64 hex — cross-host content-address comparison is meaningless. (4) lib/harness load errors are
|
||
logged but never counted.
|
||
|
||
### JS6. Transpiled `(str …)` doesn't skip nil (emission vs runtime disagree)
|
||
- severity: medium
|
||
- confidence: CONFIRMED (agent, emission rule read + runtime verified opposite)
|
||
- where: hosts/javascript/transpiler.sx js-emit-list str clause vs runtime platform.py:1138-1144
|
||
- what: The transpiler emits `String(a)+String(b)` for str, so transpiled spec code concatenates
|
||
`String(NIL)` → "nil", while the CEK runtime skips nils. Identical expressions disagree between
|
||
transpiled internals and guest code.
|
||
|
||
### JS7. Transpiler has no quasiquote emission; quoted dicts emit garbage
|
||
- severity: low
|
||
- confidence: CONFIRMED (code read)
|
||
- where: hosts/javascript/transpiler.sx js-emit-quote (no dict clause → `[object Object]`); no quasiquote case in js-emit-list
|
||
- what: Only a hazard if a transpiled spec file uses top-level quasiquote or quoted dict literals;
|
||
runtime quasiquote via CEK is correct on both hosts.
|
||
|
||
### JS8. Stale JS host metadata + stale checked-in sx-full-test.js
|
||
- severity: low
|
||
- confidence: CONFIRMED (agent)
|
||
- where: bootstrap.py:2-11 docstring (refs deleted js.sx/run_js_sx.py/platform_js.py/shared/sx/ref/*),
|
||
bootstrap.py:287 (bad default output path), cli.py:17 (_PROJECT one level too high → /root on sys.path),
|
||
manifest.py (reports the hollow bundle as healthy); shared/static/scripts/sx-full-test.js (Apr 26, predates open-input-string → 35 --full failures)
|
||
|
||
### C24. Python↔OCaml boundary validation is permanently a no-op (imports a nonexistent module)
|
||
- severity: high
|
||
- confidence: CONFIRMED (reproduced live, self-verified)
|
||
- where: shared/sx/boundary.py:34
|
||
- what: `_load_declarations()` does `from .ref.boundary_parser import …` but
|
||
`shared/sx/ref/boundary_parser.py` does not exist (real file is hosts/python/boundary_parser.py).
|
||
The ModuleNotFoundError is swallowed by a bare `except Exception` and NOT cached, so every
|
||
validate_primitive/validate_io/validate_helper/validate_boundary_value silently returns without
|
||
checking — even under SX_BOUNDARY_STRICT=1. The whole SX-boundary enforcement subsystem is dead.
|
||
NOTE: shared/sx/ is load-bearing in production (Quart services render SX via the bridge), so this
|
||
is not dead-host code.
|
||
- repro: `SX_BOUNDARY_STRICT=1 python3 -c "from shared.sx import boundary; boundary.validate_primitive('definitely-not-a-primitive-xyz')"`
|
||
→ `pure: 0` and `NO ERROR RAISED`.
|
||
|
||
### C25. Parser string-escape divergence corrupts data across the Python↔OCaml boundary (\0, \x, unknown escapes)
|
||
- severity: high
|
||
- confidence: CONFIRMED (reproduced live, self-verified)
|
||
- where: shared/sx/parser.py:108 (_ESCAPE_MAP) / :471 (serialize) vs hosts/ocaml/lib/sx_parser.ml:61-79
|
||
- what: Python maps `\0`→NUL and drops the backslash on unknown escapes (`\x`→`x`); OCaml treats
|
||
`\0` as literal backslash+zero and preserves the backslash on unknown escapes (`\x`→`\x`).
|
||
Python serialize() emits `\0` for a NUL char, so a 3-char Python string round-trips to a
|
||
4-char OCaml string — silent corruption on any path that serializes a NUL-bearing string and
|
||
re-parses it in the kernel. `</script` also diverges (Python `<\/script` vs OCaml differently).
|
||
- repro: `python3 -c "from shared.sx.parser import serialize; print(repr(serialize('a\x00b')))"` → `'"a\\0b"'`;
|
||
kernel `(len "a\0b")` → `4` (should be 3).
|
||
|
||
### C26. Python parser accepts bare unicode symbols the OCaml kernel rejects → error or crash
|
||
- severity: high
|
||
- confidence: CONFIRMED (reproduced live, self-verified — see C1b)
|
||
- where: shared/sx/parser.py:158 (SYMBOL regex accepts -) / :462 (serialize bare) vs sx_parser.ml:38 (ASCII-only)
|
||
- what: The Python tokenizer accepts unicode letters in symbol names and serializes them bare;
|
||
OCaml's symbol charset is ASCII-only and raises Parse_error on the first non-ASCII byte. In an
|
||
`(eval "…")` string it's catchable; on the command channel it's a fatal subprocess kill (C1b).
|
||
- repro: `python3 -c "from shared.sx.parser import serialize; from shared.sx.types import Symbol; print(serialize(Symbol('café')))"` → `café` (bare); kernel crashes on it.
|
||
|
||
### C27. Dict key ordering not preserved across hosts (wire/golden/cache-key mismatch)
|
||
- severity: medium
|
||
- confidence: CONFIRMED (reproduced live, self-verified)
|
||
- where: shared/sx/parser.py:394-413 / :488-493 vs OCaml kernel dict serialization
|
||
- what: Python dict preserves insertion order; the OCaml kernel reorders/reverses. Any cache key,
|
||
golden fixture, or wire payload relying on stable dict serialization mismatches between the
|
||
Python-serialized form and the kernel-serialized form.
|
||
- repro: Python `serialize(parse('{:a 1 :b "two"}'))` → `{:a 1 :b "two"}`; kernel `(quote {:a 1 :b "two"})` → `{:b "two" :a 1}`.
|
||
|
||
### C28. Two distinct `SxExpr` classes; serialize() double-quotes the types.py one (nested wire corruption)
|
||
- severity: medium
|
||
- confidence: CONFIRMED (agent-reproduced)
|
||
- where: shared/sx/parser.py:71 and shared/sx/types.py:408 (two unrelated `class SxExpr(str)`); parser.py:441 isinstance checks only the parser one
|
||
- what: parser.SxExpr and types.SxExpr are unrelated classes. serialize() emits pre-built SX
|
||
unquoted only for parser.SxExpr; a types.SxExpr falls through to the plain-string branch and is
|
||
double-quoted/escaped. handlers.py:182 returns `SxExpr(result_sx)` (the types one), so any path
|
||
nesting a handler result into an AST and re-serializing wraps the wire fragment in quotes.
|
||
- repro: `serialize([Symbol('div'), types.SxExpr('(~inner :a 1)')])` → `(div "(~inner :a 1)")` (wrong)
|
||
vs parser.SxExpr → `(div (~inner :a 1))` (right).
|
||
|
||
### C29. Reader-macro auto-resolution broken (OcamlSync.start() doesn't exist)
|
||
- severity: medium
|
||
- confidence: CONFIRMED (agent-reproduced)
|
||
- where: shared/sx/parser.py:57 (`_sync.start()`); shared/sx/ocaml_sync.py (class has `_ensure`, no `start`)
|
||
- what: `_resolve_sx_reader_macro` (auto-resolving `#name` reader macros like `#z3`) calls
|
||
`OcamlSync().start()`, but OcamlSync exposes `_ensure()`. The AttributeError is caught by
|
||
`except Exception: return None`, so every auto-resolve silently returns None →
|
||
`ParseError("Unknown reader macro: #…")`. The feature is non-functional.
|
||
- repro: `python3 -c "from shared.sx.ocaml_sync import OcamlSync; print(hasattr(OcamlSync(),'start'))"` → `False`.
|
||
|
||
### C30. Python standalone host (hosts/python/) is a non-functional stale target
|
||
- severity: medium
|
||
- confidence: CONFIRMED (agent-reproduced)
|
||
- where: hosts/python/bootstrap.py, platform.py, transpiler.sx
|
||
- what: hosts/python/ masquerades as a supported host but its bootstrapper emits syntactically
|
||
invalid Python (`if sx_truthy((len(*winders*) > …))` — SyntaxError) and can't produce a working
|
||
evaluator. Nothing imports it except its own internal bootstrap→platform. The hosts/python/tests/
|
||
dir referenced by memory/RESTRUCTURE_PLAN.md does not exist. Transpiler also warns its spec has
|
||
drifted (`eval.sx dispatches forms not in special-forms.sx: provide, scope, define-record-type …`).
|
||
- repro: `python3 hosts/python/bootstrap.py > /tmp/t.py; python3 -c "import t"` → SyntaxError at line 1564.
|
||
|
||
### C31. shared/sx/tests/ is 14/33 files broken; test_ocaml_bridge.py has 5 live failures
|
||
- severity: medium
|
||
- confidence: CONFIRMED (agent-reproduced)
|
||
- where: shared/sx/tests/ (dangling `shared.sx.ref.sx_ref` imports); test_ocaml_bridge.py
|
||
- what: 14/33 test files fail collection (import the deleted shared.sx.ref.sx_ref). test_ocaml_bridge.py
|
||
has 5 failures: `test_parse_response_ok_number` (the `(ok N)` epoch-ambiguity below) and 4
|
||
`test_render_*` that still use flat `(let (x v) body)` binding syntax the kernel now rejects
|
||
(real Python-test/kernel drift).
|
||
|
||
### C2. lib/r7rs.sx shadows `string->number`, dropping the spec's optional radix — spec test fails on the OCaml runner
|
||
- severity: medium
|
||
- confidence: CONFIRMED (baseline test failure + root cause isolated + differential vs bare server)
|
||
- what: spec/primitives.sx:1104 declares `string->number` with `&rest (radix :as number)`,
|
||
and the native prim (hosts/ocaml/lib/sx_primitives.ml:486) accepts 1–2 args. But
|
||
lib/r7rs.sx:83 redefines it as `(fn (s) ...)` (1 param) and re-exports to the global
|
||
namespace ("backward compatibility" import at end of file). Every environment that
|
||
loads r7rs.sx (run_tests does, at run_tests.ml:3711) loses the radix arg:
|
||
`(string->number "ff" 16)` → `string->number expects 1 args, got 2`.
|
||
Note the adjacent `number->string` wrapper (r7rs.sx:76) does this correctly by
|
||
delegating to the captured native prim for the radix case — `string->number` just
|
||
wasn't given the same treatment. The shadow also changes 1-arg semantics
|
||
(parse-int/parse-float vs the native parser).
|
||
- repro: baseline log line 4179 `FAIL: > string->number: string->number expects 1 args, got 2`
|
||
(spec/tests/test-math.sx:112). Bare server WITHOUT r7rs loaded:
|
||
`printf '(epoch 1)\n(eval "(string->number \"ff\" 16)")\n' | timeout 30 hosts/ocaml/_build/default/bin/sx_server.exe` → `255` (correct).
|
||
|
||
### C12. compile-modules.js SOURCE_MAP has 5 dead source paths that silently no-op
|
||
- severity: medium
|
||
- confidence: CONFIRMED
|
||
- where: hosts/ocaml/browser/compile-modules.js:38-49
|
||
- what: `signals.sx→web/signals.sx`, `hypersx.sx→web/hypersx.sx`, `tw-layout/tw-type/tw.sx→web/tw-*.sx`
|
||
all point at files that no longer exist (moved to web/web-signals.sx, web/lib/hypersx.sx,
|
||
shared/sx/templates/tw-*.sx). `fs.existsSync` guards make the skips silent, so edits to
|
||
the real sources are never synced by this script — the rot vector that plausibly produced
|
||
the C10 compiler drift.
|
||
- repro: `for f in web/signals.sx web/hypersx.sx web/tw-layout.sx web/tw-type.sx web/tw.sx; do [ -f $f ] || echo MISSING $f; done` → all 5 MISSING.
|
||
|
||
### C13. test_platform.js broken — references deleted web/signals.sx (browser platform test tier unrunnable)
|
||
- severity: medium
|
||
- confidence: CONFIRMED
|
||
- where: hosts/ocaml/browser/test_platform.js:65
|
||
- what: The "full WASM + platform stack" Node test crashes at load: web/signals.sx was
|
||
renamed to web/web-signals.sx and the file list was never updated.
|
||
- repro: `timeout 60 node hosts/ocaml/browser/test_platform.js` → `Error: ENOENT … web/signals.sx`.
|
||
|
||
### C14. hosts/ocaml/browser/dist/ is an 8-week-stale duplicate bundle that sx-build-all.sh still feeds
|
||
- severity: medium
|
||
- confidence: CONFIRMED
|
||
- where: hosts/ocaml/browser/dist/ (72 files, May 7) vs shared/static/wasm/ (101 files, Jul 1-2);
|
||
scripts/sx-build-all.sh:19-32
|
||
- what: Runtime serving is exclusively from shared/static/wasm/ (shared/sx/helpers.py:1080,
|
||
sx_server.ml:3349/4525), but bundle.sh/compile-modules.js default to dist/ and
|
||
sx-build-all.sh still syncs into and compiles from it — a full run can compile .sxbc
|
||
from May-7-era sources for anything its sync list misses. Two half-overlapping bundle
|
||
dirs are the root cause of C10/C11.
|
||
- repro: `ls hosts/ocaml/browser/dist/sx | wc -l` → 72 vs 101; dist mtimes 2026-05-07.
|
||
|
||
### C15. Git-tracked stale kernel artifacts in hosts/ocaml/shared/static/wasm (5MB dead blob)
|
||
- severity: medium
|
||
- confidence: CONFIRMED
|
||
- where: hosts/ocaml/shared/static/wasm/{sx_browser.bc.js (5.0MB Apr 9), sx_browser.bc.wasm.js, sx-platform.js (Apr 16)}
|
||
- what: A wrong-cwd copy of the deployed wasm dir, committed to git, months stale
|
||
(missing newer kernel exports like resetStepCount/setStepLimit); nothing references it.
|
||
- repro: `git ls-files hosts/ocaml/shared/` → 3 files.
|
||
|
||
### C19. HTML vs DOM adapter disagree on non-boolean attributes with raw boolean values (SSR/hydration mismatch)
|
||
- severity: medium
|
||
- confidence: CONFIRMED (HTML side run; DOM side unambiguous in source)
|
||
- where: spec/render.sx:render-attrs (~263-283) vs web/adapter-dom.sx:render-dom-element (~356-366)
|
||
- what: For an attr NOT in BOOLEAN_ATTRS with value `true`: HTML emits `attr="true"`,
|
||
DOM emits `attr=""`. With value `false`: HTML emits `attr="false"`, DOM omits the
|
||
attribute. Any `data-*`/aria attr driven by a boolean diverges between SSR and
|
||
client render → hydration mismatch + needless sync-attrs morph churn.
|
||
- repro: `sx_harness_eval (render-to-html (quote (div :data-x false :data-y true :data-z nil)))`
|
||
→ `<div data-y="true" data-x="false"></div>`; DOM branch produces `data-y=""` and drops `data-x`.
|
||
|
||
### C20. CSRF token attached to cross-origin fetches; computed `cross-origin` flag is dead
|
||
- severity: medium
|
||
- confidence: CONFIRMED (code path) / SUSPECTED (browser-level exploitability)
|
||
- where: web/orchestration.sx:do-fetch (line 183 header injection, 207-208 cross-origin flag);
|
||
consumer web/lib/boot-helpers.sx:fetch-request (354-390)
|
||
- what: do-fetch adds `X-CSRFToken` unconditionally (no origin gate) and computes a
|
||
`cross-origin` field that fetch-request never reads (nor sets credentials/mode).
|
||
Net: CSRF token leaks to third-party endpoints; the cross-origin classification is inert.
|
||
No test exercises CSRF header injection or origin gating.
|
||
|
||
### C21. Test harness IO model bypasses the real perform/suspend path (structural coverage gap)
|
||
- severity: medium
|
||
- confidence: CONFIRMED
|
||
- where: spec/harness.sx default-platform (line 34), make-interceptor (52), install-interceptors (55)
|
||
- what: The harness binds IO ops as plain synchronous NativeFns, never as perform-suspending
|
||
primitives — so no harness test can exercise CEK suspend/cek-resume or the VM inline-settle
|
||
path. The known HO+perform element-drop class (S10) is structurally invisible to the harness:
|
||
`(map (fn (u) (fetch u)) …)` under a mock returns all elements because fetch is a direct call.
|
||
- repro: sx_harness_eval of `(map (fn (u) (get (fetch u) "status")) (list "a" "b" "c"))` with
|
||
fetch mock → `(200 200 200)`, all logged — real serving path is the one at risk.
|
||
|
||
### C22. Harness interceptor logs IO call only after the mock returns — throwing mocks invisible
|
||
- severity: low
|
||
- confidence: CONFIRMED
|
||
- where: spec/harness.sx:make-interceptor (line 52)
|
||
- what: `append!` to the IO log happens after mock-fn returns; a raising mock leaves no
|
||
log entry, so assert-io-called/count falsely report "never invoked" on error-path tests.
|
||
(Positive side-finding: host IO errors DO surface as catchable SX errors.)
|
||
- repro: try-catch around `(fetch "http://a")` with mock raising "boom-io" → caught, but
|
||
IO log shows "(no IO calls)".
|
||
|
||
### C23. adapter-dom test suite tests membership predicates only — zero render-output tests
|
||
- severity: low
|
||
- confidence: CONFIRMED
|
||
- where: web/tests/test-adapter-dom.sx (18 deftests)
|
||
- what: All 18 tests assert `*-is-a-render-form?`/RENDER_HTML_FORMS membership; none test
|
||
actual render-to-dom output (boolean attrs, on-*/bind/ref/key, reactive attrs, void
|
||
elements, hydration cursor). The 1512-line DOM adapter holding C19 + hydration handoff
|
||
is the thinnest-tested adapter relative to size.
|
||
|
||
### C3. Dead stale-io-response guard in the epoch loop (13-char literal vs 14-char substring)
|
||
- severity: low
|
||
- confidence: CONFIRMED (reproduced live)
|
||
- where: hosts/ocaml/bin/sx_server.ml:5043-5046
|
||
- what: `String.sub line 0 14 = "(io-response "` compares a 14-byte substring to a
|
||
13-byte literal — never true, guard never fires. A stray `(io-response …)` line
|
||
falls through to dispatch and emits an extra `(error N "Unknown command …")` reply
|
||
instead of being silently discarded — an unexpected extra response for the client.
|
||
- repro: `printf '(epoch 1)\n(io-response 1 42)\n(eval "5")\n' | timeout 60 …/sx_server.exe`
|
||
→ `(error 1 "Unknown command: (io-response 1 42)")` then the eval reply.
|
||
|
||
### C4. Malformed `(epoch)` / `(epoch foo)` doesn't update the epoch — responses tagged stale
|
||
- severity: low
|
||
- confidence: CONFIRMED (reproduced live)
|
||
- where: hosts/ocaml/bin/sx_server.ml:5051-5054
|
||
- what: Only `(epoch <number>)` updates `current_epoch`; malformed epoch markers fall
|
||
through as unknown commands and the old epoch keeps tagging subsequent responses.
|
||
A client whose epoch line was mangled will discard every following response as
|
||
stale → hang.
|
||
- repro: `printf '(epoch)\n(eval "2")\n(epoch foo)\n(eval "3")\n' | …` → all replies
|
||
tagged epoch 0.
|
||
|
||
### C5. No monotonic-epoch enforcement despite protocol comment claiming it
|
||
- severity: low
|
||
- confidence: CONFIRMED (reproduced live)
|
||
- where: hosts/ocaml/bin/sx_server.ml:5051-5054, :4952-4955 (comment at :259-262)
|
||
- what: `current_epoch := n` unconditionally; decreasing/repeated epochs accepted
|
||
silently, so client bugs/reordering mis-tag responses instead of being detected.
|
||
- repro: `printf '(epoch 9)\n(epoch 3)\n(eval "42")\n' | …` → `(ok-len 3 2)`.
|
||
|
||
### C6. Two commands on one line → both dropped with a single error (client desync)
|
||
- severity: low
|
||
- confidence: CONFIRMED (reproduced live)
|
||
- where: hosts/ocaml/bin/sx_server.ml:5055-5056
|
||
- what: A line parsing to >1 expr returns one `(error … "Expected single command, got 2")`
|
||
and executes neither — a pipelining client gets a response-count desync. Related:
|
||
a command with no preceding `(epoch N)` is answered under the previous epoch tag.
|
||
|
||
### C7. vm-trace with compiler not loaded errors as "Not callable: nil"
|
||
- severity: low
|
||
- confidence: CONFIRMED (reproduced live)
|
||
- where: hosts/ocaml/bin/sx_server.ml:2193-2201 (vm-trace dispatch)
|
||
- what: If `lib/compiler.sx` isn't loaded (e.g. `load` failed because it is cwd-relative
|
||
and the server was started from hosts/ocaml), `(vm-trace "…")` fails with the opaque
|
||
`(error N "Not callable: nil")` instead of "compiler not loaded". Minor DX.
|
||
- repro: from hosts/ocaml: `printf '(epoch 1)\n(load "lib/compiler.sx")\n(epoch 2)\n(vm-trace "(+ 1 2)")\n' | timeout 60 _build/default/bin/sx_server.exe`
|
||
→ `(error 1 "File error: …")` then `(error 2 "Not callable: nil")`.
|
||
|
||
### C8. Stray triplicated source tree hosts/ocaml/hosts/ocaml/hosts/ocaml/
|
||
- severity: low
|
||
- confidence: CONFIRMED
|
||
- what: A nested duplicate of the OCaml tree (154 KB Apr-9 copy of sx_server.ml etc.)
|
||
sits inside hosts/ocaml. Not built by dune, but a grep/edit-wrong-file trap.
|
||
|
||
### C16. hosts/native is an orphaned but healthy PoC — builds green, referenced by nothing
|
||
- severity: low
|
||
- confidence: CONFIRMED
|
||
- where: hosts/native/ (own dune-workspace; lib_sx → ../../hosts/ocaml/lib symlink)
|
||
- what: SDL2/Cairo native SX renderer, single commit f0d8db9b (2026-04-09), unreferenced
|
||
by scripts/CI/docs. NOT bit-rotted: compiles against current `sx` library and its smoke
|
||
test passes (parse → 25-node render tree → layout → Cairo paint → PNG). Because it has
|
||
its own dune-workspace, the main build never compiles it, so silent breakage would go
|
||
unnoticed. Positive: it shares the evaluator by tracked symlink — no fork drift possible.
|
||
- repro: `cd hosts/native && eval $(opam env) && timeout 280 dune build ./test/test_render.exe`
|
||
→ exit 0; run → `=== All OK! ===`.
|
||
|
||
### C17. Tracked-but-dead browser files: sx-platform-2.js and 23 *.sxbc.json
|
||
- severity: low
|
||
- confidence: CONFIRMED (dead refs) / SUSPECTED (.sxbc.json fully unused)
|
||
- where: shared/static/wasm/sx-platform-2.js (Apr 16); shared/static/wasm/sx/*.sxbc.json
|
||
- what: sx-platform-2.js referenced by nothing. .sxbc.json written by mcp_tree.ml:1211 and
|
||
copied by compile-modules.js:355, but the client loads the `.sxbc` SX-text format — no
|
||
runtime reader found. All ~2.5 months stale vs their .sxbc twins.
|
||
|
||
### C18. Untracked repo-root clutter: spa-debug.js, scripts/sx-sessions-restore.sh
|
||
- severity: low
|
||
- confidence: CONFIRMED
|
||
- what: spa-debug.js is a leftover Playwright compare of direct-load vs boosted SPA nav of
|
||
relate-picker (Jun 30 debugging); sx-sessions-restore.sh is an ops script embedding
|
||
session UUIDs + `claude --dangerously-skip-permissions` invocations. Commit, relocate,
|
||
or remove.
|
||
|
||
### C9. run_tests spec suites print with empty suite label
|
||
- severity: low
|
||
- confidence: CONFIRMED (baseline log)
|
||
- what: spec test suites (e.g. test-math.sx) report as `FAIL: > string->number` with a
|
||
blank suite name in run_tests output — harness labeling defect, makes triage harder.
|
||
|
||
---
|
||
|
||
## CROSS-HOST PARITY (CONFIRMED divergences — same expr, different result per host)
|
||
|
||
Enumeration (900-name universe): spec declares 202 primitives; OCaml resolves 536, JS 668,
|
||
Python 112. Only 97 names common to all three. Spec primitives MISSING: OCaml 9
|
||
(`eq? eqv? escape format format-date parse-datetime pluralize strip-tags sx-parse`), JS 6
|
||
(`downcase eq? eqv? equal? json-encode upcase`), Python 108 (whole modules — see PY block).
|
||
Differential battery files in scratchpad: battery*.json, parity-js.js.
|
||
|
||
### P1. OCaml float serialization is lossy — SX-text wire round-trip destroys precision
|
||
- severity: high
|
||
- confidence: CONFIRMED (self-verified)
|
||
- where: hosts/ocaml/lib/sx_types.ml:415-423 (format_number uses `%g` = 6 sig digits for fractional floats)
|
||
- what: All fractional floats print with 6 sig digits; JS prints shortest-round-trip. Any value
|
||
crossing the SX text wire (aser, epoch protocol, persisted SX) from OCaml silently loses precision.
|
||
Integral floats ≥1e16 use `%.17g` (lossless) — inconsistent within the same host. This is the
|
||
PRODUCTION evaluator, so it affects live wire payloads.
|
||
- repro (self-verified): OCaml `(number->string (/ 1 3))` → `"0.333333"`; `(+ 0.1 0.2)` → `"0.3"`;
|
||
`(= (parse-number (number->string (/ 1 3))) (/ 1 3))` → `false`. JS same → true / 0.3333333333333333.
|
||
|
||
### P2. OCaml `sort` breaks on mixed int/float lists (polymorphic compare on constructor tag)
|
||
- severity: high
|
||
- confidence: CONFIRMED (self-verified)
|
||
- where: hosts/ocaml/lib/sx_primitives.ml:1290-1292 (`List.sort compare l`)
|
||
- what: OCaml polymorphic `compare` orders Integer before Number by variant tag, so numerically
|
||
mixed lists sort wrongly and silently.
|
||
- repro (self-verified): OCaml `(sort (list 1.5 10 2))` → `(2 10 1.5)`; JS → `(1.5 2 10)`.
|
||
|
||
### P3. `into` is not native on the OCaml kernel — routed over the IO helper bridge, fails standalone
|
||
- severity: medium
|
||
- confidence: CONFIRMED (self-verified)
|
||
- where: OCaml resolves `into` but execution emits `(io-request N "helper" "into" …)`
|
||
- what: A spec core.dict primitive requires a live Python-side helper on OCaml; JS/Python have it
|
||
native. Any pure-OCaml context (tests, CLI, and notably the `eval`/`load-source` command paths
|
||
that don't service helper IO) can't use `into`.
|
||
- repro (self-verified): OCaml `(into {} (list (list :a 1)))` → io-request then
|
||
`error … "IO bridge: stdin closed while waiting for io-response"`; JS → `{:a 1}`.
|
||
|
||
### P4. Exact-integer semantics diverge (OCaml int63 Integer vs JS float64)
|
||
- severity: high
|
||
- confidence: CONFIRMED (agent)
|
||
- where: hosts/ocaml/lib/sx_types.ml:47-48 (Integer distinct from Number); JS has only doubles
|
||
- what: Arithmetic differs beyond 2^53; literals above int63 silently become floats on OCaml with
|
||
different text form; exactness is host-dependent and not wire-preserved (both serialize `1.0` as
|
||
`"1"`, which OCaml re-reads as exact Integer).
|
||
- repro: `(+ 9007199254740992 1)` → OCaml 9007199254740993, JS …992; `(* 111111111 111111111)` →
|
||
OCaml 12345678987654321, JS …320; `(float? 1.0)` → OCaml true, JS false.
|
||
|
||
### P5. JS `=` is not deep on dicts (spec: alias for equal?); `equal?`/`eq?`/`eqv?` missing from JS bundle
|
||
- severity: high
|
||
- confidence: CONFIRMED (agent + JS agent)
|
||
- where: hosts/javascript/platform.py:841 (sxEq falls through to false for plain-object dicts);
|
||
no PRIMITIVES["equal?"/"eq?"/"eqv?"] in the JS bundle (run_tests.js:126 patches them only into the test env)
|
||
- what: `=` deep-compares lists but reference-compares dicts on JS; OCaml deep-compares both. And the
|
||
spec equality trio is absent from the browser bundle — harness assertions (spec/harness.sx:82,85,88
|
||
call equal?) and any component using equal? crash client-side. OCaml also lacks eq?/eqv? (has
|
||
non-spec identical? instead).
|
||
- repro: `(= {:a 1} {:a 1})` → OCaml true, JS false; `(equal? (list 1 2) (list 1 2))` → OCaml true, JS ERROR.
|
||
|
||
### P6. String length/indexing units differ across hosts (UTF-8 bytes vs UTF-16 code units)
|
||
- severity: high
|
||
- confidence: CONFIRMED (agent)
|
||
- where: OCaml String.length (bytes) vs JS .length (UTF-16, platform.py:1386)
|
||
- what: Neither counts codepoints; any substring/index arithmetic on non-ASCII text diverges
|
||
cross-host (and both differ from user-perceived length).
|
||
- repro: `(len "héllo")` → OCaml 6, JS 5; `(len "🎉")` → OCaml 4, JS 2.
|
||
|
||
### P7. JS lenient-coercion cluster vs OCaml strictness (silent wrong values on the client)
|
||
- severity: medium (aggregate; several individually medium/high)
|
||
- confidence: CONFIRMED (agent + JS agent)
|
||
- where: hosts/javascript/platform.py (str 1138, range 1380, len 1386, cons 1391, get 1385, parse-int 1500, split 1148, round 1058, slice 1167, max/min 1065)
|
||
- what: JS coerces where OCaml errors/strict → divergent results the client renders silently:
|
||
`(str (list 1 2))` JS "1,2" (OCaml "(1 2)"); `(str {:a 1})` JS "[object Object]" (OCaml "{:a 1}");
|
||
`(range 3)` JS `()` (OCaml `(0 1 2)`); `(len nil)` JS 2 (OCaml 0); `(cons 1 nil)` JS `(1 nil)`
|
||
(OCaml `(1)`); `(get {:a nil} :a 42)` JS nil (OCaml 42); `(get "abc" 1)` JS "b" (OCaml nil);
|
||
`(parse-int "abc")` JS 0 (OCaml nil); `(parse-int "42px")` JS 42 (OCaml nil); `(split "abc" "")`
|
||
JS `("abc")` (OCaml `("a" "b" "c")`); `(round -2.5)` JS -2 (OCaml -3); `(slice "hello" -3)` JS "llo"
|
||
(OCaml "hello"); `(max)` JS -Infinity (OCaml errors); `(+ 1 "2")` JS "12" (OCaml 3); `(mod 10 0)`
|
||
JS NaN (OCaml Division_by_zero).
|
||
|
||
### P8. nil/list-strictness family diverges, some arms violating the spec on OCaml's side
|
||
- severity: medium
|
||
- confidence: CONFIRMED (agent)
|
||
- where: OCaml sx_primitives.ml strict arms vs JS lenient (platform.py:1391-1392)
|
||
- what: `(merge nil {:a 1})` → OCaml ERROR (spec says skip nil → OCaml BUG) vs JS `{:a 1}`;
|
||
`(contains? {:a 1} :a)` → OCaml ERROR "contains?: 2 args" (spec: dict key check → OCaml BUG) vs JS true;
|
||
`(append nil (list 1))` → OCaml `(1)` vs JS ERROR; `(cons 1 2)` → OCaml ERROR vs JS `(1 2)`;
|
||
`(reverse "abc")` → OCaml ERROR vs JS "cba".
|
||
|
||
### P9. Dict key ordering differs per host (Hashtbl vs insertion order) — SX text wire not canonical
|
||
- severity: medium
|
||
- confidence: CONFIRMED (agent; also self-verified in the Python block C27)
|
||
- where: OCaml dicts are Hashtbl (arbitrary bucket order); JS objects preserve insertion; Python preserves insertion
|
||
- what: keys/vals iteration order and serialized dict text differ per host — anything comparing or
|
||
hashing serialized SX text cross-host diverges. CBOR/CID layer is safe (sx_cbor.ml:67 sorts keys).
|
||
- repro: `(keys {:z 1 :a 2 :m 3})` → OCaml `("m" "z" "a")`, JS `("z" "a" "m")`.
|
||
|
||
### P10. NaN/Infinity are not portable wire tokens; JS can't re-read its own output
|
||
- severity: medium
|
||
- confidence: CONFIRMED (agent)
|
||
- where: sx_types.ml:416-418 prints nan/inf/-inf (OCaml re-reads all forms) vs JS prints Infinity/NaN (resolves neither)
|
||
- repro: `(/ 1 0)` → OCaml inf, JS Infinity; eval `Infinity` → OCaml inf, JS ERROR undefined symbol.
|
||
|
||
### P11. `upper`/`lower` Unicode behavior differs; `upcase`/`downcase` aliases missing on JS; `round` half-mode differs
|
||
- severity: medium
|
||
- confidence: CONFIRMED (agent)
|
||
- where: sx_primitives.ml:890-897 (uppercase_ascii) vs platform.py:1145 (full Unicode); JS lacks upcase/downcase; round half-mode
|
||
- repro: `(upper "héllo")` → OCaml "HéLLO", JS "HÉLLO"; `(upcase "x")` → OCaml "X", JS ERROR;
|
||
`(round -2.5)` → OCaml -3, JS -2.
|
||
|
||
### P12. OCaml `zip-pairs` chunks pairs; spec + lib/stdlib.sx define a sliding window (kernel diverges from its own spec)
|
||
- severity: medium
|
||
- confidence: CONFIRMED (JS agent handoff)
|
||
- where: OCaml native zip-pairs vs spec/primitives.sx:663 + lib/stdlib.sx:207 (JS matches spec)
|
||
- what: OCaml native `zip-pairs` returns `((1 2)(3 4))` but the spec + stdlib define sliding window
|
||
`((1 2)(2 3)(3 4))`. Handoff to core, but the OCaml *host* is the one diverging from the spec.
|
||
|
||
### PY (Python host). Standalone hosts/python/ cannot evaluate the current spec at all
|
||
- severity: critical (as a supported target) — see C30 for the standalone-host summary
|
||
- confidence: CONFIRMED (agent)
|
||
- where: hosts/python/bootstrap.py:119 (_mangle), generated lines 1564/3482/3498; spec `match` form
|
||
- what: Four independent breaks in the Python bootstrapper: (1) _mangle RENAMES lacks `*winders*`
|
||
→ SyntaxError; (2) no rule for `->` in names → `def string_>symbol` invalid; (3) the `match`
|
||
special form is emitted as an eager function call → undefined `match`, and even shimmed it
|
||
evaluates all branches eagerly (core dispatch unrunnable); (4) references missing natives
|
||
make-char/aser-call/aser-fragment. Python resolves only 112 of the 900-name universe and lacks
|
||
8 primitive modules (math/bitwise/bytevectors/vectors/regexp/sets/rational/hash-table = 108 spec
|
||
primitives). This overlaps/extends C30. NOTE: shared/sx/ Python (parser+bridge) IS load-bearing
|
||
in production; the standalone evaluator host is what's dead.
|
||
- repro: `python3 hosts/python/bootstrap.py > /tmp/x.py && python3 -c "import x"` → SyntaxError line 1564;
|
||
after mangle-patch, `(+ 1 2)` → `name 'match' is not defined`.
|
||
|
||
---
|
||
|
||
### (informational) ok-len framing is byte-accurate and desync-resistant
|
||
- confidence: CONFIRMED — length prefixes are byte counts; newlines/framing patterns in
|
||
payloads are escaped; multibyte UTF-8 counted correctly. (Positive result.)
|
||
|
||
### (informational) WASM browser kernel boots green; kernel is single-source with server
|
||
- confidence: CONFIRMED — test_boot.sh → `WASM boot: OK`; test_wasm.sh → 29/29;
|
||
test_kernel.js → 24/24; deployed kernel artifacts byte-identical to _build outputs.
|
||
Browser links the identical `sx` OCaml library as sx_server (no evaluator fork);
|
||
only sx_browser.ml FFI glue is browser-specific. (Positive result.)
|
||
|
||
---
|
||
|
||
## SUSPECTED (reasoning only) — NOTE: S1/S4/S5 UPGRADED to CONFIRMED via live HTTP repro
|
||
|
||
> Follow-up live testing (bounded `sx_server --http` on free ports, 3 runs) upgraded three of the
|
||
> HTTP-race items and clarified two more:
|
||
> - **S1 CONFIRMED** — observed a live intermittent crash (empty responses, no exception, OOM ruled
|
||
> out at ~292 MB peak) under concurrent multi-Domain load.
|
||
> - **S4 CONFIRMED** — routing-failure page served as HTTP 200 and cached (cold 2.02s → warm 0.0005s).
|
||
> - **S5 CONFIRMED** — cache key ignores cookies (3 different `session=` → identical cached body) and query.
|
||
> - **S2/S3 remain SUSPECTED** — mechanism verified live in the code path, but no visible symptom is
|
||
> triggerable in the stock static-docs app (no full-page route renders request args / no
|
||
> expansion-sensitive component to observe). They become observable on request-varying apps.
|
||
> Their entries below carry updated per-finding confidence lines.
|
||
|
||
### S1. HTTP mode: shared mutable state raced across parallel render-worker Domains — LIVE CRASH OBSERVED
|
||
- severity: high
|
||
- confidence: **CONFIRMED (live crash reproduced once; race is real by construction)** — was SUSPECTED
|
||
- where: hosts/ocaml/bin/sx_server.ml:4717 (`Domain.spawn` × `max 4 (recommended_domain_count)`),
|
||
:4312/:4677 (`Hashtbl.replace response_cache` from workers + main), :4654 (main reads it),
|
||
:2666-2670 (`env.bindings` bind+remove), :345 (`_stream_mutex` created but NEVER locked)
|
||
- what: `http_mode` spawns 4 real `Domain.spawn` workers rendering full-page requests on the
|
||
single shared env, concurrently with the accept-domain doing AJAX renders. There is no lock
|
||
around rendering. Workers + main mutate process-global Stdlib.Hashtbls (`response_cache`,
|
||
`env.bindings`) concurrently; OCaml 5 Stdlib.Hashtbl is NOT domain-safe (memory corruption on
|
||
rehash → segfault, no catchable exception). The comment at :4204 claiming cache writes happen
|
||
"only during single-threaded startup" is false (workers write at runtime); the :343-345 comment
|
||
admits "concurrent CEK evaluations corrupt shared state".
|
||
- **LIVE REPRO (self-verified).** Booted `sx_server --http <port>` on a free port (3 runs). Under a
|
||
burst of ~300 concurrent AJAX + ~150 concurrent full-page requests to distinct uncached paths
|
||
(all 4 worker Domains + main thread writing `response_cache`/`env.bindings` simultaneously),
|
||
**run 2 CRASHED**: the process vanished mid-render — no OCaml exception logged (the try/with at
|
||
:4315 would have printed `[render] Error`; none present), no "Binary changed" restart, and a
|
||
concurrent poll of a known-good cached page began returning EMPTY bodies (md5 of "") for 59/60
|
||
reads as the server died. **OOM ruled out**: run 3 under an even heavier burst survived with peak
|
||
RSS ~292 MB (baseline ~200 MB) — memory never approached exhaustion. The crash is INTERMITTENT
|
||
(run 1 clean, run 2 crashed, run 3 survived) — the classic signature of a data race, not a
|
||
deterministic fault. Net: the production HTTP server can crash under concurrent load; I couldn't
|
||
capture a core dump (apport core_pattern, restricted) to pin the exact fault address, so the
|
||
"Hashtbl-rehash-segfault" mechanism is inferred from the crash profile + the unsynchronized-access
|
||
code, not a stack trace. Logs: /tmp/sx-review/http-server{,2,3}.log.
|
||
|
||
### S2. HTTP mode: per-request state is process-global → cross-request contamination
|
||
- severity: high
|
||
- confidence: SUSPECTED (mechanism confirmed live; visible symptom not triggerable in the stock docs app)
|
||
- where: sx_server.ml:4335-4354 (main domain sets `_req_method/_req_query/_req_headers/_req_body`
|
||
+ `Hashtbl.clear/replace _request_cookies`), :4703 (full-page render QUEUED to a worker with only
|
||
`(fd, path, headers)` — NOT the request context), consumed by request-* primitives at :3772-3829
|
||
- what: I confirmed live that full-page misses are **queued** to worker Domains carrying only the
|
||
path, while the worker's render reads the process-global `_req_query`/`_req_body`/`_request_cookies`
|
||
that the main loop overwrites on the very next request. So a queued render can read a *subsequent*
|
||
request's query/body/cookies (plus a plain main-writes-while-worker-reads data race). I could NOT
|
||
produce a visible crossed-value symptom because the sx-docs app has no full-page route that renders
|
||
`(request-arg …)`/`get-cookie`/body into its output — the docs pages are static. The contamination
|
||
is therefore structurally present and live-reachable but only becomes observable for an app whose
|
||
full-page renders vary by request args (e.g. any of the real Quart-replacement domains). Kept
|
||
SUSPECTED honestly: mechanism verified, symptom not exhibited on the stock app.
|
||
|
||
### S3. `expand-components?` global binding removed mid-flight by AJAX render
|
||
- severity: medium
|
||
- confidence: SUSPECTED (bind/remove verified live in code path; visible symptom not isolated)
|
||
- where: sx_server.ml:4157 (bound globally = true for ALL renders), :2666/:2670 (AJAX branch binds
|
||
then `Hashtbl.remove env.bindings "expand-components?"`)
|
||
- what: http_mode binds `expand-components?` once globally so full-page worker renders expand
|
||
components. The AJAX branch of http_render_page binds and then REMOVES it on the SAME shared env —
|
||
so after any AJAX request the global binding is gone, and a concurrent (or subsequent) full-page
|
||
aser render can observe it vanished (components silently not expanded), plus the remove races
|
||
worker lookups. Confirmed the bind/remove-on-shared-env code path executes during live AJAX
|
||
requests (the log shows "[sx-http] … (SX) aser=" for every AJAX hit); did not isolate a
|
||
visibly-unexpanded component in output because the docs pages' aser output is SSR-expanded through
|
||
a different path. Mechanism confirmed; symptom not pinned.
|
||
|
||
### S4. Response cache stores soft error pages (routing failures cached as HTTP 200) — LIVE CONFIRMED
|
||
- severity: medium
|
||
- confidence: **CONFIRMED (self-verified live)** — was SUSPECTED
|
||
- where: sx_server.ml:2635-2658 (`error_page_ast` returned as `Some body`), cached at :4312/:4677
|
||
- what: A routing failure renders `error_page_ast` returned as a normal `Some body` and served with
|
||
HTTP **200**; the worker/ajax paths cache any `Some body`. So the not-found/error page is cached
|
||
and served from cache to every subsequent visitor until restart. (Hard 500s from raised exceptions
|
||
at :4315 are NOT cached; soft error pages ARE.) The "transient cross-service failure" sub-case
|
||
(a fetch error rendered into the page, then cached) is reasoned from the same code path but needs
|
||
the Python bridge to trigger, so it stays inferred.
|
||
- repro (self-verified): `GET /sx/<random-nonexistent-path>` → HTTP 200 containing the "404"/not-found
|
||
page; a 2nd GET of the same path returned byte-identical bytes served from cache — cold render 2.02s
|
||
vs warm 0.0005s (~4000×), and the server log shows only ONE render line for that path.
|
||
|
||
### S5. Response cache keying ignores cookies/query/auth (except one hardcoded cookie) — LIVE CONFIRMED
|
||
- severity: medium
|
||
- confidence: **CONFIRMED (self-verified live)** — was SUSPECTED
|
||
- where: sx_server.ml:4296/:4651-4654 (`cache_key` = path, +ajax:/htmx: prefix), :4649 (only
|
||
`sx-home-stepper` bypasses the cache), :4331-4332 (query stripped before keying)
|
||
- what: The cache key is the path only; cookies (except the hardcoded `sx-home-stepper`) and the
|
||
query string are absent from it. Any page whose output varied by cookie/session/auth or query would
|
||
be cached under a shared key and served across users. Benign for the current public docs (pages
|
||
don't vary by cookie), but a real footgun for the cookie/auth-dependent Quart-replacement domains.
|
||
- repro (self-verified): `GET /sx/(geography.(hypermedia))` with three different `Cookie:` headers
|
||
(different `session=` values) → three byte-identical responses from one cache entry; same path with
|
||
`?u=1` vs `?u=2` → byte-identical (query stripped from key).
|
||
|
||
### S8. Browser env missing spec platform constructors + a small primitive tail — latent break for shared modules
|
||
- severity: medium
|
||
- where: sx_browser.ml:626+ (91 server-only binds vs 22 browser-only; lists in scratchpad
|
||
server-binds.txt / browser-binds.txt)
|
||
- what: `make-lambda/make-component/make-macro/make-thunk`, `now`, `json-encode`,
|
||
`parse-safe`, `pretty-print`, `into` are bound only in sx_server's env, absent
|
||
client-side. Currently harmless (spec/evaluator.sx isn't shipped to the browser; the
|
||
only consumer of the latter group is web/io.sx, not bundled) — but any shared .sx
|
||
module that starts using them silently breaks in the browser only.
|
||
|
||
### S9. Aser/SPA boosted-nav component expansion still fragile — hinges on boot-manifest eager-load fed by stale artifacts
|
||
- severity: medium
|
||
- where: web/adapter-sx.sx; sx-platform.js:649-660 (page-manifest boot array);
|
||
lib/host/sx/relate-picker.sx; bundle.sh:74-77
|
||
- what: The April blocker was fixed (ac65666f) and Jun-29 commits added server-embedded
|
||
`data-sx-manifest` eager-loading, but the untracked Jun-30 spa-debug.js shows boosted-nav
|
||
expansion of the picker was still being debugged the next day — and both the stale served
|
||
compiler (C10) and stale module-manifest.sx (C11) sit directly on this code path.
|
||
|
||
### S10. VM inline-resolve of IO inside HO-primitive callbacks cannot suspend/resume (async-IO correctness cliff)
|
||
- severity: high
|
||
- confidence: SUSPECTED (documented + code-confirmed; not reproducible through the harness — see C21)
|
||
- where: hosts/ocaml/lib/sx_vm.ml:call_closure_reuse (~349-375); `_cek_io_resolver` installed at sx_server.ml:4166
|
||
- what: When `perform` fires inside a JIT/VM HO-primitive callback (map/filter/reduce/for-each),
|
||
the native OCaml loop sits between perform and resume, so it can't unwind-and-resume; it
|
||
resolves IO inline via a local `settle` loop. The in-code comment states the alternative
|
||
"would drop the remaining elements — corrupting the stack so the next CALL_PRIM sees wrong
|
||
args." This works only because durable-KV reads are synchronous; a genuinely async IO op
|
||
inside an HO callback on the serving path is not correctly supported. This is the same
|
||
hazard class as the memory note about serving JIT dropping all-but-first in map/for-each.
|
||
|
||
### S11. Server page routing evaluates the URL as SX — any env-bound function is URL-invokable
|
||
- severity: medium
|
||
- confidence: SUSPECTED
|
||
- where: web/request-handler.sx (sx-url-to-expr, sx-auto-quote, sx-eval-page/call-page);
|
||
driven by sx_server.ml:http_render_page:2620 (sx-handle-request)
|
||
- what: sx-url-to-expr strips `/sx/`, splits on `.`, joins with spaces; call-page parses and
|
||
invokes it. sx-auto-quote only quotes *unbound* symbols, so a bound symbol as list head is
|
||
called with attacker-supplied (recursively evaluated) args. A path with parens (a legal
|
||
path char) can reach `(env-get env "some-fn")` + apply for any render-env binding. cek-try
|
||
swallows errors (→ nil/error page) limiting impact, but this is an SSR eval surface worth
|
||
whitelisting to `page:`-prefixed bindings. Couldn't confirm whether the OCaml front door
|
||
pre-sanitizes the path.
|
||
|
||
### S12. Island hydration replaceChildren-then-render → hydration cursor reuses no SSR DOM
|
||
- severity: low
|
||
- confidence: SUSPECTED
|
||
- where: web/boot.sx:hydrate-island (~333-350)
|
||
- what: hydrate-island calls `(host-call el "replaceChildren")` (empties the element) then
|
||
renders the body in `sx-hydrating` mode. But the hydration matcher reads childNodes.item(idx)
|
||
on the just-emptied element, so it reuses nothing — effectively clear-and-fresh-render. If
|
||
intended, the sx-hydrating scope push is dead work; if not, element-rooted islands never
|
||
reuse SSR DOM. Not runnable through the DOM path to confirm.
|
||
|
||
### S13. SSR/client parity relies on island body being pure over serialized kwargs (no dev-mode check)
|
||
- severity: low
|
||
- confidence: SUSPECTED
|
||
- where: web/adapter-html.sx:render-html-island + serialize-island-state (608) vs web/boot.sx:hydrate-island
|
||
- what: Only kwargs are serialized into data-sx-state; a body reading request-scoped/
|
||
non-deterministic IO (now, request-arg, random) or scope/context values not in kwargs
|
||
produces SSR HTML differing from the client render → hydration mismatch. Inherent to the
|
||
design, but there's no guard or dev-mode mismatch check and no test for a non-pure body.
|
||
|
||
### S14. Deep (≥2-level) nested-list children flatten differently between HTML and aser
|
||
- severity: low
|
||
- confidence: SUSPECTED
|
||
- where: spec/render.sx render-list path (recursive) vs web/adapter-sx.sx:aser-call/aser-fragment (one-level flatten)
|
||
- what: HTML recursively flattens arbitrarily nested child lists to text; aser flattens one
|
||
level and serializes deeper lists structurally. Single-level map output agrees (tested),
|
||
but 2+-level raw nesting could serialize differently between modes. No test covers depth ≥ 2.
|
||
|
||
### S-bridge. Python↔OCaml bridge desync on coroutine cancellation is unrecoverable (dead _restart, no timeouts)
|
||
- severity: high
|
||
- confidence: SUSPECTED (bridge-side reasoning; kernel-side stale-message mechanism self-verified)
|
||
- where: shared/sx/ocaml_bridge.py:99 (_restart, 0 callers), :613 (_read_until_ok), :139 (async with self._lock)
|
||
- what: render/eval/aser hold self._lock while blocked in _read_until_ok awaiting a kernel io-response.
|
||
If the awaiting coroutine is cancelled (client disconnect / timeout), the `async with` releases the
|
||
lock while the kernel is still blocked in read_io_response for the OLD epoch. The next coroutine
|
||
acquires the lock and sends `(epoch N+1)\n(command)`; the kernel — still inside read_io_response —
|
||
consumes both lines as "stale messages" and keeps blocking, so the new command never runs and the
|
||
new coroutine hangs on _readline forever. No asyncio.wait_for wraps bridge calls, and _restart()
|
||
pipe-recovery is never invoked, so the bridge can't self-heal — it stays wedged until the process
|
||
is killed. Under a shared multi-Domain HTTP server (S1/S2) this is a real liveness hazard.
|
||
- repro (self-verified, kernel-side mechanism): `printf '(epoch 1)\n(eval "(helper \"w\" 1)")\n(epoch 2)\n(eval "(+ 1 2)")\n' | timeout 20 hosts/ocaml/_build/default/bin/sx_server.exe`
|
||
→ `(io-request 1 "helper" "w" nil)`, then `[io] discarding stale message (…)` ×2 eating the epoch-2
|
||
command, then the epoch-2 eval never runs.
|
||
|
||
### S-bridge2. `_parse_response` treats a numeric result value as an epoch (ambiguity)
|
||
- severity: low
|
||
- confidence: CONFIRMED (agent)
|
||
- where: shared/sx/ocaml_bridge.py:915, shared/sx/ocaml_sync.py:115
|
||
- what: `(ok EPOCH VALUE)` parsing strips a leading number as the epoch, but the guard
|
||
`line[4:-1].isdigit()` classifies `(ok 3)` as epoch-only-no-value and returns `('ok', None)` instead
|
||
of `('ok','3')`. Latent (kernel always emits `(ok EPOCH VALUE)`), but the digit-sniffing epoch strip
|
||
is fragile for any value that begins with a digit. test_ocaml_bridge.py has 5 live failures incl this.
|
||
|
||
### S6. io_request-based primitives under `eval` can consume subsequent command lines
|
||
- severity: low
|
||
- where: sx_server.ml:617-669 (`query`/`action`/`helper` → blocking `read_io_response`),
|
||
`eval` dispatch at :1816
|
||
- what: `query`/`action`/`helper` block reading the next stdin line as an IO response;
|
||
under a client that doesn't speak the IO sub-protocol, the next queued command is
|
||
consumed → desync/hang.
|
||
|
||
### S7. Multiple eval entry points with divergent IO semantics for the same source
|
||
- severity: low
|
||
- where: `eval` (:1816-1849, import+persist inline), `load-source` (:1772-1783, no
|
||
IO handling), `load` (:1755, full `eval_expr_io`), http render (:2575, import→nil)
|
||
- what: The same source behaves differently depending on the wrapping command —
|
||
the known "unify eval/JIT paths" TODO surfacing as behavioral drift.
|
||
|
||
---
|
||
|
||
## Notes / baseline caveats
|
||
|
||
- 272/274 baseline run_tests failures are the in-progress hyperscript (`hs-*`) suites —
|
||
guest-language project, not host defects. Undefined symbols: `hs-is-set?` (16),
|
||
`eval-hs-error` (18), `hs-ref-eq` (4), `host-hs-normalize-exc` (2), plus DOM-mock
|
||
assertion failures.
|
||
- run_tests JIT is opt-in (`--jit`, run_tests.ml:4035). Baseline ran interpreter-only
|
||
(`[jit] calls=3045835 hit=0 miss=0 skip=3045835`). A `--jit` differential run is in
|
||
progress to diff pass/fail sets — results pending.
|
||
- `sx_scope.ml` does not exist on this branch (only in sibling worktrees); memory/docs
|
||
referencing it are stale for `architecture`. Scope prims live in sx_primitives.ml
|
||
as process-globals (see S1/handoffs).
|
||
|
||
## Handoffs to other lanes
|
||
|
||
- core: served browser JIT compiler missing 921db09f's HO-loop desugar (see C10) — the
|
||
browser-side `--jit == CEK` parity fix needs a re-sync + recompile pipeline fix.
|
||
- conformance: module-manifest format split (C11) — regenerate `.sx` or point
|
||
sx-platform.js at `.json`; until then hs-worker/hs-prolog never lazy-load client-side.
|
||
- conformance: hosts/native has a working test nothing runs; test_platform.js needs
|
||
`web/signals.sx` → `web/web-signals.sx` to restore the browser platform test tier.
|
||
- hygiene: unify dist/ vs shared/static/wasm/ behind one sync script; delete the ignored
|
||
hosts/ocaml/hosts/ nest, tracked stale hosts/ocaml/shared/static/wasm blobs,
|
||
sx-platform-2.js, and (if reader-less) *.sxbc.json.
|
||
- core (spec/render.sx parity): render-attrs renders non-BOOLEAN_ATTRS boolean-valued attrs
|
||
as `attr="true"`/`attr="false"` while adapter-dom emits `attr=""`/omits — pick one contract
|
||
and align all four adapters; add conformance cases (see C19).
|
||
- conformance (spec/harness.sx): (1) log the IO call before invoking the mock so throwing
|
||
mocks are recorded (C22); (2) add a harness mode that routes IO through real
|
||
perform/cek-resume so the HO+perform element-drop cliff (S10) becomes testable (C21).
|
||
- host (sx_vm.ml): add a targeted conformance test performing IO inside a JIT-compiled HO
|
||
callback over a function-produced list — the inline-settle path is a correctness cliff
|
||
for any future genuinely-async seam IO op (S10).
|
||
- core: `_scope_stacks` / `_request_cookies` / `_req_*` being process-global means
|
||
scope/provide/emit! and request primitives are not isolable per concurrent flow;
|
||
spec scope semantics assume single-flow evaluation. Needs a per-flow story if the
|
||
OCaml HTTP server stays multi-Domain.
|
||
- core: eval vs load-source vs load vs http-render resolve `import`/durable IO
|
||
differently — converge on one IO-resolution policy.
|
||
- core/eval: `scope`/`emit!` appears to LEAK across invocations — `(scope (emit! :k 1)
|
||
(emit! :k 2) (len (emitted :k)))` returns 2, 4, 6… cumulatively across epoch-server calls,
|
||
with JIT disabled too (so not a VM bug — looks like a scope-stack state leak in scope-in-frames
|
||
or the epoch env's scope stack). Flagged by the VM/JIT agent.
|
||
- core/docs: `let` is SEQUENTIAL (let*) in BOTH CEK and VM (`(let ((x 2) (y x)) y)` → 2 with
|
||
outer x=1) — contradicts the documented "let is parallel" authoring rule. Engines agree; docs don't.
|
||
- core/spec: nested quasiquote evaluates inner unquotes at depth 2 on both OCaml and JS
|
||
(```` `(a `(b ,(c))) ```` errors) — nonstandard but consistent; needs a spec ruling.
|
||
- core/spec: `=` deep-on-dicts, `eq?`/`eqv?`/`equal?`, `upcase`/`downcase`, `json-encode`,
|
||
1-arg `range`, empty-sep `split`, `parse-int` failure, `get` stored-nil-vs-default, `(max)`,
|
||
`round` half-negative, string-length units, exactness-across-wire — all need normative rulings;
|
||
then each host aligned. OCaml wire float printing (`%g`) should be shortest-round-trip; OCaml
|
||
`sort` numeric-compare Integer/Number; OCaml `into`/`contains?`-on-dict/`merge`-skip-nil made
|
||
native+conformant; OCaml `zip-pairs` reconciled with its own spec.
|
||
- conformance: NO differential CEK-vs-JIT parity suite exists — all four silent-wrong-value JIT
|
||
bugs (J1 `->`, J3 macro-arg, J4 component-kwargs, J5 stale opcodes) pass the existing tests.
|
||
A harness that runs each conformance expr both interpreted and through a forced-JIT wrapper
|
||
would catch them. Also add a cross-host differential battery (scratchpad battery*.json) — the
|
||
spec corpus currently runs green only against OCaml; the JS bundle is 2490 failing and the
|
||
Python standalone host can't load.
|
||
- conformance: add a protocol-fuzz suite for the epoch loop (malformed line must not
|
||
crash; `(epoch)`/`(epoch foo)`; stray `(io-response …)`; two-exprs-per-line).
|
||
- conformance: no suite catches the r7rs `string->number` radix shadow — the spec
|
||
test fails only in the OCaml runner env; JS/Python runner r7rs load status differs.
|
||
|
||
## Recommended triage / next steps
|
||
|
||
This is a reviewer's suggested reading order, NOT a verdict — severity/confidence tags on each
|
||
finding are the ranking signal; a maintainer decides what blocks a release. No new claims here; every
|
||
item points back to the evidence above.
|
||
|
||
**Confirmed-severe clusters, in suggested blocker order:**
|
||
|
||
1. **Production serving-JIT silently miscompiles (J1–J8).** Highest concern because it produces
|
||
*wrong values with no error* on the live sx.rose-ash.com HTTP path (JIT hook is unconditional at
|
||
sx_server.ml:4163 — see the reconciliation note; the "opt-in/default-off" belief does not apply to
|
||
http_mode). J1 (`->` in arg position) and J3 (macro args) return wrong results; J2 double-applies
|
||
side effects on fallback (a data-integrity risk for persist/counter writes). Blast radius depends
|
||
on whether rendered lambdas hit those patterns — a fleet audit of served pages, or disabling the
|
||
http_mode JIT hook until fixed, is the pragmatic call. Fix lives in lib/compiler.sx (compile-thread-step
|
||
at 934-951 for J1) + macro handling in the compiler/VM.
|
||
|
||
2. **Process-killing parse crash (C1 / C1b).** One malformed or non-ASCII line on the command channel
|
||
kills the whole sx_server process — it's the shared channel for the Python bridge and conformance
|
||
runners. Small, localized fix (wrap the command-channel parse at sx_server.ml:5048/:4950 in a
|
||
handler that catches Parse_error). Cheap to fix, high blast radius; good early win.
|
||
|
||
3. **HTTP serving path breaks under concurrency (S1 / S4 / S5).** S1 crashed the live server
|
||
intermittently (data race on unsynchronized cross-Domain Hashtbls, OOM ruled out) — needs either a
|
||
lock around cache/env mutation or per-Domain state. S4 (error pages cached as 200) and S5 (cache
|
||
key ignores cookies/query) are cheap keying fixes but S5 becomes a cross-user data-leak on any
|
||
cookie/auth-varying domain, so fix it before the Quart-replacement domains go live on this server.
|
||
|
||
4. **Hollow JS bundle + red JS CI (C0a / C0b).** Breaks conformance/CI, not visitors (the live site
|
||
serves the wasm kernel). Fix is a define-library-aware `extract_defines` (hosts/javascript/platform.py:15)
|
||
plus loading the newer lib deps + make-rtd/dom prims — or, if the JS host is being retired, stop CI
|
||
gating on it. A product/ownership decision as much as a code fix.
|
||
|
||
**Not-yet-symptomatic, needs a real app to exercise:**
|
||
- S2 (queued workers read process-global request context) and S3 (`expand-components?` removed on the
|
||
shared env) are mechanism-confirmed but only exhibit a visible symptom on a full-page route that
|
||
varies by request args/cookies. Re-run the S1-style concurrent-load repro against one of the real
|
||
Quart-replacement domains (not the static docs site) to turn these CONFIRMED or clear them.
|
||
|
||
**Cross-cutting, lower urgency:**
|
||
- Cross-host parity (P1–P12) and the Python boundary/bridge issues (C24–C31, S-bridge) matter most if
|
||
the JS/Python hosts are still targets; if OCaml is the sole production evaluator, prioritize the
|
||
OCaml-side ones that affect the wire (P1 lossy float, P9 dict order, P3 `into`).
|
||
- Hygiene items (C8, C14–C18) are safe cleanups, no behavior change.
|
||
|
||
**Handoffs above** route the core/conformance-owned items (scope leak, let-sequential docs, the missing
|
||
CEK-vs-JIT differential suite) to the sibling review lanes — not this lane's to fix.
|