Content-addressed computation: freeze → hash → CID → thaw
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Hash frozen SX to a content identifier (djb2 → hex). Same state always produces the same CID. Store by CID, retrieve by CID. - content-hash: djb2 hash of SX text → hex string - content-put/get: in-memory content store - freeze-to-cid: freeze scope → store → return CID - thaw-from-cid: look up CID → thaw signals - char-code-at / to-hex primitives for both platforms - Live demo: counter + name widget, content-address button, CID display, restore from CID input, CID history Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||||
var SX_VERSION = "2026-03-15T00:04:32Z";
|
var SX_VERSION = "2026-03-15T00:16:30Z";
|
||||||
|
|
||||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||||
@@ -5359,6 +5359,44 @@ PRIMITIVES["freeze-to-sx"] = freezeToSx;
|
|||||||
})(); };
|
})(); };
|
||||||
PRIMITIVES["thaw-from-sx"] = thawFromSx;
|
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) ===
|
// === Transpiled from signals (reactive signal runtime) ===
|
||||||
|
|
||||||
@@ -5757,6 +5795,10 @@ PRIMITIVES["resource"] = resource;
|
|||||||
PRIMITIVES["is-html-tag?"] = function(n) { return HTML_TAGS.indexOf(n) >= 0; };
|
PRIMITIVES["is-html-tag?"] = function(n) { return HTML_TAGS.indexOf(n) >= 0; };
|
||||||
PRIMITIVES["make-env"] = function() { return merge(componentEnv, PRIMITIVES); };
|
PRIMITIVES["make-env"] = function() { return merge(componentEnv, PRIMITIVES); };
|
||||||
|
|
||||||
|
// String/number utilities for content addressing
|
||||||
|
PRIMITIVES["char-code-at"] = function(s, i) { return s.charCodeAt(i); };
|
||||||
|
PRIMITIVES["to-hex"] = function(n) { return (n >>> 0).toString(16); };
|
||||||
|
|
||||||
// localStorage — defined here (before boot) so islands can use at hydration
|
// localStorage — defined here (before boot) so islands can use at hydration
|
||||||
PRIMITIVES["local-storage-get"] = function(key) {
|
PRIMITIVES["local-storage-get"] = function(key) {
|
||||||
try { var v = localStorage.getItem(key); return v === null ? NIL : v; }
|
try { var v = localStorage.getItem(key); return v === null ? NIL : v; }
|
||||||
|
|||||||
@@ -1129,3 +1129,50 @@
|
|||||||
(cek-thaw-scope (get frozen "name") frozen))))))
|
(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))))
|
||||||
|
|||||||
@@ -1532,6 +1532,10 @@ CEK_FIXUPS_JS = '''
|
|||||||
PRIMITIVES["is-html-tag?"] = function(n) { return HTML_TAGS.indexOf(n) >= 0; };
|
PRIMITIVES["is-html-tag?"] = function(n) { return HTML_TAGS.indexOf(n) >= 0; };
|
||||||
PRIMITIVES["make-env"] = function() { return merge(componentEnv, PRIMITIVES); };
|
PRIMITIVES["make-env"] = function() { return merge(componentEnv, PRIMITIVES); };
|
||||||
|
|
||||||
|
// String/number utilities for content addressing
|
||||||
|
PRIMITIVES["char-code-at"] = function(s, i) { return s.charCodeAt(i); };
|
||||||
|
PRIMITIVES["to-hex"] = function(n) { return (n >>> 0).toString(16); };
|
||||||
|
|
||||||
// localStorage — defined here (before boot) so islands can use at hydration
|
// localStorage — defined here (before boot) so islands can use at hydration
|
||||||
PRIMITIVES["local-storage-get"] = function(key) {
|
PRIMITIVES["local-storage-get"] = function(key) {
|
||||||
try { var v = localStorage.getItem(key); return v === null ? NIL : v; }
|
try { var v = localStorage.getItem(key); return v === null ? NIL : v; }
|
||||||
|
|||||||
@@ -1020,6 +1020,10 @@ number_p = PRIMITIVES["number?"]
|
|||||||
string_p = PRIMITIVES["string?"]
|
string_p = PRIMITIVES["string?"]
|
||||||
list_p = PRIMITIVES["list?"]
|
list_p = PRIMITIVES["list?"]
|
||||||
dissoc = PRIMITIVES["dissoc"]
|
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"]
|
index_of = PRIMITIVES["index-of"]
|
||||||
lower = PRIMITIVES["lower"]
|
lower = PRIMITIVES["lower"]
|
||||||
char_from_code = PRIMITIVES["char-from-code"]
|
char_from_code = PRIMITIVES["char-from-code"]
|
||||||
|
|||||||
@@ -940,6 +940,10 @@ number_p = PRIMITIVES["number?"]
|
|||||||
string_p = PRIMITIVES["string?"]
|
string_p = PRIMITIVES["string?"]
|
||||||
list_p = PRIMITIVES["list?"]
|
list_p = PRIMITIVES["list?"]
|
||||||
dissoc = PRIMITIVES["dissoc"]
|
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"]
|
index_of = PRIMITIVES["index-of"]
|
||||||
lower = PRIMITIVES["lower"]
|
lower = PRIMITIVES["lower"]
|
||||||
char_from_code = PRIMITIVES["char-from-code"]
|
char_from_code = PRIMITIVES["char-from-code"]
|
||||||
@@ -4659,6 +4663,40 @@ def thaw_from_sx(sx_text):
|
|||||||
return cek_thaw_scope(get(frozen, 'name'), frozen)
|
return cek_thaw_scope(get(frozen, 'name'), frozen)
|
||||||
return NIL
|
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) ===
|
# === Transpiled from signals (reactive signal runtime) ===
|
||||||
|
|
||||||
|
|||||||
@@ -478,6 +478,73 @@
|
|||||||
;; CEK Freeze / Thaw — serializable computation
|
;; 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")
|
||||||
|
(str "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 ()
|
(defcomp ~geography/cek/cek-freeze-content ()
|
||||||
(~docs/page :title "Freeze / Thaw"
|
(~docs/page :title "Freeze / Thaw"
|
||||||
|
|
||||||
@@ -525,6 +592,16 @@
|
|||||||
"The frozen SX appears below. Click Thaw to resume from the frozen state.")
|
"The frozen SX appears below. Click Thaw to resume from the frozen state.")
|
||||||
(~geography/cek/freeze-demo))
|
(~geography/cek/freeze-demo))
|
||||||
|
|
||||||
|
(~docs/section :title "Content addressing" :id "content-addressing"
|
||||||
|
(p "Hash the frozen SX " (~docs/inline-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"
|
(~docs/section :title "What this enables" :id "enables"
|
||||||
(ul :class "list-disc pl-6 mb-4 space-y-2 text-stone-600"
|
(ul :class "list-disc pl-6 mb-4 space-y-2 text-stone-600"
|
||||||
(li (strong "Persistence") " \u2014 save reactive island state to localStorage, "
|
(li (strong "Persistence") " \u2014 save reactive island state to localStorage, "
|
||||||
|
|||||||
Reference in New Issue
Block a user