Delete sx_ref.py — OCaml is the sole SX evaluator
Removes the 5993-line bootstrapped Python evaluator (sx_ref.py) and all
code that depended on it exclusively. Both bootstrappers (JS + OCaml)
now use a new synchronous OCaml bridge (ocaml_sync.py) to run the
transpiler. JS build produces identical output; OCaml bootstrap produces
byte-identical sx_ref.ml.
Key changes:
- New shared/sx/ocaml_sync.py: sync subprocess bridge to sx_server.exe
- hosts/javascript/bootstrap.py: serialize defines → temp file → OCaml eval
- hosts/ocaml/bootstrap.py: same pattern for OCaml transpiler
- shared/sx/{html,async_eval,resolver,jinja_bridge,handlers,pages,deps,helpers}:
stub or remove sx_ref imports; runtime uses OCaml bridge (SX_USE_OCAML=1)
- sx/sxc/pages: parse defpage/defhandler from AST instead of Python eval
- hosts/ocaml/lib/sx_primitives.ml: append handles non-list 2nd arg per spec
- Deleted: sx_ref.py, async_eval_ref.py, 6 Python test runners, misc ref/ files
Test results: JS 1078/1078, OCaml 1114/1114.
sx_docs SSR has pre-existing rendering issues to investigate separately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
147
shared/sx/ocaml_sync.py
Normal file
147
shared/sx/ocaml_sync.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
Synchronous OCaml bridge — persistent subprocess for build-time evaluation.
|
||||
|
||||
Used by bootstrappers (JS cli.py, OCaml bootstrap.py) that need a sync
|
||||
evaluator to run transpiler.sx. For async runtime use, see ocaml_bridge.py.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
_DEFAULT_BIN = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"../../hosts/ocaml/_build/default/bin/sx_server.exe",
|
||||
)
|
||||
|
||||
|
||||
class OcamlSyncError(Exception):
|
||||
"""Error from the OCaml SX kernel."""
|
||||
|
||||
|
||||
def _sx_unescape(s: str) -> str:
|
||||
"""Unescape an SX string literal (left-to-right, one pass)."""
|
||||
out = []
|
||||
i = 0
|
||||
while i < len(s):
|
||||
if s[i] == '\\' and i + 1 < len(s):
|
||||
c = s[i + 1]
|
||||
if c == 'n':
|
||||
out.append('\n')
|
||||
elif c == 'r':
|
||||
out.append('\r')
|
||||
elif c == 't':
|
||||
out.append('\t')
|
||||
elif c == '"':
|
||||
out.append('"')
|
||||
elif c == '\\':
|
||||
out.append('\\')
|
||||
else:
|
||||
out.append(c)
|
||||
i += 2
|
||||
else:
|
||||
out.append(s[i])
|
||||
i += 1
|
||||
return ''.join(out)
|
||||
|
||||
|
||||
class OcamlSync:
|
||||
"""Synchronous bridge to the OCaml sx_server subprocess."""
|
||||
|
||||
def __init__(self, binary: str | None = None):
|
||||
self._binary = binary or os.environ.get("SX_OCAML_BIN") or _DEFAULT_BIN
|
||||
self._proc: subprocess.Popen | None = None
|
||||
|
||||
def _ensure(self):
|
||||
if self._proc is not None and self._proc.poll() is None:
|
||||
return
|
||||
self._proc = subprocess.Popen(
|
||||
[self._binary],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
# Wait for (ready)
|
||||
line = self._readline()
|
||||
if line != "(ready)":
|
||||
raise OcamlSyncError(f"Expected (ready), got: {line}")
|
||||
|
||||
def _send(self, command: str):
|
||||
assert self._proc and self._proc.stdin
|
||||
self._proc.stdin.write((command + "\n").encode())
|
||||
self._proc.stdin.flush()
|
||||
|
||||
def _readline(self) -> str:
|
||||
assert self._proc and self._proc.stdout
|
||||
data = self._proc.stdout.readline()
|
||||
if not data:
|
||||
raise OcamlSyncError("OCaml subprocess died unexpectedly")
|
||||
return data.decode().rstrip("\n")
|
||||
|
||||
def _read_response(self) -> str:
|
||||
"""Read a single response. Returns the value string or raises on error."""
|
||||
line = self._readline()
|
||||
# Length-prefixed blob: (ok-len N)
|
||||
if line.startswith("(ok-len "):
|
||||
n = int(line[8:-1])
|
||||
assert self._proc and self._proc.stdout
|
||||
data = self._proc.stdout.read(n)
|
||||
self._proc.stdout.readline() # trailing newline
|
||||
value = data.decode()
|
||||
# Blob is SX-serialized — strip string quotes and unescape
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
value = _sx_unescape(value[1:-1])
|
||||
return value
|
||||
if line == "(ok)":
|
||||
return ""
|
||||
if line.startswith("(ok-raw "):
|
||||
return line[8:-1]
|
||||
if line.startswith("(ok "):
|
||||
value = line[4:-1]
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
value = _sx_unescape(value[1:-1])
|
||||
return value
|
||||
if line.startswith("(error "):
|
||||
msg = line[7:-1]
|
||||
if msg.startswith('"') and msg.endswith('"'):
|
||||
msg = _sx_unescape(msg[1:-1])
|
||||
raise OcamlSyncError(msg)
|
||||
raise OcamlSyncError(f"Unexpected response: {line}")
|
||||
|
||||
def eval(self, source: str) -> str:
|
||||
"""Evaluate SX source, return result as string."""
|
||||
self._ensure()
|
||||
escaped = source.replace("\\", "\\\\").replace('"', '\\"')
|
||||
self._send(f'(eval "{escaped}")')
|
||||
return self._read_response()
|
||||
|
||||
def load(self, path: str) -> str:
|
||||
"""Load an .sx file into the kernel."""
|
||||
self._ensure()
|
||||
self._send(f'(load "{path}")')
|
||||
return self._read_response()
|
||||
|
||||
def load_source(self, source: str) -> str:
|
||||
"""Load SX source directly into the kernel."""
|
||||
self._ensure()
|
||||
escaped = source.replace("\\", "\\\\").replace('"', '\\"')
|
||||
self._send(f'(load-source "{escaped}")')
|
||||
return self._read_response()
|
||||
|
||||
def stop(self):
|
||||
if self._proc and self._proc.poll() is None:
|
||||
self._proc.terminate()
|
||||
self._proc.wait(timeout=5)
|
||||
self._proc = None
|
||||
|
||||
|
||||
# Singleton
|
||||
_global: OcamlSync | None = None
|
||||
|
||||
|
||||
def get_sync_bridge() -> OcamlSync:
|
||||
global _global
|
||||
if _global is None:
|
||||
_global = OcamlSync()
|
||||
return _global
|
||||
Reference in New Issue
Block a user