; Phase 5 — optimization: DCE, CSE (content-id sharing), adjacent-op fusion.

(define opt-BASE (artdag/op-table-runner {:in (fn (params inputs) (get params :v)) :sq (fn (params inputs) (* (first inputs) (first inputs))) :add (fn (params inputs) (+ (nth inputs 0) (nth inputs 1))) :inc (fn (params inputs) (+ 1 (first inputs)))}))
(define opt-RUN (artdag/fusing-runner opt-BASE))
(define opt-inc? (fn (op) (= op "inc")))
(define opt-incsq? (fn (op) (or (= op "inc") (= op "sq"))))

; linear chain a(in) -> b -> c -> d, all inc
(define
  opt-chain
  (list
    (list "a" "in" (list) {:v 5})
    (list "b" "inc" (list "a") {})
    (list "c" "inc" (list "b") {})
    (list "d" "inc" (list "c") {})))

; ---- DCE ----

(define
  dce-entries
  (list
    (list "a" "in" (list) {:v 5})
    (list "b" "inc" (list "a") {})
    (list "c" "inc" (list "b") {})
    (list "x" "sq" (list "a") {})))
(define dce-G (artdag/build dce-entries))

(artdag-test
  "dce: removes dead node"
  (artdag/node-count (artdag/dce dce-G (list (artdag/dag-id dce-G "c"))))
  3)

(artdag-test
  "dce: keeps live closure intact"
  (artdag/node-count (artdag/dce dce-G (list (artdag/dag-id dce-G "x"))))
  2)

(artdag-test
  "dce: preserves surviving node ids"
  (artdag/member?
    (artdag/dag-id dce-G "c")
    (keys
      (artdag/dag-nodes (artdag/dce dce-G (list (artdag/dag-id dce-G "c"))))))
  true)

(artdag-test
  "dce: output result unchanged after elimination"
  (let
    ((cache (persist/open)))
    (artdag/result-of
      (artdag/run
        (artdag/dce dce-G (list (artdag/dag-id dce-G "c")))
        opt-RUN
        cache)
      (artdag/dag-id dce-G "c")))
  7)

(artdag-test
  "dce: nothing dead is a no-op on count"
  (artdag/node-count
    (artdag/dce
      dce-G
      (list (artdag/dag-id dce-G "c") (artdag/dag-id dce-G "x"))))
  4)

; ---- CSE (free from content addressing) ----

(define
  cse-entries
  (list
    (list "a" "in" (list) {:v 3})
    (list "s1" "sq" (list "a") {})
    (list "s2" "sq" (list "a") {})
    (list "d" "add" (list "s1" "s2") {} true)))
(define cse-C (artdag/cse cse-entries))

(artdag-test
  "cse: identical subexpressions collapse to one node"
  (artdag/node-count cse-C)
  3)

(artdag-test
  "cse: shared node computes once"
  (let
    ((cache (persist/open)))
    (artdag/recompute-count (artdag/run cse-C opt-RUN cache)))
  3)

(artdag-test
  "cse: s1 and s2 are the same id"
  (equal? (artdag/dag-id cse-C "s1") (artdag/dag-id cse-C "s2"))
  true)

(artdag-test
  "cse: result is correct"
  (let
    ((cache (persist/open)))
    (artdag/result-of
      (artdag/run cse-C opt-RUN cache)
      (artdag/dag-id cse-C "d")))
  18)

; ---- fusion ----

(artdag-test
  "fusion: collapses a unary chain"
  (artdag/node-count (artdag/fuse opt-chain opt-inc?))
  2)

(artdag-test
  "fusion: unfused has all nodes"
  (artdag/node-count (artdag/build opt-chain))
  4)

(artdag-test
  "fusion: output-equivalent to unfused"
  (let
    ((c1 (persist/open)) (c2 (persist/open)))
    (=
      (artdag/result-of
        (artdag/run (artdag/build opt-chain) opt-RUN c1)
        (artdag/dag-id (artdag/build opt-chain) "d"))
      (artdag/result-of
        (artdag/run (artdag/fuse opt-chain opt-inc?) opt-RUN c2)
        (artdag/dag-id (artdag/fuse opt-chain opt-inc?) "d"))))
  true)

(artdag-test
  "fusion: leaf is never fused"
  (artdag/node-op
    (artdag/dag-node-by-name (artdag/fuse opt-chain opt-inc?) "a"))
  "in")

(artdag-test
  "fusion: tail becomes a pipeline node"
  (artdag/node-op
    (artdag/dag-node-by-name (artdag/fuse opt-chain opt-inc?) "d"))
  "artdag/pipeline")

(artdag-test
  "fusion: mixed fusible set fuses across op kinds"
  (artdag/node-count
    (artdag/fuse
      (list
        (list "a" "in" (list) {:v 2})
        (list "b" "inc" (list "a") {})
        (list "c" "sq" (list "b") {})
        (list "d" "inc" (list "c") {}))
      opt-incsq?))
  2)

(artdag-test
  "fusion: mixed chain replays correctly"
  (let
    ((cache (persist/open)))
    (let
      ((f (artdag/fuse (list (list "a" "in" (list) {:v 2}) (list "b" "inc" (list "a") {}) (list "c" "sq" (list "b") {}) (list "d" "inc" (list "c") {})) opt-incsq?)))
      (artdag/result-of (artdag/run f opt-RUN cache) (artdag/dag-id f "d"))))
  10)

(artdag-test
  "fusion: fanout node is not fused"
  (artdag/node-count
    (artdag/fuse
      (list
        (list "a" "in" (list) {:v 1})
        (list "b" "inc" (list "a") {})
        (list "c" "inc" (list "b") {})
        (list "e" "sq" (list "b") {}))
      opt-inc?))
  4)

(artdag-test
  "fusion: empty fusible set leaves dag unchanged"
  (artdag/node-count (artdag/fuse opt-chain (fn (op) false)))
  4)

; ---- full optimization pass (fuse + dce) ----

(define
  optp-entries
  (list
    (list "a" "in" (list) {:v 5})
    (list "b" "inc" (list "a") {})
    (list "c" "inc" (list "b") {})
    (list "x" "sq" (list "a") {})))

(artdag-test
  "optimize: fuses chain and drops dead node"
  (artdag/node-count (artdag/optimize optp-entries (list "c") opt-inc?))
  2)

(artdag-test
  "optimize: leaves dead node when it is an output"
  (artdag/node-count (artdag/optimize optp-entries (list "c" "x") opt-inc?))
  3)

(artdag-test
  "optimize: result equals the unoptimized dag"
  (let
    ((c1 (persist/open)) (c2 (persist/open)))
    (let
      ((o (artdag/optimize optp-entries (list "c") opt-inc?)))
      (=
        (artdag/result-of (artdag/run o opt-RUN c1) (artdag/dag-id o "c"))
        (artdag/result-of
          (artdag/run (artdag/build optp-entries) opt-RUN c2)
          (artdag/dag-id (artdag/build optp-entries) "c")))))
  true)

(artdag-test
  "optimize: no fusible ops still drops dead nodes"
  (artdag/node-count
    (artdag/optimize optp-entries (list "c") (fn (op) false)))
  3)
