Files
rose-ash/sx/sx/plans/async-eval-convergence.sx
giles 6f96452f70 Fix empty code blocks: rename ~docs/code param, fix batched IO dispatch
Two bugs caused code blocks to render empty across the site:

1. ~docs/code component had parameter named `code` which collided with
   the HTML <code> tag name. Renamed to `src` and updated all 57
   callers. Added font-mono class for explicit monospace.

2. Batched IO dispatch in ocaml_bridge.py only skipped one leading
   number (batch ID) but the format has two (epoch + ID):
   (io-request EPOCH ID "name" args...). Changed to skip all leading
   numbers so the string name is correctly found. This fixes highlight
   and other batchable helpers returning empty results.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 18:08:40 +00:00

124 lines
11 KiB
Plaintext

;; ---------------------------------------------------------------------------
;; Async Evaluator Convergence — Bootstrap async_eval.py from Spec
;; ---------------------------------------------------------------------------
(defcomp ~plans/async-eval-convergence/plan-async-eval-convergence-content ()
(~docs/page :title "Async Evaluator Convergence"
(~docs/section :title "The Problem" :id "problem"
(p "There are currently " (strong "three") " lambda call implementations that must be kept in sync:")
(ol :class "list-decimal list-inside space-y-2 mt-2"
(li (code "shared/sx/ref/eval.sx") " — the canonical spec, bootstrapped to " (code "sx-ref.js") " and " (code "sx_ref.py"))
(li (code "shared/sx/evaluator.py") " — hand-written synchronous Python evaluator")
(li (code "shared/sx/async_eval.py") " — hand-written asynchronous Python evaluator (the production server path)"))
(p "Every semantic change to the evaluator — lenient lambda arity, new special forms, calling convention tweaks — must be applied to all three. The spec is authoritative but " (code "async_eval.py") " is what actually serves pages. This is a maintenance hazard and a source of subtle divergence bugs.")
(p "The lenient arity change (lambda params pad missing args with nil instead of erroring) exposed this: the spec and sync evaluator were updated, but " (code "async_eval.py") " still had strict arity checking, causing production crashes."))
(~docs/section :title "Why async_eval.py Exists" :id "why"
(p "The async evaluator exists because SX page rendering needs to:")
(ul :class "list-disc list-inside space-y-2 mt-2"
(li (strong "Await IO primitives") " — page helpers like " (code "highlight") ", " (code "reference-data") ", " (code "component-source") " call Python async functions (DB queries, HTTP fetches). The spec evaluator is synchronous.")
(li (strong "Expand server-affinity components") " — " (code ":affinity :server") " components must be fully expanded server-side before serialising to SX wire format. This requires async eval of the component body.")
(li (strong "Handle the aser rendering mode") " — the " (code "_aser") " function evaluates control flow server-side but serialises HTML tags and component calls as SX source for the client. This hybrid eval/serialize mode isn't in the spec."))
(p "None of these require " (em "different") " evaluation semantics — they require the " (em "same") " semantics with async IO at the boundary."))
;; -----------------------------------------------------------------------
;; Architecture
;; -----------------------------------------------------------------------
(~docs/section :title "Target Architecture" :id "architecture"
(p "The goal is to " (strong "eliminate hand-written evaluator code entirely") ". All evaluation semantics come from the spec via bootstrapping. The host provides only:")
(ul :class "list-disc list-inside space-y-2 mt-2"
(li (strong "Platform primitives") " — type constructors, env operations, DOM/HTML primitives")
(li (strong "Async IO bridge") " — a thin wrapper that makes the bootstrapped evaluator await-compatible")
(li (strong "Rendering modes") " — aser/render-to-html dispatch, already mostly specced in " (code "render.sx")))
(p "The bootstrapped " (code "sx_ref.py") " already has correct eval semantics. The question is how to make it async-aware without forking the spec."))
;; -----------------------------------------------------------------------
;; Approach: Async Adapter Layer
;; -----------------------------------------------------------------------
(~docs/section :title "Approach: Async Adapter Layer" :id "approach"
(p "Rather than making the spec itself async (which would pollute it with Python-specific concerns), introduce a thin adapter layer between the bootstrapped evaluator and the IO boundary:")
(h4 :class "font-semibold mt-4 mb-2" "Phase 1 — Async call hook")
(p "The bootstrapped evaluator calls primitives via " (code "apply(fn, args)") ". In the Python host, " (code "apply") " is a platform primitive. Replace it with an async-aware version:")
(~docs/code :src (highlight "(define apply-fn\n (fn (f args)\n ;; Platform provides: if f returns a coroutine, await it\n (apply-maybe-async f args)))" "lisp"))
(p "The bootstrapper emits " (code "apply_maybe_async") " as a Python " (code "async def") " that checks if the result is a coroutine and awaits it if so. Pure functions return immediately. IO primitives return coroutines that get awaited. " (strong "Zero overhead for pure calls") " — just an " (code "isinstance") " check.")
(h4 :class "font-semibold mt-4 mb-2" "Phase 2 — Async trampoline")
(p "The spec's trampoline loop resolves thunks synchronously. The Python bootstrapper emits an " (code "async def trampoline") " variant that can await thunks whose bodies contain IO calls. The trampoline structure is identical — only the " (code "await") " keyword is added.")
(~docs/code :src (highlight "# Bootstrapper emits this for Python async target\nasync def trampoline(val):\n while isinstance(val, Thunk):\n val = await eval_expr(val.expr, val.env)\n return val" "python"))
(h4 :class "font-semibold mt-4 mb-2" "Phase 3 — Aser as spec module")
(p "The " (code "_aser") " rendering mode (evaluate control flow, serialize HTML/components as SX source) should be specced as a module in " (code "render.sx") " alongside " (code "render-to-html") " and " (code "render-to-dom") ". It's currently hand-written Python because it predates the spec, but its logic is pure SX: walk the AST, eval certain forms, serialize others.")
(p "Once aser is specced, the bootstrapper emits it with the same async adapter — IO calls within aser bodies get awaited transparently.")
(h4 :class "font-semibold mt-4 mb-2" "Phase 4 — Delete hand-written evaluators")
(p "With the async adapter + specced aser, " (code "evaluator.py") " and " (code "async_eval.py") " become dead code. Delete them. All evaluation flows through the bootstrapped " (code "sx_ref.py") " with async adapter."))
;; -----------------------------------------------------------------------
;; What changes in the bootstrapper
;; -----------------------------------------------------------------------
(~docs/section :title "Bootstrapper Changes" :id "bootstrapper"
(p "The Python bootstrapper (" (code "bootstrap_py.py") ") gains a new emit mode: " (code "--async") ". This emits:")
(ul :class "list-disc list-inside space-y-2 mt-2"
(li (code "async def eval_expr") " instead of " (code "def eval_expr"))
(li (code "async def trampoline") " with " (code "await") " on thunk eval")
(li (code "apply_maybe_async") " that awaits coroutine results")
(li "All higher-order forms (" (code "map") ", " (code "filter") ", " (code "reduce") ", etc.) as " (code "async def") " with " (code "await") " on callback invocations"))
(p "The JS bootstrapper is unaffected — the browser evaluator is synchronous (IO is handled by the SxEngine request pipeline, not inline eval)."))
;; -----------------------------------------------------------------------
;; Migration path
;; -----------------------------------------------------------------------
(~docs/section :title "Migration Path" :id "migration"
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" "Step")
(th :class "px-3 py-2 font-medium text-stone-600" "What")
(th :class "px-3 py-2 font-medium text-stone-600" "Risk")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "1")
(td :class "px-3 py-2 text-stone-700" "Add async emit mode to bootstrap_py.py. Generate async_sx_ref.py alongside sx_ref.py.")
(td :class "px-3 py-2 text-stone-600" "Low — new file, nothing changes yet"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "2")
(td :class "px-3 py-2 text-stone-700" "Run async_sx_ref.py in parallel with async_eval.py, compare outputs on every page render.")
(td :class "px-3 py-2 text-stone-600" "Low — shadow mode, no user impact"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "3")
(td :class "px-3 py-2 text-stone-700" "Spec aser in render.sx. Bootstrap it. Shadow-compare with hand-written _aser.")
(td :class "px-3 py-2 text-stone-600" "Medium — aser has edge cases around OOB, fragments"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "4")
(td :class "px-3 py-2 text-stone-700" "Switch page rendering to async_sx_ref.py. Keep async_eval.py as fallback.")
(td :class "px-3 py-2 text-stone-600" "Medium — production path changes"))
(tr
(td :class "px-3 py-2 text-stone-700" "5")
(td :class "px-3 py-2 text-stone-700" "Delete evaluator.py and async_eval.py.")
(td :class "px-3 py-2 text-stone-600" "Low — once shadow confirms parity"))))))
;; -----------------------------------------------------------------------
;; Principles
;; -----------------------------------------------------------------------
(~docs/section :title "Principles" :id "principles"
(ul :class "list-disc list-inside space-y-2"
(li (strong "The spec is the single source of truth.") " All SX evaluation semantics live in .sx files. Host code implements platform primitives, not evaluation rules.")
(li (strong "Async is a host concern, not a language concern.") " The spec is synchronous. The Python bootstrapper emits async wrappers. The JS bootstrapper emits sync code. The spec doesn't know or care.")
(li (strong "Shadow-compare before switching.") " Every migration step runs both paths in parallel and asserts identical output. No big-bang cutover.")
(li (strong "Aser is just another render mode.") " It belongs in render.sx alongside render-to-html and render-to-dom. It's not special — it's the 'evaluate some, serialize the rest' mode.")))
(~docs/section :title "Outcome" :id "outcome"
(p "After convergence:")
(ul :class "list-disc list-inside space-y-2 mt-2"
(li "One evaluator implementation (the spec), bootstrapped to every host")
(li "Semantic changes made once in .sx, automatically propagated")
(li "~2,000 lines of hand-written Python evaluator code deleted")
(li "The lenient-arity class of bug becomes impossible — it's fixed in the spec, done everywhere")))))