Compare commits
6 Commits
4dd9968264
...
e2940e1c5f
| Author | SHA1 | Date | |
|---|---|---|---|
| e2940e1c5f | |||
| f7debec7c6 | |||
| 488fc53fda | |||
| cb4f4b85e5 | |||
| a759f4da3b | |||
| b03c84b962 |
@@ -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-15T00:27:14Z";
|
||||
|
||||
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"];
|
||||
@@ -748,6 +757,12 @@
|
||||
var charFromCode = PRIMITIVES["char-from-code"];
|
||||
|
||||
|
||||
// String/number utilities needed by transpiled spec code (content-hash etc)
|
||||
PRIMITIVES["char-code-at"] = function(s, i) { return s.charCodeAt(i); };
|
||||
var charCodeAt = PRIMITIVES["char-code-at"];
|
||||
PRIMITIVES["to-hex"] = function(n) { return (n >>> 0).toString(16); };
|
||||
var toHex = PRIMITIVES["to-hex"];
|
||||
|
||||
// =========================================================================
|
||||
// Platform: CEK module — explicit CEK machine
|
||||
// =========================================================================
|
||||
@@ -5283,6 +5298,111 @@ PRIMITIVES["eval-expr-cek"] = evalExprCek;
|
||||
var trampolineCek = function(val) { return (isSxTruthy(isThunk(val)) ? evalExprCek(thunkExpr(val), thunkEnv(val)) : val); };
|
||||
PRIMITIVES["trampoline-cek"] = trampolineCek;
|
||||
|
||||
// freeze-registry
|
||||
var freezeRegistry = {};
|
||||
PRIMITIVES["freeze-registry"] = freezeRegistry;
|
||||
|
||||
// freeze-signal
|
||||
var freezeSignal = function(name, sig) { return (function() {
|
||||
var scopeName = sxContext("sx-freeze-scope", NIL);
|
||||
return (isSxTruthy(scopeName) ? (function() {
|
||||
var entries = sxOr(get(freezeRegistry, scopeName), []);
|
||||
entries.push({["name"]: name, ["signal"]: sig});
|
||||
return dictSet(freezeRegistry, scopeName, entries);
|
||||
})() : NIL);
|
||||
})(); };
|
||||
PRIMITIVES["freeze-signal"] = freezeSignal;
|
||||
|
||||
// freeze-scope
|
||||
var freezeScope = function(name, bodyFn) { scopePush("sx-freeze-scope", name);
|
||||
freezeRegistry[name] = [];
|
||||
cekCall(bodyFn, NIL);
|
||||
scopePop("sx-freeze-scope");
|
||||
return NIL; };
|
||||
PRIMITIVES["freeze-scope"] = freezeScope;
|
||||
|
||||
// cek-freeze-scope
|
||||
var cekFreezeScope = function(name) { return (function() {
|
||||
var entries = sxOr(get(freezeRegistry, name), []);
|
||||
var signalsDict = {};
|
||||
{ var _c = entries; for (var _i = 0; _i < _c.length; _i++) { var entry = _c[_i]; signalsDict[get(entry, "name")] = signalValue(get(entry, "signal")); } }
|
||||
return {["name"]: name, ["signals"]: signalsDict};
|
||||
})(); };
|
||||
PRIMITIVES["cek-freeze-scope"] = cekFreezeScope;
|
||||
|
||||
// cek-freeze-all
|
||||
var cekFreezeAll = function() { return map(function(name) { return cekFreezeScope(name); }, keys(freezeRegistry)); };
|
||||
PRIMITIVES["cek-freeze-all"] = cekFreezeAll;
|
||||
|
||||
// cek-thaw-scope
|
||||
var cekThawScope = function(name, frozen) { return (function() {
|
||||
var entries = sxOr(get(freezeRegistry, name), []);
|
||||
var values = get(frozen, "signals");
|
||||
return (isSxTruthy(values) ? forEach(function(entry) { return (function() {
|
||||
var sigName = get(entry, "name");
|
||||
var sig = get(entry, "signal");
|
||||
var val = get(values, sigName);
|
||||
return (isSxTruthy(!isSxTruthy(isNil(val))) ? reset_b(sig, val) : NIL);
|
||||
})(); }, entries) : NIL);
|
||||
})(); };
|
||||
PRIMITIVES["cek-thaw-scope"] = cekThawScope;
|
||||
|
||||
// cek-thaw-all
|
||||
var cekThawAll = function(frozenList) { return forEach(function(frozen) { return cekThawScope(get(frozen, "name"), frozen); }, frozenList); };
|
||||
PRIMITIVES["cek-thaw-all"] = cekThawAll;
|
||||
|
||||
// freeze-to-sx
|
||||
var freezeToSx = function(name) { return sxSerialize(cekFreezeScope(name)); };
|
||||
PRIMITIVES["freeze-to-sx"] = freezeToSx;
|
||||
|
||||
// thaw-from-sx
|
||||
var thawFromSx = function(sxText) { return (function() {
|
||||
var parsed = sxParse(sxText);
|
||||
return (isSxTruthy(!isSxTruthy(isEmpty(parsed))) ? (function() {
|
||||
var frozen = first(parsed);
|
||||
return cekThawScope(get(frozen, "name"), frozen);
|
||||
})() : NIL);
|
||||
})(); };
|
||||
PRIMITIVES["thaw-from-sx"] = thawFromSx;
|
||||
|
||||
// content-store
|
||||
var contentStore = {};
|
||||
PRIMITIVES["content-store"] = contentStore;
|
||||
|
||||
// content-hash
|
||||
var contentHash = function(sxText) { return (function() {
|
||||
var hash = 5381;
|
||||
{ var _c = range(0, len(sxText)); for (var _i = 0; _i < _c.length; _i++) { var i = _c[_i]; hash = (((hash * 33) + charCodeAt(sxText, i)) % 4294967296); } }
|
||||
return toHex(hash);
|
||||
})(); };
|
||||
PRIMITIVES["content-hash"] = contentHash;
|
||||
|
||||
// content-put
|
||||
var contentPut = function(sxText) { return (function() {
|
||||
var cid = contentHash(sxText);
|
||||
contentStore[cid] = sxText;
|
||||
return cid;
|
||||
})(); };
|
||||
PRIMITIVES["content-put"] = contentPut;
|
||||
|
||||
// content-get
|
||||
var contentGet = function(cid) { return get(contentStore, cid); };
|
||||
PRIMITIVES["content-get"] = contentGet;
|
||||
|
||||
// freeze-to-cid
|
||||
var freezeToCid = function(scopeName) { return (function() {
|
||||
var sxText = freezeToSx(scopeName);
|
||||
return contentPut(sxText);
|
||||
})(); };
|
||||
PRIMITIVES["freeze-to-cid"] = freezeToCid;
|
||||
|
||||
// thaw-from-cid
|
||||
var thawFromCid = function(cid) { return (function() {
|
||||
var sxText = contentGet(cid);
|
||||
return (isSxTruthy(sxText) ? (thawFromSx(sxText), true) : NIL);
|
||||
})(); };
|
||||
PRIMITIVES["thaw-from-cid"] = thawFromCid;
|
||||
|
||||
|
||||
// === Transpiled from signals (reactive signal runtime) ===
|
||||
|
||||
@@ -5681,6 +5801,20 @@ PRIMITIVES["resource"] = resource;
|
||||
PRIMITIVES["is-html-tag?"] = function(n) { return HTML_TAGS.indexOf(n) >= 0; };
|
||||
PRIMITIVES["make-env"] = function() { return merge(componentEnv, PRIMITIVES); };
|
||||
|
||||
// localStorage — defined here (before boot) so islands can use at hydration
|
||||
PRIMITIVES["local-storage-get"] = function(key) {
|
||||
try { var v = localStorage.getItem(key); return v === null ? NIL : v; }
|
||||
catch (e) { return NIL; }
|
||||
};
|
||||
PRIMITIVES["local-storage-set"] = function(key, val) {
|
||||
try { localStorage.setItem(key, val); } catch (e) {}
|
||||
return NIL;
|
||||
};
|
||||
PRIMITIVES["local-storage-remove"] = function(key) {
|
||||
try { localStorage.removeItem(key); } catch (e) {}
|
||||
return NIL;
|
||||
};
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Platform interface — DOM adapter (browser-only)
|
||||
@@ -6943,6 +7077,7 @@ PRIMITIVES["resource"] = resource;
|
||||
function localStorageRemove(key) {
|
||||
try { localStorage.removeItem(key); } catch (e) {}
|
||||
}
|
||||
// localStorage primitives registered in CEK_FIXUPS_JS for ordering
|
||||
|
||||
// --- Cookies ---
|
||||
|
||||
|
||||
@@ -1032,3 +1032,147 @@
|
||||
(if (thunk? val)
|
||||
(eval-expr-cek (thunk-expr val) (thunk-env val))
|
||||
val)))
|
||||
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 13. Freeze scopes — named serializable state boundaries
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; 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.
|
||||
;;
|
||||
;; 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
|
||||
|
||||
;; Registry of freeze scopes: name → list of {name signal} entries
|
||||
(define freeze-registry (dict))
|
||||
|
||||
;; 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))))))
|
||||
|
||||
;; 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))
|
||||
|
||||
;; 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))))
|
||||
|
||||
;; Freeze all scopes
|
||||
(define cek-freeze-all :effects []
|
||||
(fn ()
|
||||
(map (fn (name) (cek-freeze-scope name))
|
||||
(keys freeze-registry))))
|
||||
|
||||
;; 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)))))
|
||||
|
||||
;; 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)))
|
||||
|
||||
;; 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))))))
|
||||
|
||||
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 14. Content-addressed computation
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Hash frozen SX to a content identifier. Store and retrieve by CID.
|
||||
;; The content IS the address — same SX always produces the same CID.
|
||||
;;
|
||||
;; Uses an in-memory content store. Applications can persist to
|
||||
;; localStorage or IPFS by providing their own store backend.
|
||||
|
||||
(define content-store (dict))
|
||||
|
||||
(define content-hash :effects []
|
||||
(fn (sx-text)
|
||||
;; djb2 hash → hex string. Simple, deterministic, fast.
|
||||
;; Real deployment would use SHA-256 / multihash.
|
||||
(let ((hash 5381))
|
||||
(for-each (fn (i)
|
||||
(set! hash (mod (+ (* hash 33) (char-code-at sx-text i)) 4294967296)))
|
||||
(range 0 (len sx-text)))
|
||||
(to-hex hash))))
|
||||
|
||||
(define content-put :effects [mutation]
|
||||
(fn (sx-text)
|
||||
(let ((cid (content-hash sx-text)))
|
||||
(dict-set! content-store cid sx-text)
|
||||
cid)))
|
||||
|
||||
(define content-get :effects []
|
||||
(fn (cid)
|
||||
(get content-store cid)))
|
||||
|
||||
;; Freeze a scope → store → return CID
|
||||
(define freeze-to-cid :effects [mutation]
|
||||
(fn (scope-name)
|
||||
(let ((sx-text (freeze-to-sx scope-name)))
|
||||
(content-put sx-text))))
|
||||
|
||||
;; Thaw from CID → look up → restore
|
||||
(define thaw-from-cid :effects [mutation]
|
||||
(fn (cid)
|
||||
(let ((sx-text (content-get cid)))
|
||||
(when sx-text
|
||||
(thaw-from-sx sx-text)
|
||||
true))))
|
||||
|
||||
@@ -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"];
|
||||
@@ -1472,6 +1481,12 @@ PLATFORM_JS_POST = '''
|
||||
|
||||
|
||||
PLATFORM_CEK_JS = '''
|
||||
// String/number utilities needed by transpiled spec code (content-hash etc)
|
||||
PRIMITIVES["char-code-at"] = function(s, i) { return s.charCodeAt(i); };
|
||||
var charCodeAt = PRIMITIVES["char-code-at"];
|
||||
PRIMITIVES["to-hex"] = function(n) { return (n >>> 0).toString(16); };
|
||||
var toHex = PRIMITIVES["to-hex"];
|
||||
|
||||
// =========================================================================
|
||||
// Platform: CEK module — explicit CEK machine
|
||||
// =========================================================================
|
||||
@@ -1522,6 +1537,20 @@ CEK_FIXUPS_JS = '''
|
||||
PRIMITIVES["make-symbol"] = function(n) { return new Symbol(n); };
|
||||
PRIMITIVES["is-html-tag?"] = function(n) { return HTML_TAGS.indexOf(n) >= 0; };
|
||||
PRIMITIVES["make-env"] = function() { return merge(componentEnv, PRIMITIVES); };
|
||||
|
||||
// localStorage — defined here (before boot) so islands can use at hydration
|
||||
PRIMITIVES["local-storage-get"] = function(key) {
|
||||
try { var v = localStorage.getItem(key); return v === null ? NIL : v; }
|
||||
catch (e) { return NIL; }
|
||||
};
|
||||
PRIMITIVES["local-storage-set"] = function(key, val) {
|
||||
try { localStorage.setItem(key, val); } catch (e) {}
|
||||
return NIL;
|
||||
};
|
||||
PRIMITIVES["local-storage-remove"] = function(key) {
|
||||
try { localStorage.removeItem(key); } catch (e) {}
|
||||
return NIL;
|
||||
};
|
||||
'''
|
||||
|
||||
|
||||
@@ -2903,6 +2932,7 @@ PLATFORM_BOOT_JS = """
|
||||
function localStorageRemove(key) {
|
||||
try { localStorage.removeItem(key); } catch (e) {}
|
||||
}
|
||||
// localStorage primitives registered in CEK_FIXUPS_JS for ordering
|
||||
|
||||
// --- Cookies ---
|
||||
|
||||
|
||||
@@ -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,7 +1013,17 @@ 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"]
|
||||
PRIMITIVES["char-code-at"] = lambda s, i: ord(s[int(i)]) if 0 <= int(i) < len(s) else 0
|
||||
PRIMITIVES["to-hex"] = lambda n: hex(int(n) & 0xFFFFFFFF)[2:]
|
||||
char_code_at = PRIMITIVES["char-code-at"]
|
||||
to_hex = PRIMITIVES["to-hex"]
|
||||
index_of = PRIMITIVES["index-of"]
|
||||
lower = PRIMITIVES["lower"]
|
||||
char_from_code = PRIMITIVES["char-from-code"]
|
||||
|
||||
@@ -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,7 +933,17 @@ 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"]
|
||||
PRIMITIVES["char-code-at"] = lambda s, i: ord(s[int(i)]) if 0 <= int(i) < len(s) else 0
|
||||
PRIMITIVES["to-hex"] = lambda n: hex(int(n) & 0xFFFFFFFF)[2:]
|
||||
char_code_at = PRIMITIVES["char-code-at"]
|
||||
to_hex = PRIMITIVES["to-hex"]
|
||||
index_of = PRIMITIVES["index-of"]
|
||||
lower = PRIMITIVES["lower"]
|
||||
char_from_code = PRIMITIVES["char-from-code"]
|
||||
@@ -4586,6 +4599,104 @@ def trampoline_cek(val):
|
||||
else:
|
||||
return val
|
||||
|
||||
# freeze-registry
|
||||
freeze_registry = {}
|
||||
|
||||
# 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
|
||||
return NIL
|
||||
|
||||
# cek-thaw-all
|
||||
def cek_thaw_all(frozen_list):
|
||||
for frozen in frozen_list:
|
||||
cek_thaw_scope(get(frozen, 'name'), frozen)
|
||||
return NIL
|
||||
|
||||
# freeze-to-sx
|
||||
def freeze_to_sx(name):
|
||||
return sx_serialize(cek_freeze_scope(name))
|
||||
|
||||
# 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
|
||||
|
||||
# content-store
|
||||
content_store = {}
|
||||
|
||||
# content-hash
|
||||
def content_hash(sx_text):
|
||||
_cells = {}
|
||||
_cells['hash'] = 5381
|
||||
for i in range(0, len(sx_text)):
|
||||
_cells['hash'] = (((_cells['hash'] * 33) + char_code_at(sx_text, i)) % 4294967296)
|
||||
return to_hex(_cells['hash'])
|
||||
|
||||
# content-put
|
||||
def content_put(sx_text):
|
||||
cid = content_hash(sx_text)
|
||||
content_store[cid] = sx_text
|
||||
return cid
|
||||
|
||||
# content-get
|
||||
def content_get(cid):
|
||||
return get(content_store, cid)
|
||||
|
||||
# freeze-to-cid
|
||||
def freeze_to_cid(scope_name):
|
||||
sx_text = freeze_to_sx(scope_name)
|
||||
return content_put(sx_text)
|
||||
|
||||
# thaw-from-cid
|
||||
def thaw_from_cid(cid):
|
||||
sx_text = content_get(cid)
|
||||
if sx_truthy(sx_text):
|
||||
thaw_from_sx(sx_text)
|
||||
return True
|
||||
return NIL
|
||||
|
||||
|
||||
# === Transpiled from signals (reactive signal runtime) ===
|
||||
|
||||
|
||||
@@ -473,6 +473,301 @@
|
||||
|
||||
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; CEK Freeze / Thaw — serializable computation
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defisland ~geography/cek/content-address-demo ()
|
||||
(let ((count (signal 0))
|
||||
(name (signal "world"))
|
||||
(cid-display (signal ""))
|
||||
(cid-input (signal ""))
|
||||
(cid-history (signal (list)))
|
||||
(status (signal "")))
|
||||
;; Register in freeze scope
|
||||
(freeze-scope "ca-demo" (fn ()
|
||||
(freeze-signal "count" count)
|
||||
(freeze-signal "name" name)))
|
||||
(div (~cssx/tw :tokens "space-y-4")
|
||||
;; Interactive widget
|
||||
(div (~cssx/tw :tokens "flex gap-4 items-center")
|
||||
(div (~cssx/tw :tokens "flex items-center gap-2")
|
||||
(button :on-click (fn (e) (swap! count dec))
|
||||
(~cssx/tw :tokens "px-2 py-1 rounded bg-stone-200 text-stone-700 hover:bg-stone-300") "-")
|
||||
(span (~cssx/tw :tokens "font-mono text-lg font-bold w-8 text-center") (deref count))
|
||||
(button :on-click (fn (e) (swap! count inc))
|
||||
(~cssx/tw :tokens "px-2 py-1 rounded bg-stone-200 text-stone-700 hover:bg-stone-300") "+"))
|
||||
(input :type "text" :bind name
|
||||
(~cssx/tw :tokens "px-3 py-1 rounded border border-stone-300 font-mono text-sm")))
|
||||
;; Live output
|
||||
(div (~cssx/tw :tokens "rounded bg-violet-50 border border-violet-200 p-3 text-violet-800")
|
||||
"Hello, " (deref name) "! Count = " (deref count))
|
||||
;; Content address button
|
||||
(div (~cssx/tw :tokens "flex gap-2")
|
||||
(button :on-click (fn (e)
|
||||
(let ((cid (freeze-to-cid "ca-demo")))
|
||||
(reset! cid-display cid)
|
||||
(reset! status (str "Stored as " cid))
|
||||
(swap! cid-history (fn (h) (append h (list cid))))))
|
||||
(~cssx/tw :tokens "px-3 py-1.5 rounded bg-stone-700 text-white text-sm hover:bg-stone-800")
|
||||
"Content-address"))
|
||||
;; CID display
|
||||
(when (not (empty? (deref cid-display)))
|
||||
(div (~cssx/tw :tokens "font-mono text-sm bg-stone-50 rounded p-2 flex items-center gap-2")
|
||||
(span (~cssx/tw :tokens "text-stone-400") "CID:")
|
||||
(span (~cssx/tw :tokens "text-violet-700 font-bold") (deref cid-display))))
|
||||
;; Status
|
||||
(when (not (empty? (deref status)))
|
||||
(div (~cssx/tw :tokens "text-xs text-emerald-600") (deref status)))
|
||||
;; Restore from CID
|
||||
(div (~cssx/tw :tokens "flex gap-2 items-end")
|
||||
(div (~cssx/tw :tokens "flex-1")
|
||||
(label (~cssx/tw :tokens "text-xs text-stone-400 block mb-1") "Restore from CID")
|
||||
(input :type "text" :bind cid-input
|
||||
(~cssx/tw :tokens "w-full px-3 py-1 rounded border border-stone-300 font-mono text-sm")))
|
||||
(button :on-click (fn (e)
|
||||
(if (thaw-from-cid (deref cid-input))
|
||||
(reset! status (str "Restored from " (deref cid-input)))
|
||||
(reset! status (str "CID not found: " (deref cid-input)))))
|
||||
(~cssx/tw :tokens "px-3 py-1.5 rounded bg-emerald-600 text-white text-sm hover:bg-emerald-700")
|
||||
"Restore"))
|
||||
;; CID history
|
||||
(when (not (empty? (deref cid-history)))
|
||||
(div (~cssx/tw :tokens "space-y-1")
|
||||
(label (~cssx/tw :tokens "text-xs text-stone-400 block") "History")
|
||||
(map (fn (cid)
|
||||
(button :on-click (fn (e)
|
||||
(if (thaw-from-cid cid)
|
||||
(reset! status (str "Restored from " cid))
|
||||
(reset! status (str "CID not found: " cid))))
|
||||
(~cssx/tw :tokens "block w-full text-left px-2 py-1 rounded bg-stone-50 hover:bg-stone-100 font-mono text-xs text-stone-600")
|
||||
cid))
|
||||
(deref cid-history)))))))
|
||||
|
||||
(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 "Content addressing" :id "content-addressing"
|
||||
(p "Hash the frozen SX " (code "\u2192") " content identifier. "
|
||||
"Same state always produces the same CID. Store by CID, retrieve by CID, verify by CID.")
|
||||
(~docs/code :code (highlight
|
||||
"(freeze-to-cid \"widget\")\n;; => \"d9eea67b\"\n\n(thaw-from-cid \"d9eea67b\")\n;; Signals restored. Same CID = same state."
|
||||
"lisp"))
|
||||
(p "Try it: change the values, click Content-address. Copy the CID. "
|
||||
"Change the values again. Paste the CID and Restore.")
|
||||
(~geography/cek/content-address-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 ((bg (signal "violet"))
|
||||
(size (signal "text-2xl"))
|
||||
(weight (signal "font-bold"))
|
||||
(text (signal "the joy of sx"))
|
||||
(saved (signal (list))))
|
||||
;; Register in freeze scope
|
||||
(freeze-scope "widget" (fn ()
|
||||
(freeze-signal "bg" bg)
|
||||
(freeze-signal "size" size)
|
||||
(freeze-signal "weight" weight)
|
||||
(freeze-signal "text" text)))
|
||||
;; Preload all dynamic colour variants
|
||||
(span (~cssx/tw :tokens "hidden bg-violet-50 bg-rose-50 bg-emerald-50 bg-amber-50 bg-sky-50 bg-stone-50 border-violet-200 border-rose-200 border-emerald-200 border-amber-200 border-sky-200 border-stone-200 text-violet-700 text-rose-700 text-emerald-700 text-amber-700 text-sky-700 text-stone-700"))
|
||||
(div (~cssx/tw :tokens "space-y-4")
|
||||
;; Live preview
|
||||
(div (~cssx/tw :tokens "p-6 rounded-lg text-center transition-all border")
|
||||
:class (str "bg-" (deref bg) "-50 border-" (deref bg) "-200")
|
||||
(span :class (str (deref size) " " (deref weight) " text-" (deref bg) "-700")
|
||||
(deref text)))
|
||||
;; Controls
|
||||
(div (~cssx/tw :tokens "grid grid-cols-2 gap-3")
|
||||
(div
|
||||
(label (~cssx/tw :tokens "text-xs text-stone-400 block mb-1") "Colour")
|
||||
(div (~cssx/tw :tokens "flex gap-1")
|
||||
(button :on-click (fn (e) (reset! bg "violet"))
|
||||
(~cssx/tw :tokens "w-8 h-8 rounded-full")
|
||||
:class (str "bg-violet-400" (if (= (deref bg) "violet") " ring-2 ring-offset-1 ring-stone-400" ""))
|
||||
"")
|
||||
(button :on-click (fn (e) (reset! bg "rose"))
|
||||
(~cssx/tw :tokens "w-8 h-8 rounded-full")
|
||||
:class (str "bg-rose-400" (if (= (deref bg) "rose") " ring-2 ring-offset-1 ring-stone-400" ""))
|
||||
"")
|
||||
(button :on-click (fn (e) (reset! bg "emerald"))
|
||||
(~cssx/tw :tokens "w-8 h-8 rounded-full")
|
||||
:class (str "bg-emerald-400" (if (= (deref bg) "emerald") " ring-2 ring-offset-1 ring-stone-400" ""))
|
||||
"")
|
||||
(button :on-click (fn (e) (reset! bg "amber"))
|
||||
(~cssx/tw :tokens "w-8 h-8 rounded-full")
|
||||
:class (str "bg-amber-400" (if (= (deref bg) "amber") " ring-2 ring-offset-1 ring-stone-400" ""))
|
||||
"")
|
||||
(button :on-click (fn (e) (reset! bg "sky"))
|
||||
(~cssx/tw :tokens "w-8 h-8 rounded-full")
|
||||
:class (str "bg-sky-400" (if (= (deref bg) "sky") " ring-2 ring-offset-1 ring-stone-400" ""))
|
||||
"")
|
||||
(button :on-click (fn (e) (reset! bg "stone"))
|
||||
(~cssx/tw :tokens "w-8 h-8 rounded-full")
|
||||
:class (str "bg-stone-400" (if (= (deref bg) "stone") " ring-2 ring-offset-1 ring-stone-400" ""))
|
||||
"")))
|
||||
(div
|
||||
(label (~cssx/tw :tokens "text-xs text-stone-400 block mb-1") "Size")
|
||||
(div (~cssx/tw :tokens "flex gap-1")
|
||||
(map (fn (s)
|
||||
(button :on-click (fn (e) (reset! size s))
|
||||
(~cssx/tw :tokens "px-2 py-1 rounded text-xs")
|
||||
:class (if (= (deref size) s)
|
||||
"bg-stone-700 text-white" "bg-stone-100 text-stone-600")
|
||||
s))
|
||||
(list "text-sm" "text-lg" "text-2xl" "text-4xl"))))
|
||||
(div
|
||||
(label (~cssx/tw :tokens "text-xs text-stone-400 block mb-1") "Weight")
|
||||
(div (~cssx/tw :tokens "flex gap-1")
|
||||
(map (fn (w)
|
||||
(button :on-click (fn (e) (reset! weight w))
|
||||
(~cssx/tw :tokens "px-2 py-1 rounded text-xs")
|
||||
:class (if (= (deref weight) w)
|
||||
"bg-stone-700 text-white" "bg-stone-100 text-stone-600")
|
||||
w))
|
||||
(list "font-normal" "font-bold" "font-semibold"))))
|
||||
(div
|
||||
(label (~cssx/tw :tokens "text-xs text-stone-400 block mb-1") "Text")
|
||||
(input :type "text" :bind text
|
||||
(~cssx/tw :tokens "w-full px-2 py-1 rounded border border-stone-300 text-sm"))))
|
||||
;; Freeze / Thaw
|
||||
(div (~cssx/tw :tokens "flex gap-2 items-center")
|
||||
(button :on-click (fn (e)
|
||||
(let ((sx (freeze-to-sx "widget")))
|
||||
(swap! saved (fn (l) (append l (list sx))))))
|
||||
(~cssx/tw :tokens "px-3 py-1.5 rounded bg-amber-500 text-white text-sm hover:bg-amber-600")
|
||||
"Save config")
|
||||
(span (~cssx/tw :tokens "text-xs text-stone-400")
|
||||
(str (len (deref saved)) " saved")))
|
||||
;; Saved configs
|
||||
(when (not (empty? (deref saved)))
|
||||
(div (~cssx/tw :tokens "space-y-1")
|
||||
(label (~cssx/tw :tokens "text-xs text-stone-400 block") "Saved configs")
|
||||
(map-indexed (fn (i sx)
|
||||
(button :on-click (fn (e) (thaw-from-sx sx))
|
||||
(~cssx/tw :tokens "block w-full text-left px-2 py-1 rounded bg-stone-50 hover:bg-stone-100 font-mono text-xs text-stone-600 truncate")
|
||||
(str "Config " (+ i 1))))
|
||||
(deref saved)))))))
|
||||
|
||||
|
||||
|
||||
|
||||
(defcomp ~geography/cek/cek-content-address-content ()
|
||||
(~docs/page :title "Content-Addressed Computation"
|
||||
|
||||
(p :class "text-stone-500 text-sm italic mb-8"
|
||||
"A computation is a value. A value has a hash. The hash is the address. "
|
||||
"Same state, same address, forever.")
|
||||
|
||||
(~docs/section :title "The idea" :id "idea"
|
||||
(p "Freeze a scope to SX. Hash the SX text. The hash is a content identifier (CID). "
|
||||
"Store the frozen SX keyed by CID. Later, look up the CID, thaw, resume.")
|
||||
(p "The critical property: "
|
||||
(strong "same state always produces the same CID") ". "
|
||||
"Two machines freezing identical signal values get identical CIDs. "
|
||||
"The address IS the content."))
|
||||
|
||||
(~docs/section :title "How it works" :id "how"
|
||||
(~docs/code :code (highlight
|
||||
";; Freeze a scope \u2192 hash \u2192 CID\n(freeze-to-cid \"widget\")\n;; => \"d9eea67b\"\n\n;; The frozen SX is stored by CID\n(content-get \"d9eea67b\")\n;; => {:name \"widget\" :signals {:count 42 :name \"hello\"}}\n\n;; Thaw from CID \u2192 signals restored\n(thaw-from-cid \"d9eea67b\")\n;; Signals reset to frozen values"
|
||||
"lisp"))
|
||||
(p "The hash is djb2 for now \u2014 deterministic and fast. "
|
||||
"Real deployment uses SHA-256 / multihash for IPFS compatibility."))
|
||||
|
||||
(~docs/section :title "Why it matters" :id "why"
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-2 text-stone-600"
|
||||
(li (strong "Sharing") " \u2014 send a CID, not a blob. "
|
||||
"The receiver looks up the CID and gets the exact state.")
|
||||
(li (strong "Deduplication") " \u2014 same state = same CID. "
|
||||
"Store once, reference many times.")
|
||||
(li (strong "Verification") " \u2014 re-freeze, compare CIDs. "
|
||||
"If they match, the state is identical. No diffing needed.")
|
||||
(li (strong "History") " \u2014 each CID is a snapshot. "
|
||||
"A sequence of CIDs is a complete undo history.")
|
||||
(li (strong "Distribution") " \u2014 CIDs on IPFS are global. "
|
||||
"Pin a widget state, anyone can thaw it. "
|
||||
"No server, no API, no account.")))
|
||||
|
||||
(~docs/section :title "Live demo" :id "demo"
|
||||
(p "Change the counter and name. Click " (strong "Content-address") " to freeze and hash. "
|
||||
"The CID appears below. Change the values, then click any CID in the history "
|
||||
"or paste one into the input to restore.")
|
||||
(~geography/cek/content-address-demo))
|
||||
|
||||
(~docs/section :title "The path to IPFS" :id "ipfs"
|
||||
(p "The content store is currently in-memory. The next steps:")
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1 text-stone-600"
|
||||
(li "Replace djb2 with SHA-256 (browser SubtleCrypto)")
|
||||
(li "Wrap in multihash + CIDv1 format")
|
||||
(li "Store to IPFS via the Art DAG L2 registry")
|
||||
(li "Pin CIDs attributed to " (code "sx-web.org"))
|
||||
(li "Anyone can pin, fork, extend"))
|
||||
(p "The frozen SX is the content. The CID is the address. "
|
||||
"IPFS is the network. The widget state becomes a permanent, "
|
||||
"verifiable, shareable artifact \u2014 not trapped in a database, "
|
||||
"not behind an API, not owned by anyone."))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Demo page content
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
@@ -174,7 +174,8 @@
|
||||
(when (and parent rendered)
|
||||
(dom-append parent rendered)))))
|
||||
(swap! step-idx inc)
|
||||
(update-code-highlight))))
|
||||
(update-code-highlight)
|
||||
(local-storage-set "sx-home-stepper" (freeze-to-sx "home-stepper")))))
|
||||
(do-back (fn ()
|
||||
(when (> (deref step-idx) 0)
|
||||
(let ((target (- (deref step-idx) 1))
|
||||
@@ -182,7 +183,18 @@
|
||||
(when container (dom-set-prop container "innerHTML" ""))
|
||||
(set-stack (list (get-preview)))
|
||||
(reset! step-idx 0)
|
||||
(for-each (fn (_) (do-step)) (slice (deref steps) 0 target)))))))
|
||||
(for-each (fn (_) (do-step)) (slice (deref steps) 0 target))
|
||||
(local-storage-set "sx-home-stepper" (freeze-to-sx "home-stepper")))))))
|
||||
;; Freeze scope for persistence
|
||||
(freeze-scope "home-stepper" (fn ()
|
||||
(freeze-signal "step" step-idx)))
|
||||
;; Restore from localStorage on mount
|
||||
(let ((saved (local-storage-get "sx-home-stepper")))
|
||||
(when saved
|
||||
(thaw-from-sx saved)
|
||||
;; Validate — reset to default if out of range
|
||||
(when (or (< (deref step-idx) 0) (> (deref step-idx) 16))
|
||||
(reset! step-idx 9))))
|
||||
;; Auto-parse via effect
|
||||
(effect (fn ()
|
||||
(let ((parsed (sx-parse source)))
|
||||
@@ -198,7 +210,9 @@
|
||||
;; Defer code DOM build until lake exists
|
||||
(schedule-idle (fn ()
|
||||
(build-code-dom)
|
||||
;; Replay to initial step-idx
|
||||
;; Clear preview and replay to initial step-idx
|
||||
(let ((preview (get-preview)))
|
||||
(when preview (dom-set-prop preview "innerHTML" "")))
|
||||
(let ((target (deref step-idx)))
|
||||
(reset! step-idx 0)
|
||||
(set-stack (list (get-preview)))
|
||||
@@ -213,7 +227,7 @@
|
||||
;; Controls
|
||||
(div :class "flex items-center justify-center gap-2 md:gap-3"
|
||||
(button :on-click (fn (e) (do-back))
|
||||
:class (str "px-2 py-1 rounded text-lg "
|
||||
:class (str "px-2 py-1 rounded text-3xl "
|
||||
(if (> (deref step-idx) 0)
|
||||
"text-stone-600 hover:text-stone-800 hover:bg-stone-100"
|
||||
"text-stone-300 cursor-not-allowed"))
|
||||
@@ -221,7 +235,7 @@
|
||||
(span :class "text-sm text-stone-500 font-mono tabular-nums"
|
||||
(deref step-idx) " / " (len (deref steps)))
|
||||
(button :on-click (fn (e) (do-step))
|
||||
:class (str "px-2 py-1 rounded text-lg "
|
||||
:class (str "px-2 py-1 rounded text-3xl "
|
||||
(if (< (deref step-idx) (len (deref steps)))
|
||||
"text-violet-600 hover:text-violet-800 hover:bg-violet-50"
|
||||
"text-violet-300 cursor-not-allowed"))
|
||||
|
||||
@@ -174,7 +174,11 @@
|
||||
(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.")
|
||||
(dict :label "Content Addressing" :href "/sx/(geography.(cek.content))"
|
||||
:summary "Hash frozen state to a CID. Same state = same address. Store, share, verify, reproduce.")))
|
||||
|
||||
(define plans-nav-items (list
|
||||
(dict :label "Status" :href "/sx/(etc.(plan.status))"
|
||||
|
||||
@@ -66,6 +66,8 @@
|
||||
'(~geography/cek/cek-content)
|
||||
(case slug
|
||||
"demo" '(~geography/cek/cek-demo-content)
|
||||
"freeze" '(~geography/cek/cek-freeze-content)
|
||||
"content" '(~geography/cek/cek-content-address-content)
|
||||
:else '(~geography/cek/cek-content)))))
|
||||
|
||||
(define provide
|
||||
|
||||
Reference in New Issue
Block a user