Frame-based dynamic scope: 870/870 — all tests passing
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 13m34s

provide/context and scope/emit!/emitted now use CEK continuation
frames instead of an imperative global stack. Scope state is part
of the continuation — captured by shift, restored by k invocation.

New frame types:
- ProvideFrame: holds name + value, consumed when body completes
- ScopeAccFrame: holds name + mutable emitted list

New CEK special forms:
- context: walks kont for nearest ProvideFrame, returns value
- emit!: walks kont for nearest ScopeAccFrame, appends to emitted
- emitted: walks kont for nearest ScopeAccFrame, returns list

Kont walkers: kont-find-provide, kont-find-scope-acc

This fixes the last 2 test failures:
- provide survives resume: scope captured by shift, restored by k
- scope and emit across shift: accumulator preserved in continuation

JS Full: 870/870 (100%)
JS Standard: 747/747 (100%)
Python: 679/679 (100%)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 14:40:14 +00:00
parent 719da7914e
commit 9f32c8cf0d
3 changed files with 293 additions and 12 deletions

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-15T14:20:13Z";
var SX_VERSION = "2026-03-15T14:39:41Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -913,6 +913,14 @@ PRIMITIVES["make-every-frame"] = makeEveryFrame;
var makeScopeFrame = function(name, remaining, env) { return {"type": "scope", "name": name, "remaining": remaining, "env": env}; };
PRIMITIVES["make-scope-frame"] = makeScopeFrame;
// make-provide-frame
var makeProvideFrame = function(name, value, remaining, env) { return {"type": "provide", "name": name, "value": value, "remaining": remaining, "env": env}; };
PRIMITIVES["make-provide-frame"] = makeProvideFrame;
// make-scope-acc-frame
var makeScopeAccFrame = function(name, value, remaining, env) { return {"type": "scope-acc", "name": name, "value": sxOr(value, NIL), "emitted": [], "remaining": remaining, "env": env}; };
PRIMITIVES["make-scope-acc-frame"] = makeScopeAccFrame;
// make-reset-frame
var makeResetFrame = function(env) { return {"type": "reset", "env": env}; };
PRIMITIVES["make-reset-frame"] = makeResetFrame;
@@ -974,6 +982,20 @@ PRIMITIVES["scan"] = scan;
return scan(kont, []); };
PRIMITIVES["kont-capture-to-reset"] = kontCaptureToReset;
// kont-find-provide
var kontFindProvide = function(kont, name) { return (isSxTruthy(isEmpty(kont)) ? NIL : (function() {
var frame = first(kont);
return (isSxTruthy((isSxTruthy((frameType(frame) == "provide")) && (get(frame, "name") == name))) ? frame : kontFindProvide(rest(kont), name));
})()); };
PRIMITIVES["kont-find-provide"] = kontFindProvide;
// kont-find-scope-acc
var kontFindScopeAcc = function(kont, name) { return (isSxTruthy(isEmpty(kont)) ? NIL : (function() {
var frame = first(kont);
return (isSxTruthy((isSxTruthy((frameType(frame) == "scope-acc")) && (get(frame, "name") == name))) ? frame : kontFindScopeAcc(rest(kont), name));
})()); };
PRIMITIVES["kont-find-scope-acc"] = kontFindScopeAcc;
// has-reactive-reset-frame?
var hasReactiveResetFrame_p = function(kont) { return (isSxTruthy(isEmpty(kont)) ? false : (isSxTruthy((frameType(first(kont)) == "reactive-reset")) ? true : hasReactiveResetFrame_p(rest(kont)))); };
PRIMITIVES["has-reactive-reset-frame?"] = hasReactiveResetFrame_p;
@@ -1403,10 +1425,10 @@ PRIMITIVES["step-eval"] = stepEval;
var args = rest(expr);
return (isSxTruthy(!isSxTruthy(sxOr((typeOf(head) == "symbol"), (typeOf(head) == "lambda"), (typeOf(head) == "list")))) ? (isSxTruthy(isEmpty(expr)) ? makeCekValue([], env, kont) : makeCekState(first(expr), env, kontPush(makeMapFrame(NIL, rest(expr), [], env), kont))) : (isSxTruthy((typeOf(head) == "symbol")) ? (function() {
var name = symbolName(head);
return (isSxTruthy((name == "if")) ? stepSfIf(args, env, kont) : (isSxTruthy((name == "when")) ? stepSfWhen(args, env, kont) : (isSxTruthy((name == "cond")) ? stepSfCond(args, env, kont) : (isSxTruthy((name == "case")) ? stepSfCase(args, env, kont) : (isSxTruthy((name == "and")) ? stepSfAnd(args, env, kont) : (isSxTruthy((name == "or")) ? stepSfOr(args, env, kont) : (isSxTruthy((name == "let")) ? stepSfLet(args, env, kont) : (isSxTruthy((name == "let*")) ? stepSfLet(args, env, kont) : (isSxTruthy((name == "lambda")) ? stepSfLambda(args, env, kont) : (isSxTruthy((name == "fn")) ? stepSfLambda(args, env, kont) : (isSxTruthy((name == "define")) ? stepSfDefine(args, env, kont) : (isSxTruthy((name == "defcomp")) ? makeCekValue(sfDefcomp(args, env), env, kont) : (isSxTruthy((name == "defisland")) ? makeCekValue(sfDefisland(args, env), env, kont) : (isSxTruthy((name == "defmacro")) ? makeCekValue(sfDefmacro(args, env), env, kont) : (isSxTruthy((name == "defstyle")) ? makeCekValue(sfDefstyle(args, env), env, kont) : (isSxTruthy((name == "defhandler")) ? makeCekValue(sfDefhandler(args, env), env, kont) : (isSxTruthy((name == "defpage")) ? makeCekValue(sfDefpage(args, env), env, kont) : (isSxTruthy((name == "defquery")) ? makeCekValue(sfDefquery(args, env), env, kont) : (isSxTruthy((name == "defaction")) ? makeCekValue(sfDefaction(args, env), env, kont) : (isSxTruthy((name == "deftype")) ? makeCekValue(sfDeftype(args, env), env, kont) : (isSxTruthy((name == "defeffect")) ? makeCekValue(sfDefeffect(args, env), env, kont) : (isSxTruthy((name == "begin")) ? stepSfBegin(args, env, kont) : (isSxTruthy((name == "do")) ? stepSfBegin(args, env, kont) : (isSxTruthy((name == "quote")) ? makeCekValue((isSxTruthy(isEmpty(args)) ? NIL : first(args)), env, kont) : (isSxTruthy((name == "quasiquote")) ? makeCekValue(qqExpand(first(args), env), env, kont) : (isSxTruthy((name == "->")) ? stepSfThreadFirst(args, env, kont) : (isSxTruthy((name == "set!")) ? stepSfSet(args, env, kont) : (isSxTruthy((name == "letrec")) ? makeCekValue(sfLetrec(args, env), env, kont) : (isSxTruthy((name == "reset")) ? stepSfReset(args, env, kont) : (isSxTruthy((name == "shift")) ? stepSfShift(args, env, kont) : (isSxTruthy((name == "deref")) ? stepSfDeref(args, env, kont) : (isSxTruthy((name == "scope")) ? stepSfScope(args, env, kont) : (isSxTruthy((name == "provide")) ? stepSfProvide(args, env, kont) : (isSxTruthy((name == "dynamic-wind")) ? makeCekValue(sfDynamicWind(args, env), env, kont) : (isSxTruthy((name == "map")) ? stepHoMap(args, env, kont) : (isSxTruthy((name == "map-indexed")) ? stepHoMapIndexed(args, env, kont) : (isSxTruthy((name == "filter")) ? stepHoFilter(args, env, kont) : (isSxTruthy((name == "reduce")) ? stepHoReduce(args, env, kont) : (isSxTruthy((name == "some")) ? stepHoSome(args, env, kont) : (isSxTruthy((name == "every?")) ? stepHoEvery(args, env, kont) : (isSxTruthy((name == "for-each")) ? stepHoForEach(args, env, kont) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? (function() {
return (isSxTruthy((name == "if")) ? stepSfIf(args, env, kont) : (isSxTruthy((name == "when")) ? stepSfWhen(args, env, kont) : (isSxTruthy((name == "cond")) ? stepSfCond(args, env, kont) : (isSxTruthy((name == "case")) ? stepSfCase(args, env, kont) : (isSxTruthy((name == "and")) ? stepSfAnd(args, env, kont) : (isSxTruthy((name == "or")) ? stepSfOr(args, env, kont) : (isSxTruthy((name == "let")) ? stepSfLet(args, env, kont) : (isSxTruthy((name == "let*")) ? stepSfLet(args, env, kont) : (isSxTruthy((name == "lambda")) ? stepSfLambda(args, env, kont) : (isSxTruthy((name == "fn")) ? stepSfLambda(args, env, kont) : (isSxTruthy((name == "define")) ? stepSfDefine(args, env, kont) : (isSxTruthy((name == "defcomp")) ? makeCekValue(sfDefcomp(args, env), env, kont) : (isSxTruthy((name == "defisland")) ? makeCekValue(sfDefisland(args, env), env, kont) : (isSxTruthy((name == "defmacro")) ? makeCekValue(sfDefmacro(args, env), env, kont) : (isSxTruthy((name == "defstyle")) ? makeCekValue(sfDefstyle(args, env), env, kont) : (isSxTruthy((name == "defhandler")) ? makeCekValue(sfDefhandler(args, env), env, kont) : (isSxTruthy((name == "defpage")) ? makeCekValue(sfDefpage(args, env), env, kont) : (isSxTruthy((name == "defquery")) ? makeCekValue(sfDefquery(args, env), env, kont) : (isSxTruthy((name == "defaction")) ? makeCekValue(sfDefaction(args, env), env, kont) : (isSxTruthy((name == "deftype")) ? makeCekValue(sfDeftype(args, env), env, kont) : (isSxTruthy((name == "defeffect")) ? makeCekValue(sfDefeffect(args, env), env, kont) : (isSxTruthy((name == "begin")) ? stepSfBegin(args, env, kont) : (isSxTruthy((name == "do")) ? stepSfBegin(args, env, kont) : (isSxTruthy((name == "quote")) ? makeCekValue((isSxTruthy(isEmpty(args)) ? NIL : first(args)), env, kont) : (isSxTruthy((name == "quasiquote")) ? makeCekValue(qqExpand(first(args), env), env, kont) : (isSxTruthy((name == "->")) ? stepSfThreadFirst(args, env, kont) : (isSxTruthy((name == "set!")) ? stepSfSet(args, env, kont) : (isSxTruthy((name == "letrec")) ? makeCekValue(sfLetrec(args, env), env, kont) : (isSxTruthy((name == "reset")) ? stepSfReset(args, env, kont) : (isSxTruthy((name == "shift")) ? stepSfShift(args, env, kont) : (isSxTruthy((name == "deref")) ? stepSfDeref(args, env, kont) : (isSxTruthy((name == "scope")) ? stepSfScope(args, env, kont) : (isSxTruthy((name == "provide")) ? stepSfProvide(args, env, kont) : (isSxTruthy((name == "context")) ? stepSfContext(args, env, kont) : (isSxTruthy((name == "emit!")) ? stepSfEmit(args, env, kont) : (isSxTruthy((name == "emitted")) ? stepSfEmitted(args, env, kont) : (isSxTruthy((name == "dynamic-wind")) ? makeCekValue(sfDynamicWind(args, env), env, kont) : (isSxTruthy((name == "map")) ? stepHoMap(args, env, kont) : (isSxTruthy((name == "map-indexed")) ? stepHoMapIndexed(args, env, kont) : (isSxTruthy((name == "filter")) ? stepHoFilter(args, env, kont) : (isSxTruthy((name == "reduce")) ? stepHoReduce(args, env, kont) : (isSxTruthy((name == "some")) ? stepHoSome(args, env, kont) : (isSxTruthy((name == "every?")) ? stepHoEvery(args, env, kont) : (isSxTruthy((name == "for-each")) ? stepHoForEach(args, 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(renderActiveP()) && isRenderExpr(expr))) ? makeCekValue(renderExpr(expr, env), env, kont) : stepEvalCall(head, args, env, kont))))))))))))))))))))))))))))))))))))))))))));
})() : (isSxTruthy((isSxTruthy(renderActiveP()) && isRenderExpr(expr))) ? makeCekValue(renderExpr(expr, env), env, kont) : stepEvalCall(head, args, env, kont)))))))))))))))))))))))))))))))))))))))))))))));
})() : stepEvalCall(head, args, env, kont)));
})(); };
PRIMITIVES["step-eval-list"] = stepEvalList;
@@ -1492,13 +1514,51 @@ PRIMITIVES["step-sf-thread-first"] = stepSfThreadFirst;
PRIMITIVES["step-sf-lambda"] = stepSfLambda;
// step-sf-scope
var stepSfScope = function(args, env, kont) { return makeCekValue(sfScope(args, env), env, kont); };
var stepSfScope = function(args, env, kont) { return (function() {
var name = trampoline(evalExpr(first(args), env));
var restArgs = slice(args, 1);
var val = NIL;
var body = NIL;
(isSxTruthy((isSxTruthy((len(restArgs) >= 2)) && isSxTruthy((typeOf(first(restArgs)) == "keyword")) && (keywordName(first(restArgs)) == "value"))) ? ((val = trampoline(evalExpr(nth(restArgs, 1), env))), (body = slice(restArgs, 2))) : (body = restArgs));
return (isSxTruthy(isEmpty(body)) ? makeCekValue(NIL, env, kont) : (isSxTruthy((len(body) == 1)) ? makeCekState(first(body), env, kontPush(makeScopeAccFrame(name, val, [], env), kont)) : makeCekState(first(body), env, kontPush(makeScopeAccFrame(name, val, rest(body), env), kont))));
})(); };
PRIMITIVES["step-sf-scope"] = stepSfScope;
// step-sf-provide
var stepSfProvide = function(args, env, kont) { return makeCekValue(sfProvide(args, env), env, kont); };
var stepSfProvide = function(args, env, kont) { return (function() {
var name = trampoline(evalExpr(first(args), env));
var val = trampoline(evalExpr(nth(args, 1), env));
var body = slice(args, 2);
return (isSxTruthy(isEmpty(body)) ? makeCekValue(NIL, env, kont) : (isSxTruthy((len(body) == 1)) ? makeCekState(first(body), env, kontPush(makeProvideFrame(name, val, [], env), kont)) : makeCekState(first(body), env, kontPush(makeProvideFrame(name, val, rest(body), env), kont))));
})(); };
PRIMITIVES["step-sf-provide"] = stepSfProvide;
// step-sf-context
var stepSfContext = function(args, env, kont) { return (function() {
var name = trampoline(evalExpr(first(args), env));
var defaultVal = (isSxTruthy((len(args) >= 2)) ? trampoline(evalExpr(nth(args, 1), env)) : NIL);
var frame = kontFindProvide(kont, name);
return (isSxTruthy(frame) ? makeCekValue(get(frame, "value"), env, kont) : (isSxTruthy((len(args) >= 2)) ? makeCekValue(defaultVal, env, kont) : error((String("No provider for: ") + String(name)))));
})(); };
PRIMITIVES["step-sf-context"] = stepSfContext;
// step-sf-emit
var stepSfEmit = function(args, env, kont) { return (function() {
var name = trampoline(evalExpr(first(args), env));
var val = trampoline(evalExpr(nth(args, 1), env));
var frame = kontFindScopeAcc(kont, name);
return (isSxTruthy(frame) ? (append_b(get(frame, "emitted"), val), makeCekValue(NIL, env, kont)) : error((String("No scope for emit!: ") + String(name))));
})(); };
PRIMITIVES["step-sf-emit"] = stepSfEmit;
// step-sf-emitted
var stepSfEmitted = function(args, env, kont) { return (function() {
var name = trampoline(evalExpr(first(args), env));
var frame = kontFindScopeAcc(kont, name);
return (isSxTruthy(frame) ? makeCekValue(get(frame, "emitted"), env, kont) : error((String("No scope for emitted: ") + String(name))));
})(); };
PRIMITIVES["step-sf-emitted"] = stepSfEmitted;
// step-sf-reset
var stepSfReset = function(args, env, kont) { return makeCekState(first(args), env, kontPush(makeResetFrame(env), kont)); };
PRIMITIVES["step-sf-reset"] = stepSfReset;
@@ -1780,6 +1840,18 @@ PRIMITIVES["step-ho-for-each"] = stepHoForEach;
var remaining = get(frame, "remaining");
var fenv = get(frame, "env");
return (isSxTruthy(isEmpty(remaining)) ? (scopePop(name), makeCekValue(value, fenv, restK)) : makeCekState(first(remaining), fenv, kontPush(makeScopeFrame(name, rest(remaining), fenv), restK)));
})() : (isSxTruthy((ft == "provide")) ? (function() {
var remaining = get(frame, "remaining");
var fenv = get(frame, "env");
return (isSxTruthy(isEmpty(remaining)) ? makeCekValue(value, fenv, restK) : makeCekState(first(remaining), fenv, kontPush(makeProvideFrame(get(frame, "name"), get(frame, "value"), rest(remaining), fenv), restK)));
})() : (isSxTruthy((ft == "scope-acc")) ? (function() {
var remaining = get(frame, "remaining");
var fenv = get(frame, "env");
return (isSxTruthy(isEmpty(remaining)) ? makeCekValue(value, fenv, restK) : makeCekState(first(remaining), fenv, kontPush((function() {
var newFrame = makeScopeAccFrame(get(frame, "name"), get(frame, "value"), rest(remaining), fenv);
newFrame["emitted"] = get(frame, "emitted");
return newFrame;
})(), restK)));
})() : (isSxTruthy((ft == "map")) ? (function() {
var f = get(frame, "f");
var remaining = get(frame, "remaining");
@@ -1824,7 +1896,7 @@ PRIMITIVES["step-ho-for-each"] = stepHoForEach;
var remaining = get(frame, "remaining");
var fenv = get(frame, "env");
return (isSxTruthy(!isSxTruthy(value)) ? makeCekValue(false, fenv, restK) : (isSxTruthy(isEmpty(remaining)) ? makeCekValue(true, fenv, restK) : continueWithCall(f, [first(remaining)], fenv, [], kontPush(makeEveryFrame(f, rest(remaining), fenv), restK))));
})() : error((String("Unknown frame type: ") + String(ft)))))))))))))))))))))))))));
})() : error((String("Unknown frame type: ") + String(ft)))))))))))))))))))))))))))));
})());
})(); };
PRIMITIVES["step-continue"] = stepContinue;

View File

@@ -152,11 +152,22 @@
(fn (f remaining env)
{:type "every" :f f :remaining remaining :env env}))
;; ScopeFrame: scope-pop! when frame pops
;; ScopeFrame: remaining body expressions for scope special form
(define make-scope-frame
(fn (name remaining env)
{:type "scope" :name name :remaining remaining :env env}))
;; ProvideFrame: dynamic variable binding (context reads this from kont)
(define make-provide-frame
(fn (name value remaining env)
{:type "provide" :name name :value value :remaining remaining :env env}))
;; ScopeAccFrame: accumulator scope (emit! appends, emitted reads)
(define make-scope-acc-frame
(fn (name value remaining env)
{:type "scope-acc" :name name :value (or value nil)
:emitted (list) :remaining remaining :env env}))
;; ResetFrame: delimiter for shift/reset continuations
(define make-reset-frame
(fn (env)
@@ -253,6 +264,26 @@
(scan (rest k) (append captured (list frame))))))))
(scan kont (list))))
;; Walk kont for nearest ProvideFrame with matching name
(define kont-find-provide
(fn (kont name)
(if (empty? kont) nil
(let ((frame (first kont)))
(if (and (= (frame-type frame) "provide")
(= (get frame "name") name))
frame
(kont-find-provide (rest kont) name))))))
;; Walk kont for nearest ScopeAccFrame with matching name
(define kont-find-scope-acc
(fn (kont name)
(if (empty? kont) nil
(let ((frame (first kont)))
(if (and (= (frame-type frame) "scope-acc")
(= (get frame "name") name))
frame
(kont-find-scope-acc (rest kont) name))))))
;; Check if a ReactiveResetFrame exists anywhere in the continuation
(define has-reactive-reset-frame?
(fn (kont)
@@ -1254,9 +1285,12 @@
;; Reactive deref-as-shift
(= name "deref") (step-sf-deref args env kont)
;; Scoped effects
;; Scoped effects — frame-based dynamic scope
(= name "scope") (step-sf-scope args env kont)
(= name "provide") (step-sf-provide args env kont)
(= name "context") (step-sf-context args env kont)
(= name "emit!") (step-sf-emit args env kont)
(= name "emitted") (step-sf-emitted args env kont)
;; Dynamic wind
(= name "dynamic-wind") (make-cek-value (sf-dynamic-wind args env) env kont)
@@ -1460,15 +1494,83 @@
(make-cek-value (sf-lambda args env) env kont)))
;; scope: evaluate name, then push ScopeFrame
;; scope: push ScopeAccFrame, evaluate body. emit!/emitted walk kont.
;; (scope name body...) or (scope name :value v body...)
(define step-sf-scope
(fn (args env kont)
;; Delegate to existing sf-scope for now — scope involves mutation
(make-cek-value (sf-scope args env) env kont)))
(let ((name (trampoline (eval-expr (first args) env)))
(rest-args (slice args 1))
(val nil)
(body nil))
;; Check for :value keyword
(if (and (>= (len rest-args) 2)
(= (type-of (first rest-args)) "keyword")
(= (keyword-name (first rest-args)) "value"))
(do (set! val (trampoline (eval-expr (nth rest-args 1) env)))
(set! body (slice rest-args 2)))
(set! body rest-args))
;; Push ScopeAccFrame and start evaluating body
(if (empty? body)
(make-cek-value nil env kont)
(if (= (len body) 1)
(make-cek-state (first body) env
(kont-push (make-scope-acc-frame name val (list) env) kont))
(make-cek-state (first body) env
(kont-push
(make-scope-acc-frame name val (rest body) env)
kont)))))))
;; provide: delegate to existing handler
;; provide: push ProvideFrame, evaluate body. context walks kont to read.
;; (provide name value body...)
(define step-sf-provide
(fn (args env kont)
(make-cek-value (sf-provide args env) env kont)))
(let ((name (trampoline (eval-expr (first args) env)))
(val (trampoline (eval-expr (nth args 1) env)))
(body (slice args 2)))
;; Push ProvideFrame and start evaluating body
(if (empty? body)
(make-cek-value nil env kont)
(if (= (len body) 1)
(make-cek-state (first body) env
(kont-push (make-provide-frame name val (list) env) kont))
(make-cek-state (first body) env
(kont-push
(make-provide-frame name val (rest body) env)
kont)))))))
;; context: walk kont for nearest ProvideFrame with matching name
(define step-sf-context
(fn (args env kont)
(let ((name (trampoline (eval-expr (first args) env)))
(default-val (if (>= (len args) 2)
(trampoline (eval-expr (nth args 1) env))
nil))
(frame (kont-find-provide kont name)))
(if frame
(make-cek-value (get frame "value") env kont)
(if (>= (len args) 2)
(make-cek-value default-val env kont)
(error (str "No provider for: " name)))))))
;; emit!: walk kont for nearest ScopeAccFrame, append value
(define step-sf-emit
(fn (args env kont)
(let ((name (trampoline (eval-expr (first args) env)))
(val (trampoline (eval-expr (nth args 1) env)))
(frame (kont-find-scope-acc kont name)))
(if frame
(do (append! (get frame "emitted") val)
(make-cek-value nil env kont))
(error (str "No scope for emit!: " name))))))
;; emitted: walk kont for nearest ScopeAccFrame, return accumulated list
(define step-sf-emitted
(fn (args env kont)
(let ((name (trampoline (eval-expr (first args) env)))
(frame (kont-find-scope-acc kont name)))
(if frame
(make-cek-value (get frame "emitted") env kont)
(error (str "No scope for emitted: " name))))))
;; reset: push ResetFrame, evaluate body
(define step-sf-reset
@@ -2013,6 +2115,41 @@
(make-scope-frame name (rest remaining) fenv)
rest-k))))
;; --- ProvideFrame: body expression evaluated ---
(= ft "provide")
(let ((remaining (get frame "remaining"))
(fenv (get frame "env")))
(if (empty? remaining)
;; Body done — return value, frame consumed
(make-cek-value value fenv rest-k)
;; More body expressions — keep frame on kont
(make-cek-state
(first remaining) fenv
(kont-push
(make-provide-frame
(get frame "name") (get frame "value")
(rest remaining) fenv)
rest-k))))
;; --- ScopeAccFrame: body expression evaluated ---
(= ft "scope-acc")
(let ((remaining (get frame "remaining"))
(fenv (get frame "env")))
(if (empty? remaining)
;; Body done — return value, frame consumed
(make-cek-value value fenv rest-k)
;; More body expressions — carry emitted list forward
(make-cek-state
(first remaining) fenv
(kont-push
(let ((new-frame (make-scope-acc-frame
(get frame "name") (get frame "value")
(rest remaining) fenv)))
;; Preserve accumulated emitted from current frame
(dict-set! new-frame "emitted" (get frame "emitted"))
new-frame)
rest-k))))
;; --- MapFrame: callback result for map/map-indexed ---
(= ft "map")
(let ((f (get frame "f"))

View File

@@ -491,6 +491,78 @@
(td :class "px-3 py-2 text-stone-600" "Full compiled pipeline. JIT compilation, cached WASM functions, near-native rendering speed."))))))
;; -----------------------------------------------------------------------
;; WASM responses — htmx with compiled components
;; -----------------------------------------------------------------------
(~docs/section :title "WASM Responses" :id "wasm-responses"
(p "The server doesn't have to send HTML fragments for HTMX-style swaps. "
"It can send compiled WASM functions that produce DOM directly, "
"or \u2014 when the client already has the components cached \u2014 "
"just the data as gzipped bytecode.")
(h4 :class "font-semibold mt-4 mb-2" "Three response tiers")
(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" "Client cache state")
(th :class "px-3 py-2 font-medium text-stone-600" "Server sends")
(th :class "px-3 py-2 font-medium text-stone-600" "Client does")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Nothing cached")
(td :class "px-3 py-2 text-stone-700" "HTML (fallback)")
(td :class "px-3 py-2 text-stone-600" "Parse HTML, morph DOM"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Compiler cached")
(td :class "px-3 py-2 text-stone-700" "SX source")
(td :class "px-3 py-2 text-stone-600" "Compile, execute, morph DOM"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Components cached")
(td :class "px-3 py-2 text-stone-700" "Bytecode (data only)")
(td :class "px-3 py-2 text-stone-600" "Call cached WASM function with args"))
(tr
(td :class "px-3 py-2 text-stone-700" "Components cached + gzip")
(td :class "px-3 py-2 text-stone-700" "Gzipped bytecode")
(td :class "px-3 py-2 text-stone-600" "Decompress, call cached function")))))
(h4 :class "font-semibold mt-4 mb-2" "Why bytecode is tiny")
(p "The bytecode doesn't repeat what the client already knows. "
"Component CID references are 4-byte pointers to already-cached compiled functions. "
"Tag names, class strings, attribute keys \u2014 all baked into the compiled component. "
"The only payload is the data that differs between instances.")
(~docs/code :code (highlight ";; HTML response: ~450 bytes\n<div class=\"card\"><h2>Hello</h2><p class=\"text-stone-600\">World</p></div>\n\n;; SX source response: ~60 bytes\n(~card :title \"Hello\" :body \"World\")\n\n;; Bytecode response: ~18 bytes\n[CALL_CID bafyrei..card] [STR \"Hello\"] [STR \"World\"]\n\n;; Gzipped bytecode: ~12 bytes" "text"))
(p "For a list of 50 cards:")
(~docs/code :code (highlight "HTML: 50\u00d7 <div class=\"card\"><h2>...</h2>...</div> ~22 KB\nSX source: 50\u00d7 (~card :title \"...\" :body \"...\") ~3 KB\nBytecode: [CALL_CID] + 50\u00d7 [STR, STR] ~800 bytes\nGzipped bytecode: ~400 bytes" "text"))
(p "The markup structure is in the compiled component. "
"The class strings are in the compiled component. "
"The only thing on the wire is the data that differs between instances. "
"Gzip crushes the repetitive framing to almost nothing.")
(h4 :class "font-semibold mt-4 mb-2" "Content negotiation")
(p "The client advertises what it has via request headers:")
(~docs/code :code (highlight ";; Client tells server what's cached\nAccept: application/sx-bytecode, text/sx, text/html\nX-Sx-Cached-Components: bafyrei..card, bafyrei..header, bafyrei..nav\n\n;; Server picks the smallest response:\n;; - Client has ~card cached? Send bytecode (data only)\n;; - Client has compiler but not ~card? Send SX source\n;; - Client has nothing? Send HTML" "text"))
(p "The server and client negotiate the optimal response format automatically. "
"First visit is HTML (full progressive enhancement). "
"Subsequent navigations are gzipped bytecode \u2014 "
"just the data, a few hundred bytes, instant render via cached compiled components.")
(h4 :class "font-semibold mt-4 mb-2" "The server becomes a data API")
(p "At the bytecode tier, the server is no longer rendering views. "
"It's serving data \u2014 structured arguments for compiled client-side functions. "
"The rendering logic is cached on the client as compiled WASM. "
"This inverts the traditional server-rendered model without sacrificing "
"progressive enhancement \u2014 the HTML fallback is always available."))
;; -----------------------------------------------------------------------
;; How this changes existing plans
;; -----------------------------------------------------------------------