host: execute-fold — universality proven with a second fold (composition step 7)
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 → "<b>in</b>", 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
76
lib/host/execute.sx
Normal file
76
lib/host/execute.sx
Normal file
@@ -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)))
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
87
lib/host/tests/execute.sx
Normal file
87
lib/host/tests/execute.sx
Normal file
@@ -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 "<b>in</b>")) (else (text "out")))) ctx)
|
||||
(ex-verbs (host/exec-run (quote (alt (when (has "auth") (effect enter)) (else (effect leave)))) ctx))))
|
||||
(list "<b>in</b>" (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 "<b>in</b>")) (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}))
|
||||
@@ -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 `/<cslug>/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
|
||||
|
||||
Reference in New Issue
Block a user