Theory Voblint_Compile.Compile_Invariants

theory Compile_Invariants
  imports Compile_Wellformed "Voblint_VIMP.VIMP_Program" "Voblint_CFG.CFG_Prune"
begin

section ‹What a program must satisfy, and what its graph then satisfies›

text ‹
  Two things live here.  ‹wf_compile_input› is the contract a source program has to meet
  before it is worth compiling: the procedure names are distinct, the entry procedure is
  declared and takes no arguments, and every body is a well-formed source command.
  ‹wf_program_compile_input_exec› is the executable form of that contract, and the check
  the command-line tool actually runs --- the contract itself quantifies over the whole
  procedure table, which no generated code can enumerate, so the executable form walks the
  declaration list instead and is proved sufficient.

  The remaining results are about the graph that comes out: a source const‹Return›
  compiles to an edge into its own term‹FunctionResult›, every statement index is listed
  in source post-order (‹prog_stmt_post_order›) so a report can name its source command,
  and the entry node reaches the exit.
›

subsection ‹Compiler input well-formedness›

text ‹‹prog_main_name› names the distinguished entry procedure, declared in ‹Π› with an
  empty formal list; ‹main_body Π› reads its body back out.  The callee list ‹ps› enumerates
  the other declared procedures (‹dom Π› minus ‹prog_main_name›), so the entry's nodes never
  collide with a callee's, yet ‹FunctionEntry prog_main_name› is an ordinary activation of a
  declared procedure like any other.›
definition wf_compile_input ::
  "(vname => bool) => 'a ptable => pname list => bool" where
  "wf_compile_input 𝒢 Π ps ⟷
     distinct ps ∧
     set ps = {p. Π p ~= None} - {prog_main_name} ∧
     prog_main_name ∉ set ps ∧
     wf_source_program 𝒢 Π"

text ‹The leaf definitions an executable instance unfolds to decide the contract.  No
  abstract proof unfolds them, so they only ever fire under ‹unfolding wf_compile_input_simps›.›
declare
  reserved_ret_var_def [wf_compile_input_simps]
  source_exp_def [wf_compile_input_simps]
  valid_formal_def [wf_compile_input_simps]
  value_providing_def [wf_compile_input_simps]
  special_table_def [wf_compile_input_simps]
  special_pname_nondet_int_def [wf_compile_input_simps]
  special_pname_min_def [wf_compile_input_simps]
  special_pname_max_def [wf_compile_input_simps]
  ret_var_def [wf_compile_input_simps]
  prog_main_name_def [wf_compile_input_simps]
  main_body_def [wf_compile_input_simps]

lemma wf_compile_inputD:
  assumes "wf_compile_input 𝒢 Π ps"
  shows "reserved_ret_var 𝒢" and "Π prog_main_name = Some ⦇formals = [], body = (main_body Π)⦈"
    and "wf_source_com Π (main_body Π)" and "no_return (main_body Π)"
    and "Π p = Some decl ⟹ wf_proc_decl 𝒢 Π decl"
    and "Π p = Some decl ⟹ special_table p = None"
    and "source_pi Π" and "source_com (main_body Π)"
    and "distinct ps" and "set ps = {p. Π p ≠ None} - {prog_main_name}"
    and "prog_main_name ∉ set ps"
  using wf_source_programD
    assms[unfolded wf_compile_input_def] by auto

lemmas wf_compile_input_reserved_ret_var [dest] = wf_compile_inputD(1)

abbreviation wf_program_compile_input :: "imp_prog ⇒ bool" where
  "wf_program_compile_input p ≡
    wf_compile_input (declared_global p) (prog_table p) (prog_procs p)"

subsection ‹An executable, sufficient reformulation›

text ‹
  ‹wf_source_program›'s two ‹∀p. Π p = ... ⟶ ...› conjuncts range over
  ‹Π :: pname => proc_decl option›'s entire (unbounded) function domain, so
  const‹wf_program_compile_input› cannot be exported/evaluated as literally
  stated -- Isabelle's code generator has nothing to enumerate a function's
  domain against. ‹proc_rep› is always finite in practice (‹prog_table p =
  map_of (proc_rep p)›), so both conjuncts are provably implied by the
  corresponding ‹list_all› check over ‹proc_rep p› directly: every entry
  ‹proc_rep p› actually lists is checked, which is at least as strong as
  checking only the ones ‹map_of› would resolve a lookup to.

  ‹wf_program_compile_input_exec› only needs to be ∗‹sufficient› for the CLI
  well-formedness gate to be sound (never silently accepting a source program
  const‹wf_program_compile_input› would reject) -- not a decision procedure
  complete in the other direction, which ‹map_of›'s first-occurrence-wins
  semantics on a ‹proc_rep› with a shadowed duplicate key would make
  materially harder to establish for no operational benefit here.
›

definition wf_program_compile_input_exec :: "imp_prog => bool" where
  "wf_program_compile_input_exec p ⟷
     (let procs = proc_rep p; 𝒢 = declared_global p; pi = map_of procs
      in reserved_ret_var 𝒢 ∧
         distinct (prog_procs p) ∧
         set (prog_procs p) = set (map fst procs) - {prog_main_name} ∧
         prog_main_name ∉ set (prog_procs p) ∧
         pi prog_main_name = Some (⦇formals = [], body = prog_main p⦈) ∧
         wf_source_com pi (prog_main p) ∧ no_return (prog_main p) ∧
         list_all (λ(_, decl). wf_proc_decl 𝒢 pi decl) procs ∧
         list_all (λ(q, _). special_table q = None) procs)"

lemma wf_program_compile_input_exec_sound:
  assumes "wf_program_compile_input_exec p"
  shows "wf_program_compile_input p"
proof -
  define procs where "procs = proc_rep p"
  define 𝒢 where "𝒢 = declared_global p"
  define pi where "pi = map_of procs"
  have pi_eq: "pi = prog_table p"
    unfolding pi_def procs_def prog_table_def ..
  from assms have h:
    "reserved_ret_var 𝒢"
    "distinct (prog_procs p)"
    "set (prog_procs p) = set (map fst procs) - {prog_main_name}"
    "prog_main_name ∉ set (prog_procs p)"
    "pi prog_main_name = Some (⦇formals = [], body = prog_main p⦈)"
    "wf_source_com pi (prog_main p)"
    "no_return (prog_main p)"
    "list_all (λ(_, decl). wf_proc_decl 𝒢 pi decl) procs"
    "list_all (λ(q, _). special_table q = None) procs"
    unfolding wf_program_compile_input_exec_def procs_def 𝒢_def pi_def Let_def
    by simp_all
  have domain: "set (map fst procs) = {q. pi q ≠ None}"
    unfolding pi_def by (induction procs) auto
  have every_decl: "⋀q decl. pi q = Some decl ⟹ wf_proc_decl 𝒢 pi decl"
  proof -
    fix q decl assume "pi q = Some decl"
    then have "(q, decl) ∈ set procs" unfolding pi_def by (rule map_of_SomeD)
    with h(8) show "wf_proc_decl 𝒢 pi decl" by (auto simp: list_all_iff)
  qed
  have every_special: "⋀q. pi q ≠ None ⟹ special_table q = None"
  proof -
    fix q assume "pi q ≠ None"
    then    obtain decl where "(q, decl) ∈ set procs"
      using domain by force
    with h(9) show "special_table q = None" by (auto simp: list_all_iff)
  qed
  have main_eq: "main_body pi = prog_main p" using pi_eq by simp
  have wf_source_program: "wf_source_program 𝒢 pi"
    unfolding wf_source_program_def main_eq
    using h(1) h(5) h(6) h(7) every_decl every_special by blast
  have wf_compile_input: "wf_compile_input 𝒢 pi (prog_procs p)"
    unfolding wf_compile_input_def
    using h(2) h(3) h(4) wf_source_program
    using domain by blast
  show ?thesis
    unfolding 𝒢_def[symmetric] pi_eq[symmetric] using wf_compile_input .
qed

text ‹const‹compile_prog› takes a typ‹proc_table› and a typ‹pname list›, so a
  domain holding only an typ‹imp_prog› needs the two-projection wrapper below.  The entry
  name is const‹prog_main_name› and its body is looked up, so neither is threaded through
  as a parameter.  This is also the name the OCaml export publishes: a second wrapper for
  the export alone would put the same function in the generated module twice.›

definition prog_cfg :: "imp_prog => cfg" where
  "prog_cfg p = compile_prog (prog_table p) (prog_procs p)"

subsection ‹Syntactic occurrence predicates›

text ‹‹returns_in› asks a purely syntactic question -- does this command contain a
  ‹Return› of exactly this expression -- which is the shape the compiler lemmas below need.
  They induct over the source command, so their hypothesis has to be readable off the
  syntax rather than off the graph the compiler produced from it.›
fun returns_in :: "exp option ⇒ 'a acom ⇒ bool" where
  "returns_in e (Return i e') = (e = e')"
| "returns_in e (Seq c1 c2) = (returns_in e c1 ∨ returns_in e c2)"
| "returns_in e (If i _ c1 c2) = (returns_in e c1 ∨ returns_in e c2)"
| "returns_in e (While i _ c) = returns_in e c"
| "returns_in e _ = False"

subsection ‹Return edges and call-freeness›

text ‹Every source ‹Return e› in procedure ‹p› compiles to an intra edge that
  reaches ‹FunctionResult p› through ‹EA_Ret e p›.›
lemma compile_return_edge:
  "compile Π p c k n = (n', en, E, K) ⟹ returns_in e c
   ⟹ ∃j. (Statement j, EA_Ret e p, FunctionResult p) ∈ E"
proof (induction c arbitrary: k n n' en E K rule: gcom.induct)
  case (Seq c1 c2)
  from Seq.prems(1) obtain n1 en1 E1 K1 n2 en2 E2 K2 where
    c1: "compile Π p c1 (Statement (n + csize c1)) n = (n1, en1, E1, K1)"
    and c2: "compile Π p c2 k (n + csize c1) = (n2, en2, E2, K2)"
    and E: "E = E1 ∪ E2"
    by (auto simp: Let_def split: prod.splits)
  from Seq.prems(2) have "returns_in e c1 ∨ returns_in e c2" by simp
  then show ?case using Seq.IH(1)[OF c1] Seq.IH(2)[OF c2] E by auto
next
  case (If i b c1 c2)
  from If.prems(1) obtain n1 en1 E1 K1 n2 en2 E2 K2 where
    c1: "compile Π p c1 k (Suc n) = (n1, en1, E1, K1)"
    and c2: "compile Π p c2 k n1 = (n2, en2, E2, K2)"
    and E: "E = {(Statement n, EA_Assume b, if is_SKIP c1 then k else en1),
                  (Statement n, EA_AssumeNot b, if is_SKIP c2 then k else en2)}
                  ∪ (if is_SKIP c1 then {} else E1) ∪ (if is_SKIP c2 then {} else E2)"
    by (auto split: prod.splits)
  from If.prems(2) have "returns_in e c1 ∨ returns_in e c2" by simp
  then show ?case using If.IH(1)[OF c1] If.IH(2)[OF c2] E
    by (auto split: if_splits)
next
  case (While i b c)
  from While.prems(1) obtain n1 en1 E1 K1 where
    c1: "compile Π p c (Statement n) (Suc n) = (n1, en1, E1, K1)"
    and E: "E = {(Statement n, EA_Assume b, en1), (Statement n, EA_AssumeNot b, k)} ∪ E1"
    by (auto split: prod.splits)
  from While.prems(2) have "returns_in e c" by simp
  then show ?case using While.IH[OF c1] E by auto
next
  case (Return i e') then show ?case by (auto split: if_splits)
qed auto

subsection ‹Public compiler invariants›

text ‹A ‹Return› fragment ignores its continuation: it emits its result edge and nothing
  else, so no node exists to carry a control flow the source does not have.›
theorem compile_Return_ignores_continuation:
  "compile Π p (Return i e) k n = compile Π p (Return i e) k' n"
  by simp

section ‹Where each statement index came from in the source›

text ‹A source position is nowhere in the compiled graph, yet a report has to name one.
  What follows supplies the missing link in two steps: an enumeration of the statement
  indices a fragment allocates, ordered the way a front end completes its reductions, and
  the lemmas pinning that enumeration to the compiler's own allocation.›
subsection ‹Statement order in the source›

text ‹
  Which const‹Statement› index const‹compile› gives each command of a fragment, listed in
  the order a bottom-up parser completes the commands: a reduction finishes only once its
  whole right-hand side has been read, so an ‹if› is listed after both its branches and a
  loop after its body.  Every clause allocates the same index its const‹compile›
  counterpart does: a leaf takes ‹n›; const‹Seq› hands ‹c2› the counter ‹c1› ends at;
  const‹If› and const‹While› take ‹n› for the guard and start the body at ‹Suc n›.
  A front end that records one position per reduction can pair its ‹k›th position with
  this list's ‹k›th index without reproducing const‹compile›'s counter arithmetic.
›

fun com_stmt_post_order :: "nat ⇒ 'a acom ⇒ cfg_node list" where
  "com_stmt_post_order n (SKIP i) = [Statement n]"
| "com_stmt_post_order n (Assign i x a) = [Statement n]"
| "com_stmt_post_order n (Check i l c) = [Statement n]"
| "com_stmt_post_order n (Seq c1 c2) =
     com_stmt_post_order n c1 @ com_stmt_post_order (n + csize c1) c2"
| "com_stmt_post_order n (If i b c1 c2) =
     com_stmt_post_order (Suc n) c1 @ com_stmt_post_order (Suc n + csize c1) c2
       @ [Statement n]"
| "com_stmt_post_order n (While i b c) = com_stmt_post_order (Suc n) c @ [Statement n]"
| "com_stmt_post_order n (Call i dst q actuals) = [Statement n]"
| "com_stmt_post_order n (Return i e) = [Statement n]"
| "com_stmt_post_order n (Restore i) = [Statement n]"
| "com_stmt_post_order n (Unwind i) = [Statement n]"

text ‹The enumeration has one entry per index the fragment allocates: a shorter or
  longer list would silently misalign every position after it.›

lemma length_com_stmt_post_order [simp]: "length (com_stmt_post_order n c) = csize c"
  by (induction c arbitrary: n) auto

text ‹
  const‹com_stmt_post_order› answers this for one fragment given the counter it
  starts at. A whole program needs those starting counters, and they are not a
  fragment-local fact: const‹compile_procs› lays the procedures out one after another
  and const‹compile_prog› compiles ‹main› after all of them, whatever order the source
  wrote them in.

  ‹procs_stmt_next› below is that layout and nothing else --- each declared procedure
  advances the counter past its body and past the one index const‹compile_proc›
  reserves for the epilogue. Undeclared names advance it by nothing, matching the
  clause const‹compile_procs› skips.
›

fun procs_stmt_next :: "'a ptable ⇒ pname list ⇒ nat ⇒ nat" where
  "procs_stmt_next Π [] n = n"
| "procs_stmt_next Π (p # ps) n =
     (case Π p of
        None ⇒ procs_stmt_next Π ps n
      | Some decl ⇒ procs_stmt_next Π ps (Suc (n + csize (body decl))))"

text ‹
  Each definition paired with the statement indices its body owns, in the order a
  bottom-up parser finishes them. ‹main› comes last because that is where
  const‹compile_prog› compiles it, not because of where it appears in the source ---
  which is exactly why a front end cannot pair a flat position list against the program
  and must group by definition first.
›

fun defs_stmt_post_order ::
  "'a ptable ⇒ pname list ⇒ nat ⇒ (pname × cfg_node list) list"
where
  "defs_stmt_post_order Π [] n = []"
| "defs_stmt_post_order Π (p # ps) n =
     (case Π p of
        None ⇒ defs_stmt_post_order Π ps n
      | Some decl ⇒
          (p, com_stmt_post_order n (body decl))
            # defs_stmt_post_order Π ps (Suc (n + csize (body decl))))"

definition prog_stmt_post_order :: "imp_prog ⇒ (pname × cfg_node list) list" where
  "prog_stmt_post_order p =
     defs_stmt_post_order (prog_table p) (prog_procs p) 0
       @ [(prog_main_name,
           com_stmt_post_order (procs_stmt_next (prog_table p) (prog_procs p) 0)
             (prog_main p))]"

section ‹Connectivity of a compiled graph›

text ‹A compiled fragment's entry reaches its continuation or its procedure result along
  the structural successor relation.  Nothing downstream consumes this: a routed D/G
  system's own coverage witness is ‹vars_cover_exec›, decided per node from whether a
  caller actually published a seed, not from graph reachability.  This connectivity fact
  is instead what pins the transparent const‹Restore›/const‹Unwind› encoding ---
  emitting nothing for those clauses would make them dead ends and force a
  const‹source_com› hypothesis onto the whole-program statement.›

text ‹The fragment-relative forms the compiler inductions use: an edge of a fragment
  included in the graph.›
lemma local_reaches_intra_sub:
  "(u, a, v) ∈ E ⟹ E ⊆ intra g ⟹ local_reaches g u v"
  by (auto intro: local_reaches_intra_step)

lemma local_reaches_comb_caller_sub:
  "(c, ca, FunctionEntry q, k) ∈ K ⟹ K ⊆ calls g ⟹ local_reaches g c k"
  by (auto intro: local_reaches_comb_step)

subsection ‹Compiled entry reaches its exit or its procedure result›

text ‹A fragment's entry leads to its continuation or, along an explicit const‹Return›
  edge, to the enclosing procedure's term‹FunctionResult›.  Both stay inside the
  activation, so the statement is about const‹local_reaches›.›
lemma compile_reaches:
  assumes "compile Π p c k n = (n', en, E, K)" and "E ⊆ intra g" and "K ⊆ calls g"
  shows "local_reaches g en k ∨ local_reaches g en (FunctionResult p)"
  using assms
proof (induction c arbitrary: k n n' en E K)
  case (Seq c1 c2)
  from Seq.prems(1) obtain n1 E1 K1 n2 E2 K2 where
      c1: "compile Π p c1 (Statement (n + csize c1)) n = (n1, Statement n, E1, K1)"
    and c2: "compile Π p c2 k (n + csize c1) = (n2, Statement (n + csize c1), E2, K2)"
    and res: "en = Statement n" "E = E1 ∪ E2" "K = K1 ∪ K2"
    by (rule compile_SeqE)
  have h1: "local_reaches g (Statement n) (Statement (n + csize c1))
            ∨ local_reaches g (Statement n) (FunctionResult p)"
    using Seq.IH(1)[OF c1] Seq.prems res by auto
  have h2: "local_reaches g (Statement (n + csize c1)) k
            ∨ local_reaches g (Statement (n + csize c1)) (FunctionResult p)"
    using Seq.IH(2)[OF c2] Seq.prems res by auto
  from h1 h2 show ?case unfolding res(1) by (blast intro: local_reaches_trans)
next
  case (If i b c1 c2)
  from If.prems(1) obtain n1 E1 K1 E2 K2 where
      c1: "compile Π p c1 k (Suc n) = (n1, Statement (Suc n), E1, K1)"
    and res: "en = Statement n"
      "E = {(Statement n, EA_Assume b, if is_SKIP c1 then k else Statement (Suc n)),
            (Statement n, EA_AssumeNot b,
              if is_SKIP c2 then k else Statement (Suc n + csize c1))}
            ∪ (if is_SKIP c1 then {} else E1) ∪ (if is_SKIP c2 then {} else E2)"
      "K = K1 ∪ K2"
    by (rule compile_IfE)
  have b1: "local_reaches g (Statement n) (if is_SKIP c1 then k else Statement (Suc n))"
    using res(2) If.prems(2) by (blast intro: local_reaches_intra_sub)
  show ?case
  proof (cases "is_SKIP c1")
    case True with b1 res(1) show ?thesis by simp
  next
    case False
    have "local_reaches g (Statement (Suc n)) k
          ∨ local_reaches g (Statement (Suc n)) (FunctionResult p)"
      using If.IH(1)[OF c1] If.prems res False by auto
    with b1 False show ?thesis unfolding res(1) by (auto intro: local_reaches_trans)
  qed
qed (auto simp: Let_def split: prod.splits option.splits
     intro: local_reaches_intra_sub local_reaches_comb_caller_sub)

lemma compile_proc_reaches_result:
  assumes "compile_proc Π p decl n = (n', E, K)"
      and "E ⊆ intra g" "K ⊆ calls g"
  shows "local_reaches g (FunctionEntry p) (FunctionResult p)"
proof -
  let ?r = "n + csize (body decl)"
  obtain Eb where
      body: "compile Π p (body decl) (Statement ?r) n = (?r, Statement n, Eb, K)"
    and E_eq: "E = insert (FunctionEntry p, EA_Body p, Statement n)
                     (insert (Statement ?r, EA_Ret None p, FunctionResult p) Eb)"
    by (rule compile_procE[OF assms(1)])
  have Ebg: "Eb ⊆ intra g" using E_eq assms(2) by auto
  have entry: "local_reaches g (FunctionEntry p) (Statement n)"
    using E_eq assms(2) by (blast intro: local_reaches_intra_sub)
  have epi: "local_reaches g (Statement ?r) (FunctionResult p)"
    using E_eq assms(2) by (blast intro: local_reaches_intra_sub)
  from compile_reaches[OF body Ebg assms(3)] entry epi show ?thesis
    by (blast intro: local_reaches_trans)
qed

theorem compile_prog_entry_cfg_reaches_exit:
  "cfg_reaches (compile_prog Π ps)
     (cfg_entry (compile_prog Π ps)) (cfg_exit (compile_prog Π ps))"
proof -
  obtain n1 Eprocs Kprocs n2 Emain Kmain where
      cmain: "compile_proc Π prog_main_name ⦇formals = [], body = (main_body Π)⦈ n1
                = (n2, Emain, Kmain)"
    and g: "intra (compile_prog Π ps) = Eprocs ∪ Emain"
           "calls (compile_prog Π ps) = Kprocs ∪ Kmain"
    by (rule compile_prog_intra_split)
  have "cfg_reaches (compile_prog Π ps)
          (FunctionEntry prog_main_name) (FunctionResult prog_main_name)"
    using g by (intro local_reaches_cfg_reaches compile_proc_reaches_result[OF cmain]) auto
  then show ?thesis by simp
qed

end