Theory CFG_Enumeration
theory CFG_Enumeration
imports "Voblint_CFG.CFG_Def"
begin
section ‹Solver-facing CFG enumeration›
text ‹
Backward predecessor relations and deterministic edge enumeration over a compiled
two-relation CFG. These are the equation-generation and code-generation views the TD
solver reads to build and evaluate the constraint system; they are not part of the
concrete CFG semantics --- ‹valid_activation_trace› never refers to them. Ordinary control flow is
enumerated over \<^const>‹intra›; procedure calls --- entry routing and return combining ---
over \<^const>‹calls›. There is no unified edge set and no separate combine relation: a
return is recovered from the same \<^const>‹calls› tuple that created the activation.
Every enumeration appears twice: as a set, which the proofs reason over, and as a list,
which code generation and the solver's folds consume, identified by a ‹set_..._list›
lemma whenever the edge set is finite. On the return side there is one of each ---
the set ‹call_targets› and the list ‹call_target_list› --- and every other return view
is an image or a ‹map› of those, so the views cannot come to disagree about which edges
they enumerate.
›
subsection ‹Intra predecessors›
text ‹The ordinary incoming transitions of a node: an intra edge is a total store
transformer within one activation, so this is the transfer fold the equation system runs
at every node --- including the \<^term>‹EA_Ret e p› edges into \<^term>‹FunctionResult p›, by
which return values are summarised as ordinary predecessor folding.›
definition intra_predecessors :: "cfg ⇒ cfg_node ⇒ (cfg_node × edge_action) set" where
"intra_predecessors g v = {(u, a). (u, a, v) ∈ intra g}"
lemma intra_predecessors_iff [simp]:
"(u, a) ∈ intra_predecessors g v ⟷ (u, a, v) ∈ intra g"
by (simp add: intra_predecessors_def)
lemma finite_intra_predecessors [intro]:
assumes "finite (intra g)"
shows "finite (intra_predecessors g v)"
proof -
have "intra_predecessors g v
⊆ (λe :: cfg_node × edge_action × cfg_node. (fst e, fst (snd e))) ` intra g"
unfolding intra_predecessors_def by force
then show ?thesis using assms finite_subset finite_imageI by blast
qed
definition intra_predecessor_list ::
"cfg ⇒ cfg_node ⇒ (cfg_node × edge_action) list" where
"intra_predecessor_list g v =
map (λ(u, a, w). (u, a)) (filter (λ(u, a, w). w = v) (cfg_intra_list g))"
lemma set_intra_predecessor_list [simp]:
assumes "finite (intra g)"
shows "set (intra_predecessor_list g v) = intra_predecessors g v"
unfolding intra_predecessor_list_def intra_predecessors_def
using set_cfg_intra_list[OF assms] by (force simp: image_iff)
subsection ‹Call-entry enumeration›
text ‹The call tuples whose callee entry is the queried node ‹v› (a \<^term>‹FunctionEntry p›).
The continuation does not affect the callee-entry state, so it is projected away; the
entry contribution is the caller state routed through the call action's parameter
binding, which is what an analysis's entry operation computes.›
definition entry_calls :: "cfg ⇒ cfg_node ⇒ (cfg_node × call_action) set" where
"entry_calls g v = {(c, ca). ∃k. (c, ca, v, k) ∈ calls g}"
lemma entry_calls_iff [simp]:
"(c, ca) ∈ entry_calls g v ⟷ (∃k. (c, ca, v, k) ∈ calls g)"
by (simp add: entry_calls_def)
lemma finite_entry_calls [intro]:
assumes "finite (calls g)"
shows "finite (entry_calls g v)"
proof -
have "entry_calls g v
⊆ (λe :: cfg_node × call_action × cfg_node × cfg_node. (fst e, fst (snd e))) ` calls g"
unfolding entry_calls_def by force
then show ?thesis using assms finite_subset finite_imageI by blast
qed
definition entry_call_list :: "cfg ⇒ cfg_node ⇒ (cfg_node × call_action) list" where
"entry_call_list g v =
map (λ(c, ca, ce, k). (c, ca)) (filter (λ(c, ca, ce, k). ce = v) (cfg_calls_list g))"
lemma set_entry_call_list [simp]:
assumes "finite (calls g)"
shows "set (entry_call_list g v) = entry_calls g v"
unfolding entry_call_list_def entry_calls_def
using set_cfg_calls_list[OF assms] by (force simp: image_iff)
subsection ‹Outgoing call enumeration (caller perspective)›
text ‹The call tuples whose call site is the queried node ‹v›: the entry, action, and
continuation of every call ‹v› makes. This is the caller-side counterpart of
\<^const>‹entry_calls› (callee-indexed) --- routing a call-entry seed publication at its call
site needs the callee entry and the whole \<^typ>‹call_action› together, which the
continuation-indexed enumerations below do not expose.›
definition call_successors ::
"cfg ⇒ cfg_node ⇒ (cfg_node × call_action × cfg_node) set" where
"call_successors g v = {(ce, ca, k). (v, ca, ce, k) ∈ calls g}"
lemma call_successors_iff [simp]:
"(ce, ca, k) ∈ call_successors g v ⟷ (v, ca, ce, k) ∈ calls g"
by (simp add: call_successors_def)
lemma finite_call_successors [intro]:
assumes "finite (calls g)"
shows "finite (call_successors g v)"
proof -
have "call_successors g v
⊆ (λ(c, ca, ce, k). (ce, ca, k)) ` calls g"
unfolding call_successors_def by force
then show ?thesis using assms finite_subset finite_imageI by blast
qed
definition call_successor_list ::
"cfg ⇒ cfg_node ⇒ (cfg_node × call_action × cfg_node) list" where
"call_successor_list g v =
map (λ(c, ca, ce, k). (ce, ca, k)) (filter (λ(c, ca, ce, k). c = v) (cfg_calls_list g))"
lemma set_call_successor_list [simp]:
assumes "finite (calls g)"
shows "set (call_successor_list g v) = call_successors g v"
unfolding call_successor_list_def call_successors_def
using set_cfg_calls_list[OF assms] by (force simp: image_iff)
subsection ‹Call targets at a continuation›
text ‹
The call edges whose continuation is ‹v›, each paired with the procedure its callee entry
names. This is the one continuation-indexed enumeration that reads \<^const>‹calls›
directly; every return-side view below is an image or a map of it.
‹call_target_at› decodes a single call edge, selecting on the continuation and reading the
callee's procedure off \<^term>‹FunctionEntry p› in one total function, so there is no
unreachable-but-present branch and no later definition has to re-establish that the node
it is looking at really is a callee entry.
Naming the callee by its procedure rather than by the \<^term>‹FunctionResult p› node its
result is read at keeps the call site's own data --- where the call is, what it passes,
where it returns to --- separate from the callee, which is what lets a resolver choose
among candidates rather than read a callee the enumeration already fixed. The result
node is then ‹FunctionResult p› at the point of use.
›
definition call_targets ::
"cfg ⇒ cfg_node ⇒ (cfg_node × call_action × pname) set" where
"call_targets g v = {(c, ca, p). (c, ca, FunctionEntry p, v) ∈ calls g}"
lemma call_targets_iff [simp]:
"(c, ca, p) ∈ call_targets g v ⟷ (c, ca, FunctionEntry p, v) ∈ calls g"
by (simp add: call_targets_def)
fun call_target_at ::
"cfg_node ⇒ (cfg_node × call_action × cfg_node × cfg_node)
⇒ (cfg_node × call_action × pname) option" where
"call_target_at v (c, ca, FunctionEntry p, k) =
(if k = v then Some (c, ca, p) else None)"
| "call_target_at v _ = None"
definition call_target_list ::
"cfg ⇒ cfg_node ⇒ (cfg_node × call_action × pname) list" where
"call_target_list g v = List.map_filter (call_target_at v) (cfg_calls_list g)"
lemma set_map_filter_call_target_at:
"set (List.map_filter (call_target_at v) es)
= {(c, ca, p). (c, ca, FunctionEntry p, v) ∈ set es}"
proof (induction es)
case Nil
show ?case by simp
next
case (Cons e es)
obtain c ca ce k where e: "e = (c, ca, ce, k)" by (cases e) auto
show ?case by (cases ce) (auto simp: e Cons.IH)
qed
text ‹Membership, in the form a call-site obligation states it: the enumeration lists
exactly the call edges of ‹g› whose continuation is ‹v›, each paired with the
procedure its callee entry names.›
lemma set_call_target_list [simp]:
assumes "finite (calls g)"
shows "set (call_target_list g v) = call_targets g v"
unfolding call_target_list_def call_targets_def
by (simp add: set_map_filter_call_target_at set_cfg_calls_list[OF assms])
lemma finite_call_targets [intro]:
assumes "finite (calls g)"
shows "finite (call_targets g v)"
proof -
have "call_targets g v = set (call_target_list g v)"
using set_call_target_list[OF assms] by simp
then show ?thesis by simp
qed
text ‹The enumeration lists each call edge once: \<^const>‹cfg_calls_list› is duplicate-free
and \<^const>‹call_target_at› discards exactly the components a decoded entry does not
determine. This is what makes the site and target lists below duplicate-free too, so
the generator builds one contribution per site-target pair rather than one per pair of
co-listed callees.›
lemma distinct_map_filter_call_target_at:
assumes "distinct es"
shows "distinct (List.map_filter (call_target_at v) es)"
using assms
proof (induction es)
case Nil
show ?case by simp
next
case (Cons e es)
obtain c ca ce k where e: "e = (c, ca, ce, k)" by (cases e) auto
show ?case
using Cons by (cases ce) (auto simp: e set_map_filter_call_target_at)
qed
lemma distinct_call_target_list [simp]:
assumes "finite (calls g)"
shows "distinct (call_target_list g v)"
unfolding call_target_list_def
by (rule distinct_map_filter_call_target_at) (simp add: cfg_calls_list_def assms)
lemma call_target_list_iff:
assumes "finite (calls g)"
shows "(c, ca, p) ∈ set (call_target_list g v)
⟷ (c, ca, FunctionEntry p, v) ∈ calls g"
by (simp add: set_call_target_list[OF assms])
subsection ‹Return enumeration›
text ‹The return edges, as an image of \<^const>‹call_targets› that keeps the triggering
\<^typ>‹call_action› whole. The return contribution is the caller state and the callee-exit
state assembled by ‹combine⇧♯›. The callee's \<^term>‹FunctionResult p› node comes back
directly --- recovered from the callee entry \<^term>‹FunctionEntry p› --- so no separate
combine relation is needed and no client has to rebuild the result node.
The action is kept whole rather than packaged into the call's \<^typ>‹call_info›, because
the generator must route the callee-entry seed publication and the return-side context
read from the ∗‹same› action, exactly as \<^const>‹entry_call_list› keeps ‹ca› whole on
the entry side. This layer is the only one holding the \<^const>‹calls› relation, so
whatever a consumer needs about a call site is projected here: a combine tree never sees
the CFG, and \<^const>‹wf_cfg› does not force a call site's outgoing call edge to be
unique, so there is no well-defined downstream lookup that could recover it instead.›
definition return_call_actions ::
"cfg ⇒ cfg_node ⇒ (cfg_node × call_action × cfg_node) set" where
"return_call_actions g v =
(λ(c, ca, p). (c, ca, FunctionResult p)) ` call_targets g v"
lemma return_call_actions_iff [simp]:
"(c, ca, r) ∈ return_call_actions g v
⟷ (∃p. r = FunctionResult p ∧ (c, ca, FunctionEntry p, v) ∈ calls g)"
by (auto simp: return_call_actions_def image_iff Bex_def)
lemma finite_return_call_actions [intro]:
assumes "finite (calls g)"
shows "finite (return_call_actions g v)"
using finite_call_targets[OF assms] by (simp add: return_call_actions_def)
definition return_call_action_list ::
"cfg ⇒ cfg_node ⇒ (cfg_node × call_action × cfg_node) list" where
"return_call_action_list g v =
map (λ(c, ca, p). (c, ca, FunctionResult p)) (call_target_list g v)"
lemma set_return_call_action_list [simp]:
assumes "finite (calls g)"
shows "set (return_call_action_list g v) = return_call_actions g v"
unfolding return_call_action_list_def return_call_actions_def
by (simp add: set_call_target_list[OF assms])
subsection ‹Call sites and the resolver adapter›
text ‹
The call site alone, with no callee: what a call-site-owned resolver is given. The list
is duplicate-free, so a site with several statically known callees is listed once; with
‹distinct_static_targets› below, resolving every listed site therefore visits each
site-target pair exactly once.
‹static_targets› below is the resolver that answers from the CFG and ignores the caller's
abstract state --- the behaviour a statically enumerated generator already has. A
resolver reading that state instead is what makes an indirect call expressible, and the
shape of the equation does not change when it does. Two limits are worth stating,
because the resolver type ‹cfg ⇒ cfg_node ⇒ cfg_node ⇒ call_action ⇒ 'd ⇒ pname list›
does not: a resolver may answer with any procedures at all, so a sound instance needs an
explicit coverage assumption relating its answer to the concrete callees reachable at the
site; and the sites themselves come from \<^const>‹call_target_list›, so a call with no
statically enumerated target is not listed and no resolver is consulted for it. What
this supports is therefore state-dependent choice among enumerated candidates, not a
fully late-bound call whose site exists independently of any known target.
›
definition call_site_list ::
"cfg ⇒ cfg_node ⇒ (cfg_node × call_action) list" where
"call_site_list g v = remdups (map (λ(c, ca, p). (c, ca)) (call_target_list g v))"
lemma distinct_call_site_list [simp]:
"distinct (call_site_list g v)"
by (simp add: call_site_list_def)
lemma set_call_site_list [simp]:
assumes "finite (calls g)"
shows "set (call_site_list g v)
= {(c, ca) | c ca. ∃p. (c, ca, FunctionEntry p, v) ∈ calls g}"
unfolding call_site_list_def
by (force simp: set_call_target_list[OF assms] call_targets_def image_iff)
definition static_targets ::
"cfg ⇒ cfg_node ⇒ cfg_node ⇒ call_action ⇒ pname list" where
"static_targets g v cc ca =
map (λ(c, a, p). p)
(filter (λ(c, a, p). c = cc ∧ a = ca) (call_target_list g v))"
lemma static_targets_iff [simp]:
assumes "finite (calls g)"
shows "p ∈ set (static_targets g v cc ca)
⟷ (cc, ca, FunctionEntry p, v) ∈ calls g"
unfolding static_targets_def
by (force simp: call_target_list_iff[OF assms, symmetric] image_iff)
lemma distinct_static_targets [simp]:
assumes "finite (calls g)"
shows "distinct (static_targets g v cc ca)"
proof -
let ?es = "filter (λ(c, a, p). c = cc ∧ a = ca) (call_target_list g v)"
have "inj_on (λ(c, a, p). p) (set ?es)" by (auto simp: inj_on_def)
moreover have "distinct ?es" using distinct_call_target_list[OF assms] by simp
ultimately show ?thesis unfolding static_targets_def by (simp add: distinct_map)
qed
text ‹Exactly when a site has a statically known callee, which is the form a coverage
obligation stated over the enumeration needs.›
lemma static_targets_neq_Nil_iff [simp]:
assumes "finite (calls g)"
shows "static_targets g v cc ca ≠ []
⟷ (∃p. (cc, ca, FunctionEntry p, v) ∈ calls g)"
proof -
have "static_targets g v cc ca ≠ [] ⟷ (∃p. p ∈ set (static_targets g v cc ca))"
by (cases "static_targets g v cc ca") auto
thus ?thesis using static_targets_iff[OF assms] by blast
qed
definition static_resolve ::
"cfg ⇒ cfg_node ⇒ cfg_node ⇒ call_action ⇒ 'd ⇒ pname list" where
"static_resolve g v cc ca d = static_targets g v cc ca"
lemma static_resolve_iff [simp]:
assumes "finite (calls g)"
shows "p ∈ set (static_resolve g v cc ca d)
⟷ (cc, ca, FunctionEntry p, v) ∈ calls g"
unfolding static_resolve_def by (rule static_targets_iff[OF assms])
section ‹Which unknowns a run has to have solved›
text ‹
‹vars_cover g vars› is the one recurring premise every post-solution
soundness theorem here needs: the solved key set ‹vars› contains the CFG
entry, the target of every ‹intra› edge, and both halves of every call ---
the callee's entry and the caller's continuation.
Read it as a statement about a solver run, not about the graph. It
quantifies over every edge ‹g› has, so a graph carrying a call the run never
activated does not satisfy it: in a routed system a callee's entry unknown is
solved only once some caller publishes its seed, and an unreachable call site
publishes nothing. That is why a run establishes it by deciding
‹vars_cover_exec› against the keys it actually solved, on a graph
∗‹pruned› to what it can reach, rather than by an argument about
connectivity.
The four components travel together, so callers state and discharge one
premise instead of four positional ones. Global, not locale-local: every
analysis instance and the executable pipeline cite it under this name.
›
definition vars_cover :: "cfg ⇒ (cfg_node × unit) set ⇒ bool" where
"vars_cover g vars ⟷
(cfg_entry g, ()) ∈ vars
∧ (∀u a v. (u, a, v) ∈ intra g ⟶ (v, ()) ∈ vars)
∧ (∀c dst fs as q k. (c, CallEdge dst fs as, FunctionEntry q, k) ∈ calls g
⟶ (FunctionEntry q, ()) ∈ vars ∧ (k, ()) ∈ vars)"
lemma vars_coverI [intro]:
assumes "(cfg_entry g, ()) ∈ vars"
and "⋀u a v. (u, a, v) ∈ intra g ⟹ (v, ()) ∈ vars"
and "⋀c dst fs as q k. (c, CallEdge dst fs as, FunctionEntry q, k) ∈ calls g
⟹ (FunctionEntry q, ()) ∈ vars"
and "⋀c dst fs as q k. (c, CallEdge dst fs as, FunctionEntry q, k) ∈ calls g
⟹ (k, ()) ∈ vars"
shows "vars_cover g vars"
unfolding vars_cover_def using assms by blast
lemma vars_cover_entryD [dest]: "vars_cover g vars ⟹ (cfg_entry g, ()) ∈ vars"
unfolding vars_cover_def by blast
lemma vars_cover_edgeD [dest]:
"vars_cover g vars ⟹ (u, a, v) ∈ intra g ⟹ (v, ()) ∈ vars"
unfolding vars_cover_def by blast
lemma vars_cover_enterD [dest]:
assumes cover: "vars_cover g vars"
shows "⋀c dst fs as q k. (c, CallEdge dst fs as, FunctionEntry q, k) ∈ calls g
⟹ (FunctionEntry q, ()) ∈ vars"
using cover unfolding vars_cover_def by blast
lemma vars_cover_combineD [dest]:
assumes cover: "vars_cover g vars"
shows "⋀c dst fs as q k. (c, CallEdge dst fs as, FunctionEntry q, k) ∈ calls g
⟹ (k, ()) ∈ vars"
using cover unfolding vars_cover_def by blast
text ‹
Coverage of a finite graph is testable: walk the two edge enumerations and
test membership. The test is sufficient, not equivalent, and deliberately so.
\<^const>‹vars_cover› asks only about calls of the shape
‹(c, CallEdge dst fs as, FunctionEntry q, k)›, while the test asks about
every tuple the enumeration yields, so it is the stronger of the two: it also
covers a ‹calls› entry whose action or target has some other shape. Turning
it into an equivalence would mean assuming that ‹g› has no such entry, which
is a well-formedness fact about the graph and not something a coverage check
should carry.
›
definition vars_cover_exec :: "cfg ⇒ (cfg_node × unit) set ⇒ bool" where
"vars_cover_exec g vars ⟷
(cfg_entry g, ()) ∈ vars
∧ list_all (λ(u, a, v). (v, ()) ∈ vars) (cfg_intra_list g)
∧ list_all (λ(c, ca, ce, k). (ce, ()) ∈ vars ∧ (k, ()) ∈ vars) (cfg_calls_list g)"
subsection ‹Coverage when the unknowns carry a context›
text ‹
\<^const>‹vars_cover› is fixed at the unit context: its unknowns are nodes, so it
can demand that every successor of every edge was solved. A context-sensitive
system cannot demand that. Its unknowns are node-context pairs, and which
contexts a node was solved at is decided by the run, so coverage becomes
∗‹closure›: whenever a pair was solved, the pairs its edges lead to were
solved too. Hence the conditional shape here against the unconditional shape
above.
Where a call leads is not a fact about the graph. A context policy computes
the callee's context from the call site, the caller's context, the callee, and
in general the state published at the call --- which is why ‹succ_ctx› is a
parameter rather than something read off ‹g›. An instance supplies its own
route already applied to its own solved environment.
The return side takes no such argument: a combine resumes the caller's own
activation, so the continuation is covered at the context the call was made
in.
›
definition ctx_vars_cover ::
"cfg ⇒ (cfg_node ⇒ 'c ⇒ call_action ⇒ pname ⇒ 'c) ⇒ 'c
⇒ (cfg_node × 'c) set ⇒ bool" where
"ctx_vars_cover g succ_ctx rc vars ⟷
(cfg_entry g, rc) ∈ vars
∧ (∀u a v ctx. (u, ctx) ∈ vars ⟶ (u, a, v) ∈ intra g ⟶ (v, ctx) ∈ vars)
∧ (∀u ctx dst fs as q k. (u, ctx) ∈ vars
⟶ (u, CallEdge dst fs as, FunctionEntry q, k) ∈ calls g
⟶ (FunctionEntry q, succ_ctx u ctx (CallEdge dst fs as) q) ∈ vars
∧ (k, ctx) ∈ vars)"
lemma ctx_vars_coverI [intro]:
assumes "(cfg_entry g, rc) ∈ vars"
and "⋀u a v ctx. (u, ctx) ∈ vars ⟹ (u, a, v) ∈ intra g ⟹ (v, ctx) ∈ vars"
and "⋀u ctx dst fs as q k. (u, ctx) ∈ vars
⟹ (u, CallEdge dst fs as, FunctionEntry q, k) ∈ calls g
⟹ (FunctionEntry q, succ_ctx u ctx (CallEdge dst fs as) q) ∈ vars"
and "⋀u ctx dst fs as q k. (u, ctx) ∈ vars
⟹ (u, CallEdge dst fs as, FunctionEntry q, k) ∈ calls g ⟹ (k, ctx) ∈ vars"
shows "ctx_vars_cover g succ_ctx rc vars"
unfolding ctx_vars_cover_def using assms by blast
lemma ctx_vars_cover_entryD [dest]:
"ctx_vars_cover g succ_ctx rc vars ⟹ (cfg_entry g, rc) ∈ vars"
unfolding ctx_vars_cover_def by blast
lemma ctx_vars_cover_edgeD [dest]:
"ctx_vars_cover g succ_ctx rc vars ⟹ (u, ctx) ∈ vars ⟹ (u, a, v) ∈ intra g
⟹ (v, ctx) ∈ vars"
unfolding ctx_vars_cover_def by blast
lemma ctx_vars_cover_enterD [dest]:
assumes cover: "ctx_vars_cover g succ_ctx rc vars"
shows "⋀u ctx dst fs as q k. (u, ctx) ∈ vars
⟹ (u, CallEdge dst fs as, FunctionEntry q, k) ∈ calls g
⟹ (FunctionEntry q, succ_ctx u ctx (CallEdge dst fs as) q) ∈ vars"
using cover unfolding ctx_vars_cover_def by blast
lemma ctx_vars_cover_combineD [dest]:
assumes cover: "ctx_vars_cover g succ_ctx rc vars"
shows "⋀u ctx dst fs as q k. (u, ctx) ∈ vars
⟹ (u, CallEdge dst fs as, FunctionEntry q, k) ∈ calls g ⟹ (k, ctx) ∈ vars"
using cover unfolding ctx_vars_cover_def by blast
text ‹
Contextual coverage is testable the way the unit one is, with one difference
that decides the shape: closure quantifies over the pairs the run solved, and
those are not enumerable from the graph. So the test walks the solved keys
against the two edge enumerations rather than walking the graph alone, which
is why it takes them as the set the solver returns and iterates over it.
Sufficient rather than equivalent, and deliberately so. At a call the test
asks for the continuation at every action shape, while \<^const>‹ctx_vars_cover›
asks only about ‹CallEdge›; demanding the callee entry off a non-call action
would need a context the policy never computes, so that one stays guarded.
›
definition ctx_vars_cover_exec ::
"cfg ⇒ (cfg_node ⇒ 'c ⇒ call_action ⇒ pname ⇒ 'c) ⇒ 'c
⇒ (cfg_node × 'c) set ⇒ bool" where
"ctx_vars_cover_exec g succ_ctx rc vs ⟷
(cfg_entry g, rc) ∈ vs
∧ (∀(u, ctx) ∈ vs.
list_all (λ(u', a, v). u' ≠ u ∨ (v, ctx) ∈ vs) (cfg_intra_list g)
∧ list_all (λ(c, ca, ce, k). c ≠ u ∨ ((k, ctx) ∈ vs
∧ (case ce of FunctionEntry q ⇒ (ce, succ_ctx u ctx ca q) ∈ vs
| _ ⇒ True)))
(cfg_calls_list g))"
lemma ctx_vars_cover_of_exec:
assumes finE: "finite (intra g)" and finC: "finite (calls g)"
and cover: "ctx_vars_cover_exec g succ_ctx rc (vs :: (cfg_node × 'c) set)"
shows "ctx_vars_cover g succ_ctx rc vs"
proof (intro ctx_vars_coverI)
show "(cfg_entry g, rc) ∈ vs"
using cover unfolding ctx_vars_cover_exec_def by blast
next
fix u a v ctx
assume "(u, ctx) ∈ vs" and "(u, a, v) ∈ intra g"
then show "(v, ctx) ∈ vs"
using cover unfolding ctx_vars_cover_exec_def
by (fastforce simp: list_all_iff set_cfg_intra_list [OF finE])
next
fix u ctx dst fs as q k
assume "(u, ctx) ∈ vs"
and "(u, CallEdge dst fs as, FunctionEntry q, k) ∈ calls g"
then show "(FunctionEntry q, succ_ctx u ctx (CallEdge dst fs as) q) ∈ vs"
using cover unfolding ctx_vars_cover_exec_def
by (fastforce simp: list_all_iff set_cfg_calls_list [OF finC])
next
fix u ctx dst fs as q k
assume "(u, ctx) ∈ vs"
and "(u, CallEdge dst fs as, FunctionEntry q, k) ∈ calls g"
then show "(k, ctx) ∈ vs"
using cover unfolding ctx_vars_cover_exec_def
by (fastforce simp: list_all_iff set_cfg_calls_list [OF finC])
qed
subsection ‹Coverage along live dependencies›
text ‹
\<^const>‹ctx_vars_cover› demands closure from every solved pair, including pairs
whose solved state is bottom and calls whose entered state is bottom. A
demand-driven solver never solves what such a pair would lead to, so the
unconditional closure fails on dead call sites and on procedures nothing calls,
while soundness only ever follows a dependency from a state that holds a
concrete store. The live form asks for exactly that much.
‹live u ctx› says the state solved at ‹(u, ctx)› may hold a store; an intra
successor is demanded only from a live pair. ‹succ u ctx ca q› is the callee
context a call routes to, or ‹None› when the entered state is bottom and the
call contributes nothing; the callee entry is demanded only when it is
‹Some›. The continuation stays unconditional: a call site is queried only by
its own continuation's call tree, so a solver never solves the one without the
other.
›
definition ctx_vars_cover_live ::
"cfg ⇒ (cfg_node ⇒ 'c ⇒ bool) ⇒ (cfg_node ⇒ 'c ⇒ call_action ⇒ pname ⇒ 'c option)
⇒ 'c ⇒ (cfg_node × 'c) set ⇒ bool" where
"ctx_vars_cover_live g live succ rc vars ⟷
(cfg_entry g, rc) ∈ vars
∧ (∀u a v ctx. (u, ctx) ∈ vars ⟶ live u ctx ⟶ (u, a, v) ∈ intra g
⟶ (v, ctx) ∈ vars)
∧ (∀u ctx dst fs as q k. (u, ctx) ∈ vars
⟶ (u, CallEdge dst fs as, FunctionEntry q, k) ∈ calls g
⟶ (k, ctx) ∈ vars
∧ (∀ctx'. succ u ctx (CallEdge dst fs as) q = Some ctx'
⟶ (FunctionEntry q, ctx') ∈ vars))"
lemma ctx_vars_cover_liveI [intro]:
assumes "(cfg_entry g, rc) ∈ vars"
and "⋀u a v ctx. (u, ctx) ∈ vars ⟹ live u ctx ⟹ (u, a, v) ∈ intra g
⟹ (v, ctx) ∈ vars"
and "⋀u ctx dst fs as q k ctx'. (u, ctx) ∈ vars
⟹ (u, CallEdge dst fs as, FunctionEntry q, k) ∈ calls g
⟹ succ u ctx (CallEdge dst fs as) q = Some ctx'
⟹ (FunctionEntry q, ctx') ∈ vars"
and "⋀u ctx dst fs as q k. (u, ctx) ∈ vars
⟹ (u, CallEdge dst fs as, FunctionEntry q, k) ∈ calls g ⟹ (k, ctx) ∈ vars"
shows "ctx_vars_cover_live g live succ rc vars"
unfolding ctx_vars_cover_live_def using assms by blast
lemma ctx_vars_cover_live_entryD [dest]:
"ctx_vars_cover_live g live succ rc vars ⟹ (cfg_entry g, rc) ∈ vars"
unfolding ctx_vars_cover_live_def by blast
lemma ctx_vars_cover_live_edgeD [dest]:
"ctx_vars_cover_live g live succ rc vars ⟹ (u, ctx) ∈ vars ⟹ live u ctx
⟹ (u, a, v) ∈ intra g ⟹ (v, ctx) ∈ vars"
unfolding ctx_vars_cover_live_def by blast
lemma ctx_vars_cover_live_enterD [dest]:
"ctx_vars_cover_live g live succ rc vars ⟹ (u, ctx) ∈ vars
⟹ (u, CallEdge dst fs as, FunctionEntry q, k) ∈ calls g
⟹ succ u ctx (CallEdge dst fs as) q = Some ctx' ⟹ (FunctionEntry q, ctx') ∈ vars"
unfolding ctx_vars_cover_live_def by blast
lemma ctx_vars_cover_live_combineD [dest]:
"ctx_vars_cover_live g live succ rc vars ⟹ (u, ctx) ∈ vars
⟹ (u, CallEdge dst fs as, FunctionEntry q, k) ∈ calls g ⟹ (k, ctx) ∈ vars"
unfolding ctx_vars_cover_live_def by blast
end