Theory Default_St_Reachability

theory Default_St_Reachability
  imports Default_St_Transfer "Voblint_Domain.Nonrelational_Reachability"
begin

unbundle default_st_syntax

text ‹
  A lifted state reads back pointwise under the lift: const‹Bot› stays
  const‹Bot›, and a const‹Lifted› state becomes const‹Lifted› of the
  function it represents. This is how a solved local unknown, stored as an
  typ‹'a default_st lifted›, enters the result boundary. The readback
  performs no bottom test. const‹canonicalize_lift› collapses a
  witness-bottom payload to const‹Bot› before it, and coverage stays a
  property of the solve: a covered key whose value is const‹Bot› remains a
  key.
›

adhoc_overloading readback == "λ𝒢. map_lift (default_st_to_fun 𝒢)"

section ‹Executable dead-code detection›

text ‹
  A nonrelational state denotes the product of its variables' value sets, so a
  single bottom component makes the whole state denote nothing. Deciding that
  by inspecting every variable is not executable -- there are infinitely many
  names -- so this theory builds a finite test and proves it says exactly what
  the infinite one does.

  The finite test has three witness sources: the local default, which covers
  every name no entry mentions; an explicitly stored entry that is bottom in
  the dictionary the classifier selects for its name; and a scan of the
  program's declared globals, which covers the side where no fresh-name
  argument is available. The middle one needs the classifier, since an entry
  stored in the other dictionary says nothing about the function the state
  represents. Above that sits the lifted view, where a whole unreachable state
  collapses to const‹Bot› and an update that writes a bottom value collapses
  to it incrementally, without re-testing the rest of the state.
›

subsection ‹Raw witness test›

text ‹
  Bottom detection is the one carrier operation that consults ‹𝒢›, and the
  reason is the mismatch between two readings of the same state. The quotient
  typ‹'a default_st› identifies two states exactly when their lookups agree
  at ∗‹every› location, whereas const‹default_st_rep_to_fun› reads each
  name in the one dictionary ‹𝒢› selects for it. An entry for a name in the
  other dictionary is therefore visible to equality and invisible to the
  concretization, and a test that must agree with const‹is_empty_state› on the
  represented function has to skip it. Every other operation on the carrier --
  lookup, update, order, join, widening, narrowing -- treats all locations
  alike and needs no classifier.

  The test below is sufficient but not yet exact, and takes three disjuncts:
  the local default already bottom, which covers every name no entry mentions;
  or a local entry, for a name ‹𝒢› leaves local, that is bottom; or a global
  entry, for a name ‹𝒢› calls global, that is bottom. The global default is
  deliberately not checked. A real program's classifier calls only finitely
  many names global, so no argument independent of the entry list can produce a
  fresh global escaping it, the way a fresh local always exists. Only the entry
  branch can safely observe a global; the exact test in the next subsection
  closes that gap by enumerating the declared globals outright.
›

fun default_st_rep_is_bot ::
  "(vname => bool) => ('a::executable_domain) default_st_rep => bool" where
  "default_st_rep_is_bot 𝒢 ((dl, ls), (dg, gs)) ⟷
     is_empty dl ∨
     (∃x ∈ set (map fst ls). ¬ 𝒢 x ∧ is_empty (default_dict_get (dl, ls) x)) ∨
     (∃x ∈ set (map fst gs). 𝒢 x ∧ is_empty (default_dict_get (dg, gs) x))"

text ‹
  Soundness needs ‹𝒢› to leave infinitely many vnames local, so that some local
  witness always escapes any given (finite) entry list -- true for any
  @{term declared_global} of a real program, which only ever declares finitely
  many globals while @{typ vname} is infinite.
›
lemma default_st_rep_is_bot_sound:
  assumes bot: "default_st_rep_is_bot 𝒢 s"
    and infinite_local: "infinite {x. ¬ 𝒢 x}"
  shows "is_empty_state (default_st_rep_to_fun 𝒢 s)"
proof -
  obtain l g where s_eq: "s = (l, g)" by (cases s)
  obtain dl ls where l_eq: "l = (dl, ls)" by (cases l)
  obtain dg gs where g_eq: "g = (dg, gs)" by (cases g)
  have local_lookup: "default_st_rep_to_fun 𝒢 s x = default_dict_get l x"
    if "¬ 𝒢 x" for x
    using that unfolding default_st_rep_to_fun_def location_of_def s_eq by simp
  have global_lookup: "default_st_rep_to_fun 𝒢 s x = default_dict_get g x"
    if "𝒢 x" for x
    using that unfolding default_st_rep_to_fun_def location_of_def s_eq by simp
  from bot consider
      (local_default) "is_empty dl"
    | (local_entry) x where "¬ 𝒢 x" "is_empty (default_dict_get l x)"
    | (global_entry) x where "𝒢 x" "is_empty (default_dict_get g x)"
    unfolding s_eq l_eq g_eq by auto
  then show ?thesis
  proof cases
    case local_default
    from infinite_local obtain x :: vname
      where x_local: "x ∈ {x. ¬ 𝒢 x}"
        and fresh: "x ∉ set (map fst ls)"
      by (rule obtain_fresh_vname)
    from fresh have "map_of ls x = None"
      by (simp add: map_of_resolved_none_iff)
    then have "default_dict_get l x = dl"
      unfolding l_eq by simp
    with local_default x_local have "is_empty (default_st_rep_to_fun 𝒢 s x)"
      by (simp add: local_lookup)
    then show ?thesis by (rule is_empty_stateI)
  next
    case (local_entry x)
    then have "is_empty (default_st_rep_to_fun 𝒢 s x)"
      by (simp add: local_lookup)
    then show ?thesis by (rule is_empty_stateI)
  next
    case (global_entry x)
    then have "is_empty (default_st_rep_to_fun 𝒢 s x)"
      by (simp add: global_lookup)
    then show ?thesis by (rule is_empty_stateI)
  qed
qed

text ‹
  const‹default_st_rep_is_bot› deliberately leaves the global default unchecked
  because an opaque classifier gives no way to enumerate the (finite) globally
  classified vnames. A concrete program's classifier is always backed by an
  explicit finite list (@{term declared_global_vars}), and enumerating exactly
  that list closes the gap: every globally classified vname is checked
  directly, so the global branch needs no entry witness. This makes the
  combined check an exact (not merely sound) characterization of
  @{const is_empty_state}, with no side condition on ‹𝒢› beyond ‹globals›
  actually listing its true set -- unlike @{thm default_st_rep_is_bot_sound},
  which still needs infinitely many locals to exist.
›

subsection ‹Exact test for the declared globals›

text ‹
  In these names ‹rep› marks a test on representations and ‹for› a test exact for an
  explicit list of declared globals. The classifier-only test
  const‹default_st_rep_is_bot› has no quotient counterpart: it ignores the global
  default, so extensionally equal representatives can disagree on it.
›

definition default_st_rep_is_bot_for ::
  "vname list => (vname => bool) => ('a::executable_domain) default_st_rep => bool" where
  "default_st_rep_is_bot_for globals 𝒢 s =
     ((∃x ∈ set globals. is_empty (default_st_rep_get s (location_of 𝒢 x))) ∨
      default_st_rep_is_bot 𝒢 s)"

lemma default_st_rep_is_bot_for_iff:
  fixes s :: "'a::executable_domain default_st_rep"
  assumes globals: "⋀x. 𝒢 x = (x ∈ set globals)"
  shows "default_st_rep_is_bot_for globals 𝒢 s ⟷ is_empty_state (default_st_rep_to_fun 𝒢 s)"
proof -
  have infinite_local: "infinite {x::vname. ¬ 𝒢 x}"
  proof
    assume fin: "finite {x::vname. ¬ 𝒢 x}"
    have "finite (UNIV :: vname set)"
    proof -
      have univ: "(UNIV :: vname set) = {x. ¬ 𝒢 x} ∪ set globals"
        using globals by auto
      have "finite ({x. ¬ 𝒢 x} ∪ set globals)" using fin by simp
      then show ?thesis unfolding univ .
    qed
    then show False using infinite_literal by simp
  qed
  show ?thesis
  proof
    assume "default_st_rep_is_bot_for globals 𝒢 s"
    then show "is_empty_state (default_st_rep_to_fun 𝒢 s)"
      unfolding default_st_rep_is_bot_for_def
    proof (elim disjE)
      assume "∃x ∈ set globals. is_empty (default_st_rep_get s (location_of 𝒢 x))"
      then obtain x where "is_empty (default_st_rep_get s (location_of 𝒢 x))" by blast
      then have "is_empty (default_st_rep_to_fun 𝒢 s x)"
        unfolding default_st_rep_to_fun_def by simp
      then show ?thesis by (rule is_empty_stateI)
    next
      assume bot: "default_st_rep_is_bot 𝒢 s"
      show ?thesis by (rule default_st_rep_is_bot_sound[OF bot infinite_local])
    qed
  next
    assume "is_empty_state (default_st_rep_to_fun 𝒢 s)"
    then obtain x where x: "is_empty (default_st_rep_to_fun 𝒢 s x)" by (rule is_empty_stateE)
    show "default_st_rep_is_bot_for globals 𝒢 s"
    proof (cases "𝒢 x")
      case True
      then have "x ∈ set globals" using globals by simp
      moreover have "default_st_rep_to_fun 𝒢 s x = default_st_rep_get s (location_of 𝒢 x)"
        unfolding default_st_rep_to_fun_def by (rule refl)
      ultimately have "∃y ∈ set globals. is_empty (default_st_rep_get s (location_of 𝒢 y))"
        using x by auto
      then show ?thesis unfolding default_st_rep_is_bot_for_def by (rule disjI1)
    next
      case False
      note local_x = False
      obtain l g where s_eq: "s = (l, g)" by (cases s)
      obtain dl ls where l_eq: "l = (dl, ls)" by (cases l)
      obtain dg gs where g_eq: "g = (dg, gs)" by (cases g)
      have empty: "is_empty (default_dict_get l x)"
        using x local_x unfolding default_st_rep_to_fun_def location_of_def s_eq by simp
      have "default_st_rep_is_bot 𝒢 s"
      proof (cases "x ∈ set (map fst ls)")
        case True
        with empty local_x
        have "∃y ∈ set (map fst ls). ¬ 𝒢 y ∧ is_empty (default_dict_get (dl, ls) y)"
          unfolding l_eq by blast
        then show ?thesis unfolding s_eq l_eq g_eq by simp
      next
        case False
        then have "map_of ls x = None"
          by (simp add: map_of_resolved_none_iff)
        with empty have "is_empty dl" unfolding l_eq by simp
        then show ?thesis unfolding s_eq l_eq g_eq by simp
      qed
      then show ?thesis unfolding default_st_rep_is_bot_for_def by (rule disjI2)
    qed
  qed
qed


subsection ‹Lifting the exact test to the quotient›

text ‹
  const‹default_st_rep_is_bot_for› takes ‹𝒢› as a free parameter, which makes it
  const‹eq_default_st_rep›-respectful only when ‹𝒢› agrees with ‹globals›: a
  mismatched pair can pick out a witness through one representative's
  dictionary entries that another, extensionally equal, representative encodes
  via its defaults instead. Every real caller supplies the matching pair, so fixing
  ‹𝒢› to ‹λx. x ∈ set globals› internally loses nothing and restores
  unconditional respectfulness -- which is what makes
  ‹default_st_is_bot_for› below liftable to the quotient at all.

  With that pair fixed, respectfulness is a corollary of exactness rather than
  a separate argument: extensionally equal representatives project to the same
  state, and @{thm [source] default_st_rep_is_bot_for_iff} says the test is
  exactly const‹is_empty_state› of that projection.
›

lemma eq_default_st_rep_is_bot_for:
  assumes eq: "eq_default_st_rep s t"
  shows "default_st_rep_is_bot_for globals (λx. x ∈ set globals) s
       = default_st_rep_is_bot_for globals (λx. x ∈ set globals) t"
proof -
  have iff: "default_st_rep_is_bot_for globals (λx. x ∈ set globals) u
      = is_empty_state (default_st_rep_to_fun (λx. x ∈ set globals) u)"
    for u :: "'a default_st_rep"
    by (rule default_st_rep_is_bot_for_iff) simp
  have "default_st_rep_to_fun (λx. x ∈ set globals) s
          = default_st_rep_to_fun (λx. x ∈ set globals) t"
    unfolding default_st_rep_to_fun_def
    by (rule ext) (rule eq_default_st_repD[OF eq])
  then show ?thesis unfolding iff by simp
qed

text ‹
  The exact check transported to the quotient through const‹rep_default_st›.
  This is the predicate the executable side trees are built against: unlike
  const‹is_empty_state› composed with const‹default_st_to_fun›, it has
  a genuine ‹[code]› equation, because it never quantifies over all of
  typ‹vname›.

  It takes ‹globals› alone and rebuilds ‹𝒢› from it internally, rather than
  accepting a caller-supplied classifier. That is what makes
  @{thm [source] eq_default_st_rep_is_bot_for} apply unconditionally, and so what
  makes the definition liftable at all. Nothing is lost: every real caller
  already recomputes the same ‹𝒢› from the same list at the call site.
›

lift_definition default_st_is_bot_for ::
  "vname list => ('a::executable_domain) default_st => bool"
  is "λglobals s. default_st_rep_is_bot_for globals (λx. x ∈ set globals) s"
  by (rule eq_default_st_rep_is_bot_for)

lemma default_st_is_bot_for_iff:
  fixes s :: "'a::executable_domain default_st"
  assumes globals: "⋀x. 𝒢 x = (x ∈ set globals)"
  shows "default_st_is_bot_for globals s ⟷ is_empty_state (ρ⇘𝒢⇙ s)"
proof -
  have gs_eq: "(λx. x ∈ set globals) = 𝒢"
    using globals by (simp add: fun_eq_iff)
  show ?thesis
    unfolding default_st_is_bot_for.rep_eq gs_eq
    by (simp add: default_st_rep_is_bot_for_iff[OF globals]
      default_st_to_fun_rep)
qed

text ‹
  The same test read through the carrier's concretization: a state passes it
  exactly when it describes no store.
›

lemma default_st_is_bot_for_gamma_iff:
  fixes s :: "'a::numeric_domain default_st"
  assumes globals: "⋀x. 𝒢 x = (x ∈ set globals)"
  shows "default_st_is_bot_for globals s ⟷ default_st_gamma 𝒢 s = {}"
  by (simp add: default_st_is_bot_for_iff[OF globals] default_st_gamma_def
      is_empty_state_iff_gamma_state_empty)

lemma exact_emptiness_default_st_is_bot_for:
  assumes globals: "⋀x. 𝒢 x = (x ∈ set globals)"
  shows "exact_emptiness (default_st_is_bot_for globals)
           (default_st_gamma 𝒢 :: 'a::numeric_domain default_st ⇒ store set)"
  by (rule exact_emptinessI) (rule default_st_is_bot_for_gamma_iff[OF globals])

subsection ‹The stores a lifted carrier state describes›

text ‹
  Reachability adds no new concretization: a lifted carrier state is read
  through const‹gamma_lift› over const‹default_st_gamma›, as a lifted
  pointwise state is read through const‹gamma_lift› over
  const‹gamma_state›. The lemma below relates the two through the
  represented function.
›

lemma gamma_lift_default_st_gamma_to_fun:
  "gamma_lift (default_st_gamma 𝒢) = (λd. γ⊥ (map_lift ρ⇘𝒢⇙ d))"
  by (rule ext) (simp add: gamma_lift_def default_st_gamma_def split: lifted.split)


subsection ‹Incremental dead-code tracking›

text ‹
  ‹live_default_st› states that no location of the readback is empty, the
  condition under which a transfer result counts as reachable.
›


definition live_default_st ::
  "(vname => bool) => ('a::executable_domain) default_st => bool"
where
  "live_default_st 𝒢 s = (~ is_empty_state (ρ⇘𝒢⇙ s))"

text ‹
  ‹is_empty_state› on ‹ρ𝒢 s› is an infinite existential over
  ‹vname›, not executable on a quotient value.  ‹default_st_set_lift› tracks it
  incrementally instead: given a @{const live_default_st} input and the single
  freshly computed element, the result is witness-bottom iff that element is
  ‹is_empty› -- every other location is provably unchanged
  (@{thm default_st_to_fun_set}), so it cannot newly become bottom.  This
  mirrors Goblint's per-analysis ‹Deadcode› raise while staying generic: it lives at
  the shared update primitive, not in each domain's own transfer code.
›

definition default_st_set_lift ::
  "('a::executable_domain) default_st lifted => location => 'a => 'a default_st lifted"
where
  "default_st_set_lift x loc a = do {
     s <- x;
     if is_empty a then Bot else Lifted s⟨loc := a⟩
   }"

lemma default_st_set_lift_Bot [simp]:
  "default_st_set_lift Bot loc a = Bot"
  unfolding default_st_set_lift_def by simp

lemma default_st_set_lift_Lifted:
  "default_st_set_lift (Lifted s) loc a =
     (if is_empty a then Bot else Lifted s⟨loc := a⟩)"
  unfolding default_st_set_lift_def by simp

text ‹
  The completeness theorem the location-scoped check relies on: from a live input,
  the lifted update exactly tracks the spec-level normalized result.  Only the
  freshly written variable's element needs checking -- every other variable's
  ‹abs_state› component provably survives unchanged
  (@{thm default_st_to_fun_set}), so it cannot be the source of a new
  witness-bottom.
›
lemma default_st_set_lift_correct:
  fixes s :: "'a::executable_domain default_st"
  assumes live: "live_default_st 𝒢 s"
  shows "map_lift ρ⇘𝒢⇙
           (default_st_set_lift (Lifted s) (location_of 𝒢 x) a) =
         normalize_lift is_empty_state ((ρ⇘𝒢⇙ s)(x := a))"
proof -
  from live have live': "¬ is_empty_state (ρ⇘𝒢⇙ s)"
    unfolding live_default_st_def by simp
  have upd: "is_empty_state ((ρ⇘𝒢⇙ s)(x := a)) = is_empty a"
    by (rule is_empty_state_fun_upd_iff[OF live'])
  show ?thesis
    by (cases "is_empty a")
      (simp_all add: default_st_set_lift_Lifted upd)
qed

unbundle no default_st_syntax

end