From b03c84b9628d4ae50545b7f2d7be0fbe0f09b113 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 14 Mar 2026 22:11:05 +0000 Subject: [PATCH] Serializable CEK state: cek-freeze and cek-thaw MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Freeze a CEK state to pure s-expressions. Thaw it back to a live state and resume with cek-run. Full round-trip through SX text works: freeze → sx-serialize → sx-parse → thaw → resume → same result. - cek-freeze: serialize control/env/kont/value to SX dicts - cek-thaw: reconstruct live state from frozen SX - Native functions serialize as (primitive "name"), looked up on resume - Lambdas serialize as (lambda (params) body) - Environments serialize as flat dicts of visible bindings - Continuation frames serialize as typed dicts Enables: localStorage persistence, content-addressed computation, cross-machine migration, time-travel debugging. Co-Authored-By: Claude Opus 4.6 (1M context) --- shared/static/scripts/sx-browser.js | 70 +++++++++++- shared/sx/ref/cek.sx | 162 ++++++++++++++++++++++++++++ shared/sx/ref/sx_ref.py | 104 ++++++++++++++++++ 3 files changed, 335 insertions(+), 1 deletion(-) diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index c04da09..4ee5e28 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-14T20:39:57Z"; + var SX_VERSION = "2026-03-14T22:10:44Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -5283,6 +5283,74 @@ PRIMITIVES["eval-expr-cek"] = evalExprCek; var trampolineCek = function(val) { return (isSxTruthy(isThunk(val)) ? evalExprCek(thunkExpr(val), thunkEnv(val)) : val); }; PRIMITIVES["trampoline-cek"] = trampolineCek; + // primitive-name + var primitiveName = function(f) { return (isSxTruthy(isLambda(f)) ? lambdaName(f) : (function() { + var result = NIL; + var names = ["+", "-", "*", "/", "=", "<", ">", "<=", ">=", "not", "and", "or", "str", "len", "first", "rest", "nth", "list", "cons", "append", "map", "filter", "reduce", "for-each", "some", "every?", "get", "keys", "dict", "dict?", "has-key?", "assoc", "empty?", "nil?", "number?", "string?", "list?", "type-of", "identity", "inc", "dec", "mod", "join", "split", "slice", "contains?", "starts-with?", "upper", "lower", "trim", "replace", "format"]; + { var _c = names; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; if (isSxTruthy((isSxTruthy(isNil(result)) && isSxTruthy(isPrimitive(name)) && isIdentical(f, getPrimitive(name))))) { + result = name; +} } } + return result; +})()); }; +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)))))))))))); }; +PRIMITIVES["cek-serialize-value"] = cekSerializeValue; + + // cek-serialize-env + var cekSerializeEnv = function(env) { return (function() { + var result = {}; + var ks = keys(env); + { var _c = ks; for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; result[k] = cekSerializeValue(get(env, k)); } } + return result; +})(); }; +PRIMITIVES["cek-serialize-env"] = cekSerializeEnv; + + // cek-serialize-frame + var cekSerializeFrame = function(frame) { return (function() { + var result = {}; + var ks = keys(frame); + { var _c = ks; for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; (function() { + var v = get(frame, k); + return dictSet(result, k, (isSxTruthy((k == "type")) ? v : (isSxTruthy((k == "tag")) ? v : (isSxTruthy((k == "f")) ? cekSerializeValue(v) : (isSxTruthy((k == "env")) ? cekSerializeEnv(v) : (isSxTruthy((k == "evaled")) ? map(cekSerializeValue, v) : (isSxTruthy((k == "remaining")) ? v : (isSxTruthy((k == "results")) ? map(cekSerializeValue, v) : (isSxTruthy((k == "raw-args")) ? v : (isSxTruthy((k == "current-item")) ? cekSerializeValue(v) : (isSxTruthy((k == "name")) ? v : (isSxTruthy((k == "update-fn")) ? cekSerializeValue(v) : (isSxTruthy((k == "first-render")) ? v : cekSerializeValue(v)))))))))))))); +})(); } } + return result; +})(); }; +PRIMITIVES["cek-serialize-frame"] = cekSerializeFrame; + + // cek-freeze + var cekFreeze = function(state) { return {["phase"]: get(state, "phase"), ["control"]: get(state, "control"), ["value"]: cekSerializeValue(get(state, "value")), ["env"]: cekSerializeEnv(get(state, "env")), ["kont"]: map(cekSerializeFrame, get(state, "kont"))}; }; +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)))))))))); }; +PRIMITIVES["cek-thaw-value"] = cekThawValue; + + // cek-thaw-env + var cekThawEnv = function(frozenEnv) { return (function() { + var result = makeEnv(); + { var _c = keys(frozenEnv); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; envSet(result, k, cekThawValue(get(frozenEnv, k))); } } + return result; +})(); }; +PRIMITIVES["cek-thaw-env"] = cekThawEnv; + + // cek-thaw-frame + var cekThawFrame = function(frozenFrame) { return (function() { + var result = {}; + var ks = keys(frozenFrame); + { var _c = ks; for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; (function() { + var v = get(frozenFrame, k); + return dictSet(result, k, (isSxTruthy((k == "type")) ? v : (isSxTruthy((k == "tag")) ? v : (isSxTruthy((k == "f")) ? cekThawValue(v) : (isSxTruthy((k == "env")) ? cekThawEnv(v) : (isSxTruthy((k == "evaled")) ? map(cekThawValue, v) : (isSxTruthy((k == "remaining")) ? v : (isSxTruthy((k == "results")) ? map(cekThawValue, v) : (isSxTruthy((k == "raw-args")) ? v : (isSxTruthy((k == "current-item")) ? cekThawValue(v) : (isSxTruthy((k == "name")) ? v : (isSxTruthy((k == "update-fn")) ? cekThawValue(v) : (isSxTruthy((k == "first-render")) ? v : cekThawValue(v)))))))))))))); +})(); } } + return result; +})(); }; +PRIMITIVES["cek-thaw-frame"] = cekThawFrame; + + // cek-thaw + var cekThaw = function(frozen) { return {["phase"]: get(frozen, "phase"), ["control"]: get(frozen, "control"), ["value"]: cekThawValue(get(frozen, "value")), ["env"]: cekThawEnv(get(frozen, "env")), ["kont"]: map(cekThawFrame, get(frozen, "kont"))}; }; +PRIMITIVES["cek-thaw"] = cekThaw; + // === Transpiled from signals (reactive signal runtime) === diff --git a/shared/sx/ref/cek.sx b/shared/sx/ref/cek.sx index b5918ba..c9ceff8 100644 --- a/shared/sx/ref/cek.sx +++ b/shared/sx/ref/cek.sx @@ -1032,3 +1032,165 @@ (if (thunk? val) (eval-expr-cek (thunk-expr val) (thunk-env val)) val))) + + +;; -------------------------------------------------------------------------- +;; 13. CEK state serialization — freeze and resume computation +;; -------------------------------------------------------------------------- +;; +;; Serialize a CEK state to an s-expression. The result can be: +;; - Printed as text (sx-serialize) +;; - Stored, transmitted, content-addressed +;; - Parsed back (sx-parse) and resumed (cek-run) +;; +;; Native functions serialize as (primitive "name") — looked up on resume. +;; Lambdas serialize as (lambda (params) body closure-env). +;; Environments serialize as dicts of their visible bindings. + +(define primitive-name :effects [] + (fn (f) + ;; For lambdas, use lambda-name. For native callables, check common names. + (if (lambda? f) + (lambda-name f) + ;; Native function — try common primitive names + (let ((result nil) + (names (list "+" "-" "*" "/" "=" "<" ">" "<=" ">=" "not" "and" "or" + "str" "len" "first" "rest" "nth" "list" "cons" "append" + "map" "filter" "reduce" "for-each" "some" "every?" + "get" "keys" "dict" "dict?" "has-key?" "assoc" + "empty?" "nil?" "number?" "string?" "list?" + "type-of" "identity" "inc" "dec" "mod" + "join" "split" "slice" "contains?" "starts-with?" + "upper" "lower" "trim" "replace" "format"))) + (for-each (fn (name) + (when (and (nil? result) (primitive? name) (identical? f (get-primitive name))) + (set! result name))) + names) + result)))) + +(define cek-serialize-value :effects [] + (fn (val) + (cond + (nil? val) nil + (number? val) val + (string? val) val + (= (type-of val) "boolean") val + (= (type-of val) "symbol") val + (= (type-of val) "keyword") val + (list? val) (map cek-serialize-value val) + (lambda? val) (list (make-symbol "lambda") + (lambda-params val) + (lambda-body val)) + (callable? val) (list (make-symbol "primitive") + (or (primitive-name val) "?")) + (dict? val) (cek-serialize-env val) + :else (str val)))) + +(define cek-serialize-env :effects [] + (fn (env) + (let ((result (dict)) + (ks (keys env))) + (for-each (fn (k) + (dict-set! result k (cek-serialize-value (get env k)))) + ks) + result))) + +(define cek-serialize-frame :effects [] + (fn (frame) + (let ((result (dict)) + (ks (keys frame))) + (for-each (fn (k) + (let ((v (get frame k))) + (dict-set! result k + (cond + (= k "type") v + (= k "tag") v + (= k "f") (cek-serialize-value v) + (= k "env") (cek-serialize-env v) + (= k "evaled") (map cek-serialize-value v) + (= k "remaining") v ;; unevaluated exprs stay as-is + (= k "results") (map cek-serialize-value v) + (= k "raw-args") v + (= k "current-item") (cek-serialize-value v) + (= k "name") v + (= k "update-fn") (cek-serialize-value v) + (= k "first-render") v + :else (cek-serialize-value v))))) + ks) + result))) + +(define cek-freeze :effects [] + (fn (state) + (dict + "phase" (get state "phase") + "control" (get state "control") + "value" (cek-serialize-value (get state "value")) + "env" (cek-serialize-env (get state "env")) + "kont" (map cek-serialize-frame (get state "kont"))))) + +;; Deserialize: reconstruct a runnable CEK state from frozen SX. +;; Native functions are looked up by name in the current PRIMITIVES. + +(define cek-thaw-value :effects [] + (fn (val) + (cond + (nil? val) nil + (number? val) val + (string? val) val + (= (type-of val) "boolean") val + (= (type-of val) "symbol") val + (= (type-of val) "keyword") val + ;; (primitive "name") → look up native function + (and (list? val) (not (empty? val)) + (= (type-of (first val)) "symbol") + (= (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-name (first val)) "lambda")) + (make-lambda (nth val 1) (nth val 2) (dict)) + (list? val) (map cek-thaw-value val) + (dict? val) (cek-thaw-env val) + :else val))) + +(define cek-thaw-env :effects [] + (fn (frozen-env) + (let ((result (make-env))) + (for-each (fn (k) + (env-set! result k (cek-thaw-value (get frozen-env k)))) + (keys frozen-env)) + result))) + +(define cek-thaw-frame :effects [] + (fn (frozen-frame) + (let ((result (dict)) + (ks (keys frozen-frame))) + (for-each (fn (k) + (let ((v (get frozen-frame k))) + (dict-set! result k + (cond + (= k "type") v + (= k "tag") v + (= k "f") (cek-thaw-value v) + (= k "env") (cek-thaw-env v) + (= k "evaled") (map cek-thaw-value v) + (= k "remaining") v + (= k "results") (map cek-thaw-value v) + (= k "raw-args") v + (= k "current-item") (cek-thaw-value v) + (= k "name") v + (= k "update-fn") (cek-thaw-value v) + (= k "first-render") v + :else (cek-thaw-value v))))) + ks) + result))) + +(define cek-thaw :effects [] + (fn (frozen) + (dict + "phase" (get frozen "phase") + "control" (get frozen "control") + "value" (cek-thaw-value (get frozen "value")) + "env" (cek-thaw-env (get frozen "env")) + "kont" (map cek-thaw-frame (get frozen "kont"))))) diff --git a/shared/sx/ref/sx_ref.py b/shared/sx/ref/sx_ref.py index 1afef04..35dddf7 100644 --- a/shared/sx/ref/sx_ref.py +++ b/shared/sx/ref/sx_ref.py @@ -4586,6 +4586,110 @@ def trampoline_cek(val): else: return val +# primitive-name +def primitive_name(f): + _cells = {} + if sx_truthy(is_lambda(f)): + return lambda_name(f) + else: + _cells['result'] = NIL + names = ['+', '-', '*', '/', '=', '<', '>', '<=', '>=', 'not', 'and', 'or', 'str', 'len', 'first', 'rest', 'nth', 'list', 'cons', 'append', 'map', 'filter', 'reduce', 'for-each', 'some', 'every?', 'get', 'keys', 'dict', 'dict?', 'has-key?', 'assoc', 'empty?', 'nil?', 'number?', 'string?', 'list?', 'type-of', 'identity', 'inc', 'dec', 'mod', 'join', 'split', 'slice', 'contains?', 'starts-with?', 'upper', 'lower', 'trim', 'replace', 'format'] + for name in names: + if sx_truthy((is_nil(_cells['result']) if not sx_truthy(is_nil(_cells['result'])) else (is_primitive(name) if not sx_truthy(is_primitive(name)) else is_identical(f, get_primitive(name))))): + _cells['result'] = name + return _cells['result'] + +# cek-serialize-value +def cek_serialize_value(val): + if sx_truthy(is_nil(val)): + return NIL + elif sx_truthy(number_p(val)): + return val + elif sx_truthy(string_p(val)): + return val + elif sx_truthy((type_of(val) == 'boolean')): + return val + elif sx_truthy((type_of(val) == 'symbol')): + return val + elif sx_truthy((type_of(val) == 'keyword')): + return val + elif sx_truthy(list_p(val)): + return map(cek_serialize_value, val) + elif sx_truthy(is_lambda(val)): + return [make_symbol('lambda'), lambda_params(val), lambda_body(val)] + elif sx_truthy(is_callable(val)): + return [make_symbol('primitive'), (primitive_name(val) if sx_truthy(primitive_name(val)) else '?')] + elif sx_truthy(dict_p(val)): + return cek_serialize_env(val) + else: + return sx_str(val) + +# cek-serialize-env +def cek_serialize_env(env): + result = {} + ks = keys(env) + for k in ks: + result[k] = cek_serialize_value(get(env, k)) + return result + +# cek-serialize-frame +def cek_serialize_frame(frame): + result = {} + ks = keys(frame) + for k in ks: + v = get(frame, k) + result[k] = (v if sx_truthy((k == 'type')) else (v if sx_truthy((k == 'tag')) else (cek_serialize_value(v) if sx_truthy((k == 'f')) else (cek_serialize_env(v) if sx_truthy((k == 'env')) else (map(cek_serialize_value, v) if sx_truthy((k == 'evaled')) else (v if sx_truthy((k == 'remaining')) else (map(cek_serialize_value, v) if sx_truthy((k == 'results')) else (v if sx_truthy((k == 'raw-args')) else (cek_serialize_value(v) if sx_truthy((k == 'current-item')) else (v if sx_truthy((k == 'name')) else (cek_serialize_value(v) if sx_truthy((k == 'update-fn')) else (v if sx_truthy((k == 'first-render')) else cek_serialize_value(v))))))))))))) + return result + +# cek-freeze +def cek_freeze(state): + return {'phase': get(state, 'phase'), 'control': get(state, 'control'), 'value': cek_serialize_value(get(state, 'value')), 'env': cek_serialize_env(get(state, 'env')), 'kont': map(cek_serialize_frame, get(state, 'kont'))} + +# cek-thaw-value +def cek_thaw_value(val): + if sx_truthy(is_nil(val)): + return NIL + elif sx_truthy(number_p(val)): + return val + elif sx_truthy(string_p(val)): + return val + elif sx_truthy((type_of(val) == 'boolean')): + return val + elif sx_truthy((type_of(val) == 'symbol')): + return val + elif sx_truthy((type_of(val) == 'keyword')): + 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'))))): + 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'))))): + return make_lambda(nth(val, 1), nth(val, 2), {}) + elif sx_truthy(list_p(val)): + return map(cek_thaw_value, val) + elif sx_truthy(dict_p(val)): + return cek_thaw_env(val) + else: + return val + +# cek-thaw-env +def cek_thaw_env(frozen_env): + result = make_env() + for k in keys(frozen_env): + result[k] = cek_thaw_value(get(frozen_env, k)) + return result + +# cek-thaw-frame +def cek_thaw_frame(frozen_frame): + result = {} + ks = keys(frozen_frame) + for k in ks: + v = get(frozen_frame, k) + result[k] = (v if sx_truthy((k == 'type')) else (v if sx_truthy((k == 'tag')) else (cek_thaw_value(v) if sx_truthy((k == 'f')) else (cek_thaw_env(v) if sx_truthy((k == 'env')) else (map(cek_thaw_value, v) if sx_truthy((k == 'evaled')) else (v if sx_truthy((k == 'remaining')) else (map(cek_thaw_value, v) if sx_truthy((k == 'results')) else (v if sx_truthy((k == 'raw-args')) else (cek_thaw_value(v) if sx_truthy((k == 'current-item')) else (v if sx_truthy((k == 'name')) else (cek_thaw_value(v) if sx_truthy((k == 'update-fn')) else (v if sx_truthy((k == 'first-render')) else cek_thaw_value(v))))))))))))) + return result + +# cek-thaw +def cek_thaw(frozen): + return {'phase': get(frozen, 'phase'), 'control': get(frozen, 'control'), 'value': cek_thaw_value(get(frozen, 'value')), 'env': cek_thaw_env(get(frozen, 'env')), 'kont': map(cek_thaw_frame, get(frozen, 'kont'))} + # === Transpiled from signals (reactive signal runtime) ===