diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 4ee5e28..a9fef9a 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-14T22:10:44Z"; + var SX_VERSION = "2026-03-14T22:31:34Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -352,6 +352,8 @@ PRIMITIVES["even?"] = function(n) { return n % 2 === 0; }; PRIMITIVES["zero?"] = function(n) { return n === 0; }; PRIMITIVES["boolean?"] = function(x) { return x === true || x === false; }; + PRIMITIVES["symbol?"] = function(x) { return x != null && x._sym === true; }; + PRIMITIVES["keyword?"] = function(x) { return x != null && x._kw === true; }; PRIMITIVES["component-affinity"] = componentAffinity; @@ -525,7 +527,14 @@ function mapDict(fn, d) { var r = {}; for (var k in d) r[k] = fn(k, d[k]); return r; } // Predicate aliases used by transpiled code + // Both naming conventions: isX (from js-renames) and x_p (from js-mangle of x?) + var isNumber = PRIMITIVES["number?"]; var number_p = isNumber; + var isString = PRIMITIVES["string?"]; var string_p = isString; + var isBoolean = PRIMITIVES["boolean?"]; var boolean_p = isBoolean; var isDict = PRIMITIVES["dict?"]; + var isList = PRIMITIVES["list?"]; var list_p = isList; + var isKeyword = PRIMITIVES["keyword?"]; var keyword_p = isKeyword; + var isSymbol = PRIMITIVES["symbol?"]; var symbol_p = isSymbol; // List primitives used directly by transpiled code var len = PRIMITIVES["len"]; @@ -5295,7 +5304,7 @@ PRIMITIVES["trampoline-cek"] = trampolineCek; PRIMITIVES["primitive-name"] = primitiveName; // cek-serialize-value - var cekSerializeValue = function(val) { return (isSxTruthy(isNil(val)) ? NIL : (isSxTruthy(isNumber(val)) ? val : (isSxTruthy(isString(val)) ? val : (isSxTruthy((typeOf(val) == "boolean")) ? val : (isSxTruthy((typeOf(val) == "symbol")) ? val : (isSxTruthy((typeOf(val) == "keyword")) ? val : (isSxTruthy(isList(val)) ? map(cekSerializeValue, val) : (isSxTruthy(isLambda(val)) ? [makeSymbol("lambda"), lambdaParams(val), lambdaBody(val)] : (isSxTruthy(isCallable(val)) ? [makeSymbol("primitive"), sxOr(primitiveName(val), "?")] : (isSxTruthy(isDict(val)) ? cekSerializeEnv(val) : (String(val)))))))))))); }; + var cekSerializeValue = function(val) { return (isSxTruthy(isNil(val)) ? NIL : (isSxTruthy(isNumber(val)) ? val : (isSxTruthy(isString(val)) ? val : (isSxTruthy(boolean_p(val)) ? val : (isSxTruthy(symbol_p(val)) ? val : (isSxTruthy(keyword_p(val)) ? val : (isSxTruthy(isList(val)) ? map(cekSerializeValue, val) : (isSxTruthy(isLambda(val)) ? [makeSymbol("lambda"), lambdaParams(val), lambdaBody(val)] : (isSxTruthy(isCallable(val)) ? [makeSymbol("primitive"), sxOr(primitiveName(val), "?")] : (isSxTruthy(isDict(val)) ? cekSerializeEnv(val) : (String(val)))))))))))); }; PRIMITIVES["cek-serialize-value"] = cekSerializeValue; // cek-serialize-env @@ -5324,7 +5333,7 @@ PRIMITIVES["cek-serialize-frame"] = cekSerializeFrame; PRIMITIVES["cek-freeze"] = cekFreeze; // cek-thaw-value - var cekThawValue = function(val) { return (isSxTruthy(isNil(val)) ? NIL : (isSxTruthy(isNumber(val)) ? val : (isSxTruthy(isString(val)) ? val : (isSxTruthy((typeOf(val) == "boolean")) ? val : (isSxTruthy((typeOf(val) == "symbol")) ? val : (isSxTruthy((typeOf(val) == "keyword")) ? val : (isSxTruthy((isSxTruthy(isList(val)) && isSxTruthy(!isSxTruthy(isEmpty(val))) && isSxTruthy((typeOf(first(val)) == "symbol")) && (symbolName(first(val)) == "primitive"))) ? getPrimitive(nth(val, 1)) : (isSxTruthy((isSxTruthy(isList(val)) && isSxTruthy(!isSxTruthy(isEmpty(val))) && isSxTruthy((typeOf(first(val)) == "symbol")) && (symbolName(first(val)) == "lambda"))) ? makeLambda(nth(val, 1), nth(val, 2), {}) : (isSxTruthy(isList(val)) ? map(cekThawValue, val) : (isSxTruthy(isDict(val)) ? cekThawEnv(val) : val)))))))))); }; + var cekThawValue = function(val) { return (isSxTruthy(isNil(val)) ? NIL : (isSxTruthy(isNumber(val)) ? val : (isSxTruthy(isString(val)) ? val : (isSxTruthy(boolean_p(val)) ? val : (isSxTruthy(symbol_p(val)) ? val : (isSxTruthy(keyword_p(val)) ? val : (isSxTruthy((isSxTruthy(isList(val)) && isSxTruthy(!isSxTruthy(isEmpty(val))) && isSxTruthy(symbol_p(first(val))) && (symbolName(first(val)) == "primitive"))) ? getPrimitive(nth(val, 1)) : (isSxTruthy((isSxTruthy(isList(val)) && isSxTruthy(!isSxTruthy(isEmpty(val))) && isSxTruthy(symbol_p(first(val))) && (symbolName(first(val)) == "lambda"))) ? makeLambda(nth(val, 1), nth(val, 2), {}) : (isSxTruthy(isList(val)) ? map(cekThawValue, val) : (isSxTruthy(isDict(val)) ? cekThawEnv(val) : val)))))))))); }; PRIMITIVES["cek-thaw-value"] = cekThawValue; // cek-thaw-env diff --git a/shared/sx/ref/cek.sx b/shared/sx/ref/cek.sx index c9ceff8..cef1070 100644 --- a/shared/sx/ref/cek.sx +++ b/shared/sx/ref/cek.sx @@ -1074,9 +1074,9 @@ (nil? val) nil (number? val) val (string? val) val - (= (type-of val) "boolean") val - (= (type-of val) "symbol") val - (= (type-of val) "keyword") val + (boolean? val) val + (symbol? val) val + (keyword? val) val (list? val) (map cek-serialize-value val) (lambda? val) (list (make-symbol "lambda") (lambda-params val) @@ -1137,17 +1137,17 @@ (nil? val) nil (number? val) val (string? val) val - (= (type-of val) "boolean") val - (= (type-of val) "symbol") val - (= (type-of val) "keyword") val + (boolean? val) val + (symbol? val) val + (keyword? val) val ;; (primitive "name") → look up native function (and (list? val) (not (empty? val)) - (= (type-of (first val)) "symbol") + (symbol? (first val)) (= (symbol-name (first val)) "primitive")) (get-primitive (nth val 1)) ;; (lambda (params) body) → reconstruct Lambda (and (list? val) (not (empty? val)) - (= (type-of (first val)) "symbol") + (symbol? (first val)) (= (symbol-name (first val)) "lambda")) (make-lambda (nth val 1) (nth val 2) (dict)) (list? val) (map cek-thaw-value val) diff --git a/shared/sx/ref/platform_js.py b/shared/sx/ref/platform_js.py index 72ecc4c..1a1c032 100644 --- a/shared/sx/ref/platform_js.py +++ b/shared/sx/ref/platform_js.py @@ -951,6 +951,8 @@ PRIMITIVES_JS_MODULES: dict[str, str] = { PRIMITIVES["even?"] = function(n) { return n % 2 === 0; }; PRIMITIVES["zero?"] = function(n) { return n === 0; }; PRIMITIVES["boolean?"] = function(x) { return x === true || x === false; }; + PRIMITIVES["symbol?"] = function(x) { return x != null && x._sym === true; }; + PRIMITIVES["keyword?"] = function(x) { return x != null && x._kw === true; }; PRIMITIVES["component-affinity"] = componentAffinity; ''', @@ -1353,7 +1355,14 @@ PLATFORM_JS_POST = ''' function mapDict(fn, d) { var r = {}; for (var k in d) r[k] = fn(k, d[k]); return r; } // Predicate aliases used by transpiled code + // Both naming conventions: isX (from js-renames) and x_p (from js-mangle of x?) + var isNumber = PRIMITIVES["number?"]; var number_p = isNumber; + var isString = PRIMITIVES["string?"]; var string_p = isString; + var isBoolean = PRIMITIVES["boolean?"]; var boolean_p = isBoolean; var isDict = PRIMITIVES["dict?"]; + var isList = PRIMITIVES["list?"]; var list_p = isList; + var isKeyword = PRIMITIVES["keyword?"]; var keyword_p = isKeyword; + var isSymbol = PRIMITIVES["symbol?"]; var symbol_p = isSymbol; // List primitives used directly by transpiled code var len = PRIMITIVES["len"]; diff --git a/shared/sx/ref/platform_py.py b/shared/sx/ref/platform_py.py index a84cf76..94e4752 100644 --- a/shared/sx/ref/platform_py.py +++ b/shared/sx/ref/platform_py.py @@ -721,6 +721,9 @@ PRIMITIVES["number?"] = lambda x: isinstance(x, (int, float)) and not isinstance PRIMITIVES["string?"] = lambda x: isinstance(x, str) PRIMITIVES["list?"] = lambda x: isinstance(x, _b_list) PRIMITIVES["dict?"] = lambda x: isinstance(x, _b_dict) +PRIMITIVES["boolean?"] = lambda x: isinstance(x, bool) +PRIMITIVES["symbol?"] = lambda x: isinstance(x, Symbol) +PRIMITIVES["keyword?"] = lambda x: isinstance(x, Keyword) PRIMITIVES["continuation?"] = lambda x: isinstance(x, Continuation) PRIMITIVES["empty?"] = lambda c: ( c is None or c is NIL or @@ -1010,6 +1013,12 @@ parse_int = PRIMITIVES["parse-int"] upper = PRIMITIVES["upper"] has_key_p = PRIMITIVES["has-key?"] dict_p = PRIMITIVES["dict?"] +boolean_p = PRIMITIVES["boolean?"] +symbol_p = PRIMITIVES["symbol?"] +keyword_p = PRIMITIVES["keyword?"] +number_p = PRIMITIVES["number?"] +string_p = PRIMITIVES["string?"] +list_p = PRIMITIVES["list?"] dissoc = PRIMITIVES["dissoc"] index_of = PRIMITIVES["index-of"] lower = PRIMITIVES["lower"] diff --git a/shared/sx/ref/sx_ref.py b/shared/sx/ref/sx_ref.py index 35dddf7..71934e4 100644 --- a/shared/sx/ref/sx_ref.py +++ b/shared/sx/ref/sx_ref.py @@ -692,6 +692,9 @@ PRIMITIVES["number?"] = lambda x: isinstance(x, (int, float)) and not isinstance PRIMITIVES["string?"] = lambda x: isinstance(x, str) PRIMITIVES["list?"] = lambda x: isinstance(x, _b_list) PRIMITIVES["dict?"] = lambda x: isinstance(x, _b_dict) +PRIMITIVES["boolean?"] = lambda x: isinstance(x, bool) +PRIMITIVES["symbol?"] = lambda x: isinstance(x, Symbol) +PRIMITIVES["keyword?"] = lambda x: isinstance(x, Keyword) PRIMITIVES["continuation?"] = lambda x: isinstance(x, Continuation) PRIMITIVES["empty?"] = lambda c: ( c is None or c is NIL or @@ -930,6 +933,12 @@ parse_int = PRIMITIVES["parse-int"] upper = PRIMITIVES["upper"] has_key_p = PRIMITIVES["has-key?"] dict_p = PRIMITIVES["dict?"] +boolean_p = PRIMITIVES["boolean?"] +symbol_p = PRIMITIVES["symbol?"] +keyword_p = PRIMITIVES["keyword?"] +number_p = PRIMITIVES["number?"] +string_p = PRIMITIVES["string?"] +list_p = PRIMITIVES["list?"] dissoc = PRIMITIVES["dissoc"] index_of = PRIMITIVES["index-of"] lower = PRIMITIVES["lower"] @@ -4607,11 +4616,11 @@ def cek_serialize_value(val): return val elif sx_truthy(string_p(val)): return val - elif sx_truthy((type_of(val) == 'boolean')): + elif sx_truthy(boolean_p(val)): return val - elif sx_truthy((type_of(val) == 'symbol')): + elif sx_truthy(symbol_p(val)): return val - elif sx_truthy((type_of(val) == 'keyword')): + elif sx_truthy(keyword_p(val)): return val elif sx_truthy(list_p(val)): return map(cek_serialize_value, val) @@ -4653,15 +4662,15 @@ def cek_thaw_value(val): return val elif sx_truthy(string_p(val)): return val - elif sx_truthy((type_of(val) == 'boolean')): + elif sx_truthy(boolean_p(val)): return val - elif sx_truthy((type_of(val) == 'symbol')): + elif sx_truthy(symbol_p(val)): return val - elif sx_truthy((type_of(val) == 'keyword')): + elif sx_truthy(keyword_p(val)): return val - elif sx_truthy((list_p(val) if not sx_truthy(list_p(val)) else ((not sx_truthy(empty_p(val))) if not sx_truthy((not sx_truthy(empty_p(val)))) else ((type_of(first(val)) == 'symbol') if not sx_truthy((type_of(first(val)) == 'symbol')) else (symbol_name(first(val)) == 'primitive'))))): + elif sx_truthy((list_p(val) if not sx_truthy(list_p(val)) else ((not sx_truthy(empty_p(val))) if not sx_truthy((not sx_truthy(empty_p(val)))) else (symbol_p(first(val)) if not sx_truthy(symbol_p(first(val))) else (symbol_name(first(val)) == 'primitive'))))): return get_primitive(nth(val, 1)) - elif sx_truthy((list_p(val) if not sx_truthy(list_p(val)) else ((not sx_truthy(empty_p(val))) if not sx_truthy((not sx_truthy(empty_p(val)))) else ((type_of(first(val)) == 'symbol') if not sx_truthy((type_of(first(val)) == 'symbol')) else (symbol_name(first(val)) == 'lambda'))))): + elif sx_truthy((list_p(val) if not sx_truthy(list_p(val)) else ((not sx_truthy(empty_p(val))) if not sx_truthy((not sx_truthy(empty_p(val)))) else (symbol_p(first(val)) if not sx_truthy(symbol_p(first(val))) else (symbol_name(first(val)) == 'lambda'))))): return make_lambda(nth(val, 1), nth(val, 2), {}) elif sx_truthy(list_p(val)): return map(cek_thaw_value, val) diff --git a/sx/sx/geography/cek.sx b/sx/sx/geography/cek.sx index de31b92..fb3c6d3 100644 --- a/sx/sx/geography/cek.sx +++ b/sx/sx/geography/cek.sx @@ -473,6 +473,150 @@ + +;; --------------------------------------------------------------------------- +;; CEK Freeze / Thaw — serializable computation +;; --------------------------------------------------------------------------- + +(defcomp ~geography/cek/cek-freeze-content () + (~docs/page :title "Freeze / Thaw" + + (p :class "text-stone-500 text-sm italic mb-8" + "A computation is a value. Freeze it to an s-expression. " + "Store it, transmit it, content-address it. Thaw and resume anywhere.") + + (~docs/section :title "The idea" :id "idea" + (p "The CEK machine makes evaluation explicit: every step is a pure function from state to state. " + "The state is a dict with four fields:") + (ul :class "list-disc pl-6 mb-4 space-y-1 text-stone-600" + (li (code "control") " \u2014 the expression being evaluated") + (li (code "env") " \u2014 the bindings in scope") + (li (code "kont") " \u2014 the continuation (what to do with the result)") + (li (code "phase") " \u2014 eval or continue")) + (p "Since the state is data, it can be serialized. " + (code "cek-freeze") " converts a live CEK state to pure s-expressions. " + (code "cek-thaw") " reconstructs a live state from frozen SX. " + (code "cek-run") " resumes from where it left off.")) + + (~docs/section :title "Freeze" :id "freeze" + (p "Take a computation mid-flight and freeze it:") + (~docs/code :code (highlight + "(let ((expr (sx-parse \"(+ 1 (* 2 3))\"))\n (state (make-cek-state (first expr) (make-env) (list))))\n ;; Step 4 times\n (set! state (cek-step (cek-step (cek-step (cek-step state)))))\n ;; Freeze to SX\n (cek-freeze state))" + "lisp")) + (p "The frozen state is pure SX:") + (~docs/code :code (highlight + "{:phase \"continue\"\n :control nil\n :value 1\n :env {}\n :kont ({:type \"arg\"\n :f (primitive \"+\")\n :evaled ()\n :remaining ((* 2 3))\n :env {}})}" + "lisp")) + (p "Everything is data. The continuation frame says: \u201cI was adding 1 to something, " + "and I still need to evaluate " (code "(* 2 3)") ".\u201d")) + + (~docs/section :title "Thaw and resume" :id "thaw" + (p "Parse the frozen SX back. Thaw it. Resume:") + (~docs/code :code (highlight + "(let ((frozen (sx-parse frozen-text))\n (state (cek-thaw (first frozen))))\n (cek-run state))\n;; => 7" + "lisp")) + (p "Native functions like " (code "+") " serialize as " (code "(primitive \"+\")") + " and are looked up in the primitive registry on thaw. " + "Lambdas serialize as their source AST \u2014 " (code "(lambda (x) (* x 2))") + " \u2014 and reconstruct as callable functions.")) + + (~docs/section :title "Live demo" :id "demo" + (p "Type an expression, step to any point, freeze the state. " + "The frozen SX appears below. Click Thaw to resume from the frozen state.") + (~geography/cek/freeze-demo)) + + (~docs/section :title "What this enables" :id "enables" + (ul :class "list-disc pl-6 mb-4 space-y-2 text-stone-600" + (li (strong "Persistence") " \u2014 save reactive island state to localStorage, " + "resume on page reload") + (li (strong "Migration") " \u2014 freeze a computation on one machine, " + "thaw on another. Same result, deterministically.") + (li (strong "Content addressing") " \u2014 hash the frozen SX \u2192 CID. " + "A pointer to a computation in progress, not just a value.") + (li (strong "Time travel") " \u2014 freeze at each step, store the history. " + "Jump to any point. Undo. Branch.") + (li (strong "Verification") " \u2014 re-run from a frozen state, " + "check the result matches. Reproducible computation.")) + (p "The Platonic argument made concrete: a computation IS a value. " + "The Form persists. The instance resumes.")))) + + +(defisland ~geography/cek/freeze-demo () + (let ((source (signal "(+ 1 (* 2 3))")) + (state-sig (signal nil)) + (step-count (signal 0)) + (frozen-sx (signal "")) + (thaw-result (signal ""))) + (letrec + ((do-parse (fn () + (reset! frozen-sx "") + (reset! thaw-result "") + (reset! step-count 0) + (let ((parsed (sx-parse (deref source)))) + (when (not (empty? parsed)) + (reset! state-sig (make-cek-state (first parsed) (make-env) (list))))))) + (do-step (fn () + (when (and (deref state-sig) (not (cek-terminal? (deref state-sig)))) + (reset! state-sig (cek-step (deref state-sig))) + (swap! step-count inc)))) + (do-freeze (fn () + (when (deref state-sig) + (let ((frozen (cek-freeze (deref state-sig)))) + (reset! frozen-sx (sx-serialize frozen)))))) + (do-thaw (fn () + (when (not (empty? (deref frozen-sx))) + (let ((parsed (sx-parse (deref frozen-sx)))) + (when (not (empty? parsed)) + (let ((thawed (cek-thaw (first parsed)))) + (reset! thaw-result + (str "Resumed from step " (deref step-count) ": " + (cek-run thawed))))))))) + (do-run (fn () + (when (deref state-sig) + (let run-loop () + (when (not (cek-terminal? (deref state-sig))) + (do-step) + (run-loop))))))) + ;; Auto-parse + (effect (fn () (do-parse))) + (div :class "space-y-4" + ;; Input + (div :class "flex gap-2 items-end" + (div :class "flex-1" + (label :class "text-xs text-stone-400 block mb-1" "Expression") + (input :type "text" :bind source + :class "w-full px-3 py-1.5 rounded border border-stone-300 font-mono text-sm focus:outline-none focus:border-violet-400" + :on-change (fn (e) (do-parse)))) + (div :class "flex gap-1" + (button :on-click (fn (e) (do-step)) + :class "px-3 py-1.5 rounded bg-violet-500 text-white text-sm hover:bg-violet-600" + "Step") + (button :on-click (fn (e) (do-run)) + :class "px-3 py-1.5 rounded bg-violet-700 text-white text-sm hover:bg-violet-800" + "Run"))) + ;; Step count + freeze button + (div :class "flex items-center gap-3" + (span :class "text-sm text-stone-500 font-mono" + "Step " (deref step-count)) + (when (deref state-sig) + (button :on-click (fn (e) (do-freeze)) + :class "px-3 py-1.5 rounded bg-amber-500 text-white text-sm hover:bg-amber-600" + "Freeze"))) + ;; Frozen SX output + (when (not (empty? (deref frozen-sx))) + (div :class "space-y-2" + (label :class "text-xs text-stone-400 block" "Frozen CEK state") + (pre :class "text-xs font-mono bg-stone-50 rounded p-3 overflow-x-auto whitespace-pre-wrap text-stone-700" + (deref frozen-sx)) + (button :on-click (fn (e) (do-thaw)) + :class "px-3 py-1.5 rounded bg-emerald-600 text-white text-sm hover:bg-emerald-700" + "Thaw \u2192 Resume"))) + ;; Thaw result + (when (not (empty? (deref thaw-result))) + (div :class "rounded bg-emerald-50 border border-emerald-200 p-3 text-emerald-800 font-mono text-sm" + (deref thaw-result))))))) + + ;; --------------------------------------------------------------------------- ;; Demo page content ;; --------------------------------------------------------------------------- diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx index 2a15bf4..3d46dcb 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -174,7 +174,9 @@ (dict :label "Overview" :href "/sx/(geography.(cek))" :summary "The CEK machine — explicit evaluator with Control, Environment, Kontinuation. Three registers, pure step function.") (dict :label "Demo" :href "/sx/(geography.(cek.demo))" - :summary "Live islands evaluated by the CEK machine. Counter, computed chains, reactive attributes — all through explicit continuation frames."))) + :summary "Live islands evaluated by the CEK machine. Counter, computed chains, reactive attributes — all through explicit continuation frames.") + (dict :label "Freeze / Thaw" :href "/sx/(geography.(cek.freeze))" + :summary "Serialize a CEK state to s-expressions. Ship it, store it, content-address it. Thaw and resume anywhere."))) (define plans-nav-items (list (dict :label "Status" :href "/sx/(etc.(plan.status))" diff --git a/sx/sx/page-functions.sx b/sx/sx/page-functions.sx index 8572b6b..91794c1 100644 --- a/sx/sx/page-functions.sx +++ b/sx/sx/page-functions.sx @@ -66,6 +66,7 @@ '(~geography/cek/cek-content) (case slug "demo" '(~geography/cek/cek-demo-content) + "freeze" '(~geography/cek/cek-freeze-content) :else '(~geography/cek/cek-content))))) (define provide