host: factor the shared composition CORE — one fold, N domains (composition step 8)
The roadmap's capstone: now that two folds exist (render, execute), extract the machinery
they share. host/comp-fold (compose.sx) is the reusable core — the seq/alt/each combinator
dispatch + the `when` predicate set (host/comp--pred?) + the context-environment + the `each`
source (host/comp--source) + recursion + the depth guard, ALL in one place. A domain plugs in
via a small dict {:empty :combine :leaf :overflow}; only its leaves and how results combine
differ:
render = {:empty "" :combine str …} leaf -> markup (+ row/grid layout combinators)
execute = {:empty (list) :combine concat …} leaf -> effect
host/comp-render and host/exec-run are now one-liners over host/comp-fold with their domain.
execute.sx shed its own seq/alt/each dispatch — it's just a dict + a leaf. A THIRD domain
(eval/reduce/extent over the same algebra) is now only a new dict + leaf, no new control flow.
Both folds went through the core with ZERO behaviour change: new tests/compose.sx exercises
the core + render domain directly (17/17 — leaves, seq, row, alt+when (has/eq/not), each
(items/query/empty), tmpl recursion over a (children) tree + depth guard, ref transclude, one
object two contexts); execute 13/13; blog 162/164 (2 pre-existing relate-picker fails). Full
host conformance 388/390. Wired tests/compose.sx into conformance.
plans/composition-objects.md roadmap steps 1-8 COMPLETE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,16 @@
|
||||
;; 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:
|
||||
;; lib/host/execute.sx — the EXECUTE-fold: a SECOND domain over the SAME composition core
|
||||
;; as the render-fold (lib/host/compose.sx), proving the algebra is domain-agnostic
|
||||
;; (plans/composition-objects.md steps 7-8). Now that the core (host/comp-fold: the seq/alt/
|
||||
;; each dispatch + when-predicates + each-source + context-environment + recursion) is shared,
|
||||
;; a whole new domain is just a DOMAIN DICT + a leaf function:
|
||||
;;
|
||||
;; 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
|
||||
;; render {:empty "" :combine str …} leaf -> markup; fold -> HTML string
|
||||
;; execute {:empty (list) :combine concat …} leaf -> effect; fold -> 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.
|
||||
;; seq = steps in order, alt+when = branch, each = for-each — all from the core, unchanged.
|
||||
;; Only the leaf semantics (effect vs markup) and the accumulator (list vs string) are new.
|
||||
;; So the behaviour model (Slice 9) is "an execute-fold over a composition object", not a
|
||||
;; separate system — the same structure an author edits as a document.
|
||||
|
||||
;; 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.
|
||||
@@ -22,55 +20,21 @@
|
||||
(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)
|
||||
;; the execute-fold's LEAF: an (effect VERB ARG…) node records one effect {:verb :args};
|
||||
;; anything else contributes no effects. (The core handles seq/alt/each.)
|
||||
(define host/exec--leaf
|
||||
(fn (node ctx dom)
|
||||
(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)))))))
|
||||
(if (= h "effect")
|
||||
(list {:verb (str (first args)) :args (map (fn (a) (host/exec--arg a ctx)) (rest args))})
|
||||
(list))))))
|
||||
|
||||
;; the execute DOMAIN: effects concatenate into a log; the depth guard yields a max-depth
|
||||
;; effect. host/comp-fold (compose.sx) supplies the seq/alt/each walk + when + each source.
|
||||
(define host/exec--dom
|
||||
{:empty (list) :combine concat :overflow (list {:verb "max-depth" :args (list)}) :leaf host/exec--leaf})
|
||||
|
||||
;; 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)))
|
||||
(define host/exec-run (fn (node ctx) (host/comp-fold node ctx host/exec--dom)))
|
||||
|
||||
Reference in New Issue
Block a user