Complete Python eval removal: epoch protocol, scope consolidation, JIT fixes
Route all rendering through OCaml bridge — render_to_html no longer uses Python async_eval. Fix register_components to parse &key params and &rest children from defcomp forms. Remove all dead sx_ref.py imports. Epoch protocol (prevents pipe desync): - Every command prefixed with (epoch N), all responses tagged with epoch - Both sides discard stale-epoch messages — desync structurally impossible - OCaml main loop discards stale io-responses between commands Consolidate scope primitives into sx_scope.ml: - Single source of truth for scope-push!/pop!/peek, collect!/collected, emit!/emitted, context, and 12 other scope operations - Removes duplicate registrations from sx_server.ml (including bugs where scope-emit! and clear-collected! were registered twice with different impls) - Bind scope prims into env so JIT VM finds them via OP_GLOBAL_GET JIT VM fixes: - Trampoline thunks before passing args to CALL_PRIM - as_list resolves thunks via _sx_trampoline_fn - len handles all value types (Bool, Number, RawHTML, SxExpr, Spread, etc.) Other fixes: - ~cssx/tw signature: (tokens) → (&key tokens) to match callers - Minimal Python evaluator in html.py for sync sx() Jinja function - Python scope primitive stubs (thread-local) for non-OCaml paths - Reader macro resolution via OcamlSync instead of sx_ref.py Tests: 1114 OCaml, 1078 JS, 35 Python regression, 6/6 Playwright SSR Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,7 @@ class OcamlSync:
|
||||
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
|
||||
self._epoch: int = 0
|
||||
|
||||
def _ensure(self):
|
||||
if self._proc is not None and self._proc.poll() is None:
|
||||
@@ -62,13 +63,17 @@ class OcamlSync:
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
self._epoch = 0
|
||||
# Wait for (ready)
|
||||
line = self._readline()
|
||||
if line != "(ready)":
|
||||
raise OcamlSyncError(f"Expected (ready), got: {line}")
|
||||
|
||||
def _send(self, command: str):
|
||||
"""Send a command with epoch prefix."""
|
||||
assert self._proc and self._proc.stdin
|
||||
self._epoch += 1
|
||||
self._proc.stdin.write(f"(epoch {self._epoch})\n".encode())
|
||||
self._proc.stdin.write((command + "\n").encode())
|
||||
self._proc.stdin.flush()
|
||||
|
||||
@@ -79,12 +84,26 @@ class OcamlSync:
|
||||
raise OcamlSyncError("OCaml subprocess died unexpectedly")
|
||||
return data.decode().rstrip("\n")
|
||||
|
||||
def _strip_epoch(self, inner: str) -> str:
|
||||
"""Strip leading epoch number from a response value: '42 value' → 'value'."""
|
||||
if inner and inner[0].isdigit():
|
||||
space = inner.find(" ")
|
||||
if space > 0:
|
||||
return inner[space + 1:]
|
||||
return "" # epoch only, no value
|
||||
return inner
|
||||
|
||||
def _read_response(self) -> str:
|
||||
"""Read a single response. Returns the value string or raises on error."""
|
||||
"""Read a single response. Returns the value string or raises on error.
|
||||
|
||||
Handles epoch-tagged responses: (ok EPOCH), (ok EPOCH value),
|
||||
(ok-len EPOCH N), (error EPOCH "msg").
|
||||
"""
|
||||
line = self._readline()
|
||||
# Length-prefixed blob: (ok-len N)
|
||||
# Length-prefixed blob: (ok-len N) or (ok-len EPOCH N)
|
||||
if line.startswith("(ok-len "):
|
||||
n = int(line[8:-1])
|
||||
parts = line[1:-1].split() # ["ok-len", ...]
|
||||
n = int(parts[-1]) # last number is always byte count
|
||||
assert self._proc and self._proc.stdout
|
||||
data = self._proc.stdout.read(n)
|
||||
self._proc.stdout.readline() # trailing newline
|
||||
@@ -93,17 +112,18 @@ class OcamlSync:
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
value = _sx_unescape(value[1:-1])
|
||||
return value
|
||||
if line == "(ok)":
|
||||
if line == "(ok)" or (line.startswith("(ok ") and line[4:-1].isdigit()):
|
||||
return ""
|
||||
if line.startswith("(ok-raw "):
|
||||
return line[8:-1]
|
||||
inner = self._strip_epoch(line[8:-1])
|
||||
return inner
|
||||
if line.startswith("(ok "):
|
||||
value = line[4:-1]
|
||||
value = self._strip_epoch(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]
|
||||
msg = self._strip_epoch(line[7:-1])
|
||||
if msg.startswith('"') and msg.endswith('"'):
|
||||
msg = _sx_unescape(msg[1:-1])
|
||||
raise OcamlSyncError(msg)
|
||||
|
||||
Reference in New Issue
Block a user