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:
@@ -484,6 +484,48 @@ _ASYNC_SPECIAL_FORMS: dict[str, Any] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Async delimited continuations — shift / reset
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_ASYNC_RESET_RESUME: list = []
|
||||||
|
|
||||||
|
|
||||||
|
async def _asf_reset(expr, env, ctx):
|
||||||
|
"""(reset body) — async version."""
|
||||||
|
from .types import Continuation, _ShiftSignal
|
||||||
|
body = expr[1]
|
||||||
|
try:
|
||||||
|
return await async_eval(body, env, ctx)
|
||||||
|
except _ShiftSignal as sig:
|
||||||
|
def cont_fn(value=None):
|
||||||
|
from .types import NIL
|
||||||
|
_ASYNC_RESET_RESUME.append(value if value is not None else NIL)
|
||||||
|
try:
|
||||||
|
# Sync re-evaluation; the async caller will trampoline
|
||||||
|
from .evaluator import _eval as sync_eval, _trampoline
|
||||||
|
return _trampoline(sync_eval(body, env))
|
||||||
|
finally:
|
||||||
|
_ASYNC_RESET_RESUME.pop()
|
||||||
|
k = Continuation(cont_fn)
|
||||||
|
sig_env = dict(sig.env)
|
||||||
|
sig_env[sig.k_name] = k
|
||||||
|
return await async_eval(sig.body, sig_env, ctx)
|
||||||
|
|
||||||
|
|
||||||
|
async def _asf_shift(expr, env, ctx):
|
||||||
|
"""(shift k body) — async version."""
|
||||||
|
from .types import _ShiftSignal
|
||||||
|
if _ASYNC_RESET_RESUME:
|
||||||
|
return _ASYNC_RESET_RESUME[-1]
|
||||||
|
k_name = expr[1].name
|
||||||
|
body = expr[2]
|
||||||
|
raise _ShiftSignal(k_name, body, env)
|
||||||
|
|
||||||
|
_ASYNC_SPECIAL_FORMS["reset"] = _asf_reset
|
||||||
|
_ASYNC_SPECIAL_FORMS["shift"] = _asf_shift
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Async higher-order forms
|
# Async higher-order forms
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
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
|
from .primitives import _PRIMITIVES
|
||||||
|
|
||||||
|
|
||||||
@@ -874,6 +874,42 @@ def _sf_defpage(expr: list, env: dict) -> PageDef:
|
|||||||
return page
|
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] = {
|
_SPECIAL_FORMS: dict[str, Any] = {
|
||||||
"if": _sf_if,
|
"if": _sf_if,
|
||||||
"when": _sf_when,
|
"when": _sf_when,
|
||||||
@@ -901,6 +937,8 @@ _SPECIAL_FORMS: dict[str, Any] = {
|
|||||||
"defpage": _sf_defpage,
|
"defpage": _sf_defpage,
|
||||||
"defquery": _sf_defquery,
|
"defquery": _sf_defquery,
|
||||||
"defaction": _sf_defaction,
|
"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")
|
raise EvalError("map requires fn and collection")
|
||||||
fn = _trampoline(_eval(expr[1], env))
|
fn = _trampoline(_eval(expr[1], env))
|
||||||
coll = _trampoline(_eval(expr[2], env))
|
coll = _trampoline(_eval(expr[2], env))
|
||||||
if not isinstance(fn, Lambda):
|
if isinstance(fn, Lambda):
|
||||||
raise EvalError(f"map requires lambda, got {type(fn).__name__}")
|
return [_trampoline(_call_lambda(fn, [item], env)) for item in coll]
|
||||||
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:
|
def _ho_map_indexed(expr: list, env: dict) -> list:
|
||||||
|
|||||||
@@ -187,6 +187,11 @@ def prim_is_list(x: Any) -> bool:
|
|||||||
def prim_is_dict(x: Any) -> bool:
|
def prim_is_dict(x: Any) -> bool:
|
||||||
return isinstance(x, dict)
|
return isinstance(x, dict)
|
||||||
|
|
||||||
|
@register_primitive("continuation?")
|
||||||
|
def prim_is_continuation(x: Any) -> bool:
|
||||||
|
from .types import Continuation
|
||||||
|
return isinstance(x, Continuation)
|
||||||
|
|
||||||
@register_primitive("empty?")
|
@register_primitive("empty?")
|
||||||
def prim_is_empty(coll: Any) -> bool:
|
def prim_is_empty(coll: Any) -> bool:
|
||||||
if coll is None or coll is NIL:
|
if coll is None or coll is NIL:
|
||||||
|
|||||||
@@ -174,6 +174,8 @@ class JSEmitter:
|
|||||||
"sf-quasiquote": "sfQuasiquote",
|
"sf-quasiquote": "sfQuasiquote",
|
||||||
"sf-thread-first": "sfThreadFirst",
|
"sf-thread-first": "sfThreadFirst",
|
||||||
"sf-set!": "sfSetBang",
|
"sf-set!": "sfSetBang",
|
||||||
|
"sf-reset": "sfReset",
|
||||||
|
"sf-shift": "sfShift",
|
||||||
"qq-expand": "qqExpand",
|
"qq-expand": "qqExpand",
|
||||||
"ho-map": "hoMap",
|
"ho-map": "hoMap",
|
||||||
"ho-map-indexed": "hoMapIndexed",
|
"ho-map-indexed": "hoMapIndexed",
|
||||||
@@ -1162,6 +1164,16 @@ PREAMBLE = '''\
|
|||||||
}
|
}
|
||||||
StyleValue.prototype._styleValue = true;
|
StyleValue.prototype._styleValue = true;
|
||||||
|
|
||||||
|
function Continuation(fn) { this.fn = fn; }
|
||||||
|
Continuation.prototype._continuation = true;
|
||||||
|
Continuation.prototype.call = function(value) { return this.fn(value !== undefined ? value : NIL); };
|
||||||
|
|
||||||
|
function ShiftSignal(kName, body, env) {
|
||||||
|
this.kName = kName;
|
||||||
|
this.body = body;
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
|
|
||||||
function isSym(x) { return x != null && x._sym === true; }
|
function isSym(x) { return x != null && x._sym === true; }
|
||||||
function isKw(x) { return x != null && x._kw === true; }
|
function isKw(x) { return x != null && x._kw === true; }
|
||||||
|
|
||||||
@@ -1199,6 +1211,7 @@ PLATFORM_JS = '''
|
|||||||
if (x._macro) return "macro";
|
if (x._macro) return "macro";
|
||||||
if (x._raw) return "raw-html";
|
if (x._raw) return "raw-html";
|
||||||
if (x._styleValue) return "style-value";
|
if (x._styleValue) return "style-value";
|
||||||
|
if (x._continuation) return "continuation";
|
||||||
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
|
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
|
||||||
if (Array.isArray(x)) return "list";
|
if (Array.isArray(x)) return "list";
|
||||||
if (typeof x === "object") return "dict";
|
if (typeof x === "object") return "dict";
|
||||||
@@ -1370,6 +1383,7 @@ PLATFORM_JS = '''
|
|||||||
PRIMITIVES["string?"] = function(x) { return typeof x === "string"; };
|
PRIMITIVES["string?"] = function(x) { return typeof x === "string"; };
|
||||||
PRIMITIVES["list?"] = Array.isArray;
|
PRIMITIVES["list?"] = Array.isArray;
|
||||||
PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; };
|
PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; };
|
||||||
|
PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; };
|
||||||
PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); };
|
PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); };
|
||||||
PRIMITIVES["contains?"] = function(c, k) {
|
PRIMITIVES["contains?"] = function(c, k) {
|
||||||
if (typeof c === "string") return c.indexOf(String(k)) !== -1;
|
if (typeof c === "string") return c.indexOf(String(k)) !== -1;
|
||||||
@@ -1559,7 +1573,7 @@ PLATFORM_JS = '''
|
|||||||
"if":1,"when":1,"cond":1,"case":1,"and":1,"or":1,"let":1,"let*":1,
|
"if":1,"when":1,"cond":1,"case":1,"and":1,"or":1,"let":1,"let*":1,
|
||||||
"lambda":1,"fn":1,"define":1,"defcomp":1,"defmacro":1,"defstyle":1,
|
"lambda":1,"fn":1,"define":1,"defcomp":1,"defmacro":1,"defstyle":1,
|
||||||
"defkeyframes":1,"defhandler":1,"begin":1,"do":1,
|
"defkeyframes":1,"defhandler":1,"begin":1,"do":1,
|
||||||
"quote":1,"quasiquote":1,"->":1,"set!":1
|
"quote":1,"quasiquote":1,"->":1,"set!":1,"reset":1,"shift":1
|
||||||
}; }
|
}; }
|
||||||
function isHoForm(n) { return n in {
|
function isHoForm(n) { return n in {
|
||||||
"map":1,"map-indexed":1,"filter":1,"reduce":1,"some":1,"every?":1,"for-each":1
|
"map":1,"map-indexed":1,"filter":1,"reduce":1,"some":1,"every?":1,"for-each":1
|
||||||
@@ -2639,6 +2653,44 @@ def fixups_js(has_html, has_sx, has_dom):
|
|||||||
return _rawCallLambda(f, args, callerEnv);
|
return _rawCallLambda(f, args, callerEnv);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Delimited continuations (shift/reset)
|
||||||
|
// =========================================================================
|
||||||
|
var _resetResume = []; // stack of resume values
|
||||||
|
|
||||||
|
function sfReset(args, env) {
|
||||||
|
var body = args[0];
|
||||||
|
try {
|
||||||
|
return trampoline(evalExpr(body, env));
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ShiftSignal) {
|
||||||
|
var sig = e;
|
||||||
|
var cont = new Continuation(function(value) {
|
||||||
|
if (value === undefined) value = NIL;
|
||||||
|
_resetResume.push(value);
|
||||||
|
try {
|
||||||
|
return trampoline(evalExpr(body, env));
|
||||||
|
} finally {
|
||||||
|
_resetResume.pop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var sigEnv = merge(sig.env);
|
||||||
|
sigEnv[sig.kName] = cont;
|
||||||
|
return trampoline(evalExpr(sig.body, sigEnv));
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sfShift(args, env) {
|
||||||
|
if (_resetResume.length > 0) {
|
||||||
|
return _resetResume[_resetResume.length - 1];
|
||||||
|
}
|
||||||
|
var kName = symbolName(args[0]);
|
||||||
|
var body = args[1];
|
||||||
|
throw new ShiftSignal(kName, body, env);
|
||||||
|
}
|
||||||
|
|
||||||
// Expose render functions as primitives so SX code can call them''']
|
// Expose render functions as primitives so SX code can call them''']
|
||||||
if has_html:
|
if has_html:
|
||||||
lines.append(' if (typeof renderToHtml === "function") PRIMITIVES["render-to-html"] = renderToHtml;')
|
lines.append(' if (typeof renderToHtml === "function") PRIMITIVES["render-to-html"] = renderToHtml;')
|
||||||
|
|||||||
@@ -186,6 +186,8 @@ class PyEmitter:
|
|||||||
"sf-quasiquote": "sf_quasiquote",
|
"sf-quasiquote": "sf_quasiquote",
|
||||||
"sf-thread-first": "sf_thread_first",
|
"sf-thread-first": "sf_thread_first",
|
||||||
"sf-set!": "sf_set_bang",
|
"sf-set!": "sf_set_bang",
|
||||||
|
"sf-reset": "sf_reset",
|
||||||
|
"sf-shift": "sf_shift",
|
||||||
"qq-expand": "qq_expand",
|
"qq-expand": "qq_expand",
|
||||||
"ho-map": "ho_map",
|
"ho-map": "ho_map",
|
||||||
"ho-map-indexed": "ho_map_indexed",
|
"ho-map-indexed": "ho_map_indexed",
|
||||||
@@ -887,8 +889,8 @@ from typing import Any
|
|||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
||||||
from shared.sx.types import (
|
from shared.sx.types import (
|
||||||
NIL, Symbol, Keyword, Lambda, Component, Macro, StyleValue,
|
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro, StyleValue,
|
||||||
HandlerDef, QueryDef, ActionDef, PageDef,
|
HandlerDef, QueryDef, ActionDef, PageDef, _ShiftSignal,
|
||||||
)
|
)
|
||||||
from shared.sx.parser import SxExpr
|
from shared.sx.parser import SxExpr
|
||||||
'''
|
'''
|
||||||
@@ -998,6 +1000,8 @@ def type_of(x):
|
|||||||
return "raw-html"
|
return "raw-html"
|
||||||
if isinstance(x, StyleValue):
|
if isinstance(x, StyleValue):
|
||||||
return "style-value"
|
return "style-value"
|
||||||
|
if isinstance(x, Continuation):
|
||||||
|
return "continuation"
|
||||||
if isinstance(x, list):
|
if isinstance(x, list):
|
||||||
return "list"
|
return "list"
|
||||||
if isinstance(x, dict):
|
if isinstance(x, dict):
|
||||||
@@ -1338,7 +1342,7 @@ _SPECIAL_FORM_NAMES = frozenset([
|
|||||||
"define", "defcomp", "defmacro", "defstyle", "defkeyframes",
|
"define", "defcomp", "defmacro", "defstyle", "defkeyframes",
|
||||||
"defhandler", "defpage", "defquery", "defaction", "defrelation",
|
"defhandler", "defpage", "defquery", "defaction", "defrelation",
|
||||||
"begin", "do", "quote", "quasiquote",
|
"begin", "do", "quote", "quasiquote",
|
||||||
"->", "set!",
|
"->", "set!", "reset", "shift",
|
||||||
])
|
])
|
||||||
|
|
||||||
_HO_FORM_NAMES = frozenset([
|
_HO_FORM_NAMES = frozenset([
|
||||||
@@ -1501,6 +1505,11 @@ def aser_special(name, expr, env):
|
|||||||
"defhandler", "defpage", "defquery", "defaction", "defrelation"):
|
"defhandler", "defpage", "defquery", "defaction", "defrelation"):
|
||||||
trampoline(eval_expr(expr, env))
|
trampoline(eval_expr(expr, env))
|
||||||
return NIL
|
return NIL
|
||||||
|
# reset/shift — evaluate normally in aser mode (they're control flow)
|
||||||
|
if name == "reset":
|
||||||
|
return sf_reset(args, env)
|
||||||
|
if name == "shift":
|
||||||
|
return sf_shift(args, env)
|
||||||
# Lambda/fn, quote, quasiquote, set!, -> : evaluate normally
|
# Lambda/fn, quote, quasiquote, set!, -> : evaluate normally
|
||||||
result = eval_expr(expr, env)
|
result = eval_expr(expr, env)
|
||||||
return trampoline(result)
|
return trampoline(result)
|
||||||
@@ -1587,6 +1596,7 @@ PRIMITIVES["number?"] = lambda x: isinstance(x, (int, float)) and not isinstance
|
|||||||
PRIMITIVES["string?"] = lambda x: isinstance(x, str)
|
PRIMITIVES["string?"] = lambda x: isinstance(x, str)
|
||||||
PRIMITIVES["list?"] = lambda x: isinstance(x, _b_list)
|
PRIMITIVES["list?"] = lambda x: isinstance(x, _b_list)
|
||||||
PRIMITIVES["dict?"] = lambda x: isinstance(x, _b_dict)
|
PRIMITIVES["dict?"] = lambda x: isinstance(x, _b_dict)
|
||||||
|
PRIMITIVES["continuation?"] = lambda x: isinstance(x, Continuation)
|
||||||
PRIMITIVES["empty?"] = lambda c: (
|
PRIMITIVES["empty?"] = lambda c: (
|
||||||
c is None or c is NIL or
|
c is None or c is NIL or
|
||||||
(isinstance(c, (_b_list, str, _b_dict)) and _b_len(c) == 0)
|
(isinstance(c, (_b_list, str, _b_dict)) and _b_len(c) == 0)
|
||||||
@@ -1725,6 +1735,38 @@ concat = PRIMITIVES["concat"]
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
FIXUPS_PY = '''
|
FIXUPS_PY = '''
|
||||||
|
# =========================================================================
|
||||||
|
# Delimited continuations (shift/reset)
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
_RESET_RESUME = [] # stack of resume values; empty = not resuming
|
||||||
|
|
||||||
|
def sf_reset(args, env):
|
||||||
|
"""(reset body) -- establish a continuation delimiter."""
|
||||||
|
body = first(args)
|
||||||
|
try:
|
||||||
|
return trampoline(eval_expr(body, env))
|
||||||
|
except _ShiftSignal as sig:
|
||||||
|
def cont_fn(value=NIL):
|
||||||
|
_RESET_RESUME.append(value)
|
||||||
|
try:
|
||||||
|
return trampoline(eval_expr(body, env))
|
||||||
|
finally:
|
||||||
|
_RESET_RESUME.pop()
|
||||||
|
k = Continuation(cont_fn)
|
||||||
|
sig_env = dict(sig.env)
|
||||||
|
sig_env[sig.k_name] = k
|
||||||
|
return trampoline(eval_expr(sig.body, sig_env))
|
||||||
|
|
||||||
|
def sf_shift(args, env):
|
||||||
|
"""(shift k body) -- capture continuation to nearest reset."""
|
||||||
|
if _RESET_RESUME:
|
||||||
|
return _RESET_RESUME[-1]
|
||||||
|
k_name = symbol_name(first(args))
|
||||||
|
body = nth(args, 1)
|
||||||
|
raise _ShiftSignal(k_name, body, env)
|
||||||
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Fixups -- wire up render adapter dispatch
|
# Fixups -- wire up render adapter dispatch
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|||||||
@@ -153,6 +153,8 @@
|
|||||||
(= name "quasiquote") (sf-quasiquote args env)
|
(= name "quasiquote") (sf-quasiquote args env)
|
||||||
(= name "->") (sf-thread-first args env)
|
(= name "->") (sf-thread-first args env)
|
||||||
(= name "set!") (sf-set! args env)
|
(= name "set!") (sf-set! args env)
|
||||||
|
(= name "reset") (sf-reset args env)
|
||||||
|
(= name "shift") (sf-shift args env)
|
||||||
|
|
||||||
;; Higher-order forms
|
;; Higher-order forms
|
||||||
(= name "map") (ho-map args env)
|
(= name "map") (ho-map args env)
|
||||||
|
|||||||
@@ -197,6 +197,11 @@
|
|||||||
:returns "boolean"
|
:returns "boolean"
|
||||||
:doc "True if x is a dict/map.")
|
:doc "True if x is a dict/map.")
|
||||||
|
|
||||||
|
(define-primitive "continuation?"
|
||||||
|
:params (x)
|
||||||
|
:returns "boolean"
|
||||||
|
:doc "True if x is a captured continuation.")
|
||||||
|
|
||||||
(define-primitive "empty?"
|
(define-primitive "empty?"
|
||||||
:params (coll)
|
:params (coll)
|
||||||
:returns "boolean"
|
:returns "boolean"
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ from typing import Any
|
|||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
||||||
from shared.sx.types import (
|
from shared.sx.types import (
|
||||||
NIL, Symbol, Keyword, Lambda, Component, Macro, StyleValue,
|
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro, StyleValue,
|
||||||
HandlerDef, QueryDef, ActionDef, PageDef,
|
HandlerDef, QueryDef, ActionDef, PageDef, _ShiftSignal,
|
||||||
)
|
)
|
||||||
from shared.sx.parser import SxExpr
|
from shared.sx.parser import SxExpr
|
||||||
|
|
||||||
@@ -127,6 +127,8 @@ def type_of(x):
|
|||||||
return "raw-html"
|
return "raw-html"
|
||||||
if isinstance(x, StyleValue):
|
if isinstance(x, StyleValue):
|
||||||
return "style-value"
|
return "style-value"
|
||||||
|
if isinstance(x, Continuation):
|
||||||
|
return "continuation"
|
||||||
if isinstance(x, list):
|
if isinstance(x, list):
|
||||||
return "list"
|
return "list"
|
||||||
if isinstance(x, dict):
|
if isinstance(x, dict):
|
||||||
@@ -467,7 +469,7 @@ _SPECIAL_FORM_NAMES = frozenset([
|
|||||||
"define", "defcomp", "defmacro", "defstyle", "defkeyframes",
|
"define", "defcomp", "defmacro", "defstyle", "defkeyframes",
|
||||||
"defhandler", "defpage", "defquery", "defaction", "defrelation",
|
"defhandler", "defpage", "defquery", "defaction", "defrelation",
|
||||||
"begin", "do", "quote", "quasiquote",
|
"begin", "do", "quote", "quasiquote",
|
||||||
"->", "set!",
|
"->", "set!", "reset", "shift",
|
||||||
])
|
])
|
||||||
|
|
||||||
_HO_FORM_NAMES = frozenset([
|
_HO_FORM_NAMES = frozenset([
|
||||||
@@ -630,6 +632,11 @@ def aser_special(name, expr, env):
|
|||||||
"defhandler", "defpage", "defquery", "defaction", "defrelation"):
|
"defhandler", "defpage", "defquery", "defaction", "defrelation"):
|
||||||
trampoline(eval_expr(expr, env))
|
trampoline(eval_expr(expr, env))
|
||||||
return NIL
|
return NIL
|
||||||
|
# reset/shift — evaluate normally in aser mode (they're control flow)
|
||||||
|
if name == "reset":
|
||||||
|
return sf_reset(args, env)
|
||||||
|
if name == "shift":
|
||||||
|
return sf_shift(args, env)
|
||||||
# Lambda/fn, quote, quasiquote, set!, -> : evaluate normally
|
# Lambda/fn, quote, quasiquote, set!, -> : evaluate normally
|
||||||
result = eval_expr(expr, env)
|
result = eval_expr(expr, env)
|
||||||
return trampoline(result)
|
return trampoline(result)
|
||||||
@@ -715,6 +722,7 @@ PRIMITIVES["number?"] = lambda x: isinstance(x, (int, float)) and not isinstance
|
|||||||
PRIMITIVES["string?"] = lambda x: isinstance(x, str)
|
PRIMITIVES["string?"] = lambda x: isinstance(x, str)
|
||||||
PRIMITIVES["list?"] = lambda x: isinstance(x, _b_list)
|
PRIMITIVES["list?"] = lambda x: isinstance(x, _b_list)
|
||||||
PRIMITIVES["dict?"] = lambda x: isinstance(x, _b_dict)
|
PRIMITIVES["dict?"] = lambda x: isinstance(x, _b_dict)
|
||||||
|
PRIMITIVES["continuation?"] = lambda x: isinstance(x, Continuation)
|
||||||
PRIMITIVES["empty?"] = lambda c: (
|
PRIMITIVES["empty?"] = lambda c: (
|
||||||
c is None or c is NIL or
|
c is None or c is NIL or
|
||||||
(isinstance(c, (_b_list, str, _b_dict)) and _b_len(c) == 0)
|
(isinstance(c, (_b_list, str, _b_dict)) and _b_len(c) == 0)
|
||||||
@@ -861,7 +869,7 @@ trampoline = lambda val: (lambda result: (trampoline(eval_expr(thunk_expr(result
|
|||||||
eval_expr = lambda expr, env: _sx_case(type_of(expr), [('number', lambda: expr), ('string', lambda: expr), ('boolean', lambda: expr), ('nil', lambda: NIL), ('symbol', lambda: (lambda name: (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))))(symbol_name(expr))), ('keyword', lambda: keyword_name(expr)), ('dict', lambda: map_dict(lambda k, v: trampoline(eval_expr(v, env)), expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else eval_list(expr, env))), (None, lambda: expr)])
|
eval_expr = lambda expr, env: _sx_case(type_of(expr), [('number', lambda: expr), ('string', lambda: expr), ('boolean', lambda: expr), ('nil', lambda: NIL), ('symbol', lambda: (lambda name: (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))))(symbol_name(expr))), ('keyword', lambda: keyword_name(expr)), ('dict', lambda: map_dict(lambda k, v: trampoline(eval_expr(v, env)), expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else eval_list(expr, env))), (None, lambda: expr)])
|
||||||
|
|
||||||
# eval-list
|
# eval-list
|
||||||
eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defkeyframes(args, env) if sx_truthy((name == 'defkeyframes')) else (sf_defhandler(args, env) if sx_truthy((name == 'defhandler')) else (sf_defpage(args, env) if sx_truthy((name == 'defpage')) else (sf_defquery(args, env) if sx_truthy((name == 'defquery')) else (sf_defaction(args, env) if sx_truthy((name == 'defaction')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy(is_render_expr(expr)) else eval_call(head, args, env))))))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr))
|
eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defkeyframes(args, env) if sx_truthy((name == 'defkeyframes')) else (sf_defhandler(args, env) if sx_truthy((name == 'defhandler')) else (sf_defpage(args, env) if sx_truthy((name == 'defpage')) else (sf_defquery(args, env) if sx_truthy((name == 'defquery')) else (sf_defaction(args, env) if sx_truthy((name == 'defaction')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (sf_reset(args, env) if sx_truthy((name == 'reset')) else (sf_shift(args, env) if sx_truthy((name == 'shift')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy(is_render_expr(expr)) else eval_call(head, args, env))))))))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr))
|
||||||
|
|
||||||
# eval-call
|
# eval-call
|
||||||
eval_call = lambda head, args, env: (lambda f: (lambda evaluated_args: (apply(f, evaluated_args) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else (not sx_truthy(is_component(f)))))) else (call_lambda(f, evaluated_args, env) if sx_truthy(is_lambda(f)) else (call_component(f, args, env) if sx_truthy(is_component(f)) else error(sx_str('Not callable: ', inspect(f)))))))(map(lambda a: trampoline(eval_expr(a, env)), args)))(trampoline(eval_expr(head, env)))
|
eval_call = lambda head, args, env: (lambda f: (lambda evaluated_args: (apply(f, evaluated_args) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else (not sx_truthy(is_component(f)))))) else (call_lambda(f, evaluated_args, env) if sx_truthy(is_lambda(f)) else (call_component(f, args, env) if sx_truthy(is_component(f)) else error(sx_str('Not callable: ', inspect(f)))))))(map(lambda a: trampoline(eval_expr(a, env)), args)))(trampoline(eval_expr(head, env)))
|
||||||
@@ -1100,6 +1108,38 @@ aser_fragment = lambda children, env: (lambda parts: ('' if sx_truthy(empty_p(pa
|
|||||||
aser_call = lambda name, args, env: (lambda parts: _sx_begin(reduce(lambda state, arg: (lambda skip: (assoc(state, 'skip', False, 'i', (get(state, 'i') + 1)) if sx_truthy(skip) else ((lambda val: _sx_begin((_sx_begin(_sx_append(parts, sx_str(':', keyword_name(arg))), _sx_append(parts, serialize(val))) if sx_truthy((not sx_truthy(is_nil(val)))) else NIL), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(aser(nth(args, (get(state, 'i') + 1)), env)) if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((get(state, 'i') + 1) < len(args)))) else (lambda val: _sx_begin((_sx_append(parts, serialize(val)) if sx_truthy((not sx_truthy(is_nil(val)))) else NIL), assoc(state, 'i', (get(state, 'i') + 1))))(aser(arg, env)))))(get(state, 'skip')), {'i': 0, 'skip': False}, args), sx_str('(', join(' ', parts), ')')))([name])
|
aser_call = lambda name, args, env: (lambda parts: _sx_begin(reduce(lambda state, arg: (lambda skip: (assoc(state, 'skip', False, 'i', (get(state, 'i') + 1)) if sx_truthy(skip) else ((lambda val: _sx_begin((_sx_begin(_sx_append(parts, sx_str(':', keyword_name(arg))), _sx_append(parts, serialize(val))) if sx_truthy((not sx_truthy(is_nil(val)))) else NIL), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(aser(nth(args, (get(state, 'i') + 1)), env)) if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((get(state, 'i') + 1) < len(args)))) else (lambda val: _sx_begin((_sx_append(parts, serialize(val)) if sx_truthy((not sx_truthy(is_nil(val)))) else NIL), assoc(state, 'i', (get(state, 'i') + 1))))(aser(arg, env)))))(get(state, 'skip')), {'i': 0, 'skip': False}, args), sx_str('(', join(' ', parts), ')')))([name])
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Delimited continuations (shift/reset)
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
_RESET_RESUME = [] # stack of resume values; empty = not resuming
|
||||||
|
|
||||||
|
def sf_reset(args, env):
|
||||||
|
"""(reset body) -- establish a continuation delimiter."""
|
||||||
|
body = first(args)
|
||||||
|
try:
|
||||||
|
return trampoline(eval_expr(body, env))
|
||||||
|
except _ShiftSignal as sig:
|
||||||
|
def cont_fn(value=NIL):
|
||||||
|
_RESET_RESUME.append(value)
|
||||||
|
try:
|
||||||
|
return trampoline(eval_expr(body, env))
|
||||||
|
finally:
|
||||||
|
_RESET_RESUME.pop()
|
||||||
|
k = Continuation(cont_fn)
|
||||||
|
sig_env = dict(sig.env)
|
||||||
|
sig_env[sig.k_name] = k
|
||||||
|
return trampoline(eval_expr(sig.body, sig_env))
|
||||||
|
|
||||||
|
def sf_shift(args, env):
|
||||||
|
"""(shift k body) -- capture continuation to nearest reset."""
|
||||||
|
if _RESET_RESUME:
|
||||||
|
return _RESET_RESUME[-1]
|
||||||
|
k_name = symbol_name(first(args))
|
||||||
|
body = nth(args, 1)
|
||||||
|
raise _ShiftSignal(k_name, body, env)
|
||||||
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Fixups -- wire up render adapter dispatch
|
# Fixups -- wire up render adapter dispatch
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|||||||
201
shared/sx/tests/test_continuations.py
Normal file
201
shared/sx/tests/test_continuations.py
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
"""Tests for delimited continuations (shift/reset).
|
||||||
|
|
||||||
|
Tests run against both the hand-written evaluator and the transpiled
|
||||||
|
sx_ref evaluator to verify both implementations match.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from shared.sx import parse, evaluate, EvalError, NIL
|
||||||
|
from shared.sx.types import Continuation
|
||||||
|
from shared.sx.ref import sx_ref
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def ev(text, env=None):
|
||||||
|
"""Parse and evaluate via hand-written evaluator."""
|
||||||
|
return evaluate(parse(text), env)
|
||||||
|
|
||||||
|
|
||||||
|
def ev_ref(text, env=None):
|
||||||
|
"""Parse and evaluate via transpiled sx_ref."""
|
||||||
|
return sx_ref.evaluate(parse(text), env)
|
||||||
|
|
||||||
|
|
||||||
|
EVALUATORS = [
|
||||||
|
pytest.param(ev, id="hand-written"),
|
||||||
|
pytest.param(ev_ref, id="sx_ref"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Basic shift/reset
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestBasicReset:
|
||||||
|
"""Reset without shift is a no-op wrapper."""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_reset_passthrough(self, evaluate):
|
||||||
|
assert evaluate("(reset 42)") == 42
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_reset_expression(self, evaluate):
|
||||||
|
assert evaluate("(reset (+ 1 2))") == 3
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_reset_with_let(self, evaluate):
|
||||||
|
assert evaluate("(reset (let (x 10) (+ x 5)))") == 15
|
||||||
|
|
||||||
|
|
||||||
|
class TestShiftAbort:
|
||||||
|
"""Shift without invoking k aborts to the reset boundary."""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_abort_returns_shift_body(self, evaluate):
|
||||||
|
# (reset (+ 1 (shift k 42))) → shift body 42 is returned, + 1 is abandoned
|
||||||
|
assert evaluate("(reset (+ 1 (shift k 42)))") == 42
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_abort_string(self, evaluate):
|
||||||
|
assert evaluate('(reset (+ 1 (shift k "aborted")))') == "aborted"
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_abort_with_computation(self, evaluate):
|
||||||
|
assert evaluate("(reset (+ 1 (shift k (* 6 7))))") == 42
|
||||||
|
|
||||||
|
|
||||||
|
class TestContinuationInvoke:
|
||||||
|
"""Invoking the captured continuation re-enters the reset body."""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_invoke_once(self, evaluate):
|
||||||
|
# (reset (+ 1 (shift k (k 10)))) → k resumes with 10, so + 1 10 = 11
|
||||||
|
assert evaluate("(reset (+ 1 (shift k (k 10))))") == 11
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_invoke_with_zero(self, evaluate):
|
||||||
|
assert evaluate("(reset (+ 1 (shift k (k 0))))") == 1
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_invoke_twice(self, evaluate):
|
||||||
|
# k invoked twice: (+ (k 1) (k 10)) → (+ 1 1) + ... → (+ 2 11) = 13
|
||||||
|
# Actually: (k 1) re-evaluates (+ 1 <shift>) where shift returns 1 → 2
|
||||||
|
# Then (k 10) re-evaluates (+ 1 <shift>) where shift returns 10 → 11
|
||||||
|
# Then (+ 2 11) = 13
|
||||||
|
assert evaluate("(reset (+ 1 (shift k (+ (k 1) (k 10)))))") == 13
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_invoke_transforms_value(self, evaluate):
|
||||||
|
# k wraps: (reset (* 2 (shift k (k (k 3)))))
|
||||||
|
# k(3) → (* 2 3) = 6, k(6) → (* 2 6) = 12
|
||||||
|
assert evaluate("(reset (* 2 (shift k (k (k 3)))))") == 12
|
||||||
|
|
||||||
|
|
||||||
|
class TestContinuationPredicate:
|
||||||
|
"""The continuation? predicate identifies captured continuations."""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_continuation_is_true(self, evaluate):
|
||||||
|
result = evaluate("(reset (shift k (continuation? k)))")
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_non_continuation(self, evaluate):
|
||||||
|
assert evaluate("(continuation? 42)") is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_nil_not_continuation(self, evaluate):
|
||||||
|
assert evaluate("(continuation? nil)") is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_lambda_not_continuation(self, evaluate):
|
||||||
|
assert evaluate("(continuation? (fn (x) x))") is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestStoredContinuation:
|
||||||
|
"""Continuations can be stored and invoked later."""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_stored_in_variable(self, evaluate):
|
||||||
|
code = """
|
||||||
|
(let (saved nil)
|
||||||
|
(reset (+ 1 (shift k (do (set! saved k) "captured"))))
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
# The reset returns "captured" (abort path)
|
||||||
|
assert evaluate(code) == "captured"
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_continuation_type(self, evaluate):
|
||||||
|
"""Verify that a captured continuation is identified by continuation?."""
|
||||||
|
code = '(reset (shift k (continuation? k)))'
|
||||||
|
result = evaluate(code)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestNestedReset:
|
||||||
|
"""Nested reset blocks delimit independently."""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_inner_reset(self, evaluate):
|
||||||
|
code = "(reset (+ 1 (reset (+ 2 (shift k (k 10))))))"
|
||||||
|
# Inner reset: (+ 2 (shift k (k 10))) → k(10) → (+ 2 10) = 12
|
||||||
|
# Outer reset: (+ 1 12) = 13
|
||||||
|
assert evaluate(code) == 13
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_inner_abort_outer_continues(self, evaluate):
|
||||||
|
code = "(reset (+ 1 (reset (shift k 99))))"
|
||||||
|
# Inner reset aborts with 99
|
||||||
|
# Outer reset: (+ 1 99) = 100
|
||||||
|
assert evaluate(code) == 100
|
||||||
|
|
||||||
|
|
||||||
|
class TestPracticalPatterns:
|
||||||
|
"""Practical uses of delimited continuations."""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_early_return(self, evaluate):
|
||||||
|
"""Shift without invoking k acts as early return."""
|
||||||
|
code = """
|
||||||
|
(reset
|
||||||
|
(let (x 5)
|
||||||
|
(if (> x 3)
|
||||||
|
(shift k "too big")
|
||||||
|
(* x x))))
|
||||||
|
"""
|
||||||
|
assert evaluate(code) == "too big"
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_normal_path(self, evaluate):
|
||||||
|
"""When condition doesn't trigger shift, normal result."""
|
||||||
|
code = """
|
||||||
|
(reset
|
||||||
|
(let (x 2)
|
||||||
|
(if (> x 3)
|
||||||
|
(shift k "too big")
|
||||||
|
(* x x))))
|
||||||
|
"""
|
||||||
|
assert evaluate(code) == 4
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_continuation_as_function(self, evaluate):
|
||||||
|
"""Map over a continuation to apply it to multiple values."""
|
||||||
|
code = """
|
||||||
|
(reset
|
||||||
|
(+ 10 (shift k
|
||||||
|
(map k (list 1 2 3)))))
|
||||||
|
"""
|
||||||
|
result = evaluate(code)
|
||||||
|
assert result == [11, 12, 13]
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("evaluate", EVALUATORS)
|
||||||
|
def test_default_value(self, evaluate):
|
||||||
|
"""Calling k with no args passes NIL."""
|
||||||
|
code = '(reset (shift k (nil? (k))))'
|
||||||
|
# k() passes NIL, reset body re-evals: (shift k ...) returns NIL
|
||||||
|
# Then the outer shift body checks: (nil? NIL) = true
|
||||||
|
assert evaluate(code) is True
|
||||||
@@ -302,9 +302,45 @@ class StyleValue:
|
|||||||
return self.class_name
|
return self.class_name
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Continuation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class Continuation:
|
||||||
|
"""A captured delimited continuation (shift/reset).
|
||||||
|
|
||||||
|
Callable with one argument — provides the value that the shift
|
||||||
|
expression "returns" within the delimited context.
|
||||||
|
"""
|
||||||
|
__slots__ = ("fn",)
|
||||||
|
|
||||||
|
def __init__(self, fn):
|
||||||
|
self.fn = fn
|
||||||
|
|
||||||
|
def __call__(self, value=NIL):
|
||||||
|
return self.fn(value)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<continuation>"
|
||||||
|
|
||||||
|
|
||||||
|
class _ShiftSignal(BaseException):
|
||||||
|
"""Raised by shift to unwind to the nearest reset.
|
||||||
|
|
||||||
|
Inherits from BaseException (not Exception) to avoid being caught
|
||||||
|
by generic except clauses in user code.
|
||||||
|
"""
|
||||||
|
__slots__ = ("k_name", "body", "env")
|
||||||
|
|
||||||
|
def __init__(self, k_name, body, env):
|
||||||
|
self.k_name = k_name
|
||||||
|
self.body = body
|
||||||
|
self.env = env
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Type alias
|
# Type alias
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
# An s-expression value after evaluation
|
# An s-expression value after evaluation
|
||||||
SExp = int | float | str | bool | Symbol | Keyword | Lambda | Macro | Component | HandlerDef | RelationDef | PageDef | QueryDef | ActionDef | StyleValue | list | dict | _Nil | None
|
SExp = int | float | str | bool | Symbol | Keyword | Lambda | Macro | Component | Continuation | HandlerDef | RelationDef | PageDef | QueryDef | ActionDef | StyleValue | list | dict | _Nil | None
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
(~doc-section :title "What sx is not" :id "not"
|
(~doc-section :title "What sx is not" :id "not"
|
||||||
(ul :class "space-y-2 text-stone-600"
|
(ul :class "space-y-2 text-stone-600"
|
||||||
(li "Not a general-purpose programming language — it's a UI rendering language")
|
(li "Not a general-purpose programming language — it's a UI rendering language")
|
||||||
(li "Not a full Lisp — it has macros and TCO, but no continuations or call/cc")
|
(li "Not a full Lisp — it has macros, TCO, and delimited continuations, but no full call/cc")
|
||||||
(li "Not production-hardened at scale — it runs one website")))))
|
(li "Not production-hardened at scale — it runs one website")))))
|
||||||
|
|
||||||
(defcomp ~docs-getting-started-content ()
|
(defcomp ~docs-getting-started-content ()
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user