; lib/artdag/optimize.sx — Phase 5: result-preserving DAG rewrites.
;   DCE    — drop nodes not reachable upstream from the requested outputs.
;   CSE    — free from content addressing: structurally identical subexpressions
;            already collapse to one node at build time (artdag/cse == build).
;   Fusion — collapse a maximal 1-to-1 chain of fusible unary ops into a single
;            "artdag/pipeline" node that replays the stages; output-equivalent.
;   optimize — fuse then DCE in one pass.
; Depends on dag.sx and analyze.sx.

; ---- dict helper ----

(define
  artdag/-dict-filter
  (fn
    (d keep?)
    (reduce
      (fn (acc k) (if (keep? k (get d k)) (assoc acc k (get d k)) acc))
      {}
      (keys d))))

(define
  artdag/-union
  (fn
    (a b)
    (reduce (fn (acc x) (if (artdag/member? x acc) acc (cons x acc))) a b)))

; ---- dead-node elimination ----
; keep only the outputs and their transitive dependencies; ids are preserved.
(define
  artdag/dce
  (fn
    (dag outputs)
    (let
      ((db (artdag/analyze dag)))
      (let
        ((live (reduce (fn (acc out) (artdag/-union (artdag/-union acc (list out)) (artdag/ancestors-of db out))) (list) outputs)))
        {:names (artdag/-dict-filter (artdag/dag-names dag) (fn (k v) (artdag/member? v live))) :order (filter (fn (id) (artdag/member? id live)) (artdag/dag-order dag)) :ok true :nodes (artdag/-dict-filter (artdag/dag-nodes dag) (fn (k v) (artdag/member? k live)))}))))

; ---- common-subexpression elimination ----
; structural sharing is inherent to content addressing: build already maps
; structurally identical specs to a single node/id.
(define artdag/cse artdag/build)

; ---- adjacent-op fusion (entry-level rewrite) ----

(define artdag/pipeline-op "artdag/pipeline")

(define
  artdag/-name->entry
  (fn
    (entries)
    (reduce
      (fn (m e) (assoc m (artdag/entry-name e) e))
      {}
      entries)))

; name -> list of dependent names
(define
  artdag/-deps-map
  (fn
    (entries)
    (reduce
      (fn
        (m e)
        (reduce
          (fn
            (mm i)
            (assoc
              mm
              i
              (cons
                (artdag/entry-name e)
                (if (has-key? mm i) (get mm i) (list)))))
          m
          (artdag/entry-inputs e)))
      {}
      entries)))

(define artdag/-stage (fn (e) {:op (artdag/entry-op e) :params (artdag/entry-params e)}))

; the single predecessor that `name` may absorb, or nil. Requires: name is a
; fusible unary op; its one input is a locally-defined fusible node whose ONLY
; dependent is name (so fusing cannot break sharing).
(define
  artdag/-absorbs
  (fn
    (n->e deps fusible? name)
    (let
      ((e (get n->e name)))
      (let
        ((ins (artdag/entry-inputs e)))
        (if
          (= (len ins) 1)
          (let
            ((x (first ins)))
            (if
              (and
                (has-key? n->e x)
                (fusible? (artdag/entry-op e))
                (fusible? (artdag/entry-op (get n->e x)))
                (= (get deps x) (list name)))
              x
              nil))
          nil)))))

(define
  artdag/-absorbed-set
  (fn
    (n->e deps fusible? names)
    (reduce
      (fn
        (acc y)
        (let
          ((p (artdag/-absorbs n->e deps fusible? y)))
          (if (nil? p) acc (cons p acc))))
      (list)
      names)))

; walk predecessors from a tail, building stages head->tail.
(define
  artdag/-fuse-chain
  (fn
    (n->e deps fusible? cur stages)
    (let
      ((p (artdag/-absorbs n->e deps fusible? cur)))
      (if
        (nil? p)
        {:stages (cons (artdag/-stage (get n->e cur)) stages) :head cur}
        (artdag/-fuse-chain
          n->e
          deps
          fusible?
          p
          (cons (artdag/-stage (get n->e cur)) stages))))))

(define
  artdag/fuse-entries
  (fn
    (entries fusible?)
    (let
      ((n->e (artdag/-name->entry entries))
        (deps (artdag/-deps-map entries))
        (names (map artdag/entry-name entries)))
      (let
        ((absorbed (artdag/-absorbed-set n->e deps fusible? names)))
        (map
          (fn
            (name)
            (let
              ((c (artdag/-fuse-chain n->e deps fusible? name (list))))
              (if
                (> (len (get c :stages)) 1)
                (list
                  name
                  artdag/pipeline-op
                  (artdag/entry-inputs (get n->e (get c :head)))
                  {:stages (get c :stages)})
                (get n->e name))))
          (filter (fn (name) (not (artdag/member? name absorbed))) names))))))

(define
  artdag/fuse
  (fn
    (entries fusible?)
    (artdag/build (artdag/fuse-entries entries fusible?))))

; runner that replays a fused pipeline over its single input, delegating each
; stage to a base runner; non-pipeline ops fall through unchanged.
(define
  artdag/pipeline-run
  (fn
    (base-runner)
    (fn
      (params inputs)
      (reduce
        (fn
          (val stage)
          (base-runner (get stage :op) (get stage :params) (list val)))
        (first inputs)
        (get params :stages)))))

(define
  artdag/fusing-runner
  (fn
    (base-runner)
    (fn
      (op params inputs)
      (if
        (= op artdag/pipeline-op)
        ((artdag/pipeline-run base-runner) params inputs)
        (base-runner op params inputs)))))

; ---- full optimization pass ----
; fuse the entry list, then drop everything not feeding the requested output
; names. Output names survive fusion (sinks are never absorbed).
(define
  artdag/optimize
  (fn
    (entries outputs fusible?)
    (let
      ((fused (artdag/fuse entries fusible?)))
      (artdag/dce fused (map (fn (nm) (artdag/dag-id fused nm)) outputs)))))
