spec: dynamic-wind — after-thunk fires on normal return, raise, and call/cc escape

- Add make-wind-after-frame / make-wind-return-frame CEK frame types
- Add *winders* global stack tracking active after-thunks
- Add kont-unwind-to-handler (replaces kont-find-handler in raise-eval) — calls
  after-thunks for wind frames encountered while unwinding to handler
- Add wind-escape-to — pops and calls after-thunks down to captured winders-len
- Replace sf-dynamic-wind with step-sf-dynamic-wind (full CEK dispatch)
- Fix "callcc" frame: store winders-len in continuation object
- Fix callcc-continuation? case: call wind-escape-to before escape
- JS platform: extend SxCallccContinuation to store windersLen; add
  callcc-continuation-winders-len accessor
- 8 tests: normal return, raise escape, call/cc escape, nested LIFO, guard ordering
- 1948/2500 (was 1940); zero regressions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 14:19:17 +00:00
parent 0577f245e2
commit a9d5a1082f
4 changed files with 270 additions and 28 deletions

View File

@@ -31,7 +31,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-04-26T12:42:00Z";
var SX_VERSION = "2026-04-26T14:13:13Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -103,11 +103,12 @@
function parameterUid(p) { return p._uid; }
function parameterDefault(p) { return p._default; }
function SxCallccContinuation(capturedKont) { this._captured = capturedKont; }
function SxCallccContinuation(capturedKont, windersLen) { this._captured = capturedKont; this._winders_len = windersLen !== undefined ? windersLen : 0; }
SxCallccContinuation.prototype._callcc = true;
function makeCallccContinuation(kont) { return new SxCallccContinuation(kont); }
function makeCallccContinuation(kont, windersLen) { return new SxCallccContinuation(kont, windersLen !== undefined ? windersLen : 0); }
function callccContinuation_p(x) { return x != null && x._callcc === true; }
function callccContinuationData(x) { return x._captured; }
function callccContinuationWindersLen(x) { return x._winders_len !== undefined ? x._winders_len : 0; }
function evalError_p(v) {
return v != null && typeof v === "object" && v["__eval_error__"] === true;
@@ -1257,6 +1258,14 @@ PRIMITIVES["make-reactive-reset-frame"] = makeReactiveResetFrame;
var makeCallccFrame = function(env) { return {"env": env, "type": "callcc"}; };
PRIMITIVES["make-callcc-frame"] = makeCallccFrame;
// make-wind-after-frame
var makeWindAfterFrame = function(afterThunk, windersLen, env) { return {"winders-len": windersLen, "env": env, "after-thunk": afterThunk, "type": "wind-after"}; };
PRIMITIVES["make-wind-after-frame"] = makeWindAfterFrame;
// make-wind-return-frame
var makeWindReturnFrame = function(bodyResult, env) { return {"body-result": bodyResult, "env": env, "type": "wind-return"}; };
PRIMITIVES["make-wind-return-frame"] = makeWindReturnFrame;
// make-deref-frame
var makeDerefFrame = function(env) { return {"env": env, "type": "deref"}; };
PRIMITIVES["make-deref-frame"] = makeDerefFrame;
@@ -1333,6 +1342,26 @@ PRIMITIVES["find-matching-handler"] = findMatchingHandler;
})()); };
PRIMITIVES["kont-find-handler"] = kontFindHandler;
// kont-unwind-to-handler
var kontUnwindToHandler = function(kont, condition) { return (isSxTruthy(isEmpty(kont)) ? {"handler": NIL, "kont": kont} : (function() {
var frame = first(kont);
var restK = rest(kont);
return (isSxTruthy(sxEq(frameType(frame), "handler")) ? (function() {
var match = findMatchingHandler(get(frame, "f"), condition);
return (isSxTruthy(isNil(match)) ? kontUnwindToHandler(restK, condition) : {"handler": match, "kont": kont});
})() : (isSxTruthy(sxEq(frameType(frame), "wind-after")) ? ((isSxTruthy((len(_winders_) > get(frame, "winders-len"))) ? (_winders_ = rest(_winders_)) : NIL), cekCall(get(frame, "after-thunk"), []), kontUnwindToHandler(restK, condition)) : kontUnwindToHandler(restK, condition)));
})()); };
PRIMITIVES["kont-unwind-to-handler"] = kontUnwindToHandler;
// wind-escape-to
var windEscapeTo = function(targetLen) { return (isSxTruthy((len(_winders_) > targetLen)) ? (function() {
var afterThunk = first(_winders_);
_winders_ = rest(_winders_);
cekCall(afterThunk, []);
return windEscapeTo(targetLen);
})() : NIL); };
PRIMITIVES["wind-escape-to"] = windEscapeTo;
// find-named-restart
var findNamedRestart = function(restarts, name) { return (isSxTruthy(isEmpty(restarts)) ? NIL : (function() {
var entry = first(restarts);
@@ -1445,6 +1474,10 @@ PRIMITIVES["*provide-batch-queue*"] = _provideBatchQueue_;
var _provideSubscribers_ = {};
PRIMITIVES["*provide-subscribers*"] = _provideSubscribers_;
// *winders*
var _winders_ = [];
PRIMITIVES["*winders*"] = _winders_;
// *library-registry*
var _libraryRegistry_ = {};
PRIMITIVES["*library-registry*"] = _libraryRegistry_;
@@ -1950,14 +1983,18 @@ PRIMITIVES["sf-letrec"] = sfLetrec;
})(); };
PRIMITIVES["step-sf-letrec"] = stepSfLetrec;
// sf-dynamic-wind
var sfDynamicWind = function(args, env) { return (function() {
// step-sf-dynamic-wind
var stepSfDynamicWind = function(args, env, kont) { return (function() {
var before = trampoline(evalExpr(first(args), env));
var body = trampoline(evalExpr(nth(args, 1), env));
var after = trampoline(evalExpr(nth(args, 2), env));
return dynamicWindCall(before, body, after, env);
return (cekCall(before, []), (function() {
var windersLen = len(_winders_);
_winders_ = cons(after, _winders_);
return continueWithCall(body, [], env, [], kontPush(makeWindAfterFrame(after, windersLen, env), kont));
})());
})(); };
PRIMITIVES["sf-dynamic-wind"] = sfDynamicWind;
PRIMITIVES["step-sf-dynamic-wind"] = stepSfDynamicWind;
// sf-scope
var sfScope = function(args, env) { return (function() {
@@ -2099,7 +2136,7 @@ PRIMITIVES["step-sf-let-match"] = stepSfLetMatch;
var test = first(testClause);
var result = rest(testClause);
return stepEvalList(cons(new Symbol("let"), cons(new Symbol("__do-loop"), cons(map(function(b) { return [first(b), nth(b, 1)]; }, bindings), [cons(new Symbol("if"), cons(test, cons((isSxTruthy(isEmpty(result)) ? NIL : cons(new Symbol("begin"), result)), [cons(new Symbol("begin"), append(body, [cons(new Symbol("__do-loop"), steps)]))])))]))), env, kont);
})() : stepSfBegin(args, env, kont)); if (_m == "guard") return stepSfGuard(args, env, kont); if (_m == "quote") return makeCekValue((isSxTruthy(isEmpty(args)) ? NIL : first(args)), env, kont); if (_m == "quasiquote") return makeCekValue(qqExpand(first(args), env), env, kont); if (_m == "->") return stepSfThreadFirst(args, env, kont); if (_m == "->>") return stepSfThreadLast(args, env, kont); if (_m == "|>") return stepSfThreadLast(args, env, kont); if (_m == "as->") return stepSfThreadAs(args, env, kont); if (_m == "set!") return stepSfSet(args, env, kont); if (_m == "letrec") return stepSfLetrec(args, env, kont); if (_m == "reset") return stepSfReset(args, env, kont); if (_m == "shift") return stepSfShift(args, env, kont); if (_m == "deref") return stepSfDeref(args, env, kont); if (_m == "scope") return stepSfScope(args, env, kont); if (_m == "provide") return stepSfProvide(args, env, kont); if (_m == "peek") return stepSfPeek(args, env, kont); if (_m == "provide!") return stepSfProvide_b(args, env, kont); if (_m == "context") return stepSfContext(args, env, kont); if (_m == "bind") return stepSfBind(args, env, kont); if (_m == "emit!") return stepSfEmit(args, env, kont); if (_m == "emitted") return stepSfEmitted(args, env, kont); if (_m == "handler-bind") return stepSfHandlerBind(args, env, kont); if (_m == "restart-case") return stepSfRestartCase(args, env, kont); if (_m == "signal-condition") return stepSfSignal(args, env, kont); if (_m == "invoke-restart") return stepSfInvokeRestart(args, env, kont); if (_m == "match") return stepSfMatch(args, env, kont); if (_m == "let-match") return stepSfLetMatch(args, env, kont); if (_m == "dynamic-wind") return makeCekValue(sfDynamicWind(args, env), env, kont); if (_m == "map") return stepHoMap(args, env, kont); if (_m == "map-indexed") return stepHoMapIndexed(args, env, kont); if (_m == "filter") return stepHoFilter(args, env, kont); if (_m == "reduce") return stepHoReduce(args, env, kont); if (_m == "some") return stepHoSome(args, env, kont); if (_m == "every?") return stepHoEvery(args, env, kont); if (_m == "for-each") return stepHoForEach(args, env, kont); if (_m == "raise") return stepSfRaise(args, env, kont); if (_m == "raise-continuable") return makeCekState(first(args), env, kontPush(makeRaiseEvalFrame(env, true), kont)); if (_m == "call/cc") return stepSfCallcc(args, env, kont); if (_m == "call-with-current-continuation") return stepSfCallcc(args, env, kont); if (_m == "perform") return stepSfPerform(args, env, kont); if (_m == "define-library") return stepSfDefineLibrary(args, env, kont); if (_m == "import") return stepSfImport(args, env, kont); if (_m == "define-record-type") return makeCekValue(sfDefineRecordType(args, env), env, kont); if (_m == "define-protocol") return makeCekValue(sfDefineProtocol(args, env), env, kont); if (_m == "implement") return makeCekValue(sfImplement(args, env), env, kont); if (_m == "parameterize") return stepSfParameterize(args, env, kont); if (_m == "syntax-rules") return makeCekValue(sfSyntaxRules(args, env), env, kont); if (_m == "define-syntax") return stepSfDefine(args, env, kont); return (isSxTruthy((isSxTruthy(dictHas(_customSpecialForms, name)) && !isSxTruthy(envHas(env, name)))) ? makeCekValue((get(_customSpecialForms, name))(args, env), env, kont) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? (function() {
})() : stepSfBegin(args, env, kont)); if (_m == "guard") return stepSfGuard(args, env, kont); if (_m == "quote") return makeCekValue((isSxTruthy(isEmpty(args)) ? NIL : first(args)), env, kont); if (_m == "quasiquote") return makeCekValue(qqExpand(first(args), env), env, kont); if (_m == "->") return stepSfThreadFirst(args, env, kont); if (_m == "->>") return stepSfThreadLast(args, env, kont); if (_m == "|>") return stepSfThreadLast(args, env, kont); if (_m == "as->") return stepSfThreadAs(args, env, kont); if (_m == "set!") return stepSfSet(args, env, kont); if (_m == "letrec") return stepSfLetrec(args, env, kont); if (_m == "reset") return stepSfReset(args, env, kont); if (_m == "shift") return stepSfShift(args, env, kont); if (_m == "deref") return stepSfDeref(args, env, kont); if (_m == "scope") return stepSfScope(args, env, kont); if (_m == "provide") return stepSfProvide(args, env, kont); if (_m == "peek") return stepSfPeek(args, env, kont); if (_m == "provide!") return stepSfProvide_b(args, env, kont); if (_m == "context") return stepSfContext(args, env, kont); if (_m == "bind") return stepSfBind(args, env, kont); if (_m == "emit!") return stepSfEmit(args, env, kont); if (_m == "emitted") return stepSfEmitted(args, env, kont); if (_m == "handler-bind") return stepSfHandlerBind(args, env, kont); if (_m == "restart-case") return stepSfRestartCase(args, env, kont); if (_m == "signal-condition") return stepSfSignal(args, env, kont); if (_m == "invoke-restart") return stepSfInvokeRestart(args, env, kont); if (_m == "match") return stepSfMatch(args, env, kont); if (_m == "let-match") return stepSfLetMatch(args, env, kont); if (_m == "dynamic-wind") return stepSfDynamicWind(args, env, kont); if (_m == "map") return stepHoMap(args, env, kont); if (_m == "map-indexed") return stepHoMapIndexed(args, env, kont); if (_m == "filter") return stepHoFilter(args, env, kont); if (_m == "reduce") return stepHoReduce(args, env, kont); if (_m == "some") return stepHoSome(args, env, kont); if (_m == "every?") return stepHoEvery(args, env, kont); if (_m == "for-each") return stepHoForEach(args, env, kont); if (_m == "raise") return stepSfRaise(args, env, kont); if (_m == "raise-continuable") return makeCekState(first(args), env, kontPush(makeRaiseEvalFrame(env, true), kont)); if (_m == "call/cc") return stepSfCallcc(args, env, kont); if (_m == "call-with-current-continuation") return stepSfCallcc(args, env, kont); if (_m == "perform") return stepSfPerform(args, env, kont); if (_m == "define-library") return stepSfDefineLibrary(args, env, kont); if (_m == "import") return stepSfImport(args, env, kont); if (_m == "define-record-type") return makeCekValue(sfDefineRecordType(args, env), env, kont); if (_m == "define-protocol") return makeCekValue(sfDefineProtocol(args, env), env, kont); if (_m == "implement") return makeCekValue(sfImplement(args, env), env, kont); if (_m == "parameterize") return stepSfParameterize(args, env, kont); if (_m == "syntax-rules") return makeCekValue(sfSyntaxRules(args, env), env, kont); if (_m == "define-syntax") return stepSfDefine(args, env, kont); return (isSxTruthy((isSxTruthy(dictHas(_customSpecialForms, name)) && !isSxTruthy(envHas(env, name)))) ? makeCekValue((get(_customSpecialForms, name))(args, env), env, kont) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? (function() {
var mac = envGet(env, name);
return makeCekState(expandMacro(mac, args, env), env, kont);
})() : (isSxTruthy((isSxTruthy(_renderCheck) && isSxTruthy(!isSxTruthy(envHas(env, name))) && _renderCheck(expr, env))) ? makeCekValue(_renderFn(expr, env), env, kont) : stepEvalCall(head, args, env, kont)))); })();
@@ -3142,12 +3179,20 @@ PRIMITIVES["step-ho-for-each"] = stepHoForEach;
var testValue = get(frame, "match-val");
var fenv = get(frame, "env");
return continueWithCall(value, [testValue], fenv, [testValue], restK);
})(); if (_m == "raise-eval") return (function() {
})(); if (_m == "wind-after") return (function() {
var afterThunk = get(frame, "after-thunk");
var windersLen = get(frame, "winders-len");
var bodyResult = value;
var fenv = get(frame, "env");
return ((isSxTruthy((len(_winders_) > windersLen)) ? (_winders_ = rest(_winders_)) : NIL), continueWithCall(afterThunk, [], fenv, [], kontPush(makeWindReturnFrame(bodyResult, fenv), restK)));
})(); if (_m == "wind-return") return makeCekValue(get(frame, "body-result"), get(frame, "env"), restK); if (_m == "raise-eval") return (function() {
var condition = value;
var fenv = get(frame, "env");
var continuable_p = get(frame, "scheme");
var handlerFn = kontFindHandler(restK, condition);
return (isSxTruthy(isNil(handlerFn)) ? ((_lastErrorKont_ = restK), hostError((String("Unhandled exception: ") + String(inspect(condition))))) : continueWithCall(handlerFn, [condition], fenv, [condition], (isSxTruthy(continuable_p) ? kontPush(makeSignalReturnFrame(fenv, restK), restK) : kontPush(makeRaiseGuardFrame(fenv, restK), restK))));
var unwindResult = kontUnwindToHandler(restK, condition);
var handlerFn = get(unwindResult, "handler");
var unwoundK = get(unwindResult, "kont");
return (isSxTruthy(isNil(handlerFn)) ? ((_lastErrorKont_ = unwoundK), hostError((String("Unhandled exception: ") + String(inspect(condition))))) : continueWithCall(handlerFn, [condition], fenv, [condition], (isSxTruthy(continuable_p) ? kontPush(makeSignalReturnFrame(fenv, unwoundK), unwoundK) : kontPush(makeRaiseGuardFrame(fenv, unwoundK), unwoundK))));
})(); if (_m == "raise-guard") return ((_lastErrorKont_ = restK), hostError("exception handler returned from non-continuable raise")); if (_m == "multi-map") return (function() {
var f = get(frame, "f");
var remaining = get(frame, "remaining");
@@ -3159,7 +3204,7 @@ PRIMITIVES["step-ho-for-each"] = stepHoForEach;
return continueWithCall(f, heads, fenv, [], kontPush(makeMultiMapFrame(f, tails, newResults, fenv), restK));
})());
})(); if (_m == "callcc") return (function() {
var k = makeCallccContinuation(restK);
var k = makeCallccContinuation(restK, len(_winders_));
return continueWithCall(value, [k], get(frame, "env"), [k], restK);
})(); if (_m == "vm-resume") return (function() {
var resumeFn = get(frame, "f");
@@ -3205,7 +3250,8 @@ PRIMITIVES["step-continue"] = stepContinue;
})() : (isSxTruthy(callccContinuation_p(f)) ? (function() {
var arg = (isSxTruthy(isEmpty(args)) ? NIL : first(args));
var captured = callccContinuationData(f);
return makeCekValue(arg, env, captured);
var wLen = callccContinuationWindersLen(f);
return (windEscapeTo(wLen), makeCekValue(arg, env, captured));
})() : (isSxTruthy(continuation_p(f)) ? (function() {
var arg = (isSxTruthy(isEmpty(args)) ? NIL : first(args));
var contData = continuationData(f);