Compare commits
11 Commits
48d493e9cc
...
4dd9968264
| Author | SHA1 | Date | |
|---|---|---|---|
| 4dd9968264 | |||
| 7cc1bffc23 | |||
| 169097097c | |||
| a7638e48d5 | |||
| 93e140280b | |||
| 07bf5a1142 | |||
| 623f947b52 | |||
| 41f4772ba7 | |||
| ae1ba46b44 | |||
| 0047757af8 | |||
| b3cba5e281 |
@@ -49,6 +49,7 @@ def create_base_app(
|
||||
domain_services_fn: Callable[[], None] | None = None,
|
||||
no_oauth: bool = False,
|
||||
no_db: bool = False,
|
||||
css_extras: Sequence[str] | None = None,
|
||||
) -> Quart:
|
||||
"""
|
||||
Create a Quart app with shared infrastructure.
|
||||
@@ -139,17 +140,24 @@ def create_base_app(
|
||||
_styles = BASE_DIR / "static" / "styles"
|
||||
_fa_css = BASE_DIR / "static" / "fontawesome" / "css"
|
||||
if (_styles / "tw.css").exists() and not registry_loaded():
|
||||
load_css_registry(
|
||||
_styles / "tw.css",
|
||||
extra_css=[
|
||||
if css_extras is None:
|
||||
# Legacy default: all shared CSS for blog/market/etc apps
|
||||
_extra = [
|
||||
_styles / "basics.css",
|
||||
_styles / "cards.css",
|
||||
_styles / "blog-content.css",
|
||||
_styles / "prism.css",
|
||||
_fa_css / "all.min.css",
|
||||
_fa_css / "v4-shims.min.css",
|
||||
],
|
||||
url_rewrites={"../webfonts/": "/static/fontawesome/webfonts/"},
|
||||
]
|
||||
_rewrites = {"../webfonts/": "/static/fontawesome/webfonts/"}
|
||||
else:
|
||||
_extra = [_styles / e if "/" not in e else e for e in css_extras]
|
||||
_rewrites = {}
|
||||
load_css_registry(
|
||||
_styles / "tw.css",
|
||||
extra_css=_extra,
|
||||
url_rewrites=_rewrites,
|
||||
)
|
||||
|
||||
# Dev-mode: auto-reload sx templates when files change on disk
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -952,7 +952,7 @@
|
||||
(dom-set-attr el k (str (dict-get attrs k))))
|
||||
extra-keys)
|
||||
;; Flush any newly collected CSS rules to live stylesheet
|
||||
(flush-cssx-to-dom))
|
||||
(run-post-render-hooks))
|
||||
;; No longer a spread — clear tracked state
|
||||
(do
|
||||
(set! prev-classes (list))
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
(process-elements el)
|
||||
(sx-hydrate-elements el)
|
||||
(sx-hydrate-islands el)
|
||||
(flush-cssx-to-dom))))))
|
||||
(run-post-render-hooks))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -120,7 +120,7 @@
|
||||
(process-elements el)
|
||||
(sx-hydrate-elements el)
|
||||
(sx-hydrate-islands el)
|
||||
(flush-cssx-to-dom)
|
||||
(run-post-render-hooks)
|
||||
(dom-dispatch el "sx:resolved" {:id id})))
|
||||
(log-warn (str "resolveSuspense: no element for id=" id))))))
|
||||
|
||||
@@ -418,29 +418,34 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; CSSX live flush — inject collected CSS rules into the DOM
|
||||
;; Render hooks — generic pre/post callbacks for hydration, swap, mount.
|
||||
;; The spec calls these at render boundaries; the app decides what to do.
|
||||
;; Pre-render: setup before DOM changes (e.g. prepare state).
|
||||
;; Post-render: cleanup after DOM changes (e.g. flush collected CSS).
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; ~cssx/tw collects CSS rules via collect!("cssx" ...) during rendering.
|
||||
;; On the server, ~cssx/flush emits a batch <style> tag. On the client,
|
||||
;; islands render independently and no batch flush runs. This function
|
||||
;; injects any unflushed rules into a persistent <style> element in <head>.
|
||||
;; Called after hydration (boot + post-swap) to cover all render paths.
|
||||
|
||||
(define flush-cssx-to-dom :effects [mutation io]
|
||||
(define *pre-render-hooks* (list))
|
||||
(define *post-render-hooks* (list))
|
||||
|
||||
(define register-pre-render-hook :effects [mutation]
|
||||
(fn ((hook-fn :as lambda))
|
||||
(append! *pre-render-hooks* hook-fn)))
|
||||
|
||||
(define register-post-render-hook :effects [mutation]
|
||||
(fn ((hook-fn :as lambda))
|
||||
(append! *post-render-hooks* hook-fn)))
|
||||
|
||||
(define run-pre-render-hooks :effects [mutation io]
|
||||
(fn ()
|
||||
(let ((rules (collected "cssx")))
|
||||
(when (not (empty? rules))
|
||||
(let ((style (or (dom-query "#sx-cssx-live")
|
||||
(let ((s (dom-create-element "style" nil)))
|
||||
(dom-set-attr s "id" "sx-cssx-live")
|
||||
(dom-set-attr s "data-cssx" "")
|
||||
(dom-append-to-head s)
|
||||
s))))
|
||||
(dom-set-prop style "textContent"
|
||||
(str (or (dom-get-prop style "textContent") "")
|
||||
(join "" rules))))
|
||||
(clear-collected! "cssx")))))
|
||||
(for-each (fn (hook) (cek-call hook nil)) *pre-render-hooks*)))
|
||||
|
||||
(define run-post-render-hooks :effects [mutation io]
|
||||
(fn ()
|
||||
(log-info "run-post-render-hooks:" (len *post-render-hooks*) "hooks")
|
||||
(for-each (fn (hook)
|
||||
(log-info " hook type:" (type-of hook) "callable:" (callable? hook) "lambda:" (lambda? hook))
|
||||
(cek-call hook nil))
|
||||
*post-render-hooks*)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -464,7 +469,7 @@
|
||||
(process-sx-scripts nil)
|
||||
(sx-hydrate-elements nil)
|
||||
(sx-hydrate-islands nil)
|
||||
(flush-cssx-to-dom)
|
||||
(run-post-render-hooks)
|
||||
(process-elements nil))))
|
||||
|
||||
|
||||
|
||||
@@ -618,7 +618,7 @@
|
||||
(if (not (nil? renamed))
|
||||
renamed
|
||||
;; General mangling rules
|
||||
(let ((result name))
|
||||
(let ((result (replace name "*" "_")))
|
||||
;; Handle trailing ? and !
|
||||
(let ((result (cond
|
||||
(ends-with? result "?")
|
||||
@@ -1422,23 +1422,27 @@
|
||||
(= (keyword-name (nth expr 2)) "effects"))
|
||||
(nth expr 4)
|
||||
(nth expr 2))))
|
||||
(if (nil? val-expr)
|
||||
(str "var " (js-mangle name) " = NIL;")
|
||||
;; Detect zero-arg self-tail-recursive functions → while loops
|
||||
(if (and (list? val-expr)
|
||||
(not (empty? val-expr))
|
||||
(= (type-of (first val-expr)) "symbol")
|
||||
(or (= (symbol-name (first val-expr)) "fn")
|
||||
(= (symbol-name (first val-expr)) "lambda"))
|
||||
(list? (nth val-expr 1))
|
||||
(= (len (nth val-expr 1)) 0)
|
||||
(js-is-self-tail-recursive? name (rest (rest val-expr))))
|
||||
;; While loop optimization
|
||||
(let ((body (rest (rest val-expr)))
|
||||
(loop-body (js-emit-loop-body name body)))
|
||||
(str "var " (js-mangle name) " = function() { while(true) { " loop-body " } };"))
|
||||
;; Normal define
|
||||
(str "var " (js-mangle name) " = " (js-expr val-expr) ";"))))))
|
||||
(let ((mangled (js-mangle name))
|
||||
(var-decl
|
||||
(if (nil? val-expr)
|
||||
(str "var " (js-mangle name) " = NIL;")
|
||||
;; Detect zero-arg self-tail-recursive functions → while loops
|
||||
(if (and (list? val-expr)
|
||||
(not (empty? val-expr))
|
||||
(= (type-of (first val-expr)) "symbol")
|
||||
(or (= (symbol-name (first val-expr)) "fn")
|
||||
(= (symbol-name (first val-expr)) "lambda"))
|
||||
(list? (nth val-expr 1))
|
||||
(= (len (nth val-expr 1)) 0)
|
||||
(js-is-self-tail-recursive? name (rest (rest val-expr))))
|
||||
;; While loop optimization
|
||||
(let ((body (rest (rest val-expr)))
|
||||
(loop-body (js-emit-loop-body name body)))
|
||||
(str "var " mangled " = function() { while(true) { " loop-body " } };"))
|
||||
;; Normal define
|
||||
(str "var " mangled " = " (js-expr val-expr) ";")))))
|
||||
;; Self-register: every spec define is available to evaluated SX code
|
||||
(str var-decl "\nPRIMITIVES[\"" name "\"] = " mangled ";")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -460,7 +460,7 @@
|
||||
(sx-process-scripts root)
|
||||
(sx-hydrate root)
|
||||
(sx-hydrate-islands root)
|
||||
(flush-cssx-to-dom)
|
||||
(run-post-render-hooks)
|
||||
(process-elements root)))
|
||||
|
||||
|
||||
@@ -871,7 +871,7 @@
|
||||
(hoist-head-elements-full target)
|
||||
(process-elements target)
|
||||
(sx-hydrate-elements target)
|
||||
(flush-cssx-to-dom)
|
||||
(run-post-render-hooks)
|
||||
(dom-dispatch target "sx:clientRoute"
|
||||
(dict "pathname" pathname))
|
||||
(log-info (str "sx:route client " pathname)))))
|
||||
|
||||
@@ -1508,6 +1508,20 @@ CEK_FIXUPS_JS = '''
|
||||
while (!cekTerminal_p(state)) { state = cekStep(state); }
|
||||
return cekValue(state);
|
||||
};
|
||||
|
||||
// Platform functions — defined in platform_js.py, not in .sx spec files.
|
||||
// Spec defines self-register via js-emit-define; these are the platform interface.
|
||||
PRIMITIVES["type-of"] = typeOf;
|
||||
PRIMITIVES["symbol-name"] = symbolName;
|
||||
PRIMITIVES["keyword-name"] = keywordName;
|
||||
PRIMITIVES["callable?"] = isCallable;
|
||||
PRIMITIVES["lambda?"] = isLambda;
|
||||
PRIMITIVES["lambda-name"] = lambdaName;
|
||||
PRIMITIVES["component?"] = isComponent;
|
||||
PRIMITIVES["island?"] = isIsland;
|
||||
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); };
|
||||
'''
|
||||
|
||||
|
||||
@@ -1798,8 +1812,8 @@ PLATFORM_DOM_JS = """
|
||||
// If lambda takes 0 params, call without event arg (convenience for on-click handlers)
|
||||
var wrapped = isLambda(handler)
|
||||
? (lambdaParams(handler).length === 0
|
||||
? function(e) { try { cekCall(handler, NIL); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
|
||||
: function(e) { try { cekCall(handler, [e]); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } })
|
||||
? function(e) { try { cekCall(handler, NIL); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } finally { runPostRenderHooks(); } }
|
||||
: function(e) { try { cekCall(handler, [e]); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } finally { runPostRenderHooks(); } })
|
||||
: handler;
|
||||
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
|
||||
el.addEventListener(name, wrapped);
|
||||
@@ -1807,7 +1821,7 @@ PLATFORM_DOM_JS = """
|
||||
}
|
||||
|
||||
function eventDetail(e) {
|
||||
return (e && e.detail != null) ? e.detail : nil;
|
||||
return (e && e.detail != null) ? e.detail : NIL;
|
||||
}
|
||||
|
||||
function domQuery(sel) {
|
||||
@@ -1852,7 +1866,7 @@ PLATFORM_DOM_JS = """
|
||||
if (el) { if (!el._sxData) el._sxData = {}; el._sxData[key] = val; }
|
||||
}
|
||||
function domGetData(el, key) {
|
||||
return (el && el._sxData) ? (el._sxData[key] != null ? el._sxData[key] : nil) : nil;
|
||||
return (el && el._sxData) ? (el._sxData[key] != null ? el._sxData[key] : NIL) : NIL;
|
||||
}
|
||||
function domInnerHtml(el) {
|
||||
return (el && el.innerHTML != null) ? el.innerHTML : "";
|
||||
@@ -3033,6 +3047,9 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_
|
||||
if (typeof domOuterHtml === "function") PRIMITIVES["dom-outer-html"] = domOuterHtml;
|
||||
if (typeof domInnerHtml === "function") PRIMITIVES["dom-inner-html"] = domInnerHtml;
|
||||
if (typeof domTextContent === "function") PRIMITIVES["dom-text-content"] = domTextContent;
|
||||
if (typeof domCreateElement === "function") PRIMITIVES["dom-create-element"] = domCreateElement;
|
||||
if (typeof domAppend === "function") PRIMITIVES["dom-append"] = domAppend;
|
||||
if (typeof domAppendToHead === "function") PRIMITIVES["dom-append-to-head"] = domAppendToHead;
|
||||
if (typeof jsonParse === "function") PRIMITIVES["json-parse"] = jsonParse;
|
||||
if (typeof nowMs === "function") PRIMITIVES["now-ms"] = nowMs;
|
||||
PRIMITIVES["sx-parse"] = sxParse;
|
||||
|
||||
@@ -216,13 +216,15 @@ def compile_ref_to_js(
|
||||
# Platform JS for selected adapters
|
||||
if not has_dom:
|
||||
parts.append("\n var _hasDom = false;\n")
|
||||
for name in ("dom", "engine", "orchestration", "boot"):
|
||||
if name in adapter_set and name in adapter_platform:
|
||||
parts.append(adapter_platform[name])
|
||||
|
||||
# CEK fixups + general fixups BEFORE boot (boot hydrates islands that need these)
|
||||
parts.append(fixups_js(has_html, has_sx, has_dom, has_signals, has_deps, has_page_helpers))
|
||||
if has_cek:
|
||||
parts.append(CEK_FIXUPS_JS)
|
||||
|
||||
for name in ("dom", "engine", "orchestration", "boot"):
|
||||
if name in adapter_set and name in adapter_platform:
|
||||
parts.append(adapter_platform[name])
|
||||
if has_continuations:
|
||||
parts.append(CONTINUATIONS_JS)
|
||||
if has_dom:
|
||||
|
||||
@@ -64,6 +64,7 @@ def create_app() -> "Quart":
|
||||
"sx",
|
||||
context_fn=sx_standalone_context if SX_STANDALONE else sx_docs_context,
|
||||
domain_services_fn=register_domain_services,
|
||||
css_extras=[], # No legacy CSS — SX uses CSSX + custom highlighting
|
||||
**extra_kw,
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
(defcomp ~docs-content/home-content ()
|
||||
(div :id "main-content" :class "max-w-3xl mx-auto px-4 py-6"
|
||||
(~docs/code :code (highlight (component-source "~layouts/header") "lisp"))))
|
||||
(~home/stepper)))
|
||||
|
||||
(defcomp ~docs-content/docs-introduction-content ()
|
||||
(~docs/page :title "Introduction"
|
||||
|
||||
274
sx/sx/essays/platonic-sx.sx
Normal file
274
sx/sx/essays/platonic-sx.sx
Normal file
@@ -0,0 +1,274 @@
|
||||
(defcomp ~essays/platonic-sx/essay-platonic-sx ()
|
||||
(~docs/page :title "Platonic SX"
|
||||
|
||||
(p :class "text-stone-500 text-sm italic mb-8"
|
||||
"The allegory of the cave, the theory of Forms, and why a hypermedium "
|
||||
"that defines itself participates in something Plato would have recognized.")
|
||||
|
||||
(~docs/section :title "The cave" :id "the-cave"
|
||||
(p :class "text-stone-600"
|
||||
"In Book VII of the " (a :href "https://en.wikipedia.org/wiki/Allegory_of_the_cave" :class "text-violet-600 hover:underline" "Republic")
|
||||
", Plato describes prisoners chained in a cave, facing a wall. "
|
||||
"Behind them burns a fire; between the fire and the prisoners, figures move, "
|
||||
"casting shadows on the wall. The prisoners have never seen the figures directly. "
|
||||
"They take the shadows for reality.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"The allegory is about representation. The shadows are not the things themselves "
|
||||
"but projections \u2014 reduced, flattened, stripped of depth and colour. "
|
||||
"The prisoners mistake the representation for the thing represented. "
|
||||
"They build entire theories about the behaviour of shadows, never suspecting "
|
||||
"that the shadows are derived from something more real.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"The web is a cave."))
|
||||
|
||||
(~docs/section :title "Shadows on the wall" :id "shadows"
|
||||
(p :class "text-stone-600"
|
||||
"An HTML page is a shadow. It is a projection of the thing the author intended \u2014 "
|
||||
"a structure, a meaning, a behaviour \u2014 flattened into a string of angle brackets. "
|
||||
"The structure is lost in the serialization. The meaning is implicit in class names "
|
||||
"and data attributes. The behaviour is bolted on in a separate language (JavaScript) "
|
||||
"that has no formal relationship to the markup it manipulates.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"CSS is another shadow \u2014 a projection of visual intention into a cascade of "
|
||||
"property-value pairs, separated from the structure it describes. "
|
||||
"JSON is a shadow of data, stripped of type, context, and behaviour. "
|
||||
"REST is a shadow of computation, reduced to verbs and resource paths.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"Each format is a lossy projection. And crucially, each projects into a "
|
||||
(em "different") " medium. HTML for structure. CSS for style. JavaScript for behaviour. "
|
||||
"JSON for data. The original unity \u2014 the " (em "thing itself") " \u2014 is scattered "
|
||||
"across four representations that cannot reference each other "
|
||||
"without external convention.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"Plato would have recognized this immediately. The Forms are one. "
|
||||
"The shadows are many. The task of philosophy is to see past the shadows."))
|
||||
|
||||
(~docs/section :title "The theory of Forms" :id "forms"
|
||||
(p :class "text-stone-600"
|
||||
"Plato's " (a :href "https://en.wikipedia.org/wiki/Theory_of_forms" :class "text-violet-600 hover:underline" "theory of Forms")
|
||||
" holds that behind every particular instance \u2014 every chair, every circle, "
|
||||
"every act of justice \u2014 there exists an ideal Form: the perfect Chair, "
|
||||
"the perfect Circle, Justice itself. Particular instances participate in their Form "
|
||||
"but are always imperfect copies. The Form is eternal, immutable, and more real "
|
||||
"than any instance.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"A " (code "defcomp") " definition is a Form:")
|
||||
|
||||
(~docs/code :code
|
||||
(str "(defcomp ~card (&key title subtitle &rest children)\n"
|
||||
" (div :class \"rounded-lg shadow-sm p-4\"\n"
|
||||
" (h2 :class \"font-bold\" title)\n"
|
||||
" (when subtitle (p :class \"text-stone-500\" subtitle))\n"
|
||||
" children))"))
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"This is not a card. It is the " (em "idea") " of a card \u2014 the structure that every "
|
||||
"particular card participates in. When the server evaluates "
|
||||
(code "(~card :title \"Plato\" :subtitle \"428 BC\")") ", it produces a particular instance: "
|
||||
"an HTML fragment, a shadow on a specific wall. The Form persists. "
|
||||
"The shadow is consumed and replaced on the next render.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"In Plato's ontology, Forms are more real than particulars because they are "
|
||||
"what particulars " (em "depend on") ". The HTML output depends on the component definition. "
|
||||
"The component definition does not depend on any particular output. "
|
||||
"It is prior, in the way that axioms are prior to theorems."))
|
||||
|
||||
(~docs/section :title "The divided line" :id "divided-line"
|
||||
(p :class "text-stone-600"
|
||||
"In the Republic, Plato describes " (a :href "https://en.wikipedia.org/wiki/Analogy_of_the_divided_line" :class "text-violet-600 hover:underline" "a line divided into four segments")
|
||||
", representing degrees of reality and knowledge:")
|
||||
|
||||
(div :class "overflow-x-auto mb-6"
|
||||
(table :class "min-w-full text-sm"
|
||||
(thead (tr
|
||||
(th :class "text-left pr-4 pb-2 font-semibold" "Segment")
|
||||
(th :class "text-left pr-4 pb-2 font-semibold" "Plato")
|
||||
(th :class "text-left pb-2 font-semibold" "SX")))
|
||||
(tbody
|
||||
(tr (td :class "pr-4 py-1" "Images")
|
||||
(td :class "pr-4" "Shadows, reflections")
|
||||
(td "The rendered HTML in the browser \u2014 a momentary projection"))
|
||||
(tr (td :class "pr-4 py-1" "Sensible objects")
|
||||
(td :class "pr-4" "Physical things")
|
||||
(td "The SX wire format \u2014 structured but still particular"))
|
||||
(tr (td :class "pr-4 py-1" "Mathematical objects")
|
||||
(td :class "pr-4" "Numbers, geometric shapes")
|
||||
(td "Component definitions, the CEK machine, continuation frames"))
|
||||
(tr (td :class "pr-4 py-1" "The Good / Forms")
|
||||
(td :class "pr-4" "The Form of Forms")
|
||||
(td "The s-expression itself \u2014 the representation that represents")))))
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"The bottom of the line is images \u2014 the DOM, pixels on screen. "
|
||||
"Moving up: the SX wire format preserves more structure than HTML "
|
||||
"(it retains the component calls, the s-expression nesting). "
|
||||
"Above that: the component definitions and the CEK machine \u2014 "
|
||||
"abstract structures that generate all possible instances. "
|
||||
"At the top: the s-expression itself, which is both the medium of definition "
|
||||
"and the thing defined.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"Plato's line is a hierarchy of " (em "participation") ". "
|
||||
"Each level participates in the one above. "
|
||||
"The rendered HTML participates in the component definition. "
|
||||
"The component participates in the evaluator semantics. "
|
||||
"The evaluator participates in the s-expression form. "
|
||||
"The s-expression form participates in \u2014 what? "
|
||||
"In computation itself. In the CEK machine. In logic."))
|
||||
|
||||
(~docs/section :title "Anamnesis: the evaluator remembers" :id "anamnesis"
|
||||
(p :class "text-stone-600"
|
||||
"Plato believed that learning is " (a :href "https://en.wikipedia.org/wiki/Anamnesis_(philosophy)" :class "text-violet-600 hover:underline" "recollection")
|
||||
" \u2014 " (em "anamnesis") ". The soul has seen the Forms before birth; "
|
||||
"education is not acquiring new knowledge but remembering what is already known. "
|
||||
"The particular reminds us of the universal.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"The SX evaluator does something structurally similar. "
|
||||
"When the browser receives SX wire format \u2014 ")
|
||||
|
||||
(~docs/code :code
|
||||
"(~card :title \"Plato\" :subtitle \"428 BC\")")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
" \u2014 it does not receive instructions for rendering. It receives a " (em "name") " "
|
||||
"and a set of arguments. The evaluator already knows " (code "~card") ". "
|
||||
"It has the Form in its component environment. "
|
||||
"The wire format is a reminder: " (em "produce the instance you already know how to produce") ". "
|
||||
"The particular prompts the evaluator to recollect the universal.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"This is why the SX wire format is so small. It doesn't transmit the " (em "what") " "
|
||||
"\u2014 the full HTML, the complete structure. It transmits the " (em "which") " "
|
||||
"\u2014 which Form, which arguments. The evaluator supplies the rest from memory. "
|
||||
"Bandwidth is the cost of forgetting. SX's wire format is efficient "
|
||||
"because the client " (em "remembers") "."))
|
||||
|
||||
(~docs/section :title "Platonic aesthetics" :id "aesthetics"
|
||||
(p :class "text-stone-600"
|
||||
"For Plato, beauty is not subjective. A thing is beautiful to the degree "
|
||||
"that it participates in the " (a :href "https://en.wikipedia.org/wiki/Platonic_beauty" :class "text-violet-600 hover:underline" "Form of Beauty")
|
||||
" \u2014 which is to say, to the degree that it exhibits "
|
||||
(em "order") ", " (em "proportion") ", and " (em "unity") ". "
|
||||
"The " (a :href "https://en.wikipedia.org/wiki/Symposium_(Plato)" :class "text-violet-600 hover:underline" "Symposium")
|
||||
" describes an ascent from beautiful bodies to beautiful souls to beautiful ideas, "
|
||||
"culminating in Beauty itself \u2014 the Form that makes all beautiful things beautiful.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"There is a beauty in s-expressions that is Platonic in this precise sense. "
|
||||
"Not decorative beauty \u2014 no one finds parentheses pretty. "
|
||||
"But structural beauty: the kind Plato meant.")
|
||||
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-2 text-stone-600"
|
||||
(li (strong "Unity") " \u2014 one representation for everything. "
|
||||
"Code, data, markup, wire format, component definitions, the evaluator itself. "
|
||||
"No seams, no translation boundaries, no format negotiation. "
|
||||
"The s-expression is a universal solvent.")
|
||||
(li (strong "Proportion") " \u2014 the means are proportional to the ends. "
|
||||
"A component is as complex as the thing it describes and no more. "
|
||||
"The evaluator is 900 lines of SX. The parser is 400. "
|
||||
"There is no hidden machinery, no framework overhead, no build step. "
|
||||
"The ratio of essential to accidental complexity approaches one.")
|
||||
(li (strong "Order") " \u2014 the hierarchy is strict and explicit. "
|
||||
"CEK at the bottom, continuations above, scoped effects above, patterns at the top. "
|
||||
"Each layer is definable in terms of the one below. "
|
||||
"No circular dependencies, no ad hoc escape hatches, no exceptions to the rules."))
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"Plato would say that these properties are not incidental but necessary \u2014 "
|
||||
"they follow from the proximity of s-expressions to the Forms themselves. "
|
||||
"A representation that can represent itself has fewer impediments "
|
||||
"between it and the abstract structure it encodes. "
|
||||
"It is more " (em "real") ", in the Platonic sense, than a representation "
|
||||
"that requires a separate meta-representation to describe it."))
|
||||
|
||||
(~docs/section :title "The escape from the cave" :id "escape"
|
||||
(p :class "text-stone-600"
|
||||
"In the allegory, one prisoner is freed and dragged up into the sunlight. "
|
||||
"At first the light is blinding. He can only look at reflections in water, "
|
||||
"then at objects, then at the sun itself. He returns to the cave "
|
||||
"and tries to tell the others what he saw. They think he is mad.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"The web's cave is comfortable. Developers have built elaborate theories "
|
||||
"of the shadows \u2014 virtual DOMs, hydration strategies, build tool chains, "
|
||||
"CSS-in-JS, state management libraries. Each theory explains how shadows behave. "
|
||||
"None asks why we are working with shadows at all.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"The escape is not a technology. It is a shift in perspective: "
|
||||
(em "stop working with projections and work with the thing itself") ". "
|
||||
"An s-expression is not a projection of structure into text \u2014 "
|
||||
"it IS the structure. A " (code "defcomp") " is not a description of a component "
|
||||
"in a host language \u2014 it IS the component. "
|
||||
"The SX evaluator is not described by a specification \u2014 "
|
||||
"it IS the specification, executing.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"This is what it means for a representation to be homoiconic. "
|
||||
"The map is the territory. The shadow and the figure are the same thing. "
|
||||
"The cave and the sunlit world collapse into one."))
|
||||
|
||||
(~docs/section :title "The demiurge" :id "demiurge"
|
||||
(p :class "text-stone-600"
|
||||
"In the " (a :href "https://en.wikipedia.org/wiki/Timaeus_(dialogue)" :class "text-violet-600 hover:underline" "Timaeus")
|
||||
", Plato introduces the " (a :href "https://en.wikipedia.org/wiki/Demiurge" :class "text-violet-600 hover:underline" "demiurge")
|
||||
" \u2014 the divine craftsman who looks at the eternal Forms and fashions "
|
||||
"the physical world in their image. The demiurge does not create the Forms. "
|
||||
"He creates " (em "instances") " of them, in a medium (matter) that is less perfect "
|
||||
"than the Forms themselves.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"The bootstrapper is a demiurge. It looks at the Forms (" (code "eval.sx") ", "
|
||||
(code "parser.sx") ", " (code "cek.sx") ") and fashions instances in a material medium: "
|
||||
"Python, JavaScript. The instances are less perfect than the Forms \u2014 "
|
||||
"they have platform-specific quirks, performance characteristics, "
|
||||
"memory layouts. But they " (em "participate") " in the Forms. "
|
||||
"They are correct to the degree that they faithfully instantiate the spec.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"The demiurge is not omnipotent. He works with what the medium allows. "
|
||||
"The Python bootstrapper emits " (code "def") " and " (code "lambda") "; "
|
||||
"the JavaScript bootstrapper emits " (code "var") " and " (code "function") ". "
|
||||
"Each medium has its own constraints. But the Form \u2014 the " (code ".sx") " spec \u2014 "
|
||||
"is the same. Multiple demiurges, one set of Forms, many material instances."))
|
||||
|
||||
(~docs/section :title "The good" :id "the-good"
|
||||
(p :class "text-stone-600"
|
||||
"At the apex of Plato's hierarchy is the " (a :href "https://en.wikipedia.org/wiki/Form_of_the_Good" :class "text-violet-600 hover:underline" "Form of the Good")
|
||||
" \u2014 the Form that makes all other Forms intelligible. "
|
||||
"It is not itself a thing but the condition for all things being knowable. "
|
||||
"It is the sun in the allegory: the source of light that reveals everything else.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"If we take the analogy seriously, what is SX's Form of the Good? "
|
||||
"What makes the hierarchy \u2014 CEK, continuations, scoped effects, patterns \u2014 intelligible as a whole?")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"It is the principle that " (strong "the representation and the thing represented should be identical") ". "
|
||||
"Code is data. The specification is the implementation. The wire format is the source syntax. "
|
||||
"The evaluator evaluates itself. Every level of the hierarchy obeys this principle, "
|
||||
"and it is what makes each level intelligible from the one below.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"This is not a design principle in the engineering sense \u2014 a guideline to be followed or violated. "
|
||||
"It is the structural " (em "reason") " the hierarchy exists at all. "
|
||||
"Remove it and the layers collapse. Restore it and they reconstitute. "
|
||||
"It is prior to the hierarchy, in the way the Good is prior to the Forms.")
|
||||
|
||||
(p :class "text-stone-600"
|
||||
"Plato would have understood this. He spent his life searching for the thing "
|
||||
"that is most itself, least dependent on anything else, most fully real. "
|
||||
"An s-expression that defines its own evaluator, parsed by its own parser, "
|
||||
"bootstrapped to every medium, generating instances of itself in perpetuity \u2014 "
|
||||
"this is as close to a Platonic Form as computation gets."))
|
||||
|
||||
(p :class "text-stone-500 text-sm italic mt-12"
|
||||
"The unexamined code is not worth running.")))
|
||||
@@ -148,6 +148,331 @@
|
||||
"lisp")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; CEK stepper: interactive stepping debugger
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defisland ~geography/cek/demo-stepper (&key initial-expr)
|
||||
(let ((source (signal (or initial-expr "(+ 1 (* 2 3))")))
|
||||
(state (signal nil))
|
||||
(steps (signal 0))
|
||||
(history (signal (list)))
|
||||
(error-msg (signal nil)))
|
||||
|
||||
;; Parse and create initial CEK state
|
||||
(define start-eval
|
||||
(fn ()
|
||||
(reset! error-msg nil)
|
||||
(reset! history (list))
|
||||
(reset! steps 0)
|
||||
(let ((parsed (sx-parse (deref source))))
|
||||
(if (empty? parsed)
|
||||
(reset! error-msg "Parse error: empty expression")
|
||||
(reset! state (make-cek-state (first parsed) (make-env) (list)))))))
|
||||
|
||||
;; Single step
|
||||
(define do-step
|
||||
(fn ()
|
||||
(when (and (deref state) (not (cek-terminal? (deref state))))
|
||||
(let ((prev (deref state)))
|
||||
(swap! history (fn (h) (append h (list prev))))
|
||||
(swap! steps inc)
|
||||
(reset! state (cek-step prev))))))
|
||||
|
||||
;; Run to completion
|
||||
(define do-run
|
||||
(fn ()
|
||||
(when (deref state)
|
||||
(let run-loop ((n 0))
|
||||
(when (and (not (cek-terminal? (deref state))) (< n 200))
|
||||
(do-step)
|
||||
(run-loop (+ n 1)))))))
|
||||
|
||||
;; Reset
|
||||
(define do-reset
|
||||
(fn ()
|
||||
(reset! state nil)
|
||||
(reset! steps 0)
|
||||
(reset! history (list))
|
||||
(reset! error-msg nil)))
|
||||
|
||||
;; Format control for display
|
||||
(define fmt-control
|
||||
(fn (s)
|
||||
(if (nil? s) "\u2014"
|
||||
(let ((c (get s "control")))
|
||||
(if (nil? c) "\u2014"
|
||||
(sx-serialize c))))))
|
||||
|
||||
;; Format value
|
||||
(define fmt-value
|
||||
(fn (s)
|
||||
(if (nil? s) "\u2014"
|
||||
(let ((v (get s "value")))
|
||||
(cond
|
||||
(nil? v) "nil"
|
||||
(callable? v) (str "\u03bb:" (or (lambda-name v) "fn"))
|
||||
:else (sx-serialize v))))))
|
||||
|
||||
;; Format kont
|
||||
(define fmt-kont
|
||||
(fn (s)
|
||||
(if (nil? s) "\u2014"
|
||||
(let ((k (get s "kont")))
|
||||
(if (empty? k) "[]"
|
||||
(str "[" (join " " (map (fn (f) (get f "type")) k)) "]"))))))
|
||||
|
||||
;; Initialize on first render
|
||||
(start-eval)
|
||||
|
||||
(div :class "space-y-4"
|
||||
;; Input
|
||||
(div :class "flex gap-2 items-end"
|
||||
(div :class "flex-1"
|
||||
(label :class "text-xs text-stone-400 block mb-1" "Expression")
|
||||
(input :type "text" :bind source
|
||||
:class "w-full px-3 py-1.5 rounded border border-stone-300 font-mono text-sm focus:outline-none focus:border-violet-400"
|
||||
:on-change (fn (e) (start-eval))))
|
||||
(div :class "flex gap-1"
|
||||
(button :on-click (fn (e) (start-eval))
|
||||
:class "px-3 py-1.5 rounded bg-stone-200 text-stone-700 text-sm hover:bg-stone-300" "Reset")
|
||||
(button :on-click (fn (e) (do-step))
|
||||
:class "px-3 py-1.5 rounded bg-violet-500 text-white text-sm hover:bg-violet-600" "Step")
|
||||
(button :on-click (fn (e) (do-run))
|
||||
:class "px-3 py-1.5 rounded bg-violet-700 text-white text-sm hover:bg-violet-800" "Run")))
|
||||
|
||||
;; Error
|
||||
(when (deref error-msg)
|
||||
(div :class "text-red-600 text-sm" (deref error-msg)))
|
||||
|
||||
;; Current state
|
||||
(when (deref state)
|
||||
(div :class "rounded border border-stone-200 bg-white p-3 font-mono text-sm space-y-1"
|
||||
(div :class "flex gap-4"
|
||||
(span :class "text-stone-400 w-16" "Step")
|
||||
(span :class "font-bold" (deref steps)))
|
||||
(div :class "flex gap-4"
|
||||
(span :class "text-stone-400 w-16" "Phase")
|
||||
(span :class (str "font-bold " (if (= (get (deref state) "phase") "eval") "text-blue-600" "text-green-600"))
|
||||
(get (deref state) "phase")))
|
||||
(div :class "flex gap-4"
|
||||
(span :class "text-violet-500 w-16" "C")
|
||||
(span (fmt-control (deref state))))
|
||||
(div :class "flex gap-4"
|
||||
(span :class "text-amber-600 w-16" "V")
|
||||
(span (fmt-value (deref state))))
|
||||
(div :class "flex gap-4"
|
||||
(span :class "text-emerald-600 w-16" "K")
|
||||
(span (fmt-kont (deref state))))
|
||||
(when (cek-terminal? (deref state))
|
||||
(div :class "mt-2 pt-2 border-t border-stone-200 text-stone-800 font-bold"
|
||||
(str "Result: " (sx-serialize (cek-value (deref state))))))))
|
||||
|
||||
;; Step history
|
||||
(when (not (empty? (deref history)))
|
||||
(div :class "rounded border border-stone-100 bg-stone-50 p-2"
|
||||
(div :class "text-xs text-stone-400 mb-1" "History")
|
||||
(div :class "space-y-0.5 font-mono text-xs max-h-48 overflow-y-auto"
|
||||
(map-indexed (fn (i s)
|
||||
(div :class "flex gap-2 text-stone-500"
|
||||
(span :class "text-stone-300 w-6 text-right" (+ i 1))
|
||||
(span :class (if (= (get s "phase") "eval") "text-blue-400" "text-green-400") (get s "phase"))
|
||||
(span :class "text-violet-400 truncate" (fmt-control s))
|
||||
(span :class "text-amber-400" (fmt-value s))
|
||||
(span :class "text-emerald-400" (fmt-kont s))))
|
||||
(deref history))))))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Render stepper: watch a component render itself, tag by tag
|
||||
;;
|
||||
;; Walks the SX AST depth-first. At each step, renders ONE subtree
|
||||
;; via render-to-html and appends to the accumulating output.
|
||||
;; The preview pane shows partial HTML building up.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defisland ~geography/cek/demo-render-stepper (&key initial-expr)
|
||||
(let ((source (signal (or initial-expr
|
||||
"(div (~cssx/tw :tokens \"p-6 rounded-lg border border-stone-200 bg-white text-center\")\n (h1 (~cssx/tw :tokens \"text-3xl font-bold mb-2\")\n (span (~cssx/tw :tokens \"text-rose-500\") \"the \")\n (span (~cssx/tw :tokens \"text-amber-500\") \"joy \")\n (span (~cssx/tw :tokens \"text-emerald-500\") \"of \")\n (span (~cssx/tw :tokens \"text-violet-600 text-4xl\") \"sx\")))")))
|
||||
(steps (signal (list)))
|
||||
(step-idx (signal 0))
|
||||
(parsed-ok (signal false))
|
||||
(error-msg (signal nil))
|
||||
(dom-stack-sig (signal (list))))
|
||||
(letrec
|
||||
((split-tag (fn (expr result)
|
||||
(cond
|
||||
(not (list? expr))
|
||||
(append! result {"type" "leaf" "expr" expr})
|
||||
(empty? expr) nil
|
||||
(not (= (type-of (first expr)) "symbol"))
|
||||
(append! result {"type" "leaf" "expr" expr})
|
||||
(is-html-tag? (symbol-name (first expr)))
|
||||
(let ((ctag (symbol-name (first expr)))
|
||||
(cargs (rest expr))
|
||||
(cch (list))
|
||||
(cat (list))
|
||||
(spreads (list))
|
||||
(ckw false))
|
||||
(for-each (fn (a)
|
||||
(cond
|
||||
(= (type-of a) "keyword") (do (set! ckw true) (append! cat a))
|
||||
ckw (do (set! ckw false) (append! cat a))
|
||||
(and (list? a) (not (empty? a))
|
||||
(= (type-of (first a)) "symbol")
|
||||
(starts-with? (symbol-name (first a)) "~"))
|
||||
(do (set! ckw false) (append! spreads a))
|
||||
:else (do (set! ckw false) (append! cch a))))
|
||||
cargs)
|
||||
(append! result {"type" "open" "tag" ctag "attrs" cat "spreads" spreads})
|
||||
(for-each (fn (c) (split-tag c result)) cch)
|
||||
(append! result {"type" "close" "tag" ctag}))
|
||||
:else
|
||||
(append! result {"type" "expr" "expr" expr}))))
|
||||
(get-preview (fn () (dom-query "[data-sx-lake=\"preview\"]")))
|
||||
(get-stack (fn () (deref dom-stack-sig)))
|
||||
(set-stack (fn (v) (reset! dom-stack-sig v)))
|
||||
(push-stack (fn (el) (reset! dom-stack-sig (append (deref dom-stack-sig) (list el)))))
|
||||
(pop-stack (fn ()
|
||||
(let ((s (deref dom-stack-sig)))
|
||||
(when (> (len s) 1)
|
||||
(reset! dom-stack-sig (slice s 0 (- (len s) 1)))))))
|
||||
(do-parse (fn ()
|
||||
(reset! error-msg nil)
|
||||
(reset! step-idx 0)
|
||||
(reset! parsed-ok false)
|
||||
(set-stack (list))
|
||||
(let ((container (get-preview)))
|
||||
(when container (dom-set-prop container "innerHTML" "")))
|
||||
(let ((parsed (sx-parse (deref source))))
|
||||
(if (empty? parsed)
|
||||
(do (reset! error-msg "Parse error") (reset! steps (list)))
|
||||
(let ((result (list)))
|
||||
(split-tag (first parsed) result)
|
||||
(reset! steps result)
|
||||
(reset! parsed-ok true)
|
||||
(set-stack (list (get-preview))))))))
|
||||
(do-step (fn ()
|
||||
(when (and (deref parsed-ok) (< (deref step-idx) (len (deref steps))))
|
||||
(let ((step (nth (deref steps) (deref step-idx)))
|
||||
(step-type (get step "type"))
|
||||
(stack (get-stack))
|
||||
(parent (if (empty? (get-stack)) (get-preview) (last (get-stack)))))
|
||||
(cond
|
||||
(= step-type "open")
|
||||
(let ((el (dom-create-element (get step "tag") nil))
|
||||
(attrs (get step "attrs"))
|
||||
(spreads (or (get step "spreads") (list))))
|
||||
(let loop ((i 0))
|
||||
(when (< i (len attrs))
|
||||
(dom-set-attr el (keyword-name (nth attrs i)) (nth attrs (+ i 1)))
|
||||
(loop (+ i 2))))
|
||||
(for-each (fn (sp)
|
||||
(let ((result (eval-expr sp (make-env))))
|
||||
(when (and result (spread? result))
|
||||
(let ((sattrs (spread-attrs result)))
|
||||
(for-each (fn (k)
|
||||
(if (= k "class")
|
||||
(dom-set-attr el "class"
|
||||
(str (or (dom-get-attr el "class") "") " " (get sattrs k)))
|
||||
(dom-set-attr el k (get sattrs k))))
|
||||
(keys sattrs))))))
|
||||
spreads)
|
||||
(when parent (dom-append parent el))
|
||||
(push-stack el))
|
||||
(= step-type "close")
|
||||
(pop-stack)
|
||||
(= step-type "leaf")
|
||||
(when parent
|
||||
(let ((val (get step "expr")))
|
||||
(dom-append parent (create-text-node (if (string? val) val (str val))))))
|
||||
(= step-type "expr")
|
||||
(let ((rendered (render-to-dom (get step "expr") (make-env) nil)))
|
||||
(when (and parent rendered)
|
||||
(dom-append parent rendered)))))
|
||||
(swap! step-idx inc))))
|
||||
(do-run (fn ()
|
||||
(let loop ()
|
||||
(when (< (deref step-idx) (len (deref steps)))
|
||||
(do-step)
|
||||
(loop)))))
|
||||
(do-back (fn ()
|
||||
(when (and (deref parsed-ok) (> (deref step-idx) 0))
|
||||
(let ((target (- (deref step-idx) 1))
|
||||
(container (get-preview)))
|
||||
(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)))))))
|
||||
(div :class "space-y-4"
|
||||
(div
|
||||
(label :class "text-xs text-stone-400 block mb-1" "Component expression")
|
||||
(textarea :bind source :rows 4
|
||||
:class "w-full px-3 py-2 rounded border border-stone-300 font-mono text-xs focus:outline-none focus:border-violet-400"))
|
||||
(div :class "flex gap-1"
|
||||
(button :on-click (fn (e) (do-parse))
|
||||
:class "px-3 py-1.5 rounded bg-stone-700 text-white text-sm hover:bg-stone-800" "Parse")
|
||||
(button :on-click (fn (e) (do-back))
|
||||
:class (str "px-3 py-1.5 rounded text-sm "
|
||||
(if (and (deref parsed-ok) (> (deref step-idx) 0))
|
||||
"bg-stone-200 text-stone-700 hover:bg-stone-300"
|
||||
"bg-stone-100 text-stone-300 cursor-not-allowed"))
|
||||
"\u25c0")
|
||||
(button :on-click (fn (e) (do-step))
|
||||
:class (str "px-3 py-1.5 rounded text-sm "
|
||||
(if (and (deref parsed-ok) (< (deref step-idx) (len (deref steps))))
|
||||
"bg-violet-500 text-white hover:bg-violet-600"
|
||||
"bg-violet-200 text-violet-400 cursor-not-allowed"))
|
||||
"Step \u25b6")
|
||||
(button :on-click (fn (e) (do-run))
|
||||
:class (str "px-3 py-1.5 rounded text-sm "
|
||||
(if (deref parsed-ok)
|
||||
"bg-violet-700 text-white hover:bg-violet-800"
|
||||
"bg-violet-200 text-violet-400 cursor-not-allowed"))
|
||||
"Run \u25b6\u25b6"))
|
||||
(when (deref error-msg)
|
||||
(div :class "text-red-600 text-sm" (deref error-msg)))
|
||||
(when (and (deref parsed-ok) (= (deref step-idx) 0))
|
||||
(div :class "text-sm text-stone-500 bg-stone-50 rounded p-2"
|
||||
(str "Parsed " (len (deref steps)) " render steps. Click Step to begin.")))
|
||||
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
(when (deref parsed-ok)
|
||||
(div :class "rounded border border-stone-200 bg-white p-3 min-h-24"
|
||||
(div :class "text-xs text-stone-400 mb-2"
|
||||
(str (deref step-idx) " / " (len (deref steps))
|
||||
(if (= (deref step-idx) (len (deref steps))) " \u2014 complete" "")))
|
||||
(div :class "space-y-0.5 font-mono text-xs max-h-64 overflow-y-auto"
|
||||
(map-indexed (fn (i step)
|
||||
(div :class (str "flex gap-2 px-1 rounded "
|
||||
(cond
|
||||
(= i (deref step-idx)) "bg-violet-100 text-violet-700 font-bold"
|
||||
(< i (deref step-idx)) "text-stone-400"
|
||||
:else "text-stone-300"))
|
||||
(span :class "w-4 text-right"
|
||||
(if (< i (deref step-idx)) "\u2713" (str (+ i 1))))
|
||||
(span :class "truncate"
|
||||
(let ((lbl (get step "label")))
|
||||
(if lbl
|
||||
(if (> (len lbl) 60) (str (slice lbl 0 57) "...") lbl)
|
||||
(let ((tp (get step "type")))
|
||||
(cond
|
||||
(= tp "open") (str "<" (get step "tag") ">")
|
||||
(= tp "close") (str "</" (get step "tag") ">")
|
||||
:else (sx-serialize (get step "expr")))))))))
|
||||
(deref steps)))))
|
||||
(div :class "rounded border border-stone-200 p-3 min-h-24"
|
||||
(div :class "text-xs text-stone-400 mb-2" "Live DOM")
|
||||
(lake :id "preview")))))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Demo page content
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -157,39 +482,42 @@
|
||||
|
||||
(~docs/section :title "What this demonstrates" :id "what"
|
||||
(p "These are " (strong "live islands") " evaluated by the CEK machine. Every " (code "eval-expr") " goes through " (code "cek-run") ". Every " (code "(deref sig)") " in an island creates a reactive DOM binding via continuation frames.")
|
||||
(p "The CEK machine is defined in " (code "cek.sx") " (160 lines) and " (code "frames.sx") " (100 lines) — pure s-expressions, bootstrapped to both JavaScript and Python."))
|
||||
(p "The CEK machine is defined in " (code "cek.sx") " and " (code "frames.sx") " — pure s-expressions, bootstrapped to both JavaScript and Python."))
|
||||
|
||||
(~docs/section :title "Stepper" :id "stepper"
|
||||
(p "The CEK machine is pure data\u2192data. Each step takes a state dict and returns a new one. "
|
||||
"Type an expression, click Step to advance one CEK transition.")
|
||||
(~geography/cek/demo-stepper :initial-expr "(let ((x 10)) (+ x (* 2 3)))")
|
||||
(~docs/code :code (highlight (component-source "~geography/cek/demo-stepper") "lisp")))
|
||||
|
||||
(~docs/section :title "Render stepper" :id "render-stepper"
|
||||
(p "Watch a component render itself. The CEK evaluates the expression — "
|
||||
"when it encounters " (code "(div ...)") ", the render adapter produces HTML in one step. "
|
||||
"Click Run to see the rendered output appear in the preview.")
|
||||
(~geography/cek/demo-render-stepper)
|
||||
(~docs/code :code (highlight (component-source "~geography/cek/demo-render-stepper") "lisp")))
|
||||
|
||||
(~docs/section :title "1. Counter" :id "demo-counter"
|
||||
(p (code "(deref count)") " in text position creates a reactive text node. " (code "(deref doubled)") " is a computed that updates when count changes.")
|
||||
(~geography/cek/demo-counter :initial 0)
|
||||
(~docs/code :code (highlight
|
||||
"(defisland ~demo-counter (&key initial)\n (let ((count (signal (or initial 0)))\n (doubled (computed (fn () (* 2 (deref count))))))\n (div\n (button :on-click (fn (e) (swap! count dec)) \"-\")\n (span (deref count))\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (p (str \"doubled: \" (deref doubled))))))"
|
||||
"lisp")))
|
||||
(~docs/code :code (highlight (component-source "~geography/cek/demo-counter") "lisp")))
|
||||
|
||||
(~docs/section :title "2. Computed chain" :id "demo-chain"
|
||||
(p "Three levels of computed: base -> doubled -> quadrupled. Change base, all propagate.")
|
||||
(~geography/cek/demo-chain)
|
||||
(~docs/code :code (highlight
|
||||
"(let ((base (signal 1))\n (doubled (computed (fn () (* (deref base) 2))))\n (quadrupled (computed (fn () (* (deref doubled) 2)))))\n (span (deref base))\n (p (str \"doubled: \" (deref doubled)\n \" | quadrupled: \" (deref quadrupled))))"
|
||||
"lisp")))
|
||||
(~docs/code :code (highlight (component-source "~geography/cek/demo-chain") "lisp")))
|
||||
|
||||
(~docs/section :title "3. Reactive attributes" :id "demo-attr"
|
||||
(p (code "(deref sig)") " in " (code ":class") " position. The CEK evaluates the " (code "str") " expression, and when the signal changes, the continuation re-evaluates and updates the attribute.")
|
||||
(~geography/cek/demo-reactive-attr)
|
||||
(~docs/code :code (highlight
|
||||
"(div :class (str \"p-3 rounded font-medium \"\n (if (deref danger)\n \"bg-red-100 text-red-800\"\n \"bg-green-100 text-green-800\"))\n (if (deref danger) \"DANGER\" \"SAFE\"))"
|
||||
"lisp")))
|
||||
(~docs/code :code (highlight (component-source "~geography/cek/demo-reactive-attr") "lisp")))
|
||||
|
||||
(~docs/section :title "4. Effect + cleanup" :id "demo-stopwatch"
|
||||
(p "Effects still work through CEK. This stopwatch uses " (code "effect") " with cleanup — toggling the signal clears the interval.")
|
||||
(~geography/cek/demo-stopwatch)
|
||||
(~docs/code :code (highlight
|
||||
"(effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! elapsed inc)) 100)))\n (fn () (clear-interval id))))))"
|
||||
"lisp")))
|
||||
(~docs/code :code (highlight (component-source "~geography/cek/demo-stopwatch") "lisp")))
|
||||
|
||||
(~docs/section :title "5. Batch coalescing" :id "demo-batch"
|
||||
(p "Two signals updated in " (code "batch") " — one notification cycle. Compare render counts between batch and no-batch.")
|
||||
(~geography/cek/demo-batch)
|
||||
(~docs/code :code (highlight
|
||||
"(batch (fn ()\n (swap! first-sig inc)\n (swap! second-sig inc)))\n;; One render pass, not two."
|
||||
"lisp")))))
|
||||
(~docs/code :code (highlight (component-source "~geography/cek/demo-batch") "lisp")))))
|
||||
|
||||
230
sx/sx/home-stepper.sx
Normal file
230
sx/sx/home-stepper.sx
Normal file
@@ -0,0 +1,230 @@
|
||||
(defisland ~home/stepper ()
|
||||
(let ((source "(div (~cssx/tw :tokens \"text-center\")\n (h1 (~cssx/tw :tokens \"text-3xl font-bold mb-2\")\n (span (~cssx/tw :tokens \"text-rose-500\") \"the \")\n (span (~cssx/tw :tokens \"text-amber-500\") \"joy \")\n (span (~cssx/tw :tokens \"text-emerald-500\") \"of \")\n (span (~cssx/tw :tokens \"text-violet-600 text-4xl\") \"sx\")))")
|
||||
(steps (signal (list)))
|
||||
(step-idx (signal 9))
|
||||
(dom-stack-sig (signal (list)))
|
||||
(code-tokens (signal (list)))
|
||||
(code-spans (list)))
|
||||
(letrec
|
||||
((split-tag (fn (expr result)
|
||||
(cond
|
||||
(not (list? expr))
|
||||
(append! result {"type" "leaf" "expr" expr})
|
||||
(empty? expr) nil
|
||||
(not (= (type-of (first expr)) "symbol"))
|
||||
(append! result {"type" "leaf" "expr" expr})
|
||||
(is-html-tag? (symbol-name (first expr)))
|
||||
(let ((ctag (symbol-name (first expr)))
|
||||
(cargs (rest expr))
|
||||
(cch (list))
|
||||
(cat (list))
|
||||
(spreads (list))
|
||||
(ckw false))
|
||||
(for-each (fn (a)
|
||||
(cond
|
||||
(= (type-of a) "keyword") (do (set! ckw true) (append! cat a))
|
||||
ckw (do (set! ckw false) (append! cat a))
|
||||
(and (list? a) (not (empty? a))
|
||||
(= (type-of (first a)) "symbol")
|
||||
(starts-with? (symbol-name (first a)) "~"))
|
||||
(do (set! ckw false) (append! spreads a))
|
||||
:else (do (set! ckw false) (append! cch a))))
|
||||
cargs)
|
||||
(append! result {"type" "open" "tag" ctag "attrs" cat "spreads" spreads})
|
||||
(for-each (fn (c) (split-tag c result)) cch)
|
||||
(append! result {"type" "close" "tag" ctag}))
|
||||
:else
|
||||
(append! result {"type" "expr" "expr" expr}))))
|
||||
(build-code-tokens (fn (expr tokens step-ref indent)
|
||||
(cond
|
||||
(string? expr)
|
||||
(do (append! tokens {"text" (str "\"" expr "\"") "cls" "text-emerald-700" "step" (get step-ref "v")})
|
||||
(dict-set! step-ref "v" (+ (get step-ref "v") 1)))
|
||||
(number? expr)
|
||||
(do (append! tokens {"text" (str expr) "cls" "text-amber-700" "step" (get step-ref "v")})
|
||||
(dict-set! step-ref "v" (+ (get step-ref "v") 1)))
|
||||
(= (type-of expr) "keyword")
|
||||
(append! tokens {"text" (str ":" (keyword-name expr)) "cls" "text-violet-600" "step" (get step-ref "v")})
|
||||
(= (type-of expr) "symbol")
|
||||
(let ((name (symbol-name expr)))
|
||||
(append! tokens {"text" name "cls"
|
||||
(cond
|
||||
(is-html-tag? name) "text-sky-700 font-semibold"
|
||||
(starts-with? name "~") "text-rose-600 font-semibold"
|
||||
:else "text-stone-700")
|
||||
"step" (get step-ref "v")}))
|
||||
(list? expr)
|
||||
(when (not (empty? expr))
|
||||
(let ((head (first expr))
|
||||
(is-tag (and (= (type-of head) "symbol") (is-html-tag? (symbol-name head))))
|
||||
(is-comp (and (= (type-of head) "symbol") (starts-with? (symbol-name head) "~")))
|
||||
(open-step (get step-ref "v")))
|
||||
(append! tokens {"text" "(" "cls" "text-stone-400" "step" open-step})
|
||||
(build-code-tokens head tokens step-ref indent)
|
||||
(when is-tag
|
||||
(dict-set! step-ref "v" (+ (get step-ref "v") 1)))
|
||||
(for-each (fn (a)
|
||||
(let ((is-child (and (list? a) (not (empty? a))
|
||||
(= (type-of (first a)) "symbol")
|
||||
(or (is-html-tag? (symbol-name (first a)))
|
||||
(starts-with? (symbol-name (first a)) "~"))))
|
||||
(is-spread (and (list? a) (not (empty? a))
|
||||
(= (type-of (first a)) "symbol")
|
||||
(starts-with? (symbol-name (first a)) "~"))))
|
||||
(if is-spread
|
||||
;; Component spread: save counter, process, restore
|
||||
;; All tokens inside share parent open-step
|
||||
(let ((saved (get step-ref "v"))
|
||||
(saved-tokens-len (len tokens)))
|
||||
(append! tokens {"text" " " "cls" "" "step" -1})
|
||||
(build-code-tokens a tokens step-ref indent)
|
||||
;; Mark all tokens added during spread as spread tokens
|
||||
(let mark-loop ((j saved-tokens-len))
|
||||
(when (< j (len tokens))
|
||||
(dict-set! (nth tokens j) "spread" true)
|
||||
(mark-loop (+ j 1))))
|
||||
(dict-set! step-ref "v" saved))
|
||||
(if (and is-tag is-child)
|
||||
(do (append! tokens {"text" (str "\n" (join "" (map (fn (_) " ") (range 0 (+ indent 1))))) "cls" "" "step" -1})
|
||||
(build-code-tokens a tokens step-ref (+ indent 1)))
|
||||
(do (append! tokens {"text" " " "cls" "" "step" -1})
|
||||
(build-code-tokens a tokens step-ref indent))))))
|
||||
(rest expr))
|
||||
(append! tokens {"text" ")" "cls" "text-stone-400" "step" open-step})
|
||||
(when is-tag
|
||||
(dict-set! step-ref "v" (+ (get step-ref "v") 1)))))
|
||||
:else nil)))
|
||||
(get-preview (fn () (dom-query "[data-sx-lake=\"home-preview\"]")))
|
||||
(get-code-view (fn () (dom-query "[data-sx-lake=\"code-view\"]")))
|
||||
(get-stack (fn () (deref dom-stack-sig)))
|
||||
(set-stack (fn (v) (reset! dom-stack-sig v)))
|
||||
(push-stack (fn (el) (reset! dom-stack-sig (append (deref dom-stack-sig) (list el)))))
|
||||
(pop-stack (fn ()
|
||||
(let ((s (deref dom-stack-sig)))
|
||||
(when (> (len s) 1)
|
||||
(reset! dom-stack-sig (slice s 0 (- (len s) 1)))))))
|
||||
(build-code-dom (fn ()
|
||||
(when (and (empty? code-spans) (not (empty? (deref code-tokens))))
|
||||
(let ((code-el (get-code-view)))
|
||||
(when code-el
|
||||
(dom-set-prop code-el "innerHTML" "")
|
||||
(for-each (fn (tok)
|
||||
(let ((sp (dom-create-element "span" nil)))
|
||||
(dom-set-attr sp "class" (get tok "cls"))
|
||||
(dom-set-prop sp "textContent" (get tok "text"))
|
||||
(dom-append code-el sp)
|
||||
(append! code-spans (dict "el" sp "step" (get tok "step") "cls" (get tok "cls") "spread" (get tok "spread")))))
|
||||
(deref code-tokens)))))))
|
||||
(update-code-highlight (fn ()
|
||||
(let ((cur (deref step-idx)))
|
||||
(for-each (fn (s)
|
||||
(let ((step-num (get s "step"))
|
||||
(el (get s "el"))
|
||||
(base (get s "cls")))
|
||||
(when (not (= step-num -1))
|
||||
(dom-set-attr el "class"
|
||||
(str base
|
||||
(let ((is-spread (get s "spread")))
|
||||
(cond
|
||||
(and (= step-num cur) is-spread) " opacity-60"
|
||||
(= step-num cur) " bg-amber-100 rounded px-0.5 font-bold text-sm"
|
||||
(and (< step-num cur) is-spread) " opacity-60"
|
||||
(< step-num cur) " font-bold text-xs"
|
||||
:else " opacity-40")))))))
|
||||
code-spans))))
|
||||
(do-step (fn ()
|
||||
(build-code-dom)
|
||||
(when (< (deref step-idx) (len (deref steps)))
|
||||
(when (empty? (get-stack))
|
||||
(let ((p (get-preview)))
|
||||
(when p (set-stack (list p)))))
|
||||
(let ((step (nth (deref steps) (deref step-idx)))
|
||||
(step-type (get step "type"))
|
||||
(parent (if (empty? (get-stack)) (get-preview) (last (get-stack)))))
|
||||
(cond
|
||||
(= step-type "open")
|
||||
(let ((el (dom-create-element (get step "tag") nil))
|
||||
(attrs (get step "attrs"))
|
||||
(spreads (or (get step "spreads") (list))))
|
||||
(let loop ((i 0))
|
||||
(when (< i (len attrs))
|
||||
(dom-set-attr el (keyword-name (nth attrs i)) (nth attrs (+ i 1)))
|
||||
(loop (+ i 2))))
|
||||
(for-each (fn (sp)
|
||||
(let ((result (eval-expr sp (make-env))))
|
||||
(when (and result (spread? result))
|
||||
(let ((sattrs (spread-attrs result)))
|
||||
(for-each (fn (k)
|
||||
(if (= k "class")
|
||||
(dom-set-attr el "class"
|
||||
(str (or (dom-get-attr el "class") "") " " (get sattrs k)))
|
||||
(dom-set-attr el k (get sattrs k))))
|
||||
(keys sattrs))))))
|
||||
spreads)
|
||||
(when parent (dom-append parent el))
|
||||
(push-stack el))
|
||||
(= step-type "close")
|
||||
(pop-stack)
|
||||
(= step-type "leaf")
|
||||
(when parent
|
||||
(let ((val (get step "expr")))
|
||||
(dom-append parent (create-text-node (if (string? val) val (str val))))))
|
||||
(= step-type "expr")
|
||||
(let ((rendered (render-to-dom (get step "expr") (make-env) nil)))
|
||||
(when (and parent rendered)
|
||||
(dom-append parent rendered)))))
|
||||
(swap! step-idx inc)
|
||||
(update-code-highlight))))
|
||||
(do-back (fn ()
|
||||
(when (> (deref step-idx) 0)
|
||||
(let ((target (- (deref step-idx) 1))
|
||||
(container (get-preview)))
|
||||
(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)))))))
|
||||
;; Auto-parse via effect
|
||||
(effect (fn ()
|
||||
(let ((parsed (sx-parse source)))
|
||||
(when (not (empty? parsed))
|
||||
(let ((result (list))
|
||||
(step-ref (dict "v" 0)))
|
||||
(split-tag (first parsed) result)
|
||||
(reset! steps result)
|
||||
(let ((tokens (list)))
|
||||
(dict-set! step-ref "v" 0)
|
||||
(build-code-tokens (first parsed) tokens step-ref 0)
|
||||
(reset! code-tokens tokens))
|
||||
;; Defer code DOM build until lake exists
|
||||
(schedule-idle (fn ()
|
||||
(build-code-dom)
|
||||
;; Replay to initial step-idx
|
||||
(let ((target (deref step-idx)))
|
||||
(reset! step-idx 0)
|
||||
(set-stack (list (get-preview)))
|
||||
(for-each (fn (_) (do-step)) (slice (deref steps) 0 target)))
|
||||
(update-code-highlight)
|
||||
(run-post-render-hooks))))))))
|
||||
(div :class "space-y-4"
|
||||
;; Code view lake — spans built imperatively, classes updated on step
|
||||
(div (~cssx/tw :tokens "font-mono bg-stone-50 rounded p-2 overflow-x-auto leading-relaxed whitespace-pre-wrap")
|
||||
:style "font-size:0.5rem"
|
||||
(lake :id "code-view"))
|
||||
;; 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 "
|
||||
(if (> (deref step-idx) 0)
|
||||
"text-stone-600 hover:text-stone-800 hover:bg-stone-100"
|
||||
"text-stone-300 cursor-not-allowed"))
|
||||
"\u25c0")
|
||||
(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 "
|
||||
(if (< (deref step-idx) (len (deref steps)))
|
||||
"text-violet-600 hover:text-violet-800 hover:bg-violet-50"
|
||||
"text-violet-300 cursor-not-allowed"))
|
||||
"\u25b6"))
|
||||
;; Live preview lake
|
||||
(lake :id "home-preview")))))
|
||||
@@ -113,7 +113,9 @@
|
||||
(dict :label "SX and Dennett" :href "/sx/(etc.(philosophy.dennett))"
|
||||
:summary "Real patterns, intentional stance, and multiple drafts — Dennett's philosophy of mind as a framework for understanding SX.")
|
||||
(dict :label "S-Existentialism" :href "/sx/(etc.(philosophy.existentialism))"
|
||||
:summary "Existence precedes essence — Sartre, Camus, and the absurd freedom of writing a Lisp for the web.")))
|
||||
:summary "Existence precedes essence — Sartre, Camus, and the absurd freedom of writing a Lisp for the web.")
|
||||
(dict :label "Platonic SX" :href "/sx/(etc.(philosophy.platonic-sx))"
|
||||
:summary "The allegory of the cave, the theory of Forms, and why a self-defining hypermedium participates in something Plato would have recognized.")))
|
||||
|
||||
(define specs-nav-items (list
|
||||
{:label "Core" :href "/sx/(language.(spec.core))" :children (list
|
||||
|
||||
@@ -506,6 +506,7 @@
|
||||
"wittgenstein" '(~essays/sx-and-wittgenstein/essay-sx-and-wittgenstein)
|
||||
"dennett" '(~essays/sx-and-dennett/essay-sx-and-dennett)
|
||||
"existentialism" '(~essays/s-existentialism/essay-s-existentialism)
|
||||
"platonic-sx" '(~essays/platonic-sx/essay-platonic-sx)
|
||||
:else '(~essays/philosophy-index/content)))))
|
||||
|
||||
;; Plans (under etc)
|
||||
|
||||
@@ -21,16 +21,50 @@
|
||||
"No layer can be decomposed without the layer beneath it.")
|
||||
|
||||
(~docs/code :code
|
||||
(str "Layer 0: CEK machine (expression + environment + continuation)\n"
|
||||
"Layer 1: Continuations (shift / reset \u2014 delimited capture)\n"
|
||||
"Layer 2: Algebraic effects (operations + handlers)\n"
|
||||
"Layer 3: Scoped effects (+ region delimitation)\n"
|
||||
"Layer 4: SX patterns (spread, provide, island, lake, signal, collect)"))
|
||||
(str "Layer 0: CEK machine (expression + environment + continuation) \u2714 DONE\n"
|
||||
"Layer 1: Continuations (shift / reset \u2014 delimited capture) \u2714 DONE\n"
|
||||
"Layer 2: Algebraic effects (operations + handlers) \u2714 DONE\n"
|
||||
"Layer 3: Scoped effects (+ region delimitation) \u2714 DONE\n"
|
||||
"Layer 4: SX patterns (spread, provide, island, lake, signal) \u2714 DONE"))
|
||||
|
||||
(p "SX currently has layers 0, 1, and 4. "
|
||||
"Layer 3 is the scoped-effects plan (provide/context/emit!). "
|
||||
"Layer 2 falls out of layers 1 and 3 and doesn't need its own representation. "
|
||||
"This document is about layers 0 through 2 \u2014 the machinery beneath scoped effects.")
|
||||
(p "All five layers are implemented. The entire hierarchy from patterns down to raw CEK "
|
||||
"is specced in " (code ".sx") " files and bootstrapped to Python and JavaScript. "
|
||||
"No hand-written evaluation logic remains.")
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; What we built (status)
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
(h2 :class "text-xl font-bold mt-12 mb-4" "What We Built")
|
||||
|
||||
(div :class "overflow-x-auto mb-6"
|
||||
(table :class "min-w-full text-sm"
|
||||
(thead (tr
|
||||
(th :class "text-left pr-4 pb-2 font-semibold" "Layer")
|
||||
(th :class "text-left pr-4 pb-2 font-semibold" "Spec files")
|
||||
(th :class "text-left pr-4 pb-2 font-semibold" "What it provides")
|
||||
(th :class "text-left pb-2 font-semibold" "Tests")))
|
||||
(tbody
|
||||
(tr (td :class "pr-4 py-1" "0 \u2014 CEK")
|
||||
(td :class "pr-4 font-mono text-xs" "cek.sx, frames.sx")
|
||||
(td :class "pr-4" "Explicit step function, 20+ frame types, cek-call dispatch, CEK-native HO forms")
|
||||
(td "43 CEK + 26 reactive"))
|
||||
(tr (td :class "pr-4 py-1" "1 \u2014 Continuations")
|
||||
(td :class "pr-4 font-mono text-xs" "continuations.sx, callcc.sx")
|
||||
(td :class "pr-4" "shift/reset (delimited), call/cc (full), ReactiveResetFrame + DerefFrame")
|
||||
(td "Continuation tests"))
|
||||
(tr (td :class "pr-4 py-1" "2 \u2014 Effect signatures")
|
||||
(td :class "pr-4 font-mono text-xs" "boundary.sx, eval.sx")
|
||||
(td :class "pr-4" ":effects annotations on define, boundary enforcement at startup")
|
||||
(td "Boundary validation"))
|
||||
(tr (td :class "pr-4 py-1" "3 \u2014 Scoped effects")
|
||||
(td :class "pr-4 font-mono text-xs" "eval.sx, adapters")
|
||||
(td :class "pr-4" "scope/provide/context/emit!/emitted, scope-push!/scope-pop!")
|
||||
(td "Scope integration"))
|
||||
(tr (td :class "pr-4 py-1" "4 \u2014 Patterns")
|
||||
(td :class "pr-4 font-mono text-xs" "signals.sx, adapter-dom.sx, engine.sx")
|
||||
(td :class "pr-4" "signal/deref/computed/effect/batch, island/lake, spread/collect")
|
||||
(td "20 signal + 26 CEK reactive")))))
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Layer 0: The CEK machine
|
||||
@@ -65,36 +99,35 @@
|
||||
|
||||
(p "Three things, all necessary, none decomposable further.")
|
||||
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "CEK in eval.sx")
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "CEK in SX")
|
||||
|
||||
(p "SX already implements CEK. It just doesn't name it:")
|
||||
(p "The CEK machine is the default evaluator on both client (JS) and server (Python). "
|
||||
"Every " (code "eval-expr") " call goes through " (code "cek-run") ". "
|
||||
"The spec lives in two files:")
|
||||
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (code "frames.sx") " \u2014 20+ frame types (IfFrame, ArgFrame, MapFrame, ReactiveResetFrame, ...)")
|
||||
(li (code "cek.sx") " \u2014 step function, run loop, special form handlers, HO form handlers, cek-call"))
|
||||
|
||||
(p (code "cek-call") " is the universal function dispatch. "
|
||||
"It replaces the old " (code "invoke") " shim \u2014 SX lambdas go through " (code "cek-run") ", "
|
||||
"native callables through " (code "apply") ". One calling convention, bootstrapped identically to every host.")
|
||||
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "CEK-native higher-order forms")
|
||||
|
||||
(p "All higher-order forms step element-by-element through the CEK machine:")
|
||||
|
||||
(~docs/code :code
|
||||
(str ";; eval-expr IS the CEK transition function\n"
|
||||
";; C = expr, E = env, K = implicit (call stack / trampoline)\n"
|
||||
"(define eval-expr\n"
|
||||
" (fn (expr env)\n"
|
||||
" (cond\n"
|
||||
" (number? expr) expr ;; literal: C \u2192 value, K unchanged\n"
|
||||
" (string? expr) expr\n"
|
||||
" (symbol? expr) (env-get env expr) ;; variable: C + E \u2192 value\n"
|
||||
" (list? expr) ;; compound: modify K (push frame)\n"
|
||||
" (let ((head (first expr)))\n"
|
||||
" ...))))\n"
|
||||
(str ";; map pushes a MapFrame, calls f on each element via continue-with-call\n"
|
||||
"(map (fn (x) (* 2 (deref counter))) items)\n"
|
||||
"\n"
|
||||
";; The trampoline IS the K register made explicit:\n"
|
||||
";; instead of growing the call stack, thunks are continuations\n"
|
||||
"(define trampoline\n"
|
||||
" (fn (val)\n"
|
||||
" (let loop ((v val))\n"
|
||||
" (if (thunk? v)\n"
|
||||
" (loop (eval-expr (thunk-expr v) (thunk-env v)))\n"
|
||||
" v))))"))
|
||||
";; deref inside the callback goes through CEK's DerefFrame\n"
|
||||
";; \u2192 reactive-shift-deref fires if inside a reactive-reset boundary\n"
|
||||
";; \u2192 the continuation from deref to the MapFrame is captured as a subscriber"))
|
||||
|
||||
(p "The trampoline is the K register. Thunks are suspended continuations. "
|
||||
"Tail-call optimization is replacing K instead of extending it. "
|
||||
"SX's evaluation model is already a CEK machine \u2014 the plan is to make this explicit, "
|
||||
"not to build something new.")
|
||||
(p "This means " (code "deref") " works inside " (code "map") ", " (code "filter") ", "
|
||||
(code "reduce") ", " (code "for-each") ", " (code "some") ", " (code "every?") " callbacks. "
|
||||
"The HO forms don't escape to tree-walk \u2014 the CEK machine processes every step.")
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Layer 1: Delimited continuations
|
||||
@@ -116,15 +149,25 @@
|
||||
(code "shift") " says: \"give me everything between here and that boundary as a callable function.\" "
|
||||
"The captured continuation " (em "is") " a slice of the K register.")
|
||||
|
||||
(p "This is already specced in SX (continuations.sx). What it gives us beyond CEK:")
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "Deref as shift")
|
||||
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (strong "Suspendable computation") " \u2014 capture where you are, resume later")
|
||||
(li (strong "Backtracking") " \u2014 capture a choice point, try alternatives")
|
||||
(li (strong "Coroutines") " \u2014 two computations yielding to each other")
|
||||
(li (strong "Async as a library") " \u2014 async/await is shift/reset with a scheduler"))
|
||||
(p "The reactive payoff. " (code "deref") " inside a " (code "reactive-reset") " boundary "
|
||||
"is shift/reset applied to signals:")
|
||||
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "The Filinski Embedding")
|
||||
(~docs/code :code
|
||||
(str ";; User writes:\n"
|
||||
"(div :class (str \"count-\" (deref counter))\n"
|
||||
" (str \"Value: \" (deref counter)))\n"
|
||||
"\n"
|
||||
";; CEK sees (deref counter) \u2192 signal? \u2192 reactive-reset on stack?\n"
|
||||
";; Yes: capture (str \"count-\" [HOLE]) as continuation\n"
|
||||
";; Register as subscriber. Return current value.\n"
|
||||
";; When counter changes: re-invoke continuation \u2192 update DOM."))
|
||||
|
||||
(p "No explicit " (code "effect()") " wrapping needed. "
|
||||
"The continuation capture IS the subscription mechanism.")
|
||||
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "The Filinski embedding")
|
||||
|
||||
(p "Filinski (1994) proved that " (code "shift/reset") " can encode "
|
||||
(em "any") " monadic effect. State, exceptions, nondeterminism, I/O, "
|
||||
@@ -132,78 +175,6 @@
|
||||
"This means layer 1 is already computationally complete for effects. "
|
||||
"Everything above is structure, not power.")
|
||||
|
||||
(p "This is the key insight: "
|
||||
(strong "layers 2\u20134 add no computational power. ") "They add " (em "structure") " \u2014 "
|
||||
"they make effects composable, nameable, handleable. "
|
||||
"But anything you can do with scoped effects, "
|
||||
"you can do with raw shift/reset. You'd just hate writing it.")
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Layer 2: Algebraic effects
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
(h2 :class "text-xl font-bold mt-12 mb-4" "Layer 2: Algebraic Effects")
|
||||
|
||||
(p "Plotkin & Pretnar (2009) observed that most effects have algebraic structure: "
|
||||
"an operation (\"perform this effect\") and a handler (\"here's what that effect means\"). "
|
||||
"The handler receives the operation's argument and a continuation to resume the program.")
|
||||
|
||||
(~docs/code :code
|
||||
(str ";; Pseudocode \u2014 algebraic effect style\n"
|
||||
"(handle\n"
|
||||
" (fn () (+ 1 (perform :ask \"what number?\")))\n"
|
||||
" {:ask (fn (prompt resume)\n"
|
||||
" (resume 41))})\n"
|
||||
";; => 42"))
|
||||
|
||||
(p (code "perform") " is shift. " (code "handle") " is reset. "
|
||||
"But with names and types. The handler pattern gives you:")
|
||||
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (strong "Named effects") " \u2014 not just \"capture here\" but \"I need state / logging / auth\"")
|
||||
(li (strong "Composable handlers") " \u2014 stack handlers, each handling different effects")
|
||||
(li (strong "Effect signatures") " \u2014 a function declares what effects it needs; "
|
||||
"the type system ensures all effects are handled"))
|
||||
|
||||
(p "Plotkin & Power (2003) proved that this captures: "
|
||||
"state, exceptions, nondeterminism, I/O, cooperative concurrency, "
|
||||
"probability, and backtracking. All as instances of one algebraic structure.")
|
||||
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "What algebraic effects cannot express")
|
||||
|
||||
(p "Standard algebraic effects have a limitation: their operations are " (em "first-order") ". "
|
||||
"An operation takes a value and produces a value. But some effects need operations that "
|
||||
"take " (em "computations") " as arguments:")
|
||||
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (code "catch") " \u2014 takes a computation that might throw, runs it with a handler")
|
||||
(li (code "local") " \u2014 takes a computation, runs it with modified state")
|
||||
(li (code "once") " \u2014 takes a nondeterministic computation, commits to its first result")
|
||||
(li (code "scope") " \u2014 takes a computation, runs it within a delimited region"))
|
||||
|
||||
(p "These are " (strong "higher-order effects") ". They need computations as arguments, "
|
||||
"not just values. This is precisely what the scoped-effects plan addresses.")
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Layer 3: Scoped effects (the bridge)
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
(h2 :class "text-xl font-bold mt-12 mb-4" "Layer 3: Scoped and Higher-Order Effects")
|
||||
|
||||
(p "Wu, Schrijvers, and Hinze (2014) introduced " (em "scoped effects") " \u2014 "
|
||||
"algebraic effects extended with operations that delimit regions. "
|
||||
"Pirog, Polesiuk, and Sieczkowski (2018) proved these are "
|
||||
(strong "strictly more expressive") " than standard algebraic effects.")
|
||||
|
||||
(p "Bach Poulsen and van der Rest (2023) generalized further with "
|
||||
(em "hefty algebras") " \u2014 a framework that captures " (em "all") " known higher-order effects, "
|
||||
"with scoped effects as a special case. This is the current state of the art.")
|
||||
|
||||
(p "SX's " (code "provide") " is a scoped effect. It creates a region (the body), "
|
||||
"makes a value available within it (context), and collects contributions from within it (emit/emitted). "
|
||||
"This is why it can express things that plain algebraic effects can't: "
|
||||
"the region boundary is part of the effect, not an accident of the call stack.")
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; The floor proof
|
||||
;; -----------------------------------------------------------------------
|
||||
@@ -228,85 +199,96 @@
|
||||
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (strong "C without E") " = combinatory logic. Turing-complete but inhumane. "
|
||||
"No named bindings \u2014 everything via S, K, I combinators. "
|
||||
"Proves you can drop E in theory, but the resulting system "
|
||||
"can't express abstraction (which is what E provides).")
|
||||
"No named bindings \u2014 everything via S, K, I combinators.")
|
||||
(li (strong "C without K") " = single expression evaluation. "
|
||||
"You can compute one thing but can't compose it with anything. "
|
||||
"Technically you can encode K into C (CPS transform), "
|
||||
"but this transforms the expression to include the continuation explicitly \u2014 "
|
||||
"K hasn't been removed, just moved into C.")
|
||||
"You can compute one thing but can't compose it with anything.")
|
||||
(li (strong "E without C") " = a phone book with no one to call.")
|
||||
(li (strong "K without C") " = a to-do list with nothing on it."))
|
||||
|
||||
(p "You can " (em "encode") " any register into another (CPS eliminates K, "
|
||||
"De Bruijn indices eliminate E), but encoding isn't elimination. "
|
||||
"The information is still there, just hidden in a different representation. "
|
||||
"Three independent concerns; three registers.")
|
||||
"The information is still there, just hidden in a different representation.")
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Open questions
|
||||
;; Digging deeper: what's next
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
(h2 :class "text-xl font-bold mt-12 mb-4" "Open Questions")
|
||||
(h2 :class "text-xl font-bold mt-12 mb-4" "Digging Deeper")
|
||||
|
||||
(p "The hierarchy above is well-established for " (em "sequential") " computation. "
|
||||
"But there are orthogonal axes where the story is incomplete:")
|
||||
(p "The hierarchy is built. Now we test whether the floor is solid. "
|
||||
"Each step below validates the foundation before superstructure is added.")
|
||||
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "Concurrency")
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "Step 1: Serializable CEK state")
|
||||
|
||||
(p "Scoped effects assume tree-shaped execution: one thing happens, then the next. "
|
||||
"But real computation forks:")
|
||||
(p (code "make-cek-state") " already returns a plain dict. Can we serialize it, "
|
||||
"ship it to another machine, and resume?")
|
||||
|
||||
(~docs/code :code
|
||||
(str ";; Freeze a computation mid-flight\n"
|
||||
"(let ((state (make-cek-state expr env (list))))\n"
|
||||
" ;; Step a few times\n"
|
||||
" (let ((state2 (cek-step (cek-step state))))\n"
|
||||
" ;; Serialize to JSON\n"
|
||||
" (json-serialize state2)\n"
|
||||
" ;; Ship to worker, persist to disk, or content-address as CID\n"
|
||||
" ))"))
|
||||
|
||||
(p "If this works, every SX computation is a value. "
|
||||
"If it breaks, the state representation needs fixing " (em "before") " we build channels and fork/join on top.")
|
||||
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li "Video processing \u2014 pipeline stages run in parallel, frames are processed concurrently")
|
||||
(li "CI/CD \u2014 test suites fork, builds parallelize, deployment is staged")
|
||||
(li "Web rendering \u2014 async I/O, streaming SSE, suspense boundaries")
|
||||
(li "Art DAG \u2014 the entire engine is a DAG of dependent transforms"))
|
||||
(li "Can environments serialize? (They're dicts with parent chains \u2014 cycles?)")
|
||||
(li "Can continuations serialize? (Frames are dicts, but closures inside them?)")
|
||||
(li "Can native functions serialize? (No \u2014 need a registry mapping names to functions)")
|
||||
(li "What about signals? (Dict with mutable subscribers list \u2014 serialize the value, not the graph)"))
|
||||
|
||||
(p "The \u03c0-calculus (Milner 1999) handles concurrency well but not effects. "
|
||||
"Effect systems handle effects well but not concurrency. "
|
||||
"Combining them is an open problem. "
|
||||
"Brachth\u00e4user, Schuster, and Ostermann (2020) have partial results for "
|
||||
"algebraic effects with multi-shot handlers (where the continuation can be invoked "
|
||||
"on multiple concurrent threads), but a full synthesis doesn't exist yet.")
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "Step 2: CEK stepping debugger")
|
||||
|
||||
(p "For SX, this matters because the Art DAG is fundamentally a concurrent execution engine. "
|
||||
"If SX ever specifies DAG execution natively, it'll need something beyond scoped effects.")
|
||||
(p (code "cek-step") " is pure data\u2192data. A debugger is just a UI over it:")
|
||||
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "Linearity")
|
||||
(~docs/code :code
|
||||
(str ";; The debugger is an island that renders CEK state\n"
|
||||
"(defisland ~debugger (&key expr)\n"
|
||||
" (let ((state (signal (make-cek-state (parse expr) env (list)))))\n"
|
||||
" (div\n"
|
||||
" (button :on-click (fn () (swap! state cek-step)) \"Step\")\n"
|
||||
" (pre (str \"C: \" (inspect (get (deref state) \"control\"))))\n"
|
||||
" (pre (str \"K: \" (len (get (deref state) \"kont\")) \" frames\")))))"))
|
||||
|
||||
(p "Can an effect handler be invoked more than once? Must a scope be entered? "
|
||||
"Must every emitted value be consumed?")
|
||||
(p "This is a live island. Each click of Step calls " (code "cek-step") " on the state signal. "
|
||||
"The deref-as-shift mechanism updates the DOM. No framework, no virtual DOM, no diffing.")
|
||||
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "Step 3: Content-addressed computation")
|
||||
|
||||
(p "Hash a CEK state \u2192 CID. This is where SX meets the Art DAG:")
|
||||
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (strong "Unrestricted") " \u2014 use as many times as you want (current SX)")
|
||||
(li (strong "Affine") " \u2014 use at most once (Rust's ownership model)")
|
||||
(li (strong "Linear") " \u2014 use exactly once (quantum no-cloning, exactly-once delivery)"))
|
||||
(li "A CID identifies a computation in progress, not just a value")
|
||||
(li "Two machines given the same CID produce the same result (deterministic)")
|
||||
(li "Memoize: if you've already run this state, return the cached result")
|
||||
(li "Distribute: ship the CID to whichever machine has the data it needs")
|
||||
(li "Verify: re-run from the CID, check the result matches"))
|
||||
|
||||
(p "Linear types constrain the continuation: if a handler is linear, "
|
||||
"it " (em "must") " resume the computation exactly once. No dropping (resource leak), "
|
||||
"no duplicating (nondeterminism). This connects to:")
|
||||
(p "This requires solving Step 1 (serialization) first. "
|
||||
"Content-addressing is serialization + hashing.")
|
||||
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li "Resource management \u2014 file handles that " (em "must") " be closed")
|
||||
(li "Protocol correctness \u2014 a session type that must complete")
|
||||
(li "Transaction semantics \u2014 exactly-once commit/rollback")
|
||||
(li "Quantum computing \u2014 no-cloning theorem as a type constraint"))
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "Step 4: Concurrent CEK")
|
||||
|
||||
(p "Benton (1994) established the connection between linear logic and computation. "
|
||||
"Adding linear effects to SX would constrain what handlers can do, "
|
||||
"enabling stronger guarantees about resource safety. "
|
||||
"This is orthogonal to depth \u2014 it's about " (em "discipline") ", not " (em "power") ".")
|
||||
(p "Multiple CEK machines running in parallel, communicating via channels:")
|
||||
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "Higher-order effects beyond hefty algebras")
|
||||
(~docs/code :code
|
||||
(str ";; Fork: two CEK states from one\n"
|
||||
"(let ((left (make-cek-state expr-a env (list)))\n"
|
||||
" (right (make-cek-state expr-b env (list))))\n"
|
||||
" ;; Interleave steps\n"
|
||||
" (scheduler (list left right)))"))
|
||||
|
||||
(p "Nobody has proved that hefty algebras capture " (em "all possible") " higher-order effects. "
|
||||
"The hierarchy might continue. This is active research with no clear terminus. "
|
||||
"The question \"is there a structured effect that no framework can express?\" "
|
||||
"is analogous to G\u00f6del's incompleteness \u2014 it may be that every effect framework "
|
||||
"has blind spots, and the only \"complete\" system is raw continuations (layer 1), "
|
||||
"which are universal but unstructured.")
|
||||
(p "The Art DAG is the natural first consumer. Its execution model is already "
|
||||
"a DAG of dependent computations. CEK states as DAG nodes. "
|
||||
"Channels as edges. The scheduler is the DAG executor.")
|
||||
|
||||
(p "This is where scoped effects meet the \u03c0-calculus. "
|
||||
"But it depends on Steps 1\u20133 being solid.")
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; The three-axis model
|
||||
@@ -318,242 +300,29 @@
|
||||
"It's a point in a three-dimensional space:")
|
||||
|
||||
(~docs/code :code
|
||||
(str "depth: CEK \u2192 continuations \u2192 algebraic effects \u2192 scoped effects\n"
|
||||
"topology: sequential \u2192 concurrent \u2192 distributed\n"
|
||||
"linearity: unrestricted \u2192 affine \u2192 linear"))
|
||||
(str "depth: CEK \u2192 continuations \u2192 algebraic effects \u2192 scoped effects [all done]\n"
|
||||
"topology: sequential \u2192 concurrent \u2192 distributed [sequential done]\n"
|
||||
"linearity: unrestricted \u2192 affine \u2192 linear [unrestricted done]"))
|
||||
|
||||
(p "Each axis is independent. You can have:")
|
||||
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li "Scoped effects + sequential + unrestricted \u2014 " (strong "SX today"))
|
||||
(li "Scoped effects + concurrent + unrestricted \u2014 the Art DAG integration")
|
||||
(li "Scoped effects + sequential + linear \u2014 resource-safe SX")
|
||||
(li "Algebraic effects + concurrent + linear \u2014 something like Rust + Tokio + effect handlers")
|
||||
(li "CEK + distributed + unrestricted \u2014 raw Erlang/BEAM"))
|
||||
|
||||
(p "SX's current position and trajectory:")
|
||||
(p "Each axis is independent. SX's current position:")
|
||||
|
||||
(div :class "overflow-x-auto mb-6"
|
||||
(table :class "min-w-full text-sm"
|
||||
(thead (tr
|
||||
(th :class "text-left pr-4 pb-2 font-semibold" "Axis")
|
||||
(th :class "text-left pr-4 pb-2 font-semibold" "Current")
|
||||
(th :class "text-left pr-4 pb-2 font-semibold" "Next")
|
||||
(th :class "text-left pb-2 font-semibold" "Eventual")))
|
||||
(th :class "text-left pb-2 font-semibold" "Next")))
|
||||
(tbody
|
||||
(tr (td :class "pr-4 py-1" "Depth")
|
||||
(td :class "pr-4" "Layer 4 (patterns) + Layer 1 (continuations)")
|
||||
(td :class "pr-4" "Layer 3 (scoped effects)")
|
||||
(td "Layer 0 (explicit CEK)"))
|
||||
(td :class "pr-4" "All layers (0\u20134) implemented")
|
||||
(td "Validate via serialization + stepping"))
|
||||
(tr (td :class "pr-4 py-1" "Topology")
|
||||
(td :class "pr-4" "Sequential")
|
||||
(td :class "pr-4" "Async I/O (partial concurrency)")
|
||||
(td "DAG execution (Art DAG)"))
|
||||
(td :class "pr-4" "Sequential (+ async I/O)")
|
||||
(td "Concurrent CEK (Art DAG integration)"))
|
||||
(tr (td :class "pr-4 py-1" "Linearity")
|
||||
(td :class "pr-4" "Unrestricted")
|
||||
(td :class "pr-4" "Unrestricted")
|
||||
(td "Affine (resource safety)")))))
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; What explicit CEK gives SX
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
(h2 :class "text-xl font-bold mt-12 mb-4" "What Explicit CEK Gives SX")
|
||||
|
||||
(p "Making the CEK machine explicit in the spec (rather than implicit in eval-expr) enables:")
|
||||
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "Stepping")
|
||||
|
||||
(p "A CEK machine transitions one step at a time. "
|
||||
"If the transition function is explicit, you can:")
|
||||
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li "Single-step through evaluation (debugger)")
|
||||
(li "Pause and serialize mid-evaluation (suspend to disk, resume on another machine)")
|
||||
(li "Instrument each step (profiling, tracing, time-travel debugging)")
|
||||
(li "Interleave steps from multiple computations (cooperative scheduling without OS threads)"))
|
||||
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "Serializable computation")
|
||||
|
||||
(p "If C, E, and K are all data structures (not host stack frames), "
|
||||
"the entire computation state is serializable:")
|
||||
|
||||
(~docs/code :code
|
||||
(str ";; Freeze a computation mid-flight\n"
|
||||
"(let ((state (capture-cek)))\n"
|
||||
" (send-to-worker state) ;; ship to another machine\n"
|
||||
" ;; or: (store state) ;; persist to disk\n"
|
||||
" ;; or: (fork state) ;; run the same computation twice\n"
|
||||
" )"))
|
||||
|
||||
(p "This connects to content-addressed computation: "
|
||||
"a CID identifying a CEK state is a pointer to a computation in progress. "
|
||||
"Resume it anywhere. Verify it. Cache it. Share it.")
|
||||
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "Formal verification")
|
||||
|
||||
(p "An explicit CEK machine is a state machine. State machines are verifiable. "
|
||||
"You can prove properties about all possible execution paths: "
|
||||
"termination, resource bounds, effect safety. "
|
||||
"The theorem prover (prove.sx) could verify CEK transitions directly.")
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; SX's existing layers
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
(h2 :class "text-xl font-bold mt-12 mb-4" "SX's Existing Layers")
|
||||
|
||||
(p "SX already has most of the hierarchy, specced or planned:")
|
||||
|
||||
(div :class "overflow-x-auto mb-6"
|
||||
(table :class "min-w-full text-sm"
|
||||
(thead (tr
|
||||
(th :class "text-left pr-4 pb-2 font-semibold" "Layer")
|
||||
(th :class "text-left pr-4 pb-2 font-semibold" "SX has")
|
||||
(th :class "text-left pr-4 pb-2 font-semibold" "Where")
|
||||
(th :class "text-left pb-2 font-semibold" "Status")))
|
||||
(tbody
|
||||
(tr (td :class "pr-4 py-1" "0 \u2014 CEK")
|
||||
(td :class "pr-4" "eval-expr + trampoline")
|
||||
(td :class "pr-4" "eval.sx")
|
||||
(td "Implicit. Works, but CEK is hidden in the host call stack."))
|
||||
(tr (td :class "pr-4 py-1" "1 \u2014 Continuations")
|
||||
(td :class "pr-4" "shift / reset")
|
||||
(td :class "pr-4" "continuations.sx")
|
||||
(td "Specced. Bootstraps to Python and JavaScript."))
|
||||
(tr (td :class "pr-4 py-1" "2 \u2014 Algebraic effects")
|
||||
(td :class "pr-4" "\u2014")
|
||||
(td :class "pr-4" "\u2014")
|
||||
(td "Falls out of layers 1 + 3. No dedicated spec needed."))
|
||||
(tr (td :class "pr-4 py-1" "3 \u2014 Scoped effects")
|
||||
(td :class "pr-4" "provide / context / emit!")
|
||||
(td :class "pr-4" "scoped-effects plan")
|
||||
(td "Planned. Implements over eval.sx + adapters."))
|
||||
(tr (td :class "pr-4 py-1" "4 \u2014 Patterns")
|
||||
(td :class "pr-4" "spread, collect, island, lake, signal, store")
|
||||
(td :class "pr-4" "various .sx files")
|
||||
(td "Implemented. Will be redefined in terms of layer 3.")))))
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Implementation path
|
||||
;; -----------------------------------------------------------------------
|
||||
|
||||
(h2 :class "text-xl font-bold mt-12 mb-4" "Implementation Path")
|
||||
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "Phase 1: Scoped effects (Layer 3)")
|
||||
|
||||
(p "This is the scoped-effects plan. Immediate next step.")
|
||||
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li "Spec " (code "provide") ", " (code "context") ", " (code "emit!") ", " (code "emitted") " in eval.sx")
|
||||
(li "Implement in all adapters (HTML, DOM, SX wire, async)")
|
||||
(li "Redefine spread, collect, and reactive-spread as instances")
|
||||
(li "Prove existing tests still pass"))
|
||||
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "Phase 2: Effect signatures (Layer 2)")
|
||||
|
||||
(p "Add optional effect annotations to function definitions:")
|
||||
|
||||
(~docs/code :code
|
||||
(str ";; Declare what effects a function uses\n"
|
||||
"(define fetch-user :effects [io auth]\n"
|
||||
" (fn (id) ...))\n"
|
||||
"\n"
|
||||
";; Pure function \u2014 no effects\n"
|
||||
"(define add :effects []\n"
|
||||
" (fn (a b) (+ a b)))\n"
|
||||
"\n"
|
||||
";; Scoped effect \u2014 uses context + emit\n"
|
||||
"(define themed-heading :effects [context]\n"
|
||||
" (fn (text)\n"
|
||||
" (h1 :style (str \"color:\" (get (context \"theme\") :primary))\n"
|
||||
" text)))"))
|
||||
|
||||
(p "Effect signatures are checked at registration time. "
|
||||
"A function that declares " (code ":effects []") " cannot call " (code "emit!") " or " (code "context") ". "
|
||||
"This is layer 2 \u2014 algebraic effect structure \u2014 applied as a type discipline.")
|
||||
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "Phase 3: Explicit CEK (Layer 0)")
|
||||
|
||||
(p "Refactor eval.sx to expose the CEK registers as data:")
|
||||
|
||||
(~docs/code :code
|
||||
(str ";; The CEK state is a value\n"
|
||||
"(define-record CEK\n"
|
||||
" :control expr ;; the expression\n"
|
||||
" :env env ;; the bindings\n"
|
||||
" :kont kont) ;; the continuation stack\n"
|
||||
"\n"
|
||||
";; One step\n"
|
||||
"(define step :effects []\n"
|
||||
" (fn (cek)\n"
|
||||
" (case (type-of (get cek :control))\n"
|
||||
" :literal (apply-kont (get cek :kont) (get cek :control))\n"
|
||||
" :symbol (apply-kont (get cek :kont)\n"
|
||||
" (env-get (get cek :env) (get cek :control)))\n"
|
||||
" :list (let ((head (first (get cek :control))))\n"
|
||||
" ...))))\n"
|
||||
"\n"
|
||||
";; Run to completion\n"
|
||||
"(define run :effects []\n"
|
||||
" (fn (cek)\n"
|
||||
" (if (final? cek)\n"
|
||||
" (get cek :control)\n"
|
||||
" (run (step cek)))))"))
|
||||
|
||||
(p "This makes computation " (em "inspectable") ". A CEK state can be:")
|
||||
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li "Serialized to a CID (content-addressed frozen computation)")
|
||||
(li "Single-stepped by a debugger")
|
||||
(li "Forked (run the same state with different inputs)")
|
||||
(li "Migrated (ship to another machine, resume there)")
|
||||
(li "Verified (prove properties about all reachable states)"))
|
||||
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "Phase 4: Concurrent effects (topology axis)")
|
||||
|
||||
(p "Extend the CEK machine to support multiple concurrent computations:")
|
||||
|
||||
(~docs/code :code
|
||||
(str ";; Fork: create two CEK states from one\n"
|
||||
"(define fork :effects [concurrency]\n"
|
||||
" (fn (cek)\n"
|
||||
" (list (step cek) (step cek))))\n"
|
||||
"\n"
|
||||
";; Join: merge results from two computations\n"
|
||||
"(define join :effects [concurrency]\n"
|
||||
" (fn (cek-a cek-b combine)\n"
|
||||
" (combine (run cek-a) (run cek-b))))\n"
|
||||
"\n"
|
||||
";; Channel: typed communication between concurrent computations\n"
|
||||
"(define channel :effects [concurrency]\n"
|
||||
" (fn (name)\n"
|
||||
" {:send (fn (v) (emit! name v))\n"
|
||||
" :recv (fn () (shift k (handle-recv name k)))}))"))
|
||||
|
||||
(p "This is where scoped effects meet the \u03c0-calculus. "
|
||||
"The Art DAG is the natural first consumer \u2014 "
|
||||
"its execution model is already a DAG of dependent computations.")
|
||||
|
||||
(h3 :class "text-lg font-semibold mt-8 mb-3" "Phase 5: Linear effects (linearity axis)")
|
||||
|
||||
(p "Add resource-safety constraints:")
|
||||
|
||||
(~docs/code :code
|
||||
(str ";; Linear scope: must be entered, must complete\n"
|
||||
"(define-linear open-file :effects [io linear]\n"
|
||||
" (fn (path)\n"
|
||||
" (provide \"file\" (fs-open path)\n"
|
||||
" ;; body MUST consume the file handle exactly once\n"
|
||||
" ;; compiler error if handle is dropped or duplicated\n"
|
||||
" (yield (file-read (context \"file\")))\n"
|
||||
" ;; cleanup runs unconditionally\n"
|
||||
" )))"))
|
||||
|
||||
(p "This is the furthest horizon. "
|
||||
"Linear effects connect SX to session types, protocol verification, "
|
||||
"and the kind of safety guarantees that Rust provides at the type level.")
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; The Curry-Howard correspondence
|
||||
;; -----------------------------------------------------------------------
|
||||
@@ -584,21 +353,9 @@
|
||||
(tr (td :class "pr-4 py-1" "Implication (A \u2192 B)")
|
||||
(td :class "pr-4" "Function type")
|
||||
(td "Lambda"))
|
||||
(tr (td :class "pr-4 py-1" "Conjunction (A \u2227 B)")
|
||||
(td :class "pr-4" "Product type")
|
||||
(td "Dict / record"))
|
||||
(tr (td :class "pr-4 py-1" "Disjunction (A \u2228 B)")
|
||||
(td :class "pr-4" "Sum type / union")
|
||||
(td "Case dispatch"))
|
||||
(tr (td :class "pr-4 py-1" "Universal (\u2200x.P)")
|
||||
(td :class "pr-4" "Polymorphism")
|
||||
(td "Generic components"))
|
||||
(tr (td :class "pr-4 py-1" "Existential (\u2203x.P)")
|
||||
(td :class "pr-4" "Abstract type")
|
||||
(td "Opaque scope"))
|
||||
(tr (td :class "pr-4 py-1" "Double negation (\u00ac\u00acA)")
|
||||
(td :class "pr-4" "Continuation")
|
||||
(td "shift/reset")))))
|
||||
(td "shift/reset, deref-as-shift")))))
|
||||
|
||||
(p "A program is a proof that a computation is possible. "
|
||||
"An effect signature is a proposition about what the program does to the world. "
|
||||
@@ -607,8 +364,7 @@
|
||||
|
||||
(p "This is why CEK is the floor: it " (em "is") " logic. "
|
||||
"Expression = proposition, environment = hypotheses, continuation = proof context. "
|
||||
"You can't go beneath logic and still be doing computation. "
|
||||
"The Curry\u2013Howard correspondence is not a metaphor. It's an isomorphism.")
|
||||
"You can't go beneath logic and still be doing computation.")
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Summary
|
||||
@@ -616,46 +372,19 @@
|
||||
|
||||
(h2 :class "text-xl font-bold mt-12 mb-4" "Summary")
|
||||
|
||||
(p "The path from where SX stands to the computational floor:")
|
||||
(p "The depth axis is complete. Every layer from patterns down to raw CEK "
|
||||
"is specced, bootstrapped, and tested. 89 tests across three suites.")
|
||||
|
||||
(div :class "overflow-x-auto mb-6"
|
||||
(table :class "min-w-full text-sm"
|
||||
(thead (tr
|
||||
(th :class "text-left pr-4 pb-2 font-semibold" "Step")
|
||||
(th :class "text-left pr-4 pb-2 font-semibold" "What")
|
||||
(th :class "text-left pr-4 pb-2 font-semibold" "Enables")
|
||||
(th :class "text-left pb-2 font-semibold" "Depends on")))
|
||||
(tbody
|
||||
(tr (td :class "pr-4 py-1" "1")
|
||||
(td :class "pr-4" "Scoped effects")
|
||||
(td :class "pr-4" "Unify spread/collect/island/lake/context")
|
||||
(td "eval.sx + adapters"))
|
||||
(tr (td :class "pr-4 py-1" "2")
|
||||
(td :class "pr-4" "Effect signatures")
|
||||
(td :class "pr-4" "Static effect checking, pure/IO boundary")
|
||||
(td "types.sx + scoped effects"))
|
||||
(tr (td :class "pr-4 py-1" "3")
|
||||
(td :class "pr-4" "Explicit CEK")
|
||||
(td :class "pr-4" "Stepping, serialization, migration, verification")
|
||||
(td "eval.sx refactor"))
|
||||
(tr (td :class "pr-4 py-1" "4")
|
||||
(td :class "pr-4" "Concurrent effects")
|
||||
(td :class "pr-4" "DAG execution, parallel pipelines")
|
||||
(td "CEK + channels"))
|
||||
(tr (td :class "pr-4 py-1" "5")
|
||||
(td :class "pr-4" "Linear effects")
|
||||
(td :class "pr-4" "Resource safety, protocol verification")
|
||||
(td "Effect signatures + linear types")))))
|
||||
(p "The next moves are lateral, not downward:")
|
||||
|
||||
(p "Each step is independently valuable. The first is immediate. "
|
||||
"The last may be years out or may never arrive. "
|
||||
"The hierarchy exists whether we traverse it or not \u2014 "
|
||||
"it's the structure of computation itself, "
|
||||
"and SX is better for knowing where it sits within it.")
|
||||
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||
(li (strong "Validate the floor") " \u2014 serialize CEK state, build a stepping debugger, "
|
||||
"content-address computations. These test whether the foundation holds weight.")
|
||||
(li (strong "Topology") " \u2014 concurrent CEK for the Art DAG. Multiple machines, channels, scheduling.")
|
||||
(li (strong "Linearity") " \u2014 resource safety. Affine continuations that must be resumed exactly once."))
|
||||
|
||||
(p "Each step is independently valuable. The foundation is built. "
|
||||
"Now we find out what it can carry.")
|
||||
|
||||
(p :class "text-stone-500 text-sm italic mt-12"
|
||||
"The true foundation of any language is not its syntax or its runtime "
|
||||
"but the mathematical structure it participates in. "
|
||||
"SX is an s-expression language, which makes it a notation for the lambda calculus, "
|
||||
"which is a notation for logic, which is the structure of thought. "
|
||||
"The floor is thought itself. We can't go deeper, because there's no one left to dig.")))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; SX app boot — styles and behaviors injected on page load
|
||||
;; SX app boot — styles, behaviors, and post-render hooks
|
||||
;;
|
||||
;; Replaces inline_css and init_sx from Python app config.
|
||||
;; Called as a data-init script on every page.
|
||||
@@ -11,6 +11,25 @@
|
||||
(collect! "cssx" "@keyframes sxJiggle{0%,100%{transform:translateX(0)}25%{transform:translateX(-.5px)}75%{transform:translateX(.5px)}}")
|
||||
(collect! "cssx" "a.sx-request{animation:sxJiggle .3s ease-in-out infinite}")
|
||||
|
||||
;; CSSX flush hook — inject collected CSS rules into a <style> tag.
|
||||
;; The spec calls (run-post-render-hooks) after hydration/swap/mount.
|
||||
;; This is the application's CSS injection strategy.
|
||||
(console-log "init-client: registering cssx flush hook, type:" (type-of (fn () nil)))
|
||||
(register-post-render-hook
|
||||
(fn ()
|
||||
(console-log "cssx flush: running, rules:" (len (collected "cssx")))
|
||||
(let ((rules (collected "cssx")))
|
||||
(when (not (empty? rules))
|
||||
(let ((style (or (dom-query "[data-cssx]")
|
||||
(let ((s (dom-create-element "style" nil)))
|
||||
(dom-set-attr s "data-cssx" "")
|
||||
(dom-append-to-head s)
|
||||
s))))
|
||||
(dom-set-prop style "textContent"
|
||||
(str (or (dom-get-prop style "textContent") "")
|
||||
(join "" rules))))
|
||||
(clear-collected! "cssx")))))
|
||||
|
||||
;; Nav link aria-selected update on client-side routing
|
||||
(dom-listen (dom-body) "sx:clientRoute"
|
||||
(fn (e)
|
||||
|
||||
Reference in New Issue
Block a user