Implement delimited continuations (shift/reset) across all evaluators

Bootstrap shift/reset to both Python and JS targets. The implementation
uses exception-based capture with re-evaluation: reset wraps in try/catch
for ShiftSignal, shift raises to the nearest reset, and continuation
invocation pushes a resume value and re-evaluates the body.

- Add Continuation type and _ShiftSignal to shared/sx/types.py
- Add sf_reset/sf_shift to hand-written evaluator.py
- Add async versions to async_eval.py
- Add shift/reset dispatch to eval.sx spec
- Bootstrap to Python: FIXUPS_PY with sf_reset/sf_shift, regenerate sx_ref.py
- Bootstrap to JS: Continuation/ShiftSignal types, sfReset/sfShift in fixups
- Add continuation? primitive to both bootstrappers and primitives.sx
- Allow callables (including Continuation) in hand-written HO map
- 44 unit tests (22 per evaluator) covering: passthrough, abort, invoke,
  double invoke, predicate, stored continuation, nested reset, practical patterns
- Update continuations essay to reflect implemented status with examples

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 00:58:50 +00:00
parent 12fe93bb55
commit 102a27e845
12 changed files with 480 additions and 15 deletions

View File

@@ -33,7 +33,7 @@ from __future__ import annotations
from typing import Any
from .types import Component, HandlerDef, Keyword, Lambda, Macro, NIL, PageDef, RelationDef, Symbol
from .types import Component, Continuation, HandlerDef, Keyword, Lambda, Macro, NIL, PageDef, RelationDef, Symbol, _ShiftSignal
from .primitives import _PRIMITIVES
@@ -874,6 +874,42 @@ def _sf_defpage(expr: list, env: dict) -> PageDef:
return page
# ---------------------------------------------------------------------------
# Delimited continuations — shift / reset
# ---------------------------------------------------------------------------
_RESET_RESUME = [] # stack of resume values; empty = not resuming
_RESET_SENTINEL = object()
def _sf_reset(expr, env):
"""(reset body) — establish a continuation delimiter."""
body = expr[1]
try:
return _trampoline(_eval(body, env))
except _ShiftSignal as sig:
def cont_fn(value=NIL):
_RESET_RESUME.append(value)
try:
return _trampoline(_eval(body, env))
finally:
_RESET_RESUME.pop()
k = Continuation(cont_fn)
sig_env = dict(sig.env)
sig_env[sig.k_name] = k
return _trampoline(_eval(sig.body, sig_env))
def _sf_shift(expr, env):
"""(shift k body) — capture continuation to nearest reset."""
if _RESET_RESUME:
return _RESET_RESUME[-1]
k_name = expr[1].name # symbol
body = expr[2]
raise _ShiftSignal(k_name, body, env)
_SPECIAL_FORMS: dict[str, Any] = {
"if": _sf_if,
"when": _sf_when,
@@ -901,6 +937,8 @@ _SPECIAL_FORMS: dict[str, Any] = {
"defpage": _sf_defpage,
"defquery": _sf_defquery,
"defaction": _sf_defaction,
"reset": _sf_reset,
"shift": _sf_shift,
}
@@ -913,9 +951,11 @@ def _ho_map(expr: list, env: dict) -> list:
raise EvalError("map requires fn and collection")
fn = _trampoline(_eval(expr[1], env))
coll = _trampoline(_eval(expr[2], env))
if not isinstance(fn, Lambda):
raise EvalError(f"map requires lambda, got {type(fn).__name__}")
return [_trampoline(_call_lambda(fn, [item], env)) for item in coll]
if isinstance(fn, Lambda):
return [_trampoline(_call_lambda(fn, [item], env)) for item in coll]
if callable(fn):
return [fn(item) for item in coll]
raise EvalError(f"map requires lambda, got {type(fn).__name__}")
def _ho_map_indexed(expr: list, env: dict) -> list: