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:
@@ -28,20 +28,163 @@ import contextvars
|
||||
from typing import Any
|
||||
|
||||
from .types import Component, Island, Keyword, Lambda, Macro, NIL, Symbol
|
||||
# sx_ref.py removed — these stubs exist so the module loads.
|
||||
# With SX_USE_OCAML=1, rendering goes through the OCaml bridge; these
|
||||
# are only called if a service falls back to Python-side rendering.
|
||||
def _not_available(*a, **kw):
|
||||
raise RuntimeError("sx_ref.py has been removed — use SX_USE_OCAML=1")
|
||||
_raw_eval = _raw_call_component = _expand_macro = _trampoline = _not_available
|
||||
|
||||
|
||||
def _eval(expr, env):
|
||||
"""Evaluate and unwrap thunks — all html.py _eval calls are non-tail."""
|
||||
return _trampoline(_raw_eval(expr, env))
|
||||
"""Minimal Python evaluator for sync html.py rendering.
|
||||
|
||||
def _call_component(comp, raw_args, env):
|
||||
"""Call component and unwrap thunks — non-tail in html.py."""
|
||||
return _trampoline(_raw_call_component(comp, raw_args, env))
|
||||
Handles: literals, symbols, keywords, dicts, special forms (if, when,
|
||||
cond, let, begin/do, and, or, str, not, list), lambda calls, and
|
||||
primitive function calls. Enough for the sync sx() Jinja function.
|
||||
"""
|
||||
from .primitives import _PRIMITIVES
|
||||
|
||||
# Literals
|
||||
if isinstance(expr, (int, float, str, bool)):
|
||||
return expr
|
||||
if expr is None or expr is NIL:
|
||||
return NIL
|
||||
|
||||
# Symbol lookup
|
||||
if isinstance(expr, Symbol):
|
||||
name = expr.name
|
||||
if name in env:
|
||||
return env[name]
|
||||
if name in _PRIMITIVES:
|
||||
return _PRIMITIVES[name]
|
||||
if name == "true":
|
||||
return True
|
||||
if name == "false":
|
||||
return False
|
||||
if name == "nil":
|
||||
return NIL
|
||||
from .types import EvalError
|
||||
raise EvalError(f"Undefined symbol: {name}")
|
||||
|
||||
# Keyword
|
||||
if isinstance(expr, Keyword):
|
||||
return expr.name
|
||||
|
||||
# Dict
|
||||
if isinstance(expr, dict):
|
||||
return {k: _eval(v, env) for k, v in expr.items()}
|
||||
|
||||
# List — dispatch
|
||||
if not isinstance(expr, list):
|
||||
return expr
|
||||
if not expr:
|
||||
return []
|
||||
|
||||
head = expr[0]
|
||||
if isinstance(head, Symbol):
|
||||
name = head.name
|
||||
|
||||
# Special forms
|
||||
if name == "if":
|
||||
cond = _eval(expr[1], env)
|
||||
if cond and cond is not NIL:
|
||||
return _eval(expr[2], env)
|
||||
return _eval(expr[3], env) if len(expr) > 3 else NIL
|
||||
|
||||
if name == "when":
|
||||
cond = _eval(expr[1], env)
|
||||
if cond and cond is not NIL:
|
||||
result = NIL
|
||||
for body in expr[2:]:
|
||||
result = _eval(body, env)
|
||||
return result
|
||||
return NIL
|
||||
|
||||
if name == "let":
|
||||
bindings = expr[1]
|
||||
local = dict(env)
|
||||
if isinstance(bindings, list):
|
||||
if bindings and isinstance(bindings[0], list):
|
||||
for b in bindings:
|
||||
vname = b[0].name if isinstance(b[0], Symbol) else b[0]
|
||||
local[vname] = _eval(b[1], local)
|
||||
elif len(bindings) % 2 == 0:
|
||||
for i in range(0, len(bindings), 2):
|
||||
vname = bindings[i].name if isinstance(bindings[i], Symbol) else bindings[i]
|
||||
local[vname] = _eval(bindings[i + 1], local)
|
||||
result = NIL
|
||||
for body in expr[2:]:
|
||||
result = _eval(body, local)
|
||||
return result
|
||||
|
||||
if name in ("begin", "do"):
|
||||
result = NIL
|
||||
for body in expr[1:]:
|
||||
result = _eval(body, env)
|
||||
return result
|
||||
|
||||
if name == "and":
|
||||
result = True
|
||||
for arg in expr[1:]:
|
||||
result = _eval(arg, env)
|
||||
if not result or result is NIL:
|
||||
return result
|
||||
return result
|
||||
|
||||
if name == "or":
|
||||
for arg in expr[1:]:
|
||||
result = _eval(arg, env)
|
||||
if result and result is not NIL:
|
||||
return result
|
||||
return NIL
|
||||
|
||||
if name == "not":
|
||||
val = _eval(expr[1], env)
|
||||
return val is NIL or val is False or val is None
|
||||
|
||||
if name == "lambda" or name == "fn":
|
||||
params_form = expr[1]
|
||||
param_names = [p.name if isinstance(p, Symbol) else str(p) for p in params_form]
|
||||
return Lambda(params=param_names, body=expr[2], closure=dict(env))
|
||||
|
||||
if name == "define":
|
||||
var_name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
|
||||
env[var_name] = _eval(expr[2], env)
|
||||
return NIL
|
||||
|
||||
if name == "quote":
|
||||
return expr[1]
|
||||
|
||||
if name == "str":
|
||||
parts = []
|
||||
for arg in expr[1:]:
|
||||
val = _eval(arg, env)
|
||||
if val is NIL or val is None:
|
||||
parts.append("")
|
||||
else:
|
||||
parts.append(str(val))
|
||||
return "".join(parts)
|
||||
|
||||
if name == "list":
|
||||
return [_eval(arg, env) for arg in expr[1:]]
|
||||
|
||||
# Primitive or function call
|
||||
fn = _eval(head, env)
|
||||
else:
|
||||
fn = _eval(head, env)
|
||||
|
||||
# Evaluate args
|
||||
args = [_eval(a, env) for a in expr[1:]]
|
||||
|
||||
# Call
|
||||
if callable(fn):
|
||||
return fn(*args)
|
||||
if isinstance(fn, Lambda):
|
||||
local = dict(fn.closure)
|
||||
local.update(env)
|
||||
for p, v in zip(fn.params, args):
|
||||
local[p] = v
|
||||
return _eval(fn.body, local)
|
||||
return NIL
|
||||
|
||||
|
||||
def _expand_macro(*a, **kw):
|
||||
raise RuntimeError("Macro expansion requires OCaml bridge")
|
||||
|
||||
# ContextVar for collecting CSS class names during render.
|
||||
# Set to a set[str] to collect; None to skip.
|
||||
|
||||
Reference in New Issue
Block a user