Theory Contextual_Check_Report
theory Contextual_Check_Report
imports Check_Report Solved_Table Analysis_Query
begin
section ‹Contextual check verdicts over a solved result table›
text ‹
\<^const>‹classify_checks› reads exactly one abstract state per checked node.
A context-sensitive result has one state per ‹(node, context)› pair
instead, and some of those pairs represent no concrete execution at all: a
branch that is dead inside one activation, or an activation a node is never
reached under. Classifying such a point against its stored state answers
vacuously: a classifier's soundness obligation only constrains the stores
its state represents, so on a state representing none, either definite
answer satisfies that obligation vacuously --- a verdict read there is
evidence of nothing, and reporting it as \<^const>‹Check_Proved› would
fabricate a proof about executions that do not exist.
‹contextual_verdict› keeps that case outside \<^typ>‹check_result› rather
than folding it into one of its three values. ‹Dead› means ``no concrete
execution is represented here''; it is neither \<^const>‹Check_Unknown›, which
says only that the abstraction decided the condition neither way on a state
representing some stores --- it asserts no reachability, since those stores
may over-approximate a point no execution reaches --- nor
\<^const>‹Check_Proved›.
›
text ‹
‹contextual_verdict› is literally \<^typ>‹check_result lifted›
(\<^theory>‹Voblint_Domain.Reachability_Lift›): ‹Dead› is \<^const>‹Bot›, ‹Decided›
is \<^const>‹Lifted›, and its order/bottom/join are \<^typ>‹'a lifted›'s own
generic instance at
\<^typ>‹check_result› (itself \<^class>‹semilattice_sup›, from
\<^theory>‹Voblint_Framework.Check_Result›) constructor for constructor: ‹Dead ≤ _›
always, a ‹Decided› never sits below ‹Dead›, and two ‹Decided› verdicts
compare and join through \<^typ>‹check_result›'s own flat order. Reusing it
here keeps one lattice construction for "structurally absent vs. present
with a payload" instead of two.
›
type_synonym contextual_verdict = "check_result lifted"
abbreviation Dead :: contextual_verdict where
"Dead ≡ Bot"
abbreviation Decided :: "check_result ⇒ contextual_verdict" where
"Decided r ≡ Lifted r"
subsection ‹Classifying one contextual point›
text ‹
The reachability decision is not remade here: it was already made when the
raw solved local unknown crossed the result boundary, one layer before the
lifted readback relabels it structurally. A witness-bottom
\<^const>‹Lifted› payload is collapsed to \<^const>‹Bot› by
\<^const>‹canonicalize_lift›, so by the time a value is read back --- as
every public result adapter's raw value is --- \<^const>‹Bot› already
denotes no store. A \<^const>‹Lifted› state
may still denote none, since the emptiness test is sound and incomplete, and
classifying it is then vacuous but harmless. ‹classify_point› only refuses to
classify against a state that represents nothing: a covered-but-dead
activation, whose raw stored state a solver run could otherwise leave as
a witness-bottom \<^const>‹Lifted›, reaches ‹classify_point› as
\<^const>‹Bot› rather than fabricating a vacuous
\<^const>‹Check_Proved› off an empty state.
›
fun classify_point ::
"(exp ⇒ 'a ⇒ check_result) ⇒ exp ⇒ 'a lifted ⇒ contextual_verdict"
where
"classify_point classify c Bot = Dead"
| "classify_point classify c (Lifted st) = Decided (classify c st)"
subsection ‹Classifying by query›
text ‹
A check ‹c› is decided by one question, the value of its truth as an
integer, which every analysis in a product answers and whose combined answer
is the meet. Goblint's ‹assert› analysis asks ‹EvalInt (c != 0)› through
‹Queries.eval_bool›; a comparison or logical operator already evaluates to
‹0› or ‹1›, so for those Voblint asks about ‹c› itself and an analysis sees
the comparison rather than its test against zero. An exact ‹1› proves the
check and an exact ‹0› refutes it. ‹QBot› holds at no store, so a sound
combined answer is ‹QBot› only where the state represents no store, and
‹classify_answer› reads it as ‹Dead›. Anything else decides
nothing.
›
fun bool_valued :: "exp ⇒ bool" where
"bool_valued (Not _) = True"
| "bool_valued (And _ _) = True"
| "bool_valued (Or _ _) = True"
| "bool_valued (Less _ _) = True"
| "bool_valued (LessEq _ _) = True"
| "bool_valued (Greater _ _) = True"
| "bool_valued (GreaterEq _ _) = True"
| "bool_valued (Eq _ _) = True"
| "bool_valued (NotEq _ _) = True"
| "bool_valued _ = False"
lemma aval_bool_valued:
"bool_valued e ⟹ ⟦e⟧⇩e s = (if truthy (⟦e⟧⇩e s) then 1 else 0)"
by (cases e) auto
definition check_truth_exp :: "exp ⇒ exp" where
"check_truth_exp c = (if bool_valued c then c else NotEq c (N 0))"
lemma bool_valued_check_truth_exp [simp]: "bool_valued (check_truth_exp c)"
by (simp add: check_truth_exp_def)
lemma aval_check_truth_exp:
"⟦check_truth_exp c⟧⇩e s = (if truthy (⟦c⟧⇩e s) then 1 else 0)"
by (auto simp: check_truth_exp_def dest: aval_bool_valued[of c s])
abbreviation check_query_of :: "exp ⇒ query" where
"check_query_of c ≡ EvalInt (check_truth_exp c)"
fun classify_answer :: "answer ⇒ contextual_verdict" where
"classify_answer QBot = Dead"
| "classify_answer a =
(if answer_const a = Some 1 then Decided Check_Proved
else if answer_const a = Some 0 then Decided Check_Refuted
else Decided Check_Unknown)"
lemma classify_answer_proved:
assumes "classify_answer a = Decided Check_Proved"
and "eval_holds (check_query_of c) a s"
shows "truthy (⟦c⟧⇩e s)"
proof -
have "answer_const a = Some 1"
using assms(1) by (cases a rule: classify_answer.cases)
(auto split: option.splits if_splits simp del: answer_const.simps)
from eval_holds_constD[OF assms(2) this] show ?thesis
by (simp add: aval_check_truth_exp split: if_splits)
qed
lemma classify_answer_refuted:
assumes "classify_answer a = Decided Check_Refuted"
and "eval_holds (check_query_of c) a s"
shows "¬ truthy (⟦c⟧⇩e s)"
proof -
have "answer_const a = Some 0"
using assms(1) by (cases a rule: classify_answer.cases)
(auto split: option.splits if_splits simp del: answer_const.simps)
from eval_holds_constD[OF assms(2) this] show ?thesis
by (simp add: aval_check_truth_exp split: if_splits)
qed
lemma classify_answer_dead:
assumes "classify_answer a = Dead"
shows "¬ eval_holds (EvalInt e) a s"
using assms by (cases a) (auto split: if_splits)
subsection ‹Aggregating the contexts observed at one check›
text ‹
Folding \<^const>‹sup› from ‹Dead› over a finite set of observations. The
fold's unit is the empty-set answer, so a check with no covered context
needs no separate guard. The ‹[code]› equation is the usual
‹comp_fun_idem› transfer to a list fold: over an unordered set the value is
well defined only because \<^const>‹sup› is commutative, associative, and
idempotent.
›
definition aggregate_verdicts :: "contextual_verdict set ⇒ contextual_verdict" where
"aggregate_verdicts vs = Finite_Set.fold sup Dead vs"
lemma aggregate_verdicts_code [code]:
"aggregate_verdicts (set vs) = List.fold sup vs Dead"
proof -
interpret ci: comp_fun_idem "sup :: contextual_verdict ⇒ _"
by (rule comp_fun_idem_sup)
show ?thesis unfolding aggregate_verdicts_def by (rule ci.fold_set_fold)
qed
lemma aggregate_verdicts_empty [simp]: "aggregate_verdicts {} = Dead"
unfolding aggregate_verdicts_def by simp
lemma aggregate_verdicts_insert [simp]:
"finite vs ⟹ aggregate_verdicts (insert v vs) = v ⊔ aggregate_verdicts vs"
proof -
assume "finite vs"
interpret ci: comp_fun_idem "sup :: contextual_verdict ⇒ _"
by (rule comp_fun_idem_sup)
show ?thesis
unfolding aggregate_verdicts_def using ‹finite vs› by simp
qed
lemma sup_contextual_verdict_eq_Dead_iff [simp]:
"(x ⊔ y = Dead) ⟷ x = Dead ∧ y = Dead"
by (cases x; cases y) simp_all
text ‹The finiteness premise is not bookkeeping: \<^const>‹Finite_Set.fold› returns
its identity on an infinite carrier, so an infinite set of observations
aggregates to ‹Dead› whatever its members are, and only the left-to-right
direction would survive without it.›
lemma aggregate_verdicts_eq_Dead_iff:
assumes "finite vs"
shows "aggregate_verdicts vs = Dead ⟷ (∀v ∈ vs. v = Dead)"
using assms by (induction vs rule: finite_induct) simp_all
text ‹The opposite direction, for a report that has only one observation per
check and therefore no dead case to distinguish: every entry is
‹Decided›. Deadness is not asserted either way --- such a report simply does
not carry the channel that would express it.›
definition decided_report ::
"check_report_entry list ⇒ (pp × exp × contextual_verdict) list" where
"decided_report = map (λ(u, cnd, r). (u, cnd, Decided r))"
lemma length_decided_report [simp]: "length (decided_report rs) = length rs"
unfolding decided_report_def by simp
subsection ‹Whole-program contextual check report›
text ‹
The context-sensitive sibling of \<^const>‹classify_checks›: the same
\<^const>‹EA_Check› traversal in the same \<^const>‹cfg_intra_list› order, but
reading a \<^typ>‹('ctx, 'a) solved_table› instead of a single
\<^typ>‹pp ⇒ 's› environment, and retaining one verdict per context covered
at the checked node rather than collapsing them at construction time.
‹classify_checks_ctx_positions› below is the load-bearing consequence: the
node and condition columns coincide with \<^const>‹classify_checks›'s, so a
positional consumer pairing this report with the source's own check list
stays aligned.
The per-check observations are a set, not a list. \<^typ>‹'ctx› carries no
ordering constraint --- \<^const>‹table_contexts› is a set for precisely that
reason, and real context types such as interval vectors have no total
order --- so no canonical list exists to produce. Nothing downstream needs
one: \<^const>‹aggregate_verdicts› is order-independent by construction.
›
definition classify_checks_ctx ::
"cfg ⇒ ('ctx, 'a) solved_table ⇒ (exp ⇒ 'a ⇒ check_result)
⇒ (pp × exp × ('ctx × contextual_verdict) set) list" where
"classify_checks_ctx g r classify =
map (λ(u, a, v). (u, ea_check_cond a,
(λctx. (ctx, classify_point classify (ea_check_cond a) (lookup_table r u ctx)))
` table_contexts r u))
(filter (λ(u, a, v). is_EA_Check a) (cfg_intra_list g))"
definition classify_checks_verdicts ::
"cfg ⇒ ('ctx, 'a) solved_table ⇒ (exp ⇒ 'a ⇒ check_result)
⇒ (pp × exp × contextual_verdict) list" where
"classify_checks_verdicts g r classify =
map (λ(u, c, vs). (u, c, aggregate_verdicts (snd ` vs))) (classify_checks_ctx g r classify)"
lemma classify_checks_verdicts_proj [simp]:
"map (λ(u, c, vs). (u, c, aggregate_verdicts (snd ` vs))) (classify_checks_ctx g r classify)
= classify_checks_verdicts g r classify"
unfolding classify_checks_verdicts_def by (rule refl)
lemma classify_checks_ctx_positions:
"map (λ(u, c, vs). (u, c)) (classify_checks_ctx g r classify)
= map (λ(u, c, res). (u, c)) (classify_checks g env classify')"
unfolding classify_checks_ctx_def classify_checks_def
by (simp add: comp_def case_prod_beta)
text ‹Membership unfolds to an \<^const>‹EA_Check› edge at the entry's own source
node, with that node's whole context fan-out attached --- the same shape
‹classify_checks_mem_iff› gives the context-insensitive report.›
lemma classify_checks_ctx_mem_iff:
assumes "finite (intra g)"
shows "(v, c, vs) ∈ set (classify_checks_ctx g r classify)
⟷ (∃l tgt. (v, EA_Check l c, tgt) ∈ intra g)
∧ vs = (λctx. (ctx, classify_point classify c (lookup_table r v ctx))) ` table_contexts r v"
unfolding classify_checks_ctx_def set_map set_filter
using set_cfg_intra_list[OF assms]
by (auto simp: image_iff split: edge_action.splits)
(metis edge_action.collapse surjective_pairing)
subsection ‹Contextual proved/refuted soundness›
text ‹
The ‹Decided Check_Proved›/‹Decided Check_Refuted› analogues of
‹classify_checks_proved_sound›/‹classify_checks_refuted_sound›: an
aggregated verdict of \<^term>‹Decided Check_Proved› forces every observation
folded into it to be \<^const>‹Dead› or \<^term>‹Decided Check_Proved› --- never
a disagreeing \<^term>‹Decided Check_Refuted› or \<^term>‹Decided Check_Unknown›,
since either would collapse the join to \<^term>‹Decided Check_Unknown›
instead. ‹Dead› observations contribute nothing and are filtered out at the
call site, not here.
›
text ‹
The order-theoretic route: every member of a finite set sits below the
set's own fold-join (‹aggregate_verdicts_member_le›), a decided,
non-‹Check_Unknown› aggregate already forces the set finite
(‹aggregate_verdicts_decided_finite›, since an infinite carrier folds to the
identity ‹Dead›), and sitting below such a verdict has only two shapes
(‹contextual_verdict_le_Decided_iff›). Composing the three gives
‹aggregate_verdicts_decided_dest› directly, with no case-bash over the fold
step: disagreement, from either side, could only have collapsed the join to
\<^term>‹Decided Check_Unknown› instead.
›
lemma aggregate_verdicts_member_le:
assumes "finite vs" and "v ∈ vs"
shows "v ≤ aggregate_verdicts vs"
using assms
by (induction vs rule: finite_induct) (auto intro: order_trans sup_ge2)
lemma contextual_verdict_le_Decided_iff [simp]:
assumes "r ≠ Check_Unknown"
shows "v ≤ Decided r ⟷ v = Dead ∨ v = Decided r"
using assms by (cases v; cases r) (simp_all add: less_eq_check_result_def)
lemma aggregate_verdicts_decided_finite:
"aggregate_verdicts vs = Decided r ⟹ finite vs"
by (rule ccontr) (simp add: aggregate_verdicts_def)
lemma aggregate_verdicts_decided_dest:
assumes agg: "aggregate_verdicts vs = Decided r" and known: "r ≠ Check_Unknown"
shows "∀v ∈ vs. v = Dead ∨ v = Decided r"
proof
fix v
assume "v ∈ vs"
have "v ≤ aggregate_verdicts vs"
using aggregate_verdicts_member_le[OF aggregate_verdicts_decided_finite[OF agg] ‹v ∈ vs›] .
with agg known show "v = Dead ∨ v = Decided r" by simp
qed
subsection ‹One condition at one point, over its contexts›
text ‹
The verdict a table gives a condition at a point: the classifier's verdict at
every context the point was solved at, aggregated. A check and an arithmetic
obligation are both read this way.
›
definition point_verdict ::
"('ctx, 'a) solved_table ⇒ (exp ⇒ 'a ⇒ check_result) ⇒ pp ⇒ exp
⇒ contextual_verdict" where
"point_verdict r classify v e =
aggregate_verdicts
((λctx. classify_point classify e (lookup_table r v ctx)) ` table_contexts r v)"
text ‹
A decided, non-‹Check_Unknown› point verdict is the classifier's own reading at
every \<^const>‹Lifted› context of the point.
›
lemma point_verdict_decided:
assumes agg: "point_verdict r classify v e = Decided res" and known: "res ≠ Check_Unknown"
and look: "lookup_table r v ctx = Lifted st"
shows "classify e st = res"
proof -
have "classify_point classify e (lookup_table r v ctx)
∈ (λc. classify_point classify e (lookup_table r v c)) ` table_contexts r v"
using lookup_table_LiftedD[OF look] by blast
with aggregate_verdicts_decided_dest[OF agg[unfolded point_verdict_def] known] look
show ?thesis by auto
qed
text ‹A point with a \<^const>‹Lifted› entry among finitely many contexts is not
‹Dead›.›
lemma point_verdict_not_dead:
assumes fin: "finite (table_contexts r v)" and look: "lookup_table r v ctx = Lifted st"
shows "point_verdict r classify v e ≠ Dead"
proof
assume "point_verdict r classify v e = Dead"
with fin have "∀x ∈ (λc. classify_point classify e (lookup_table r v c)) ` table_contexts r v.
x = Dead"
unfolding point_verdict_def by (simp add: aggregate_verdicts_eq_Dead_iff)
then have "classify_point classify e (lookup_table r v ctx) = Dead"
using lookup_table_LiftedD[OF look] by blast
with look show False by simp
qed
text ‹
The context-indexed report analogue of ‹classify_checks_proved_sound›/
‹classify_checks_refuted_sound›, stated once for any decided,
non-‹Check_Unknown› aggregate: it forces the classifier's own reading at
every \<^const>‹Lifted› context to agree. The image set being folded is
shown finite from the aggregate equation itself (‹Finite_Set.fold_infinite›:
an infinite carrier folds to the identity ‹Dead›, which a decided,
non-‹Check_Unknown› result already rules out), so no separate finiteness
assumption on the covered contexts is needed.
›
lemma classify_checks_verdicts_mem_iff:
assumes "finite (intra g)"
shows "(v, c, vr) ∈ set (classify_checks_verdicts g ar classify)
⟷ (∃l tgt. (v, EA_Check l c, tgt) ∈ intra g)
∧ vr = aggregate_verdicts
((λctx. classify_point classify c (lookup_table ar v ctx)) ` table_contexts ar v)"
proof -
have img: "snd ` (λctx. (ctx, classify_point classify c (lookup_table ar v ctx)))
` table_contexts ar v
= (λctx. classify_point classify c (lookup_table ar v ctx)) ` table_contexts ar v"
by (simp add: image_comp comp_def)
have "(v, c, vr) ∈ set (classify_checks_verdicts g ar classify)
⟷ (∃vs. (v, c, vs) ∈ set (classify_checks_ctx g ar classify)
∧ vr = aggregate_verdicts (snd ` vs))"
unfolding classify_checks_verdicts_def set_map by (force simp: image_iff)
also have "... ⟷ (∃l tgt. (v, EA_Check l c, tgt) ∈ intra g)
∧ vr = aggregate_verdicts
((λctx. classify_point classify c (lookup_table ar v ctx)) ` table_contexts ar v)"
unfolding classify_checks_ctx_mem_iff[OF assms] using img by auto
finally show ?thesis .
qed
theorem classify_checks_ctx_decided_sound:
assumes fin: "finite (intra g)"
and mem: "(v, c, Decided r) ∈ set (classify_checks_verdicts g ar classify)"
and reach: "lookup_table ar v ctx = Lifted st"
and known: "r ≠ Check_Unknown"
shows "classify c st = r"
proof -
have "point_verdict ar classify v c = Decided r"
using classify_checks_verdicts_mem_iff[OF fin, of v c "Decided r" ar classify] mem
by (simp add: point_verdict_def)
then show ?thesis by (rule point_verdict_decided[OF _ known reach])
qed
end