diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 7ed36fd..d618aa1 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-15T00:04:32Z"; + var SX_VERSION = "2026-03-15T00:16:30Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -5359,6 +5359,44 @@ PRIMITIVES["freeze-to-sx"] = freezeToSx; })(); }; 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) === @@ -5757,6 +5795,10 @@ PRIMITIVES["resource"] = resource; PRIMITIVES["is-html-tag?"] = function(n) { return HTML_TAGS.indexOf(n) >= 0; }; 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 PRIMITIVES["local-storage-get"] = function(key) { try { var v = localStorage.getItem(key); return v === null ? NIL : v; } diff --git a/shared/sx/ref/cek.sx b/shared/sx/ref/cek.sx index 4343b3a..b93437f 100644 --- a/shared/sx/ref/cek.sx +++ b/shared/sx/ref/cek.sx @@ -1129,3 +1129,50 @@ (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)))) diff --git a/shared/sx/ref/platform_js.py b/shared/sx/ref/platform_js.py index a15a48d..060baf3 100644 --- a/shared/sx/ref/platform_js.py +++ b/shared/sx/ref/platform_js.py @@ -1532,6 +1532,10 @@ CEK_FIXUPS_JS = ''' PRIMITIVES["is-html-tag?"] = function(n) { return HTML_TAGS.indexOf(n) >= 0; }; 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 PRIMITIVES["local-storage-get"] = function(key) { try { var v = localStorage.getItem(key); return v === null ? NIL : v; } diff --git a/shared/sx/ref/platform_py.py b/shared/sx/ref/platform_py.py index 94e4752..55c8032 100644 --- a/shared/sx/ref/platform_py.py +++ b/shared/sx/ref/platform_py.py @@ -1020,6 +1020,10 @@ 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"] diff --git a/shared/sx/ref/sx_ref.py b/shared/sx/ref/sx_ref.py index 500c330..70a81cf 100644 --- a/shared/sx/ref/sx_ref.py +++ b/shared/sx/ref/sx_ref.py @@ -940,6 +940,10 @@ 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"] @@ -4659,6 +4663,40 @@ def thaw_from_sx(sx_text): 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) === diff --git a/sx/sx/geography/cek.sx b/sx/sx/geography/cek.sx index 230e9d7..6616c83 100644 --- a/sx/sx/geography/cek.sx +++ b/sx/sx/geography/cek.sx @@ -478,6 +478,73 @@ ;; 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 () (~docs/page :title "Freeze / Thaw" @@ -525,6 +592,16 @@ "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 " (~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" (ul :class "list-disc pl-6 mb-4 space-y-2 text-stone-600" (li (strong "Persistence") " \u2014 save reactive island state to localStorage, "