From ed68b9883db286bc35d5a57b10b6a5751a3e7cf5 Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 30 Jun 2026 23:49:41 +0000 Subject: [PATCH] =?UTF-8?q?host:=20execute-fold=20=E2=80=94=20universality?= =?UTF-8?q?=20proven=20with=20a=20second=20fold=20(composition=20step=207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The keystone validation of the universal-algebra thesis. lib/host/execute.sx is a SECOND interpreter over the SAME seq/alt/each composition algebra as the render-fold — but a different fold: leaves are EFFECTS, seq = steps in order, alt+when = branch, each = for-each, and the accumulator is an effect log instead of an HTML string. It REUSES compose.sx's shared machinery verbatim — host/comp--pred? (when), host/comp--field (field/value), host/comp--source (each source) — so the predicate set, context-environment, and iteration source are domain-agnostic; only the leaf semantics + accumulator are new. KEYSTONE (tested): ONE (alt (when (has "auth") …) …) skeleton + ONE context folds two ways — render picks the branch → "in", execute picks the SAME branch → {:verb "enter"}. A publish workflow (validate → branch-on-status → notify-each) runs as one execute-fold over a composition object. So the behaviour model (Slice 9) is "an execute-fold over a composition object", not a separate system — the way the recursive tree proved recursion, this proves the algebra is domain-agnostic. host/exec-run; 13/13 (new execute suite); wired into conformance + serve. Full host conformance 371/373 in 42s (warm); the 2 fails are the pre-existing relate-picker pair. Co-Authored-By: Claude Opus 4.8 --- lib/host/conformance.sh | 2 + lib/host/execute.sx | 76 +++++++++++++++++++++++++++++++ lib/host/serve.sh | 1 + lib/host/tests/execute.sx | 87 ++++++++++++++++++++++++++++++++++++ plans/composition-objects.md | 14 +++--- 5 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 lib/host/execute.sx create mode 100644 lib/host/tests/execute.sx diff --git a/lib/host/conformance.sh b/lib/host/conformance.sh index f7742cad..dde8daba 100755 --- a/lib/host/conformance.sh +++ b/lib/host/conformance.sh @@ -93,6 +93,7 @@ MODULES=( "lib/host/feed.sx" "lib/host/relations.sx" "lib/host/compose.sx" + "lib/host/execute.sx" "lib/host/blog.sx" "lib/host/page.sx" "lib/host/server.sx" @@ -108,6 +109,7 @@ SUITES=( "feed host-fd-tests-run! lib/host/tests/feed.sx" "relations host-rl-tests-run! lib/host/tests/relations.sx" "blog host-bl-tests-run! lib/host/tests/blog.sx" + "execute host-ex-tests-run! lib/host/tests/execute.sx" "session host-se-tests-run! lib/host/tests/session.sx" "page host-pg-tests-run! lib/host/tests/page.sx" "server host-sv-tests-run! lib/host/tests/server.sx" diff --git a/lib/host/execute.sx b/lib/host/execute.sx new file mode 100644 index 00000000..bd208bc2 --- /dev/null +++ b/lib/host/execute.sx @@ -0,0 +1,76 @@ +;; lib/host/execute.sx — the EXECUTE-fold: a SECOND interpreter over the SAME composition +;; algebra (seq/alt/each) as the render-fold (lib/host/compose.sx), proving the algebra is +;; domain-agnostic (plans/composition-objects.md step 7 — "prove universality with a second +;; fold"). What changes between folds is only what the combinators + leaves MEAN: +;; +;; domain fold seq alt+when each leaf +;; content render -> block order choose map items markup -> HTML string +;; behaviour execute -> steps in order branch for-each effect -> effect log +;; +;; Crucially this REUSES compose.sx's shared machinery — the `when` predicate set +;; (host/comp--pred?), the field/value resolver (host/comp--field), and the `each` source +;; (host/comp--source). So the predicate set, the context-environment, and the iteration +;; source are domain-agnostic; ONLY the leaf semantics (effect vs markup) and the fold's +;; accumulator (a list of effects vs a string) are new. The behaviour model (Slice 9) is +;; therefore "an execute-fold over a composition object", not a separate system. + +;; resolve an effect argument against the context: (field K) reads the :item/ctx value via +;; the SAME resolver the render-fold uses; anything else is a literal. +(define host/exec--arg + (fn (a ctx) + (if (and (= (type-of a) "list") (= (str (first a)) "field")) + (host/comp--field (first (rest a)) ctx) + a))) + +;; a leaf effect: (effect VERB ARG…) -> one effect record {:verb :args}. The execute-fold's +;; analogue of a render leaf — it performs (records) an effect rather than emitting markup. +(define host/exec--effect + (fn (verb args ctx) + (list {:verb (str verb) :args (map (fn (a) (host/exec--arg a ctx)) args)}))) + +;; seq: run every step IN ORDER, concatenating their effects (the sequential strategy). +(define host/exec--run-all + (fn (nodes ctx) (reduce (fn (acc n) (concat acc (host/exec--run n ctx))) (list) nodes))) + +;; alt: BRANCH — run the FIRST branch whose `when` holds (reusing the render-fold's +;; predicate host/comp--pred?), else `else`. This is if/cond for the behaviour domain. +(define host/exec--alt + (fn (branches ctx) + (if (empty? branches) + (list) + (let ((br (first branches)) (bh (str (first (first branches))))) + (cond + ((= bh "else") (host/exec--run (first (rest br)) ctx)) + ((= bh "when") (if (host/comp--pred? (first (rest br)) ctx) + (host/exec--run (first (rest (rest br))) ctx) + (host/exec--alt (rest branches) ctx))) + (else (host/exec--alt (rest branches) ctx))))))) + +;; each: FOR-EACH — run the body per item from the (reused) source, :item bound, in order; +;; depth guard backstops runaway recursion, same as the render-fold. +(define host/exec--each + (fn (src body ctx) + (let ((depth (or (get ctx "depth") 0))) + (if (> depth 40) + (list {:verb "max-depth" :args (list)}) + (reduce + (fn (acc item) + (concat acc (host/exec--run body (merge ctx {"item" item "depth" (+ depth 1)})))) + (list) (host/comp--source src ctx)))))) + +;; the execute-fold (the interpreter): same combinator dispatch shape as host/comp--render, +;; but leaves are effects and the accumulator is an effect log. +(define host/exec--run + (fn (node ctx) + (if (not (= (type-of node) "list")) + (list) + (let ((h (str (first node))) (args (rest node))) + (cond + ((= h "seq") (host/exec--run-all args ctx)) + ((= h "alt") (host/exec--alt args ctx)) + ((= h "each") (host/exec--each (first args) (first (rest args)) ctx)) + ((= h "effect") (host/exec--effect (first args) (rest args) ctx)) + (else (list))))))) + +;; public entry: execute a composition node against a context -> the effect log (the run). +(define host/exec-run (fn (node ctx) (host/exec--run node ctx))) diff --git a/lib/host/serve.sh b/lib/host/serve.sh index c271bc58..b676b5cb 100755 --- a/lib/host/serve.sh +++ b/lib/host/serve.sh @@ -87,6 +87,7 @@ MODULES=( "lib/host/feed.sx" "lib/host/relations.sx" "lib/host/compose.sx" + "lib/host/execute.sx" "lib/host/blog.sx" "lib/host/server.sx" ) diff --git a/lib/host/tests/execute.sx b/lib/host/tests/execute.sx new file mode 100644 index 00000000..84c0c181 --- /dev/null +++ b/lib/host/tests/execute.sx @@ -0,0 +1,87 @@ +;; lib/host/tests/execute.sx — the EXECUTE-fold (lib/host/execute.sx): a second interpreter +;; over the SAME seq/alt/each composition algebra as the render-fold, proving the algebra is +;; domain-agnostic (plans/composition-objects.md step 7). Leaves are effects; the fold +;; returns an effect log. Reuses compose.sx's when-predicates / field resolver / each source. + +(define host-ex-pass 0) +(define host-ex-fail 0) +(define host-ex-fails (list)) +(define host-ex-test + (fn (name actual expected) + (if (= actual expected) + (set! host-ex-pass (+ host-ex-pass 1)) + (begin + (set! host-ex-fail (+ host-ex-fail 1)) + (append! host-ex-fails {:name name :actual actual :expected expected}))))) + +;; the verbs of an effect log, in order (effect records are {:verb :args}). +(define ex-verbs (fn (effects) (map (fn (e) (get e :verb)) effects))) +(define ex-args (fn (effects) (map (fn (e) (get e :args)) effects))) + +;; -- seq: steps in order -- +(host-ex-test "seq runs effects in order" + (ex-verbs (host/exec-run (quote (seq (effect a) (effect b) (effect c))) {})) + (list "a" "b" "c")) +(host-ex-test "nested seq flattens in order" + (ex-verbs (host/exec-run (quote (seq (effect a) (seq (effect b) (effect c)) (effect d))) {})) + (list "a" "b" "c" "d")) + +;; -- alt + when: branch (reusing the render-fold's predicate set) -- +(host-ex-test "alt runs the first branch whose when holds" + (ex-verbs (host/exec-run (quote (alt (when (has "auth") (effect publish)) (else (effect hold)))) {"auth" "y"})) + (list "publish")) +(host-ex-test "alt falls through to else when no when holds" + (ex-verbs (host/exec-run (quote (alt (when (has "auth") (effect publish)) (else (effect hold)))) {})) + (list "hold")) +(host-ex-test "alt eq predicate branches on a context value" + (ex-verbs (host/exec-run (quote (alt (when (eq "role" "admin") (effect grant)) (else (effect deny)))) {"role" "admin"})) + (list "grant")) + +;; -- each: for-each over the (reused) source, with field resolution from the item -- +(host-ex-test "each runs the body per item (for-each)" + (ex-verbs (host/exec-run (quote (each (items {:email "a"} {:email "b"}) (effect notify))) {})) + (list "notify" "notify")) +(host-ex-test "effect args resolve (field K) from the current item" + (ex-args (host/exec-run (quote (each (items {:email "a@x"} {:email "b@x"}) (effect notify (field :email)))) {})) + (list (list "a@x") (list "b@x"))) +(host-ex-test "effect args resolve (field K) from the context, and literals pass through" + (ex-args (host/exec-run (quote (seq (effect log (field :who) "done"))) {"who" "alice"})) + (list (list "alice" "done"))) + +;; -- robustness: non-effect leaves / unknown heads produce no effects -- +(host-ex-test "a non-list node yields no effects" + (host/exec-run "bare" {}) (list)) +(host-ex-test "an unknown combinator head yields no effects" + (host/exec-run (quote (frobnicate 1 2)) {}) (list)) + +;; -- the KEYSTONE: ONE control skeleton, folded TWO ways. Same alt+when, same context, the +;; SAME branch is chosen (both use host/comp--pred?); render emits HTML, execute emits an +;; effect. The composition algebra is domain-agnostic — render and behaviour are two folds. -- +(host-ex-test "same skeleton folds two ways — render picks the branch, execute picks the SAME branch (authed)" + (let ((ctx {"auth" "y"})) + (list (host/comp-render (quote (alt (when (has "auth") (text "in")) (else (text "out")))) ctx) + (ex-verbs (host/exec-run (quote (alt (when (has "auth") (effect enter)) (else (effect leave)))) ctx)))) + (list "in" (list "enter"))) +(host-ex-test "same skeleton folds two ways — the else branch agrees across folds (anon)" + (let ((ctx {})) + (list (host/comp-render (quote (alt (when (has "auth") (text "in")) (else (text "out")))) ctx) + (ex-verbs (host/exec-run (quote (alt (when (has "auth") (effect enter)) (else (effect leave)))) ctx)))) + (list "out" (list "leave"))) + +;; -- a small workflow: validate -> (branch on status) -> notify each recipient. Proves the +;; behaviour model is just an execute-fold over a composition object. -- +(host-ex-test "a publish workflow runs as one execute-fold over the composition" + (ex-verbs + (host/exec-run + (quote (seq + (effect validate (field :slug)) + (alt (when (eq "status" "ready") (effect publish (field :slug))) + (else (effect hold (field :slug)))) + (each (items {:to "a"} {:to "b"}) (effect notify (field :to))))) + {"slug" "post-1" "status" "ready"})) + (list "validate" "publish" "notify" "notify")) + +(define host-ex-tests-run! + (fn () + {:total (+ host-ex-pass host-ex-fail) + :passed host-ex-pass :failed host-ex-fail :fails host-ex-fails})) diff --git a/plans/composition-objects.md b/plans/composition-objects.md index 133ef846..a2a42f93 100644 --- a/plans/composition-objects.md +++ b/plans/composition-objects.md @@ -125,11 +125,15 @@ Transclusion = a `ref` leaf. Sort/filter/limit/group = the *source query* langua `POST /:slug/blocks/{add,:cslug/remove,:cslug/move}` (guarded, SX-htmx outerHTML swap). Per-block field editing is free — a card is an object, edited via its own `//edit`. (Live SX-htmx swap still wants a Playwright check; `alt`/`each` block insertion deferred.) -7. **Prove universality with a second fold.** Write a tiny `execute`-fold over the *same* - `seq/alt/each` structure that *runs* a workflow (leaves = effects; `seq` = steps in order, `alt` - = branch, `each` = for-each) — the way the recursive tree proved recursion, this proves the - composition algebra is domain-agnostic. Then the *behaviour* model (Slice 9) is "an `execute`-fold - over a composition object", not a separate system. +7. **(done)** Prove universality with a second fold. `lib/host/execute.sx` is an `execute`-fold + over the *same* `seq/alt/each` structure: leaves = effects, `seq` = steps in order, `alt`+`when` + = branch, `each` = for-each; the fold returns an effect log. It REUSES compose.sx's shared + machinery — `host/comp--pred?` (when), `host/comp--field` (field/value), `host/comp--source` + (each source) — so only the leaf semantics + accumulator differ. KEYSTONE proven (tests): ONE + `(alt (when …) …)` skeleton + ONE context folds two ways — render picks the branch → HTML, + execute picks the SAME branch → effect. A publish workflow (validate→branch→notify-each) runs as + one execute-fold. The behaviour model (Slice 9) is "an execute-fold over a composition object", + not a separate system. 13/13 (execute suite). Wired into conformance + serve. 8. **Factor out the shared machinery** once two folds exist: the fork model (ordered, labelled, `when`), the combinator dispatch, the context-environment, and recursion become a reusable `compose` core; each domain (`render`, `execute`, `eval`, …) supplies only its leaf + combinator