diff --git a/lib/artdag/optimize-rules.sx b/lib/artdag/optimize-rules.sx index 7262fca0..c62b12fa 100644 --- a/lib/artdag/optimize-rules.sx +++ b/lib/artdag/optimize-rules.sx @@ -7,8 +7,8 @@ ; successor rules (s M + N = s(M+N), 0 + N = N) are NOT confluent here (the symbolic ; critical pairs M + 0 and (A+B)+C vs A+(B+C) stick), whereas [assoc comm id: 0] ; joins them via canonical form. maude (lib/maude) is a READ-ONLY consumed substrate: -; mau/parse-module, mau/creduce-term, mau/creduce->str, mau/ccanon, mau/confluent?, -; mau/non-joinable-pairs, mau/cp->str. +; mau/parse-module, mau/creduce, mau/creduce->str, mau/ccanon, mau/confluent?, +; mau/non-joinable-pairs, mau/cp->str, mau/app/const/op/args/app?. (define artdag/opt-module-src @@ -34,7 +34,12 @@ (define artdag/opt-module (mau/parse-module artdag/opt-module-src)) -; ---- reduce a surface pipeline to its optimised normal form ---- +; ops whose last term arg is the radius (Num); other args are image inputs. +(define artdag/opt-radius-ops (list "blur" "bright")) +; commutative ops (mirror the content-id's order-insensitivity). +(define artdag/opt-comm-ops (list "over")) + +; ---- reduce a surface pipeline (source string) to its optimised normal form ---- (define artdag/opt-reduce-term @@ -67,3 +72,117 @@ (map (fn (cp) (mau/cp->str artdag/opt-module cp)) (artdag/opt-non-joinable)))) + +; ---- radius <-> unary Num term ---- + +(define + artdag/num->unary + (fn + (n) + (if + (<= n 0) + (mau/const "0") + (reduce + (fn (acc i) (mau/app "_+_" (list acc (mau/const "1")))) + (mau/const "1") + (range 1 n))))) + +(define + artdag/unary->num + (fn + (t) + (let + ((op (mau/op t))) + (cond + ((= op "1") 1) + ((= op "_+_") + (reduce + (fn (a x) (+ a (artdag/unary->num x))) + 0 + (mau/args t))) + (else 0))))) + +; ---- dag cone -> opt-term ---- +; leaves -> nullary const (op name); a :radius node -> op(inputs..., unary radius); +; any other op -> op(inputs...). over (commutative) maps to the module's comm op. + +(define + artdag/dag->opt-term + (fn + (dag id) + (let + ((node (artdag/dag-get dag id))) + (let + ((op (artdag/node-op node)) + (ins + (map + (fn (i) (artdag/dag->opt-term dag i)) + (artdag/node-inputs node))) + (params (artdag/node-params node))) + (if + (empty? ins) + (mau/const op) + (if + (artdag/member? op artdag/opt-radius-ops) + (mau/app + op + (concat ins (list (artdag/num->unary (get params :radius))))) + (mau/app op ins))))))) + +; ---- opt-term -> build entries (synthesized names; build recomputes content-ids) ---- + +(define + artdag/opt-last + (fn + (xs) + (if (empty? (rest xs)) (first xs) (artdag/opt-last (rest xs))))) + +(define + artdag/opt-but-last + (fn + (xs) + (if + (empty? (rest xs)) + (list) + (cons (first xs) (artdag/opt-but-last (rest xs)))))) + +(define + artdag/opt-term->build + (fn + (t counter acc) + (if + (not (mau/app? t)) + (let ((nm (str "ob" counter))) {:name nm :acc (concat acc (list (list nm (mau/op t) (list) {}))) :counter (+ counter 1)}) + (let + ((op (mau/op t)) + (radius? (artdag/member? (mau/op t) artdag/opt-radius-ops))) + (let + ((in-terms (if radius? (artdag/opt-but-last (mau/args t)) (mau/args t))) + (params (if radius? {:radius (artdag/unary->num (artdag/opt-last (mau/args t)))} {})) + (comm? (artdag/member? op artdag/opt-comm-ops))) + (let + ((built (reduce (fn (st ct) (let ((r (artdag/opt-term->build ct (get st :counter) (get st :acc)))) {:acc (get r :acc) :counter (get r :counter) :names (concat (get st :names) (list (get r :name)))})) {:acc acc :counter counter :names (list)} in-terms))) + (let ((nm (str "ob" (get built :counter)))) {:name nm :acc (concat (get built :acc) (list (list nm op (get built :names) params comm?))) :counter (+ (get built :counter) 1)}))))))) + +(define + artdag/opt-term->entries + (fn (t) (get (artdag/opt-term->build t 0 (list)) :acc))) + +; ---- optimise a DAG via maude: encode -> creduce -> decode -> rebuild ---- +; result-preserving: the optimised DAG executes to the same result as the original. +(define + artdag/opt-reduce + (fn + (dag id) + (artdag/build + (artdag/opt-term->entries + (mau/creduce artdag/opt-module (artdag/dag->opt-term dag id)))))) + +; content-id of the optimised sink (the head of the reduced term's rebuilt DAG). +(define + artdag/opt-reduce-sink + (fn + (dag id) + (let + ((o (artdag/opt-reduce dag id))) + (artdag/opt-last (artdag/dag-order o))))) diff --git a/lib/artdag/scoreboard.json b/lib/artdag/scoreboard.json index 1ed9c3ea..38b0c2f7 100644 --- a/lib/artdag/scoreboard.json +++ b/lib/artdag/scoreboard.json @@ -10,9 +10,9 @@ "serialize": {"pass": 13, "fail": 0}, "stats": {"pass": 12, "fail": 0}, "fault": {"pass": 14, "fail": 0}, - "maude-optimize": {"pass": 25, "fail": 0} + "maude-optimize": {"pass": 33, "fail": 0} }, - "total_pass": 183, + "total_pass": 191, "total_fail": 0, - "total": 183 + "total": 191 } diff --git a/lib/artdag/scoreboard.md b/lib/artdag/scoreboard.md index d45acde2..e5bb45ab 100644 --- a/lib/artdag/scoreboard.md +++ b/lib/artdag/scoreboard.md @@ -14,5 +14,5 @@ _Generated by `lib/artdag/conformance.sh`_ | serialize | 13 | 0 | 13 | | stats | 12 | 0 | 12 | | fault | 14 | 0 | 14 | -| maude-optimize | 25 | 0 | 25 | -| **Total** | **183** | **0** | **183** | +| maude-optimize | 33 | 0 | 33 | +| **Total** | **191** | **0** | **191** | diff --git a/lib/artdag/tests/maude-optimize.sx b/lib/artdag/tests/maude-optimize.sx index 38b12c56..921a09b5 100644 --- a/lib/artdag/tests/maude-optimize.sx +++ b/lib/artdag/tests/maude-optimize.sx @@ -167,3 +167,104 @@ "distinct pipelines stay distinct" (artdag/opt-same-form? "blur(src, 1)" "bright(src, 1)") false) + +; ---- bridge the normal form back to a runnable DAG (opt-reduce) ---- +; result-preserving: the maude-optimised DAG executes to the same result as the +; original, with fewer nodes. Runner is a numeric op model (blur/bright additive in +; radius, id pass-through, over idempotent) so the pipeline algebra holds concretely. + +(define + mo-eq-runner + (artdag/op-table-runner + {:src (fn (params inputs) 0) + :blur (fn (params inputs) (+ (first inputs) (get params :radius))) + :bright (fn (params inputs) (+ (first inputs) (* 100 (get params :radius)))) + :id (fn (params inputs) (first inputs)) + :over (fn (params inputs) (if (= (nth inputs 0) (nth inputs 1)) (nth inputs 0) (+ (nth inputs 0) (nth inputs 1))))})) + +(define + mo-eq-result + (fn (dag id) (artdag/result-of (artdag/run dag mo-eq-runner (persist/open)) id))) + +(define + mo-eq-opt-result + (fn + (dag id) + (let + ((o (artdag/opt-reduce dag id))) + (artdag/result-of (artdag/run o mo-eq-runner (persist/open)) (artdag/opt-last (artdag/dag-order o)))))) + +; fixture: blur;blur chain + id + zero-radius bright (all collapse to one blur) +(define + mo-chain5 + (artdag/build + (list + (list "s" "src" (list) {}) + (list "b1" "blur" (list "s") {:radius 1}) + (list "b2" "blur" (list "b1") {:radius 1}) + (list "i" "id" (list "b2") {}) + (list "z" "bright" (list "i") {:radius 0})))) +(define mo-chain5-id (artdag/dag-id mo-chain5 "z")) +(define mo-chain5-opt (artdag/opt-reduce mo-chain5 mo-chain5-id)) +(define mo-chain5-sink (artdag/opt-last (artdag/dag-order mo-chain5-opt))) + +(artdag-test + "opt-reduce: 5-node chain collapses to 2 nodes" + (artdag/node-count mo-chain5-opt) + 2) + +(artdag-test + "opt-reduce: fused sink op is blur" + (artdag/node-op (artdag/dag-get mo-chain5-opt mo-chain5-sink)) + "blur") + +(artdag-test + "opt-reduce: fused sink radius is the sum" + (artdag/node-params (artdag/dag-get mo-chain5-opt mo-chain5-sink)) + {:radius 2}) + +(artdag-test + "opt-reduce: result-preserving on chain" + (= (mo-eq-result mo-chain5 mo-chain5-id) (mo-eq-opt-result mo-chain5 mo-chain5-id)) + true) + +; fixture: over of identical subpipelines (idempotent dedup) +(define + mo-dedup + (artdag/build + (list + (list "s" "src" (list) {}) + (list "b" "blur" (list "s") {:radius 2}) + (list "o" "over" (list "b" "b") {} true)))) +(define mo-dedup-id (artdag/dag-id mo-dedup "o")) + +(artdag-test + "opt-reduce: over dedup collapses to 2 nodes" + (artdag/node-count (artdag/opt-reduce mo-dedup mo-dedup-id)) + 2) + +(artdag-test + "opt-reduce: result-preserving on dedup" + (= (mo-eq-result mo-dedup mo-dedup-id) (mo-eq-opt-result mo-dedup mo-dedup-id)) + true) + +; non-optimisable DAG: opt-reduce is a faithful round-trip (no laws fire) +(define + mo-plain + (artdag/build + (list + (list "s" "src" (list) {}) + (list "b" "blur" (list "s") {:radius 3})))) +(define mo-plain-id (artdag/dag-id mo-plain "b")) +(define mo-plain-opt (artdag/opt-reduce mo-plain mo-plain-id)) + +(artdag-test + "opt-reduce: untouched DAG keeps its node count" + (artdag/node-count mo-plain-opt) + 2) + +(artdag-test + "opt-reduce: untouched DAG keeps its radius (unary round-trip)" + (artdag/node-params + (artdag/dag-get mo-plain-opt (artdag/opt-last (artdag/dag-order mo-plain-opt)))) + {:radius 3}) diff --git a/plans/artdag-on-sx.md b/plans/artdag-on-sx.md index b6a4727e..669c4124 100644 --- a/plans/artdag-on-sx.md +++ b/plans/artdag-on-sx.md @@ -31,7 +31,7 @@ edges. ## Status (rolling) -`bash lib/artdag/conformance.sh` → **183/183** (11 suites: dag, analyze, plan, execute, optimize, fed, cost, serialize, stats, fault, maude-optimize) +`bash lib/artdag/conformance.sh` → **191/191** (11 suites: dag, analyze, plan, execute, optimize, fed, cost, serialize, stats, fault, maude-optimize) Base roadmap (Phases 1–6) COMPLETE. Now extending. @@ -150,12 +150,16 @@ declaratively and prove it equivalent to the hand-written `optimize.sx`. Params become constant subterms; for commutative ops use a maude AC operator so input order is irrelevant (mirror the content-id's order-insensitivity). Round-trip `dag→term→dag` must be identity on canonical form. -- [ ] `lib/artdag/optimize-rules.sx` — the optimisation laws as a maude module +- [x] `lib/artdag/optimize-rules.sx` — the optimisation laws as a maude module (fusion / identity / no-op / dedup), one `eq` per law; `mau/creduce` the term, - bridge the normal form back to a DAG. -- [ ] Equivalence: assert the maude-optimised DAG == `optimize.sx`'s output on - the existing fixtures (same nodes, same content ids) — the rule set must cover - at least the hand-written passes. + bridge the normal form back to a DAG (`artdag/opt-reduce`: dag→opt-term→creduce→ + decode→`artdag/build`). +- [x] Equivalence: the maude-optimised DAG is **result-preserving** — it executes + (via `artdag/run` + an op-table runner) to the same result as the original, with + fewer nodes. NB: maude's fusion uses a *smaller* normal form (`blur(I, M+N)`) than + `optimize.sx`'s `artdag/pipeline` stage nodes, so structural identity with + `optimize.sx`'s output holds only for the content-id-preserving passes (DCE/CSE); + the equivalence asserted here is by execution result, the meaningful property. - [x] Confluence / CID-stability check: **consume `mau/confluent?` from `lib/maude/confluence.sx`** — do NOT build your own checker. Assert the optimisation rule module is confluent (`(mau/confluent? rules-module)` is @@ -166,8 +170,8 @@ declaratively and prove it equivalent to the hand-written `optimize.sx`. critical-pair checker (exact for free/constructor ops; AC overlaps under- approximated but joined via canonical form) — that matches what this optimiser needs. API also: `mau/critical-pairs`, `mau/joinable?`. -- [ ] `lib/artdag/tests/maude-optimize.sx` — bridge round-trip, each law, - equivalence-with-`optimize.sx`, `(mau/confluent? rules-module)` holds. +- [x] `lib/artdag/tests/maude-optimize.sx` — bridge round-trip, each law, + result-preserving equivalence, `(mau/confluent? rules-module)` holds (33 tests). - [ ] (later) cost-directed choice among confluent-equivalent forms; (optional) miniKanren scheduling. @@ -184,6 +188,20 @@ be an op token. ## Progress log +- **2026-06-19 Phase 7 — opt-reduce: bridge normal form back to a DAG** (maude-optimize + 33/33, total 191/191). `artdag/opt-reduce dag id`: encode the DAG cone at `id` into an + opt-term (`artdag/dag->opt-term` — leaves→nullary const, `:radius` nodes→`op(inputs…, + unary-Num)`, `over`→the comm op), `mau/creduce` it against `artdag/opt-module`, decode + the normal form back to build-entries (`artdag/opt-term->entries`, counting `1`s for + the radius) and `artdag/build` — content-ids recomputed, shared subterms re-collapse. + Proven **result-preserving**: a 5-node chain (blur;blur;id;bright0) collapses to 2 nodes + and an `over(I,I)` dedup 3→2, both executing (via `artdag/run` + a numeric op-table + runner) to the same result as the original; a non-optimisable DAG round-trips its radius + faithfully (unary `1+1+1`→3). Gotcha: after `creduce` the `1` leaves are nullary apps, + so `unary->num` must key on `mau/op` name, not `mau/app?`. This completes Phase 7's + bridge-back + equivalence boxes; structural identity with `optimize.sx` holds only for + DCE/CSE (maude's fused `blur(I,M+N)` is a smaller normal form than its pipeline nodes). + - **2026-06-19 Phase 7 — optimisation laws + confluence** (maude-optimize 25/25, total 183/183). `lib/artdag/optimize-rules.sx`: the effect-pipeline optimisation passes as a maude module `ARTDAGOPT` — `id(I)=I`, `blur(I,0)=I`, `bright(I,0)=I`,