Theory Abstract_Domain
theory Abstract_Domain
imports "TD.Warrowing"
begin
unbundle lattice_syntax
section ‹Abstract value domains›
text ‹
An executable domain supplies the required lattice operations, the widening
and narrowing the solver applies (the vendored ‹warrowing› class), and an
exact emptiness test. Sound domains add a concretization into integers.
Abstract states and control-flow reachability are separate modules.
›
subsection ‹Exact and sound emptiness›
text ‹
An emptiness test is ∗‹exact› for a concretization when it holds of exactly the
values that describe nothing, and ∗‹sound› when every value it holds of describes
nothing, with no converse. Each layer states which of the two its test is: a single
domain's and a single state's test are exact, the combined state's only sound,
because two analyses can each describe some value while their intersection is
empty.
›
definition sound_emptiness :: "('s ⇒ bool) ⇒ ('s ⇒ 'c set) ⇒ bool" where
"sound_emptiness e γ' ⟷ (∀x. e x ⟶ γ' x = {})"
definition exact_emptiness :: "('s ⇒ bool) ⇒ ('s ⇒ 'c set) ⇒ bool" where
"exact_emptiness e γ' ⟷ (∀x. e x ⟷ γ' x = {})"
lemma sound_emptinessI [intro]: "(⋀x. e x ⟹ γ' x = {}) ⟹ sound_emptiness e γ'"
unfolding sound_emptiness_def by blast
lemma sound_emptinessD [dest]: "sound_emptiness e γ' ⟹ e x ⟹ γ' x = {}"
unfolding sound_emptiness_def by blast
lemma exact_emptinessI [intro]: "(⋀x. e x ⟷ γ' x = {}) ⟹ exact_emptiness e γ'"
unfolding exact_emptiness_def by blast
lemma exact_emptinessD: "exact_emptiness e γ' ⟹ e x ⟷ γ' x = {}"
unfolding exact_emptiness_def by blast
lemma exact_emptiness_sound: "exact_emptiness e γ' ⟹ sound_emptiness e γ'"
unfolding exact_emptiness_def sound_emptiness_def by blast
subsection ‹Executable and sound domains›
text ‹
The vendored ‹widening› and ‹narrowing› classes constrain their type
parameter (‹'a::order›) instead of naming ‹order› as a superclass, so a class
extending them starts from ‹order› and would reject ‹⊥› and ‹⊤› in its
axioms. The lattice classes therefore enter the same way, through the sort of
the first parameter, and ‹numeric_domain› repeats that sort.
›
class executable_domain = warrowing +
fixes is_empty :: "'a::{bounded_semilattice_sup_bot, order_top} ⇒ bool"
fixes to_string :: "'a ⇒ String.literal"
text ‹
A numeric domain adds a concretization ‹gamma› to an executable domain:
bottom denotes no integer, top every integer, the order is sound for
‹gamma›, and ‹is_empty› decides emptiness exactly.
›
class numeric_domain = executable_domain +
fixes gamma :: "'a::{bounded_semilattice_sup_bot, order_top} ⇒ int set" ("γ")
assumes gamma_bot[simp]: "γ ⊥ = {}"
assumes gamma_top[simp]: "γ ⊤ = UNIV"
assumes gamma_mono: "a ≤ b ⟹ γ a ⊆ γ b"
assumes is_empty_correct: "is_empty a ⟷ γ a = {}"
lemma exact_emptiness_is_empty: "exact_emptiness is_empty (γ :: 'a::numeric_domain ⇒ int set)"
by (rule exact_emptinessI) (rule is_empty_correct)
text ‹
‹executable_domain› carries exactly the executable per-element operations a
concrete domain's runtime representation needs: the lattice structure, the
solver's ‹widen› and ‹narrow›, ‹is_empty› (a finite decision procedure on every real instance
-- Interval's bound comparison, Sign's constructor match, ...), and
‹to_string› for reporting a solved value back to a caller. ‹numeric_domain›
extends it with ‹gamma›, which is not executable in general (an infinite
\<^typ>‹int set›) and exists purely to state and prove soundness. Splitting the
class this way keeps ‹gamma› out of the type-class dictionary that code
generation must materialize for any constant that only needs ‹is_empty›
(the finite witness-bottom tests over a ‹default_st›, in
particular): requesting ‹'a::executable_domain› there never drags \<^const>‹gamma›'s
code equation into the dependency closure, even though every
\<^class>‹numeric_domain› instance is automatically a \<^class>‹executable_domain›
instance too.
›
text ‹
‹is_empty› is a semantic classifier, not a structural equality
test against ‹bot›: it mirrors Goblint's own ‹Lattice.Bot›
signature (@{url "https://github.com/goblint/analyzer/blob/master/src/domain/lattice.ml"}):
‹val is_bot: t -> bool› is a per-domain operation there too, not a
generic derived test. Goblint's own default implementation (‹IntDomain0.Std›
in
@{url "https://github.com/goblint/analyzer/blob/master/src/cdomain/value/cdomains/intDomain0.ml"})
in fact defines it as structural equality against one canonical ‹bot_of›
value, since most of its domains keep a single bottom representation
(Interval's ‹bot () = None›, normalized on every operation). Voblint
cannot take that shortcut: some Voblint domains admit representations with
more than one empty-denoting value that are never normalized away
(Interval's inverted bound pairs, e.g. \<^term>‹Ivl (Fin 5) (Fin (-1))›, none
of them favored over ‹bot› itself), so ‹a = bot› would silently miss some
of them. Fixing ‹is_empty› as its own class operation, correct against
\<^const>‹gamma› rather than against \<^const>‹bot›, makes every
\<^class>‹numeric_domain› instance responsible for its own exact emptiness
test, the same obligation every domain already carries for
\<^const>‹gamma› itself. The lattice constant ‹bot› stays the canonical
representative; ‹is_empty› answers a different question (what a
value denotes), and a proof that genuinely needs the canonical element
still writes ‹a = bot› directly.
›
subsection ‹Concretization bounds›
lemma gamma_sup_ub1[intro]: "γ a ⊆ γ (a ⊔ b)" for a b :: "'a::numeric_domain"
by (rule gamma_mono[OF sup_ge1])
lemma gamma_sup_ub2[intro]: "γ b ⊆ γ (a ⊔ b)" for a b :: "'a::numeric_domain"
by (rule gamma_mono[OF sup_ge2])
text ‹
Emptiness is downward closed under the abstract order: it follows from
‹gamma_mono› alone, with no per-domain fact needed. This is what lets the
generic transfer dispatcher's short-circuit stay monotone: a smaller input
can only be witness-empty ∗‹more› often than a larger one, never less.
›
lemma is_empty_antimono:
"a ≤ b ⟹ is_empty b ⟹ is_empty a" for a b :: "'a::numeric_domain"
using gamma_mono unfolding is_empty_correct by blast
subsection ‹Domains with widening›
text ‹
‹warrow› is deliberately not join-like: it takes the narrowing branch
whenever ‹b ≤ a›, and narrowing moves the result down toward ‹b›, not up
past ‹a›, so no ‹a ≤ a ∇Δ b› counterpart to ‹warrowing_properties› holds
in general. What does hold unconditionally in that regime is the matching
upper bound on the ‹a› side, ‹warrow_le_when_le›, together with
‹warrowing_properties›' ‹b ≤ a ∇Δ b› that gives the full sandwich
‹b ≤ a ∇Δ b ≤ a›. ‹warrow_idem› is the fixpoint case of that sandwich.
›
subsection ‹Printing values›
text ‹
How a solved value is shown, following Goblint's notation (‹1+3ℤ›, ‹⊤›,
‹⊥›). Nothing is proved about it: it is not part of any soundness claim.
A \<^typ>‹String.literal› holds ASCII only, so a mathematical symbol is written
as its Isabelle symbol name without the backslash --- ‹<int>› for ‹ℤ› --- and
the CLI decodes these tokens into Unicode before rendering. No variable name,
numeral or bracket a printer emits contains ‹<›, so the tokens are unambiguous.
›
abbreviation (input) sym_bottom :: String.literal where "sym_bottom ≡ STR ''<bottom>''"
abbreviation (input) sym_top :: String.literal where "sym_top ≡ STR ''<top>''"
abbreviation (input) sym_int :: String.literal where "sym_int ≡ STR ''<int>''"
abbreviation (input) sym_infinity :: String.literal where "sym_infinity ≡ STR ''<infinity>''"
abbreviation (input) sym_le :: String.literal where "sym_le ≡ STR ''<le>''"
abbreviation (input) sym_ge :: String.literal where "sym_ge ≡ STR ''<ge>''"
abbreviation (input) sym_and :: String.literal where "sym_and ≡ STR ''<and>''"
fun string_of_nat :: "nat ⇒ String.literal" where
"string_of_nat n =
(if n < 10 then String.implode [char_of (n + 48)]
else string_of_nat (n div 10) + String.implode [char_of (n mod 10 + 48)])"
definition string_of_int :: "int ⇒ String.literal" where
"string_of_int i =
(if i < 0 then STR ''-'' + string_of_nat (nat (- i))
else string_of_nat (nat i))"
end