Named freeze scopes for serializable reactive state
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 13m36s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 13m36s
Replace raw CEK state serialization with named freeze scopes.
A freeze scope collects signals registered within it. On freeze,
signal values are serialized to SX. On thaw, values are restored.
- freeze-scope: scoped effect delimiter for signal collection
- freeze-signal: register a signal with a name in the current scope
- cek-freeze-scope / cek-thaw-scope: freeze/thaw by scope name
- freeze-to-sx / thaw-from-sx: full SX text round-trip
- cek-freeze-all / cek-thaw-all: batch operations
Also: register boolean?, symbol?, keyword? predicates in both
Python and JS platforms with proper var aliases.
Demo: counter + name input with Freeze/Thaw buttons.
Frozen SX: {:name "demo" :signals {:count 5 :name "world"}}
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1034,163 +1034,98 @@
|
||||
val)))
|
||||
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 13. CEK state serialization — freeze and resume computation
|
||||
;; 13. Freeze scopes — named serializable state boundaries
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; 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)
|
||||
;; A freeze scope collects signals registered within it. On freeze,
|
||||
;; their current values are serialized to SX. On thaw, values are
|
||||
;; restored. Multiple named scopes can coexist independently.
|
||||
;;
|
||||
;; 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.
|
||||
;; Uses the scoped effects system: scope-push!/scope-pop!/context.
|
||||
;;
|
||||
;; Usage:
|
||||
;; (freeze-scope "editor"
|
||||
;; (let ((doc (signal "hello")))
|
||||
;; (freeze-signal "doc" doc)
|
||||
;; ...))
|
||||
;;
|
||||
;; (cek-freeze-scope "editor") → {:name "editor" :signals {:doc "hello"}}
|
||||
;; (cek-thaw-scope "editor" frozen-data) → restores signal values
|
||||
|
||||
(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))))
|
||||
;; Registry of freeze scopes: name → list of {name signal} entries
|
||||
(define freeze-registry (dict))
|
||||
|
||||
(define cek-serialize-value :effects []
|
||||
(fn (val)
|
||||
(cond
|
||||
(nil? val) nil
|
||||
(number? val) val
|
||||
(string? val) 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)
|
||||
(lambda-body val))
|
||||
(callable? val) (list (make-symbol "primitive")
|
||||
(or (primitive-name val) "?"))
|
||||
(dict? val) (cek-serialize-env val)
|
||||
:else (str val))))
|
||||
;; Register a signal in the current freeze scope
|
||||
(define freeze-signal :effects [mutation]
|
||||
(fn (name sig)
|
||||
(let ((scope-name (context "sx-freeze-scope" nil)))
|
||||
(when scope-name
|
||||
(let ((entries (or (get freeze-registry scope-name) (list))))
|
||||
(append! entries (dict "name" name "signal" sig))
|
||||
(dict-set! freeze-registry scope-name entries))))))
|
||||
|
||||
(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)))
|
||||
;; Freeze scope delimiter — collects signals registered within body
|
||||
(define freeze-scope :effects [mutation]
|
||||
(fn (name body-fn)
|
||||
(scope-push! "sx-freeze-scope" name)
|
||||
;; Initialize empty entry list for this scope
|
||||
(dict-set! freeze-registry name (list))
|
||||
(cek-call body-fn nil)
|
||||
(scope-pop! "sx-freeze-scope")
|
||||
nil))
|
||||
|
||||
(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)))
|
||||
;; Freeze a named scope → SX dict of signal values
|
||||
(define cek-freeze-scope :effects []
|
||||
(fn (name)
|
||||
(let ((entries (or (get freeze-registry name) (list)))
|
||||
(signals-dict (dict)))
|
||||
(for-each (fn (entry)
|
||||
(dict-set! signals-dict
|
||||
(get entry "name")
|
||||
(signal-value (get entry "signal"))))
|
||||
entries)
|
||||
(dict "name" name "signals" signals-dict))))
|
||||
|
||||
(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")))))
|
||||
;; Freeze all scopes
|
||||
(define cek-freeze-all :effects []
|
||||
(fn ()
|
||||
(map (fn (name) (cek-freeze-scope name))
|
||||
(keys freeze-registry))))
|
||||
|
||||
;; Deserialize: reconstruct a runnable CEK state from frozen SX.
|
||||
;; Native functions are looked up by name in the current PRIMITIVES.
|
||||
;; Thaw a named scope — restore signal values from frozen data
|
||||
(define cek-thaw-scope :effects [mutation]
|
||||
(fn (name frozen)
|
||||
(let ((entries (or (get freeze-registry name) (list)))
|
||||
(values (get frozen "signals")))
|
||||
(when values
|
||||
(for-each (fn (entry)
|
||||
(let ((sig-name (get entry "name"))
|
||||
(sig (get entry "signal"))
|
||||
(val (get values sig-name)))
|
||||
(when (not (nil? val))
|
||||
(reset! sig val))))
|
||||
entries)))))
|
||||
|
||||
(define cek-thaw-value :effects []
|
||||
(fn (val)
|
||||
(cond
|
||||
(nil? val) nil
|
||||
(number? val) val
|
||||
(string? val) val
|
||||
(boolean? val) val
|
||||
(symbol? val) val
|
||||
(keyword? val) val
|
||||
;; (primitive "name") → look up native function
|
||||
(and (list? val) (not (empty? val))
|
||||
(symbol? (first val))
|
||||
(= (symbol-name (first val)) "primitive"))
|
||||
(get-primitive (nth val 1))
|
||||
;; (lambda (params) body) → reconstruct Lambda
|
||||
(and (list? val) (not (empty? val))
|
||||
(symbol? (first val))
|
||||
(= (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)))
|
||||
;; Thaw all scopes from a list of frozen scope dicts
|
||||
(define cek-thaw-all :effects [mutation]
|
||||
(fn (frozen-list)
|
||||
(for-each (fn (frozen)
|
||||
(cek-thaw-scope (get frozen "name") frozen))
|
||||
frozen-list)))
|
||||
|
||||
(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)))
|
||||
;; Serialize a frozen scope to SX text
|
||||
(define freeze-to-sx :effects []
|
||||
(fn (name)
|
||||
(sx-serialize (cek-freeze-scope name))))
|
||||
|
||||
;; Restore from SX text
|
||||
(define thaw-from-sx :effects [mutation]
|
||||
(fn (sx-text)
|
||||
(let ((parsed (sx-parse sx-text)))
|
||||
(when (not (empty? parsed))
|
||||
(let ((frozen (first parsed)))
|
||||
(cek-thaw-scope (get frozen "name") frozen))))))
|
||||
|
||||
(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")))))
|
||||
|
||||
@@ -4595,109 +4595,69 @@ 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']
|
||||
# freeze-registry
|
||||
freeze_registry = {}
|
||||
|
||||
# cek-serialize-value
|
||||
def cek_serialize_value(val):
|
||||
if sx_truthy(is_nil(val)):
|
||||
# freeze-signal
|
||||
def freeze_signal(name, sig):
|
||||
scope_name = sx_context('sx-freeze-scope', NIL)
|
||||
if sx_truthy(scope_name):
|
||||
entries = (get(freeze_registry, scope_name) if sx_truthy(get(freeze_registry, scope_name)) else [])
|
||||
entries.append({'name': name, 'signal': sig})
|
||||
return _sx_dict_set(freeze_registry, scope_name, entries)
|
||||
return NIL
|
||||
|
||||
# freeze-scope
|
||||
def freeze_scope(name, body_fn):
|
||||
scope_push('sx-freeze-scope', name)
|
||||
freeze_registry[name] = []
|
||||
cek_call(body_fn, NIL)
|
||||
scope_pop('sx-freeze-scope')
|
||||
return NIL
|
||||
|
||||
# cek-freeze-scope
|
||||
def cek_freeze_scope(name):
|
||||
entries = (get(freeze_registry, name) if sx_truthy(get(freeze_registry, name)) else [])
|
||||
signals_dict = {}
|
||||
for entry in entries:
|
||||
signals_dict[get(entry, 'name')] = signal_value(get(entry, 'signal'))
|
||||
return {'name': name, 'signals': signals_dict}
|
||||
|
||||
# cek-freeze-all
|
||||
def cek_freeze_all():
|
||||
return map(lambda name: cek_freeze_scope(name), keys(freeze_registry))
|
||||
|
||||
# cek-thaw-scope
|
||||
def cek_thaw_scope(name, frozen):
|
||||
entries = (get(freeze_registry, name) if sx_truthy(get(freeze_registry, name)) else [])
|
||||
values = get(frozen, 'signals')
|
||||
if sx_truthy(values):
|
||||
for entry in entries:
|
||||
sig_name = get(entry, 'name')
|
||||
sig = get(entry, 'signal')
|
||||
val = get(values, sig_name)
|
||||
if sx_truthy((not sx_truthy(is_nil(val)))):
|
||||
reset_b(sig, val)
|
||||
return NIL
|
||||
elif sx_truthy(number_p(val)):
|
||||
return val
|
||||
elif sx_truthy(string_p(val)):
|
||||
return val
|
||||
elif sx_truthy(boolean_p(val)):
|
||||
return val
|
||||
elif sx_truthy(symbol_p(val)):
|
||||
return val
|
||||
elif sx_truthy(keyword_p(val)):
|
||||
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)
|
||||
return NIL
|
||||
|
||||
# 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-thaw-all
|
||||
def cek_thaw_all(frozen_list):
|
||||
for frozen in frozen_list:
|
||||
cek_thaw_scope(get(frozen, 'name'), frozen)
|
||||
return NIL
|
||||
|
||||
# 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
|
||||
# freeze-to-sx
|
||||
def freeze_to_sx(name):
|
||||
return sx_serialize(cek_freeze_scope(name))
|
||||
|
||||
# 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(boolean_p(val)):
|
||||
return val
|
||||
elif sx_truthy(symbol_p(val)):
|
||||
return val
|
||||
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 (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 (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)
|
||||
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'))}
|
||||
# thaw-from-sx
|
||||
def thaw_from_sx(sx_text):
|
||||
parsed = sx_parse(sx_text)
|
||||
if sx_truthy((not sx_truthy(empty_p(parsed)))):
|
||||
frozen = first(parsed)
|
||||
return cek_thaw_scope(get(frozen, 'name'), frozen)
|
||||
return NIL
|
||||
|
||||
|
||||
# === Transpiled from signals (reactive signal runtime) ===
|
||||
|
||||
Reference in New Issue
Block a user