diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 117cfad..67ce7af 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -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; diff --git a/spec/evaluator.sx b/spec/evaluator.sx index 3db8d8b..3127c12 100644 --- a/spec/evaluator.sx +++ b/spec/evaluator.sx @@ -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")) diff --git a/sx/sx/plans/mother-language.sx b/sx/sx/plans/mother-language.sx index 961a8fc..d6626d8 100644 --- a/sx/sx/plans/mother-language.sx +++ b/sx/sx/plans/mother-language.sx @@ -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
World