Theory Voblint_Solver.Strategy_Tree_Side_Buffering

theory Strategy_Tree_Side_Buffering
  imports "TD.Basics_side"
begin

section ‹Per-key buffering of one right-hand side's side effects›

text ‹
  One right-hand side may emit several const‹Side› writes to the same key.
  Declaratively that is already a join --- const‹sides_of_rhs›'s own
  const‹Side› equation joins into the key it writes --- but a solver applies
  its update rule once per const‹Side›, on the accumulate reached so far. An
  update rule that keeps one slot per write origin therefore records a partial
  accumulate, and the global it feeds can fall below a value that same origin
  has already established; the drop destabilizes that global's readers, whose
  re-evaluation restores it, and the two alternate without converging.

  ‹buffer_sides› removes the cause rather than the symptom: it accumulates
  every write per key and flushes each key exactly once, so an update rule only
  ever sees a completed contribution. Nothing here is specific to procedure
  calls, contexts, or the D/G layer --- the property being repaired is that a
  right-hand side may name one key twice, which any equation generator can do.
›

subsection ‹The per-key accumulator›

text ‹
  An association list rather than a map: the flush below emits one
  const‹Side› per entry in list order, and ‹acc_add› appends an
  unseen key at the end, so that order is first occurrence in the traversal.
  A map would leave the flush order to the key type's own arrangement, which
  is exactly the scheduling detail a narrowing update rule is sensitive to.
›

fun acc_add ::
  "'g ⇒ 'd::bounded_semilattice_sup_bot ⇒ ('g × 'd) list ⇒ ('g × 'd) list"
where
  "acc_add k d [] = [(k, d)]"
| "acc_add k d ((k', d') # kvs) =
     (if k' = k then (k', d' ⊔ d) # kvs else (k', d') # acc_add k d kvs)"

fun acc_at :: "'g ⇒ ('g × 'd::bounded_semilattice_sup_bot) list ⇒ 'd" where
  "acc_at k [] = bot"
| "acc_at k ((k', d') # kvs) = (if k' = k then d' else bot) ⊔ acc_at k kvs"

definition acc_val ::
  "('g × 'd::bounded_semilattice_sup_bot) list ⇒ 'x + 'g ⇒ 'd" where
  "acc_val acc z = (case z of Inl _ ⇒ bot | Inr k ⇒ acc_at k acc)"

lemma acc_val_Inl [simp]: "acc_val acc (Inl u) = bot"
  by (simp add: acc_val_def)

lemma acc_val_Inr [simp]: "acc_val acc (Inr k) = acc_at k acc"
  by (simp add: acc_val_def)

lemma acc_val_Nil [simp]: "acc_val [] z = bot"
  by (cases z) simp_all

lemma acc_at_add:
  "acc_at k' (acc_add k d acc) = acc_at k' acc ⊔ (if k' = k then d else bot)"
  by (induction acc) (auto simp: ac_simps)

lemma acc_val_add:
  "acc_val (acc_add k d acc) z = acc_val acc z ⊔ (if z = Inr k then d else bot)"
  by (cases z) (simp_all add: acc_at_add)

lemma acc_val_Cons:
  "acc_val ((k, d) # kvs) z = acc_val kvs z ⊔ (if z = Inr k then d else bot)"
  by (cases z) (auto simp: ac_simps)

subsubsection ‹Key order and distinctness›

text ‹What the flush order rests on: an unseen key is appended, a seen one is joined in
  place, so the key list only ever grows at the end and stays distinct.›
lemma acc_keys_add:
  "map fst (acc_add k d acc)
     = (if k ∈ set (map fst acc) then map fst acc else map fst acc @ [k])"
  by (induction acc) (auto simp add: rev_image_eqI)

lemma acc_add_append:
  "k ∉ set (map fst acc) ⟹ acc_add k d acc = acc @ [(k, d)]"
  by (induction acc) (auto simp add: rev_image_eqI)

lemma distinct_acc_add:
  "distinct (map fst acc) ⟹ distinct (map fst (acc_add k d acc))"
  by (cases "k ∈ set (map fst acc)") (simp_all add: acc_keys_add)

subsection ‹Buffering a right-hand side›

text ‹The traversal that does the buffering: writes are collected into the accumulator on
  the way down and emitted at the next flush point, so a query branch cannot lose the writes
  made before it.

  A flush point is every answer, and every local read ‹QueryL y› with
  ‹publish_before y›. Flushing only at answers gives the strongest guarantee: no key is
  written twice on any path, whatever the tree. But it also holds back a write past the
  local reads that follow it, and a local read is what makes the solver evaluate the
  unknown read. A call site publishes the callee's entry state and then reads the
  callee's exit; held back to the answer, the publication reaches the solver only after
  the callee has been solved from an entry it has not yet received. Goblint's
  normal-call transfer issues ‹sidel› before ‹getl›, and its solver applies a
  side effect when it is issued, so there the callee starts from the published entry. A
  flush before such a read costs the at-most-once guarantee for keys written again after
  it, so ‹distinct_side_path_buffer_sides› below states when it still holds, and the
  caller chooses ‹publish_before› where it can tell that it does.›
text ‹
  A key whose completed contribution is const‹bot› is not flushed. Publishing
  it would be a no-op for every consumer --- ‹sides_of_rhs› joins it in, and a
  join with const‹bot› changes nothing --- while still costing the solver an
  update-rule application and a destabilization of that global's readers. The
  generator appends the publication to every answer path without asking whether
  the transfer wrote anything, so on a node that touches no global this is the
  only place the emptiness is known.
›
primrec flush_sides ::
  "('g × 'd::bounded_semilattice_sup_bot) list ⇒ ('x, 'g, 'd) strategy_tree
   ⇒ ('x, 'g, 'd) strategy_tree"
where
  "flush_sides [] t = t"
| "flush_sides (kv # kvs) t =
     (if snd kv ≤ bot then flush_sides kvs t
      else Side (fst kv) (snd kv) (flush_sides kvs t))"

abbreviation flushed :: "('g × 'd::bounded_semilattice_sup_bot) list ⇒ ('g × 'd) list"
  where "flushed acc ≡ filter (λkv. ¬ snd kv ≤ bot) acc"

primrec buffer_aux ::
  "('x ⇒ bool) ⇒ ('g × 'd) list
   ⇒ ('x, 'g, 'd::bounded_semilattice_sup_bot) strategy_tree
   ⇒ ('x, 'g, 'd) strategy_tree"
where
  "buffer_aux publish_before acc (Answer d) = flush_sides acc (Answer d)"
| "buffer_aux publish_before acc (QueryL y g) =
     (if publish_before y
      then flush_sides acc (QueryL y (λv. buffer_aux publish_before [] (g v)))
      else QueryL y (λv. buffer_aux publish_before acc (g v)))"
| "buffer_aux publish_before acc (QueryG y g) =
     QueryG y (λv. buffer_aux publish_before acc (g v))"
| "buffer_aux publish_before acc (Side y d t) = buffer_aux publish_before (acc_add y d acc) t"

definition buffer_sides ::
  "('x ⇒ bool) ⇒ ('x, 'g, 'd::bounded_semilattice_sup_bot) strategy_tree
   ⇒ ('x, 'g, 'd) strategy_tree"
where
  "buffer_sides publish_before t = buffer_aux publish_before [] t"

definition buffer_eqs ::
  "('x ⇒ 'x ⇒ bool) ⇒ ('x, 'g, 'd::bounded_semilattice_sup_bot) eqsT
   ⇒ ('x, 'g, 'd) eqsT" where
  "buffer_eqs publish_before T = (λx. buffer_sides (publish_before x) (T x))"

lemma buffer_eqs_apply [simp]:
  "buffer_eqs publish_before T x = buffer_sides (publish_before x) (T x)"
  by (simp add: buffer_eqs_def)

subsection ‹The declarative reading is unchanged›

text ‹Buffering is invisible to the declarative reading: the local value is untouched, and
  the contribution at each key is the same join as before, only emitted at a flush
  point. That is what makes this a scheduling change and not a semantic one, wherever the
  flush points are.›
lemma traverse_rhs_flush_sides [simp]:
  "traverse_rhs (flush_sides acc t) σ = traverse_rhs t σ"
  by (induction acc) simp_all

lemma traverse_rhs_buffer_aux [simp]:
  "traverse_rhs (buffer_aux publish_before acc t) σ = traverse_rhs t σ"
  by (induction t arbitrary: acc) simp_all

lemma traverse_rhs_buffer_sides [simp]:
  "traverse_rhs (buffer_sides publish_before t) σ = traverse_rhs t σ"
  by (simp add: buffer_sides_def)

lemma sides_of_rhs_flush_sides [simp]:
  "sides_of_rhs (flush_sides acc t) σ z = sides_of_rhs t σ z ⊔ acc_val acc z"
  by (induction acc arbitrary: z)
     (auto simp add: Let_def acc_val_Cons ac_simps dest!: le_bot)

lemma sides_of_rhs_buffer_aux [simp]:
  "sides_of_rhs (buffer_aux publish_before acc t) σ z = sides_of_rhs t σ z ⊔ acc_val acc z"
  by (induction t arbitrary: acc z) (auto simp add: acc_val_add Let_def ac_simps)

lemma sides_of_rhs_buffer_sides [simp]:
  "sides_of_rhs (buffer_sides publish_before t) σ = sides_of_rhs t σ"
  by (rule ext) (simp add: buffer_sides_def)

lemma dep_aux_flush_sides [simp]:
  "dep_aux σ (flush_sides acc t) = dep_aux σ t"
  by (induction acc) simp_all

lemma dep_aux_buffer_aux [simp]:
  "dep_aux σ (buffer_aux publish_before acc t) = dep_aux σ t"
  by (induction t arbitrary: acc) simp_all

lemma dep_aux_buffer_sides [simp]:
  "dep_aux σ (buffer_sides publish_before t) = dep_aux σ t"
  by (simp add: buffer_sides_def)

subsection ‹The operational property the update rules rely on›

text ‹
  Semantic preservation alone does not say a solver sees fewer writes; it says
  the writes sum to the same thing. ‹side_path› names the keys one
  evaluation actually writes, in the order it writes them --- the continuations
  are functions, so which const‹Side› nodes an evaluation meets depends on
  ‹σ›, and the statement has to be relative to it.
›

primrec side_path ::
  "('x + 'g ⇒ 'd) ⇒ ('x, 'g, 'd) strategy_tree ⇒ 'g list" where
  "side_path σ (Answer d) = []"
| "side_path σ (QueryL y g) = side_path σ (g (σ (Inl y)))"
| "side_path σ (QueryG y g) = side_path σ (g (σ (Inr y)))"
| "side_path σ (Side y d t) = y # side_path σ t"

lemma side_path_flush_sides [simp]:
  "side_path σ (flush_sides acc t) = map fst (flushed acc) @ side_path σ t"
  by (induction acc) simp_all

text ‹
  The flush points split one evaluation of the original tree into segments, and
  the buffer writes a key once for each segment in which the key receives a value
  other than const‹bot›. The buffered evaluation therefore writes each key at
  most once exactly when no key receives such a value in two segments.
  ‹settled_writes C P› walks the evaluation and checks this: ‹C› holds the
  keys an earlier flush point has already written, and ‹P› the keys the current
  segment will write at the next one.
›

primrec settled_writes ::
  "('x ⇒ bool) ⇒ ('x + 'g ⇒ 'd) ⇒ 'g set ⇒ 'g set
   ⇒ ('x, 'g, 'd::bounded_semilattice_sup_bot) strategy_tree ⇒ bool"
where
  "settled_writes publish_before σ C P (Answer d) = True"
| "settled_writes publish_before σ C P (QueryL y g) =
     (if publish_before y
      then settled_writes publish_before σ (C ∪ P) {} (g (σ (Inl y)))
      else settled_writes publish_before σ C P (g (σ (Inl y))))"
| "settled_writes publish_before σ C P (QueryG y g) =
     settled_writes publish_before σ C P (g (σ (Inr y)))"
| "settled_writes publish_before σ C P (Side y d t) =
     ((d ≤ bot ∨ y ∉ C)
      ∧ settled_writes publish_before σ C (if d ≤ bot then P else insert y P) t)"

lemma flushed_keys_acc_add:
  "fst ` set (flushed (acc_add k d acc))
     = fst ` set (flushed acc) ∪ (if d ≤ bot then {} else {k})"
proof (induction acc)
  case Nil
  then show ?case by simp
next
  case (Cons kv acc)
  obtain k' d' where kv: "kv = (k', d')" by (cases kv)
  show ?case
  proof (cases "k' = k")
    case True
    then show ?thesis
      by (cases "d' ≤ bot"; cases "d ≤ bot") (auto simp add: kv)
  next
    case False
    then show ?thesis
      using Cons.IH by (cases "d' ≤ bot") (auto simp add: kv)
  qed
qed

lemma distinct_side_path_buffer_aux:
  assumes "distinct (map fst acc)"
    and "fst ` set (flushed acc) ∩ C = {}"
    and "settled_writes publish_before σ C (fst ` set (flushed acc)) t"
  shows "distinct (side_path σ (buffer_aux publish_before acc t))
           ∧ set (side_path σ (buffer_aux publish_before acc t)) ∩ C = {}"
  using assms
proof (induction t arbitrary: acc C)
  case (Answer d)
  then show ?case by (auto simp add: distinct_map_filter)
next
  case (QueryL y g)
  show ?case
  proof (cases "publish_before y")
    case True
    have "distinct (side_path σ (buffer_aux publish_before [] (g (σ (Inl y)))))
        ∧ set (side_path σ (buffer_aux publish_before [] (g (σ (Inl y)))))
            ∩ (C ∪ fst ` set (flushed acc)) = {}"
      by (rule QueryL.IH) (use QueryL.prems(3) True in simp_all)
    then show ?thesis
      using QueryL.prems(1,2) True by (auto simp add: distinct_map_filter)
  next
    case False
    then show ?thesis using QueryL.prems by (simp add: QueryL.IH)
  qed
next
  case (QueryG y g)
  then show ?case by simp
next
  case (Side y d t)
  have "distinct (map fst (acc_add y d acc))"
    using Side.prems(1) by (rule distinct_acc_add)
  moreover have "fst ` set (flushed (acc_add y d acc)) ∩ C = {}"
    unfolding flushed_keys_acc_add using Side.prems(2,3) by auto
  moreover have "settled_writes publish_before σ C (fst ` set (flushed (acc_add y d acc))) t"
    unfolding flushed_keys_acc_add using Side.prems(3) by (cases "d ≤ bot") simp_all
  ultimately have "distinct (side_path σ (buffer_aux publish_before (acc_add y d acc) t))
      ∧ set (side_path σ (buffer_aux publish_before (acc_add y d acc) t)) ∩ C = {}"
    by (rule Side.IH)
  then show ?case by simp
qed

theorem distinct_side_path_buffer_sides:
  assumes "settled_writes publish_before σ {} {} t"
  shows "distinct (side_path σ (buffer_sides publish_before t))"
  using distinct_side_path_buffer_aux[where acc = "[]" and C = "{}"] assms
  by (simp add: buffer_sides_def)

text ‹With no flush point before a local read nothing is ever closed, so the
  condition holds for every tree and every evaluation: flushing at answers only
  writes each key at most once unconditionally.›

lemma settled_writes_answers_only:
  "settled_writes (λ_. False) σ {} P t"
  by (induction t arbitrary: P) simp_all

corollary distinct_side_path_buffer_sides_answers_only:
  "distinct (side_path σ (buffer_sides (λ_. False) t))"
  by (rule distinct_side_path_buffer_sides) (rule settled_writes_answers_only)

text ‹The elision, pinned at both polarities: a completed contribution of
  const‹bot› costs the evaluation no write, and one carrying a value still
  costs exactly one. Stated over ‹side_path› because the writes an evaluation
  performs are what the elision is for.›

lemma side_path_flush_sides_bot [simp]:
  "side_path σ (flush_sides [(k, bot)] t) = side_path σ t"
  by simp

lemma side_path_flush_sides_value:
  assumes "¬ d ≤ bot"
  shows "side_path σ (flush_sides [(k, d)] t) = k # side_path σ t"
  using assms by simp

text ‹
  So a buffered system writes each key at most once per evaluation whenever its
  writes settle: every key an evaluation contributes to is flushed exactly once,
  with its completed contribution, and a key the evaluation never touches is not
  written at all. Flushing at answers only replaces the generator-level
  assumption that no two contributions of one right-hand side can name the same
  key --- an assumption about how equations happen to be built, which a shared
  resume node refutes --- with a property of the equations handed to the solver.
  A caller that also flushes before local reads takes that assumption back for
  the keys written on both sides of such a read, and must know there are none.
›

subsection ‹Buffering is idempotent›

text ‹The entries a flush drops are exactly the ones it would have skipped, so
  flushing the filtered accumulator is the same tree.›
lemma flush_sides_flushed [simp]: "flush_sides (flushed acc) t = flush_sides acc t"
  by (induction acc) simp_all

lemma acc_add_fold_append:
  "distinct (map fst (acc0 @ acc))
     ⟹ foldl (λa kv. acc_add (fst kv) (snd kv) a) acc0 acc = acc0 @ acc"
  by (induction acc arbitrary: acc0) (auto simp add: acc_add_append)

lemma buffer_aux_flush_sides:
  "buffer_aux publish_before acc0 (flush_sides acc t)
     = buffer_aux publish_before
         (foldl (λa kv. acc_add (fst kv) (snd kv) a) acc0 (flushed acc)) t"
  by (induction acc arbitrary: acc0) simp_all

lemma buffer_sides_idem [simp]:
  "buffer_sides publish_before (buffer_sides publish_before t) = buffer_sides publish_before t"
proof -
  have "⋀acc. distinct (map fst acc)
      ⟹ buffer_aux publish_before [] (buffer_aux publish_before acc t)
          = buffer_aux publish_before acc t"
    by (induction t)
       (auto simp add: buffer_aux_flush_sides acc_add_fold_append distinct_acc_add
             distinct_map_filter)
  then show ?thesis by (simp add: buffer_sides_def)
qed

subsection ‹The one transfer every consumer needs›

text ‹
  const‹part_post_solution› --- the solver interface the analyzer soundness
  spine consumes --- is stated over const‹depL›,
  const‹traverse_rhs› and const‹sides_of_rhs› alone. All three are
  invariant under buffering, wherever the flush points are, so a solution of the
  buffered system is a solution of the original one. Buffering preserves this
  declarative right-hand-side semantics and the post-solution obligation the
  soundness proof consumes; it does not claim the buffered and unbuffered systems
  drive the solver through the same operational iteration, or, under widening, to
  the same computed post-solution.
›

lemma dep_buffer_eqs [simp]: "dep (buffer_eqs publish_before T) σ x = dep T σ x"
  by (simp add: dep_def)

lemma dep_L_buffer_eqs [simp]: "depL (buffer_eqs publish_before T) σ x = depL T σ x"
  by (simp add: depL_def)

theorem part_post_solution_buffer_eqs [simp]:
  "part_post_solution (buffer_eqs publish_before T) x σ vars
     = part_post_solution T x σ vars"
  by simp

end